From 6e9bb4183ca0d7dbab6841f85a25961388c10c81 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 3 May 2024 03:08:32 -0700 Subject: [PATCH 001/305] hyprland/focus_grab: add HyprlandFocusGrab --- CMakeLists.txt | 2 + src/core/proxywindow.cpp | 1 + src/core/proxywindow.hpp | 1 + src/wayland/CMakeLists.txt | 4 + src/wayland/hyprland/CMakeLists.txt | 11 ++ .../hyprland/focus_grab/CMakeLists.txt | 19 +++ src/wayland/hyprland/focus_grab/grab.cpp | 78 +++++++++++ src/wayland/hyprland/focus_grab/grab.hpp | 46 ++++++ .../focus_grab/hyprland-focus-grab-v1.xml | 128 +++++++++++++++++ src/wayland/hyprland/focus_grab/init.cpp | 20 +++ src/wayland/hyprland/focus_grab/manager.cpp | 27 ++++ src/wayland/hyprland/focus_grab/manager.hpp | 22 +++ src/wayland/hyprland/focus_grab/qml.cpp | 132 ++++++++++++++++++ src/wayland/hyprland/focus_grab/qml.hpp | 111 +++++++++++++++ src/wayland/hyprland/module.md | 6 + 15 files changed, 608 insertions(+) create mode 100644 src/wayland/hyprland/CMakeLists.txt create mode 100644 src/wayland/hyprland/focus_grab/CMakeLists.txt create mode 100644 src/wayland/hyprland/focus_grab/grab.cpp create mode 100644 src/wayland/hyprland/focus_grab/grab.hpp create mode 100644 src/wayland/hyprland/focus_grab/hyprland-focus-grab-v1.xml create mode 100644 src/wayland/hyprland/focus_grab/init.cpp create mode 100644 src/wayland/hyprland/focus_grab/manager.cpp create mode 100644 src/wayland/hyprland/focus_grab/manager.hpp create mode 100644 src/wayland/hyprland/focus_grab/qml.cpp create mode 100644 src/wayland/hyprland/focus_grab/qml.hpp create mode 100644 src/wayland/hyprland/module.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cb376d2..e5f2042f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ option(SOCKETS "Enable unix socket support" ON) option(WAYLAND "Enable wayland support" ON) option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON) option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON) +option(HYPRLAND "Support hyprland specific features" ON) option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) message(STATUS "Quickshell configuration") @@ -27,6 +28,7 @@ if (WAYLAND) endif () message(STATUS " Services") message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") +message(STATUS " Hyprland: ${HYPRLAND}") if (NOT DEFINED GIT_REVISION) execute_process( diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index 17473dce..e2a80a54 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -88,6 +88,7 @@ void ProxyWindowBase::createWindow() { } void ProxyWindowBase::deleteWindow() { + if (this->window != nullptr) emit this->windowDestroyed(); if (auto* window = this->disownWindow()) { if (auto* generation = EngineGeneration::findObjectGeneration(this)) { generation->deregisterIncubationController(window->incubationController()); diff --git a/src/core/proxywindow.hpp b/src/core/proxywindow.hpp index 14ee09da..40f14c4a 100644 --- a/src/core/proxywindow.hpp +++ b/src/core/proxywindow.hpp @@ -102,6 +102,7 @@ public: signals: void windowConnected(); + void windowDestroyed(); void visibleChanged(); void backerVisibilityChanged(); void xChanged(); diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index e56c0430..48140a91 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -68,6 +68,10 @@ if (WAYLAND_SESSION_LOCK) add_subdirectory(session_lock) endif() +if (HYPRLAND) + add_subdirectory(hyprland) +endif() + target_link_libraries(quickshell-wayland PRIVATE ${QT_DEPS}) target_link_libraries(quickshell-wayland-init PRIVATE ${QT_DEPS}) diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt new file mode 100644 index 00000000..4bc0eeaa --- /dev/null +++ b/src/wayland/hyprland/CMakeLists.txt @@ -0,0 +1,11 @@ +qt_add_library(quickshell-hyprland STATIC) +qt_add_qml_module(quickshell-hyprland URI Quickshell.Hyprland VERSION 0.1) + +add_subdirectory(focus_grab) + +target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS}) + +qs_pch(quickshell-hyprland) +qs_pch(quickshell-hyprlandplugin) + +target_link_libraries(quickshell PRIVATE quickshell-hyprlandplugin) diff --git a/src/wayland/hyprland/focus_grab/CMakeLists.txt b/src/wayland/hyprland/focus_grab/CMakeLists.txt new file mode 100644 index 00000000..7e826aa5 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/CMakeLists.txt @@ -0,0 +1,19 @@ +qt_add_library(quickshell-hyprland-focus-grab STATIC + manager.cpp + grab.cpp + qml.cpp +) + +qt_add_qml_module(quickshell-hyprland-focus-grab URI Quickshell.Hyprland._FocusGrab VERSION 0.1) + +add_library(quickshell-hyprland-focus-grab-init OBJECT init.cpp) + +wl_proto(quickshell-hyprland-focus-grab hyprland-focus-grab-v1 "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml") +target_link_libraries(quickshell-hyprland-focus-grab PRIVATE ${QT_DEPS} wayland-client) +target_link_libraries(quickshell-hyprland-focus-grab-init PRIVATE ${QT_DEPS}) + +qs_pch(quickshell-hyprland-focus-grab) +qs_pch(quickshell-hyprland-focus-grabplugin) +qs_pch(quickshell-hyprland-focus-grab-init) + +target_link_libraries(quickshell PRIVATE quickshell-hyprland-focus-grabplugin quickshell-hyprland-focus-grab-init) diff --git a/src/wayland/hyprland/focus_grab/grab.cpp b/src/wayland/hyprland/focus_grab/grab.cpp new file mode 100644 index 00000000..a45cf4ec --- /dev/null +++ b/src/wayland/hyprland/focus_grab/grab.cpp @@ -0,0 +1,78 @@ +#include "grab.hpp" + +#include +#include +#include +#include +#include + +namespace qs::hyprland::focus_grab { + +FocusGrab::FocusGrab(::hyprland_focus_grab_v1* grab) { this->init(grab); } + +FocusGrab::~FocusGrab() { + if (this->isInitialized()) { + this->destroy(); + } +} + +bool FocusGrab::isActive() const { return this->active; } + +void FocusGrab::addWindow(QWindow* window) { + if (auto* waylandWindow = dynamic_cast(window->handle())) { + this->addWaylandWindow(waylandWindow); + } else { + QObject::connect(window, &QWindow::visibleChanged, this, [this, window]() { + if (window->isVisible()) { + if (window->handle() == nullptr) { + window->create(); + } + + auto* waylandWindow = dynamic_cast(window->handle()); + this->addWaylandWindow(waylandWindow); + this->sync(); + } + }); + } +} + +void FocusGrab::removeWindow(QWindow* window) { + QObject::disconnect(window, nullptr, this, nullptr); + + if (auto* waylandWindow = dynamic_cast(window->handle())) { + this->pendingAdditions.removeAll(waylandWindow); + this->remove_surface(waylandWindow->surface()); + this->commitRequired = true; + } +} + +void FocusGrab::addWaylandWindow(QWaylandWindow* window) { + this->add_surface(window->surface()); + this->pendingAdditions.append(window); + this->commitRequired = true; +} + +void FocusGrab::sync() { + if (this->commitRequired) { + this->commit(); + this->commitRequired = false; + + // the protocol will always send cleared() when the grab is deactivated, + // even if it was due to window destruction, so we don't need to track it. + if (!this->pendingAdditions.isEmpty()) { + this->pendingAdditions.clear(); + + if (!this->active) { + this->active = true; + emit this->activated(); + } + } + } +} + +void FocusGrab::hyprland_focus_grab_v1_cleared() { + this->active = false; + emit this->cleared(); +} + +} // namespace qs::hyprland::focus_grab diff --git a/src/wayland/hyprland/focus_grab/grab.hpp b/src/wayland/hyprland/focus_grab/grab.hpp new file mode 100644 index 00000000..2a9384d9 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/grab.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qs::hyprland::focus_grab { +using HyprlandFocusGrab = QtWayland::hyprland_focus_grab_v1; + +class FocusGrab + : public QObject + , public HyprlandFocusGrab { + using QWaylandWindow = QtWaylandClient::QWaylandWindow; + + Q_OBJECT; + +public: + explicit FocusGrab(::hyprland_focus_grab_v1* grab); + ~FocusGrab() override; + Q_DISABLE_COPY_MOVE(FocusGrab); + + [[nodiscard]] bool isActive() const; + void addWindow(QWindow* window); + void removeWindow(QWindow* window); + void sync(); + +signals: + void activated(); + void cleared(); + +private: + void hyprland_focus_grab_v1_cleared() override; + + void addWaylandWindow(QWaylandWindow* window); + + QList pendingAdditions; + bool commitRequired = false; + bool active = false; +}; + +} // namespace qs::hyprland::focus_grab diff --git a/src/wayland/hyprland/focus_grab/hyprland-focus-grab-v1.xml b/src/wayland/hyprland/focus_grab/hyprland-focus-grab-v1.xml new file mode 100644 index 00000000..3b3cd344 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/hyprland-focus-grab-v1.xml @@ -0,0 +1,128 @@ + + + + Copyright © 2024 outfoxxed + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + + This protocol allows clients to limit input focus to a specific set + of surfaces and receive a notification when the limiter is removed as + detailed below. + + + + + This interface allows a client to create surface grab objects. + + + + + Create a surface grab object. + + + + + + + + Destroy the focus grab manager. + This doesn't destroy existing focus grab objects. + + + + + + + This interface restricts input focus to a specified whitelist of + surfaces as long as the focus grab object exists and has at least + one comitted surface. + + Mouse and touch events inside a whitelisted surface will be passed + to the surface normally, while events outside of a whitelisted surface + will clear the grab object. Keyboard events will be passed to the client + and a compositor-picked surface in the whitelist will receive a + wl_keyboard::enter event if a whitelisted surface is not already entered. + + Upon meeting implementation-defined criteria usually meaning a mouse or + touch input outside of any whitelisted surfaces, the compositor will + clear the whitelist, rendering the grab inert and sending the cleared + event. The same will happen if another focus grab or similar action + is started at the compositor's discretion. + + + + + Add a surface to the whitelist. Destroying the surface is treated the + same as an explicit call to remove_surface and duplicate additions are + ignored. + + Does not take effect until commit is called. + + + + + + + + Remove a surface from the whitelist. Destroying the surface is treated + the same as an explicit call to this function. + + If the grab was active and the removed surface was entered by the + keyboard, another surface will be entered on commit. + + Does not take effect until commit is called. + + + + + + + + Commit pending changes to the surface whitelist. + + If the list previously had no entries and now has at least one, the grab + will start. If it previously had entries and now has none, the grab will + become inert. + + + + + + Destroy the grab object and remove the grab if active. + + + + + + Sent when an active grab is cancelled by the compositor, + regardless of cause. + + + + diff --git a/src/wayland/hyprland/focus_grab/init.cpp b/src/wayland/hyprland/focus_grab/init.cpp new file mode 100644 index 00000000..784c7f26 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/init.cpp @@ -0,0 +1,20 @@ +#include + +#include "../../../core/plugin.hpp" + +namespace { + +class HyprlandFocusGrabPlugin: public QuickshellPlugin { + void registerTypes() override { + qmlRegisterModuleImport( + "Quickshell.Hyprland", + QQmlModuleImportModuleAny, + "Quickshell.Hyprland._FocusGrab", + QQmlModuleImportLatest + ); + } +}; + +QS_REGISTER_PLUGIN(HyprlandFocusGrabPlugin); + +} // namespace diff --git a/src/wayland/hyprland/focus_grab/manager.cpp b/src/wayland/hyprland/focus_grab/manager.cpp new file mode 100644 index 00000000..fb93e317 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/manager.cpp @@ -0,0 +1,27 @@ +#include "manager.hpp" + +#include + +#include "grab.hpp" + +namespace qs::hyprland::focus_grab { + +FocusGrabManager::FocusGrabManager(): QWaylandClientExtensionTemplate(1) { + this->initialize(); +} + +bool FocusGrabManager::available() const { return this->isActive(); } + +FocusGrab* FocusGrabManager::createGrab() { return new FocusGrab(this->create_grab()); } + +FocusGrabManager* FocusGrabManager::instance() { + static FocusGrabManager* instance = nullptr; // NOLINT + + if (instance == nullptr) { + instance = new FocusGrabManager(); + } + + return instance; +} + +} // namespace qs::hyprland::focus_grab diff --git a/src/wayland/hyprland/focus_grab/manager.hpp b/src/wayland/hyprland/focus_grab/manager.hpp new file mode 100644 index 00000000..86681482 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/manager.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace qs::hyprland::focus_grab { +using HyprlandFocusGrabManager = QtWayland::hyprland_focus_grab_manager_v1; +class FocusGrab; + +class FocusGrabManager + : public QWaylandClientExtensionTemplate + , public HyprlandFocusGrabManager { +public: + explicit FocusGrabManager(); + + [[nodiscard]] bool available() const; + [[nodiscard]] FocusGrab* createGrab(); + + static FocusGrabManager* instance(); +}; + +} // namespace qs::hyprland::focus_grab diff --git a/src/wayland/hyprland/focus_grab/qml.cpp b/src/wayland/hyprland/focus_grab/qml.cpp new file mode 100644 index 00000000..9f4557aa --- /dev/null +++ b/src/wayland/hyprland/focus_grab/qml.cpp @@ -0,0 +1,132 @@ +#include "qml.hpp" +#include + +#include +#include +#include +#include +#include +#include + +#include "../../../core/proxywindow.hpp" +#include "../../../core/windowinterface.hpp" +#include "grab.hpp" +#include "manager.hpp" + +namespace qs::hyprland { +using focus_grab::FocusGrab; +using focus_grab::FocusGrabManager; + +void HyprlandFocusGrab::componentComplete() { this->tryActivate(); } + +bool HyprlandFocusGrab::isActive() const { return this->grab != nullptr && this->grab->isActive(); } + +void HyprlandFocusGrab::setActive(bool active) { + if (active == this->targetActive) return; + this->targetActive = active; + + if (!active) { + delete this->grab; + this->grab = nullptr; + emit this->activeChanged(); + } else { + this->tryActivate(); + } +} + +QObjectList HyprlandFocusGrab::windows() const { return this->windowObjects; } + +void HyprlandFocusGrab::setWindows(QObjectList windows) { + if (windows == this->windowObjects) return; + this->windowObjects = std::move(windows); + this->syncWindows(); + emit this->windowsChanged(); +} + +void HyprlandFocusGrab::onGrabActivated() { emit this->activeChanged(); } + +void HyprlandFocusGrab::onGrabCleared() { + emit this->cleared(); + this->setActive(false); +} + +void HyprlandFocusGrab::onProxyConnected() { + if (this->grab != nullptr) { + this->grab->addWindow(qobject_cast(this->sender())->backingWindow()); + this->grab->sync(); + } +} + +void HyprlandFocusGrab::tryActivate() { + qDebug() << "tryactivate"; + if (!this->targetActive || this->isActive()) return; + + auto* manager = FocusGrabManager::instance(); + if (!manager->isActive()) { + qWarning() << "The active compositor does not support the hyprland_focus_grab_v1 protocol. " + "HyprlandFocusGrab will not work."; + qWarning() << "** Learn why $XDG_CURRENT_DESKTOP sucks and download a better compositor " + "today at https://hyprland.org"; + return; + } + + this->grab = manager->createGrab(); + this->grab->setParent(this); + QObject::connect(this->grab, &FocusGrab::activated, this, &HyprlandFocusGrab::onGrabActivated); + QObject::connect(this->grab, &FocusGrab::cleared, this, &HyprlandFocusGrab::onGrabCleared); + + for (auto* proxy: this->trackedProxies) { + if (proxy->backingWindow() != nullptr) { + this->grab->addWindow(proxy->backingWindow()); + } + } + + this->grab->sync(); +} + +void HyprlandFocusGrab::syncWindows() { + auto newProxy = QList(); + for (auto* windowObject: this->windowObjects) { + auto* proxyWindow = qobject_cast(windowObject); + + if (proxyWindow == nullptr) { + if (auto* iface = qobject_cast(windowObject)) { + proxyWindow = iface->proxyWindow(); + } + } + + if (proxyWindow != nullptr) { + newProxy.push_back(proxyWindow); + } + } + + for (auto* oldWindow: this->trackedProxies) { + if (!newProxy.contains(oldWindow)) { + QObject::disconnect(oldWindow, nullptr, this, nullptr); + + if (this->grab != nullptr && oldWindow->backingWindow() != nullptr) { + this->grab->removeWindow(oldWindow->backingWindow()); + } + } + } + + for (auto* newProxy: newProxy) { + if (!this->trackedProxies.contains(newProxy)) { + QObject::connect( + newProxy, + &ProxyWindowBase::windowConnected, + this, + &HyprlandFocusGrab::onProxyConnected + ); + + if (this->grab != nullptr && newProxy->backingWindow() != nullptr) { + this->grab->addWindow(newProxy->backingWindow()); + } + } + } + + this->trackedProxies = newProxy; + if (this->grab != nullptr) this->grab->sync(); +} + +} // namespace qs::hyprland diff --git a/src/wayland/hyprland/focus_grab/qml.hpp b/src/wayland/hyprland/focus_grab/qml.hpp new file mode 100644 index 00000000..4ba7227d --- /dev/null +++ b/src/wayland/hyprland/focus_grab/qml.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class ProxyWindowBase; + +namespace qs::hyprland { + +namespace focus_grab { +class FocusGrab; +} + +///! Input focus grabber +/// Object for managing input focus grabs via the [hyprland_focus_grab_v1] +/// wayland protocol. +/// +/// When enabled, all of the windows listed in the `windows` property will +/// receive input normally, and will retain keyboard focus even if the mouse +/// is moved off of them. When areas of the screen that are not part of a listed +/// window are clicked or touched, the grab will become inactive and emit the +/// cleared signal. +/// +/// This is useful for implementing dismissal of popup type windows. +/// ```qml +/// import Quickshell +/// import Quickshell.Hyprland +/// import QtQuick.Controls +/// +/// ShellRoot { +/// FloatingWindow { +/// id: window +/// +/// Button { +/// anchors.centerIn: parent +/// text: grab.active ? "Remove exclusive focus" : "Take exclusive focus" +/// onClicked: grab.active = !grab.active +/// } +/// +/// HyprlandFocusGrab { +/// id: grab +/// windows: [ window ] +/// } +/// } +/// } +/// ``` +/// +/// [hyprland_focus_grab_v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml +class HyprlandFocusGrab + : public QObject + , public QQmlParserStatus { + Q_OBJECT; + /// If the focus grab is active. Defaults to false. + /// + /// When set to true, an input grab will be created for the listed windows. + /// + /// This property will change to false once the grab is dismissed. + /// It will not change to true until the grab begins, which requires + /// at least one visible window. + 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) {} + + void classBegin() override {} + void componentComplete() override; + + [[nodiscard]] bool isActive() const; + void setActive(bool active); + + [[nodiscard]] QObjectList windows() const; + void setWindows(QObjectList windows); + +signals: + /// Sent whenever the compositor clears the focus grab. + /// + /// This may be in response to all windows being removed + /// from the list or simultaneously hidden, in addition to + /// a normal clear. + void cleared(); + + void activeChanged(); + void windowsChanged(); + +private slots: + void onGrabActivated(); + void onGrabCleared(); + void onProxyConnected(); + +private: + void tryActivate(); + void syncWindows(); + + bool targetActive = false; + QObjectList windowObjects; + QList trackedProxies; + QList trackedWindows; + + focus_grab::FocusGrab* grab = nullptr; +}; + +}; // namespace qs::hyprland diff --git a/src/wayland/hyprland/module.md b/src/wayland/hyprland/module.md new file mode 100644 index 00000000..f00bce37 --- /dev/null +++ b/src/wayland/hyprland/module.md @@ -0,0 +1,6 @@ +name = "Quickshell.Hyprland" +description = "Hyprland specific Quickshell types" +headers = [ + "focus_grab/qml.hpp" +] +----- From 87a884ca36b235befde094fa49d8af46538abc3c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 3 May 2024 03:08:32 -0700 Subject: [PATCH 002/305] hyprland/focus_grab: add HyprlandFocusGrab --- CMakeLists.txt | 2 + src/core/proxywindow.cpp | 1 + src/core/proxywindow.hpp | 1 + src/wayland/CMakeLists.txt | 4 + src/wayland/hyprland/CMakeLists.txt | 11 ++ .../hyprland/focus_grab/CMakeLists.txt | 19 +++ src/wayland/hyprland/focus_grab/grab.cpp | 78 +++++++++++ src/wayland/hyprland/focus_grab/grab.hpp | 46 ++++++ .../focus_grab/hyprland-focus-grab-v1.xml | 128 +++++++++++++++++ src/wayland/hyprland/focus_grab/init.cpp | 20 +++ src/wayland/hyprland/focus_grab/manager.cpp | 27 ++++ src/wayland/hyprland/focus_grab/manager.hpp | 22 +++ src/wayland/hyprland/focus_grab/qml.cpp | 131 ++++++++++++++++++ src/wayland/hyprland/focus_grab/qml.hpp | 111 +++++++++++++++ src/wayland/hyprland/module.md | 6 + 15 files changed, 607 insertions(+) create mode 100644 src/wayland/hyprland/CMakeLists.txt create mode 100644 src/wayland/hyprland/focus_grab/CMakeLists.txt create mode 100644 src/wayland/hyprland/focus_grab/grab.cpp create mode 100644 src/wayland/hyprland/focus_grab/grab.hpp create mode 100644 src/wayland/hyprland/focus_grab/hyprland-focus-grab-v1.xml create mode 100644 src/wayland/hyprland/focus_grab/init.cpp create mode 100644 src/wayland/hyprland/focus_grab/manager.cpp create mode 100644 src/wayland/hyprland/focus_grab/manager.hpp create mode 100644 src/wayland/hyprland/focus_grab/qml.cpp create mode 100644 src/wayland/hyprland/focus_grab/qml.hpp create mode 100644 src/wayland/hyprland/module.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cb376d2..e5f2042f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ option(SOCKETS "Enable unix socket support" ON) option(WAYLAND "Enable wayland support" ON) option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON) option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON) +option(HYPRLAND "Support hyprland specific features" ON) option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) message(STATUS "Quickshell configuration") @@ -27,6 +28,7 @@ if (WAYLAND) endif () message(STATUS " Services") message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") +message(STATUS " Hyprland: ${HYPRLAND}") if (NOT DEFINED GIT_REVISION) execute_process( diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index 17473dce..e2a80a54 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -88,6 +88,7 @@ void ProxyWindowBase::createWindow() { } void ProxyWindowBase::deleteWindow() { + if (this->window != nullptr) emit this->windowDestroyed(); if (auto* window = this->disownWindow()) { if (auto* generation = EngineGeneration::findObjectGeneration(this)) { generation->deregisterIncubationController(window->incubationController()); diff --git a/src/core/proxywindow.hpp b/src/core/proxywindow.hpp index 14ee09da..40f14c4a 100644 --- a/src/core/proxywindow.hpp +++ b/src/core/proxywindow.hpp @@ -102,6 +102,7 @@ public: signals: void windowConnected(); + void windowDestroyed(); void visibleChanged(); void backerVisibilityChanged(); void xChanged(); diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index e56c0430..48140a91 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -68,6 +68,10 @@ if (WAYLAND_SESSION_LOCK) add_subdirectory(session_lock) endif() +if (HYPRLAND) + add_subdirectory(hyprland) +endif() + target_link_libraries(quickshell-wayland PRIVATE ${QT_DEPS}) target_link_libraries(quickshell-wayland-init PRIVATE ${QT_DEPS}) diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt new file mode 100644 index 00000000..4bc0eeaa --- /dev/null +++ b/src/wayland/hyprland/CMakeLists.txt @@ -0,0 +1,11 @@ +qt_add_library(quickshell-hyprland STATIC) +qt_add_qml_module(quickshell-hyprland URI Quickshell.Hyprland VERSION 0.1) + +add_subdirectory(focus_grab) + +target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS}) + +qs_pch(quickshell-hyprland) +qs_pch(quickshell-hyprlandplugin) + +target_link_libraries(quickshell PRIVATE quickshell-hyprlandplugin) diff --git a/src/wayland/hyprland/focus_grab/CMakeLists.txt b/src/wayland/hyprland/focus_grab/CMakeLists.txt new file mode 100644 index 00000000..7e826aa5 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/CMakeLists.txt @@ -0,0 +1,19 @@ +qt_add_library(quickshell-hyprland-focus-grab STATIC + manager.cpp + grab.cpp + qml.cpp +) + +qt_add_qml_module(quickshell-hyprland-focus-grab URI Quickshell.Hyprland._FocusGrab VERSION 0.1) + +add_library(quickshell-hyprland-focus-grab-init OBJECT init.cpp) + +wl_proto(quickshell-hyprland-focus-grab hyprland-focus-grab-v1 "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml") +target_link_libraries(quickshell-hyprland-focus-grab PRIVATE ${QT_DEPS} wayland-client) +target_link_libraries(quickshell-hyprland-focus-grab-init PRIVATE ${QT_DEPS}) + +qs_pch(quickshell-hyprland-focus-grab) +qs_pch(quickshell-hyprland-focus-grabplugin) +qs_pch(quickshell-hyprland-focus-grab-init) + +target_link_libraries(quickshell PRIVATE quickshell-hyprland-focus-grabplugin quickshell-hyprland-focus-grab-init) diff --git a/src/wayland/hyprland/focus_grab/grab.cpp b/src/wayland/hyprland/focus_grab/grab.cpp new file mode 100644 index 00000000..a45cf4ec --- /dev/null +++ b/src/wayland/hyprland/focus_grab/grab.cpp @@ -0,0 +1,78 @@ +#include "grab.hpp" + +#include +#include +#include +#include +#include + +namespace qs::hyprland::focus_grab { + +FocusGrab::FocusGrab(::hyprland_focus_grab_v1* grab) { this->init(grab); } + +FocusGrab::~FocusGrab() { + if (this->isInitialized()) { + this->destroy(); + } +} + +bool FocusGrab::isActive() const { return this->active; } + +void FocusGrab::addWindow(QWindow* window) { + if (auto* waylandWindow = dynamic_cast(window->handle())) { + this->addWaylandWindow(waylandWindow); + } else { + QObject::connect(window, &QWindow::visibleChanged, this, [this, window]() { + if (window->isVisible()) { + if (window->handle() == nullptr) { + window->create(); + } + + auto* waylandWindow = dynamic_cast(window->handle()); + this->addWaylandWindow(waylandWindow); + this->sync(); + } + }); + } +} + +void FocusGrab::removeWindow(QWindow* window) { + QObject::disconnect(window, nullptr, this, nullptr); + + if (auto* waylandWindow = dynamic_cast(window->handle())) { + this->pendingAdditions.removeAll(waylandWindow); + this->remove_surface(waylandWindow->surface()); + this->commitRequired = true; + } +} + +void FocusGrab::addWaylandWindow(QWaylandWindow* window) { + this->add_surface(window->surface()); + this->pendingAdditions.append(window); + this->commitRequired = true; +} + +void FocusGrab::sync() { + if (this->commitRequired) { + this->commit(); + this->commitRequired = false; + + // the protocol will always send cleared() when the grab is deactivated, + // even if it was due to window destruction, so we don't need to track it. + if (!this->pendingAdditions.isEmpty()) { + this->pendingAdditions.clear(); + + if (!this->active) { + this->active = true; + emit this->activated(); + } + } + } +} + +void FocusGrab::hyprland_focus_grab_v1_cleared() { + this->active = false; + emit this->cleared(); +} + +} // namespace qs::hyprland::focus_grab diff --git a/src/wayland/hyprland/focus_grab/grab.hpp b/src/wayland/hyprland/focus_grab/grab.hpp new file mode 100644 index 00000000..2a9384d9 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/grab.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qs::hyprland::focus_grab { +using HyprlandFocusGrab = QtWayland::hyprland_focus_grab_v1; + +class FocusGrab + : public QObject + , public HyprlandFocusGrab { + using QWaylandWindow = QtWaylandClient::QWaylandWindow; + + Q_OBJECT; + +public: + explicit FocusGrab(::hyprland_focus_grab_v1* grab); + ~FocusGrab() override; + Q_DISABLE_COPY_MOVE(FocusGrab); + + [[nodiscard]] bool isActive() const; + void addWindow(QWindow* window); + void removeWindow(QWindow* window); + void sync(); + +signals: + void activated(); + void cleared(); + +private: + void hyprland_focus_grab_v1_cleared() override; + + void addWaylandWindow(QWaylandWindow* window); + + QList pendingAdditions; + bool commitRequired = false; + bool active = false; +}; + +} // namespace qs::hyprland::focus_grab diff --git a/src/wayland/hyprland/focus_grab/hyprland-focus-grab-v1.xml b/src/wayland/hyprland/focus_grab/hyprland-focus-grab-v1.xml new file mode 100644 index 00000000..3b3cd344 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/hyprland-focus-grab-v1.xml @@ -0,0 +1,128 @@ + + + + Copyright © 2024 outfoxxed + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + + This protocol allows clients to limit input focus to a specific set + of surfaces and receive a notification when the limiter is removed as + detailed below. + + + + + This interface allows a client to create surface grab objects. + + + + + Create a surface grab object. + + + + + + + + Destroy the focus grab manager. + This doesn't destroy existing focus grab objects. + + + + + + + This interface restricts input focus to a specified whitelist of + surfaces as long as the focus grab object exists and has at least + one comitted surface. + + Mouse and touch events inside a whitelisted surface will be passed + to the surface normally, while events outside of a whitelisted surface + will clear the grab object. Keyboard events will be passed to the client + and a compositor-picked surface in the whitelist will receive a + wl_keyboard::enter event if a whitelisted surface is not already entered. + + Upon meeting implementation-defined criteria usually meaning a mouse or + touch input outside of any whitelisted surfaces, the compositor will + clear the whitelist, rendering the grab inert and sending the cleared + event. The same will happen if another focus grab or similar action + is started at the compositor's discretion. + + + + + Add a surface to the whitelist. Destroying the surface is treated the + same as an explicit call to remove_surface and duplicate additions are + ignored. + + Does not take effect until commit is called. + + + + + + + + Remove a surface from the whitelist. Destroying the surface is treated + the same as an explicit call to this function. + + If the grab was active and the removed surface was entered by the + keyboard, another surface will be entered on commit. + + Does not take effect until commit is called. + + + + + + + + Commit pending changes to the surface whitelist. + + If the list previously had no entries and now has at least one, the grab + will start. If it previously had entries and now has none, the grab will + become inert. + + + + + + Destroy the grab object and remove the grab if active. + + + + + + Sent when an active grab is cancelled by the compositor, + regardless of cause. + + + + diff --git a/src/wayland/hyprland/focus_grab/init.cpp b/src/wayland/hyprland/focus_grab/init.cpp new file mode 100644 index 00000000..784c7f26 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/init.cpp @@ -0,0 +1,20 @@ +#include + +#include "../../../core/plugin.hpp" + +namespace { + +class HyprlandFocusGrabPlugin: public QuickshellPlugin { + void registerTypes() override { + qmlRegisterModuleImport( + "Quickshell.Hyprland", + QQmlModuleImportModuleAny, + "Quickshell.Hyprland._FocusGrab", + QQmlModuleImportLatest + ); + } +}; + +QS_REGISTER_PLUGIN(HyprlandFocusGrabPlugin); + +} // namespace diff --git a/src/wayland/hyprland/focus_grab/manager.cpp b/src/wayland/hyprland/focus_grab/manager.cpp new file mode 100644 index 00000000..fb93e317 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/manager.cpp @@ -0,0 +1,27 @@ +#include "manager.hpp" + +#include + +#include "grab.hpp" + +namespace qs::hyprland::focus_grab { + +FocusGrabManager::FocusGrabManager(): QWaylandClientExtensionTemplate(1) { + this->initialize(); +} + +bool FocusGrabManager::available() const { return this->isActive(); } + +FocusGrab* FocusGrabManager::createGrab() { return new FocusGrab(this->create_grab()); } + +FocusGrabManager* FocusGrabManager::instance() { + static FocusGrabManager* instance = nullptr; // NOLINT + + if (instance == nullptr) { + instance = new FocusGrabManager(); + } + + return instance; +} + +} // namespace qs::hyprland::focus_grab diff --git a/src/wayland/hyprland/focus_grab/manager.hpp b/src/wayland/hyprland/focus_grab/manager.hpp new file mode 100644 index 00000000..86681482 --- /dev/null +++ b/src/wayland/hyprland/focus_grab/manager.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace qs::hyprland::focus_grab { +using HyprlandFocusGrabManager = QtWayland::hyprland_focus_grab_manager_v1; +class FocusGrab; + +class FocusGrabManager + : public QWaylandClientExtensionTemplate + , public HyprlandFocusGrabManager { +public: + explicit FocusGrabManager(); + + [[nodiscard]] bool available() const; + [[nodiscard]] FocusGrab* createGrab(); + + static FocusGrabManager* instance(); +}; + +} // namespace qs::hyprland::focus_grab diff --git a/src/wayland/hyprland/focus_grab/qml.cpp b/src/wayland/hyprland/focus_grab/qml.cpp new file mode 100644 index 00000000..ca644b9d --- /dev/null +++ b/src/wayland/hyprland/focus_grab/qml.cpp @@ -0,0 +1,131 @@ +#include "qml.hpp" +#include + +#include +#include +#include +#include +#include +#include + +#include "../../../core/proxywindow.hpp" +#include "../../../core/windowinterface.hpp" +#include "grab.hpp" +#include "manager.hpp" + +namespace qs::hyprland { +using focus_grab::FocusGrab; +using focus_grab::FocusGrabManager; + +void HyprlandFocusGrab::componentComplete() { this->tryActivate(); } + +bool HyprlandFocusGrab::isActive() const { return this->grab != nullptr && this->grab->isActive(); } + +void HyprlandFocusGrab::setActive(bool active) { + if (active == this->targetActive) return; + this->targetActive = active; + + if (!active) { + delete this->grab; + this->grab = nullptr; + emit this->activeChanged(); + } else { + this->tryActivate(); + } +} + +QObjectList HyprlandFocusGrab::windows() const { return this->windowObjects; } + +void HyprlandFocusGrab::setWindows(QObjectList windows) { + if (windows == this->windowObjects) return; + this->windowObjects = std::move(windows); + this->syncWindows(); + emit this->windowsChanged(); +} + +void HyprlandFocusGrab::onGrabActivated() { emit this->activeChanged(); } + +void HyprlandFocusGrab::onGrabCleared() { + emit this->cleared(); + this->setActive(false); +} + +void HyprlandFocusGrab::onProxyConnected() { + if (this->grab != nullptr) { + this->grab->addWindow(qobject_cast(this->sender())->backingWindow()); + this->grab->sync(); + } +} + +void HyprlandFocusGrab::tryActivate() { + if (!this->targetActive || this->isActive()) return; + + auto* manager = FocusGrabManager::instance(); + if (!manager->isActive()) { + qWarning() << "The active compositor does not support the hyprland_focus_grab_v1 protocol. " + "HyprlandFocusGrab will not work."; + qWarning() << "** Learn why $XDG_CURRENT_DESKTOP sucks and download a better compositor " + "today at https://hyprland.org"; + return; + } + + this->grab = manager->createGrab(); + this->grab->setParent(this); + QObject::connect(this->grab, &FocusGrab::activated, this, &HyprlandFocusGrab::onGrabActivated); + QObject::connect(this->grab, &FocusGrab::cleared, this, &HyprlandFocusGrab::onGrabCleared); + + for (auto* proxy: this->trackedProxies) { + if (proxy->backingWindow() != nullptr) { + this->grab->addWindow(proxy->backingWindow()); + } + } + + this->grab->sync(); +} + +void HyprlandFocusGrab::syncWindows() { + auto newProxy = QList(); + for (auto* windowObject: this->windowObjects) { + auto* proxyWindow = qobject_cast(windowObject); + + if (proxyWindow == nullptr) { + if (auto* iface = qobject_cast(windowObject)) { + proxyWindow = iface->proxyWindow(); + } + } + + if (proxyWindow != nullptr) { + newProxy.push_back(proxyWindow); + } + } + + for (auto* oldWindow: this->trackedProxies) { + if (!newProxy.contains(oldWindow)) { + QObject::disconnect(oldWindow, nullptr, this, nullptr); + + if (this->grab != nullptr && oldWindow->backingWindow() != nullptr) { + this->grab->removeWindow(oldWindow->backingWindow()); + } + } + } + + for (auto* newProxy: newProxy) { + if (!this->trackedProxies.contains(newProxy)) { + QObject::connect( + newProxy, + &ProxyWindowBase::windowConnected, + this, + &HyprlandFocusGrab::onProxyConnected + ); + + if (this->grab != nullptr && newProxy->backingWindow() != nullptr) { + this->grab->addWindow(newProxy->backingWindow()); + } + } + } + + this->trackedProxies = newProxy; + if (this->grab != nullptr) this->grab->sync(); +} + +} // namespace qs::hyprland diff --git a/src/wayland/hyprland/focus_grab/qml.hpp b/src/wayland/hyprland/focus_grab/qml.hpp new file mode 100644 index 00000000..4ba7227d --- /dev/null +++ b/src/wayland/hyprland/focus_grab/qml.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class ProxyWindowBase; + +namespace qs::hyprland { + +namespace focus_grab { +class FocusGrab; +} + +///! Input focus grabber +/// Object for managing input focus grabs via the [hyprland_focus_grab_v1] +/// wayland protocol. +/// +/// When enabled, all of the windows listed in the `windows` property will +/// receive input normally, and will retain keyboard focus even if the mouse +/// is moved off of them. When areas of the screen that are not part of a listed +/// window are clicked or touched, the grab will become inactive and emit the +/// cleared signal. +/// +/// This is useful for implementing dismissal of popup type windows. +/// ```qml +/// import Quickshell +/// import Quickshell.Hyprland +/// import QtQuick.Controls +/// +/// ShellRoot { +/// FloatingWindow { +/// id: window +/// +/// Button { +/// anchors.centerIn: parent +/// text: grab.active ? "Remove exclusive focus" : "Take exclusive focus" +/// onClicked: grab.active = !grab.active +/// } +/// +/// HyprlandFocusGrab { +/// id: grab +/// windows: [ window ] +/// } +/// } +/// } +/// ``` +/// +/// [hyprland_focus_grab_v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml +class HyprlandFocusGrab + : public QObject + , public QQmlParserStatus { + Q_OBJECT; + /// If the focus grab is active. Defaults to false. + /// + /// When set to true, an input grab will be created for the listed windows. + /// + /// This property will change to false once the grab is dismissed. + /// It will not change to true until the grab begins, which requires + /// at least one visible window. + 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) {} + + void classBegin() override {} + void componentComplete() override; + + [[nodiscard]] bool isActive() const; + void setActive(bool active); + + [[nodiscard]] QObjectList windows() const; + void setWindows(QObjectList windows); + +signals: + /// Sent whenever the compositor clears the focus grab. + /// + /// This may be in response to all windows being removed + /// from the list or simultaneously hidden, in addition to + /// a normal clear. + void cleared(); + + void activeChanged(); + void windowsChanged(); + +private slots: + void onGrabActivated(); + void onGrabCleared(); + void onProxyConnected(); + +private: + void tryActivate(); + void syncWindows(); + + bool targetActive = false; + QObjectList windowObjects; + QList trackedProxies; + QList trackedWindows; + + focus_grab::FocusGrab* grab = nullptr; +}; + +}; // namespace qs::hyprland diff --git a/src/wayland/hyprland/module.md b/src/wayland/hyprland/module.md new file mode 100644 index 00000000..f00bce37 --- /dev/null +++ b/src/wayland/hyprland/module.md @@ -0,0 +1,6 @@ +name = "Quickshell.Hyprland" +description = "Hyprland specific Quickshell types" +headers = [ + "focus_grab/qml.hpp" +] +----- From bba8cb8a7dcce95905b6dc3c23d1b1c8e56cab2c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 6 May 2024 22:05:46 -0700 Subject: [PATCH 003/305] hyprland/global_shortcuts: add GlobalShortcut --- CMakeLists.txt | 6 + src/services/CMakeLists.txt | 4 +- src/wayland/hyprland/CMakeLists.txt | 8 +- .../hyprland/focus_grab/CMakeLists.txt | 16 ++- .../hyprland/global_shortcuts/CMakeLists.txt | 29 +++++ .../hyprland-global-shortcuts-v1.xml | 112 +++++++++++++++++ .../hyprland/global_shortcuts/init.cpp | 20 +++ .../hyprland/global_shortcuts/manager.cpp | 55 +++++++++ .../hyprland/global_shortcuts/manager.hpp | 34 ++++++ src/wayland/hyprland/global_shortcuts/qml.cpp | 115 ++++++++++++++++++ src/wayland/hyprland/global_shortcuts/qml.hpp | 108 ++++++++++++++++ .../hyprland/global_shortcuts/shortcut.cpp | 33 +++++ .../hyprland/global_shortcuts/shortcut.hpp | 32 +++++ src/wayland/hyprland/module.md | 3 +- 14 files changed, 569 insertions(+), 6 deletions(-) create mode 100644 src/wayland/hyprland/global_shortcuts/CMakeLists.txt create mode 100644 src/wayland/hyprland/global_shortcuts/hyprland-global-shortcuts-v1.xml create mode 100644 src/wayland/hyprland/global_shortcuts/init.cpp create mode 100644 src/wayland/hyprland/global_shortcuts/manager.cpp create mode 100644 src/wayland/hyprland/global_shortcuts/manager.hpp create mode 100644 src/wayland/hyprland/global_shortcuts/qml.cpp create mode 100644 src/wayland/hyprland/global_shortcuts/qml.hpp create mode 100644 src/wayland/hyprland/global_shortcuts/shortcut.cpp create mode 100644 src/wayland/hyprland/global_shortcuts/shortcut.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e5f2042f..7eb81f5e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,8 @@ option(WAYLAND "Enable wayland support" ON) option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON) option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON) option(HYPRLAND "Support hyprland specific features" ON) +option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON) +option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON) option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) message(STATUS "Quickshell configuration") @@ -29,6 +31,10 @@ endif () message(STATUS " Services") message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") message(STATUS " Hyprland: ${HYPRLAND}") +if (HYPRLAND) + message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}") + message(STATUS " Global Shortcuts: ${HYPRLAND_GLOBAL_SHORTCUTS}") +endif() if (NOT DEFINED GIT_REVISION) execute_process( diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt index 909acc00..56d7f669 100644 --- a/src/services/CMakeLists.txt +++ b/src/services/CMakeLists.txt @@ -1 +1,3 @@ -add_subdirectory(status_notifier) +if (SERVICE_STATUS_NOTIFIER) + add_subdirectory(status_notifier) +endif() diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt index 4bc0eeaa..06121a7e 100644 --- a/src/wayland/hyprland/CMakeLists.txt +++ b/src/wayland/hyprland/CMakeLists.txt @@ -1,7 +1,13 @@ qt_add_library(quickshell-hyprland STATIC) qt_add_qml_module(quickshell-hyprland URI Quickshell.Hyprland VERSION 0.1) -add_subdirectory(focus_grab) +if (HYPRLAND_FOCUS_GRAB) + add_subdirectory(focus_grab) +endif() + +if (HYPRLAND_GLOBAL_SHORTCUTS) + add_subdirectory(global_shortcuts) +endif() target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS}) diff --git a/src/wayland/hyprland/focus_grab/CMakeLists.txt b/src/wayland/hyprland/focus_grab/CMakeLists.txt index 7e826aa5..587ae939 100644 --- a/src/wayland/hyprland/focus_grab/CMakeLists.txt +++ b/src/wayland/hyprland/focus_grab/CMakeLists.txt @@ -4,11 +4,18 @@ qt_add_library(quickshell-hyprland-focus-grab STATIC qml.cpp ) -qt_add_qml_module(quickshell-hyprland-focus-grab URI Quickshell.Hyprland._FocusGrab VERSION 0.1) +qt_add_qml_module(quickshell-hyprland-focus-grab + URI Quickshell.Hyprland._FocusGrab + VERSION 0.1 +) add_library(quickshell-hyprland-focus-grab-init OBJECT init.cpp) -wl_proto(quickshell-hyprland-focus-grab hyprland-focus-grab-v1 "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml") +wl_proto(quickshell-hyprland-focus-grab + hyprland-focus-grab-v1 + "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml" +) + target_link_libraries(quickshell-hyprland-focus-grab PRIVATE ${QT_DEPS} wayland-client) target_link_libraries(quickshell-hyprland-focus-grab-init PRIVATE ${QT_DEPS}) @@ -16,4 +23,7 @@ qs_pch(quickshell-hyprland-focus-grab) qs_pch(quickshell-hyprland-focus-grabplugin) qs_pch(quickshell-hyprland-focus-grab-init) -target_link_libraries(quickshell PRIVATE quickshell-hyprland-focus-grabplugin quickshell-hyprland-focus-grab-init) +target_link_libraries(quickshell PRIVATE + quickshell-hyprland-focus-grabplugin + quickshell-hyprland-focus-grab-init +) diff --git a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt new file mode 100644 index 00000000..804c0a3c --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt @@ -0,0 +1,29 @@ +qt_add_library(quickshell-hyprland-global-shortcuts STATIC + qml.cpp + manager.cpp + shortcut.cpp +) + +qt_add_qml_module(quickshell-hyprland-global-shortcuts + URI Quickshell.Hyprland._GlobalShortcuts + VERSION 0.1 +) + +add_library(quickshell-hyprland-global-shortcuts-init OBJECT init.cpp) + +wl_proto(quickshell-hyprland-global-shortcuts + hyprland-global-shortcuts-v1 + "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-global-shortcuts-v1.xml" +) + +target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE ${QT_DEPS} wayland-client) +target_link_libraries(quickshell-hyprland-global-shortcuts-init PRIVATE ${QT_DEPS}) + +qs_pch(quickshell-hyprland-global-shortcuts) +qs_pch(quickshell-hyprland-global-shortcutsplugin) +qs_pch(quickshell-hyprland-global-shortcuts-init) + +target_link_libraries(quickshell PRIVATE + quickshell-hyprland-global-shortcutsplugin + quickshell-hyprland-global-shortcuts-init +) diff --git a/src/wayland/hyprland/global_shortcuts/hyprland-global-shortcuts-v1.xml b/src/wayland/hyprland/global_shortcuts/hyprland-global-shortcuts-v1.xml new file mode 100644 index 00000000..784d887e --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/hyprland-global-shortcuts-v1.xml @@ -0,0 +1,112 @@ + + + + Copyright © 2022 Vaxry + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + + This protocol allows a client to register triggerable actions, + meant to be global shortcuts. + + + + + This object is a manager which offers requests to create global shortcuts. + + + + + Register a new global shortcut. + + A global shortcut is anonymous, meaning the app does not know what key(s) trigger it. + + The shortcut's keybinding shall be dealt with by the compositor. + + In the case of a duplicate app_id + id combination, the already_taken protocol error is raised. + + + + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + + + + + This object represents a single shortcut. + + + + + The keystroke was pressed. + + tv_ values hold the timestamp of the occurrence. + + + + + + + + + The keystroke was released. + + tv_ values hold the timestamp of the occurrence. + + + + + + + + + Destroys the shortcut. Can be sent at any time by the client. + + + + diff --git a/src/wayland/hyprland/global_shortcuts/init.cpp b/src/wayland/hyprland/global_shortcuts/init.cpp new file mode 100644 index 00000000..12fed07f --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/init.cpp @@ -0,0 +1,20 @@ +#include + +#include "../../../core/plugin.hpp" + +namespace { + +class HyprlandFocusGrabPlugin: public QuickshellPlugin { + void registerTypes() override { + qmlRegisterModuleImport( + "Quickshell.Hyprland", + QQmlModuleImportModuleAny, + "Quickshell.Hyprland._GlobalShortcuts", + QQmlModuleImportLatest + ); + } +}; + +QS_REGISTER_PLUGIN(HyprlandFocusGrabPlugin); + +} // namespace diff --git a/src/wayland/hyprland/global_shortcuts/manager.cpp b/src/wayland/hyprland/global_shortcuts/manager.cpp new file mode 100644 index 00000000..ce0f24c4 --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/manager.cpp @@ -0,0 +1,55 @@ +#include "manager.hpp" + +#include +#include + +#include "shortcut.hpp" + +namespace qs::hyprland::global_shortcuts::impl { + +GlobalShortcutManager::GlobalShortcutManager() + : QWaylandClientExtensionTemplate(1) { + this->initialize(); +} + +GlobalShortcut* GlobalShortcutManager::registerShortcut( + const QString& appid, + const QString& name, + const QString& description, + const QString& triggerDescription +) { + auto shortcut = this->shortcuts.value({appid, name}); + + if (shortcut.second != nullptr) { + this->shortcuts.insert({appid, name}, {shortcut.first + 1, shortcut.second}); + return shortcut.second; + } else { + auto* shortcutObj = this->register_shortcut(name, appid, description, triggerDescription); + auto* managedObj = new GlobalShortcut(shortcutObj); + this->shortcuts.insert({appid, name}, {1, managedObj}); + return managedObj; + } +} + +void GlobalShortcutManager::unregisterShortcut(const QString& appid, const QString& name) { + auto shortcut = this->shortcuts.value({appid, name}); + + if (shortcut.first > 1) { + this->shortcuts.insert({appid, name}, {shortcut.first - 1, shortcut.second}); + } else { + delete shortcut.second; + this->shortcuts.remove({appid, name}); + } +} + +GlobalShortcutManager* GlobalShortcutManager::instance() { + static GlobalShortcutManager* instance = nullptr; // NOLINT + + if (instance == nullptr) { + instance = new GlobalShortcutManager(); + } + + return instance; +} + +} // namespace qs::hyprland::global_shortcuts::impl diff --git a/src/wayland/hyprland/global_shortcuts/manager.hpp b/src/wayland/hyprland/global_shortcuts/manager.hpp new file mode 100644 index 00000000..0a165c53 --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/manager.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "shortcut.hpp" + +namespace qs::hyprland::global_shortcuts::impl { + +class GlobalShortcutManager + : public QWaylandClientExtensionTemplate + , public QtWayland::hyprland_global_shortcuts_manager_v1 { +public: + explicit GlobalShortcutManager(); + + GlobalShortcut* registerShortcut( + const QString& appid, + const QString& name, + const QString& description, + const QString& triggerDescription + ); + + void unregisterShortcut(const QString& appid, const QString& name); + + static GlobalShortcutManager* instance(); + +private: + QHash, QPair> shortcuts; +}; + +} // namespace qs::hyprland::global_shortcuts::impl diff --git a/src/wayland/hyprland/global_shortcuts/qml.cpp b/src/wayland/hyprland/global_shortcuts/qml.cpp new file mode 100644 index 00000000..ff957eaf --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/qml.cpp @@ -0,0 +1,115 @@ +#include "qml.hpp" +#include + +#include +#include +#include + +#include "manager.hpp" +#include "shortcut.hpp" + +namespace qs::hyprland::global_shortcuts { +using impl::GlobalShortcutManager; + +GlobalShortcut::~GlobalShortcut() { + auto* manager = GlobalShortcutManager::instance(); + if (manager != nullptr) { + manager->unregisterShortcut(this->mAppid, this->mName); + } +} + +void GlobalShortcut::onPostReload() { + if (this->mName.isEmpty()) { + qWarning() << "Unable to create GlobalShortcut with empty name."; + return; + } + + auto* manager = GlobalShortcutManager::instance(); + if (manager == nullptr) { + qWarning() << "The active compositor does not support hyprland_global_shortcuts_v1."; + qWarning() << "GlobalShortcut will not work."; + return; + } + + this->shortcut = manager->registerShortcut( + this->mAppid, + this->mName, + this->mDescription, + this->mTriggerDescription + ); + + QObject::connect(this->shortcut, &ShortcutImpl::pressed, this, &GlobalShortcut::onPressed); + QObject::connect(this->shortcut, &ShortcutImpl::released, this, &GlobalShortcut::onReleased); +} + +bool GlobalShortcut::isPressed() const { return this->mPressed; } + +QString GlobalShortcut::appid() const { return this->mAppid; } + +void GlobalShortcut::setAppid(QString appid) { + if (this->shortcut != nullptr) { + qWarning() << "GlobalShortcut cannot be modified after creation."; + return; + } + + if (appid == this->mAppid) return; + + this->mAppid = std::move(appid); + emit this->appidChanged(); +} + +QString GlobalShortcut::name() const { return this->mName; } + +void GlobalShortcut::setName(QString name) { + if (this->shortcut != nullptr) { + qWarning() << "GlobalShortcut cannot be modified after creation."; + return; + } + + if (name == this->mName) return; + + this->mName = std::move(name); + emit this->nameChanged(); +} + +QString GlobalShortcut::description() const { return this->mDescription; } + +void GlobalShortcut::setDescription(QString description) { + if (this->shortcut != nullptr) { + qWarning() << "GlobalShortcut cannot be modified after creation."; + return; + } + + if (description == this->mDescription) return; + + this->mDescription = std::move(description); + emit this->descriptionChanged(); +} + +QString GlobalShortcut::triggerDescription() const { return this->mTriggerDescription; } + +void GlobalShortcut::setTriggerDescription(QString triggerDescription) { + if (this->shortcut != nullptr) { + qWarning() << "GlobalShortcut cannot be modified after creation."; + return; + } + + if (triggerDescription == this->mTriggerDescription) return; + + this->mTriggerDescription = std::move(triggerDescription); + emit this->triggerDescriptionChanged(); +} + +void GlobalShortcut::onPressed() { + this->mPressed = true; + emit this->pressed(); + emit this->pressedChanged(); +} + +void GlobalShortcut::onReleased() { + this->mPressed = false; + emit this->released(); + emit this->pressedChanged(); +} + +} // namespace qs::hyprland::global_shortcuts diff --git a/src/wayland/hyprland/global_shortcuts/qml.hpp b/src/wayland/hyprland/global_shortcuts/qml.hpp new file mode 100644 index 00000000..a43d963e --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/qml.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include +#include + +#include "../../../core/reload.hpp" +#include "shortcut.hpp" + +namespace qs::hyprland::global_shortcuts { + +///! Hyprland global shortcut. +/// Global shortcut implemented with [hyprland_global_shortcuts_v1]. +/// +/// You can use this within hyprland as a global shortcut: +/// ``` +/// bind = , , global, : +/// ``` +/// See [the wiki] for details. +/// +/// > [!WARNING] The shortcuts protocol does not allow duplicate appid + name pairs. +/// > Within a single instance of quickshell this is handled internally, and both +/// > users will be notified, but multiple instances of quickshell or XDPH may collide. +/// > +/// > If that happens, whichever client that tries to register the shortcuts last will crash. +/// +/// > [!INFO] This type does *not* use the xdg-desktop-portal global shortcuts protocol, +/// > as it is not fully functional without flatpak and would cause a considerably worse +/// > user experience from other limitations. It will only work with Hyprland. +/// > Note that, as this type bypasses xdg-desktop-portal, XDPH is not required. +/// +/// [hyprland_global_shortcuts_v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml +/// [the wiki]: https://wiki.hyprland.org/Configuring/Binds/#dbus-global-shortcuts +class GlobalShortcut + : public QObject + , public PostReloadHook { + using ShortcutImpl = impl::GlobalShortcut; + + Q_OBJECT; + // clang-format off + /// If the keybind is currently pressed. + Q_PROPERTY(bool pressed READ isPressed NOTIFY pressedChanged); + /// The appid of the shortcut. Defaults to `quickshell`. + /// You cannot change this at runtime. + /// + /// If you have more than one shortcut we recommend subclassing + /// GlobalShortcut to set this. + Q_PROPERTY(QString appid READ appid WRITE setAppid NOTIFY appidChanged); + /// The name of the shortcut. + /// You cannot change this at runtime. + Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged); + /// The description of the shortcut that appears in `hyprctl globalshortcuts`. + /// You cannot change this at runtime. + Q_PROPERTY(QString description READ description WRITE setDescription NOTIFY descriptionChanged); + /// Have not seen this used ever, but included for completeness. Safe to ignore. + Q_PROPERTY(QString triggerDescription READ triggerDescription WRITE setTriggerDescription NOTIFY triggerDescriptionChanged); + // clang-format on + QML_ELEMENT; + +public: + explicit GlobalShortcut(QObject* parent = nullptr): QObject(parent) {} + ~GlobalShortcut() override; + Q_DISABLE_COPY_MOVE(GlobalShortcut); + + void onPostReload() override; + + [[nodiscard]] bool isPressed() const; + + [[nodiscard]] QString appid() const; + void setAppid(QString appid); + + [[nodiscard]] QString name() const; + void setName(QString name); + + [[nodiscard]] QString description() const; + void setDescription(QString description); + + [[nodiscard]] QString triggerDescription() const; + void setTriggerDescription(QString triggerDescription); + +signals: + /// Fired when the keybind is pressed. + void pressed(); + /// Fired when the keybind is released. + void released(); + + void pressedChanged(); + void appidChanged(); + void nameChanged(); + void descriptionChanged(); + void triggerDescriptionChanged(); + +private slots: + void onPressed(); + void onReleased(); + +private: + impl::GlobalShortcut* shortcut = nullptr; + + bool mPressed = false; + QString mAppid = "quickshell"; + QString mName; + QString mDescription; + QString mTriggerDescription; +}; + +} // namespace qs::hyprland::global_shortcuts diff --git a/src/wayland/hyprland/global_shortcuts/shortcut.cpp b/src/wayland/hyprland/global_shortcuts/shortcut.cpp new file mode 100644 index 00000000..2178d775 --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/shortcut.cpp @@ -0,0 +1,33 @@ +#include "shortcut.hpp" + +#include +#include +#include + +namespace qs::hyprland::global_shortcuts::impl { + +GlobalShortcut::GlobalShortcut(::hyprland_global_shortcut_v1* shortcut) { this->init(shortcut); } + +GlobalShortcut::~GlobalShortcut() { + if (this->isInitialized()) { + this->destroy(); + } +} + +void GlobalShortcut::hyprland_global_shortcut_v1_pressed( + quint32 /*unused*/, + quint32 /*unused*/, + quint32 /*unused*/ +) { + emit this->pressed(); +} + +void GlobalShortcut::hyprland_global_shortcut_v1_released( + quint32 /*unused*/, + quint32 /*unused*/, + quint32 /*unused*/ +) { + emit this->released(); +} + +} // namespace qs::hyprland::global_shortcuts::impl diff --git a/src/wayland/hyprland/global_shortcuts/shortcut.hpp b/src/wayland/hyprland/global_shortcuts/shortcut.hpp new file mode 100644 index 00000000..6aa8fd58 --- /dev/null +++ b/src/wayland/hyprland/global_shortcuts/shortcut.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace qs::hyprland::global_shortcuts::impl { + +class GlobalShortcut + : public QObject + , public QtWayland::hyprland_global_shortcut_v1 { + Q_OBJECT; + +public: + explicit GlobalShortcut(::hyprland_global_shortcut_v1* shortcut); + ~GlobalShortcut() override; + Q_DISABLE_COPY_MOVE(GlobalShortcut); + +signals: + void pressed(); + void released(); + +private: + // clang-format off + void hyprland_global_shortcut_v1_pressed(quint32 tvSecHi, quint32 tvSecLo, quint32 tvNsec) override; + void hyprland_global_shortcut_v1_released(quint32 tvSecHi, quint32 tvSecLo, quint32 tvNsec) override; + // clang-format on +}; + +} // namespace qs::hyprland::global_shortcuts::impl diff --git a/src/wayland/hyprland/module.md b/src/wayland/hyprland/module.md index f00bce37..1b3e2fbf 100644 --- a/src/wayland/hyprland/module.md +++ b/src/wayland/hyprland/module.md @@ -1,6 +1,7 @@ name = "Quickshell.Hyprland" description = "Hyprland specific Quickshell types" headers = [ - "focus_grab/qml.hpp" + "focus_grab/qml.hpp", + "global_shortcuts/qml.hpp", ] ----- From 3e80c4a4fdff5cc6fd8913054b8903140c800b92 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 19 May 2024 02:23:11 -0700 Subject: [PATCH 004/305] service/pipewire: add pipewire module --- .clang-tidy | 1 + CMakeLists.txt | 2 + default.nix | 7 +- docs | 2 +- src/services/CMakeLists.txt | 4 + src/services/pipewire/CMakeLists.txt | 24 ++ src/services/pipewire/connection.cpp | 23 ++ src/services/pipewire/connection.hpp | 25 ++ src/services/pipewire/core.cpp | 87 +++++ src/services/pipewire/core.hpp | 59 ++++ src/services/pipewire/link.cpp | 184 ++++++++++ src/services/pipewire/link.hpp | 99 ++++++ src/services/pipewire/metadata.cpp | 146 ++++++++ src/services/pipewire/metadata.hpp | 64 ++++ src/services/pipewire/node.cpp | 384 ++++++++++++++++++++ src/services/pipewire/node.hpp | 174 +++++++++ src/services/pipewire/qml.cpp | 472 +++++++++++++++++++++++++ src/services/pipewire/qml.hpp | 368 +++++++++++++++++++ src/services/pipewire/registry.cpp | 193 ++++++++++ src/services/pipewire/registry.hpp | 160 +++++++++ src/services/status_notifier/module.md | 2 +- 21 files changed, 2476 insertions(+), 4 deletions(-) create mode 100644 src/services/pipewire/CMakeLists.txt create mode 100644 src/services/pipewire/connection.cpp create mode 100644 src/services/pipewire/connection.hpp create mode 100644 src/services/pipewire/core.cpp create mode 100644 src/services/pipewire/core.hpp create mode 100644 src/services/pipewire/link.cpp create mode 100644 src/services/pipewire/link.hpp create mode 100644 src/services/pipewire/metadata.cpp create mode 100644 src/services/pipewire/metadata.hpp create mode 100644 src/services/pipewire/node.cpp create mode 100644 src/services/pipewire/node.hpp create mode 100644 src/services/pipewire/qml.cpp create mode 100644 src/services/pipewire/qml.hpp create mode 100644 src/services/pipewire/registry.cpp create mode 100644 src/services/pipewire/registry.hpp diff --git a/.clang-tidy b/.clang-tidy index ff820f6e..6362e662 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -36,6 +36,7 @@ Checks: > -readability-braces-around-statements, -readability-redundant-access-specifiers, -readability-else-after-return, + -readability-container-data-pointer, tidyfox-*, CheckOptions: performance-for-range-copy.WarnOnAllAutoCopies: true diff --git a/CMakeLists.txt b/CMakeLists.txt index 7eb81f5e..159acd49 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,7 @@ option(HYPRLAND "Support hyprland specific features" ON) option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON) option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON) option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) +option(SERVICE_PIPEWIRE "PipeWire service" ON) message(STATUS "Quickshell configuration") message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}") @@ -30,6 +31,7 @@ if (WAYLAND) endif () message(STATUS " Services") message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") +message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") message(STATUS " Hyprland: ${HYPRLAND}") if (HYPRLAND) message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}") diff --git a/default.nix b/default.nix index 3f2029b5..514c7946 100644 --- a/default.nix +++ b/default.nix @@ -24,6 +24,7 @@ debug ? false, enableWayland ? true, + enablePipewire ? true, nvidiaCompat ? false, svgSupport ? true, # you almost always want this }: buildStdenv.mkDerivation { @@ -46,7 +47,8 @@ qt6.qtdeclarative ] ++ (lib.optionals enableWayland [ qt6.qtwayland wayland ]) - ++ (lib.optionals svgSupport [ qt6.qtsvg ]); + ++ (lib.optionals svgSupport [ qt6.qtsvg ]) + ++ (lib.optionals enablePipewire [ pipewire ]); QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner"; @@ -62,7 +64,8 @@ cmakeFlags = [ "-DGIT_REVISION=${gitRev}" ] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF" - ++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON"; + ++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON" + ++ lib.optional (!enablePipewire) "-DSERVICE_PIPEWIRE=OFF"; buildPhase = "ninjaBuildPhase"; enableParallelBuilding = true; diff --git a/docs b/docs index 149b784a..ff5da84a 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 149b784a5a4c40ada67cb9f6af5a5350678ab6d4 +Subproject commit ff5da84a8b258a9b2caaf978ddb6de23635d8903 diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt index 56d7f669..091a7ec6 100644 --- a/src/services/CMakeLists.txt +++ b/src/services/CMakeLists.txt @@ -1,3 +1,7 @@ if (SERVICE_STATUS_NOTIFIER) add_subdirectory(status_notifier) endif() + +if (SERVICE_PIPEWIRE) + add_subdirectory(pipewire) +endif() diff --git a/src/services/pipewire/CMakeLists.txt b/src/services/pipewire/CMakeLists.txt new file mode 100644 index 00000000..4fccdc0e --- /dev/null +++ b/src/services/pipewire/CMakeLists.txt @@ -0,0 +1,24 @@ +find_package(PkgConfig REQUIRED) +pkg_check_modules(pipewire REQUIRED IMPORTED_TARGET libpipewire-0.3) + +qt_add_library(quickshell-service-pipewire STATIC + qml.cpp + core.cpp + connection.cpp + registry.cpp + node.cpp + metadata.cpp + link.cpp +) + +qt_add_qml_module(quickshell-service-pipewire + URI Quickshell.Services.Pipewire + VERSION 0.1 +) + +target_link_libraries(quickshell-service-pipewire PRIVATE ${QT_DEPS} PkgConfig::pipewire) + +qs_pch(quickshell-service-pipewire) +qs_pch(quickshell-service-pipewireplugin) + +target_link_libraries(quickshell PRIVATE quickshell-service-pipewireplugin) diff --git a/src/services/pipewire/connection.cpp b/src/services/pipewire/connection.cpp new file mode 100644 index 00000000..ac4c5e6a --- /dev/null +++ b/src/services/pipewire/connection.cpp @@ -0,0 +1,23 @@ +#include "connection.hpp" + +#include + +namespace qs::service::pipewire { + +PwConnection::PwConnection(QObject* parent): QObject(parent) { + if (this->core.isValid()) { + this->registry.init(this->core); + } +} + +PwConnection* PwConnection::instance() { + static PwConnection* instance = nullptr; // NOLINT + + if (instance == nullptr) { + instance = new PwConnection(); + } + + return instance; +} + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/connection.hpp b/src/services/pipewire/connection.hpp new file mode 100644 index 00000000..fa270356 --- /dev/null +++ b/src/services/pipewire/connection.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "core.hpp" +#include "metadata.hpp" +#include "registry.hpp" + +namespace qs::service::pipewire { + +class PwConnection: public QObject { + Q_OBJECT; + +public: + explicit PwConnection(QObject* parent = nullptr); + + PwRegistry registry; + PwDefaultsMetadata defaults {&this->registry}; + + static PwConnection* instance(); + +private: + // init/destroy order is important. do not rearrange. + PwCore core; +}; + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/core.cpp b/src/services/pipewire/core.cpp new file mode 100644 index 00000000..4f997155 --- /dev/null +++ b/src/services/pipewire/core.cpp @@ -0,0 +1,87 @@ +#include "core.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qs::service::pipewire { + +Q_LOGGING_CATEGORY(logLoop, "quickshell.service.pipewire.loop", QtWarningMsg); + +PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read) { + qCInfo(logLoop) << "Creating pipewire event loop."; + pw_init(nullptr, nullptr); + + this->loop = pw_loop_new(nullptr); + if (this->loop == nullptr) { + qCCritical(logLoop) << "Failed to create pipewire event loop."; + return; + } + + this->context = pw_context_new(this->loop, nullptr, 0); + if (this->context == nullptr) { + qCCritical(logLoop) << "Failed to create pipewire context."; + return; + } + + 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; + } + + qCInfo(logLoop) << "Linking pipewire event loop."; + // Tie the pw event loop into qt. + auto fd = pw_loop_get_fd(this->loop); + this->notifier.setSocket(fd); + QObject::connect(&this->notifier, &QSocketNotifier::activated, this, &PwCore::poll); + this->notifier.setEnabled(true); +} + +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); + } +} + +bool PwCore::isValid() const { + // others must init first + return this->core != nullptr; +} + +void PwCore::poll() const { + qCDebug(logLoop) << "Pipewire event loop received new events, iterating."; + // Spin pw event loop. + pw_loop_iterate(this->loop, 0); + qCDebug(logLoop) << "Done iterating pipewire event loop."; +} + +SpaHook::SpaHook() { // NOLINT + spa_zero(this->hook); +} + +void SpaHook::remove() { + spa_hook_remove(&this->hook); + spa_zero(this->hook); +} + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/core.hpp b/src/services/pipewire/core.hpp new file mode 100644 index 00000000..ebf5c63e --- /dev/null +++ b/src/services/pipewire/core.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qs::service::pipewire { + +class PwCore: public QObject { + Q_OBJECT; + +public: + explicit PwCore(QObject* parent = nullptr); + ~PwCore() override; + Q_DISABLE_COPY_MOVE(PwCore); + + [[nodiscard]] bool isValid() const; + + pw_loop* loop = nullptr; + pw_context* context = nullptr; + pw_core* core = nullptr; + +private slots: + void poll() const; + +private: + QSocketNotifier notifier; +}; + +template +class PwObject { +public: + explicit PwObject(T* object = nullptr): object(object) {} + ~PwObject() { + pw_proxy_destroy(reinterpret_cast(this->object)); // NOLINT + } + + Q_DISABLE_COPY_MOVE(PwObject); + + T* object; +}; + +class SpaHook { +public: + explicit SpaHook(); + + void remove(); + spa_hook hook; +}; + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/link.cpp b/src/services/pipewire/link.cpp new file mode 100644 index 00000000..8370446b --- /dev/null +++ b/src/services/pipewire/link.cpp @@ -0,0 +1,184 @@ +#include "link.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "registry.hpp" + +namespace qs::service::pipewire { + +Q_LOGGING_CATEGORY(logLink, "quickshell.service.pipewire.link", QtWarningMsg); + +QString PwLinkState::toString(Enum value) { + return QString(pw_link_state_as_string(static_cast(value))); +} + +void PwLink::bindHooks() { + pw_link_add_listener(this->proxy(), &this->listener.hook, &PwLink::EVENTS, this); +} + +void PwLink::unbindHooks() { + this->listener.remove(); + this->setState(PW_LINK_STATE_UNLINKED); +} + +void PwLink::initProps(const spa_dict* props) { + qCDebug(logLink) << "Parsing initial SPA props of link" << this; + + const spa_dict_item* item = nullptr; + spa_dict_for_each(item, props) { + if (strcmp(item->key, "link.output.node") == 0) { + auto str = QString(item->value); + auto ok = false; + auto value = str.toInt(&ok); + if (ok) this->setOutputNode(value); + else { + qCWarning(logLink) << "Could not parse link.output.node for" << this << ":" << item->value; + } + } else if (strcmp(item->key, "link.input.node") == 0) { + auto str = QString(item->value); + auto ok = false; + auto value = str.toInt(&ok); + if (ok) this->setInputNode(value); + else { + qCWarning(logLink) << "Could not parse link.input.node for" << this << ":" << item->value; + } + } + } +} + +const pw_link_events PwLink::EVENTS = { + .version = PW_VERSION_LINK_EVENTS, + .info = &PwLink::onInfo, +}; + +void PwLink::onInfo(void* data, const struct pw_link_info* info) { + auto* self = static_cast(data); + qCDebug(logLink) << "Got link info update for" << self << "with mask" << info->change_mask; + self->setOutputNode(info->output_node_id); + self->setInputNode(info->input_node_id); + + if ((info->change_mask & PW_LINK_CHANGE_MASK_STATE) != 0) { + self->setState(info->state); + } +} + +quint32 PwLink::outputNode() const { return this->mOutputNode; } +quint32 PwLink::inputNode() const { return this->mInputNode; } +PwLinkState::Enum PwLink::state() const { return static_cast(this->mState); } + +void PwLink::setOutputNode(quint32 outputNode) { + if (outputNode == this->mOutputNode) return; + + if (this->mOutputNode != 0) { + qCWarning(logLink) << "Got unexpected output node update for" << this << "to" << outputNode; + } + + this->mOutputNode = outputNode; + qCDebug(logLink) << "Updated output node of" << this; +} + +void PwLink::setInputNode(quint32 inputNode) { + if (inputNode == this->mInputNode) return; + + if (this->mInputNode != 0) { + qCWarning(logLink) << "Got unexpected input node update for" << this << "to" << inputNode; + } + + this->mInputNode = inputNode; + qCDebug(logLink) << "Updated input node of" << this; +} + +void PwLink::setState(pw_link_state state) { + if (state == this->mState) return; + + this->mState = state; + qCDebug(logLink) << "Updated state of" << this; + emit this->stateChanged(); +} + +QDebug operator<<(QDebug debug, const PwLink* link) { + if (link == nullptr) { + debug << "PwLink(0x0)"; + } else { + auto saver = QDebugStateSaver(debug); + debug.nospace() << "PwLink(" << link->outputNode() << " -> " << link->inputNode() << ", " + << static_cast(link) << ", id="; + link->debugId(debug); + debug << ", state=" << link->state() << ')'; + } + + return debug; +} + +PwLinkGroup::PwLinkGroup(PwLink* firstLink, QObject* parent) + : QObject(parent) + , mOutputNode(firstLink->outputNode()) + , mInputNode(firstLink->inputNode()) { + this->tryAddLink(firstLink); +} + +void PwLinkGroup::ref() { + this->refcount++; + + if (this->refcount == 1) { + this->trackedLink = *this->links.begin(); + this->trackedLink->ref(); + QObject::connect(this->trackedLink, &PwLink::stateChanged, this, &PwLinkGroup::stateChanged); + emit this->stateChanged(); + } +} + +void PwLinkGroup::unref() { + if (this->refcount == 0) return; + this->refcount--; + + if (this->refcount == 0) { + this->trackedLink->unref(); + this->trackedLink = nullptr; + emit this->stateChanged(); + } +} + +quint32 PwLinkGroup::outputNode() const { return this->mOutputNode; } + +quint32 PwLinkGroup::inputNode() const { return this->mInputNode; } + +PwLinkState::Enum PwLinkGroup::state() const { + if (this->trackedLink == nullptr) { + return PwLinkState::Unlinked; + } else { + return this->trackedLink->state(); + } +} + +bool PwLinkGroup::tryAddLink(PwLink* link) { + if (link->outputNode() != this->mOutputNode || link->inputNode() != this->mInputNode) + return false; + + this->links.insert(link->id, link); + QObject::connect(link, &PwBindableObject::destroying, this, &PwLinkGroup::onLinkRemoved); + return true; +} + +void PwLinkGroup::onLinkRemoved(QObject* object) { + auto* link = static_cast(object); // NOLINT + this->links.remove(link->id); + + if (this->links.empty()) { + delete this; + } else if (link == this->trackedLink) { + this->trackedLink = *this->links.begin(); + QObject::connect(this->trackedLink, &PwLink::stateChanged, this, &PwLinkGroup::stateChanged); + emit this->stateChanged(); + } +} + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/link.hpp b/src/services/pipewire/link.hpp new file mode 100644 index 00000000..e5ff2ce9 --- /dev/null +++ b/src/services/pipewire/link.hpp @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "registry.hpp" + +namespace qs::service::pipewire { + +class PwLinkState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum { + Error = PW_LINK_STATE_ERROR, + Unlinked = PW_LINK_STATE_UNLINKED, + Init = PW_LINK_STATE_INIT, + Negotiating = PW_LINK_STATE_NEGOTIATING, + Allocating = PW_LINK_STATE_ALLOCATING, + Paused = PW_LINK_STATE_PAUSED, + Active = PW_LINK_STATE_ACTIVE, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(PwLinkState::Enum value); +}; + +constexpr const char TYPE_INTERFACE_Link[] = PW_TYPE_INTERFACE_Link; // NOLINT +class PwLink: public PwBindable { // NOLINT + Q_OBJECT; + +public: + void bindHooks() override; + void unbindHooks() override; + void initProps(const spa_dict* props) override; + + [[nodiscard]] quint32 outputNode() const; + [[nodiscard]] quint32 inputNode() const; + [[nodiscard]] PwLinkState::Enum state() const; + +signals: + void stateChanged(); + +private: + static const pw_link_events EVENTS; + static void onInfo(void* data, const struct pw_link_info* info); + + void setOutputNode(quint32 outputNode); + void setInputNode(quint32 inputNode); + void setState(pw_link_state state); + + SpaHook listener; + + quint32 mOutputNode = 0; + quint32 mInputNode = 0; + pw_link_state mState = PW_LINK_STATE_UNLINKED; +}; + +QDebug operator<<(QDebug debug, const PwLink* link); + +class PwLinkGroup: public QObject { + Q_OBJECT; + +public: + explicit PwLinkGroup(PwLink* firstLink, QObject* parent = nullptr); + + void ref(); + void unref(); + + [[nodiscard]] quint32 outputNode() const; + [[nodiscard]] quint32 inputNode() const; + [[nodiscard]] PwLinkState::Enum state() const; + + QHash links; + + bool tryAddLink(PwLink* link); + +signals: + void stateChanged(); + +private slots: + void onLinkRemoved(QObject* object); + +private: + quint32 mOutputNode = 0; + quint32 mInputNode = 0; + PwLink* trackedLink = nullptr; + quint32 refcount = 0; +}; + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/metadata.cpp b/src/services/pipewire/metadata.cpp new file mode 100644 index 00000000..3a64a38d --- /dev/null +++ b/src/services/pipewire/metadata.cpp @@ -0,0 +1,146 @@ +#include "metadata.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "registry.hpp" + +namespace qs::service::pipewire { + +Q_LOGGING_CATEGORY(logMeta, "quickshell.service.pipewire.metadata", QtWarningMsg); + +void PwMetadata::bindHooks() { + pw_metadata_add_listener(this->proxy(), &this->listener.hook, &PwMetadata::EVENTS, this); +} + +void PwMetadata::unbindHooks() { this->listener.remove(); } + +const pw_metadata_events PwMetadata::EVENTS = { + .version = PW_VERSION_METADATA_EVENTS, + .property = &PwMetadata::onProperty, +}; + +int PwMetadata::onProperty( + void* data, + quint32 subject, + const char* key, + const char* type, + const char* value +) { + auto* self = static_cast(data); + qCDebug(logMeta) << "Received metadata for" << self << "- subject:" << subject + << "key:" << QString(key) << "type:" << QString(type) + << "value:" << QString(value); + + emit self->registry->metadataUpdate(self, subject, key, type, value); + + // ideally we'd dealloc metadata that wasn't picked up but there's no information + // available about if updates can come in later, so I assume they can. + + return 0; // ??? - no docs and no reason for a callback to return an int +} + +PwDefaultsMetadata::PwDefaultsMetadata(PwRegistry* registry) { + QObject::connect( + registry, + &PwRegistry::metadataUpdate, + this, + &PwDefaultsMetadata::onMetadataUpdate + ); +} + +QString PwDefaultsMetadata::defaultSink() const { return this->mDefaultSink; } + +QString PwDefaultsMetadata::defaultSource() const { return this->mDefaultSource; } + +// we don't really care if the metadata objects are destroyed, but try to ref them so we get property updates +void PwDefaultsMetadata::onMetadataUpdate( + PwMetadata* metadata, + quint32 subject, + const char* key, + const char* /*type*/, + const char* value +) { + if (subject != 0) return; + + // non "configured" sinks and sources have lower priority as wireplumber seems to only change + // the "configured" ones. + + bool sink = false; + if (strcmp(key, "default.configured.audio.sink") == 0) { + sink = true; + this->sinkConfigured = true; + } else if ((!this->sinkConfigured && strcmp(key, "default.audio.sink") == 0)) { + sink = true; + } + + if (sink) { + this->defaultSinkHolder.setObject(metadata); + + auto newSink = PwDefaultsMetadata::parseNameSpaJson(value); + qCInfo(logMeta) << "Got default sink" << newSink << "configured:" << this->sinkConfigured; + if (newSink == this->mDefaultSink) return; + + this->mDefaultSink = newSink; + emit this->defaultSinkChanged(); + return; + } + + bool source = false; + if (strcmp(key, "default.configured.audio.source") == 0) { + source = true; + this->sourceConfigured = true; + } else if ((!this->sourceConfigured && strcmp(key, "default.audio.source") == 0)) { + source = true; + } + + if (source) { + this->defaultSourceHolder.setObject(metadata); + + auto newSource = PwDefaultsMetadata::parseNameSpaJson(value); + qCInfo(logMeta) << "Got default source" << newSource << "configured:" << this->sourceConfigured; + if (newSource == this->mDefaultSource) return; + + this->mDefaultSource = newSource; + emit this->defaultSourceChanged(); + return; + } +} + +QString PwDefaultsMetadata::parseNameSpaJson(const char* spaJson) { + auto iter = std::array(); + spa_json_init(&iter[0], spaJson, strlen(spaJson)); + + if (spa_json_enter_object(&iter[0], &iter[1]) < 0) { + qCWarning(logMeta) << "Failed to parse source/sink SPA json - failed to enter object of" + << QString(spaJson); + return ""; + } + + auto buf = std::array(); + while (spa_json_get_string(&iter[1], buf.data(), buf.size()) > 0) { + if (strcmp(buf.data(), "name") != 0) continue; + + if (spa_json_get_string(&iter[1], buf.data(), buf.size()) < 0) { + qCWarning(logMeta + ) << "Failed to parse source/sink SPA json - failed to read value of name property" + << QString(spaJson); + return ""; + } + + return QString(buf.data()); + } + + qCWarning(logMeta) << "Failed to parse source/sink SPA json - failed to find name property of" + << QString(spaJson); + return ""; +} + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/metadata.hpp b/src/services/pipewire/metadata.hpp new file mode 100644 index 00000000..4937a747 --- /dev/null +++ b/src/services/pipewire/metadata.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include + +#include "core.hpp" +#include "registry.hpp" + +namespace qs::service::pipewire { + +constexpr const char TYPE_INTERFACE_Metadata[] = PW_TYPE_INTERFACE_Metadata; // NOLINT +class PwMetadata + : public PwBindable { // NOLINT + Q_OBJECT; + +public: + void bindHooks() override; + void unbindHooks() override; + +private: + static const pw_metadata_events EVENTS; + static int + onProperty(void* data, quint32 subject, const char* key, const char* type, const char* value); + + SpaHook listener; +}; + +class PwDefaultsMetadata: public QObject { + Q_OBJECT; + +public: + explicit PwDefaultsMetadata(PwRegistry* registry); + + [[nodiscard]] QString defaultSource() const; + [[nodiscard]] QString defaultSink() const; + +signals: + void defaultSourceChanged(); + void defaultSinkChanged(); + +private slots: + void onMetadataUpdate( + PwMetadata* metadata, + quint32 subject, + const char* key, + const char* type, + const char* value + ); + +private: + static QString parseNameSpaJson(const char* spaJson); + + PwBindableRef defaultSinkHolder; + PwBindableRef defaultSourceHolder; + + bool sinkConfigured = false; + QString mDefaultSink; + bool sourceConfigured = false; + QString mDefaultSource; +}; + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp new file mode 100644 index 00000000..969a8b71 --- /dev/null +++ b/src/services/pipewire/node.cpp @@ -0,0 +1,384 @@ +#include "node.hpp" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qs::service::pipewire { + +Q_LOGGING_CATEGORY(logNode, "quickshell.service.pipewire.node", QtWarningMsg); + +QString PwAudioChannel::toString(Enum value) { + switch (value) { + case Unknown: return "Unknown"; + case NA: return "N/A"; + case Mono: return "Mono"; + case FrontCenter: return "Front Center"; + case FrontLeft: return "Front Left"; + case FrontRight: return "Front Right"; + case FrontLeftCenter: return "Front Left Center"; + case FrontRightCenter: return "Front Right Center"; + case FrontLeftWide: return "Front Left Wide"; + case FrontRightWide: return "Front Right Wide"; + case FrontCenterHigh: return "Front Center High"; + case FrontLeftHigh: return "Front Left High"; + case FrontRightHigh: return "Front Right High"; + case LowFrequencyEffects: return "Low Frequency Effects"; + case LowFrequencyEffects2: return "Low Frequency Effects 2"; + case LowFrequencyEffectsLeft: return "Low Frequency Effects Left"; + case LowFrequencyEffectsRight: return "Low Frequency Effects Right"; + case SideLeft: return "Side Left"; + case SideRight: return "Side Right"; + case RearCenter: return "Rear Center"; + case RearLeft: return "Rear Left"; + case RearRight: return "Rear Right"; + case RearLeftCenter: return "Rear Left Center"; + case RearRightCenter: return "Rear Right Center"; + case TopCenter: return "Top Center"; + case TopFrontCenter: return "Top Front Center"; + case TopFrontLeft: return "Top Front Left"; + case TopFrontRight: return "Top Front Right"; + case TopFrontLeftCenter: return "Top Front Left Center"; + case TopFrontRightCenter: return "Top Front Right Center"; + case TopSideLeft: return "Top Side Left"; + case TopSideRight: return "Top Side Right"; + case TopRearCenter: return "Top Rear Center"; + case TopRearLeft: return "Top Rear Left"; + case TopRearRight: return "Top Rear Right"; + case BottomCenter: return "Bottom Center"; + case BottomLeftCenter: return "Bottom Left Center"; + case BottomRightCenter: return "Bottom Right Center"; + default: + if (value >= AuxRangeStart && value <= AuxRangeEnd) { + return QString("Aux %1").arg(value - AuxRangeStart + 1); + } else if (value >= CustomRangeStart) { + return QString("Custom %1").arg(value - CustomRangeStart + 1); + } else { + return "Unknown"; + } + } +} + +void PwNode::bindHooks() { + pw_node_add_listener(this->proxy(), &this->listener.hook, &PwNode::EVENTS, this); +} + +void PwNode::unbindHooks() { + this->listener.remove(); + this->properties.clear(); + emit this->propertiesChanged(); + + if (this->boundData != nullptr) { + this->boundData->onUnbind(); + } +} + +void PwNode::initProps(const spa_dict* props) { + if (const auto* mediaClass = spa_dict_lookup(props, SPA_KEY_MEDIA_CLASS)) { + if (strcmp(mediaClass, "Audio/Sink") == 0) { + this->type = PwNodeType::Audio; + this->isSink = true; + this->isStream = false; + } else if (strcmp(mediaClass, "Audio/Source") == 0) { + this->type = PwNodeType::Audio; + this->isSink = false; + this->isStream = false; + } else if (strcmp(mediaClass, "Stream/Output/Audio") == 0) { + this->type = PwNodeType::Audio; + this->isSink = false; + this->isStream = true; + } else if (strcmp(mediaClass, "Stream/Input/Audio") == 0) { + this->type = PwNodeType::Audio; + this->isSink = true; + this->isStream = true; + } + } + + if (const auto* nodeName = spa_dict_lookup(props, SPA_KEY_NODE_NAME)) { + this->name = nodeName; + } + + if (const auto* nodeDesc = spa_dict_lookup(props, SPA_KEY_NODE_DESCRIPTION)) { + this->description = nodeDesc; + } + + if (const auto* nodeNick = spa_dict_lookup(props, "node.nick")) { + this->nick = nodeNick; + } + + if (this->type == PwNodeType::Audio) { + this->boundData = new PwNodeBoundAudio(this); + } +} + +const pw_node_events PwNode::EVENTS = { + .version = PW_VERSION_NODE_EVENTS, + .info = &PwNode::onInfo, + .param = &PwNode::onParam, +}; + +void PwNode::onInfo(void* data, const pw_node_info* info) { + auto* self = static_cast(data); + + if ((info->change_mask & PW_NODE_CHANGE_MASK_PROPS) != 0) { + auto properties = QMap(); + + const spa_dict_item* item = nullptr; + spa_dict_for_each(item, info->props) { properties.insert(item->key, item->value); } + + self->properties = properties; + emit self->propertiesChanged(); + } + + if (self->boundData != nullptr) { + self->boundData->onInfo(info); + } +} + +void PwNode::onParam( + void* data, + qint32 /*seq*/, + quint32 id, + quint32 index, + quint32 /*next*/, + const spa_pod* param +) { + auto* self = static_cast(data); + if (self->boundData != nullptr) { + self->boundData->onSpaParam(id, index, param); + } +} + +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++) { + auto& param = info->params[i]; // NOLINT + + if (param.id == SPA_PARAM_Props && (param.flags & SPA_PARAM_INFO_READ) != 0) { + pw_node_enum_params(this->node->proxy(), 0, param.id, 0, UINT32_MAX, nullptr); + } + } + } +} + +void PwNodeBoundAudio::onSpaParam(quint32 id, quint32 index, const spa_pod* param) { + if (id == SPA_PARAM_Props && index == 0) { + this->updateVolumeFromParam(param); + this->updateMutedFromParam(param); + } +} + +void PwNodeBoundAudio::updateVolumeFromParam(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); + + if (volumesProp == nullptr) { + qCWarning(logNode) << "Cannot update volume props of" << this->node + << "- channelVolumes was null."; + return; + } + + if (channelsProp == nullptr) { + qCWarning(logNode) << "Cannot update volume props of" << this->node << "- channelMap was null."; + return; + } + + if (spa_pod_is_array(&volumesProp->value) == 0) { + qCWarning(logNode) << "Cannot update volume props of" << this->node + << "- channelVolumes was not an array."; + return; + } + + if (spa_pod_is_array(&channelsProp->value) == 0) { + qCWarning(logNode) << "Cannot update volume props of" << this->node + << "- channelMap was not an array."; + return; + } + + const auto* volumes = reinterpret_cast(&volumesProp->value); // NOLINT + const auto* channels = reinterpret_cast(&channelsProp->value); // NOLINT + + auto volumesVec = QVector(); + auto channelsVec = QVector(); + + 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); // NOLINT + auto visual = std::cbrt(linear); + volumesVec.push_back(visual); + } + + SPA_POD_ARRAY_FOREACH(channels, iter) { + channelsVec.push_back(*reinterpret_cast(iter)); // NOLINT + } + + if (volumesVec.size() != channelsVec.size()) { + qCWarning(logNode) << "Cannot update volume props of" << this->node + << "- channelVolumes and channelMap are not the same size. Sizes:" + << volumesVec.size() << channelsVec.size(); + return; + } + + // It is important that the lengths of channels and volumes stay in sync whenever you read them. + auto channelsChanged = false; + auto volumesChanged = false; + + if (this->mChannels != channelsVec) { + this->mChannels = channelsVec; + channelsChanged = true; + qCDebug(logNode) << "Got updated channels of" << this->node << '-' << this->mChannels; + } + + if (this->mVolumes != volumesVec) { + this->mVolumes = volumesVec; + volumesChanged = true; + qCDebug(logNode) << "Got updated volumes of" << this->node << '-' << this->mVolumes; + } + + if (channelsChanged) emit this->channelsChanged(); + if (volumesChanged) emit this->volumesChanged(); +} + +void PwNodeBoundAudio::updateMutedFromParam(const spa_pod* param) { + const auto* mutedProp = spa_pod_find_prop(param, nullptr, SPA_PROP_mute); + + if (mutedProp == nullptr) { + qCWarning(logNode) << "Cannot update muted state of" << this->node + << "- mute property was null."; + return; + } + + if (spa_pod_is_bool(&mutedProp->value) == 0) { + qCWarning(logNode) << "Cannot update muted state of" << this->node + << "- mute property was not a boolean."; + return; + } + + bool muted = false; + spa_pod_get_bool(&mutedProp->value, &muted); + + if (muted != this->mMuted) { + qCDebug(logNode) << "Got updated mute status of" << this->node << '-' << muted; + this->mMuted = muted; + emit this->mutedChanged(); + } +} + +void PwNodeBoundAudio::onUnbind() { + this->mChannels.clear(); + this->mVolumes.clear(); + emit this->channelsChanged(); + emit this->volumesChanged(); +} + +bool PwNodeBoundAudio::isMuted() const { return this->mMuted; } + +void PwNodeBoundAudio::setMuted(bool muted) { + if (this->node->proxy() == nullptr) { + qCWarning(logNode) << "Tried to change mute state for" << this->node << "which is not bound."; + return; + } + + if (muted == this->mMuted) return; + + auto buffer = std::array(); + auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size()); + + // is this a leak? seems like probably not? docs don't say, as usual. + // clang-format off + auto* pod = spa_pod_builder_add_object( + &builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, + SPA_PROP_mute, SPA_POD_Bool(muted) + ); + // clang-format on + + qCDebug(logNode) << "Changed muted state of" << this->node << "to" << muted; + this->mMuted = muted; + pw_node_set_param(this->node->proxy(), SPA_PARAM_Props, 0, static_cast(pod)); + emit this->mutedChanged(); +} + +float PwNodeBoundAudio::averageVolume() const { + float total = 0; + + for (auto volume: this->mVolumes) { + total += volume; + } + + return total / static_cast(this->mVolumes.size()); +} + +void PwNodeBoundAudio::setAverageVolume(float volume) { + auto oldAverage = this->averageVolume(); + auto mul = oldAverage == 0 ? 0 : volume / oldAverage; + auto volumes = QVector(); + + for (auto oldVolume: this->mVolumes) { + volumes.push_back(mul == 0 ? volume : oldVolume * mul); + } + + this->setVolumes(volumes); +} + +QVector PwNodeBoundAudio::channels() const { return this->mChannels; } + +QVector PwNodeBoundAudio::volumes() const { return this->mVolumes; } + +void PwNodeBoundAudio::setVolumes(const QVector& volumes) { + if (this->node->proxy() == nullptr) { + qCWarning(logNode) << "Tried to change node volumes for" << this->node << "which is not bound."; + return; + } + + if (volumes == this->mVolumes) return; + + if (volumes.length() != this->mVolumes.length()) { + qCWarning(logNode) << "Tried to change node volumes for" << this->node << "from" + << this->mVolumes << "to" << volumes + << "which has a different length than the list of channels" + << this->mChannels; + return; + } + + auto buffer = std::array(); + auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size()); + + auto cubedVolumes = QVector(); + for (auto volume: volumes) { + cubedVolumes.push_back(volume * volume * volume); + } + + // clang-format off + auto* pod = spa_pod_builder_add_object( + &builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, + SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float), SPA_TYPE_Float, cubedVolumes.length(), cubedVolumes.data()) + ); + // clang-format on + + qCDebug(logNode) << "Changed volumes of" << this->node << "to" << volumes; + this->mVolumes = volumes; + pw_node_set_param(this->node->proxy(), SPA_PARAM_Props, 0, static_cast(pod)); + emit this->volumesChanged(); +} + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp new file mode 100644 index 00000000..a1a60c93 --- /dev/null +++ b/src/services/pipewire/node.hpp @@ -0,0 +1,174 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core.hpp" +#include "registry.hpp" + +namespace qs::service::pipewire { + +class PwAudioChannel: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum { + Unknown = SPA_AUDIO_CHANNEL_UNKNOWN, + NA = SPA_AUDIO_CHANNEL_NA, + Mono = SPA_AUDIO_CHANNEL_MONO, + FrontCenter = SPA_AUDIO_CHANNEL_FC, + FrontLeft = SPA_AUDIO_CHANNEL_FL, + FrontRight = SPA_AUDIO_CHANNEL_FR, + FrontLeftCenter = SPA_AUDIO_CHANNEL_FLC, + FrontRightCenter = SPA_AUDIO_CHANNEL_FRC, + FrontLeftWide = SPA_AUDIO_CHANNEL_FLW, + FrontRightWide = SPA_AUDIO_CHANNEL_FRW, + FrontCenterHigh = SPA_AUDIO_CHANNEL_FCH, + FrontLeftHigh = SPA_AUDIO_CHANNEL_FLH, + FrontRightHigh = SPA_AUDIO_CHANNEL_FRH, + LowFrequencyEffects = SPA_AUDIO_CHANNEL_LFE, + LowFrequencyEffects2 = SPA_AUDIO_CHANNEL_LFE2, + LowFrequencyEffectsLeft = SPA_AUDIO_CHANNEL_LLFE, + LowFrequencyEffectsRight = SPA_AUDIO_CHANNEL_RLFE, + SideLeft = SPA_AUDIO_CHANNEL_SL, + SideRight = SPA_AUDIO_CHANNEL_SR, + RearCenter = SPA_AUDIO_CHANNEL_RC, + RearLeft = SPA_AUDIO_CHANNEL_RL, + RearRight = SPA_AUDIO_CHANNEL_RR, + RearLeftCenter = SPA_AUDIO_CHANNEL_RLC, + RearRightCenter = SPA_AUDIO_CHANNEL_RRC, + TopCenter = SPA_AUDIO_CHANNEL_TC, + TopFrontCenter = SPA_AUDIO_CHANNEL_TFC, + TopFrontLeft = SPA_AUDIO_CHANNEL_TFL, + TopFrontRight = SPA_AUDIO_CHANNEL_TFR, + TopFrontLeftCenter = SPA_AUDIO_CHANNEL_TFLC, + TopFrontRightCenter = SPA_AUDIO_CHANNEL_TFRC, + TopSideLeft = SPA_AUDIO_CHANNEL_TSL, + TopSideRight = SPA_AUDIO_CHANNEL_TSR, + TopRearCenter = SPA_AUDIO_CHANNEL_TRC, + TopRearLeft = SPA_AUDIO_CHANNEL_TRL, + TopRearRight = SPA_AUDIO_CHANNEL_TRR, + BottomCenter = SPA_AUDIO_CHANNEL_BC, + BottomLeftCenter = SPA_AUDIO_CHANNEL_BLC, + BottomRightCenter = SPA_AUDIO_CHANNEL_BRC, + /// The start of the aux channel range. + /// + /// Values between AuxRangeStart and AuxRangeEnd are valid. + AuxRangeStart = SPA_AUDIO_CHANNEL_START_Aux, + /// The end of the aux channel range. + /// + /// Values between AuxRangeStart and AuxRangeEnd are valid. + AuxRangeEnd = SPA_AUDIO_CHANNEL_LAST_Aux, + /// The end of the custom channel range. + /// + /// Values starting at CustomRangeStart are valid. + CustomRangeStart = SPA_AUDIO_CHANNEL_START_Custom, + }; + Q_ENUM(Enum); + + /// Print a human readable representation of the given channel, + /// including aux and custom channel ranges. + Q_INVOKABLE static QString toString(PwAudioChannel::Enum value); +}; + +enum class PwNodeType { + Untracked, + Audio, +}; + +class PwNode; + +class PwNodeBoundData { +public: + PwNodeBoundData() = default; + virtual ~PwNodeBoundData() = default; + Q_DISABLE_COPY_MOVE(PwNodeBoundData); + + virtual void onInfo(const pw_node_info* /*info*/) {} + virtual void onSpaParam(quint32 /*id*/, quint32 /*index*/, const spa_pod* /*param*/) {} + virtual void onUnbind() {} +}; + +class PwNodeBoundAudio + : public QObject + , public PwNodeBoundData { + Q_OBJECT; + +public: + explicit PwNodeBoundAudio(PwNode* node): node(node) {} + + void onInfo(const pw_node_info* info) override; + void onSpaParam(quint32 id, quint32 index, const spa_pod* param) override; + void onUnbind() override; + + [[nodiscard]] bool isMuted() const; + void setMuted(bool muted); + + [[nodiscard]] float averageVolume() const; + void setAverageVolume(float volume); + + [[nodiscard]] QVector channels() const; + + [[nodiscard]] QVector volumes() const; + void setVolumes(const QVector& volumes); + +signals: + void volumesChanged(); + void channelsChanged(); + void mutedChanged(); + +private: + void updateVolumeFromParam(const spa_pod* param); + void updateMutedFromParam(const spa_pod* param); + + bool mMuted = false; + QVector mChannels; + QVector mVolumes; + PwNode* node; +}; + +constexpr const char TYPE_INTERFACE_Node[] = PW_TYPE_INTERFACE_Node; // NOLINT +class PwNode: public PwBindable { // NOLINT + Q_OBJECT; + +public: + void bindHooks() override; + void unbindHooks() override; + void initProps(const spa_dict* props) override; + + QString name; + QString description; + QString nick; + QMap properties; + + PwNodeType type = PwNodeType::Untracked; + bool isSink = false; + bool isStream = false; + + PwNodeBoundData* boundData = nullptr; + +signals: + void propertiesChanged(); + +private: + static const pw_node_events EVENTS; + static void onInfo(void* data, const pw_node_info* info); + static void + onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param); + + SpaHook listener; +}; + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp new file mode 100644 index 00000000..a6617d29 --- /dev/null +++ b/src/services/pipewire/qml.cpp @@ -0,0 +1,472 @@ +#include "qml.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "connection.hpp" +#include "link.hpp" +#include "metadata.hpp" +#include "node.hpp" +#include "registry.hpp" + +namespace qs::service::pipewire { + +void PwObjectIface::ref() { + this->refcount++; + + if (this->refcount == 1) { + this->object->ref(); + } +} + +void PwObjectIface::unref() { + if (this->refcount == 0) return; + this->refcount--; + + if (this->refcount == 0) { + this->object->unref(); + } +} + +Pipewire::Pipewire(QObject* parent): QObject(parent) { + auto* connection = PwConnection::instance(); + + for (auto* node: connection->registry.nodes.values()) { + this->onNodeAdded(node); + } + + QObject::connect(&connection->registry, &PwRegistry::nodeAdded, this, &Pipewire::onNodeAdded); + + for (auto* link: connection->registry.links.values()) { + this->onLinkAdded(link); + } + + QObject::connect(&connection->registry, &PwRegistry::linkAdded, this, &Pipewire::onLinkAdded); + + for (auto* group: connection->registry.linkGroups) { + this->onLinkGroupAdded(group); + } + + QObject::connect( + &connection->registry, + &PwRegistry::linkGroupAdded, + this, + &Pipewire::onLinkGroupAdded + ); + + // clang-format off + QObject::connect(&connection->defaults, &PwDefaultsMetadata::defaultSinkChanged, this, &Pipewire::defaultAudioSinkChanged); + QObject::connect(&connection->defaults, &PwDefaultsMetadata::defaultSourceChanged, this, &Pipewire::defaultAudioSourceChanged); + // clang-format on +} + +QQmlListProperty Pipewire::nodes() { + return QQmlListProperty(this, nullptr, &Pipewire::nodesCount, &Pipewire::nodeAt); +} + +qsizetype Pipewire::nodesCount(QQmlListProperty* property) { + return static_cast(property->object)->mNodes.count(); // NOLINT +} + +PwNodeIface* Pipewire::nodeAt(QQmlListProperty* property, qsizetype index) { + return static_cast(property->object)->mNodes.at(index); // NOLINT +} + +void Pipewire::onNodeAdded(PwNode* node) { + auto* iface = PwNodeIface::instance(node); + QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onNodeRemoved); + + this->mNodes.push_back(iface); + emit this->nodesChanged(); +} + +void Pipewire::onNodeRemoved(QObject* object) { + auto* iface = static_cast(object); // NOLINT + this->mNodes.removeOne(iface); + emit this->nodesChanged(); +} + +QQmlListProperty Pipewire::links() { + return QQmlListProperty(this, nullptr, &Pipewire::linksCount, &Pipewire::linkAt); +} + +qsizetype Pipewire::linksCount(QQmlListProperty* property) { + return static_cast(property->object)->mLinks.count(); // NOLINT +} + +PwLinkIface* Pipewire::linkAt(QQmlListProperty* property, qsizetype index) { + return static_cast(property->object)->mLinks.at(index); // NOLINT +} + +void Pipewire::onLinkAdded(PwLink* link) { + auto* iface = PwLinkIface::instance(link); + QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkRemoved); + + this->mLinks.push_back(iface); + emit this->linksChanged(); +} + +void Pipewire::onLinkRemoved(QObject* object) { + auto* iface = static_cast(object); // NOLINT + this->mLinks.removeOne(iface); + emit this->linksChanged(); +} + +QQmlListProperty Pipewire::linkGroups() { + return QQmlListProperty( + this, + nullptr, + &Pipewire::linkGroupsCount, + &Pipewire::linkGroupAt + ); +} + +qsizetype Pipewire::linkGroupsCount(QQmlListProperty* property) { + return static_cast(property->object)->mLinkGroups.count(); // NOLINT +} + +PwLinkGroupIface* +Pipewire::linkGroupAt(QQmlListProperty* property, qsizetype index) { + return static_cast(property->object)->mLinkGroups.at(index); // NOLINT +} + +void Pipewire::onLinkGroupAdded(PwLinkGroup* linkGroup) { + auto* iface = PwLinkGroupIface::instance(linkGroup); + QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkGroupRemoved); + + this->mLinkGroups.push_back(iface); + emit this->linkGroupsChanged(); +} + +void Pipewire::onLinkGroupRemoved(QObject* object) { + auto* iface = static_cast(object); // NOLINT + this->mLinkGroups.removeOne(iface); + emit this->linkGroupsChanged(); +} + +PwNodeIface* Pipewire::defaultAudioSink() const { // NOLINT + auto* connection = PwConnection::instance(); + auto name = connection->defaults.defaultSink(); + + for (auto* node: connection->registry.nodes.values()) { + if (name == node->name) { + return PwNodeIface::instance(node); + } + } + + return nullptr; +} + +PwNodeIface* Pipewire::defaultAudioSource() const { // NOLINT + auto* connection = PwConnection::instance(); + auto name = connection->defaults.defaultSource(); + + for (auto* node: connection->registry.nodes.values()) { + if (name == node->name) { + return PwNodeIface::instance(node); + } + } + + return nullptr; +} + +PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; } + +void PwNodeLinkTracker::setNode(PwNodeIface* node) { + if (node == this->mNode) return; + + if (this->mNode != nullptr) { + if (node == nullptr) { + QObject::disconnect(&PwConnection::instance()->registry, nullptr, this, nullptr); + } + + QObject::disconnect(this->mNode, nullptr, this, nullptr); + } + + if (node != nullptr) { + if (this->mNode == nullptr) { + QObject::connect( + &PwConnection::instance()->registry, + &PwRegistry::linkGroupAdded, + this, + &PwNodeLinkTracker::onLinkGroupCreated + ); + } + + QObject::connect(node, &QObject::destroyed, this, &PwNodeLinkTracker::onNodeDestroyed); + } + + this->mNode = node; + this->updateLinks(); + emit this->nodeChanged(); +} + +void PwNodeLinkTracker::updateLinks() { + // done first to avoid unref->reref of nodes + auto newLinks = QVector(); + if (this->mNode != nullptr) { + auto* connection = PwConnection::instance(); + + for (auto* link: connection->registry.linkGroups) { + if ((!this->mNode->isSink() && link->outputNode() == this->mNode->id()) + || (this->mNode->isSink() && link->inputNode() == this->mNode->id())) + { + auto* iface = PwLinkGroupIface::instance(link); + + // do not connect twice + if (!this->mLinkGroups.contains(iface)) { + QObject::connect( + iface, + &QObject::destroyed, + this, + &PwNodeLinkTracker::onLinkGroupDestroyed + ); + } + + newLinks.push_back(iface); + } + } + } + + for (auto* iface: this->mLinkGroups) { + // only disconnect no longer used nodes + if (!newLinks.contains(iface)) { + QObject::disconnect(iface, nullptr, this, nullptr); + } + } + + this->mLinkGroups = newLinks; + emit this->linkGroupsChanged(); +} + +QQmlListProperty PwNodeLinkTracker::linkGroups() { + return QQmlListProperty( + this, + nullptr, + &PwNodeLinkTracker::linkGroupsCount, + &PwNodeLinkTracker::linkGroupAt + ); +} + +qsizetype PwNodeLinkTracker::linkGroupsCount(QQmlListProperty* property) { + return static_cast(property->object)->mLinkGroups.count(); // NOLINT +} + +PwLinkGroupIface* +PwNodeLinkTracker::linkGroupAt(QQmlListProperty* property, qsizetype index) { + return static_cast(property->object)->mLinkGroups.at(index); // NOLINT +} + +void PwNodeLinkTracker::onNodeDestroyed() { + this->mNode = nullptr; + this->updateLinks(); + emit this->nodeChanged(); +} + +void PwNodeLinkTracker::onLinkGroupCreated(PwLinkGroup* linkGroup) { + if ((!this->mNode->isSink() && linkGroup->outputNode() == this->mNode->id()) + || (this->mNode->isSink() && linkGroup->inputNode() == this->mNode->id())) + { + auto* iface = PwLinkGroupIface::instance(linkGroup); + QObject::connect(iface, &QObject::destroyed, this, &PwNodeLinkTracker::onLinkGroupDestroyed); + this->mLinkGroups.push_back(iface); + emit this->linkGroupsChanged(); + } +} + +void PwNodeLinkTracker::onLinkGroupDestroyed(QObject* object) { + if (this->mLinkGroups.removeOne(object)) { + emit this->linkGroupsChanged(); + } +} + +PwNodeAudioIface::PwNodeAudioIface(PwNodeBoundAudio* boundData, QObject* parent) + : QObject(parent) + , boundData(boundData) { + // clang-format off + QObject::connect(boundData, &PwNodeBoundAudio::mutedChanged, this, &PwNodeAudioIface::mutedChanged); + QObject::connect(boundData, &PwNodeBoundAudio::channelsChanged, this, &PwNodeAudioIface::channelsChanged); + QObject::connect(boundData, &PwNodeBoundAudio::volumesChanged, this, &PwNodeAudioIface::volumesChanged); + // clang-format on +} + +bool PwNodeAudioIface::isMuted() const { return this->boundData->isMuted(); } + +void PwNodeAudioIface::setMuted(bool muted) { this->boundData->setMuted(muted); } + +float PwNodeAudioIface::averageVolume() const { return this->boundData->averageVolume(); } + +void PwNodeAudioIface::setAverageVolume(float volume) { this->boundData->setAverageVolume(volume); } + +QVector PwNodeAudioIface::channels() const { + return this->boundData->channels(); +} + +QVector PwNodeAudioIface::volumes() const { return this->boundData->volumes(); } + +void PwNodeAudioIface::setVolumes(const QVector& volumes) { + this->boundData->setVolumes(volumes); +} + +PwNodeIface::PwNodeIface(PwNode* node): PwObjectIface(node), mNode(node) { + QObject::connect(node, &PwNode::propertiesChanged, this, &PwNodeIface::propertiesChanged); + + if (auto* audioBoundData = dynamic_cast(node->boundData)) { + this->audioIface = new PwNodeAudioIface(audioBoundData, this); + } +} + +PwNode* PwNodeIface::node() const { return this->mNode; } + +QString PwNodeIface::name() const { return this->mNode->name; } + +quint32 PwNodeIface::id() const { return this->mNode->id; } + +QString PwNodeIface::description() const { return this->mNode->description; } + +QString PwNodeIface::nickname() const { return this->mNode->nick; } + +bool PwNodeIface::isSink() const { return this->mNode->isSink; } + +bool PwNodeIface::isStream() const { return this->mNode->isStream; } + +QVariantMap PwNodeIface::properties() const { + auto map = QVariantMap(); + for (auto [k, v]: this->mNode->properties.asKeyValueRange()) { + map.insert(k, QVariant::fromValue(v)); + } + + return map; +} + +PwNodeAudioIface* PwNodeIface::audio() const { return this->audioIface; } + +PwNodeIface* PwNodeIface::instance(PwNode* node) { + auto v = node->property("iface"); + if (v.canConvert()) { + return v.value(); + } + + auto* instance = new PwNodeIface(node); + node->setProperty("iface", QVariant::fromValue(instance)); + + return instance; +} + +PwLinkIface::PwLinkIface(PwLink* link): PwObjectIface(link), mLink(link) { + QObject::connect(link, &PwLink::stateChanged, this, &PwLinkIface::stateChanged); +} + +PwLink* PwLinkIface::link() const { return this->mLink; } + +quint32 PwLinkIface::id() const { return this->mLink->id; } + +PwNodeIface* PwLinkIface::target() const { + return PwNodeIface::instance( + PwConnection::instance()->registry.nodes.value(this->mLink->inputNode()) + ); +} + +PwNodeIface* PwLinkIface::source() const { + return PwNodeIface::instance( + PwConnection::instance()->registry.nodes.value(this->mLink->outputNode()) + ); +} + +PwLinkState::Enum PwLinkIface::state() const { return this->mLink->state(); } + +PwLinkIface* PwLinkIface::instance(PwLink* link) { + auto v = link->property("iface"); + if (v.canConvert()) { + return v.value(); + } + + auto* instance = new PwLinkIface(link); + link->setProperty("iface", QVariant::fromValue(instance)); + + return instance; +} + +PwLinkGroupIface::PwLinkGroupIface(PwLinkGroup* group): QObject(group), mGroup(group) { + QObject::connect(group, &PwLinkGroup::stateChanged, this, &PwLinkGroupIface::stateChanged); + QObject::connect(group, &QObject::destroyed, this, [this]() { delete this; }); +} + +void PwLinkGroupIface::ref() { this->mGroup->ref(); } + +void PwLinkGroupIface::unref() { this->mGroup->unref(); } + +PwLinkGroup* PwLinkGroupIface::group() const { return this->mGroup; } + +PwNodeIface* PwLinkGroupIface::target() const { + return PwNodeIface::instance( + PwConnection::instance()->registry.nodes.value(this->mGroup->inputNode()) + ); +} + +PwNodeIface* PwLinkGroupIface::source() const { + return PwNodeIface::instance( + PwConnection::instance()->registry.nodes.value(this->mGroup->outputNode()) + ); +} + +PwLinkState::Enum PwLinkGroupIface::state() const { return this->mGroup->state(); } + +PwLinkGroupIface* PwLinkGroupIface::instance(PwLinkGroup* group) { + auto v = group->property("iface"); + if (v.canConvert()) { + return v.value(); + } + + auto* instance = new PwLinkGroupIface(group); + group->setProperty("iface", QVariant::fromValue(instance)); + + return instance; +} + +PwObjectTracker::~PwObjectTracker() { this->clearList(); } + +QList PwObjectTracker::objects() const { return this->trackedObjects; } + +void PwObjectTracker::setObjects(const QList& objects) { + // +1 ref before removing old refs to avoid an unbind->bind. + for (auto* object: objects) { + if (auto* pwObject = dynamic_cast(object)) { + pwObject->ref(); + } + } + + this->clearList(); + + // connect destroy + for (auto* object: objects) { + if (auto* pwObject = dynamic_cast(object)) { + QObject::connect(object, &QObject::destroyed, this, &PwObjectTracker::objectDestroyed); + } + } + + this->trackedObjects = objects; +} + +void PwObjectTracker::clearList() { + for (auto* object: this->trackedObjects) { + if (auto* pwObject = dynamic_cast(object)) { + pwObject->unref(); + QObject::disconnect(object, nullptr, this, nullptr); + } + } + + this->trackedObjects.clear(); +} + +void PwObjectTracker::objectDestroyed(QObject* object) { + this->trackedObjects.removeOne(object); + emit this->objectsChanged(); +} + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp new file mode 100644 index 00000000..9b452727 --- /dev/null +++ b/src/services/pipewire/qml.hpp @@ -0,0 +1,368 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "link.hpp" +#include "node.hpp" +#include "registry.hpp" + +namespace qs::service::pipewire { + +class PwNodeIface; +class PwLinkIface; +class PwLinkGroupIface; + +class PwObjectRefIface { +public: + PwObjectRefIface() = default; + virtual ~PwObjectRefIface() = default; + Q_DISABLE_COPY_MOVE(PwObjectRefIface); + + virtual void ref() = 0; + virtual void unref() = 0; +}; + +class PwObjectIface + : public QObject + , public PwObjectRefIface { + Q_OBJECT; + +public: + explicit PwObjectIface(PwBindableObject* object): QObject(object), object(object) {}; + // destructor should ONLY be called by the pw object destructor, making an unref unnecessary + ~PwObjectIface() override = default; + Q_DISABLE_COPY_MOVE(PwObjectIface); + + void ref() override; + void unref() override; + +private: + quint32 refcount = 0; + PwBindableObject* object; +}; + +///! Contains links to all pipewire objects. +class Pipewire: public QObject { + Q_OBJECT; + // clang-format off + /// All pipewire nodes. + Q_PROPERTY(QQmlListProperty nodes READ nodes NOTIFY nodesChanged); + /// All pipewire links. + Q_PROPERTY(QQmlListProperty links READ links NOTIFY linksChanged); + /// All pipewire link groups. + Q_PROPERTY(QQmlListProperty linkGroups READ linkGroups NOTIFY linkGroupsChanged); + /// The default audio sink or `null`. + Q_PROPERTY(PwNodeIface* defaultAudioSink READ defaultAudioSink NOTIFY defaultAudioSinkChanged); + /// The default audio source or `null`. + Q_PROPERTY(PwNodeIface* defaultAudioSource READ defaultAudioSource NOTIFY defaultAudioSourceChanged); + // clang-format on + QML_ELEMENT; + QML_SINGLETON; + +public: + explicit Pipewire(QObject* parent = nullptr); + + [[nodiscard]] QQmlListProperty nodes(); + [[nodiscard]] QQmlListProperty links(); + [[nodiscard]] QQmlListProperty linkGroups(); + [[nodiscard]] PwNodeIface* defaultAudioSink() const; + [[nodiscard]] PwNodeIface* defaultAudioSource() const; + +signals: + void nodesChanged(); + void linksChanged(); + void linkGroupsChanged(); + void defaultAudioSinkChanged(); + void defaultAudioSourceChanged(); + +private slots: + void onNodeAdded(PwNode* node); + void onNodeRemoved(QObject* object); + void onLinkAdded(PwLink* link); + void onLinkRemoved(QObject* object); + void onLinkGroupAdded(PwLinkGroup* group); + void onLinkGroupRemoved(QObject* object); + +private: + static qsizetype nodesCount(QQmlListProperty* property); + static PwNodeIface* nodeAt(QQmlListProperty* property, qsizetype index); + static qsizetype linksCount(QQmlListProperty* property); + static PwLinkIface* linkAt(QQmlListProperty* property, qsizetype index); + static qsizetype linkGroupsCount(QQmlListProperty* property); + static PwLinkGroupIface* + linkGroupAt(QQmlListProperty* property, qsizetype index); + + QVector mNodes; + QVector mLinks; + QVector mLinkGroups; +}; + +///! Tracks all link connections to a given node. +class PwNodeLinkTracker: public QObject { + Q_OBJECT; + // clang-format off + /// The node to track connections to. + Q_PROPERTY(PwNodeIface* node READ node WRITE setNode NOTIFY nodeChanged); + /// Link groups connected to the given node. + /// + /// 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. + Q_PROPERTY(QQmlListProperty linkGroups READ linkGroups NOTIFY linkGroupsChanged); + // clang-format on + QML_ELEMENT; + +public: + explicit PwNodeLinkTracker(QObject* parent = nullptr): QObject(parent) {} + + [[nodiscard]] PwNodeIface* node() const; + void setNode(PwNodeIface* node); + + [[nodiscard]] QQmlListProperty linkGroups(); + +signals: + void nodeChanged(); + void linkGroupsChanged(); + +private slots: + void onNodeDestroyed(); + void onLinkGroupCreated(PwLinkGroup* linkGroup); + void onLinkGroupDestroyed(QObject* object); + +private: + static qsizetype linkGroupsCount(QQmlListProperty* property); + static PwLinkGroupIface* + linkGroupAt(QQmlListProperty* property, qsizetype index); + + void updateLinks(); + + PwNodeIface* mNode = nullptr; + QVector mLinkGroups; +}; + +///! Audio specific properties of pipewire nodes. +class PwNodeAudioIface: public QObject { + Q_OBJECT; + /// If the node is currently muted. Setting this property changes the mute state. + /// + /// **This property is invalid unless the node is [bound](../pwobjecttracker).** + Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged); + /// The average volume over all channels of the node. + /// Setting this property modifies the volume of all channels proportionately. + /// + /// **This property is invalid unless the node is [bound](../pwobjecttracker).** + Q_PROPERTY(float volume READ averageVolume WRITE setAverageVolume NOTIFY volumesChanged); + /// The audio channels present on the node. + /// + /// **This property is invalid unless the node is [bound](../pwobjecttracker).** + Q_PROPERTY(QVector channels READ channels NOTIFY channelsChanged); + /// The volumes of each audio channel individually. Each entry corrosponds to + /// the channel at the same index in `channels`. `volumes` and `channels` will always be + /// the same length. + /// + /// **This property is invalid unless the node is [bound](../pwobjecttracker).** + Q_PROPERTY(QVector volumes READ volumes WRITE setVolumes NOTIFY volumesChanged); + QML_NAMED_ELEMENT(PwNodeAudio); + QML_UNCREATABLE("PwNodeAudio cannot be created directly"); + +public: + explicit PwNodeAudioIface(PwNodeBoundAudio* boundData, QObject* parent); + + [[nodiscard]] bool isMuted() const; + void setMuted(bool muted); + + [[nodiscard]] float averageVolume() const; + void setAverageVolume(float volume); + + [[nodiscard]] QVector channels() const; + + [[nodiscard]] QVector volumes() const; + void setVolumes(const QVector& volumes); + +signals: + void mutedChanged(); + void channelsChanged(); + void volumesChanged(); + +private: + PwNodeBoundAudio* boundData; +}; + +///! A node in the pipewire connection graph. +class PwNodeIface: public PwObjectIface { + Q_OBJECT; + /// The pipewire object id of the node. + /// + /// Mainly useful for debugging. you can inspect the node directly + /// with `pw-cli i `. + Q_PROPERTY(quint32 id READ id CONSTANT); + /// The node's name, corrosponding to the object's `node.name` property. + Q_PROPERTY(QString name READ name CONSTANT); + /// The node's description, corrosponding to the object's `node.description` property. + /// + /// May be empty. Generally more human readable than `name`. + Q_PROPERTY(QString description READ description CONSTANT); + /// The node's nickname, corrosponding to the object's `node.nickname` property. + /// + /// May be empty. Generally but not always more human readable than `description`. + Q_PROPERTY(QString nickname READ nickname CONSTANT); + /// If `true`, then the node accepts audio input from other nodes, + /// if `false` the node outputs audio to other nodes. + Q_PROPERTY(bool isSink READ isSink CONSTANT); + /// If `true` then the node is likely to be a program, if false it is liekly to be hardware. + Q_PROPERTY(bool isStream READ isStream CONSTANT); + /// The property set present on the node, as an object containing key-value pairs. + /// You can inspect this directly with `pw-cli i `. + /// + /// A few properties of note, which may or may not be present: + /// - `application.name` - A suggested human readable name for the node. + /// - `application.icon-name` - The name of an icon recommended to display for the node. + /// - `media.name` - A description of the currently playing media. + /// (more likely to be present than `media.title` and `media.artist`) + /// - `media.title` - The title of the currently playing media. + /// - `media.artist` - The artist of the currently playing media. + /// + /// **This property is invalid unless the node is [bound](../pwobjecttracker).** + Q_PROPERTY(QVariantMap properties READ properties NOTIFY propertiesChanged); + /// Extra information present only if the node sends or receives audio. + Q_PROPERTY(PwNodeAudioIface* audio READ audio CONSTANT); + QML_NAMED_ELEMENT(PwNode); + QML_UNCREATABLE("PwNodes cannot be created directly"); + +public: + explicit PwNodeIface(PwNode* node); + + [[nodiscard]] PwNode* node() const; + [[nodiscard]] quint32 id() const; + [[nodiscard]] QString name() const; + [[nodiscard]] QString description() const; + [[nodiscard]] QString nickname() const; + [[nodiscard]] bool isSink() const; + [[nodiscard]] bool isStream() const; + [[nodiscard]] QVariantMap properties() const; + [[nodiscard]] PwNodeAudioIface* audio() const; + + static PwNodeIface* instance(PwNode* node); + +signals: + void propertiesChanged(); + +private: + PwNode* mNode; + PwNodeAudioIface* audioIface = nullptr; +}; + +///! A connection between pipewire nodes. +/// Note that there is one link per *channel* of a connection between nodes. +/// You usually want [PwLinkGroup](../pwlinkgroup). +class PwLinkIface: public PwObjectIface { + Q_OBJECT; + /// The pipewire object id of the link. + /// + /// Mainly useful for debugging. you can inspect the link directly + /// with `pw-cli i `. + Q_PROPERTY(quint32 id READ id CONSTANT); + /// The node that is *receiving* information. (the sink) + Q_PROPERTY(PwNodeIface* target READ target CONSTANT); + /// The node that is *sending* information. (the source) + Q_PROPERTY(PwNodeIface* source READ source CONSTANT); + /// The current state of the link. + /// + /// **This property is invalid unless the link is [bound](../pwobjecttracker).** + Q_PROPERTY(PwLinkState::Enum state READ state NOTIFY stateChanged); + QML_NAMED_ELEMENT(PwLink); + QML_UNCREATABLE("PwLinks cannot be created directly"); + +public: + explicit PwLinkIface(PwLink* link); + + [[nodiscard]] PwLink* link() const; + [[nodiscard]] quint32 id() const; + [[nodiscard]] PwNodeIface* target() const; + [[nodiscard]] PwNodeIface* source() const; + [[nodiscard]] PwLinkState::Enum state() const; + + static PwLinkIface* instance(PwLink* link); + +signals: + void stateChanged(); + +private: + PwLink* mLink; +}; + +///! A group of connections between pipewire nodes. +/// A group of connections between pipewire nodes, one per source->target pair. +class PwLinkGroupIface + : public QObject + , public PwObjectRefIface { + Q_OBJECT; + /// The node that is *receiving* information. (the sink) + Q_PROPERTY(PwNodeIface* target READ target CONSTANT); + /// The node that is *sending* information. (the source) + Q_PROPERTY(PwNodeIface* source READ source CONSTANT); + /// The current state of the link group. + /// + /// **This property is invalid unless the link is [bound](../pwobjecttracker).** + Q_PROPERTY(PwLinkState::Enum state READ state NOTIFY stateChanged); + QML_NAMED_ELEMENT(PwLinkGroup); + QML_UNCREATABLE("PwLinkGroups cannot be created directly"); + +public: + explicit PwLinkGroupIface(PwLinkGroup* group); + // destructor should ONLY be called by the pw object destructor, making an unref unnecessary + ~PwLinkGroupIface() override = default; + Q_DISABLE_COPY_MOVE(PwLinkGroupIface); + + void ref() override; + void unref() override; + + [[nodiscard]] PwLinkGroup* group() const; + [[nodiscard]] PwNodeIface* target() const; + [[nodiscard]] PwNodeIface* source() const; + [[nodiscard]] PwLinkState::Enum state() const; + + static PwLinkGroupIface* instance(PwLinkGroup* group); + +signals: + void stateChanged(); + +private: + PwLinkGroup* mGroup; +}; + +///! Binds pipewire objects. +/// If the object list of at least one PwObjectTracker contains a given pipewire object, +/// it will become *bound* and you will be able to interact with bound-only properties. +class PwObjectTracker: public QObject { + Q_OBJECT; + /// The list of objects to bind. + Q_PROPERTY(QList objects READ objects WRITE setObjects NOTIFY objectsChanged); + QML_ELEMENT; + +public: + explicit PwObjectTracker(QObject* parent = nullptr): QObject(parent) {} + ~PwObjectTracker() override; + Q_DISABLE_COPY_MOVE(PwObjectTracker); + + [[nodiscard]] QList objects() const; + void setObjects(const QList& objects); + +signals: + void objectsChanged(); + +private slots: + void objectDestroyed(QObject* object); + +private: + void clearList(); + + QList trackedObjects; +}; + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/registry.cpp b/src/services/pipewire/registry.cpp new file mode 100644 index 00000000..28142765 --- /dev/null +++ b/src/services/pipewire/registry.cpp @@ -0,0 +1,193 @@ +#include "registry.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core.hpp" +#include "link.hpp" +#include "metadata.hpp" +#include "node.hpp" + +namespace qs::service::pipewire { + +Q_LOGGING_CATEGORY(logRegistry, "quickshell.service.pipewire.registry", QtWarningMsg); + +PwBindableObject::~PwBindableObject() { + if (this->id != 0) { + qCFatal(logRegistry) << "Destroyed pipewire object" << this + << "without causing safeDestroy. THIS IS UNDEFINED BEHAVIOR."; + } +} + +void PwBindableObject::init(PwRegistry* registry, quint32 id, quint32 perms) { + this->id = id; + this->perms = perms; + this->registry = registry; + this->setParent(registry); + qCDebug(logRegistry) << "Creating object" << this; +} + +void PwBindableObject::safeDestroy() { + this->unbind(); + qCDebug(logRegistry) << "Destroying object" << this; + emit this->destroying(this); + this->id = 0; + delete this; +} + +void PwBindableObject::debugId(QDebug& debug) const { + auto saver = QDebugStateSaver(debug); + debug.nospace() << this->id << "/" << (this->object == nullptr ? "unbound" : "bound"); +} + +void PwBindableObject::ref() { + this->refcount++; + if (this->refcount == 1) this->bind(); +} + +void PwBindableObject::unref() { + this->refcount--; + if (this->refcount == 0) this->unbind(); +} + +void PwBindableObject::bind() { + qCDebug(logRegistry) << "Bound object" << this; + this->bindHooks(); +} + +void PwBindableObject::unbind() { + if (this->object == nullptr) return; + qCDebug(logRegistry) << "Unbinding object" << this; + this->unbindHooks(); + pw_proxy_destroy(this->object); + this->object = nullptr; +} + +QDebug operator<<(QDebug debug, const PwBindableObject* object) { + if (object == nullptr) { + debug << "PwBindableObject(0x0)"; + } else { + auto saver = QDebugStateSaver(debug); + // 0 if not present, start of class name if present + auto idx = QString(object->metaObject()->className()).lastIndexOf(':') + 1; + debug.nospace() << (object->metaObject()->className() + idx) << '(' // NOLINT + << static_cast(object) << ", id="; + object->debugId(debug); + debug << ')'; + } + + return debug; +} + +PwBindableObjectRef::PwBindableObjectRef(PwBindableObject* object) { this->setObject(object); } + +PwBindableObjectRef::~PwBindableObjectRef() { this->setObject(nullptr); } + +void PwBindableObjectRef::setObject(PwBindableObject* object) { + if (this->mObject != nullptr) { + this->mObject->unref(); + QObject::disconnect(this->mObject, nullptr, this, nullptr); + } + + this->mObject = object; + + if (object != nullptr) { + this->mObject->ref(); + QObject::connect(object, &QObject::destroyed, this, &PwBindableObjectRef::onObjectDestroyed); + } +} + +void PwBindableObjectRef::onObjectDestroyed() { + // allow references to it so consumers can disconnect themselves + emit this->objectDestroyed(); + this->mObject = nullptr; +} + +void PwRegistry::init(PwCore& core) { + this->object = pw_core_get_registry(core.core, PW_VERSION_REGISTRY, 0); + pw_registry_add_listener(this->object, &this->listener.hook, &PwRegistry::EVENTS, this); +} + +const pw_registry_events PwRegistry::EVENTS = { + .version = PW_VERSION_REGISTRY_EVENTS, + .global = &PwRegistry::onGlobal, + .global_remove = &PwRegistry::onGlobalRemoved, +}; + +void PwRegistry::onGlobal( + void* data, + quint32 id, + quint32 permissions, + const char* type, + quint32 /*version*/, + const spa_dict* props +) { + auto* self = static_cast(data); + + if (strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) { + auto* meta = new PwMetadata(); + meta->init(self, id, permissions); + meta->initProps(props); + + self->metadata.emplace(id, meta); + meta->bind(); + } else if (strcmp(type, PW_TYPE_INTERFACE_Link) == 0) { + auto* link = new PwLink(); + link->init(self, id, permissions); + link->initProps(props); + + self->links.emplace(id, link); + self->addLinkToGroup(link); + emit self->linkAdded(link); + } else if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) { + auto* node = new PwNode(); + node->init(self, id, permissions); + node->initProps(props); + + self->nodes.emplace(id, node); + emit self->nodeAdded(node); + } +} + +void PwRegistry::onGlobalRemoved(void* data, quint32 id) { + auto* self = static_cast(data); + + if (auto* meta = self->metadata.value(id)) { + self->metadata.remove(id); + meta->safeDestroy(); + } else if (auto* link = self->links.value(id)) { + self->links.remove(id); + link->safeDestroy(); + } else if (auto* node = self->nodes.value(id)) { + self->nodes.remove(id); + node->safeDestroy(); + } +} + +void PwRegistry::addLinkToGroup(PwLink* link) { + for (auto* group: this->linkGroups) { + if (group->tryAddLink(link)) return; + } + + auto* group = new PwLinkGroup(link); + QObject::connect(group, &QObject::destroyed, this, &PwRegistry::onLinkGroupDestroyed); + this->linkGroups.push_back(group); + emit this->linkGroupAdded(group); +} + +void PwRegistry::onLinkGroupDestroyed(QObject* object) { + auto* group = static_cast(object); // NOLINT + this->linkGroups.removeOne(group); +} + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/registry.hpp b/src/services/pipewire/registry.hpp new file mode 100644 index 00000000..dab01af7 --- /dev/null +++ b/src/services/pipewire/registry.hpp @@ -0,0 +1,160 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core.hpp" + +namespace qs::service::pipewire { + +Q_DECLARE_LOGGING_CATEGORY(logRegistry); + +class PwRegistry; +class PwMetadata; +class PwNode; +class PwLink; +class PwLinkGroup; + +class PwBindableObject: public QObject { + Q_OBJECT; + +public: + PwBindableObject() = default; + ~PwBindableObject() override; + Q_DISABLE_COPY_MOVE(PwBindableObject); + + // constructors and destructors can't do virtual calls. + virtual void init(PwRegistry* registry, quint32 id, quint32 perms); + virtual void initProps(const spa_dict* /*props*/) {} + virtual void safeDestroy(); + + quint32 id = 0; + quint32 perms = 0; + + void debugId(QDebug& debug) const; + void ref(); + void unref(); + +signals: + // goes with safeDestroy + void destroying(PwBindableObject* self); + +protected: + virtual void bind(); + void unbind(); + virtual void bindHooks() {}; + virtual void unbindHooks() {}; + + quint32 refcount = 0; + pw_proxy* object = nullptr; + PwRegistry* registry = nullptr; +}; + +QDebug operator<<(QDebug debug, const PwBindableObject* object); + +template +class PwBindable: public PwBindableObject { +public: + T* proxy() { + return reinterpret_cast(this->object); // NOLINT + } + +protected: + void bind() override { + if (this->object != nullptr) return; + auto* object = + pw_registry_bind(this->registry->object, this->id, INTERFACE, VERSION, 0); // NOLINT + this->object = static_cast(object); + this->PwBindableObject::bind(); + } + + friend class PwRegistry; +}; + +class PwBindableObjectRef: public QObject { + Q_OBJECT; + +public: + explicit PwBindableObjectRef(PwBindableObject* object = nullptr); + ~PwBindableObjectRef() override; + Q_DISABLE_COPY_MOVE(PwBindableObjectRef); + +signals: + void objectDestroyed(); + +private slots: + void onObjectDestroyed(); + +protected: + void setObject(PwBindableObject* object); + + PwBindableObject* mObject = nullptr; +}; + +template +class PwBindableRef: public PwBindableObjectRef { +public: + explicit PwBindableRef(T* object = nullptr): PwBindableObjectRef(object) {} + + void setObject(T* object) { this->PwBindableObjectRef::setObject(object); } + + T* object() { return this->mObject; } +}; + +class PwRegistry + : public QObject + , public PwObject { + Q_OBJECT; + +public: + void init(PwCore& core); + + //QHash clients; + QHash metadata; + QHash nodes; + QHash links; + QVector linkGroups; + +signals: + void nodeAdded(PwNode* node); + void linkAdded(PwLink* link); + void linkGroupAdded(PwLinkGroup* group); + void metadataUpdate( + PwMetadata* owner, + quint32 subject, + const char* key, + const char* type, + const char* value + ); + +private slots: + void onLinkGroupDestroyed(QObject* object); + +private: + static const pw_registry_events EVENTS; + + static void onGlobal( + void* data, + quint32 id, + quint32 permissions, + const char* type, + quint32 version, + const spa_dict* props + ); + + static void onGlobalRemoved(void* data, quint32 id); + + void addLinkToGroup(PwLink* link); + + SpaHook listener; +}; + +} // namespace qs::service::pipewire diff --git a/src/services/status_notifier/module.md b/src/services/status_notifier/module.md index ff1e6208..ffe0b4cd 100644 --- a/src/services/status_notifier/module.md +++ b/src/services/status_notifier/module.md @@ -1,4 +1,4 @@ -name = "Quickshell.Service.SystemTray" +name = "Quickshell.Services.SystemTray" description = "Types for implementing a system tray" headers = [ "qml.hpp" ] ----- From 908ba3eef5f6a5b698e4e3cdfe85ed9c8f943e92 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 19 May 2024 02:32:43 -0700 Subject: [PATCH 005/305] hyprland/global_shortcuts: fix crash when protocol is not present --- src/services/pipewire/module.md | 8 ++++++++ src/wayland/hyprland/global_shortcuts/qml.cpp | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 src/services/pipewire/module.md diff --git a/src/services/pipewire/module.md b/src/services/pipewire/module.md new file mode 100644 index 00000000..e10809ef --- /dev/null +++ b/src/services/pipewire/module.md @@ -0,0 +1,8 @@ +name = "Quickshell.Services.PipeWire" +description = "Pipewire API" +headers = [ + "qml.hpp", + "link.hpp", + "node.hpp", +] +----- diff --git a/src/wayland/hyprland/global_shortcuts/qml.cpp b/src/wayland/hyprland/global_shortcuts/qml.cpp index ff957eaf..94423378 100644 --- a/src/wayland/hyprland/global_shortcuts/qml.cpp +++ b/src/wayland/hyprland/global_shortcuts/qml.cpp @@ -25,7 +25,7 @@ void GlobalShortcut::onPostReload() { } auto* manager = GlobalShortcutManager::instance(); - if (manager == nullptr) { + if (!manager->isActive()) { qWarning() << "The active compositor does not support hyprland_global_shortcuts_v1."; qWarning() << "GlobalShortcut will not work."; return; From 73cfeba61bdc8fcebad10e1888b7ffe23862638f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 20 May 2024 02:16:44 -0700 Subject: [PATCH 006/305] x11: add XPanelWindow --- CMakeLists.txt | 2 + README.md | 8 +- default.nix | 6 +- src/CMakeLists.txt | 6 +- src/core/panelinterface.hpp | 12 + src/core/proxywindow.cpp | 2 +- src/wayland/init.cpp | 16 +- src/wayland/wlr_layershell.cpp | 16 ++ src/wayland/wlr_layershell.hpp | 15 ++ src/x11/CMakeLists.txt | 22 ++ src/x11/init.cpp | 29 +++ src/x11/panel_window.cpp | 431 +++++++++++++++++++++++++++++++++ src/x11/panel_window.hpp | 160 ++++++++++++ src/x11/util.cpp | 55 +++++ src/x11/util.hpp | 29 +++ 15 files changed, 804 insertions(+), 5 deletions(-) create mode 100644 src/x11/CMakeLists.txt create mode 100644 src/x11/init.cpp create mode 100644 src/x11/panel_window.cpp create mode 100644 src/x11/panel_window.hpp create mode 100644 src/x11/util.cpp create mode 100644 src/x11/util.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 159acd49..0bf20ab4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ option(SOCKETS "Enable unix socket support" ON) option(WAYLAND "Enable wayland support" ON) option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON) option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON) +option(X11 "Enable X11 support" ON) option(HYPRLAND "Support hyprland specific features" ON) option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON) option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON) @@ -29,6 +30,7 @@ if (WAYLAND) message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}") message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}") endif () +message(STATUS " X11: ${X11}") message(STATUS " Services") message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") diff --git a/README.md b/README.md index d05e3347..c17af3a8 100644 --- a/README.md +++ b/README.md @@ -62,16 +62,22 @@ To build quickshell at all, you will need the following packages (names may vary - just - cmake -- pkg-config - ninja - Qt6 [ QtBase, QtDeclarative ] To build with wayland support you will additionally need: +- pkg-config - wayland - wayland-scanner (may be part of wayland on some distros) - wayland-protocols - Qt6 [ QtWayland ] +To build with x11 support you will additionally need: +- libxcb + +To build with pipewire support you will additionally need: +- libpipewire + ### Building To make a release build of quickshell run: diff --git a/default.nix b/default.nix index 514c7946..0985d843 100644 --- a/default.nix +++ b/default.nix @@ -10,6 +10,8 @@ qt6, wayland, wayland-protocols, + xorg, + pipewire, gitRev ? (let headExists = builtins.pathExists ./.git/HEAD; @@ -24,6 +26,7 @@ debug ? false, enableWayland ? true, + enableX11 ? true, enablePipewire ? true, nvidiaCompat ? false, svgSupport ? true, # you almost always want this @@ -42,11 +45,12 @@ wayland-scanner ]); - buildInputs = with pkgs; [ + buildInputs = [ qt6.qtbase qt6.qtdeclarative ] ++ (lib.optionals enableWayland [ qt6.qtwayland wayland ]) + ++ (lib.optionals enableX11 [ xorg.libxcb ]) ++ (lib.optionals svgSupport [ qt6.qtsvg ]) ++ (lib.optionals enablePipewire [ pipewire ]); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8fe9c651..be3adaf8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,10 @@ endif() if (WAYLAND) add_subdirectory(wayland) -endif () +endif() + +if (X11) + add_subdirectory(x11) +endif() add_subdirectory(services) diff --git a/src/core/panelinterface.hpp b/src/core/panelinterface.hpp index b46c25ca..e7ae0322 100644 --- a/src/core/panelinterface.hpp +++ b/src/core/panelinterface.hpp @@ -117,6 +117,10 @@ class PanelWindowInterface: public WindowInterface { Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged); /// Defaults to `ExclusionMode.Auto`. Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged); + /// If the panel should render above standard windows. Defaults to true. + QSDOC_HIDE Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY aboveWindowsChanged); + /// Defaults to false. + QSDOC_HIDE Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged); // clang-format on QSDOC_NAMED_ELEMENT(PanelWindow); @@ -135,9 +139,17 @@ public: [[nodiscard]] virtual ExclusionMode::Enum exclusionMode() const = 0; virtual void setExclusionMode(ExclusionMode::Enum exclusionMode) = 0; + [[nodiscard]] virtual bool aboveWindows() const = 0; + virtual void setAboveWindows(bool aboveWindows) = 0; + + [[nodiscard]] virtual bool focusable() const = 0; + virtual void setFocusable(bool focusable) = 0; + signals: void anchorsChanged(); void marginsChanged(); void exclusiveZoneChanged(); void exclusionModeChanged(); + void aboveWindowsChanged(); + void focusableChanged(); }; diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index e2a80a54..50370d9d 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -46,7 +46,7 @@ ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(); } void ProxyWindowBase::onReload(QObject* oldInstance) { this->window = this->retrieveWindow(oldInstance); auto wasVisible = this->window != nullptr && this->window->isVisible(); - if (this->window == nullptr) this->window = new QQuickWindow(); + if (this->window == nullptr) this->window = this->createQQuickWindow(); // 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 diff --git a/src/wayland/init.cpp b/src/wayland/init.cpp index 4a70de8d..194bad4c 100644 --- a/src/wayland/init.cpp +++ b/src/wayland/init.cpp @@ -1,5 +1,7 @@ #include +#include #include +#include #include "../core/plugin.hpp" @@ -10,7 +12,19 @@ namespace { class WaylandPlugin: public QuickshellPlugin { - bool applies() override { return QGuiApplication::platformName() == "wayland"; } + bool applies() override { + auto isWayland = QGuiApplication::platformName() == "wayland"; + + if (!isWayland && !qEnvironmentVariable("WAYLAND_DISPLAY").isEmpty()) { + qWarning() << "--- WARNING ---"; + qWarning() << "WAYLAND_DISPLAY is present but QT_QPA_PLATFORM is" + << QGuiApplication::platformName(); + qWarning() << "If you are actually running wayland, set QT_QPA_PLATFORM to \"wayland\" or " + "most functionality will be broken."; + } + + return isWayland; + } void registerTypes() override { #ifdef QS_WAYLAND_WLR_LAYERSHELL diff --git a/src/wayland/wlr_layershell.cpp b/src/wayland/wlr_layershell.cpp index dd1fee93..6a381c01 100644 --- a/src/wayland/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell.cpp @@ -114,6 +114,18 @@ void WlrLayershell::setAnchors(Anchors anchors) { if (!anchors.verticalConstraint()) this->ProxyWindowBase::setHeight(this->mHeight); } +bool WlrLayershell::aboveWindows() const { return this->layer() > WlrLayer::Bottom; } + +void WlrLayershell::setAboveWindows(bool aboveWindows) { + this->setLayer(aboveWindows ? WlrLayer::Top : WlrLayer::Bottom); +} + +bool WlrLayershell::focusable() const { return this->keyboardFocus() != WlrKeyboardFocus::None; } + +void WlrLayershell::setFocusable(bool focusable) { + this->setKeyboardFocus(focusable ? WlrKeyboardFocus::OnDemand : WlrKeyboardFocus::None); +} + QString WlrLayershell::ns() const { return this->ext->ns(); } void WlrLayershell::setNamespace(QString ns) { @@ -190,6 +202,8 @@ WaylandPanelInterface::WaylandPanelInterface(QObject* parent) QObject::connect(this->layer, &WlrLayershell::marginsChanged, this, &WaylandPanelInterface::marginsChanged); QObject::connect(this->layer, &WlrLayershell::exclusiveZoneChanged, this, &WaylandPanelInterface::exclusiveZoneChanged); QObject::connect(this->layer, &WlrLayershell::exclusionModeChanged, this, &WaylandPanelInterface::exclusionModeChanged); + QObject::connect(this->layer, &WlrLayershell::layerChanged, this, &WaylandPanelInterface::aboveWindowsChanged); + QObject::connect(this->layer, &WlrLayershell::keyboardFocusChanged, this, &WaylandPanelInterface::focusableChanged); // clang-format on } @@ -224,6 +238,8 @@ proxyPair(Anchors, anchors, setAnchors); proxyPair(Margins, margins, setMargins); proxyPair(qint32, exclusiveZone, setExclusiveZone); proxyPair(ExclusionMode::Enum, exclusionMode, setExclusionMode); +proxyPair(bool, focusable, setFocusable); +proxyPair(bool, aboveWindows, setAboveWindows); #undef proxyPair // NOLINTEND diff --git a/src/wayland/wlr_layershell.hpp b/src/wayland/wlr_layershell.hpp index 4a176bde..cf9abe4f 100644 --- a/src/wayland/wlr_layershell.hpp +++ b/src/wayland/wlr_layershell.hpp @@ -8,6 +8,7 @@ #include #include "../core/doc.hpp" +#include "../core/panelinterface.hpp" #include "../core/proxywindow.hpp" #include "wlr_layershell/window.hpp" @@ -54,6 +55,8 @@ class WlrLayershell: public ProxyWindowBase { QSDOC_HIDE Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged); QSDOC_HIDE Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged); QSDOC_HIDE Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged); + QSDOC_HIDE Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY layerChanged); + QSDOC_HIDE Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY keyboardFocusChanged); QML_ATTACHED(WlrLayershell); QML_ELEMENT; // clang-format on @@ -92,6 +95,12 @@ public: [[nodiscard]] Margins margins() const; void setMargins(Margins margins); // NOLINT + [[nodiscard]] bool aboveWindows() const; + void setAboveWindows(bool aboveWindows); + + [[nodiscard]] bool focusable() const; + void setFocusable(bool focusable); + static WlrLayershell* qmlAttachedProperties(QObject* object); signals: @@ -161,6 +170,12 @@ public: [[nodiscard]] ExclusionMode::Enum exclusionMode() const override; void setExclusionMode(ExclusionMode::Enum exclusionMode) override; + + [[nodiscard]] bool aboveWindows() const override; + void setAboveWindows(bool aboveWindows) override; + + [[nodiscard]] bool focusable() const override; + void setFocusable(bool focusable) override; // NOLINTEND private: diff --git a/src/x11/CMakeLists.txt b/src/x11/CMakeLists.txt new file mode 100644 index 00000000..2da30238 --- /dev/null +++ b/src/x11/CMakeLists.txt @@ -0,0 +1,22 @@ +find_package(XCB REQUIRED COMPONENTS XCB) + +qt_add_library(quickshell-x11 STATIC + util.cpp + panel_window.cpp +) + +qt_add_qml_module(quickshell-x11 + URI Quickshell.X11 + VERSION 0.1 +) + +add_library(quickshell-x11-init OBJECT init.cpp) + +target_link_libraries(quickshell-x11 PRIVATE ${QT_DEPS} ${XCB_LIBRARIES}) +target_link_libraries(quickshell-x11-init PRIVATE ${QT_DEPS} ${XCB_LIBRARIES}) + +qs_pch(quickshell-x11) +qs_pch(quickshell-x11plugin) +qs_pch(quickshell-x11-init) + +target_link_libraries(quickshell PRIVATE quickshell-x11plugin quickshell-x11-init) diff --git a/src/x11/init.cpp b/src/x11/init.cpp new file mode 100644 index 00000000..00080036 --- /dev/null +++ b/src/x11/init.cpp @@ -0,0 +1,29 @@ +#include +#include + +#include "../core/plugin.hpp" +#include "panel_window.hpp" +#include "util.hpp" + +namespace { + +class X11Plugin: public QuickshellPlugin { + bool applies() override { return QGuiApplication::platformName() == "xcb"; } + + void init() override { XAtom::initAtoms(); } + + void registerTypes() override { + qmlRegisterType("Quickshell._X11Overlay", 1, 0, "PanelWindow"); + + qmlRegisterModuleImport( + "Quickshell", + QQmlModuleImportModuleAny, + "Quickshell._X11Overlay", + QQmlModuleImportLatest + ); + } +}; + +QS_REGISTER_PLUGIN(X11Plugin); + +} // namespace diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp new file mode 100644 index 00000000..3a65ec92 --- /dev/null +++ b/src/x11/panel_window.cpp @@ -0,0 +1,431 @@ +#include "panel_window.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/generation.hpp" +#include "../core/panelinterface.hpp" +#include "../core/proxywindow.hpp" +#include "util.hpp" + +class XPanelStack { +public: + static XPanelStack* instance() { + static XPanelStack* stack = nullptr; // NOLINT + + if (stack == nullptr) { + stack = new XPanelStack(); + } + + return stack; + } + + [[nodiscard]] const QList& panels(XPanelWindow* panel) { + return this->mPanels[EngineGeneration::findObjectGeneration(panel)]; + } + + void addPanel(XPanelWindow* panel) { + auto& panels = this->mPanels[EngineGeneration::findObjectGeneration(panel)]; + if (!panels.contains(panel)) { + panels.push_back(panel); + } + } + + void removePanel(XPanelWindow* panel) { + auto& panels = this->mPanels[EngineGeneration::findObjectGeneration(panel)]; + if (panels.removeOne(panel)) { + if (panels.isEmpty()) { + this->mPanels.erase(EngineGeneration::findObjectGeneration(panel)); + } + + // from the bottom up, update all panels + for (auto* panel: panels) { + panel->updateDimensions(); + } + } + } + +private: + std::map> mPanels; +}; + +bool XPanelEventFilter::eventFilter(QObject* watched, QEvent* event) { + if (event->type() == QEvent::PlatformSurface) { + auto* surfaceEvent = static_cast(event); // NOLINT + + if (surfaceEvent->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) { + emit this->surfaceCreated(); + } + } + + return this->QObject::eventFilter(watched, event); +} + +XPanelWindow::XPanelWindow(QObject* parent): ProxyWindowBase(parent) { + QObject::connect( + &this->eventFilter, + &XPanelEventFilter::surfaceCreated, + this, + &XPanelWindow::xInit + ); +} + +XPanelWindow::~XPanelWindow() { XPanelStack::instance()->removePanel(this); } + +void XPanelWindow::connectWindow() { + this->ProxyWindowBase::connectWindow(); + + this->window->installEventFilter(&this->eventFilter); + this->connectScreen(); + // clang-format off + QObject::connect(this->window, &QQuickWindow::screenChanged, this, &XPanelWindow::connectScreen); + QObject::connect(this->window, &QQuickWindow::visibleChanged, this, &XPanelWindow::updatePanelStack); + // clang-format on + + // qt overwrites _NET_WM_STATE, so we have to use the qt api + // QXcbWindow::WindowType::Dock in qplatformwindow_p.h + // see QXcbWindow::setWindowFlags in qxcbwindow.cpp + this->window->setProperty("_q_xcb_wm_window_type", 0x000004); + + // at least one flag needs to change for the above property to apply + this->window->setFlag(Qt::FramelessWindowHint); + this->updateAboveWindows(); + this->updateFocusable(); + + if (this->window->handle() != nullptr) { + this->xInit(); + this->updatePanelStack(); + } +} + +void XPanelWindow::setWidth(qint32 width) { + this->mWidth = width; + + // only update the actual size if not blocked by anchors + if (!this->mAnchors.horizontalConstraint()) { + this->ProxyWindowBase::setWidth(width); + this->updateDimensions(); + } +} + +void XPanelWindow::setHeight(qint32 height) { + this->mHeight = height; + + // only update the actual size if not blocked by anchors + if (!this->mAnchors.verticalConstraint()) { + this->ProxyWindowBase::setHeight(height); + this->updateDimensions(); + } +} + +Anchors XPanelWindow::anchors() const { return this->mAnchors; } + +void XPanelWindow::setAnchors(Anchors anchors) { + if (this->mAnchors == anchors) return; + this->mAnchors = anchors; + this->updateDimensions(); + emit this->anchorsChanged(); +} + +qint32 XPanelWindow::exclusiveZone() const { return this->mExclusiveZone; } + +void XPanelWindow::setExclusiveZone(qint32 exclusiveZone) { + if (this->mExclusiveZone == exclusiveZone) return; + this->mExclusiveZone = exclusiveZone; + const bool wasNormal = this->mExclusionMode == ExclusionMode::Normal; + this->setExclusionMode(ExclusionMode::Normal); + if (wasNormal) this->updateStrut(); + emit this->exclusiveZoneChanged(); +} + +ExclusionMode::Enum XPanelWindow::exclusionMode() const { return this->mExclusionMode; } + +void XPanelWindow::setExclusionMode(ExclusionMode::Enum exclusionMode) { + if (this->mExclusionMode == exclusionMode) return; + this->mExclusionMode = exclusionMode; + this->updateStrut(); + emit this->exclusionModeChanged(); +} + +Margins XPanelWindow::margins() const { return this->mMargins; } + +void XPanelWindow::setMargins(Margins margins) { + if (this->mMargins == margins) return; + this->mMargins = margins; + this->updateDimensions(); + emit this->marginsChanged(); +} + +bool XPanelWindow::aboveWindows() const { return this->mAboveWindows; } + +void XPanelWindow::setAboveWindows(bool aboveWindows) { + if (this->mAboveWindows == aboveWindows) return; + this->mAboveWindows = aboveWindows; + this->updateAboveWindows(); + emit this->aboveWindowsChanged(); +} + +bool XPanelWindow::focusable() const { return this->mFocusable; } + +void XPanelWindow::setFocusable(bool focusable) { + if (this->mFocusable == focusable) return; + this->mFocusable = focusable; + this->updateFocusable(); + emit this->focusableChanged(); +} + +void XPanelWindow::xInit() { this->updateDimensions(); } + +void XPanelWindow::connectScreen() { + if (this->mTrackedScreen != nullptr) { + QObject::disconnect(this->mTrackedScreen, nullptr, this, nullptr); + } + + this->mTrackedScreen = this->window->screen(); + + if (this->mTrackedScreen != nullptr) { + QObject::connect( + this->mTrackedScreen, + &QScreen::geometryChanged, + this, + &XPanelWindow::updateDimensions + ); + } +} + +void XPanelWindow::updateDimensions() { + if (this->window == nullptr || this->window->handle() == nullptr) return; + + auto screenGeometry = this->window->screen()->virtualGeometry(); + + if (this->mExclusionMode != ExclusionMode::Ignore) { + for (auto* panel: XPanelStack::instance()->panels(this)) { + // we only care about windows below us + if (panel == this) break; + + int side = -1; + quint32 exclusiveZone = 0; + panel->getExclusion(side, exclusiveZone); + + if (exclusiveZone == 0) continue; + + auto zone = static_cast(exclusiveZone); + + screenGeometry.adjust( + side == 0 ? zone : 0, + side == 2 ? zone : 0, + side == 1 ? -zone : 0, + side == 3 ? -zone : 0 + ); + } + } + + auto geometry = QRect(); + + if (this->mAnchors.horizontalConstraint()) { + geometry.setX(screenGeometry.x() + this->mMargins.mLeft); + geometry.setWidth(screenGeometry.width() - this->mMargins.mLeft - this->mMargins.mRight); + } else { + if (this->mAnchors.mLeft) { + geometry.setX(screenGeometry.x() + this->mMargins.mLeft); + } else if (this->mAnchors.mRight) { + geometry.setX( + screenGeometry.x() + screenGeometry.width() - this->mWidth - this->mMargins.mRight + ); + } else { + geometry.setX(screenGeometry.x() + screenGeometry.width() / 2 - this->mWidth / 2); + } + + geometry.setWidth(this->mWidth); + } + + if (this->mAnchors.verticalConstraint()) { + geometry.setY(screenGeometry.y() + this->mMargins.mTop); + geometry.setHeight(screenGeometry.height() - this->mMargins.mTop - this->mMargins.mBottom); + } else { + if (this->mAnchors.mTop) { + geometry.setY(screenGeometry.y() + this->mMargins.mTop); + } else if (this->mAnchors.mBottom) { + geometry.setY( + screenGeometry.y() + screenGeometry.height() - this->mHeight - this->mMargins.mBottom + ); + } else { + geometry.setY(screenGeometry.y() + screenGeometry.height() / 2 - this->mHeight / 2); + } + + geometry.setHeight(this->mHeight); + } + + this->window->setGeometry(geometry); + this->updateStrut(); +} + +void XPanelWindow::updatePanelStack() { + if (this->window->isVisible()) { + XPanelStack::instance()->addPanel(this); + } else { + XPanelStack::instance()->removePanel(this); + } +} + +void XPanelWindow::getExclusion(int& side, quint32& exclusiveZone) { + if (this->mExclusionMode == ExclusionMode::Ignore) return; + + auto& anchors = this->mAnchors; + if (anchors.mLeft || anchors.mRight || anchors.mTop || anchors.mBottom) { + if (!anchors.horizontalConstraint() + && (anchors.verticalConstraint() || (!anchors.mTop && !anchors.mBottom))) + { + side = anchors.mLeft ? 0 : anchors.mRight ? 1 : -1; + } else if (!anchors.verticalConstraint() + && (anchors.horizontalConstraint() || (!anchors.mLeft && !anchors.mRight))) + { + side = anchors.mTop ? 2 : anchors.mBottom ? 3 : -1; + } + } + + if (side == -1) return; + + auto autoExclude = this->mExclusionMode == ExclusionMode::Auto; + + if (autoExclude) { + if (side == 0 || side == 1) { + exclusiveZone = this->mWidth + (side == 0 ? this->mMargins.mLeft : this->mMargins.mRight); + } else { + exclusiveZone = this->mHeight + (side == 2 ? this->mMargins.mTop : this->mMargins.mBottom); + } + } else { + exclusiveZone = this->mExclusiveZone; + } +} + +void XPanelWindow::updateStrut() { + if (this->window == nullptr || this->window->handle() == nullptr) return; + auto* conn = x11Connection(); + + int side = -1; + quint32 exclusiveZone = 0; + + this->getExclusion(side, exclusiveZone); + + if (side == -1 || this->mExclusionMode == ExclusionMode::Ignore) { + xcb_delete_property(conn, this->window->winId(), XAtom::_NET_WM_STRUT.atom()); + xcb_delete_property(conn, this->window->winId(), XAtom::_NET_WM_STRUT_PARTIAL.atom()); + return; + } + + auto data = std::array(); + data[side] = exclusiveZone; + + // https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm45573693101552 + // assuming "specified in root window coordinates" means relative to the window geometry + // in which case only the end position should be set, to the opposite extent. + data[side * 2 + 5] = side == 0 || side == 1 ? this->window->height() : this->window->width(); + + xcb_change_property( + conn, + XCB_PROP_MODE_REPLACE, + this->window->winId(), + XAtom::_NET_WM_STRUT.atom(), + XCB_ATOM_CARDINAL, + 32, + 4, + data.data() + ); + + xcb_change_property( + conn, + XCB_PROP_MODE_REPLACE, + this->window->winId(), + XAtom::_NET_WM_STRUT_PARTIAL.atom(), + XCB_ATOM_CARDINAL, + 32, + 12, + data.data() + ); +} + +void XPanelWindow::updateAboveWindows() { + if (this->window == nullptr) return; + + this->window->setFlag(Qt::WindowStaysOnBottomHint, !this->mAboveWindows); + this->window->setFlag(Qt::WindowStaysOnTopHint, this->mAboveWindows); +} + +void XPanelWindow::updateFocusable() { + if (this->window == nullptr) return; + this->window->setFlag(Qt::WindowDoesNotAcceptFocus, !this->mFocusable); +} + +// XPanelInterface + +XPanelInterface::XPanelInterface(QObject* parent) + : PanelWindowInterface(parent) + , panel(new XPanelWindow(this)) { + + // 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::heightChanged, this, &XPanelInterface::heightChanged); + QObject::connect(this->panel, &ProxyWindowBase::widthChanged, this, &XPanelInterface::widthChanged); + 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); + + // 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); + QObject::connect(this->panel, &XPanelWindow::exclusionModeChanged, this, &XPanelInterface::exclusionModeChanged); + QObject::connect(this->panel, &XPanelWindow::aboveWindowsChanged, this, &XPanelInterface::aboveWindowsChanged); + QObject::connect(this->panel, &XPanelWindow::focusableChanged, this, &XPanelInterface::focusableChanged); + // clang-format on +} + +void XPanelInterface::onReload(QObject* oldInstance) { + QQmlEngine::setContextForObject(this->panel, QQmlEngine::contextForObject(this)); + + auto* old = qobject_cast(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(); } + +// 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, width, setWidth); +proxyPair(qint32, height, setHeight); +proxyPair(QuickshellScreenInfo*, screen, setScreen); +proxyPair(QColor, color, setColor); +proxyPair(PendingRegion*, mask, setMask); + +// panel specific +proxyPair(Anchors, anchors, setAnchors); +proxyPair(Margins, margins, setMargins); +proxyPair(qint32, exclusiveZone, setExclusiveZone); +proxyPair(ExclusionMode::Enum, exclusionMode, setExclusionMode); +proxyPair(bool, focusable, setFocusable); +proxyPair(bool, aboveWindows, setAboveWindows); + +#undef proxyPair +// NOLINTEND diff --git a/src/x11/panel_window.hpp b/src/x11/panel_window.hpp new file mode 100644 index 00000000..db8de7d2 --- /dev/null +++ b/src/x11/panel_window.hpp @@ -0,0 +1,160 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../core/doc.hpp" +#include "../core/panelinterface.hpp" +#include "../core/proxywindow.hpp" + +class XPanelStack; + +class XPanelEventFilter: public QObject { + Q_OBJECT; + +public: + explicit XPanelEventFilter(QObject* parent = nullptr): QObject(parent) {} + +signals: + void surfaceCreated(); + +protected: + bool eventFilter(QObject* watched, QEvent* event) override; +}; + +class XPanelWindow: public ProxyWindowBase { + QSDOC_BASECLASS(PanelWindowInterface); + Q_OBJECT; + // clang-format off + QSDOC_HIDE Q_PROPERTY(Anchors anchors READ anchors WRITE setAnchors NOTIFY anchorsChanged); + QSDOC_HIDE Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged); + QSDOC_HIDE Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged); + QSDOC_HIDE Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged); + QSDOC_HIDE Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY aboveWindowsChanged); + QSDOC_HIDE Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged); + // clang-format on + QML_ELEMENT; + +public: + explicit XPanelWindow(QObject* parent = nullptr); + ~XPanelWindow() override; + Q_DISABLE_COPY_MOVE(XPanelWindow); + + void connectWindow() override; + + void setWidth(qint32 width) override; + void setHeight(qint32 height) override; + + [[nodiscard]] Anchors anchors() const; + void setAnchors(Anchors anchors); + + [[nodiscard]] qint32 exclusiveZone() const; + void setExclusiveZone(qint32 exclusiveZone); + + [[nodiscard]] ExclusionMode::Enum exclusionMode() const; + void setExclusionMode(ExclusionMode::Enum exclusionMode); + + [[nodiscard]] Margins margins() const; + void setMargins(Margins margins); + + [[nodiscard]] bool aboveWindows() const; + void setAboveWindows(bool aboveWindows); + + [[nodiscard]] bool focusable() const; + void setFocusable(bool focusable); + +signals: + QSDOC_HIDE void anchorsChanged(); + QSDOC_HIDE void exclusiveZoneChanged(); + QSDOC_HIDE void exclusionModeChanged(); + QSDOC_HIDE void marginsChanged(); + QSDOC_HIDE void aboveWindowsChanged(); + QSDOC_HIDE void focusableChanged(); + +private slots: + void xInit(); + void connectScreen(); + void updateDimensions(); + void updatePanelStack(); + +private: + void getExclusion(int& side, quint32& exclusiveZone); + void updateStrut(); + void updateAboveWindows(); + void updateFocusable(); + + QPointer mTrackedScreen = nullptr; + bool mAboveWindows = true; + bool mFocusable = false; + Anchors mAnchors; + Margins mMargins; + qint32 mExclusiveZone = 0; + ExclusionMode::Enum mExclusionMode = ExclusionMode::Auto; + XPanelEventFilter eventFilter; + + friend class XPanelStack; +}; + +class XPanelInterface: public PanelWindowInterface { + Q_OBJECT; + +public: + explicit XPanelInterface(QObject* parent = nullptr); + + 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 width() const override; + void setWidth(qint32 width) override; + + [[nodiscard]] qint32 height() const override; + void setHeight(qint32 height) 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]] QQmlListProperty data() override; + + // panel specific + + [[nodiscard]] Anchors anchors() const override; + void setAnchors(Anchors anchors) override; + + [[nodiscard]] Margins margins() const override; + void setMargins(Margins margins) override; + + [[nodiscard]] qint32 exclusiveZone() const override; + void setExclusiveZone(qint32 exclusiveZone) override; + + [[nodiscard]] ExclusionMode::Enum exclusionMode() const override; + void setExclusionMode(ExclusionMode::Enum exclusionMode) override; + + [[nodiscard]] bool aboveWindows() const override; + void setAboveWindows(bool aboveWindows) override; + + [[nodiscard]] bool focusable() const override; + void setFocusable(bool focusable) override; + // NOLINTEND + +private: + XPanelWindow* panel; + + friend class WlrLayershell; +}; diff --git a/src/x11/util.cpp b/src/x11/util.cpp new file mode 100644 index 00000000..8760ea30 --- /dev/null +++ b/src/x11/util.cpp @@ -0,0 +1,55 @@ +#include "util.hpp" + +#include +#include +#include +#include +#include + +xcb_connection_t* x11Connection() { + static xcb_connection_t* conn = nullptr; // NOLINT + + if (conn == nullptr) { + if (auto* x11Application = dynamic_cast(QGuiApplication::instance()) + ->nativeInterface()) + { + conn = x11Application->connection(); + } + } + + return conn; +} + +// NOLINTBEGIN +XAtom XAtom::_NET_WM_STRUT {}; +XAtom XAtom::_NET_WM_STRUT_PARTIAL {}; +// NOLINTEND + +void XAtom::initAtoms() { + _NET_WM_STRUT.init("_NET_WM_STRUT"); + _NET_WM_STRUT_PARTIAL.init("_NET_WM_STRUT_PARTIAL"); +} + +void XAtom::init(const QByteArray& name) { + this->cookie = xcb_intern_atom(x11Connection(), 0, name.length(), name.data()); +} + +bool XAtom::isValid() { + this->resolve(); + return this->mAtom != XCB_ATOM_NONE; +} + +const xcb_atom_t& XAtom::atom() { + this->resolve(); + return this->mAtom; +} + +void XAtom::resolve() { + if (!this->resolved) { + this->resolved = true; + + auto* reply = xcb_intern_atom_reply(x11Connection(), this->cookie, nullptr); + if (reply != nullptr) this->mAtom = reply->atom; + free(reply); // NOLINT + } +} diff --git a/src/x11/util.hpp b/src/x11/util.hpp new file mode 100644 index 00000000..da3f718a --- /dev/null +++ b/src/x11/util.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include +#include + +xcb_connection_t* x11Connection(); + +class XAtom { +public: + [[nodiscard]] bool isValid(); + [[nodiscard]] const xcb_atom_t& atom(); + + // NOLINTBEGIN + static XAtom _NET_WM_STRUT; + static XAtom _NET_WM_STRUT_PARTIAL; + // NOLINTEND + + static void initAtoms(); + +private: + void init(const QByteArray& name); + void resolve(); + + bool resolved = false; + xcb_atom_t mAtom = XCB_ATOM_NONE; + xcb_intern_atom_cookie_t cookie {}; +}; From 3b6d1c3bd874619eb7e6186a03338ee38e7fa593 Mon Sep 17 00:00:00 2001 From: kossLAN Date: Sun, 19 May 2024 21:09:16 -0400 Subject: [PATCH 007/305] feat: mpris --- CMakeLists.txt | 4 +- src/services/CMakeLists.txt | 4 + src/services/mpris/CMakeLists.txt | 47 +++++ .../mpris/org.mpris.MediaPlayer2.Player.xml | 33 ++++ src/services/mpris/org.mpris.MprisWatcher.xml | 16 ++ src/services/mpris/player.cpp | 87 +++++++++ src/services/mpris/player.hpp | 67 +++++++ src/services/mpris/qml.cpp | 180 ++++++++++++++++++ src/services/mpris/qml.hpp | 113 +++++++++++ src/services/mpris/watcher.cpp | 146 ++++++++++++++ src/services/mpris/watcher.hpp | 53 ++++++ 11 files changed, 749 insertions(+), 1 deletion(-) create mode 100644 src/services/mpris/CMakeLists.txt create mode 100644 src/services/mpris/org.mpris.MediaPlayer2.Player.xml create mode 100644 src/services/mpris/org.mpris.MprisWatcher.xml create mode 100644 src/services/mpris/player.cpp create mode 100644 src/services/mpris/player.hpp create mode 100644 src/services/mpris/qml.cpp create mode 100644 src/services/mpris/qml.hpp create mode 100644 src/services/mpris/watcher.cpp create mode 100644 src/services/mpris/watcher.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bf20ab4..2e5dffb7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON) option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON) option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) option(SERVICE_PIPEWIRE "PipeWire service" ON) +option(SERVICE_MPRIS "Mpris service" ON) message(STATUS "Quickshell configuration") message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}") @@ -34,6 +35,7 @@ message(STATUS " X11: ${X11}") message(STATUS " Services") message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") +message(STATUS " Mpris: ${SERVICE_MPRIS}") message(STATUS " Hyprland: ${HYPRLAND}") if (HYPRLAND) message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}") @@ -89,7 +91,7 @@ if (WAYLAND) list(APPEND QT_FPDEPS WaylandClient) endif() -if (SERVICE_STATUS_NOTIFIER) +if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS) set(DBUS ON) endif() diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt index 091a7ec6..4915762c 100644 --- a/src/services/CMakeLists.txt +++ b/src/services/CMakeLists.txt @@ -5,3 +5,7 @@ endif() if (SERVICE_PIPEWIRE) add_subdirectory(pipewire) endif() + +if (SERVICE_MPRIS) + add_subdirectory(mpris) +endif() diff --git a/src/services/mpris/CMakeLists.txt b/src/services/mpris/CMakeLists.txt new file mode 100644 index 00000000..ffe6d0ad --- /dev/null +++ b/src/services/mpris/CMakeLists.txt @@ -0,0 +1,47 @@ +qt_add_dbus_adaptor(DBUS_INTERFACES + org.mpris.MprisWatcher.xml + watcher.hpp + qs::service::mp::MprisWatcher + dbus_watcher + MprisWatcherAdaptor +) + +set_source_files_properties(org.mpris.MediaPlayer2.Player.xml PROPERTIES + CLASSNAME DBusMprisPlayer +) + +qt_add_dbus_interface(DBUS_INTERFACES + org.mpris.MediaPlayer2.Player.xml + dbus_player +) + +set_source_files_properties(org.mpris.MprisWatcher.xml PROPERTIES + CLASSNAME DBusMprisWatcher +) + +qt_add_dbus_interface(DBUS_INTERFACES + org.mpris.MprisWatcher.xml + dbus_watcher_interface +) + +qt_add_library(quickshell-service-mpris STATIC + qml.cpp + + watcher.cpp + player.cpp + ${DBUS_INTERFACES} +) + +# dbus headers +target_include_directories(quickshell-service-mpris PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +qt_add_qml_module(quickshell-service-mpris + URI Quickshell.Services.Mpris + VERSION 0.1 +) + +target_link_libraries(quickshell-service-mpris PRIVATE ${QT_DEPS} quickshell-dbus) +target_link_libraries(quickshell PRIVATE quickshell-service-mprisplugin) + +qs_pch(quickshell-service-mpris) +qs_pch(quickshell-service-mprisplugin) diff --git a/src/services/mpris/org.mpris.MediaPlayer2.Player.xml b/src/services/mpris/org.mpris.MediaPlayer2.Player.xml new file mode 100644 index 00000000..846c539c --- /dev/null +++ b/src/services/mpris/org.mpris.MediaPlayer2.Player.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/services/mpris/org.mpris.MprisWatcher.xml b/src/services/mpris/org.mpris.MprisWatcher.xml new file mode 100644 index 00000000..ab631558 --- /dev/null +++ b/src/services/mpris/org.mpris.MprisWatcher.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp new file mode 100644 index 00000000..b8676bad --- /dev/null +++ b/src/services/mpris/player.cpp @@ -0,0 +1,87 @@ +#include "player.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "dbus_player.h" + +using namespace qs::dbus; + +Q_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg); + +namespace qs::service::mp { + +MprisPlayer::MprisPlayer(const QString& address, QObject* parent) + : QObject(parent) + , watcherId(address) { + // qDBusRegisterMetaType(); + // qDBusRegisterMetaType(); + // qDBusRegisterMetaType(); + // spec is unclear about what exactly an item address is, so account for both + auto splitIdx = address.indexOf('/'); + auto conn = splitIdx == -1 ? address : address.sliced(0, splitIdx); + auto path = splitIdx == -1 ? "/org/mpris/MediaPlayer2" : address.sliced(splitIdx); + + this->player = new DBusMprisPlayer(conn, path, QDBusConnection::sessionBus(), this); + + if (!this->player->isValid()) { + qCWarning(logMprisPlayer).noquote() << "Cannot create MprisPlayer for" << conn; + return; + } + + // clang-format off + QObject::connect(&this->canControl, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->canGoNext, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->canGoPrevious, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->canPlay, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->canPause, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->metadata, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->playbackStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->position, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->minimumRate, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->maximumRate, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + + QObject::connect(&this->loopStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->rate, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->shuffle, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->volume, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + + QObject::connect(&this->properties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished); + // clang-format on + + this->properties.setInterface(this->player); + this->properties.updateAllViaGetAll(); +} + +bool MprisPlayer::isValid() const { return this->player->isValid(); } +bool MprisPlayer::isReady() const { return this->mReady; } + +void MprisPlayer::setPosition(QDBusObjectPath trackId, qlonglong position) { // NOLINT + this->player->SetPosition(trackId, position); +} +void MprisPlayer::next() { this->player->Next(); } +void MprisPlayer::previous() { this->player->Previous(); } +void MprisPlayer::pause() { this->player->Pause(); } +void MprisPlayer::playPause() { this->player->PlayPause(); } +void MprisPlayer::stop() { this->player->Stop(); } +void MprisPlayer::play() { this->player->Play(); } + +void MprisPlayer::onGetAllFinished() { + if (this->mReady) return; + this->mReady = true; + emit this->ready(); +} + +void MprisPlayer::updatePlayer() { // NOLINT + // TODO: emit signal here +} + +} // namespace qs::service::mp diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp new file mode 100644 index 00000000..168006a0 --- /dev/null +++ b/src/services/mpris/player.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "dbus_player.h" + +Q_DECLARE_LOGGING_CATEGORY(logMprisPlayer); + +namespace qs::service::mp { + +class MprisPlayer; + +class MprisPlayer: public QObject { + Q_OBJECT; + +public: + explicit MprisPlayer(const QString& address, QObject* parent = nullptr); + QString watcherId; // TODO: maybe can be private CHECK + + void setPosition(QDBusObjectPath trackId, qlonglong position); + void next(); + void previous(); + void pause(); + void playPause(); + void stop(); + void play(); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] bool isReady() const; + + // clang-format off + dbus::DBusPropertyGroup properties; + dbus::DBusProperty canControl {this->properties, "CanControl" }; + dbus::DBusProperty canGoNext {this->properties, "CanGoNext" }; + dbus::DBusProperty canGoPrevious {this->properties, "CanGoPrevious" }; + dbus::DBusProperty canPlay {this->properties, "CanPlay" }; + dbus::DBusProperty canPause {this->properties, "CanPause" }; + dbus::DBusProperty metadata {this->properties, "Metadata"}; + dbus::DBusProperty playbackStatus {this->properties, "PlaybackStatus" }; + dbus::DBusProperty position {this->properties, "Position" }; + dbus::DBusProperty minimumRate {this->properties, "MinimumRate" }; + dbus::DBusProperty maximumRate {this->properties, "MaximumRate" }; + + dbus::DBusProperty loopStatus {this->properties, "LoopStatus" }; + dbus::DBusProperty rate {this->properties, "Rate" }; + dbus::DBusProperty shuffle {this->properties, "Shuffle" }; + dbus::DBusProperty volume {this->properties, "Volume" }; + // clang-format on + +signals: + void ready(); + +private slots: + void onGetAllFinished(); + void updatePlayer(); + +private: + DBusMprisPlayer* player = nullptr; + bool mReady = false; +}; + +} // namespace qs::service::mp diff --git a/src/services/mpris/qml.cpp b/src/services/mpris/qml.cpp new file mode 100644 index 00000000..4e99a9f2 --- /dev/null +++ b/src/services/mpris/qml.cpp @@ -0,0 +1,180 @@ +#include "qml.hpp" + +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "player.hpp" +#include "watcher.hpp" + +using namespace qs::dbus; +using namespace qs::service::mp; + +Player::Player(qs::service::mp::MprisPlayer* player, QObject* parent) + : QObject(parent) + , player(player) { + + // clang-format off + QObject::connect(&this->player->canControl, &AbstractDBusProperty::changed, this, &Player::canControlChanged); + QObject::connect(&this->player->canGoNext, &AbstractDBusProperty::changed, this, &Player::canGoNextChanged); + QObject::connect(&this->player->canGoPrevious, &AbstractDBusProperty::changed, this, &Player::canGoPreviousChanged); + QObject::connect(&this->player->canPlay, &AbstractDBusProperty::changed, this, &Player::canPlayChanged); + QObject::connect(&this->player->canPause, &AbstractDBusProperty::changed, this, &Player::canPauseChanged); + QObject::connect(&this->player->metadata, &AbstractDBusProperty::changed, this, &Player::metadataChanged); + QObject::connect(&this->player->playbackStatus, &AbstractDBusProperty::changed, this, &Player::playbackStatusChanged); + QObject::connect(&this->player->position, &AbstractDBusProperty::changed, this, &Player::positionChanged); + QObject::connect(&this->player->minimumRate, &AbstractDBusProperty::changed, this, &Player::positionChanged); + QObject::connect(&this->player->maximumRate, &AbstractDBusProperty::changed, this, &Player::positionChanged); + + QObject::connect(&this->player->loopStatus, &AbstractDBusProperty::changed, this, &Player::loopStatusChanged); + QObject::connect(&this->player->rate, &AbstractDBusProperty::changed, this, &Player::rateChanged); + QObject::connect(&this->player->shuffle, &AbstractDBusProperty::changed, this, &Player::shuffleChanged); + QObject::connect(&this->player->volume, &AbstractDBusProperty::changed, this, &Player::volumeChanged); + // clang-format on +} + +bool Player::canControl() const { + if (this->player == nullptr) return false; + return this->player->canControl.get(); +} + +bool Player::canGoNext() const { + if (this->player == nullptr) return false; + return this->player->canGoNext.get(); +} + +bool Player::canGoPrevious() const { + if (this->player == nullptr) return false; + return this->player->canGoPrevious.get(); +} + +bool Player::canPlay() const { + if (this->player == nullptr) return false; + return this->player->canPlay.get(); +} + +bool Player::canPause() const { + if (this->player == nullptr) return false; + return this->player->canPause.get(); +} + +QVariantMap Player::metadata() const { + if (this->player == nullptr) return {}; + return this->player->metadata.get(); +} + +QString Player::playbackStatus() const { + if (this->player == nullptr) return ""; + + if (this->player->playbackStatus.get().isEmpty()) return "Unsupported"; + return this->player->playbackStatus.get(); +} + +qlonglong Player::position() const { + if (this->player == nullptr) return 0; + return this->player->position.get(); +} + +double Player::minimumRate() const { + if (this->player == nullptr) return 0.0; + return this->player->minimumRate.get(); +} + +double Player::maximumRate() const { + if (this->player == nullptr) return 0.0; + return this->player->maximumRate.get(); +} + +QString Player::loopStatus() const { + if (this->player == nullptr) return ""; + + if (this->player->loopStatus.get().isEmpty()) return "Unsupported"; + return this->player->loopStatus.get(); +} + +double Player::rate() const { + if (this->player == nullptr) return 0.0; + return this->player->rate.get(); +} + +bool Player::shuffle() const { + if (this->player == nullptr) return false; + return this->player->shuffle.get(); +} + +double Player::volume() const { + if (this->player == nullptr) return 0.0; + return this->player->volume.get(); +} + +// NOLINTBEGIN +void Player::setPosition(QDBusObjectPath trackId, qlonglong position) const { + this->player->setPosition(trackId, position); +} +void Player::next() const { this->player->next(); } +void Player::previous() const { this->player->previous(); } +void Player::pause() const { this->player->pause(); } +void Player::playPause() const { this->player->playPause(); } +void Player::stop() const { this->player->stop(); } +void Player::play() const { this->player->play(); } +// NOLINTEND + +Mpris::Mpris(QObject* parent): QObject(parent) { + auto* watcher = MprisWatcher::instance(); + + // clang-format off + QObject::connect(watcher, &MprisWatcher::MprisPlayerRegistered, this, &Mpris::onPlayerRegistered); + QObject::connect(watcher, &MprisWatcher::MprisPlayerUnregistered, this, &Mpris::onPlayerUnregistered); + // clang-format on + + for (QString& player: watcher->players) { + this->mPlayers.push_back(new Player(new MprisPlayer(player), this)); + } +} + +void Mpris::onPlayerRegistered(const QString& service) { + this->mPlayers.push_back(new Player(new MprisPlayer(service), this)); + emit this->playersChanged(); +} + +void Mpris::onPlayerUnregistered(const QString& service) { + Player* mprisPlayer = nullptr; + MprisPlayer* player = playerWithAddress(players(), service)->player; + + this->mPlayers.removeIf([player, &mprisPlayer](Player* testPlayer) { + if (testPlayer->player == player) { + mprisPlayer = testPlayer; + return true; + } else return false; + }); + + emit this->playersChanged(); + + delete mprisPlayer->player; + delete mprisPlayer; +} + +QQmlListProperty Mpris::players() { + return QQmlListProperty(this, nullptr, &Mpris::playersCount, &Mpris::playerAt); +} + +qsizetype Mpris::playersCount(QQmlListProperty* property) { + return reinterpret_cast(property->object)->mPlayers.count(); // NOLINT +} + +Player* Mpris::playerAt(QQmlListProperty* property, qsizetype index) { + return reinterpret_cast(property->object)->mPlayers.at(index); // NOLINT +} + +Player* Mpris::playerWithAddress(QQmlListProperty property, const QString& address) { + for (Player* player: reinterpret_cast(property.object)->mPlayers) { // NOLINT + if (player->player->watcherId == address) { + return player; + } + } + + return nullptr; +} diff --git a/src/services/mpris/qml.hpp b/src/services/mpris/qml.hpp new file mode 100644 index 00000000..4e7896b0 --- /dev/null +++ b/src/services/mpris/qml.hpp @@ -0,0 +1,113 @@ +#pragma once + +#include +#include +#include +#include + +#include "player.hpp" + + + +///! Mpris implementation for quickshell +/// mpris service, get useful information from apps that implement media player fucntionality [mpris spec] +/// (Beware of misuse of spec, it is just a suggestion for most) +/// +/// +/// [mpris spec]: https://specifications.freedesktop.org/mpris-spec +class Player: public QObject { + Q_OBJECT; + // READ-ONLY + Q_PROPERTY(bool canControl READ canControl NOTIFY canControlChanged); + Q_PROPERTY(bool canGoNext READ canGoNext NOTIFY canGoNextChanged); + Q_PROPERTY(bool canGoPrevious READ canGoPrevious NOTIFY canGoPreviousChanged); + Q_PROPERTY(bool canPlay READ canPlay NOTIFY canPlayChanged); + Q_PROPERTY(bool canPause READ canPause NOTIFY canPauseChanged); + Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged); + Q_PROPERTY(QString playbackStatus READ playbackStatus NOTIFY playbackStatusChanged); + Q_PROPERTY(qlonglong position READ position NOTIFY positionChanged); + Q_PROPERTY(double minimumRate READ minimumRate NOTIFY minimumRateChanged); + Q_PROPERTY(double maximumRate READ maximumRate NOTIFY maximumRateChanged); + + // READ/WRITE - Write isn't implemented thus this need to fixed when that happens. + Q_PROPERTY(QString loopStatus READ loopStatus NOTIFY loopStatusChanged); + Q_PROPERTY(double rate READ rate NOTIFY rateChanged); + Q_PROPERTY(bool shuffle READ shuffle NOTIFY shuffleChanged); + Q_PROPERTY(double volume READ volume NOTIFY volumeChanged); + + QML_ELEMENT; + QML_UNCREATABLE("MprisPlayers can only be acquired from Mpris"); + +public: + explicit Player(qs::service::mp::MprisPlayer* player, QObject* parent = nullptr); + + // These are all self-explanatory. + Q_INVOKABLE void setPosition(QDBusObjectPath trackId, qlonglong position) const; + Q_INVOKABLE void next() const; + Q_INVOKABLE void previous() const; + Q_INVOKABLE void pause() const; + Q_INVOKABLE void playPause() const; + Q_INVOKABLE void stop() const; + Q_INVOKABLE void play() const; + + [[nodiscard]] bool canControl() const; + [[nodiscard]] bool canGoNext() const; + [[nodiscard]] bool canGoPrevious() const; + [[nodiscard]] bool canPlay() const; + [[nodiscard]] bool canPause() const; + [[nodiscard]] QVariantMap metadata() const; + [[nodiscard]] QString playbackStatus() const; + [[nodiscard]] qlonglong position() const; + [[nodiscard]] double minimumRate() const; + [[nodiscard]] double maximumRate() const; + + [[nodiscard]] QString loopStatus() const; + [[nodiscard]] double rate() const; + [[nodiscard]] bool shuffle() const; + [[nodiscard]] double volume() const; + + qs::service::mp::MprisPlayer* player = nullptr; + +signals: + void canControlChanged(); + void canGoNextChanged(); + void canGoPreviousChanged(); + void canPlayChanged(); + void canPauseChanged(); + void metadataChanged(); + void playbackStatusChanged(); + void positionChanged(); + void minimumRateChanged(); + void maximumRateChanged(); + + void loopStatusChanged(); + void rateChanged(); + void shuffleChanged(); + void volumeChanged(); +}; + +class Mpris: public QObject { + Q_OBJECT; + Q_PROPERTY(QQmlListProperty players READ players NOTIFY playersChanged); + QML_ELEMENT; + QML_SINGLETON; + +public: + explicit Mpris(QObject* parent = nullptr); + + [[nodiscard]] QQmlListProperty players(); + +signals: + void playersChanged(); + +private slots: + void onPlayerRegistered(const QString& service); + void onPlayerUnregistered(const QString& service); + +private: + static qsizetype playersCount(QQmlListProperty* property); + static Player* playerAt(QQmlListProperty* property, qsizetype index); + static Player* playerWithAddress(QQmlListProperty property, const QString& address); + + QList mPlayers; +}; diff --git a/src/services/mpris/watcher.cpp b/src/services/mpris/watcher.cpp new file mode 100644 index 00000000..2a735f74 --- /dev/null +++ b/src/services/mpris/watcher.cpp @@ -0,0 +1,146 @@ +#include "watcher.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mp.watcher", QtWarningMsg); + +namespace qs::service::mp { + +MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) { + new MprisWatcherAdaptor(this); + + qCDebug(logMprisWatcher) << "Starting MprisWatcher"; + + auto bus = QDBusConnection::sessionBus(); + + if (!bus.isConnected()) { + qCWarning(logMprisWatcher) << "Could not connect to DBus. Mpris service will not work."; + return; + } + + if (!bus.registerObject("/MprisWatcher", this)) { + qCWarning(logMprisWatcher) << "Could not register MprisWatcher object with " + "DBus. Mpris service will not work."; + return; + } + + // clang-format off + QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, &MprisWatcher::onServiceRegistered); + QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &MprisWatcher::onServiceUnregistered); + + this->serviceWatcher.setWatchMode(QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration); + // clang-format on + + this->serviceWatcher.addWatchedService("org.mpris.MediaPlayer2*"); + this->serviceWatcher.addWatchedService("org.mpris.MprisWatcher"); + this->serviceWatcher.setConnection(bus); + + this->tryRegister(); +} + +void MprisWatcher::tryRegister() { // NOLINT + auto bus = QDBusConnection::sessionBus(); + auto success = bus.registerService("org.mpris.MprisWatcher"); + + if (success) { + qCDebug(logMprisWatcher) << "Registered watcher at org.mpris.MprisWatcher"; + emit this->MprisWatcherRegistered(); + registerExisting(bus); // Register services that already existed before creation. + } else { + qCDebug(logMprisWatcher) << "Could not register watcher at " + "org.mpris.MprisWatcher, presumably because one is " + "already registered."; + qCDebug(logMprisWatcher + ) << "Registration will be attempted again if the active service is unregistered."; + } +} + +void MprisWatcher::registerExisting(const QDBusConnection& connection) { + QStringList list = connection.interface()->registeredServiceNames(); + for (const QString& service: list) { + if (service.contains("org.mpris.MediaPlayer2")) { + qCDebug(logMprisWatcher).noquote() << "Found Mpris service" << service; + RegisterMprisPlayer(service); + } + } +} + +void MprisWatcher::onServiceRegistered(const QString& service) { + if (service == "org.mpris.MprisWatcher") { + qCDebug(logMprisWatcher) << "MprisWatcher"; + return; + } else if (service.contains("org.mpris.MediaPlayer2")) { + qCDebug(logMprisWatcher).noquote() << "Mpris service " << service << " registered."; + RegisterMprisPlayer(service); + } else { + qCWarning(logMprisWatcher) << "Got a registration event for a untracked service"; + } +} + +// TODO: This is getting triggered twice on unregistration, investigate. +void MprisWatcher::onServiceUnregistered(const QString& service) { + if (service == "org.mpris.MprisWatcher") { + qCDebug(logMprisWatcher) << "Active MprisWatcher unregistered, attempting registration"; + this->tryRegister(); + return; + } else { + QString qualifiedPlayer; + this->players.removeIf([&](const QString& player) { + if (QString::compare(player, service) == 0) { + qualifiedPlayer = player; + return true; + } else return false; + }); + + if (!qualifiedPlayer.isEmpty()) { + qCDebug(logMprisWatcher).noquote() + << "Unregistered MprisPlayer" << qualifiedPlayer << "from watcher"; + + emit this->MprisPlayerUnregistered(qualifiedPlayer); + } else { + qCWarning(logMprisWatcher).noquote() + << "Got service unregister event for untracked service" << service; + } + } + + this->serviceWatcher.removeWatchedService(service); +} + +QList MprisWatcher::registeredPlayers() const { return this->players; } + +void MprisWatcher::RegisterMprisPlayer(const QString& player) { + if (this->players.contains(player)) { + qCDebug(logMprisWatcher).noquote() + << "Skipping duplicate registration of MprisPlayer" << player << "to watcher"; + return; + } + + if (!QDBusConnection::sessionBus().interface()->serviceOwner(player).isValid()) { + qCWarning(logMprisWatcher).noquote() + << "Ignoring invalid MprisPlayer registration of" << player << "to watcher"; + return; + } + + this->serviceWatcher.addWatchedService(player); + this->players.push_back(player); + + qCDebug(logMprisWatcher).noquote() << "Registered MprisPlayer" << player << "to watcher"; + + emit this->MprisPlayerRegistered(player); +} + +MprisWatcher* MprisWatcher::instance() { + static MprisWatcher* instance = nullptr; // NOLINT + if (instance == nullptr) instance = new MprisWatcher(); + return instance; +} + +} // namespace qs::service::mp diff --git a/src/services/mpris/watcher.hpp b/src/services/mpris/watcher.hpp new file mode 100644 index 00000000..b3e0bdeb --- /dev/null +++ b/src/services/mpris/watcher.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(logMprisWatcher); + +namespace qs::service::mp { + +class MprisWatcher + : public QObject + , protected QDBusContext { + Q_OBJECT; + Q_PROPERTY(qint32 ProtocolVersion READ protocolVersion); + Q_PROPERTY(QList RegisteredMprisPlayers READ registeredPlayers); + +public: + explicit MprisWatcher(QObject* parent = nullptr); + + void tryRegister(); + void registerExisting(const QDBusConnection &connection); + + [[nodiscard]] qint32 protocolVersion() const { return 0; } // NOLINT + [[nodiscard]] QList registeredPlayers() const; + + // NOLINTBEGIN + void RegisterMprisPlayer(const QString& player); + // NOLINTEND + + static MprisWatcher* instance(); + QList players; + +signals: + // NOLINTBEGIN + void MprisWatcherRegistered(); + void MprisPlayerRegistered(const QString& service); + void MprisPlayerUnregistered(const QString& service); + // NOLINTEND + +private slots: + void onServiceRegistered(const QString& service); + void onServiceUnregistered(const QString& service); + +private: + QDBusServiceWatcher serviceWatcher; +}; + +} // namespace qs::service::mp From 4ee9ac7f7caa79e08ab62df65c8fc980633371b1 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 21 May 2024 04:05:15 -0700 Subject: [PATCH 008/305] service/mpris: finish mpris implementation --- CMakeLists.txt | 2 +- src/dbus/dbusmenu/dbusmenu.hpp | 4 +- src/dbus/properties.cpp | 58 ++- src/dbus/properties.hpp | 37 +- src/services/mpris/CMakeLists.txt | 24 +- src/services/mpris/module.md | 7 + .../mpris/org.mpris.MediaPlayer2.Player.xml | 47 +- src/services/mpris/org.mpris.MediaPlayer2.xml | 6 + src/services/mpris/org.mpris.MprisWatcher.xml | 16 - src/services/mpris/player.cpp | 440 ++++++++++++++++-- src/services/mpris/player.hpp | 339 ++++++++++++-- src/services/mpris/qml.cpp | 180 ------- src/services/mpris/qml.hpp | 113 ----- src/services/mpris/watcher.cpp | 154 +++--- src/services/mpris/watcher.hpp | 54 +-- src/services/status_notifier/item.hpp | 8 +- 16 files changed, 911 insertions(+), 578 deletions(-) create mode 100644 src/services/mpris/module.md create mode 100644 src/services/mpris/org.mpris.MediaPlayer2.xml delete mode 100644 src/services/mpris/org.mpris.MprisWatcher.xml delete mode 100644 src/services/mpris/qml.cpp delete mode 100644 src/services/mpris/qml.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e5dffb7..2d17758b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,7 +35,7 @@ message(STATUS " X11: ${X11}") message(STATUS " Services") message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") -message(STATUS " Mpris: ${SERVICE_MPRIS}") +message(STATUS " Mpris: ${SERVICE_MPRIS}") message(STATUS " Hyprland: ${HYPRLAND}") if (HYPRLAND) message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}") diff --git a/src/dbus/dbusmenu/dbusmenu.hpp b/src/dbus/dbusmenu/dbusmenu.hpp index b07919ad..ab485c45 100644 --- a/src/dbus/dbusmenu/dbusmenu.hpp +++ b/src/dbus/dbusmenu/dbusmenu.hpp @@ -188,9 +188,9 @@ public: dbus::DBusPropertyGroup properties; dbus::DBusProperty version {this->properties, "Version"}; - dbus::DBusProperty textDirection {this->properties, "TextDirection"}; + dbus::DBusProperty textDirection {this->properties, "TextDirection", "", false}; dbus::DBusProperty status {this->properties, "Status"}; - dbus::DBusProperty iconThemePath {this->properties, "IconThemePath"}; + dbus::DBusProperty iconThemePath {this->properties, "IconThemePath", {}, false}; void prepareToShow(qint32 item, bool sendOpened); void updateLayout(qint32 parent, qint32 depth); diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index 1e5e0bd1..7dac84ab 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -112,6 +112,8 @@ void asyncReadPropertyInternal( } void AbstractDBusProperty::tryUpdate(const QVariant& variant) { + this->mExists = true; + auto error = this->read(variant); if (error.isValid()) { qCWarning(logDbusProperties).noquote() @@ -159,6 +161,44 @@ void AbstractDBusProperty::update() { } } +void AbstractDBusProperty::write() { + if (this->group == nullptr) { + qFatal(logDbusProperties) << "Tried to write dbus property" << this->name + << "which is not attached to a group"; + } else { + const QString propStr = this->toString(); + + if (this->group->interface == nullptr) { + qFatal(logDbusProperties).noquote() + << "Tried to write property" << propStr << "of a disconnected interface"; + } + + qCDebug(logDbusProperties).noquote() << "Writing property" << propStr; + + auto pendingCall = this->group->propertyInterface->Set( + this->group->interface->interface(), + this->name, + QDBusVariant(this->serialize()) + ); + + auto* call = new QDBusPendingCallWatcher(pendingCall, this); + + auto responseCallback = [propStr](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + + if (reply.isError()) { + qCWarning(logDbusProperties).noquote() << "Error writing property" << propStr; + qCWarning(logDbusProperties) << reply.error(); + } + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); + } +} + +bool AbstractDBusProperty::exists() const { return this->mExists; } + QString AbstractDBusProperty::toString() const { const QString group = this->group == nullptr ? "{ NO GROUP }" : this->group->toString(); return group + ':' + this->name; @@ -232,7 +272,7 @@ void DBusPropertyGroup::updateAllViaGetAll() { } else { qCDebug(logDbusProperties).noquote() << "Received GetAll property set for" << this->toString(); - this->updatePropertySet(reply.value()); + this->updatePropertySet(reply.value(), true); } delete call; @@ -242,7 +282,7 @@ void DBusPropertyGroup::updateAllViaGetAll() { QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); } -void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties) { +void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool complainMissing) { for (const auto [name, value]: properties.asKeyValueRange()) { auto prop = std::find_if( this->properties.begin(), @@ -251,11 +291,21 @@ void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties) { ); if (prop == this->properties.end()) { - qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" << this; + qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" + << this->toString(); } else { (*prop)->tryUpdate(value); } } + + if (complainMissing) { + for (const auto* prop: this->properties) { + if (prop->required && !properties.contains(prop->name)) { + qCWarning(logDbusProperties) + << prop->name << "missing from property set for" << this->toString(); + } + } + } } QString DBusPropertyGroup::toString() const { @@ -291,7 +341,7 @@ void DBusPropertyGroup::onPropertiesChanged( } } - this->updatePropertySet(changedProperties); + this->updatePropertySet(changedProperties, false); } } // namespace qs::dbus diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index 3aac07f6..e24d23fb 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -79,22 +79,31 @@ class AbstractDBusProperty: public QObject { Q_OBJECT; public: - explicit AbstractDBusProperty(QString name, const QMetaType& type, QObject* parent = nullptr) + explicit AbstractDBusProperty( + QString name, + const QMetaType& type, + bool required, + QObject* parent = nullptr + ) : QObject(parent) , name(std::move(name)) - , type(type) {} + , type(type) + , required(required) {} + [[nodiscard]] bool exists() const; [[nodiscard]] QString toString() const; [[nodiscard]] virtual QString valueString() = 0; public slots: void update(); + void write(); signals: void changed(); protected: virtual QDBusError read(const QVariant& variant) = 0; + virtual QVariant serialize() = 0; private: void tryUpdate(const QVariant& variant); @@ -103,6 +112,8 @@ private: QString name; QMetaType type; + bool required; + bool mExists = false; friend class DBusPropertyGroup; }; @@ -133,7 +144,7 @@ private slots: ); private: - void updatePropertySet(const QVariantMap& properties); + void updatePropertySet(const QVariantMap& properties, bool complainMissing); DBusPropertiesInterface* propertyInterface = nullptr; QDBusAbstractInterface* interface = nullptr; @@ -145,17 +156,23 @@ private: template class DBusProperty: public AbstractDBusProperty { public: - explicit DBusProperty(QString name, QObject* parent = nullptr, T value = T()) - : AbstractDBusProperty(std::move(name), QMetaType::fromType(), parent) + explicit DBusProperty( + QString name, + T value = T(), + bool required = true, + QObject* parent = nullptr + ) + : AbstractDBusProperty(std::move(name), QMetaType::fromType(), required, parent) , value(std::move(value)) {} explicit DBusProperty( DBusPropertyGroup& group, QString name, - QObject* parent = nullptr, - T value = T() + T value = T(), + bool required = true, + QObject* parent = nullptr ) - : DBusProperty(std::move(name), parent, std::move(value)) { + : DBusProperty(std::move(name), std::move(value), required, parent) { group.attachProperty(this); } @@ -165,7 +182,7 @@ public: return str; } - [[nodiscard]] T get() const { return this->value; } + [[nodiscard]] const T& get() const { return this->value; } void set(T value) { this->value = std::move(value); @@ -183,6 +200,8 @@ protected: return result.error; } + QVariant serialize() override { return QVariant::fromValue(this->value); } + private: T value; diff --git a/src/services/mpris/CMakeLists.txt b/src/services/mpris/CMakeLists.txt index ffe6d0ad..3ee96061 100644 --- a/src/services/mpris/CMakeLists.txt +++ b/src/services/mpris/CMakeLists.txt @@ -1,13 +1,6 @@ -qt_add_dbus_adaptor(DBUS_INTERFACES - org.mpris.MprisWatcher.xml - watcher.hpp - qs::service::mp::MprisWatcher - dbus_watcher - MprisWatcherAdaptor -) - set_source_files_properties(org.mpris.MediaPlayer2.Player.xml PROPERTIES - CLASSNAME DBusMprisPlayer + CLASSNAME DBusMprisPlayer + NO_NAMESPACE TRUE ) qt_add_dbus_interface(DBUS_INTERFACES @@ -15,20 +8,19 @@ qt_add_dbus_interface(DBUS_INTERFACES dbus_player ) -set_source_files_properties(org.mpris.MprisWatcher.xml PROPERTIES - CLASSNAME DBusMprisWatcher +set_source_files_properties(org.mpris.MediaPlayer2.xml PROPERTIES + CLASSNAME DBusMprisPlayerApp + NO_NAMESPACE TRUE ) qt_add_dbus_interface(DBUS_INTERFACES - org.mpris.MprisWatcher.xml - dbus_watcher_interface + org.mpris.MediaPlayer2.xml + dbus_player_app ) qt_add_library(quickshell-service-mpris STATIC - qml.cpp - - watcher.cpp player.cpp + watcher.cpp ${DBUS_INTERFACES} ) diff --git a/src/services/mpris/module.md b/src/services/mpris/module.md new file mode 100644 index 00000000..e2256e8c --- /dev/null +++ b/src/services/mpris/module.md @@ -0,0 +1,7 @@ +name = "Quickshell.Services.Mpris" +description = "Mpris Service" +headers = [ + "player.hpp", + "watcher.hpp", +] +----- diff --git a/src/services/mpris/org.mpris.MediaPlayer2.Player.xml b/src/services/mpris/org.mpris.MediaPlayer2.Player.xml index 846c539c..a0095231 100644 --- a/src/services/mpris/org.mpris.MediaPlayer2.Player.xml +++ b/src/services/mpris/org.mpris.MediaPlayer2.Player.xml @@ -1,33 +1,24 @@ - - - - - - - - - - - - - - - - - - - - + + + - - + + + - - - - - - + + + + + + + + + + + + + diff --git a/src/services/mpris/org.mpris.MediaPlayer2.xml b/src/services/mpris/org.mpris.MediaPlayer2.xml new file mode 100644 index 00000000..fa880d08 --- /dev/null +++ b/src/services/mpris/org.mpris.MediaPlayer2.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/services/mpris/org.mpris.MprisWatcher.xml b/src/services/mpris/org.mpris.MprisWatcher.xml deleted file mode 100644 index ab631558..00000000 --- a/src/services/mpris/org.mpris.MprisWatcher.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index b8676bad..3b0c7463 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -1,87 +1,427 @@ #include "player.hpp" +#include +#include +#include #include -#include #include #include -#include #include -#include #include +#include #include #include "../../dbus/properties.hpp" #include "dbus_player.h" +#include "dbus_player_app.h" using namespace qs::dbus; +namespace qs::service::mpris { + Q_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg); -namespace qs::service::mp { +QString MprisPlaybackState::toString(MprisPlaybackState::Enum status) { + switch (status) { + case MprisPlaybackState::Stopped: return "Stopped"; + case MprisPlaybackState::Playing: return "Playing"; + case MprisPlaybackState::Paused: return "Paused"; + default: return "Unknown Status"; + } +} -MprisPlayer::MprisPlayer(const QString& address, QObject* parent) - : QObject(parent) - , watcherId(address) { - // qDBusRegisterMetaType(); - // qDBusRegisterMetaType(); - // qDBusRegisterMetaType(); - // spec is unclear about what exactly an item address is, so account for both - auto splitIdx = address.indexOf('/'); - auto conn = splitIdx == -1 ? address : address.sliced(0, splitIdx); - auto path = splitIdx == -1 ? "/org/mpris/MediaPlayer2" : address.sliced(splitIdx); +QString MprisLoopState::toString(MprisLoopState::Enum status) { + switch (status) { + case MprisLoopState::None: return "None"; + case MprisLoopState::Track: return "Track"; + case MprisLoopState::Playlist: return "Playlist"; + default: return "Unknown Status"; + } +} - this->player = new DBusMprisPlayer(conn, path, QDBusConnection::sessionBus(), this); +MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(parent) { + this->app = new DBusMprisPlayerApp( + address, + "/org/mpris/MediaPlayer2", + QDBusConnection::sessionBus(), + this + ); - if (!this->player->isValid()) { - qCWarning(logMprisPlayer).noquote() << "Cannot create MprisPlayer for" << conn; + this->player = + new DBusMprisPlayer(address, "/org/mpris/MediaPlayer2", QDBusConnection::sessionBus(), this); + + if (!this->player->isValid() || !this->app->isValid()) { + qCWarning(logMprisPlayer) << "Cannot create MprisPlayer for" << address; return; } // clang-format off - QObject::connect(&this->canControl, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->canGoNext, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->canGoPrevious, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->canPlay, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->canPause, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->metadata, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->playbackStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->position, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->minimumRate, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->maximumRate, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->pCanQuit, &AbstractDBusProperty::changed, this, &MprisPlayer::canQuitChanged); + QObject::connect(&this->pCanRaise, &AbstractDBusProperty::changed, this, &MprisPlayer::canRaiseChanged); + QObject::connect(&this->pCanSetFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::canSetFullscreenChanged); + QObject::connect(&this->pIdentity, &AbstractDBusProperty::changed, this, &MprisPlayer::identityChanged); + QObject::connect(&this->pFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::fullscreenChanged); + QObject::connect(&this->pSupportedUriSchemes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedUriSchemesChanged); + QObject::connect(&this->pSupportedMimeTypes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedMimeTypesChanged); - QObject::connect(&this->loopStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->rate, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->shuffle, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->volume, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->pCanControl, &AbstractDBusProperty::changed, this, &MprisPlayer::canControlChanged); + QObject::connect(&this->pCanSeek, &AbstractDBusProperty::changed, this, &MprisPlayer::canSeekChanged); + QObject::connect(&this->pCanGoNext, &AbstractDBusProperty::changed, this, &MprisPlayer::canGoNextChanged); + QObject::connect(&this->pCanGoPrevious, &AbstractDBusProperty::changed, this, &MprisPlayer::canGoPreviousChanged); + QObject::connect(&this->pCanPlay, &AbstractDBusProperty::changed, this, &MprisPlayer::canPlayChanged); + QObject::connect(&this->pCanPause, &AbstractDBusProperty::changed, this, &MprisPlayer::canPauseChanged); + QObject::connect(&this->pPosition, &AbstractDBusProperty::changed, this, &MprisPlayer::onPositionChanged); + QObject::connect(this->player, &DBusMprisPlayer::Seeked, this, &MprisPlayer::onSeek); + QObject::connect(&this->pVolume, &AbstractDBusProperty::changed, this, &MprisPlayer::volumeChanged); + QObject::connect(&this->pMetadata, &AbstractDBusProperty::changed, this, &MprisPlayer::onMetadataChanged); + QObject::connect(&this->pPlaybackStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::onPlaybackStatusChanged); + QObject::connect(&this->pLoopStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::onLoopStatusChanged); + QObject::connect(&this->pRate, &AbstractDBusProperty::changed, this, &MprisPlayer::rateChanged); + QObject::connect(&this->pMinRate, &AbstractDBusProperty::changed, this, &MprisPlayer::minRateChanged); + QObject::connect(&this->pMaxRate, &AbstractDBusProperty::changed, this, &MprisPlayer::maxRateChanged); + QObject::connect(&this->pShuffle, &AbstractDBusProperty::changed, this, &MprisPlayer::shuffleChanged); - QObject::connect(&this->properties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished); + QObject::connect(&this->playerProperties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished); + + // Ensure user triggered position updates can update length. + QObject::connect(this, &MprisPlayer::positionChanged, this, &MprisPlayer::onExportedPositionChanged); // clang-format on - this->properties.setInterface(this->player); - this->properties.updateAllViaGetAll(); + this->appProperties.setInterface(this->app); + this->playerProperties.setInterface(this->player); + this->appProperties.updateAllViaGetAll(); + this->playerProperties.updateAllViaGetAll(); +} + +void MprisPlayer::raise() { + if (!this->canRaise()) { + qWarning() << "Cannot call raise() on" << this << "because canRaise is false."; + return; + } + + this->app->Raise(); +} + +void MprisPlayer::quit() { + if (!this->canQuit()) { + qWarning() << "Cannot call quit() on" << this << "because canQuit is false."; + return; + } + + this->app->Quit(); +} + +void MprisPlayer::openUri(const QString& uri) { this->player->OpenUri(uri); } + +void MprisPlayer::next() { + if (!this->canGoNext()) { + qWarning() << "Cannot call next() on" << this << "because canGoNext is false."; + return; + } + + this->player->Next(); +} + +void MprisPlayer::previous() { + if (!this->canGoPrevious()) { + qWarning() << "Cannot call previous() on" << this << "because canGoPrevious is false."; + return; + } + + this->player->Previous(); +} + +void MprisPlayer::seek(qreal offset) { + if (!this->canSeek()) { + qWarning() << "Cannot call seek() on" << this << "because canSeek is false."; + return; + } + + auto target = static_cast(offset * 1000) * 1000; + this->player->Seek(target); } bool MprisPlayer::isValid() const { return this->player->isValid(); } -bool MprisPlayer::isReady() const { return this->mReady; } +QString MprisPlayer::address() const { return this->player->service(); } -void MprisPlayer::setPosition(QDBusObjectPath trackId, qlonglong position) { // NOLINT - this->player->SetPosition(trackId, position); +bool MprisPlayer::canControl() const { return this->pCanControl.get(); } +bool MprisPlayer::canPlay() const { return this->canControl() && this->pCanPlay.get(); } +bool MprisPlayer::canPause() const { return this->canControl() && this->pCanPause.get(); } +bool MprisPlayer::canSeek() const { return this->canControl() && this->pCanSeek.get(); } +bool MprisPlayer::canGoNext() const { return this->canControl() && this->pCanGoNext.get(); } +bool MprisPlayer::canGoPrevious() const { return this->canControl() && this->pCanGoPrevious.get(); } +bool MprisPlayer::canQuit() const { return this->pCanQuit.get(); } +bool MprisPlayer::canRaise() const { return this->pCanRaise.get(); } +bool MprisPlayer::canSetFullscreen() const { return this->pCanSetFullscreen.get(); } + +QString MprisPlayer::identity() const { return this->pIdentity.get(); } + +qlonglong MprisPlayer::positionMs() const { + if (!this->positionSupported()) return 0; // unsupported + if (this->mPlaybackState == MprisPlaybackState::Stopped) return 0; + + auto paused = this->mPlaybackState == MprisPlaybackState::Paused; + auto time = paused ? this->pausedTime : QDateTime::currentDateTime(); + auto offset = time - this->lastPositionTimestamp; + auto rateMul = static_cast(this->pRate.get() * 1000); + offset = (offset * rateMul) / 1000; + + return (this->pPosition.get() / 1000) + offset.count(); } -void MprisPlayer::next() { this->player->Next(); } -void MprisPlayer::previous() { this->player->Previous(); } -void MprisPlayer::pause() { this->player->Pause(); } -void MprisPlayer::playPause() { this->player->PlayPause(); } -void MprisPlayer::stop() { this->player->Stop(); } -void MprisPlayer::play() { this->player->Play(); } + +qreal MprisPlayer::position() const { + if (!this->positionSupported()) return 0; // unsupported + if (this->mPlaybackState == MprisPlaybackState::Stopped) return 0; + + return static_cast(this->positionMs()) / 1000.0; // NOLINT +} + +bool MprisPlayer::positionSupported() const { return this->pPosition.exists(); } + +void MprisPlayer::setPosition(qreal position) { + if (this->pPosition.get() == -1) { + qWarning() << "Cannot set position of" << this << "because position is not supported."; + return; + } + + if (!this->canSeek()) { + qWarning() << "Cannot set position of" << this << "because canSeek is false."; + return; + } + + auto target = static_cast(position * 1000) * 1000; + this->pPosition.set(target); + + if (!this->mTrackId.isEmpty()) { + this->player->SetPosition(QDBusObjectPath(this->mTrackId), target); + return; + } else { + auto pos = this->positionMs() * 1000; + this->player->Seek(target - pos); + } +} + +void MprisPlayer::onPositionChanged() { + const bool firstChange = !this->lastPositionTimestamp.isValid(); + this->lastPositionTimestamp = QDateTime::currentDateTimeUtc(); + emit this->positionChanged(); + if (firstChange) emit this->positionSupportedChanged(); +} + +void MprisPlayer::onExportedPositionChanged() { + if (!this->lengthSupported()) emit this->lengthChanged(); +} + +void MprisPlayer::onSeek(qlonglong time) { this->pPosition.set(time); } + +qreal MprisPlayer::length() const { + if (this->mLength == -1) { + return this->position(); // unsupported + } else { + return static_cast(this->mLength / 1000) / 1000; // NOLINT + } +} + +bool MprisPlayer::lengthSupported() const { return this->mLength != -1; } + +qreal MprisPlayer::volume() const { return this->pVolume.get(); } +bool MprisPlayer::volumeSupported() const { return this->pVolume.exists(); } + +void MprisPlayer::setVolume(qreal volume) { + if (!this->canControl()) { + qWarning() << "Cannot set volume of" << this << "because canControl is false."; + return; + } + + if (!this->volumeSupported()) { + qWarning() << "Cannot set volume of" << this << "because volume is not supported."; + return; + } + + this->pVolume.set(volume); + this->pVolume.write(); +} + +QVariantMap MprisPlayer::metadata() const { return this->pMetadata.get(); } + +void MprisPlayer::onMetadataChanged() { + auto lengthVariant = this->pMetadata.get().value("mpris:length"); + qlonglong length = -1; + if (lengthVariant.isValid() && lengthVariant.canConvert()) { + length = lengthVariant.value(); + } + + if (length != this->mLength) { + this->mLength = length; + emit this->lengthChanged(); + } + + auto trackidVariant = this->pMetadata.get().value("mpris:trackid"); + if (trackidVariant.isValid() && trackidVariant.canConvert()) { + this->mTrackId = trackidVariant.value(); + this->onSeek(0); + } + + emit this->metadataChanged(); +} + +MprisPlaybackState::Enum MprisPlayer::playbackState() const { return this->mPlaybackState; } + +void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) { + if (playbackState == this->mPlaybackState) return; + + switch (playbackState) { + case MprisPlaybackState::Stopped: + if (!this->canControl()) { + qWarning() << "Cannot set playbackState of" << this + << "to Stopped because canControl is false."; + return; + } + + this->player->Stop(); + break; + case MprisPlaybackState::Playing: + if (!this->canPlay()) { + qWarning() << "Cannot set playbackState of" << this << "to Playing because canPlay is false."; + return; + } + + this->player->Play(); + break; + case MprisPlaybackState::Paused: + if (!this->canPause()) { + qWarning() << "Cannot set playbackState of" << this << "to Paused because canPause is false."; + return; + } + + this->player->Pause(); + break; + default: + qWarning() << "Cannot set playbackState of" << this << "to unknown value" << playbackState; + return; + } +} + +void MprisPlayer::onPlaybackStatusChanged() { + const auto& status = this->pPlaybackStatus.get(); + + if (status == "Playing") { + this->mPlaybackState = MprisPlaybackState::Playing; + } else if (status == "Paused") { + this->mPlaybackState = MprisPlaybackState::Paused; + } else if (status == "Stopped") { + this->mPlaybackState = MprisPlaybackState::Stopped; + } else { + this->mPlaybackState = MprisPlaybackState::Stopped; + qWarning() << "Received unexpected PlaybackStatus for" << this << status; + } + + emit this->playbackStateChanged(); +} + +MprisLoopState::Enum MprisPlayer::loopState() const { return this->mLoopState; } +bool MprisPlayer::loopSupported() const { return this->pLoopStatus.exists(); } + +void MprisPlayer::setLoopState(MprisLoopState::Enum loopState) { + if (!this->canControl()) { + qWarning() << "Cannot set loopState of" << this << "because canControl is false."; + return; + } + + if (!this->loopSupported()) { + qWarning() << "Cannot set loopState of" << this << "because loop state is not supported."; + return; + } + + if (loopState == this->mLoopState) return; + + QString loopStatusStr; + switch (loopState) { + case MprisLoopState::None: loopStatusStr = "None"; break; + case MprisLoopState::Track: loopStatusStr = "Track"; break; + case MprisLoopState::Playlist: loopStatusStr = "Playlist"; break; + default: + qWarning() << "Cannot set loopState of" << this << "to unknown value" << loopState; + return; + } + + this->pLoopStatus.set(loopStatusStr); + this->pLoopStatus.write(); +} + +void MprisPlayer::onLoopStatusChanged() { + const auto& status = this->pLoopStatus.get(); + + if (status == "None") { + this->mLoopState = MprisLoopState::None; + } else if (status == "Track") { + this->mLoopState = MprisLoopState::Track; + } else if (status == "Playlist") { + this->mLoopState = MprisLoopState::Playlist; + } else { + this->mLoopState = MprisLoopState::None; + qWarning() << "Received unexpected LoopStatus for" << this << status; + } + + emit this->loopStateChanged(); +} + +qreal MprisPlayer::rate() const { return this->pRate.get(); } +qreal MprisPlayer::minRate() const { return this->pMinRate.get(); } +qreal MprisPlayer::maxRate() const { return this->pMaxRate.get(); } + +void MprisPlayer::setRate(qreal rate) { + if (rate == this->pRate.get()) return; + + if (rate < this->pMinRate.get() || rate > this->pMaxRate.get()) { + qWarning() << "Cannot set rate for" << this << "to" << rate + << "which is outside of minRate and maxRate" << this->pMinRate.get() + << this->pMaxRate.get(); + return; + } + + this->pRate.set(rate); + this->pRate.write(); +} + +bool MprisPlayer::shuffle() const { return this->pShuffle.get(); } +bool MprisPlayer::shuffleSupported() const { return this->pShuffle.exists(); } + +void MprisPlayer::setShuffle(bool shuffle) { + if (!this->shuffleSupported()) { + qWarning() << "Cannot set shuffle for" << this << "because shuffle is not supported."; + return; + } + + if (!this->canControl()) { + qWarning() << "Cannot set shuffle state of" << this << "because canControl is false."; + return; + } + + this->pShuffle.set(shuffle); + this->pShuffle.write(); +} + +bool MprisPlayer::fullscreen() const { return this->pFullscreen.get(); } + +void MprisPlayer::setFullscreen(bool fullscreen) { + if (!this->canSetFullscreen()) { + qWarning() << "Cannot set fullscreen for" << this << "because canSetFullscreen is false."; + return; + } + + this->pFullscreen.set(fullscreen); + this->pFullscreen.write(); +} + +QList MprisPlayer::supportedUriSchemes() const { return this->pSupportedUriSchemes.get(); } +QList MprisPlayer::supportedMimeTypes() const { return this->pSupportedMimeTypes.get(); } void MprisPlayer::onGetAllFinished() { - if (this->mReady) return; - this->mReady = true; + if (this->volumeSupported()) emit this->volumeSupportedChanged(); + if (this->loopSupported()) emit this->loopSupportedChanged(); + if (this->shuffleSupported()) emit this->shuffleSupportedChanged(); emit this->ready(); } -void MprisPlayer::updatePlayer() { // NOLINT - // TODO: emit signal here -} - -} // namespace qs::service::mp +} // namespace qs::service::mpris diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 168006a0..0b18d78c 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -1,67 +1,324 @@ #pragma once -#include -#include -#include +#include #include +#include +#include #include +#include "../../core/doc.hpp" #include "../../dbus/properties.hpp" #include "dbus_player.h" +#include "dbus_player_app.h" -Q_DECLARE_LOGGING_CATEGORY(logMprisPlayer); +namespace qs::service::mpris { -namespace qs::service::mp { - -class MprisPlayer; - -class MprisPlayer: public QObject { +class MprisPlaybackState: public QObject { Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; public: - explicit MprisPlayer(const QString& address, QObject* parent = nullptr); - QString watcherId; // TODO: maybe can be private CHECK + enum Enum { + Stopped = 0, + Playing = 1, + Paused = 2, + }; + Q_ENUM(Enum); - void setPosition(QDBusObjectPath trackId, qlonglong position); - void next(); - void previous(); - void pause(); - void playPause(); - void stop(); - void play(); + Q_INVOKABLE static QString toString(MprisPlaybackState::Enum status); +}; + +class MprisLoopState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum { + None = 0, + Track = 1, + Playlist = 2, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(MprisLoopState::Enum status); +}; + +///! A media player exposed over MPRIS. +/// A media player exposed over MPRIS. +/// +/// > [!WARNING] Support for various functionality and general compliance to +/// > the MPRIS specification varies wildly by player. +/// > Always check the associated `canXyz` and `xyzSupported` properties if available. +/// +/// > [!INFO] The TrackList and Playlist interfaces were not implemented as we could not +/// > find any media players using them to test against. +class MprisPlayer: public QObject { + Q_OBJECT; + // clang-format off + Q_PROPERTY(bool canControl READ canControl NOTIFY canControlChanged); + Q_PROPERTY(bool canPlay READ canPlay NOTIFY canPlayChanged); + Q_PROPERTY(bool canPause READ canPause NOTIFY canPauseChanged); + Q_PROPERTY(bool canSeek READ canSeek NOTIFY canSeekChanged); + Q_PROPERTY(bool canGoNext READ canGoNext NOTIFY canGoNextChanged); + Q_PROPERTY(bool canGoPrevious READ canGoPrevious NOTIFY canGoPreviousChanged); + Q_PROPERTY(bool canQuit READ canQuit NOTIFY canQuitChanged); + Q_PROPERTY(bool canRaise READ canRaise NOTIFY canRaiseChanged); + Q_PROPERTY(bool canSetFullscreen READ canSetFullscreen NOTIFY canSetFullscreenChanged); + /// The human readable name of the media player. + Q_PROPERTY(QString identity READ identity NOTIFY identityChanged); + /// The current position in the playing track, as seconds, with millisecond precision, + /// or `0` if `positionSupported` is false. + /// + /// May only be written to if `canSeek` and `positionSupported` are true. + /// + /// > [!WARNING] To avoid excessive property updates wasting CPU while `position` is not + /// > actively monitored, `position` usually will not update reactively, unless a nonlinear + /// > change in position occurs, however reading it will always return the current position. + /// > + /// > If you want to actively monitor the position, the simplest way it to emit the `positionChanged` + /// > signal manually for the duration you are monitoring it, Using a [FrameAnimation] if you need + /// > the value to update smoothly, such as on a slider, or a [Timer] if not, as shown below. + /// > + /// > ```qml {filename="Using a FrameAnimation"} + /// > FrameAnimation { + /// > // only emit the signal when the position is actually changing. + /// > running: player.playbackState == MprisPlaybackState.Playing + /// > // emit the positionChanged signal every frame. + /// > onTriggered: player.positionChanged() + /// > } + /// > ``` + /// > + /// > ```qml {filename="Using a Timer"} + /// > Timer { + /// > // only emit the signal when the position is actually changing. + /// > running: player.playbackState == MprisPlaybackState.Playing + /// > // Make sure the position updates at least once per second. + /// > interval: 1000 + /// > repeat: true + /// > // emit the positionChanged signal every second. + /// > onTriggered: player.positionChanged() + /// > } + /// > ``` + /// + /// [FrameAnimation]: https://doc.qt.io/qt-6/qml-qtquick-frameanimation.html + /// [Timer]: https://doc.qt.io/qt-6/qml-qtqml-timer.html + Q_PROPERTY(qreal position READ position WRITE setPosition NOTIFY positionChanged); + Q_PROPERTY(bool positionSupported READ positionSupported NOTIFY positionSupportedChanged); + /// The length of the playing track, as seconds, with millisecond precision, + /// or the value of `position` if `lengthSupported` is false. + Q_PROPERTY(qreal length READ length NOTIFY lengthChanged); + Q_PROPERTY(bool lengthSupported READ lengthSupported NOTIFY lengthSupportedChanged); + /// The volume of the playing track from 0.0 to 1.0, or 1.0 if `volumeSupported` is false. + /// + /// May only be written to if `canControl` and `volumeSupported` are true. + Q_PROPERTY(qreal volume READ volume WRITE setVolume NOTIFY volumeChanged); + Q_PROPERTY(bool volumeSupported READ volumeSupported NOTIFY volumeSupportedChanged); + /// Metadata of the current track. + /// + /// A map of common properties is available [here](https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata). + /// Do not count on any of them actually being present. + Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged); + /// The playback state of the media player. + /// + /// - If `canPlay` is false, you cannot assign the `Playing` state. + /// - If `canPause` is false, you cannot assign the `Paused` state. + /// - If `canControl` is false, you cannot assign the `Stopped` state. + /// (or any of the others, though their repsective properties will also be false) + Q_PROPERTY(MprisPlaybackState::Enum playbackState READ playbackState WRITE setPlaybackState NOTIFY playbackStateChanged); + /// The loop state of the media player, or `None` if `loopSupported` is false. + /// + /// May only be written to if `canControl` and `loopSupported` are true. + Q_PROPERTY(MprisLoopState::Enum loopState READ loopState WRITE setLoopState NOTIFY loopStateChanged); + Q_PROPERTY(bool loopSupported READ loopSupported NOTIFY loopSupportedChanged); + /// The speed the song is playing at, as a multiplier. + /// + /// Only values between `minRate` and `maxRate` (inclusive) may be written to the property. + /// Additionally, It is recommended that you only write common values such as `0.25`, `0.5`, `1.0`, `2.0` + /// to the property, as media players are free to ignore the value, and are more likely to + /// accept common ones. + Q_PROPERTY(qreal rate READ rate WRITE setRate NOTIFY rateChanged); + Q_PROPERTY(qreal minRate READ minRate NOTIFY minRateChanged); + Q_PROPERTY(qreal maxRate READ maxRate NOTIFY maxRateChanged); + /// If the play queue is currently being shuffled, or false if `shuffleSupported` is false. + /// + /// May only be written if `canControl` and `shuffleSupported` are true. + Q_PROPERTY(bool shuffle READ shuffle WRITE setShuffle NOTIFY shuffleChanged); + Q_PROPERTY(bool shuffleSupported READ shuffleSupported NOTIFY shuffleSupportedChanged); + /// If the player is currently shown in fullscreen. + /// + /// May only be written to if `canSetFullscreen` is true. + Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged); + /// Uri schemes supported by `openUri`. + Q_PROPERTY(QList supportedUriSchemes READ supportedUriSchemes NOTIFY supportedUriSchemesChanged); + /// Mime types supported by `openUri`. + Q_PROPERTY(QList supportedMimeTypes READ supportedMimeTypes NOTIFY supportedMimeTypesChanged); + // clang-format on + QML_ELEMENT; + QML_UNCREATABLE("MprisPlayers can only be acquired from Mpris"); + +public: + explicit MprisPlayer(const QString& address, QObject* parent = nullptr); + + /// Bring the media player to the front of the window stack. + /// + /// May only be called if `canRaise` is true. + Q_INVOKABLE void raise(); + /// Quit the media player. + /// + /// May only be called if `canQuit` is true. + Q_INVOKABLE void quit(); + /// Open the given URI in the media player. + /// + /// Many players will silently ignore this, especially if the uri + /// does not match `supportedUriSchemes` and `supportedMimeTypes`. + Q_INVOKABLE void openUri(const QString& uri); + /// Play the next song. + /// + /// May only be called if `canGoNext` is true. + Q_INVOKABLE void next(); + /// Play the previous song, or go back to the beginning of the current one. + /// + /// May only be called if `canGoPrevious` is true. + Q_INVOKABLE void previous(); + /// Change `position` by an offset. + /// + /// Even if `positionSupported` is false and you cannot set `position`, + /// this function may work. + /// + /// May only be called if `canSeek` is true. + Q_INVOKABLE void seek(qreal offset); [[nodiscard]] bool isValid() const; - [[nodiscard]] bool isReady() const; - - // clang-format off - dbus::DBusPropertyGroup properties; - dbus::DBusProperty canControl {this->properties, "CanControl" }; - dbus::DBusProperty canGoNext {this->properties, "CanGoNext" }; - dbus::DBusProperty canGoPrevious {this->properties, "CanGoPrevious" }; - dbus::DBusProperty canPlay {this->properties, "CanPlay" }; - dbus::DBusProperty canPause {this->properties, "CanPause" }; - dbus::DBusProperty metadata {this->properties, "Metadata"}; - dbus::DBusProperty playbackStatus {this->properties, "PlaybackStatus" }; - dbus::DBusProperty position {this->properties, "Position" }; - dbus::DBusProperty minimumRate {this->properties, "MinimumRate" }; - dbus::DBusProperty maximumRate {this->properties, "MaximumRate" }; + [[nodiscard]] QString address() const; - dbus::DBusProperty loopStatus {this->properties, "LoopStatus" }; - dbus::DBusProperty rate {this->properties, "Rate" }; - dbus::DBusProperty shuffle {this->properties, "Shuffle" }; - dbus::DBusProperty volume {this->properties, "Volume" }; - // clang-format on + [[nodiscard]] bool canControl() const; + [[nodiscard]] bool canSeek() const; + [[nodiscard]] bool canGoNext() const; + [[nodiscard]] bool canGoPrevious() const; + [[nodiscard]] bool canPlay() const; + [[nodiscard]] bool canPause() const; + [[nodiscard]] bool canQuit() const; + [[nodiscard]] bool canRaise() const; + [[nodiscard]] bool canSetFullscreen() const; + + [[nodiscard]] QString identity() const; + + [[nodiscard]] qlonglong positionMs() const; + [[nodiscard]] qreal position() const; + [[nodiscard]] bool positionSupported() const; + void setPosition(qreal position); + + [[nodiscard]] qreal length() const; + [[nodiscard]] bool lengthSupported() const; + + [[nodiscard]] qreal volume() const; + [[nodiscard]] bool volumeSupported() const; + void setVolume(qreal volume); + + [[nodiscard]] QVariantMap metadata() const; + + [[nodiscard]] MprisPlaybackState::Enum playbackState() const; + void setPlaybackState(MprisPlaybackState::Enum playbackState); + + [[nodiscard]] MprisLoopState::Enum loopState() const; + [[nodiscard]] bool loopSupported() const; + void setLoopState(MprisLoopState::Enum loopState); + + [[nodiscard]] qreal rate() const; + [[nodiscard]] qreal minRate() const; + [[nodiscard]] qreal maxRate() const; + void setRate(qreal rate); + + [[nodiscard]] bool shuffle() const; + [[nodiscard]] bool shuffleSupported() const; + void setShuffle(bool shuffle); + + [[nodiscard]] bool fullscreen() const; + void setFullscreen(bool fullscreen); + + [[nodiscard]] QList supportedUriSchemes() const; + [[nodiscard]] QList supportedMimeTypes() const; signals: - void ready(); + QSDOC_HIDE void ready(); + void canControlChanged(); + void canPlayChanged(); + void canPauseChanged(); + void canSeekChanged(); + void canGoNextChanged(); + void canGoPreviousChanged(); + void canQuitChanged(); + void canRaiseChanged(); + void canSetFullscreenChanged(); + void identityChanged(); + void positionChanged(); + void positionSupportedChanged(); + void lengthChanged(); + void lengthSupportedChanged(); + void volumeChanged(); + void volumeSupportedChanged(); + void metadataChanged(); + void playbackStateChanged(); + void loopStateChanged(); + void loopSupportedChanged(); + void rateChanged(); + void minRateChanged(); + void maxRateChanged(); + void shuffleChanged(); + void shuffleSupportedChanged(); + void fullscreenChanged(); + void supportedUriSchemesChanged(); + void supportedMimeTypesChanged(); private slots: void onGetAllFinished(); - void updatePlayer(); + void onPositionChanged(); + void onExportedPositionChanged(); + void onSeek(qlonglong time); + void onMetadataChanged(); + void onPlaybackStatusChanged(); + void onLoopStatusChanged(); private: + // clang-format off + dbus::DBusPropertyGroup appProperties; + dbus::DBusProperty pIdentity {this->appProperties, "Identity"}; + dbus::DBusProperty pCanQuit {this->appProperties, "CanQuit"}; + dbus::DBusProperty pCanRaise {this->appProperties, "CanRaise"}; + dbus::DBusProperty pFullscreen {this->appProperties, "Fullscreen", false, false}; + dbus::DBusProperty pCanSetFullscreen {this->appProperties, "CanSetFullscreen", false, false}; + dbus::DBusProperty> pSupportedUriSchemes {this->appProperties, "SupportedUriSchemes"}; + dbus::DBusProperty> pSupportedMimeTypes {this->appProperties, "SupportedMimeTypes"}; + + dbus::DBusPropertyGroup playerProperties; + dbus::DBusProperty pCanControl {this->playerProperties, "CanControl"}; + dbus::DBusProperty pCanPlay {this->playerProperties, "CanPlay"}; + dbus::DBusProperty pCanPause {this->playerProperties, "CanPause"}; + dbus::DBusProperty pCanSeek {this->playerProperties, "CanSeek"}; + dbus::DBusProperty pCanGoNext {this->playerProperties, "CanGoNext"}; + dbus::DBusProperty pCanGoPrevious {this->playerProperties, "CanGoPrevious"}; + dbus::DBusProperty pPosition {this->playerProperties, "Position", 0, false}; // "required" + dbus::DBusProperty pVolume {this->playerProperties, "Volume", 1, false}; // "required" + dbus::DBusProperty pMetadata {this->playerProperties, "Metadata"}; + dbus::DBusProperty pPlaybackStatus {this->playerProperties, "PlaybackStatus"}; + dbus::DBusProperty pLoopStatus {this->playerProperties, "LoopStatus", "", false}; + dbus::DBusProperty pRate {this->playerProperties, "Rate", 1, false}; // "required" + dbus::DBusProperty pMinRate {this->playerProperties, "MinimumRate", 1, false}; // "required" + dbus::DBusProperty pMaxRate {this->playerProperties, "MaximumRate", 1, false}; // "required" + dbus::DBusProperty pShuffle {this->playerProperties, "Shuffle", false, false}; + // clang-format on + + MprisPlaybackState::Enum mPlaybackState = MprisPlaybackState::Stopped; + MprisLoopState::Enum mLoopState = MprisLoopState::None; + QDateTime lastPositionTimestamp; + QDateTime pausedTime; + qlonglong mLength = -1; + + DBusMprisPlayerApp* app = nullptr; DBusMprisPlayer* player = nullptr; - bool mReady = false; + QString mTrackId; }; -} // namespace qs::service::mp +} // namespace qs::service::mpris diff --git a/src/services/mpris/qml.cpp b/src/services/mpris/qml.cpp deleted file mode 100644 index 4e99a9f2..00000000 --- a/src/services/mpris/qml.cpp +++ /dev/null @@ -1,180 +0,0 @@ -#include "qml.hpp" - -#include -#include -#include -#include -#include - -#include "../../dbus/properties.hpp" -#include "player.hpp" -#include "watcher.hpp" - -using namespace qs::dbus; -using namespace qs::service::mp; - -Player::Player(qs::service::mp::MprisPlayer* player, QObject* parent) - : QObject(parent) - , player(player) { - - // clang-format off - QObject::connect(&this->player->canControl, &AbstractDBusProperty::changed, this, &Player::canControlChanged); - QObject::connect(&this->player->canGoNext, &AbstractDBusProperty::changed, this, &Player::canGoNextChanged); - QObject::connect(&this->player->canGoPrevious, &AbstractDBusProperty::changed, this, &Player::canGoPreviousChanged); - QObject::connect(&this->player->canPlay, &AbstractDBusProperty::changed, this, &Player::canPlayChanged); - QObject::connect(&this->player->canPause, &AbstractDBusProperty::changed, this, &Player::canPauseChanged); - QObject::connect(&this->player->metadata, &AbstractDBusProperty::changed, this, &Player::metadataChanged); - QObject::connect(&this->player->playbackStatus, &AbstractDBusProperty::changed, this, &Player::playbackStatusChanged); - QObject::connect(&this->player->position, &AbstractDBusProperty::changed, this, &Player::positionChanged); - QObject::connect(&this->player->minimumRate, &AbstractDBusProperty::changed, this, &Player::positionChanged); - QObject::connect(&this->player->maximumRate, &AbstractDBusProperty::changed, this, &Player::positionChanged); - - QObject::connect(&this->player->loopStatus, &AbstractDBusProperty::changed, this, &Player::loopStatusChanged); - QObject::connect(&this->player->rate, &AbstractDBusProperty::changed, this, &Player::rateChanged); - QObject::connect(&this->player->shuffle, &AbstractDBusProperty::changed, this, &Player::shuffleChanged); - QObject::connect(&this->player->volume, &AbstractDBusProperty::changed, this, &Player::volumeChanged); - // clang-format on -} - -bool Player::canControl() const { - if (this->player == nullptr) return false; - return this->player->canControl.get(); -} - -bool Player::canGoNext() const { - if (this->player == nullptr) return false; - return this->player->canGoNext.get(); -} - -bool Player::canGoPrevious() const { - if (this->player == nullptr) return false; - return this->player->canGoPrevious.get(); -} - -bool Player::canPlay() const { - if (this->player == nullptr) return false; - return this->player->canPlay.get(); -} - -bool Player::canPause() const { - if (this->player == nullptr) return false; - return this->player->canPause.get(); -} - -QVariantMap Player::metadata() const { - if (this->player == nullptr) return {}; - return this->player->metadata.get(); -} - -QString Player::playbackStatus() const { - if (this->player == nullptr) return ""; - - if (this->player->playbackStatus.get().isEmpty()) return "Unsupported"; - return this->player->playbackStatus.get(); -} - -qlonglong Player::position() const { - if (this->player == nullptr) return 0; - return this->player->position.get(); -} - -double Player::minimumRate() const { - if (this->player == nullptr) return 0.0; - return this->player->minimumRate.get(); -} - -double Player::maximumRate() const { - if (this->player == nullptr) return 0.0; - return this->player->maximumRate.get(); -} - -QString Player::loopStatus() const { - if (this->player == nullptr) return ""; - - if (this->player->loopStatus.get().isEmpty()) return "Unsupported"; - return this->player->loopStatus.get(); -} - -double Player::rate() const { - if (this->player == nullptr) return 0.0; - return this->player->rate.get(); -} - -bool Player::shuffle() const { - if (this->player == nullptr) return false; - return this->player->shuffle.get(); -} - -double Player::volume() const { - if (this->player == nullptr) return 0.0; - return this->player->volume.get(); -} - -// NOLINTBEGIN -void Player::setPosition(QDBusObjectPath trackId, qlonglong position) const { - this->player->setPosition(trackId, position); -} -void Player::next() const { this->player->next(); } -void Player::previous() const { this->player->previous(); } -void Player::pause() const { this->player->pause(); } -void Player::playPause() const { this->player->playPause(); } -void Player::stop() const { this->player->stop(); } -void Player::play() const { this->player->play(); } -// NOLINTEND - -Mpris::Mpris(QObject* parent): QObject(parent) { - auto* watcher = MprisWatcher::instance(); - - // clang-format off - QObject::connect(watcher, &MprisWatcher::MprisPlayerRegistered, this, &Mpris::onPlayerRegistered); - QObject::connect(watcher, &MprisWatcher::MprisPlayerUnregistered, this, &Mpris::onPlayerUnregistered); - // clang-format on - - for (QString& player: watcher->players) { - this->mPlayers.push_back(new Player(new MprisPlayer(player), this)); - } -} - -void Mpris::onPlayerRegistered(const QString& service) { - this->mPlayers.push_back(new Player(new MprisPlayer(service), this)); - emit this->playersChanged(); -} - -void Mpris::onPlayerUnregistered(const QString& service) { - Player* mprisPlayer = nullptr; - MprisPlayer* player = playerWithAddress(players(), service)->player; - - this->mPlayers.removeIf([player, &mprisPlayer](Player* testPlayer) { - if (testPlayer->player == player) { - mprisPlayer = testPlayer; - return true; - } else return false; - }); - - emit this->playersChanged(); - - delete mprisPlayer->player; - delete mprisPlayer; -} - -QQmlListProperty Mpris::players() { - return QQmlListProperty(this, nullptr, &Mpris::playersCount, &Mpris::playerAt); -} - -qsizetype Mpris::playersCount(QQmlListProperty* property) { - return reinterpret_cast(property->object)->mPlayers.count(); // NOLINT -} - -Player* Mpris::playerAt(QQmlListProperty* property, qsizetype index) { - return reinterpret_cast(property->object)->mPlayers.at(index); // NOLINT -} - -Player* Mpris::playerWithAddress(QQmlListProperty property, const QString& address) { - for (Player* player: reinterpret_cast(property.object)->mPlayers) { // NOLINT - if (player->player->watcherId == address) { - return player; - } - } - - return nullptr; -} diff --git a/src/services/mpris/qml.hpp b/src/services/mpris/qml.hpp deleted file mode 100644 index 4e7896b0..00000000 --- a/src/services/mpris/qml.hpp +++ /dev/null @@ -1,113 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "player.hpp" - - - -///! Mpris implementation for quickshell -/// mpris service, get useful information from apps that implement media player fucntionality [mpris spec] -/// (Beware of misuse of spec, it is just a suggestion for most) -/// -/// -/// [mpris spec]: https://specifications.freedesktop.org/mpris-spec -class Player: public QObject { - Q_OBJECT; - // READ-ONLY - Q_PROPERTY(bool canControl READ canControl NOTIFY canControlChanged); - Q_PROPERTY(bool canGoNext READ canGoNext NOTIFY canGoNextChanged); - Q_PROPERTY(bool canGoPrevious READ canGoPrevious NOTIFY canGoPreviousChanged); - Q_PROPERTY(bool canPlay READ canPlay NOTIFY canPlayChanged); - Q_PROPERTY(bool canPause READ canPause NOTIFY canPauseChanged); - Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged); - Q_PROPERTY(QString playbackStatus READ playbackStatus NOTIFY playbackStatusChanged); - Q_PROPERTY(qlonglong position READ position NOTIFY positionChanged); - Q_PROPERTY(double minimumRate READ minimumRate NOTIFY minimumRateChanged); - Q_PROPERTY(double maximumRate READ maximumRate NOTIFY maximumRateChanged); - - // READ/WRITE - Write isn't implemented thus this need to fixed when that happens. - Q_PROPERTY(QString loopStatus READ loopStatus NOTIFY loopStatusChanged); - Q_PROPERTY(double rate READ rate NOTIFY rateChanged); - Q_PROPERTY(bool shuffle READ shuffle NOTIFY shuffleChanged); - Q_PROPERTY(double volume READ volume NOTIFY volumeChanged); - - QML_ELEMENT; - QML_UNCREATABLE("MprisPlayers can only be acquired from Mpris"); - -public: - explicit Player(qs::service::mp::MprisPlayer* player, QObject* parent = nullptr); - - // These are all self-explanatory. - Q_INVOKABLE void setPosition(QDBusObjectPath trackId, qlonglong position) const; - Q_INVOKABLE void next() const; - Q_INVOKABLE void previous() const; - Q_INVOKABLE void pause() const; - Q_INVOKABLE void playPause() const; - Q_INVOKABLE void stop() const; - Q_INVOKABLE void play() const; - - [[nodiscard]] bool canControl() const; - [[nodiscard]] bool canGoNext() const; - [[nodiscard]] bool canGoPrevious() const; - [[nodiscard]] bool canPlay() const; - [[nodiscard]] bool canPause() const; - [[nodiscard]] QVariantMap metadata() const; - [[nodiscard]] QString playbackStatus() const; - [[nodiscard]] qlonglong position() const; - [[nodiscard]] double minimumRate() const; - [[nodiscard]] double maximumRate() const; - - [[nodiscard]] QString loopStatus() const; - [[nodiscard]] double rate() const; - [[nodiscard]] bool shuffle() const; - [[nodiscard]] double volume() const; - - qs::service::mp::MprisPlayer* player = nullptr; - -signals: - void canControlChanged(); - void canGoNextChanged(); - void canGoPreviousChanged(); - void canPlayChanged(); - void canPauseChanged(); - void metadataChanged(); - void playbackStatusChanged(); - void positionChanged(); - void minimumRateChanged(); - void maximumRateChanged(); - - void loopStatusChanged(); - void rateChanged(); - void shuffleChanged(); - void volumeChanged(); -}; - -class Mpris: public QObject { - Q_OBJECT; - Q_PROPERTY(QQmlListProperty players READ players NOTIFY playersChanged); - QML_ELEMENT; - QML_SINGLETON; - -public: - explicit Mpris(QObject* parent = nullptr); - - [[nodiscard]] QQmlListProperty players(); - -signals: - void playersChanged(); - -private slots: - void onPlayerRegistered(const QString& service); - void onPlayerUnregistered(const QString& service); - -private: - static qsizetype playersCount(QQmlListProperty* property); - static Player* playerAt(QQmlListProperty* property, qsizetype index); - static Player* playerWithAddress(QQmlListProperty property, const QString& address); - - QList mPlayers; -}; diff --git a/src/services/mpris/watcher.cpp b/src/services/mpris/watcher.cpp index 2a735f74..1e107660 100644 --- a/src/services/mpris/watcher.cpp +++ b/src/services/mpris/watcher.cpp @@ -1,22 +1,24 @@ #include "watcher.hpp" -#include +#include #include #include #include #include #include #include -#include #include +#include +#include +#include -Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mp.watcher", QtWarningMsg); +#include "player.hpp" -namespace qs::service::mp { +namespace qs::service::mpris { + +Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg); MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) { - new MprisWatcherAdaptor(this); - qCDebug(logMprisWatcher) << "Starting MprisWatcher"; auto bus = QDBusConnection::sessionBus(); @@ -26,121 +28,99 @@ MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) { return; } - if (!bus.registerObject("/MprisWatcher", this)) { - qCWarning(logMprisWatcher) << "Could not register MprisWatcher object with " - "DBus. Mpris service will not work."; - return; - } - // clang-format off QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, &MprisWatcher::onServiceRegistered); QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &MprisWatcher::onServiceUnregistered); - - this->serviceWatcher.setWatchMode(QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration); // clang-format on + this->serviceWatcher.setWatchMode( + QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration + ); + this->serviceWatcher.addWatchedService("org.mpris.MediaPlayer2*"); - this->serviceWatcher.addWatchedService("org.mpris.MprisWatcher"); this->serviceWatcher.setConnection(bus); - this->tryRegister(); + this->registerExisting(); } -void MprisWatcher::tryRegister() { // NOLINT - auto bus = QDBusConnection::sessionBus(); - auto success = bus.registerService("org.mpris.MprisWatcher"); - - if (success) { - qCDebug(logMprisWatcher) << "Registered watcher at org.mpris.MprisWatcher"; - emit this->MprisWatcherRegistered(); - registerExisting(bus); // Register services that already existed before creation. - } else { - qCDebug(logMprisWatcher) << "Could not register watcher at " - "org.mpris.MprisWatcher, presumably because one is " - "already registered."; - qCDebug(logMprisWatcher - ) << "Registration will be attempted again if the active service is unregistered."; - } -} - -void MprisWatcher::registerExisting(const QDBusConnection& connection) { - QStringList list = connection.interface()->registeredServiceNames(); +void MprisWatcher::registerExisting() { + const QStringList& list = QDBusConnection::sessionBus().interface()->registeredServiceNames(); for (const QString& service: list) { - if (service.contains("org.mpris.MediaPlayer2")) { + if (service.startsWith("org.mpris.MediaPlayer2")) { qCDebug(logMprisWatcher).noquote() << "Found Mpris service" << service; - RegisterMprisPlayer(service); + this->registerPlayer(service); } } } void MprisWatcher::onServiceRegistered(const QString& service) { - if (service == "org.mpris.MprisWatcher") { - qCDebug(logMprisWatcher) << "MprisWatcher"; - return; - } else if (service.contains("org.mpris.MediaPlayer2")) { + if (service.startsWith("org.mpris.MediaPlayer2")) { qCDebug(logMprisWatcher).noquote() << "Mpris service " << service << " registered."; - RegisterMprisPlayer(service); + this->registerPlayer(service); } else { - qCWarning(logMprisWatcher) << "Got a registration event for a untracked service"; + qCWarning(logMprisWatcher) << "Got a registration event for untracked service" << service; } } -// TODO: This is getting triggered twice on unregistration, investigate. void MprisWatcher::onServiceUnregistered(const QString& service) { - if (service == "org.mpris.MprisWatcher") { - qCDebug(logMprisWatcher) << "Active MprisWatcher unregistered, attempting registration"; - this->tryRegister(); - return; + if (auto* player = this->mPlayers.value(service)) { + player->deleteLater(); + this->mPlayers.remove(service); + qCDebug(logMprisWatcher) << "Unregistered MprisPlayer" << service; } else { - QString qualifiedPlayer; - this->players.removeIf([&](const QString& player) { - if (QString::compare(player, service) == 0) { - qualifiedPlayer = player; - return true; - } else return false; - }); - - if (!qualifiedPlayer.isEmpty()) { - qCDebug(logMprisWatcher).noquote() - << "Unregistered MprisPlayer" << qualifiedPlayer << "from watcher"; - - emit this->MprisPlayerUnregistered(qualifiedPlayer); - } else { - qCWarning(logMprisWatcher).noquote() - << "Got service unregister event for untracked service" << service; - } + qCWarning(logMprisWatcher) << "Got service unregister event for untracked service" << service; } - - this->serviceWatcher.removeWatchedService(service); } -QList MprisWatcher::registeredPlayers() const { return this->players; } +void MprisWatcher::onPlayerReady() { + auto* player = qobject_cast(this->sender()); + this->readyPlayers.push_back(player); + emit this->playersChanged(); +} -void MprisWatcher::RegisterMprisPlayer(const QString& player) { - if (this->players.contains(player)) { - qCDebug(logMprisWatcher).noquote() - << "Skipping duplicate registration of MprisPlayer" << player << "to watcher"; +void MprisWatcher::onPlayerDestroyed(QObject* object) { + auto* player = static_cast(object); // NOLINT + + if (this->readyPlayers.removeOne(player)) { + emit this->playersChanged(); + } +} + +QQmlListProperty MprisWatcher::players() { + return QQmlListProperty( + this, + nullptr, + &MprisWatcher::playersCount, + &MprisWatcher::playerAt + ); +} + +qsizetype MprisWatcher::playersCount(QQmlListProperty* property) { + return static_cast(property->object)->readyPlayers.count(); // NOLINT +} + +MprisPlayer* MprisWatcher::playerAt(QQmlListProperty* property, qsizetype index) { + return static_cast(property->object)->readyPlayers.at(index); // NOLINT +} + +void MprisWatcher::registerPlayer(const QString& address) { + if (this->mPlayers.contains(address)) { + qCDebug(logMprisWatcher) << "Skipping duplicate registration of MprisPlayer" << address; return; } - if (!QDBusConnection::sessionBus().interface()->serviceOwner(player).isValid()) { - qCWarning(logMprisWatcher).noquote() - << "Ignoring invalid MprisPlayer registration of" << player << "to watcher"; + auto* player = new MprisPlayer(address, this); + if (!player->isValid()) { + qCWarning(logMprisWatcher) << "Ignoring invalid MprisPlayer registration of" << address; + delete player; return; } - this->serviceWatcher.addWatchedService(player); - this->players.push_back(player); + this->mPlayers.insert(address, player); + QObject::connect(player, &MprisPlayer::ready, this, &MprisWatcher::onPlayerReady); + QObject::connect(player, &QObject::destroyed, this, &MprisWatcher::onPlayerDestroyed); - qCDebug(logMprisWatcher).noquote() << "Registered MprisPlayer" << player << "to watcher"; - - emit this->MprisPlayerRegistered(player); + qCDebug(logMprisWatcher) << "Registered MprisPlayer" << address; } -MprisWatcher* MprisWatcher::instance() { - static MprisWatcher* instance = nullptr; // NOLINT - if (instance == nullptr) instance = new MprisWatcher(); - return instance; -} - -} // namespace qs::service::mp +} // namespace qs::service::mpris diff --git a/src/services/mpris/watcher.hpp b/src/services/mpris/watcher.hpp index b3e0bdeb..a1e4df7c 100644 --- a/src/services/mpris/watcher.hpp +++ b/src/services/mpris/watcher.hpp @@ -3,51 +3,51 @@ #include #include #include +#include #include #include #include +#include +#include +#include #include -Q_DECLARE_LOGGING_CATEGORY(logMprisWatcher); +#include "player.hpp" -namespace qs::service::mp { +namespace qs::service::mpris { -class MprisWatcher - : public QObject - , protected QDBusContext { +///! Provides access to MprisPlayers. +class MprisWatcher: public QObject { Q_OBJECT; - Q_PROPERTY(qint32 ProtocolVersion READ protocolVersion); - Q_PROPERTY(QList RegisteredMprisPlayers READ registeredPlayers); + QML_NAMED_ELEMENT(Mpris); + QML_SINGLETON; + /// All connected MPRIS players. + Q_PROPERTY(QQmlListProperty players READ players NOTIFY playersChanged); public: explicit MprisWatcher(QObject* parent = nullptr); - void tryRegister(); - void registerExisting(const QDBusConnection &connection); - - [[nodiscard]] qint32 protocolVersion() const { return 0; } // NOLINT - [[nodiscard]] QList registeredPlayers() const; - - // NOLINTBEGIN - void RegisterMprisPlayer(const QString& player); - // NOLINTEND - - static MprisWatcher* instance(); - QList players; + [[nodiscard]] QQmlListProperty players(); signals: - // NOLINTBEGIN - void MprisWatcherRegistered(); - void MprisPlayerRegistered(const QString& service); - void MprisPlayerUnregistered(const QString& service); - // NOLINTEND + void playersChanged(); private slots: - void onServiceRegistered(const QString& service); + void onServiceRegistered(const QString& service); void onServiceUnregistered(const QString& service); + void onPlayerReady(); + void onPlayerDestroyed(QObject* object); + +private: + static qsizetype playersCount(QQmlListProperty* property); + static MprisPlayer* playerAt(QQmlListProperty* property, qsizetype index); + + void registerExisting(); + void registerPlayer(const QString& address); -private: QDBusServiceWatcher serviceWatcher; + QHash mPlayers; + QList readyPlayers; }; -} // namespace qs::service::mp +} // namespace qs::service::mpris diff --git a/src/services/status_notifier/item.hpp b/src/services/status_notifier/item.hpp index 9dbf02fd..aa411909 100644 --- a/src/services/status_notifier/item.hpp +++ b/src/services/status_notifier/item.hpp @@ -54,14 +54,14 @@ public: dbus::DBusProperty status {this->properties, "Status"}; dbus::DBusProperty category {this->properties, "Category"}; dbus::DBusProperty windowId {this->properties, "WindowId"}; - dbus::DBusProperty iconThemePath {this->properties, "IconThemePath"}; - dbus::DBusProperty iconName {this->properties, "IconName"}; - dbus::DBusProperty iconPixmaps {this->properties, "IconPixmap"}; + dbus::DBusProperty iconThemePath {this->properties, "IconThemePath", "", false}; + dbus::DBusProperty iconName {this->properties, "IconName", "", false}; // IconPixmap may be set + dbus::DBusProperty iconPixmaps {this->properties, "IconPixmap", {}, false}; // IconName may be set dbus::DBusProperty overlayIconName {this->properties, "OverlayIconName"}; dbus::DBusProperty overlayIconPixmaps {this->properties, "OverlayIconPixmap"}; dbus::DBusProperty attentionIconName {this->properties, "AttentionIconName"}; dbus::DBusProperty attentionIconPixmaps {this->properties, "AttentionIconPixmap"}; - dbus::DBusProperty attentionMovieName {this->properties, "AttentionMovieName"}; + dbus::DBusProperty attentionMovieName {this->properties, "AttentionMovieName", "", false}; dbus::DBusProperty tooltip {this->properties, "ToolTip"}; dbus::DBusProperty isMenu {this->properties, "ItemIsMenu"}; dbus::DBusProperty menuPath {this->properties, "Menu"}; From ed3708f5cb9558ad9024446087b36d019de2df80 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 21 May 2024 05:07:24 -0700 Subject: [PATCH 009/305] service/mpris: add trackChanged signal --- src/services/mpris/player.cpp | 8 +++++++- src/services/mpris/player.hpp | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 3b0c7463..ebdbfd64 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -258,7 +258,13 @@ void MprisPlayer::onMetadataChanged() { auto trackidVariant = this->pMetadata.get().value("mpris:trackid"); if (trackidVariant.isValid() && trackidVariant.canConvert()) { - this->mTrackId = trackidVariant.value(); + auto trackId = trackidVariant.value(); + + if (trackId != this->mTrackId) { + this->mTrackId = trackId; + emit this->trackChanged(); + } + this->onSeek(0); } diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 0b18d78c..97181a59 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -242,6 +242,8 @@ public: [[nodiscard]] QList supportedMimeTypes() const; signals: + void trackChanged(); + QSDOC_HIDE void ready(); void canControlChanged(); void canPlayChanged(); From f2df3da596adf3b42a36759ed09d0cc8d4bc46f8 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 22 May 2024 04:34:56 -0700 Subject: [PATCH 010/305] service/mpris: fix position being incorrect after pausing --- src/services/mpris/player.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index ebdbfd64..1bb9f7be 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -312,8 +312,11 @@ void MprisPlayer::onPlaybackStatusChanged() { const auto& status = this->pPlaybackStatus.get(); if (status == "Playing") { + // update the timestamp + this->onSeek(this->positionMs() * 1000); this->mPlaybackState = MprisPlaybackState::Playing; } else if (status == "Paused") { + this->pausedTime = QDateTime::currentDateTimeUtc(); this->mPlaybackState = MprisPlaybackState::Paused; } else if (status == "Stopped") { this->mPlaybackState = MprisPlaybackState::Stopped; From ac339cb23b5d07de381e16ad7c939507cf0d59f7 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 22 May 2024 05:40:03 -0700 Subject: [PATCH 011/305] service/mpris: expose desktopEntry property --- src/services/mpris/player.cpp | 2 ++ src/services/mpris/player.hpp | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 1bb9f7be..27ba34c4 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -60,6 +60,7 @@ MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(paren QObject::connect(&this->pCanRaise, &AbstractDBusProperty::changed, this, &MprisPlayer::canRaiseChanged); QObject::connect(&this->pCanSetFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::canSetFullscreenChanged); QObject::connect(&this->pIdentity, &AbstractDBusProperty::changed, this, &MprisPlayer::identityChanged); + QObject::connect(&this->pDesktopEntry, &AbstractDBusProperty::changed, this, &MprisPlayer::desktopEntryChanged); QObject::connect(&this->pFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::fullscreenChanged); QObject::connect(&this->pSupportedUriSchemes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedUriSchemesChanged); QObject::connect(&this->pSupportedMimeTypes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedMimeTypesChanged); @@ -155,6 +156,7 @@ bool MprisPlayer::canRaise() const { return this->pCanRaise.get(); } bool MprisPlayer::canSetFullscreen() const { return this->pCanSetFullscreen.get(); } QString MprisPlayer::identity() const { return this->pIdentity.get(); } +QString MprisPlayer::desktopEntry() const { return this->pDesktopEntry.get(); } qlonglong MprisPlayer::positionMs() const { if (!this->positionSupported()) return 0; // unsupported diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 97181a59..ddbb87cb 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -68,6 +68,8 @@ class MprisPlayer: public QObject { Q_PROPERTY(bool canSetFullscreen READ canSetFullscreen NOTIFY canSetFullscreenChanged); /// The human readable name of the media player. Q_PROPERTY(QString identity READ identity NOTIFY identityChanged); + /// The name of the desktop entry for the media player, or an empty string if not provided. + Q_PROPERTY(QString desktopEntry READ desktopEntry NOTIFY desktopEntryChanged); /// The current position in the playing track, as seconds, with millisecond precision, /// or `0` if `positionSupported` is false. /// @@ -204,6 +206,7 @@ public: [[nodiscard]] bool canSetFullscreen() const; [[nodiscard]] QString identity() const; + [[nodiscard]] QString desktopEntry() const; [[nodiscard]] qlonglong positionMs() const; [[nodiscard]] qreal position() const; @@ -255,6 +258,7 @@ signals: void canRaiseChanged(); void canSetFullscreenChanged(); void identityChanged(); + void desktopEntryChanged(); void positionChanged(); void positionSupportedChanged(); void lengthChanged(); @@ -287,6 +291,7 @@ private: // clang-format off dbus::DBusPropertyGroup appProperties; dbus::DBusProperty pIdentity {this->appProperties, "Identity"}; + dbus::DBusProperty pDesktopEntry {this->appProperties, "DesktopEntry", "", false}; dbus::DBusProperty pCanQuit {this->appProperties, "CanQuit"}; dbus::DBusProperty pCanRaise {this->appProperties, "CanRaise"}; dbus::DBusProperty pFullscreen {this->appProperties, "Fullscreen", false, false}; From 6326f60ce23c9c95494cbbe0511d42f15e12d736 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 23 May 2024 02:38:26 -0700 Subject: [PATCH 012/305] service/mpris: re-query position on playback and metadata change --- src/services/mpris/player.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 27ba34c4..e17f3e82 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -267,7 +267,8 @@ void MprisPlayer::onMetadataChanged() { emit this->trackChanged(); } - this->onSeek(0); + // Some players don't seem to send position updats or seeks on track change. + this->pPosition.update(); } emit this->metadataChanged(); @@ -313,21 +314,25 @@ void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) { void MprisPlayer::onPlaybackStatusChanged() { const auto& status = this->pPlaybackStatus.get(); + auto state = MprisPlaybackState::Stopped; if (status == "Playing") { - // update the timestamp - this->onSeek(this->positionMs() * 1000); - this->mPlaybackState = MprisPlaybackState::Playing; + state = MprisPlaybackState::Playing; } else if (status == "Paused") { this->pausedTime = QDateTime::currentDateTimeUtc(); - this->mPlaybackState = MprisPlaybackState::Paused; + state = MprisPlaybackState::Paused; } else if (status == "Stopped") { - this->mPlaybackState = MprisPlaybackState::Stopped; + state = MprisPlaybackState::Stopped; } else { - this->mPlaybackState = MprisPlaybackState::Stopped; + state = MprisPlaybackState::Stopped; qWarning() << "Received unexpected PlaybackStatus for" << this << status; } - emit this->playbackStateChanged(); + if (state != this->mPlaybackState) { + // make sure we're in sync at least on play/pause. Some players don't automatically send this. + this->pPosition.update(); + this->mPlaybackState = state; + emit this->playbackStateChanged(); + } } MprisLoopState::Enum MprisPlayer::loopState() const { return this->mLoopState; } From 5016dbf0d4c9e12f7e3d5f872b3063785d20b2bf Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 23 May 2024 17:28:07 -0700 Subject: [PATCH 013/305] all: replace list properties with ObjectModels --- src/core/CMakeLists.txt | 1 + src/core/doc.hpp | 3 + src/core/model.cpp | 67 ++++++++++++++++++++++ src/core/model.hpp | 86 ++++++++++++++++++++++++++++ src/core/module.md | 1 + src/services/mpris/watcher.cpp | 29 ++-------- src/services/mpris/watcher.hpp | 15 ++--- src/services/pipewire/qml.cpp | 64 ++++----------------- src/services/pipewire/qml.hpp | 30 ++++------ src/services/status_notifier/qml.cpp | 42 ++++---------- src/services/status_notifier/qml.hpp | 15 ++--- 11 files changed, 201 insertions(+), 152 deletions(-) create mode 100644 src/core/model.cpp create mode 100644 src/core/model.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b40b807f..88c26241 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -26,6 +26,7 @@ qt_add_library(quickshell-core STATIC imageprovider.cpp transformwatcher.cpp boundcomponent.cpp + model.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/doc.hpp b/src/core/doc.hpp index b619b0a6..e1f2ee4c 100644 --- a/src/core/doc.hpp +++ b/src/core/doc.hpp @@ -10,5 +10,8 @@ #define QSDOC_ELEMENT #define QSDOC_NAMED_ELEMENT(name) +// change the cname used for this type +#define QSDOC_CNAME(name) + // overridden properties #define QSDOC_PROPERTY_OVERRIDE(...) diff --git a/src/core/model.cpp b/src/core/model.cpp new file mode 100644 index 00000000..74c7c284 --- /dev/null +++ b/src/core/model.cpp @@ -0,0 +1,67 @@ +#include "model.hpp" + +#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 != 0) return QVariant(); + return QVariant::fromValue(this->valuesList.at(index.row())); +} + +QHash UntypedObjectModel::roleNames() const { return {{0, "modelData"}}; } + +QQmlListProperty UntypedObjectModel::values() { + return QQmlListProperty( + this, + nullptr, + &UntypedObjectModel::valuesCount, + &UntypedObjectModel::valueAt + ); +} + +qsizetype UntypedObjectModel::valuesCount(QQmlListProperty* property) { + return static_cast(property->object)->valuesList.count(); // NOLINT +} + +QObject* UntypedObjectModel::valueAt(QQmlListProperty* property, qsizetype index) { + return static_cast(property->object)->valuesList.at(index); // NOLINT +} + +void UntypedObjectModel::insertObject(QObject* object, qsizetype index) { + auto iindex = index == -1 ? this->valuesList.length() : index; + auto intIndex = static_cast(iindex); + + this->beginInsertRows(QModelIndex(), intIndex, intIndex); + this->valuesList.insert(iindex, object); + this->endInsertRows(); + emit this->valuesChanged(); +} + +void UntypedObjectModel::removeAt(qsizetype index) { + auto intIndex = static_cast(index); + + this->beginRemoveRows(QModelIndex(), intIndex, intIndex); + this->valuesList.removeAt(index); + this->endRemoveRows(); + emit this->valuesChanged(); +} + +bool UntypedObjectModel::removeObject(const QObject* object) { + auto index = this->valuesList.indexOf(object); + if (index == -1) return false; + + this->removeAt(index); + return true; +} + +qsizetype UntypedObjectModel::indexOf(QObject* object) { return this->valuesList.indexOf(object); } diff --git a/src/core/model.hpp b/src/core/model.hpp new file mode 100644 index 00000000..bcf5ab62 --- /dev/null +++ b/src/core/model.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "doc.hpp" + +///! View into a list of objets +/// Typed view into a list of objects. +/// +/// An ObjectModel works as a QML [Data Model], allowing efficient interaction with +/// components that act on models. It has a single role named `modelData`, to match the +/// behavior of lists. +/// The same information contained in the list model is available as a normal list +/// via the `values` property. +/// +/// #### Differences from a list +/// Unlike with a list, the following property binding will never be updated when `model[3]` changes. +/// ```qml +/// // will not update reactively +/// property var foo: model[3] +/// ``` +/// +/// You can work around this limitation using the `values` property of the model to view it as a list. +/// ```qml +/// // will update reactively +/// property var foo: model.values[3] +/// ``` +/// +/// [Data Model]: https://doc.qt.io/qt-6/qtquick-modelviewsdata-modelview.html#qml-data-models +class UntypedObjectModel: public QAbstractListModel { + QSDOC_CNAME(ObjectModel); + Q_OBJECT; + /// The content of the object model, as a QML list. + /// The values of this property will always be of the type of the model. + Q_PROPERTY(QQmlListProperty values READ values NOTIFY valuesChanged); + QML_NAMED_ELEMENT(ObjectModel); + QML_UNCREATABLE("ObjectModels cannot be created directly."); + +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]] QQmlListProperty values(); + void removeAt(qsizetype index); + + Q_INVOKABLE qsizetype indexOf(QObject* object); + +signals: + void valuesChanged(); + +protected: + void insertObject(QObject* object, qsizetype index = -1); + bool removeObject(const QObject* object); + + QVector valuesList; + +private: + static qsizetype valuesCount(QQmlListProperty* property); + static QObject* valueAt(QQmlListProperty* property, qsizetype index); +}; + +template +class ObjectModel: public UntypedObjectModel { +public: + explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {} + + [[nodiscard]] const QVector& valueList() const { + return *reinterpret_cast*>(&this->valuesList); // NOLINT + } + + void insertObject(T* object, qsizetype index = -1) { + this->UntypedObjectModel::insertObject(object, index); + } + + void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); } +}; diff --git a/src/core/module.md b/src/core/module.md index 8eb9b638..dc1f204d 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -18,5 +18,6 @@ headers = [ "easingcurve.hpp", "transformwatcher.hpp", "boundcomponent.hpp", + "model.hpp", ] ----- diff --git a/src/services/mpris/watcher.cpp b/src/services/mpris/watcher.cpp index 1e107660..8c67a4d6 100644 --- a/src/services/mpris/watcher.cpp +++ b/src/services/mpris/watcher.cpp @@ -4,14 +4,12 @@ #include #include #include -#include #include #include #include #include -#include -#include +#include "../../core/model.hpp" #include "player.hpp" namespace qs::service::mpris { @@ -74,34 +72,15 @@ void MprisWatcher::onServiceUnregistered(const QString& service) { void MprisWatcher::onPlayerReady() { auto* player = qobject_cast(this->sender()); - this->readyPlayers.push_back(player); - emit this->playersChanged(); + this->readyPlayers.insertObject(player); } void MprisWatcher::onPlayerDestroyed(QObject* object) { auto* player = static_cast(object); // NOLINT - - if (this->readyPlayers.removeOne(player)) { - emit this->playersChanged(); - } + this->readyPlayers.removeObject(player); } -QQmlListProperty MprisWatcher::players() { - return QQmlListProperty( - this, - nullptr, - &MprisWatcher::playersCount, - &MprisWatcher::playerAt - ); -} - -qsizetype MprisWatcher::playersCount(QQmlListProperty* property) { - return static_cast(property->object)->readyPlayers.count(); // NOLINT -} - -MprisPlayer* MprisWatcher::playerAt(QQmlListProperty* property, qsizetype index) { - return static_cast(property->object)->readyPlayers.at(index); // NOLINT -} +ObjectModel* MprisWatcher::players() { return &this->readyPlayers; } void MprisWatcher::registerPlayer(const QString& address) { if (this->mPlayers.contains(address)) { diff --git a/src/services/mpris/watcher.hpp b/src/services/mpris/watcher.hpp index a1e4df7c..91275c7e 100644 --- a/src/services/mpris/watcher.hpp +++ b/src/services/mpris/watcher.hpp @@ -4,14 +4,13 @@ #include #include #include -#include #include #include #include #include #include -#include +#include "../../core/model.hpp" #include "player.hpp" namespace qs::service::mpris { @@ -22,15 +21,12 @@ class MprisWatcher: public QObject { QML_NAMED_ELEMENT(Mpris); QML_SINGLETON; /// All connected MPRIS players. - Q_PROPERTY(QQmlListProperty players READ players NOTIFY playersChanged); + Q_PROPERTY(ObjectModel* players READ players CONSTANT); public: explicit MprisWatcher(QObject* parent = nullptr); - [[nodiscard]] QQmlListProperty players(); - -signals: - void playersChanged(); + [[nodiscard]] ObjectModel* players(); private slots: void onServiceRegistered(const QString& service); @@ -39,15 +35,12 @@ private slots: void onPlayerDestroyed(QObject* object); private: - static qsizetype playersCount(QQmlListProperty* property); - static MprisPlayer* playerAt(QQmlListProperty* property, qsizetype index); - void registerExisting(); void registerPlayer(const QString& address); QDBusServiceWatcher serviceWatcher; QHash mPlayers; - QList readyPlayers; + ObjectModel readyPlayers {this}; }; } // namespace qs::service::mpris diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp index a6617d29..b40de687 100644 --- a/src/services/pipewire/qml.cpp +++ b/src/services/pipewire/qml.cpp @@ -8,6 +8,7 @@ #include #include +#include "../../core/model.hpp" #include "connection.hpp" #include "link.hpp" #include "metadata.hpp" @@ -65,88 +66,43 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) { // clang-format on } -QQmlListProperty Pipewire::nodes() { - return QQmlListProperty(this, nullptr, &Pipewire::nodesCount, &Pipewire::nodeAt); -} - -qsizetype Pipewire::nodesCount(QQmlListProperty* property) { - return static_cast(property->object)->mNodes.count(); // NOLINT -} - -PwNodeIface* Pipewire::nodeAt(QQmlListProperty* property, qsizetype index) { - return static_cast(property->object)->mNodes.at(index); // NOLINT -} +ObjectModel* Pipewire::nodes() { return &this->mNodes; } void Pipewire::onNodeAdded(PwNode* node) { auto* iface = PwNodeIface::instance(node); QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onNodeRemoved); - - this->mNodes.push_back(iface); - emit this->nodesChanged(); + this->mNodes.insertObject(iface); } void Pipewire::onNodeRemoved(QObject* object) { auto* iface = static_cast(object); // NOLINT - this->mNodes.removeOne(iface); - emit this->nodesChanged(); + this->mNodes.removeObject(iface); } -QQmlListProperty Pipewire::links() { - return QQmlListProperty(this, nullptr, &Pipewire::linksCount, &Pipewire::linkAt); -} - -qsizetype Pipewire::linksCount(QQmlListProperty* property) { - return static_cast(property->object)->mLinks.count(); // NOLINT -} - -PwLinkIface* Pipewire::linkAt(QQmlListProperty* property, qsizetype index) { - return static_cast(property->object)->mLinks.at(index); // NOLINT -} +ObjectModel* Pipewire::links() { return &this->mLinks; } void Pipewire::onLinkAdded(PwLink* link) { auto* iface = PwLinkIface::instance(link); QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkRemoved); - - this->mLinks.push_back(iface); - emit this->linksChanged(); + this->mLinks.insertObject(iface); } void Pipewire::onLinkRemoved(QObject* object) { auto* iface = static_cast(object); // NOLINT - this->mLinks.removeOne(iface); - emit this->linksChanged(); + this->mLinks.removeObject(iface); } -QQmlListProperty Pipewire::linkGroups() { - return QQmlListProperty( - this, - nullptr, - &Pipewire::linkGroupsCount, - &Pipewire::linkGroupAt - ); -} - -qsizetype Pipewire::linkGroupsCount(QQmlListProperty* property) { - return static_cast(property->object)->mLinkGroups.count(); // NOLINT -} - -PwLinkGroupIface* -Pipewire::linkGroupAt(QQmlListProperty* property, qsizetype index) { - return static_cast(property->object)->mLinkGroups.at(index); // NOLINT -} +ObjectModel* Pipewire::linkGroups() { return &this->mLinkGroups; } void Pipewire::onLinkGroupAdded(PwLinkGroup* linkGroup) { auto* iface = PwLinkGroupIface::instance(linkGroup); QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkGroupRemoved); - - this->mLinkGroups.push_back(iface); - emit this->linkGroupsChanged(); + this->mLinkGroups.insertObject(iface); } void Pipewire::onLinkGroupRemoved(QObject* object) { auto* iface = static_cast(object); // NOLINT - this->mLinkGroups.removeOne(iface); - emit this->linkGroupsChanged(); + this->mLinkGroups.removeObject(iface); } PwNodeIface* Pipewire::defaultAudioSink() const { // NOLINT diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 9b452727..8d456419 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -8,6 +8,7 @@ #include #include +#include "../../core/model.hpp" #include "link.hpp" #include "node.hpp" #include "registry.hpp" @@ -52,11 +53,11 @@ class Pipewire: public QObject { Q_OBJECT; // clang-format off /// All pipewire nodes. - Q_PROPERTY(QQmlListProperty nodes READ nodes NOTIFY nodesChanged); + Q_PROPERTY(ObjectModel* nodes READ nodes CONSTANT); /// All pipewire links. - Q_PROPERTY(QQmlListProperty links READ links NOTIFY linksChanged); + Q_PROPERTY(ObjectModel* links READ links CONSTANT); /// All pipewire link groups. - Q_PROPERTY(QQmlListProperty linkGroups READ linkGroups NOTIFY linkGroupsChanged); + Q_PROPERTY(ObjectModel* linkGroups READ linkGroups CONSTANT); /// The default audio sink or `null`. Q_PROPERTY(PwNodeIface* defaultAudioSink READ defaultAudioSink NOTIFY defaultAudioSinkChanged); /// The default audio source or `null`. @@ -68,16 +69,13 @@ class Pipewire: public QObject { public: explicit Pipewire(QObject* parent = nullptr); - [[nodiscard]] QQmlListProperty nodes(); - [[nodiscard]] QQmlListProperty links(); - [[nodiscard]] QQmlListProperty linkGroups(); + [[nodiscard]] ObjectModel* nodes(); + [[nodiscard]] ObjectModel* links(); + [[nodiscard]] ObjectModel* linkGroups(); [[nodiscard]] PwNodeIface* defaultAudioSink() const; [[nodiscard]] PwNodeIface* defaultAudioSource() const; signals: - void nodesChanged(); - void linksChanged(); - void linkGroupsChanged(); void defaultAudioSinkChanged(); void defaultAudioSourceChanged(); @@ -90,17 +88,9 @@ private slots: void onLinkGroupRemoved(QObject* object); private: - static qsizetype nodesCount(QQmlListProperty* property); - static PwNodeIface* nodeAt(QQmlListProperty* property, qsizetype index); - static qsizetype linksCount(QQmlListProperty* property); - static PwLinkIface* linkAt(QQmlListProperty* property, qsizetype index); - static qsizetype linkGroupsCount(QQmlListProperty* property); - static PwLinkGroupIface* - linkGroupAt(QQmlListProperty* property, qsizetype index); - - QVector mNodes; - QVector mLinks; - QVector mLinkGroups; + ObjectModel mNodes {this}; + ObjectModel mLinks {this}; + ObjectModel mLinkGroups {this}; }; ///! Tracks all link connections to a given node. diff --git a/src/services/status_notifier/qml.cpp b/src/services/status_notifier/qml.cpp index cea5646e..f81a6381 100644 --- a/src/services/status_notifier/qml.cpp +++ b/src/services/status_notifier/qml.cpp @@ -9,6 +9,7 @@ #include #include +#include "../../core/model.hpp" #include "../../dbus/dbusmenu/dbusmenu.hpp" #include "../../dbus/properties.hpp" #include "host.hpp" @@ -106,46 +107,25 @@ SystemTray::SystemTray(QObject* parent): QObject(parent) { // clang-format on for (auto* item: host->items()) { - this->mItems.push_back(new SystemTrayItem(item, this)); + this->mItems.insertObject(new SystemTrayItem(item, this)); } } void SystemTray::onItemRegistered(StatusNotifierItem* item) { - this->mItems.push_back(new SystemTrayItem(item, this)); - emit this->itemsChanged(); + this->mItems.insertObject(new SystemTrayItem(item, this)); } void SystemTray::onItemUnregistered(StatusNotifierItem* item) { - SystemTrayItem* trayItem = nullptr; - - this->mItems.removeIf([item, &trayItem](SystemTrayItem* testItem) { - if (testItem->item == item) { - trayItem = testItem; - return true; - } else return false; - }); - - emit this->itemsChanged(); - - delete trayItem; + for (const auto* storedItem: this->mItems.valueList()) { + if (storedItem->item == item) { + this->mItems.removeObject(storedItem); + delete storedItem; + break; + } + } } -QQmlListProperty SystemTray::items() { - return QQmlListProperty( - this, - nullptr, - &SystemTray::itemsCount, - &SystemTray::itemAt - ); -} - -qsizetype SystemTray::itemsCount(QQmlListProperty* property) { - return reinterpret_cast(property->object)->mItems.count(); // NOLINT -} - -SystemTrayItem* SystemTray::itemAt(QQmlListProperty* property, qsizetype index) { - return reinterpret_cast(property->object)->mItems.at(index); // NOLINT -} +ObjectModel* SystemTray::items() { return &this->mItems; } SystemTrayItem* SystemTrayMenuWatcher::trayItem() const { return this->item; } diff --git a/src/services/status_notifier/qml.hpp b/src/services/status_notifier/qml.hpp index 01f6bb05..e55509df 100644 --- a/src/services/status_notifier/qml.hpp +++ b/src/services/status_notifier/qml.hpp @@ -2,10 +2,9 @@ #include #include -#include #include -#include +#include "../../core/model.hpp" #include "item.hpp" namespace SystemTrayStatus { // NOLINT @@ -108,27 +107,21 @@ signals: class SystemTray: public QObject { Q_OBJECT; /// List of all system tray icons. - Q_PROPERTY(QQmlListProperty items READ items NOTIFY itemsChanged); + Q_PROPERTY(ObjectModel* items READ items CONSTANT); QML_ELEMENT; QML_SINGLETON; public: explicit SystemTray(QObject* parent = nullptr); - [[nodiscard]] QQmlListProperty items(); - -signals: - void itemsChanged(); + [[nodiscard]] ObjectModel* items(); private slots: void onItemRegistered(qs::service::sni::StatusNotifierItem* item); void onItemUnregistered(qs::service::sni::StatusNotifierItem* item); private: - static qsizetype itemsCount(QQmlListProperty* property); - static SystemTrayItem* itemAt(QQmlListProperty* property, qsizetype index); - - QList mItems; + ObjectModel mItems {this}; }; ///! Accessor for SystemTrayItem menus. From 06240ccf8027a38e72eaae2d598d262258688b27 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 23 May 2024 18:15:49 -0700 Subject: [PATCH 014/305] service/mpris: improve compatibility with noncompliant players --- src/services/mpris/player.cpp | 30 +++++++++++++++++++++++------- src/services/mpris/player.hpp | 1 + 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index e17f3e82..b659badf 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -192,15 +192,15 @@ void MprisPlayer::setPosition(qreal position) { } auto target = static_cast(position * 1000) * 1000; - this->pPosition.set(target); if (!this->mTrackId.isEmpty()) { this->player->SetPosition(QDBusObjectPath(this->mTrackId), target); - return; } else { auto pos = this->positionMs() * 1000; this->player->Seek(target - pos); } + + this->pPosition.set(target); } void MprisPlayer::onPositionChanged() { @@ -247,6 +247,8 @@ void MprisPlayer::setVolume(qreal volume) { QVariantMap MprisPlayer::metadata() const { return this->pMetadata.get(); } void MprisPlayer::onMetadataChanged() { + emit this->metadataChanged(); + auto lengthVariant = this->pMetadata.get().value("mpris:length"); qlonglong length = -1; if (lengthVariant.isValid() && lengthVariant.canConvert()) { @@ -258,20 +260,34 @@ void MprisPlayer::onMetadataChanged() { emit this->lengthChanged(); } + auto trackChanged = false; + auto trackidVariant = this->pMetadata.get().value("mpris:trackid"); if (trackidVariant.isValid() && trackidVariant.canConvert()) { auto trackId = trackidVariant.value(); if (trackId != this->mTrackId) { this->mTrackId = trackId; - emit this->trackChanged(); + trackChanged = true; } - - // Some players don't seem to send position updats or seeks on track change. - this->pPosition.update(); } - emit this->metadataChanged(); + // Helps to catch players without trackid. + auto urlVariant = this->pMetadata.get().value("xesam:url"); + if (urlVariant.isValid() && urlVariant.canConvert()) { + auto url = urlVariant.value(); + + if (url != this->mUrl) { + this->mUrl = url; + trackChanged = true; + } + } + + if (trackChanged) { + // Some players don't seem to send position updates or seeks on track change. + this->pPosition.update(); + emit this->trackChanged(); + } } MprisPlaybackState::Enum MprisPlayer::playbackState() const { return this->mPlaybackState; } diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index ddbb87cb..1172505a 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -326,6 +326,7 @@ private: DBusMprisPlayerApp* app = nullptr; DBusMprisPlayer* player = nullptr; QString mTrackId; + QString mUrl; }; } // namespace qs::service::mpris From 5a84e734426fc73e6a4bef993244ea422782e68b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 23 May 2024 19:12:21 -0700 Subject: [PATCH 015/305] core/objectmodel: add signals for changes to the list --- src/core/model.cpp | 11 +++++++++-- src/core/model.hpp | 8 ++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/core/model.cpp b/src/core/model.cpp index 74c7c284..64f7d765 100644 --- a/src/core/model.cpp +++ b/src/core/model.cpp @@ -39,21 +39,28 @@ QObject* UntypedObjectModel::valueAt(QQmlListProperty* property, qsizet void UntypedObjectModel::insertObject(QObject* object, qsizetype index) { auto iindex = index == -1 ? this->valuesList.length() : index; - auto intIndex = static_cast(iindex); + emit this->objectInsertedPre(object, index); + auto intIndex = static_cast(iindex); this->beginInsertRows(QModelIndex(), intIndex, intIndex); this->valuesList.insert(iindex, object); this->endInsertRows(); + emit this->valuesChanged(); + emit this->objectInsertedPost(object, index); } void UntypedObjectModel::removeAt(qsizetype index) { - auto intIndex = static_cast(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) { diff --git a/src/core/model.hpp b/src/core/model.hpp index bcf5ab62..10465bba 100644 --- a/src/core/model.hpp +++ b/src/core/model.hpp @@ -57,6 +57,14 @@ public: signals: void valuesChanged(); + /// Sent immediately before an object is inserted into the list. + void objectInsertedPre(QObject* object, qsizetype index); + /// Sent immediately after an object is inserted into the list. + void objectInsertedPost(QObject* object, qsizetype index); + /// Sent immediately before an object is removed from the list. + void objectRemovedPre(QObject* object, qsizetype index); + /// Sent immediately after an object is removed from the list. + void objectRemovedPost(QObject* object, qsizetype index); protected: void insertObject(QObject* object, qsizetype index = -1); From 4e92d8299299c926f837fca98513599220e89e90 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 27 May 2024 22:51:49 -0700 Subject: [PATCH 016/305] core: add options to enable QML debugging --- src/core/main.cpp | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/core/main.cpp b/src/core/main.cpp index 2cfd4d9c..24acdfc0 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,9 @@ int qs_main(int argc, char** argv) { auto desktopSettingsAware = true; QHash envOverrides; + int debugPort = -1; + bool waitForDebug = false; + { const auto app = QCoreApplication(argc, argv); QCoreApplication::setApplicationName("quickshell"); @@ -44,6 +48,8 @@ int qs_main(int argc, char** argv) { auto configOption = QCommandLineOption({"c", "config"}, "Name of a configuration in the manifest.", "name"); auto pathOption = QCommandLineOption({"p", "path"}, "Path to a configuration file.", "path"); auto workdirOption = QCommandLineOption({"d", "workdir"}, "Initial working directory.", "path"); + auto debugPortOption = QCommandLineOption("debugport", "Enable the QML debugger.", "port"); + auto debugWaitOption = QCommandLineOption("waitfordebug", "Wait for debugger connection before launching."); // clang-format on parser.addOption(currentOption); @@ -51,8 +57,30 @@ int qs_main(int argc, char** argv) { parser.addOption(configOption); parser.addOption(pathOption); parser.addOption(workdirOption); + parser.addOption(debugPortOption); + parser.addOption(debugWaitOption); parser.process(app); + auto debugPortStr = parser.value(debugPortOption); + if (!debugPortStr.isEmpty()) { + auto ok = false; + debugPort = debugPortStr.toInt(&ok); + + if (!ok) { + qCritical() << "Debug port must be a valid port number."; + return -1; + } + } + + if (parser.isSet(debugWaitOption)) { + if (debugPort == -1) { + qCritical() << "Cannot wait for debugger without a debug port set."; + return -1; + } + + waitForDebug = true; + } + { auto printCurrent = parser.isSet(currentOption); @@ -308,6 +336,13 @@ int qs_main(int argc, char** argv) { app = new QGuiApplication(argc, argv); } + if (debugPort != -1) { + QQmlDebuggingEnabler::enableDebugging(true); + auto wait = waitForDebug ? QQmlDebuggingEnabler::WaitForClient + : QQmlDebuggingEnabler::DoNotWaitForClient; + QQmlDebuggingEnabler::startTcpDebugServer(debugPort, wait); + } + if (!workingDirectory.isEmpty()) { QDir::setCurrent(workingDirectory); } From 7ad3671dd1d3884e45b53212b3cd90065c86ee8b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 28 May 2024 15:36:25 -0700 Subject: [PATCH 017/305] core/reloader: fix file watcher compatibility with vim --- src/core/generation.cpp | 30 +++++++++++++++++++++++++++++- src/core/generation.hpp | 3 +++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 77e4a9cb..a0b465f8 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -117,13 +119,21 @@ void EngineGeneration::setWatchingFiles(bool watching) { for (auto& file: this->scanner.scannedFiles) { this->watcher->addPath(file); + this->watcher->addPath(QFileInfo(file).dir().absolutePath()); } QObject::connect( this->watcher, &QFileSystemWatcher::fileChanged, this, - &EngineGeneration::filesChanged + &EngineGeneration::onFileChanged + ); + + QObject::connect( + this->watcher, + &QFileSystemWatcher::directoryChanged, + this, + &EngineGeneration::onDirectoryChanged ); } } else { @@ -134,6 +144,24 @@ void EngineGeneration::setWatchingFiles(bool watching) { } } +void EngineGeneration::onFileChanged(const QString& name) { + if (!this->watcher->files().contains(name)) { + this->deletedWatchedFiles.push_back(name); + } else { + emit this->filesChanged(); + } +} + +void EngineGeneration::onDirectoryChanged() { + // try to find any files that were just deleted from a replace operation + for (auto& file: this->deletedWatchedFiles) { + if (QFileInfo(file).exists()) { + emit this->filesChanged(); + break; + } + } +} + void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) { auto* obj = dynamic_cast(controller); diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 11ebf0be..3c8f3997 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -40,6 +40,7 @@ public: ShellRoot* root = nullptr; SingletonRegistry singletonRegistry; QFileSystemWatcher* watcher = nullptr; + QVector deletedWatchedFiles; DelayedQmlIncubationController delayedIncubationController; bool reloadComplete = false; @@ -50,6 +51,8 @@ signals: void reloadFinished(); private slots: + void onFileChanged(const QString& name); + void onDirectoryChanged(); void incubationControllerDestroyed(); private: From 33fac6779815fa9d14b588186c700b0529c9e3ec Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 28 May 2024 20:22:01 -0700 Subject: [PATCH 018/305] core: use the simple animation driver Seems to provide much higher quality animations. --- src/core/main.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/core/main.cpp b/src/core/main.cpp index 24acdfc0..220bde30 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -326,6 +326,13 @@ int qs_main(int argc, char** argv) { qputenv(var.toUtf8(), val.toUtf8()); } + // The simple animation driver seems to work far better than the default one + // when more than one window is in use, and even with a single window appears + // to improve animation quality. + if (!qEnvironmentVariableIsSet("QSG_USE_SIMPLE_ANIMATION_DRIVER")) { + qputenv("QSG_USE_SIMPLE_ANIMATION_DRIVER", "1"); + } + QGuiApplication::setDesktopSettingsAware(desktopSettingsAware); QGuiApplication* app = nullptr; From 0519acf1d6625a9a423fcc49ef90be1a55570091 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 29 May 2024 15:07:10 -0700 Subject: [PATCH 019/305] core: support `root:` and `root:/` paths for the config root This works everywhere urls are accepted and rewrites them from the config root as a qsintercept url. --- src/core/generation.cpp | 6 ++++-- src/core/generation.hpp | 4 +++- src/core/qsintercept.cpp | 17 ++++++++++++++++- src/core/qsintercept.hpp | 8 +++++++- src/core/rootwrapper.cpp | 5 +++-- src/core/scan.cpp | 10 +++++++++- src/core/scan.hpp | 6 ++++++ 7 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index a0b465f8..8dbad323 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -27,8 +27,10 @@ static QHash g_generations; // NOLINT -EngineGeneration::EngineGeneration(QmlScanner scanner) - : scanner(std::move(scanner)) +EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) + : rootPath(rootPath) + , scanner(std::move(scanner)) + , urlInterceptor(this->rootPath) , interceptNetFactory(this->scanner.qmldirIntercepts) , engine(new QQmlEngine()) { g_generations.insert(this->engine, this); diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 3c8f3997..c077c1bf 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -19,7 +20,7 @@ class EngineGeneration: public QObject { Q_OBJECT; public: - explicit EngineGeneration(QmlScanner scanner); + explicit EngineGeneration(const QDir& rootPath, QmlScanner scanner); ~EngineGeneration() override; Q_DISABLE_COPY_MOVE(EngineGeneration); @@ -33,6 +34,7 @@ public: static EngineGeneration* findObjectGeneration(QObject* object); RootWrapper* wrapper = nullptr; + QDir rootPath; QmlScanner scanner; QsUrlInterceptor urlInterceptor; QsInterceptNetworkAccessManagerFactory interceptNetFactory; diff --git a/src/core/qsintercept.cpp b/src/core/qsintercept.cpp index 2eaf498e..ba46ab7b 100644 --- a/src/core/qsintercept.cpp +++ b/src/core/qsintercept.cpp @@ -16,7 +16,22 @@ Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg); -QUrl QsUrlInterceptor::intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) { +QUrl QsUrlInterceptor::intercept( + const QUrl& originalUrl, + QQmlAbstractUrlInterceptor::DataType type +) { + auto url = originalUrl; + + if (url.scheme() == "root") { + url.setScheme("qsintercept"); + + auto path = url.path(); + if (path.startsWith('/')) path = path.sliced(1); + url.setPath(this->configRoot.filePath(path)); + + qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url; + } + // Some types such as Image take into account where they are loading from, and force // asynchronous loading over a network. qsintercept is considered to be over a network. if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") { diff --git a/src/core/qsintercept.hpp b/src/core/qsintercept.hpp index d51b78e6..57923568 100644 --- a/src/core/qsintercept.hpp +++ b/src/core/qsintercept.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -13,7 +14,12 @@ Q_DECLARE_LOGGING_CATEGORY(logQsIntercept); class QsUrlInterceptor: public QQmlAbstractUrlInterceptor { public: - QUrl intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) override; + explicit QsUrlInterceptor(const QDir& configRoot): configRoot(configRoot) {} + + QUrl intercept(const QUrl& originalUrl, QQmlAbstractUrlInterceptor::DataType type) override; + +private: + QDir configRoot; }; class QsInterceptDataReply: public QNetworkReply { diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index ed2ef4b7..ea2adf18 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -42,10 +42,11 @@ RootWrapper::~RootWrapper() { } void RootWrapper::reloadGraph(bool hard) { - auto scanner = QmlScanner(); + auto rootPath = QFileInfo(this->rootPath).dir(); + auto scanner = QmlScanner(rootPath); scanner.scanQmlFile(this->rootPath); - auto* generation = new EngineGeneration(std::move(scanner)); + auto* generation = new EngineGeneration(rootPath, std::move(scanner)); generation->wrapper = this; // todo: move into EngineGeneration diff --git a/src/core/scan.cpp b/src/core/scan.cpp index f5f078aa..59ec05b6 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -103,7 +103,15 @@ bool QmlScanner::scanQmlFile(const QString& path) { this->scanDir(currentdir.path()); for (auto& import: imports) { - auto ipath = currentdir.filePath(import); + QString ipath; + if (import.startsWith("root:")) { + auto path = import.sliced(5); + if (path.startsWith('/')) path = path.sliced(1); + ipath = this->rootPath.filePath(path); + } else { + ipath = currentdir.filePath(import); + } + auto cpath = QFileInfo(ipath).canonicalFilePath(); if (cpath.isEmpty()) { diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 32a6166d..e3071a88 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -10,6 +11,8 @@ Q_DECLARE_LOGGING_CATEGORY(logQmlScanner); // expects canonical paths class QmlScanner { public: + QmlScanner(const QDir& rootPath): rootPath(rootPath) {} + void scanDir(const QString& path); // returns if the file has a singleton bool scanQmlFile(const QString& path); @@ -17,4 +20,7 @@ public: QVector scannedDirs; QVector scannedFiles; QHash qmldirIntercepts; + +private: + QDir rootPath; }; From 569c40494d8792b161c7141f674b14aeafd99fb6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 29 May 2024 19:29:57 -0700 Subject: [PATCH 020/305] all: import module dependencies via qmldir Improves compatibility with qml tooling. --- src/wayland/CMakeLists.txt | 11 +++++++++- src/wayland/hyprland/CMakeLists.txt | 13 ++++++++++-- .../hyprland/focus_grab/CMakeLists.txt | 9 +-------- src/wayland/hyprland/focus_grab/init.cpp | 20 ------------------- .../hyprland/global_shortcuts/CMakeLists.txt | 9 +-------- .../hyprland/global_shortcuts/init.cpp | 20 ------------------- src/wayland/init.cpp | 7 ------- 7 files changed, 23 insertions(+), 66 deletions(-) delete mode 100644 src/wayland/hyprland/focus_grab/init.cpp delete mode 100644 src/wayland/hyprland/global_shortcuts/init.cpp diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 48140a91..f20bc11d 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -51,16 +51,19 @@ endfunction() # ----- qt_add_library(quickshell-wayland STATIC) -qt_add_qml_module(quickshell-wayland URI Quickshell.Wayland VERSION 0.1) # required to make sure the constructor is linked add_library(quickshell-wayland-init OBJECT init.cpp) +set(WAYLAND_MODULES) + if (WAYLAND_WLR_LAYERSHELL) target_sources(quickshell-wayland PRIVATE wlr_layershell.cpp) add_subdirectory(wlr_layershell) target_compile_definitions(quickshell-wayland PRIVATE QS_WAYLAND_WLR_LAYERSHELL) target_compile_definitions(quickshell-wayland-init PRIVATE QS_WAYLAND_WLR_LAYERSHELL) + + list(APPEND WAYLAND_MODULES Quickshell.Wayland._WlrLayerShell) endif() if (WAYLAND_SESSION_LOCK) @@ -75,6 +78,12 @@ endif() target_link_libraries(quickshell-wayland PRIVATE ${QT_DEPS}) target_link_libraries(quickshell-wayland-init PRIVATE ${QT_DEPS}) +qt_add_qml_module(quickshell-wayland + URI Quickshell.Wayland + VERSION 0.1 + IMPORTS ${WAYLAND_MODULES} +) + qs_pch(quickshell-wayland) qs_pch(quickshell-waylandplugin) qs_pch(quickshell-wayland-init) diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt index 06121a7e..be6bf49c 100644 --- a/src/wayland/hyprland/CMakeLists.txt +++ b/src/wayland/hyprland/CMakeLists.txt @@ -1,15 +1,24 @@ qt_add_library(quickshell-hyprland STATIC) -qt_add_qml_module(quickshell-hyprland URI Quickshell.Hyprland VERSION 0.1) + +target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS}) + +set(HYPRLAND_MODULES) if (HYPRLAND_FOCUS_GRAB) add_subdirectory(focus_grab) + list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._FocusGrab) endif() if (HYPRLAND_GLOBAL_SHORTCUTS) add_subdirectory(global_shortcuts) + list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._GlobalShortcuts) endif() -target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS}) +qt_add_qml_module(quickshell-hyprland + URI Quickshell.Hyprland + VERSION 0.1 + IMPORTS ${HYPRLAND_MODULES} +) qs_pch(quickshell-hyprland) qs_pch(quickshell-hyprlandplugin) diff --git a/src/wayland/hyprland/focus_grab/CMakeLists.txt b/src/wayland/hyprland/focus_grab/CMakeLists.txt index 587ae939..1e37c9fe 100644 --- a/src/wayland/hyprland/focus_grab/CMakeLists.txt +++ b/src/wayland/hyprland/focus_grab/CMakeLists.txt @@ -9,21 +9,14 @@ qt_add_qml_module(quickshell-hyprland-focus-grab VERSION 0.1 ) -add_library(quickshell-hyprland-focus-grab-init OBJECT init.cpp) - wl_proto(quickshell-hyprland-focus-grab hyprland-focus-grab-v1 "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml" ) target_link_libraries(quickshell-hyprland-focus-grab PRIVATE ${QT_DEPS} wayland-client) -target_link_libraries(quickshell-hyprland-focus-grab-init PRIVATE ${QT_DEPS}) qs_pch(quickshell-hyprland-focus-grab) qs_pch(quickshell-hyprland-focus-grabplugin) -qs_pch(quickshell-hyprland-focus-grab-init) -target_link_libraries(quickshell PRIVATE - quickshell-hyprland-focus-grabplugin - quickshell-hyprland-focus-grab-init -) +target_link_libraries(quickshell PRIVATE quickshell-hyprland-focus-grabplugin) diff --git a/src/wayland/hyprland/focus_grab/init.cpp b/src/wayland/hyprland/focus_grab/init.cpp deleted file mode 100644 index 784c7f26..00000000 --- a/src/wayland/hyprland/focus_grab/init.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include - -#include "../../../core/plugin.hpp" - -namespace { - -class HyprlandFocusGrabPlugin: public QuickshellPlugin { - void registerTypes() override { - qmlRegisterModuleImport( - "Quickshell.Hyprland", - QQmlModuleImportModuleAny, - "Quickshell.Hyprland._FocusGrab", - QQmlModuleImportLatest - ); - } -}; - -QS_REGISTER_PLUGIN(HyprlandFocusGrabPlugin); - -} // namespace diff --git a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt index 804c0a3c..2ccfb74d 100644 --- a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt +++ b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt @@ -9,21 +9,14 @@ qt_add_qml_module(quickshell-hyprland-global-shortcuts VERSION 0.1 ) -add_library(quickshell-hyprland-global-shortcuts-init OBJECT init.cpp) - wl_proto(quickshell-hyprland-global-shortcuts hyprland-global-shortcuts-v1 "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-global-shortcuts-v1.xml" ) target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE ${QT_DEPS} wayland-client) -target_link_libraries(quickshell-hyprland-global-shortcuts-init PRIVATE ${QT_DEPS}) qs_pch(quickshell-hyprland-global-shortcuts) qs_pch(quickshell-hyprland-global-shortcutsplugin) -qs_pch(quickshell-hyprland-global-shortcuts-init) -target_link_libraries(quickshell PRIVATE - quickshell-hyprland-global-shortcutsplugin - quickshell-hyprland-global-shortcuts-init -) +target_link_libraries(quickshell PRIVATE quickshell-hyprland-global-shortcutsplugin) diff --git a/src/wayland/hyprland/global_shortcuts/init.cpp b/src/wayland/hyprland/global_shortcuts/init.cpp deleted file mode 100644 index 12fed07f..00000000 --- a/src/wayland/hyprland/global_shortcuts/init.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include - -#include "../../../core/plugin.hpp" - -namespace { - -class HyprlandFocusGrabPlugin: public QuickshellPlugin { - void registerTypes() override { - qmlRegisterModuleImport( - "Quickshell.Hyprland", - QQmlModuleImportModuleAny, - "Quickshell.Hyprland._GlobalShortcuts", - QQmlModuleImportLatest - ); - } -}; - -QS_REGISTER_PLUGIN(HyprlandFocusGrabPlugin); - -} // namespace diff --git a/src/wayland/init.cpp b/src/wayland/init.cpp index 194bad4c..95adb248 100644 --- a/src/wayland/init.cpp +++ b/src/wayland/init.cpp @@ -34,13 +34,6 @@ class WaylandPlugin: public QuickshellPlugin { // will not be registered. This can be worked around with a module import which makes // the QML_ELMENT module import the old register-type style module. - qmlRegisterModuleImport( - "Quickshell.Wayland", - QQmlModuleImportModuleAny, - "Quickshell.Wayland._WlrLayerShell", - QQmlModuleImportLatest - ); - qmlRegisterModuleImport( "Quickshell", QQmlModuleImportModuleAny, From 7feae55ebe276d1fb0f68fa711758dc8cb0a6393 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 30 May 2024 02:39:37 -0700 Subject: [PATCH 021/305] core/reloader: add reload signals for visual notifications --- src/core/generation.cpp | 6 +++++- src/core/generation.hpp | 4 ++++ src/core/qmlglobal.cpp | 11 +++++++++++ src/core/qmlglobal.hpp | 10 ++++++++-- src/core/rootwrapper.cpp | 23 ++++++++++++++++++++--- 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 8dbad323..1021566b 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -265,12 +265,16 @@ void EngineGeneration::assignIncubationController() { this->engine->setIncubationController(controller); } +EngineGeneration* EngineGeneration::findEngineGeneration(QQmlEngine* engine) { + return g_generations.value(engine); +} + EngineGeneration* EngineGeneration::findObjectGeneration(QObject* object) { while (object != nullptr) { auto* context = QQmlEngine::contextForObject(object); if (context != nullptr) { - if (auto* generation = g_generations.value(context->engine())) { + if (auto* generation = EngineGeneration::findEngineGeneration(context->engine())) { return generation; } } diff --git a/src/core/generation.hpp b/src/core/generation.hpp index c077c1bf..f757113e 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -15,6 +16,7 @@ #include "singleton.hpp" class RootWrapper; +class QuickshellGlobal; class EngineGeneration: public QObject { Q_OBJECT; @@ -31,6 +33,7 @@ public: void registerIncubationController(QQmlIncubationController* controller); void deregisterIncubationController(QQmlIncubationController* controller); + static EngineGeneration* findEngineGeneration(QQmlEngine* engine); static EngineGeneration* findObjectGeneration(QObject* object); RootWrapper* wrapper = nullptr; @@ -45,6 +48,7 @@ public: QVector deletedWatchedFiles; DelayedQmlIncubationController delayedIncubationController; bool reloadComplete = false; + QuickshellGlobal* qsgInstance = nullptr; void destroy(); diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 70d7b416..05197f26 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -187,3 +187,14 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT return qEnvironmentVariable(vstr.data()); } + +QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) { + auto* qsg = new QuickshellGlobal(); + auto* generation = EngineGeneration::findEngineGeneration(engine); + + if (generation->qsgInstance == nullptr) { + generation->qsgInstance = qsg; + } + + return qsg; +} diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 83ef68d4..8de55fc2 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -110,8 +110,6 @@ class QuickshellGlobal: public QObject { public: [[nodiscard]] qint32 processId() const; - QuickshellGlobal(QObject* parent = nullptr); - QQmlListProperty screens(); /// Reload the shell from the [ShellRoot]. @@ -133,17 +131,25 @@ public: [[nodiscard]] bool watchFiles() const; void setWatchFiles(bool watchFiles); + static QuickshellGlobal* create(QQmlEngine* engine, QJSEngine* /*unused*/); + signals: /// Sent when the last window is closed. /// /// To make the application exit when the last window is closed run `Qt.quit()`. void lastWindowClosed(); + /// The reload sequence has completed successfully. + void reloadCompleted(); + /// The reload sequence has failed. + void reloadFailed(QString errorString); void screensChanged(); void workingDirectoryChanged(); void watchFilesChanged(); private: + QuickshellGlobal(QObject* parent = nullptr); + static qsizetype screensCount(QQmlListProperty* prop); static QuickshellScreenInfo* screenAt(QQmlListProperty* prop, qsizetype i); }; diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index ea2adf18..35060bec 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "generation.hpp" @@ -64,17 +65,28 @@ void RootWrapper::reloadGraph(bool hard) { auto* obj = component.beginCreate(generation->engine->rootContext()); if (obj == nullptr) { - qWarning() << component.errorString().toStdString().c_str(); - qWarning() << "failed to create root component"; + QString error = "failed to create root component\n" + component.errorString(); + qWarning().noquote() << error; delete generation; + + if (this->generation != nullptr && this->generation->qsgInstance != nullptr) { + emit this->generation->qsgInstance->reloadFailed(error); + } + return; } auto* newRoot = qobject_cast(obj); if (newRoot == nullptr) { - qWarning() << "root component was not a Quickshell.ShellRoot"; + QString error = "root component was not a Quickshell.ShellRoot"; + qWarning().noquote() << error; delete obj; delete generation; + + if (this->generation != nullptr && this->generation->qsgInstance != nullptr) { + emit this->generation->qsgInstance->reloadFailed(error); + } + return; } @@ -82,6 +94,7 @@ void RootWrapper::reloadGraph(bool hard) { component.completeCreate(); + auto isReload = this->generation != nullptr; generation->onReload(hard ? nullptr : this->generation); if (hard) delete this->generation; this->generation = generation; @@ -96,6 +109,10 @@ void RootWrapper::reloadGraph(bool hard) { ); this->onWatchFilesChanged(); + + if (isReload && this->generation->qsgInstance != nullptr) { + emit this->generation->qsgInstance->reloadCompleted(); + } } void RootWrapper::onWatchFilesChanged() { From 6c9526761cd5d17b732dc00b7bbcdb7b0f5e3259 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 31 May 2024 00:24:58 -0700 Subject: [PATCH 022/305] wayland: fix UAF in layershell surface destructor --- src/wayland/wlr_layershell/surface.cpp | 11 +++++++++-- src/wayland/wlr_layershell/window.cpp | 6 ++++++ src/wayland/wlr_layershell/window.hpp | 3 +++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/wayland/wlr_layershell/surface.cpp b/src/wayland/wlr_layershell/surface.cpp index ac80ebd0..5c369f2b 100644 --- a/src/wayland/wlr_layershell/surface.cpp +++ b/src/wayland/wlr_layershell/surface.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -18,6 +17,10 @@ #include "shell_integration.hpp" #include "window.hpp" +#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) +#include +#endif + // clang-format off [[nodiscard]] QtWayland::zwlr_layer_shell_v1::layer toWaylandLayer(const WlrLayer::Enum& layer) noexcept; [[nodiscard]] QtWayland::zwlr_layer_surface_v1::anchor toWaylandAnchors(const Anchors& anchors) noexcept; @@ -72,7 +75,10 @@ QSWaylandLayerSurface::QSWaylandLayerSurface( } QSWaylandLayerSurface::~QSWaylandLayerSurface() { - this->ext->surface = nullptr; + if (this->ext != nullptr) { + this->ext->surface = nullptr; + } + this->destroy(); } @@ -106,6 +112,7 @@ void QSWaylandLayerSurface::applyConfigure() { } void QSWaylandLayerSurface::setWindowGeometry(const QRect& geometry) { + if (this->ext == nullptr) return; auto size = constrainedSize(this->ext->mAnchors, geometry.size()); this->set_size(size.width(), size.height()); } diff --git a/src/wayland/wlr_layershell/window.cpp b/src/wayland/wlr_layershell/window.cpp index 035bae1d..a671d59e 100644 --- a/src/wayland/wlr_layershell/window.cpp +++ b/src/wayland/wlr_layershell/window.cpp @@ -13,6 +13,12 @@ #include "shell_integration.hpp" #include "surface.hpp" +LayershellWindowExtension::~LayershellWindowExtension() { + if (this->surface != nullptr) { + this->surface->ext = nullptr; + } +} + LayershellWindowExtension* LayershellWindowExtension::get(QWindow* window) { auto v = window->property("layershell_ext"); diff --git a/src/wayland/wlr_layershell/window.hpp b/src/wayland/wlr_layershell/window.hpp index 163f3aa7..37092a6a 100644 --- a/src/wayland/wlr_layershell/window.hpp +++ b/src/wayland/wlr_layershell/window.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -56,6 +57,8 @@ class LayershellWindowExtension: public QObject { public: LayershellWindowExtension(QObject* parent = nullptr): QObject(parent) {} + ~LayershellWindowExtension() override; + Q_DISABLE_COPY_MOVE(LayershellWindowExtension); // returns the layershell extension if attached, otherwise nullptr static LayershellWindowExtension* get(QWindow* window); From 84bb4098adb8cf9a559772ad42a05ed0cdf8e7b1 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 31 May 2024 00:26:34 -0700 Subject: [PATCH 023/305] core/reloader: fix incorrect generation teardown on hard reload --- src/core/rootwrapper.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index 35060bec..3c69615f 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -65,7 +65,7 @@ void RootWrapper::reloadGraph(bool hard) { auto* obj = component.beginCreate(generation->engine->rootContext()); if (obj == nullptr) { - QString error = "failed to create root component\n" + component.errorString(); + const QString error = "failed to create root component\n" + component.errorString(); qWarning().noquote() << error; delete generation; @@ -78,7 +78,7 @@ void RootWrapper::reloadGraph(bool hard) { auto* newRoot = qobject_cast(obj); if (newRoot == nullptr) { - QString error = "root component was not a Quickshell.ShellRoot"; + const QString error = "root component was not a Quickshell.ShellRoot"; qWarning().noquote() << error; delete obj; delete generation; @@ -96,7 +96,11 @@ void RootWrapper::reloadGraph(bool hard) { auto isReload = this->generation != nullptr; generation->onReload(hard ? nullptr : this->generation); - if (hard) delete this->generation; + + if (hard && this->generation != nullptr) { + this->generation->destroy(); + } + this->generation = generation; qInfo() << "Configuration Loaded"; From d56c07ceb3389229d0bb52c71c80ae1866fb7656 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 31 May 2024 00:27:18 -0700 Subject: [PATCH 024/305] core/reloader: simplify generation teardown The extra complexity previously masked the use after free in 6c95267. --- src/core/generation.cpp | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 1021566b..71530430 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include "iconimageprovider.hpp" @@ -47,32 +46,30 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) } EngineGeneration::~EngineGeneration() { - g_generations.remove(this->engine); - delete this->engine; + if (this->engine != nullptr) { + qFatal() << this << "destroyed without calling destroy()"; + } } void EngineGeneration::destroy() { // Multiple generations can detect a reload at the same time. - delete this->watcher; + QObject::disconnect(this->watcher, nullptr, this, nullptr); + this->watcher->deleteLater(); this->watcher = nullptr; - // Yes all of this is actually necessary. if (this->engine != nullptr && this->root != nullptr) { QObject::connect(this->root, &QObject::destroyed, this, [this]() { - // The timer seems to fix *one* of the possible qml item destructor crashes. - QTimer::singleShot(0, [this]() { - // Garbage is not collected during engine destruction. - this->engine->collectGarbage(); + // prevent further js execution between garbage collection and engine destruction. + this->engine->setInterrupted(true); - QObject::connect(this->engine, &QObject::destroyed, this, [this]() { delete this; }); + g_generations.remove(this->engine); - // Even after all of that there's still multiple failing assertions and segfaults. - // Pray you don't hit one. - // Note: it appeats *some* of the crashes are related to values owned by the generation. - // Test by commenting the connect() above. - this->engine->deleteLater(); - this->engine = nullptr; - }); + // Garbage is not collected during engine destruction. + this->engine->collectGarbage(); + + delete this->engine; + this->engine = nullptr; + delete this; }); this->root->deleteLater(); From a8506edbb931867d881d5b854f7d15cb74e9086f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 31 May 2024 01:28:35 -0700 Subject: [PATCH 025/305] build: link jemalloc by default to reduce heap fragmentation The QML engine and the quickshell reloader both cause large amounts of heap fragmentation that stacks up over time, leading to a perceived memory leak. Jemalloc is able to handle the fragmentation much better, leading to lower user facing memory usage. --- CMakeLists.txt | 9 +++++++++ README.md | 5 ++++- default.nix | 17 +++++++++++------ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d17758b..e790ec0c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ option(BUILD_TESTING "Build tests" OFF) option(ASAN "Enable ASAN" OFF) option(FRAME_POINTERS "Always keep frame pointers" ${ASAN}) +option(USE_JEMALLOC "Use jemalloc over the system malloc implementation" ON) option(NVIDIA_COMPAT "Workarounds for nvidia gpus" OFF) option(SOCKETS "Enable unix socket support" ON) option(WAYLAND "Enable wayland support" ON) @@ -23,6 +24,7 @@ option(SERVICE_PIPEWIRE "PipeWire service" ON) option(SERVICE_MPRIS "Mpris service" ON) message(STATUS "Quickshell configuration") +message(STATUS " Jemalloc: ${USE_JEMALLOC}") message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}") message(STATUS " Build tests: ${BUILD_TESTING}") message(STATUS " Sockets: ${SOCKETS}") @@ -137,3 +139,10 @@ if (NVIDIA_COMPAT) endif() add_subdirectory(src) + +if (USE_JEMALLOC) + find_package(PkgConfig REQUIRED) + # IMPORTED_TARGET not working for some reason + pkg_check_modules(JEMALLOC REQUIRED jemalloc) + target_link_libraries(quickshell PRIVATE ${JEMALLOC_LIBRARIES}) +endif() diff --git a/README.md b/README.md index c17af3a8..173ddd17 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,13 @@ To build quickshell at all, you will need the following packages (names may vary - just - cmake - ninja +- pkg-config - Qt6 [ QtBase, QtDeclarative ] +Jemalloc is recommended, in which case you will need: +- jemalloc + To build with wayland support you will additionally need: -- pkg-config - wayland - wayland-scanner (may be part of wayland on some distros) - wayland-protocols diff --git a/default.nix b/default.nix index 0985d843..048e181e 100644 --- a/default.nix +++ b/default.nix @@ -8,6 +8,7 @@ cmake, ninja, qt6, + jemalloc, wayland, wayland-protocols, xorg, @@ -29,7 +30,8 @@ enableX11 ? true, enablePipewire ? true, nvidiaCompat ? false, - svgSupport ? true, # you almost always want this + withQtSvg ? true, # svg support + withJemalloc ? true, # masks heap fragmentation }: buildStdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; version = "0.1.0"; @@ -39,8 +41,8 @@ cmake ninja qt6.wrapQtAppsHook - ] ++ (lib.optionals enableWayland [ pkg-config + ] ++ (lib.optionals enableWayland [ wayland-protocols wayland-scanner ]); @@ -49,10 +51,11 @@ qt6.qtbase qt6.qtdeclarative ] + ++ (lib.optional withJemalloc jemalloc) + ++ (lib.optional withQtSvg qt6.qtsvg) ++ (lib.optionals enableWayland [ qt6.qtwayland wayland ]) - ++ (lib.optionals enableX11 [ xorg.libxcb ]) - ++ (lib.optionals svgSupport [ qt6.qtsvg ]) - ++ (lib.optionals enablePipewire [ pipewire ]); + ++ (lib.optional enableX11 xorg.libxcb) + ++ (lib.optional enablePipewire pipewire); QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner"; @@ -67,7 +70,9 @@ cmakeFlags = [ "-DGIT_REVISION=${gitRev}" - ] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF" + ] + ++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF" + ++ lib.optional (!enableWayland) "-DWAYLAND=OFF" ++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON" ++ lib.optional (!enablePipewire) "-DSERVICE_PIPEWIRE=OFF"; From 238ca8cf0bf02453cb47a93e5310934ba395f23b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 31 May 2024 04:03:00 -0700 Subject: [PATCH 026/305] core/reloader: fix crashing on failed reload --- src/core/generation.cpp | 17 ++++++++++++----- src/core/rootwrapper.cpp | 4 ++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 71530430..e43db6ee 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -52,12 +52,14 @@ EngineGeneration::~EngineGeneration() { } void EngineGeneration::destroy() { - // Multiple generations can detect a reload at the same time. - QObject::disconnect(this->watcher, nullptr, this, nullptr); - this->watcher->deleteLater(); - this->watcher = nullptr; + if (this->watcher != nullptr) { + // Multiple generations can detect a reload at the same time. + QObject::disconnect(this->watcher, nullptr, this, nullptr); + this->watcher->deleteLater(); + this->watcher = nullptr; + } - if (this->engine != nullptr && this->root != nullptr) { + if (this->root != nullptr) { QObject::connect(this->root, &QObject::destroyed, this, [this]() { // prevent further js execution between garbage collection and engine destruction. this->engine->setInterrupted(true); @@ -74,6 +76,11 @@ void EngineGeneration::destroy() { this->root->deleteLater(); this->root = nullptr; + } else { + // the engine has never been used, no need to clean up + delete this->engine; + this->engine = nullptr; + delete this; } } diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index 3c69615f..1afb30cf 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -67,7 +67,7 @@ void RootWrapper::reloadGraph(bool hard) { if (obj == nullptr) { const QString error = "failed to create root component\n" + component.errorString(); qWarning().noquote() << error; - delete generation; + generation->destroy(); if (this->generation != nullptr && this->generation->qsgInstance != nullptr) { emit this->generation->qsgInstance->reloadFailed(error); @@ -81,7 +81,7 @@ void RootWrapper::reloadGraph(bool hard) { const QString error = "root component was not a Quickshell.ShellRoot"; qWarning().noquote() << error; delete obj; - delete generation; + generation->destroy(); if (this->generation != nullptr && this->generation->qsgInstance != nullptr) { emit this->generation->qsgInstance->reloadFailed(error); From bd504daf56b6fe6ccbc26c40ed1b155dd850797e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 2 Jun 2024 14:37:48 -0700 Subject: [PATCH 027/305] docs: add build, packaging and development instructions --- BUILD.md | 151 ++++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 69 ++++++++++++++++++++++ README.md | 94 +++++++----------------------- 3 files changed, 241 insertions(+), 73 deletions(-) create mode 100644 BUILD.md create mode 100644 CONTRIBUTING.md diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 00000000..d7844c6a --- /dev/null +++ b/BUILD.md @@ -0,0 +1,151 @@ +# Build instructions +Instructions for building from source and distro packagers. We highly recommend +distro packagers read through this page fully. + +## Dependencies +Quickshell has a set of base dependencies you will always need, names vary by distro: + +- `cmake` +- `qt6base` +- `qt6declarative` +- `pkg-config` + +At least Qt 6.6 is required. + +All features are enabled by default and some have their own dependencies. + +##### Additional note to packagers: +If your package manager supports enabling some features but not others, +we recommend not exposing the subfeatures and just the main ones that introduce +new dependencies: `wayland`, `x11`, `pipewire`, `hyprland` + +### Jemalloc +We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused +by the QML engine, which results in much lower memory usage. Without this you +will get a perceived memory leak. + +To disable: `-DUSE_JEMALLOC=OFF` + +Dependencies: `jemalloc` + +### Unix Sockets +This feature allows interaction with unix sockets and creating socket servers +which is useful for IPC and has no additional dependencies. + +WARNING: Disabling unix sockets will NOT make it safe to run arbitrary code using quickshell. +There are many vectors which mallicious code can use to escape into your system. + +To disable: `-DSOCKETS=OFF` + +### Wayland +This feature enables wayland support. Subfeatures exist for each particular wayland integration. + +WARNING: Wayland integration relies on featurs that are not part of the public Qt API and which +may break in minor releases. Updating quickshell's dependencies without ensuring without ensuring +that the current Qt version is supported WILL result in quickshell failing to build or misbehaving +at runtime. + +Currently supported Qt versions: `6.6`, `6.7`. + +To disable: `-DWAYLAND=OFF` + +Dependencies: + - `qt6wayland` + - `wayland` (libwayland-client) + - `wayland-scanner` (may be part of your distro's wayland package) + - `wayland-protocols` + +#### Wlroots Layershell +Enables wlroots layershell integration through the [wlr-layer-shell-unstable-v1] protocol, +enabling use cases such as bars overlays and backgrounds. +This feature has no extra dependencies. + +To disable: `-DWAYLAND_WLR_LAYERSHELL=OFF` + +[wlr-layer-shell-unstable-v1]: https://wayland.app/protocols/wlr-layer-shell-unstable-v1 + +#### Session Lock +Enables session lock support through the [ext-session-lock-v1] protocol, +which allows quickshell to be used as a session lock under compatible wayland compositors. + +[ext-session-lock-v1]: https://wayland.app/protocols/ext-session-lock-v1 + +### X11 +This feature enables x11 support. Currently this implements panel windows for X11 similarly +to the wlroots layershell above. + +To disable: `-DX11=OFF` + +Dependencies: `libxcb` + +### Pipewire +This features enables viewing and management of pipewire nodes. + +To disable: `-DSERVICE_PIPEWIRE=OFF` + +Dependencies: `libpipewire` + +### StatusNotifier / System Tray +This feature enables system tray support using the status notifier dbus protocol. + +To disable: `-DSERVICE_STATUS_NOTIFIER=OFF` + +Dependencies: `qt6dbus` (usually part of qt6base) + +### MPRIS +This feature enables access to MPRIS compatible media players using its dbus protocol. + +To disable: `-DSERVICE_MPRIS=OFF` + +Dependencies: `qt6dbus` (usually part of qt6base) + +### Hyprland +This feature enables hyprland specific integrations. It requires wayland support +but has no extra dependencies. + +To disable: `-DHYPRLAND=OFF` + +#### Hyprland Global Shortcuts +Enables creation of global shortcuts under hyprland through the [hyprland-global-shortcuts-v1] +protocol. Generally a much nicer alternative to using unix sockets to implement the same thing. +This feature has no extra dependencies. + +To disable: `-DHYPRLAND_GLOBAL_SHORTCUTS=OFF` + +[hyprland-global-shortcuts-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml + +#### Hyprland Focus Grab +Enables windows to grab focus similarly to a context menu undr hyprland through the +[hyprland-focus-grab-v1] protocol. This feature has no extra dependencies. + +To disable: `-DHYPRLAND_FOCUS_GRAB=OFF` + +[hyprland-focus-grab-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-focus-grab-v1.xml + +## Building +*For developers and prospective contributors: See [CONTRIBUTING.md](CONTRIBUTING.md).* + +We highly recommend using `ninja` to run the build, but you can use makefiles if you must. + +#### Configuring the build +```sh +$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here] +``` + +Note that features you do not supply dependencies for MUST be disabled with their associated flags +or quickshell will fail to build. + +Additionally, note that clang builds much faster than gcc if you care. + +You may disable debug information but it's only a couple megabytes and is extremely helpful +for helping us fix problems when they do arise. + +#### Building +```sh +$ cmake --build build +``` + +#### Installing +```sh +$ cmake --install build +``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..a5fd4836 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# Contributing / Development +Instructions for development setup and upstreaming patches. + +If you just want to build or package quickshell see [BUILD.md](BUILD.md). + +## Development +Install the dependencies listed in [BUILD.md](BUILD.md). +You probably want all of them even if you don't use all of them +to ensure tests work correctly and avoid passing a bunch of configure +flags when you need to wipe the build directory. + +Quickshell also uses `just` for common development command aliases. + +The dependencies are also available as a nix shell or nix flake which we recommend +using with nix-direnv. + +Common aliases: +- `just configure [ [extra cmake args]]` (note that you must specify debug/release to specify extra args) +- `just build` - runs the build, configuring if not configured already. +- `just run [args]` - runs quickshell with the given arguments +- `just clean` - clean up build artifacts. `just clean build` is somewhat common. + +### Formatting +All contributions should be formatted similarly to what already exists. +Group related functionality together. + +Run the formatter using `just fmt`. +If the results look stupid, fix the clang-format file if possible, +or disable clang-format in the affected area +using `// clang-format off` and `// clang-format on`. + +### Linter +All contributions should pass the linter. + +Note that running the linter requires disabling precompiled +headers and including the test codepaths: +```sh +$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON +$ just lint +``` + +If the linter is complaining about something that you think it should not, +please disable the lint in your MR and explain your reasoning. + +### Tests +If you feel like the feature you are working on is very complex or likely to break, +please write some tests. We will ask you to directly if you send in an MR for an +overly complex or breakable feature. + +At least all tests that passed before your changes should still be passing +by the time your contribution is ready. + +You can run the tests using `just test` but you must enable them first +using `-DBUILD_TESTING=ON`. + +### Documentation +Most of quickshell's documentation is automatically generated from the source code. +You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser +cannot handle random line breaks and will usually require you to disable clang-format if the +lines are too long. + +Before submitting an MR, if adding new features please make sure the documentation is generated +reasonably using the `quickshell-docs` repo. + +Doc comments take the form `///` or `///!` (summary) and work with markdown. +Look at existing code for how it works. + +Quickshell modules additionally have a `module.md` file which contains a summary, description, +and list of headers to scan for documentation. diff --git a/README.md b/README.md index 173ddd17..012a33a6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Hosted on: [outfoxxed's gitea], [github] Documentation available at [quickshell.outfoxxed.me](https://quickshell.outfoxxed.me) or can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quickshell-docs) repo. -Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples) +Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples) repo. Both the documentation and examples are included as submodules with revisions that work with the current @@ -48,84 +48,32 @@ This repo has a nix flake you can use to install the package directly: Quickshell's binary is available at `quickshell.packages..default` to be added to lists such as `environment.systemPackages` or `home.packages`. -`quickshell.packages..nvidia` is also available for nvidia users which fixes some -common crashes. +The package contains several features detailed in [BUILD.md](BUILD.md) which can be enabled +or disabled with overrides: + +```nix +quickshell.packages..default.override { + enableWayland = true; + enableX11 = true; + enablePipewire = true; + withQtSvg = true; + withJemalloc = true; +} +``` Note: by default this package is built with clang as it is significantly faster. -## Manual +## Arch (AUR) +Quickshell has a third party [AUR package] available under the same name. +As is usual with the AUR it is not maintained by us and should be looked over before use. -If not using nix, you'll have to build from source. +[AUR package]: https://aur.archlinux.org/packages/quickshell -### Dependencies -To build quickshell at all, you will need the following packages (names may vary by distro) +## Anything else +See [BUILD.md](BUILD.md) for instructions on building and packaging quickshell. -- just -- cmake -- ninja -- pkg-config -- Qt6 [ QtBase, QtDeclarative ] - -Jemalloc is recommended, in which case you will need: -- jemalloc - -To build with wayland support you will additionally need: -- wayland -- wayland-scanner (may be part of wayland on some distros) -- wayland-protocols -- Qt6 [ QtWayland ] - -To build with x11 support you will additionally need: -- libxcb - -To build with pipewire support you will additionally need: -- libpipewire - -### Building - -To make a release build of quickshell run: -```sh -$ just release -``` - -If running an nvidia GPU, instead run: -```sh -$ just configure release -DNVIDIA_COMPAT=ON -$ just build -``` - -(These commands are just aliases for cmake commands you can run directly, -see the Justfile for more information.) - -If you have all the dependencies installed and they are in expected -locations this will build correctly. - -To install to /usr/local/bin run as root (usually `sudo`) in the same folder: -``` -$ just install -``` - -### Building (Nix) - -You can build directly using the provided nix flake or nix package. -``` -nix build -nix build -f package.nix # calls default.nix with a basic callPackage expression -``` - -# Development - -For nix there is a devshell available from `shell.nix` and as a devShell -output from the flake. - -The Justfile contains various useful aliases: -- `just configure [ [extra cmake args]]` -- `just build` (runs configure for debug mode) -- `just run [args]` -- `just clean` -- `just test [args]` (configure with `-DBUILD_TESTING=ON` first) -- `just fmt` -- `just lint` +# Contributing / Development +See [CONTRIBUTING.md](CONTRIBUTING.md) for details. #### License From 7d20b472dd01ec9ae4f3c4f2bda2c808b5631d83 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 2 Jun 2024 15:23:19 -0700 Subject: [PATCH 028/305] misc: remove the docs and examples submodules They have not been correctly updated in lock-step for a while now. --- .gitignore | 4 ++++ .gitmodules | 6 ------ CONTRIBUTING.md | 2 +- README.md | 13 ------------- docs | 1 - examples | 1 - 6 files changed, 5 insertions(+), 22 deletions(-) delete mode 100644 .gitmodules delete mode 160000 docs delete mode 160000 examples diff --git a/.gitignore b/.gitignore index 1933837e..dcdefe39 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# related repos +/docs +/examples + # build files /result /build/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 74013769..00000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "docs"] - path = docs - url = https://git.outfoxxed.me/outfoxxed/quickshell-docs -[submodule "examples"] - path = examples - url = https://git.outfoxxed.me/outfoxxed/quickshell-examples diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a5fd4836..2aad2b3c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,7 +60,7 @@ cannot handle random line breaks and will usually require you to disable clang-f lines are too long. Before submitting an MR, if adding new features please make sure the documentation is generated -reasonably using the `quickshell-docs` repo. +reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo. Doc comments take the form `///` or `///!` (summary) and work with markdown. Look at existing code for how it works. diff --git a/README.md b/README.md index 012a33a6..1959583f 100644 --- a/README.md +++ b/README.md @@ -14,19 +14,6 @@ can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quick Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples) repo. -Both the documentation and examples are included as submodules with revisions that work with the current -version of quickshell. - -You can clone everything with -``` -$ git clone --recursive https://git.outfoxxed.me/outfoxxed/quickshell.git -``` - -Or clone missing submodules later with -``` -$ git submodule update --init --recursive -``` - # Installation ## Nix diff --git a/docs b/docs deleted file mode 160000 index ff5da84a..00000000 --- a/docs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ff5da84a8b258a9b2caaf978ddb6de23635d8903 diff --git a/examples b/examples deleted file mode 160000 index b9e744b5..00000000 --- a/examples +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b9e744b50673304dfddb68f3da2a2e906d028b96 From 29f02d837d4e9902a9efa1f6295e98d042f38341 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 2 Jun 2024 15:36:33 -0700 Subject: [PATCH 029/305] all: remove NVIDIA workarounds They fixed the driver. --- CMakeLists.txt | 6 ------ default.nix | 2 -- flake.nix | 2 -- src/core/proxywindow.cpp | 7 ------- 4 files changed, 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e790ec0c..a386f5a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,6 @@ option(ASAN "Enable ASAN" OFF) option(FRAME_POINTERS "Always keep frame pointers" ${ASAN}) option(USE_JEMALLOC "Use jemalloc over the system malloc implementation" ON) -option(NVIDIA_COMPAT "Workarounds for nvidia gpus" OFF) option(SOCKETS "Enable unix socket support" ON) option(WAYLAND "Enable wayland support" ON) option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON) @@ -25,7 +24,6 @@ option(SERVICE_MPRIS "Mpris service" ON) message(STATUS "Quickshell configuration") message(STATUS " Jemalloc: ${USE_JEMALLOC}") -message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}") message(STATUS " Build tests: ${BUILD_TESTING}") message(STATUS " Sockets: ${SOCKETS}") message(STATUS " Wayland: ${WAYLAND}") @@ -134,10 +132,6 @@ function (qs_pch target) endif() endfunction() -if (NVIDIA_COMPAT) - add_compile_definitions(NVIDIA_COMPAT) -endif() - add_subdirectory(src) if (USE_JEMALLOC) diff --git a/default.nix b/default.nix index 048e181e..d96ff3d1 100644 --- a/default.nix +++ b/default.nix @@ -29,7 +29,6 @@ enableWayland ? true, enableX11 ? true, enablePipewire ? true, - nvidiaCompat ? false, withQtSvg ? true, # svg support withJemalloc ? true, # masks heap fragmentation }: buildStdenv.mkDerivation { @@ -73,7 +72,6 @@ ] ++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF" ++ lib.optional (!enableWayland) "-DWAYLAND=OFF" - ++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON" ++ lib.optional (!enablePipewire) "-DSERVICE_PIPEWIRE=OFF"; buildPhase = "ninjaBuildPhase"; diff --git a/flake.nix b/flake.nix index 5bb5069e..a0bc18d4 100644 --- a/flake.nix +++ b/flake.nix @@ -12,10 +12,8 @@ quickshell = pkgs.callPackage ./default.nix { gitRev = self.rev or self.dirtyRev; }; - quickshell-nvidia = quickshell.override { nvidiaCompat = true; }; default = quickshell; - nvidia = quickshell-nvidia; }); devShells = forEachSystem (system: pkgs: rec { diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index 50370d9d..c2961c24 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -157,14 +157,7 @@ void ProxyWindowBase::completeWindow() { } bool ProxyWindowBase::deleteOnInvisible() const { -#ifdef NVIDIA_COMPAT - // Nvidia drivers and Qt do not play nice when hiding and showing a window - // so for nvidia compatibility we can never reuse windows if they have been - // hidden. - return true; -#else return false; -#endif } QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; } From 9d5dd402b916cea2842a7186ca09159442afdccc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 2 Jun 2024 15:37:47 -0700 Subject: [PATCH 030/305] docs: recommend packagers add a dependency on qtsvg --- BUILD.md | 3 +++ README.md | 9 +++++---- default.nix | 24 +++++++++++++----------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/BUILD.md b/BUILD.md index d7844c6a..92589ccc 100644 --- a/BUILD.md +++ b/BUILD.md @@ -10,6 +10,9 @@ Quickshell has a set of base dependencies you will always need, names vary by di - `qt6declarative` - `pkg-config` +We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and +svg icons will not work, including system ones. + At least Qt 6.6 is required. All features are enabled by default and some have their own dependencies. diff --git a/README.md b/README.md index 1959583f..4def09ed 100644 --- a/README.md +++ b/README.md @@ -40,11 +40,12 @@ or disabled with overrides: ```nix quickshell.packages..default.override { - enableWayland = true; - enableX11 = true; - enablePipewire = true; - withQtSvg = true; withJemalloc = true; + withQtSvg = true; + withWayland = true; + withX11 = true; + withPipewire = true; + withHyprland = true; } ``` diff --git a/default.nix b/default.nix index d96ff3d1..01624c4a 100644 --- a/default.nix +++ b/default.nix @@ -26,11 +26,12 @@ else "unknown"), debug ? false, - enableWayland ? true, - enableX11 ? true, - enablePipewire ? true, - withQtSvg ? true, # svg support withJemalloc ? true, # masks heap fragmentation + withQtSvg ? true, + withWayland ? true, + withX11 ? true, + withPipewire ? true, + withHyprland ? true, }: buildStdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; version = "0.1.0"; @@ -41,7 +42,7 @@ ninja qt6.wrapQtAppsHook pkg-config - ] ++ (lib.optionals enableWayland [ + ] ++ (lib.optionals withWayland [ wayland-protocols wayland-scanner ]); @@ -52,11 +53,11 @@ ] ++ (lib.optional withJemalloc jemalloc) ++ (lib.optional withQtSvg qt6.qtsvg) - ++ (lib.optionals enableWayland [ qt6.qtwayland wayland ]) - ++ (lib.optional enableX11 xorg.libxcb) - ++ (lib.optional enablePipewire pipewire); + ++ (lib.optionals withWayland [ qt6.qtwayland wayland ]) + ++ (lib.optional withX11 xorg.libxcb) + ++ (lib.optional withPipewire pipewire); - QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner"; + QTWAYLANDSCANNER = lib.optionalString withWayland "${qt6.qtwayland}/libexec/qtwaylandscanner"; configurePhase = let cmakeBuildType = if debug @@ -71,8 +72,9 @@ "-DGIT_REVISION=${gitRev}" ] ++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF" - ++ lib.optional (!enableWayland) "-DWAYLAND=OFF" - ++ lib.optional (!enablePipewire) "-DSERVICE_PIPEWIRE=OFF"; + ++ lib.optional (!withWayland) "-DWAYLAND=OFF" + ++ lib.optional (!withPipewire) "-DSERVICE_PIPEWIRE=OFF" + ++ lib.optional (!withHyprland) "-DHYPRLAND=OFF"; buildPhase = "ninjaBuildPhase"; enableParallelBuilding = true; From b1f5a5eb94badd180f182cd2bc1fb9687cb2f672 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 2 Jun 2024 16:18:45 -0700 Subject: [PATCH 031/305] service/mpris: preserve mpris watcher and players across reload --- BUILD.md | 2 +- src/core/proxywindow.cpp | 4 +--- src/services/mpris/watcher.cpp | 11 ++++++++++- src/services/mpris/watcher.hpp | 23 +++++++++++++++++------ 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/BUILD.md b/BUILD.md index 92589ccc..c9909598 100644 --- a/BUILD.md +++ b/BUILD.md @@ -43,7 +43,7 @@ To disable: `-DSOCKETS=OFF` ### Wayland This feature enables wayland support. Subfeatures exist for each particular wayland integration. -WARNING: Wayland integration relies on featurs that are not part of the public Qt API and which +WARNING: Wayland integration relies on features that are not part of the public Qt API and which may break in minor releases. Updating quickshell's dependencies without ensuring without ensuring that the current Qt version is supported WILL result in quickshell failing to build or misbehaving at runtime. diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index c2961c24..4eef5f38 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -156,9 +156,7 @@ void ProxyWindowBase::completeWindow() { emit this->screenChanged(); } -bool ProxyWindowBase::deleteOnInvisible() const { - return false; -} +bool ProxyWindowBase::deleteOnInvisible() const { return false; } QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; } QQuickItem* ProxyWindowBase::contentItem() const { return this->mContentItem; } diff --git a/src/services/mpris/watcher.cpp b/src/services/mpris/watcher.cpp index 8c67a4d6..8a788933 100644 --- a/src/services/mpris/watcher.cpp +++ b/src/services/mpris/watcher.cpp @@ -16,7 +16,7 @@ namespace qs::service::mpris { Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg); -MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) { +MprisWatcher::MprisWatcher() { qCDebug(logMprisWatcher) << "Starting MprisWatcher"; auto bus = QDBusConnection::sessionBus(); @@ -102,4 +102,13 @@ void MprisWatcher::registerPlayer(const QString& address) { qCDebug(logMprisWatcher) << "Registered MprisPlayer" << address; } +MprisWatcher* MprisWatcher::instance() { + static MprisWatcher* instance = new MprisWatcher(); // NOLINT + return instance; +} + +ObjectModel* MprisQml::players() { // NOLINT + return MprisWatcher::instance()->players(); +} + } // namespace qs::service::mpris diff --git a/src/services/mpris/watcher.hpp b/src/services/mpris/watcher.hpp index 91275c7e..d60471cc 100644 --- a/src/services/mpris/watcher.hpp +++ b/src/services/mpris/watcher.hpp @@ -18,16 +18,12 @@ namespace qs::service::mpris { ///! Provides access to MprisPlayers. class MprisWatcher: public QObject { Q_OBJECT; - QML_NAMED_ELEMENT(Mpris); - QML_SINGLETON; - /// All connected MPRIS players. - Q_PROPERTY(ObjectModel* players READ players CONSTANT); public: - explicit MprisWatcher(QObject* parent = nullptr); - [[nodiscard]] ObjectModel* players(); + static MprisWatcher* instance(); + private slots: void onServiceRegistered(const QString& service); void onServiceUnregistered(const QString& service); @@ -35,6 +31,8 @@ private slots: void onPlayerDestroyed(QObject* object); private: + explicit MprisWatcher(); + void registerExisting(); void registerPlayer(const QString& address); @@ -43,4 +41,17 @@ private: ObjectModel readyPlayers {this}; }; +class MprisQml: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(Mpris); + QML_SINGLETON; + /// All connected MPRIS players. + Q_PROPERTY(ObjectModel* players READ players CONSTANT); + +public: + explicit MprisQml(QObject* parent = nullptr): QObject(parent) {}; + + [[nodiscard]] ObjectModel* players(); +}; + } // namespace qs::service::mpris From 37fecfc9905c563083fd4169b921188a9b16bba0 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 3 Jun 2024 00:38:22 -0700 Subject: [PATCH 032/305] docs: add commit style instructions --- CONTRIBUTING.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2aad2b3c..6fdef09c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,7 @@ Instructions for development setup and upstreaming patches. If you just want to build or package quickshell see [BUILD.md](BUILD.md). ## Development + Install the dependencies listed in [BUILD.md](BUILD.md). You probably want all of them even if you don't use all of them to ensure tests work correctly and avoid passing a bunch of configure @@ -67,3 +68,32 @@ Look at existing code for how it works. Quickshell modules additionally have a `module.md` file which contains a summary, description, and list of headers to scan for documentation. + +## Contributing + +### Commits +Please structure your commit messages as `scope[!]: commit` where +the scope is something like `core` or `service/mpris`. (pick what has been +used historically or what makes sense if new.) Add `!` for changes that break +existing APIs or functionality. + +Commit descriptions should contain a summary of the changes if they are not +sufficiently addressed in the commit message. + +Please squash/rebase additions or edits to previous changes and follow the +commit style to keep the history easily searchable at a glance. +Depending on the change, it is often reasonable to squash it into just +a single commit. (If you do not follow this we will squash your changes +for you.) + +### Sending patches +You may contribute by submitting a pull request on github, asking for +an account on our git server, or emailing patches / git bundles +directly to `outfoxxed@outfoxxed.me`. + +### Getting help +If you're getting stuck, you can come talk to us in the +[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me) +for help on implementation, conventions, etc. +Feel free to ask for advice early in your implementation if you are +unsure. From be237b6ab5f4e3ec875d11359b71d4cbb543314d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 4 Jun 2024 13:14:39 -0700 Subject: [PATCH 033/305] core/elapsedtimer: add ElapsedTimer --- src/core/CMakeLists.txt | 1 + src/core/elapsedtimer.cpp | 22 +++++++++++++++++++ src/core/elapsedtimer.hpp | 45 +++++++++++++++++++++++++++++++++++++++ src/core/module.md | 1 + 4 files changed, 69 insertions(+) create mode 100644 src/core/elapsedtimer.cpp create mode 100644 src/core/elapsedtimer.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 88c26241..24d2e685 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -27,6 +27,7 @@ qt_add_library(quickshell-core STATIC transformwatcher.cpp boundcomponent.cpp model.cpp + elapsedtimer.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/elapsedtimer.cpp b/src/core/elapsedtimer.cpp new file mode 100644 index 00000000..91321122 --- /dev/null +++ b/src/core/elapsedtimer.cpp @@ -0,0 +1,22 @@ +#include "elapsedtimer.hpp" + +#include + +ElapsedTimer::ElapsedTimer() { this->timer.start(); } + +qreal ElapsedTimer::elapsed() { return static_cast(this->elapsedNs()) / 1000000000.0; } + +qreal ElapsedTimer::restart() { return static_cast(this->restartNs()) / 1000000000.0; } + +qint64 ElapsedTimer::elapsedMs() { return this->timer.elapsed(); } + +qint64 ElapsedTimer::restartMs() { return this->timer.restart(); } + +qint64 ElapsedTimer::elapsedNs() { return this->timer.nsecsElapsed(); } + +qint64 ElapsedTimer::restartNs() { + // see qelapsedtimer.cpp + auto old = this->timer; + this->timer.start(); + return old.durationTo(this->timer).count(); +} diff --git a/src/core/elapsedtimer.hpp b/src/core/elapsedtimer.hpp new file mode 100644 index 00000000..85850963 --- /dev/null +++ b/src/core/elapsedtimer.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include + +///! Measures time between events +/// The ElapsedTimer measures time since its last restart, and is useful +/// for determining the time between events that don't supply it. +class ElapsedTimer: public QObject { + Q_OBJECT; + QML_ELEMENT; + +public: + explicit ElapsedTimer(); + + /// Return the number of seconds since the timer was last + /// started or restarted, with nanosecond precision. + Q_INVOKABLE qreal elapsed(); + + /// Restart the timer, returning the number of seconds since + /// the timer was last started or restarted, with nanosecond precision. + Q_INVOKABLE qreal restart(); + + /// Return the number of milliseconds since the timer was last + /// started or restarted. + Q_INVOKABLE qint64 elapsedMs(); + + /// Restart the timer, returning the number of milliseconds since + /// the timer was last started or restarted. + Q_INVOKABLE qint64 restartMs(); + + /// Return the number of nanoseconds since the timer was last + /// started or restarted. + Q_INVOKABLE qint64 elapsedNs(); + + /// Restart the timer, returning the number of nanoseconds since + /// the timer was last started or restarted. + Q_INVOKABLE qint64 restartNs(); + +private: + QElapsedTimer timer; +}; diff --git a/src/core/module.md b/src/core/module.md index dc1f204d..13218610 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -19,5 +19,6 @@ headers = [ "transformwatcher.hpp", "boundcomponent.hpp", "model.hpp", + "elapsedtimer.hpp", ] ----- From d14ca709849ba0a0e3de5126b77cfc7819c9b100 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 5 Jun 2024 19:26:20 -0700 Subject: [PATCH 034/305] hyprland/ipc: add hyprland ipc Only monitors and workspaces are fully tracked for now. --- CMakeLists.txt | 2 + src/wayland/hyprland/CMakeLists.txt | 5 + src/wayland/hyprland/ipc/CMakeLists.txt | 18 + src/wayland/hyprland/ipc/connection.cpp | 542 ++++++++++++++++++++++++ src/wayland/hyprland/ipc/connection.hpp | 123 ++++++ src/wayland/hyprland/ipc/monitor.cpp | 136 ++++++ src/wayland/hyprland/ipc/monitor.hpp | 85 ++++ src/wayland/hyprland/ipc/qml.cpp | 52 +++ src/wayland/hyprland/ipc/qml.hpp | 66 +++ src/wayland/hyprland/ipc/workspace.cpp | 79 ++++ src/wayland/hyprland/ipc/workspace.hpp | 59 +++ src/wayland/hyprland/module.md | 4 + 12 files changed, 1171 insertions(+) create mode 100644 src/wayland/hyprland/ipc/CMakeLists.txt create mode 100644 src/wayland/hyprland/ipc/connection.cpp create mode 100644 src/wayland/hyprland/ipc/connection.hpp create mode 100644 src/wayland/hyprland/ipc/monitor.cpp create mode 100644 src/wayland/hyprland/ipc/monitor.hpp create mode 100644 src/wayland/hyprland/ipc/qml.cpp create mode 100644 src/wayland/hyprland/ipc/qml.hpp create mode 100644 src/wayland/hyprland/ipc/workspace.cpp create mode 100644 src/wayland/hyprland/ipc/workspace.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a386f5a8..246428ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,7 @@ option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON) option(X11 "Enable X11 support" ON) option(HYPRLAND "Support hyprland specific features" ON) +option(HYPRLAND_IPC "Hyprland IPC" ON) option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON) option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON) option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) @@ -38,6 +39,7 @@ message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") message(STATUS " Mpris: ${SERVICE_MPRIS}") message(STATUS " Hyprland: ${HYPRLAND}") if (HYPRLAND) + message(STATUS " IPC: ${HYPRLAND_IPC}") message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}") message(STATUS " Global Shortcuts: ${HYPRLAND_GLOBAL_SHORTCUTS}") endif() diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt index be6bf49c..be2f0c59 100644 --- a/src/wayland/hyprland/CMakeLists.txt +++ b/src/wayland/hyprland/CMakeLists.txt @@ -4,6 +4,11 @@ target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS}) set(HYPRLAND_MODULES) +if (HYPRLAND_IPC) + add_subdirectory(ipc) + list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._Ipc) +endif() + if (HYPRLAND_FOCUS_GRAB) add_subdirectory(focus_grab) list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._FocusGrab) diff --git a/src/wayland/hyprland/ipc/CMakeLists.txt b/src/wayland/hyprland/ipc/CMakeLists.txt new file mode 100644 index 00000000..59200462 --- /dev/null +++ b/src/wayland/hyprland/ipc/CMakeLists.txt @@ -0,0 +1,18 @@ +qt_add_library(quickshell-hyprland-ipc STATIC + connection.cpp + monitor.cpp + workspace.cpp + qml.cpp +) + +qt_add_qml_module(quickshell-hyprland-ipc + URI Quickshell.Hyprland._Ipc + VERSION 0.1 +) + +target_link_libraries(quickshell-hyprland-ipc PRIVATE ${QT_DEPS}) + +qs_pch(quickshell-hyprland-ipc) +qs_pch(quickshell-hyprland-ipcplugin) + +target_link_libraries(quickshell PRIVATE quickshell-hyprland-ipcplugin) diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp new file mode 100644 index 00000000..e7265c70 --- /dev/null +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -0,0 +1,542 @@ +#include "connection.hpp" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "monitor.hpp" +#include "workspace.hpp" + +namespace qs::hyprland::ipc { + +Q_LOGGING_CATEGORY(logHyprlandIpc, "quickshell.hyprland.ipc", QtWarningMsg); +Q_LOGGING_CATEGORY(logHyprlandIpcEvents, "quickshell.hyprland.ipc.events", QtWarningMsg); + +HyprlandIpc::HyprlandIpc() { + auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"); + if (his.isEmpty()) { + qWarning() << "$HYPRLAND_INSTANCE_SIGNATURE is unset. Cannot connect to hyprland."; + return; + } + + auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR"); + auto hyprlandDir = runtimeDir + "/hypr/" + his; + + if (!QFileInfo(hyprlandDir).isDir()) { + hyprlandDir = "/tmp/hypr/" + his; + } + + if (!QFileInfo(hyprlandDir).isDir()) { + qWarning() << "Unable to find hyprland socket. Cannot connect to hyprland."; + return; + } + + this->mRequestSocketPath = hyprlandDir + "/.socket.sock"; + this->mEventSocketPath = hyprlandDir + "/.socket2.sock"; + + // clang-format off + QObject::connect(&this->eventSocket, &QLocalSocket::errorOccurred, this, &HyprlandIpc::eventSocketError); + QObject::connect(&this->eventSocket, &QLocalSocket::stateChanged, this, &HyprlandIpc::eventSocketStateChanged); + QObject::connect(&this->eventSocket, &QLocalSocket::readyRead, this, &HyprlandIpc::eventSocketReady); + // clang-format on + + // Sockets don't appear to be able to send data in the first event loop + // cycle of the program, so delay it by one. No idea why this is the case. + QTimer::singleShot(0, [this]() { + this->eventSocket.connectToServer(this->mEventSocketPath, QLocalSocket::ReadOnly); + this->refreshMonitors(true); + this->refreshWorkspaces(true); + }); +} + +QString HyprlandIpc::requestSocketPath() const { return this->mRequestSocketPath; } +QString HyprlandIpc::eventSocketPath() const { return this->mEventSocketPath; } + +void HyprlandIpc::eventSocketError(QLocalSocket::LocalSocketError error) const { + if (!this->valid) { + qWarning() << "Unable to connect to hyprland event socket:" << error; + } else { + qWarning() << "Hyprland event socket error:" << error; + } +} + +void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) { + if (state == QLocalSocket::ConnectedState) { + qCInfo(logHyprlandIpc) << "Hyprland event socket connected."; + emit this->connected(); + } else if (state == QLocalSocket::UnconnectedState && this->valid) { + qCWarning(logHyprlandIpc) << "Hyprland event socket disconnected."; + } + + this->valid = state == QLocalSocket::ConnectedState; +} + +void HyprlandIpc::eventSocketReady() { + while (true) { + auto rawEvent = this->eventSocket.readLine(); + if (rawEvent.isEmpty()) break; + + // remove trailing \n + rawEvent.truncate(rawEvent.length() - 1); + auto splitIdx = rawEvent.indexOf(">>"); + auto event = QByteArrayView(rawEvent.data(), splitIdx); + auto data = QByteArrayView( + rawEvent.data() + splitIdx + 2, // NOLINT + rawEvent.data() + rawEvent.length() // NOLINT + ); + qCDebug(logHyprlandIpcEvents) << "Received event:" << rawEvent << "parsed as" << event << data; + + this->event.name = event; + this->event.data = data; + this->onEvent(&this->event); + emit this->rawEvent(&this->event); + } +} + +void HyprlandIpc::makeRequest( + const QByteArray& request, + const std::function& callback +) { + auto* requestSocket = new QLocalSocket(this); + qCDebug(logHyprlandIpc) << "Making request:" << request; + + auto connectedCallback = [this, request, requestSocket, callback]() { + auto responseCallback = [requestSocket, callback]() { + auto response = requestSocket->readAll(); + callback(true, std::move(response)); + delete requestSocket; + }; + + QObject::connect(requestSocket, &QLocalSocket::readyRead, this, responseCallback); + + requestSocket->write(request); + }; + + auto errorCallback = [=](QLocalSocket::LocalSocketError error) { + qCWarning(logHyprlandIpc) << "Error making request:" << error << "request:" << request; + requestSocket->deleteLater(); + callback(false, {}); + }; + + QObject::connect(requestSocket, &QLocalSocket::connected, this, connectedCallback); + QObject::connect(requestSocket, &QLocalSocket::errorOccurred, this, errorCallback); + + requestSocket->connectToServer(this->mRequestSocketPath); +} + +void HyprlandIpc::dispatch(const QString& request) { + this->makeRequest( + ("dispatch " + request).toUtf8(), + [request](bool success, const QByteArray& response) { + if (!success) { + qCWarning(logHyprlandIpc) << "Failed to request dispatch of" << request; + return; + } + + if (response != "ok") { + qCWarning(logHyprlandIpc) + << "Dispatch request" << request << "failed with error" << response; + } + } + ); +} + +ObjectModel* HyprlandIpc::monitors() { return &this->mMonitors; } + +ObjectModel* HyprlandIpc::workspaces() { return &this->mWorkspaces; } + +QVector HyprlandIpc::parseEventArgs(QByteArrayView event, quint16 count) { + auto args = QVector(); + + for (auto i = 0; i < count - 1; i++) { + auto splitIdx = event.indexOf(','); + if (splitIdx == -1) break; + args.push_back(event.sliced(0, splitIdx)); + event = event.sliced(splitIdx + 1); + } + + if (!event.isEmpty()) { + args.push_back(event); + } + + return args; +} + +QVector HyprlandIpcEvent::parse(qint32 argumentCount) const { + auto args = QVector(); + + for (auto arg: this->parseView(argumentCount)) { + args.push_back(QString::fromUtf8(arg)); + } + + return args; +} + +QVector HyprlandIpcEvent::parseView(qint32 argumentCount) const { + return HyprlandIpc::parseEventArgs(this->data, argumentCount); +} + +QString HyprlandIpcEvent::nameStr() const { return QString::fromUtf8(this->name); } +QString HyprlandIpcEvent::dataStr() const { return QString::fromUtf8(this->data); } + +HyprlandIpc* HyprlandIpc::instance() { + static HyprlandIpc* instance = nullptr; // NOLINT + + if (instance == nullptr) { + instance = new HyprlandIpc(); + } + + return instance; +} + +void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { + if (event->name == "configreloaded") { + this->refreshMonitors(true); + this->refreshWorkspaces(true); + } else if (event->name == "monitoraddedv2") { + auto args = event->parseView(3); + + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + // hyprland will often reference the monitor before creation, in which case + // it will already exist. + auto* monitor = this->findMonitorByName(name, false); + auto existed = monitor != nullptr; + + if (monitor == nullptr) { + monitor = new HyprlandMonitor(this); + } + + qCDebug(logHyprlandIpc) << "Monitor added with id" << id << "name" << name + << "preemptively created:" << existed; + + monitor->updateInitial(id, name, QString::fromUtf8(args.at(2))); + + if (!existed) { + this->mMonitors.insertObject(monitor); + } + + // refresh even if it already existed because workspace focus might have changed. + this->refreshMonitors(false); + } else if (event->name == "monitorremoved") { + const auto& mList = this->mMonitors.valueList(); + auto name = QString::fromUtf8(event->data); + + auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { + return m->name() == name; + }); + + if (monitorIter == mList.end()) { + qCWarning(logHyprlandIpc) << "Got removal for monitor" << name + << "which was not previously tracked."; + return; + } + + auto index = monitorIter - mList.begin(); + auto* monitor = *monitorIter; + + qCDebug(logHyprlandIpc) << "Monitor removed with id" << monitor->id() << "name" + << monitor->name(); + this->mMonitors.removeAt(index); + + // delete the monitor object in the next event loop cycle so it's likely to + // still exist when future events reference it after destruction. + // If we get to the next cycle and things still reference it (unlikely), nulls + // can make it to the frontend. + monitor->deleteLater(); + } else if (event->name == "createworkspacev2") { + auto args = event->parseView(2); + + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + qCDebug(logHyprlandIpc) << "Workspace created with id" << id << "name" << name; + + auto* workspace = this->findWorkspaceByName(name, false); + auto existed = workspace != nullptr; + + if (workspace == nullptr) { + workspace = new HyprlandWorkspace(this); + } + + workspace->updateInitial(id, name); + + if (!existed) { + this->refreshWorkspaces(false); + this->mWorkspaces.insertObject(workspace); + } + } else if (event->name == "destroyworkspacev2") { + auto args = event->parseView(2); + + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + const auto& mList = this->mWorkspaces.valueList(); + + auto workspaceIter = std::find_if(mList.begin(), mList.end(), [id](const HyprlandWorkspace* m) { + return m->id() == id; + }); + + if (workspaceIter == mList.end()) { + qCWarning(logHyprlandIpc) << "Got removal for workspace id" << id << "name" << name + << "which was not previously tracked."; + return; + } + + auto index = workspaceIter - mList.begin(); + auto* workspace = *workspaceIter; + + qCDebug(logHyprlandIpc) << "Workspace removed with id" << id << "name" << name; + this->mWorkspaces.removeAt(index); + + // workspaces have not been observed to be referenced after deletion + delete workspace; + + for (auto* monitor: this->mMonitors.valueList()) { + if (monitor->activeWorkspace() == nullptr) { + // removing a monitor will cause a new workspace to be created and destroyed after removal, + // but it won't go back to a real workspace afterwards and just leaves a null, so we + // re-query monitors if this appears to be the case. + this->refreshMonitors(false); + break; + } + } + } else if (event->name == "focusedmon") { + auto args = event->parseView(2); + auto name = QString::fromUtf8(args.at(0)); + auto workspaceName = QString::fromUtf8(args.at(1)); + + HyprlandWorkspace* workspace = nullptr; + if (workspaceName != "?") { // what the fuck + workspace = this->findWorkspaceByName(workspaceName, false); + } + + auto* monitor = this->findMonitorByName(name, true); + this->setFocusedMonitor(monitor); + monitor->setActiveWorkspace(workspace); + } else if (event->name == "workspacev2") { + auto args = event->parseView(2); + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + if (this->mFocusedMonitor != nullptr) { + auto* workspace = this->findWorkspaceByName(name, true, id); + this->mFocusedMonitor->setActiveWorkspace(workspace); + } + } else if (event->name == "moveworkspacev2") { + auto args = event->parseView(3); + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + auto monitorName = QString::fromUtf8(args.at(2)); + + auto* workspace = this->findWorkspaceByName(name, true, id); + auto* monitor = this->findMonitorByName(monitorName, true); + + workspace->setMonitor(monitor); + } +} + +HyprlandWorkspace* +HyprlandIpc::findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id) { + const auto& mList = this->mWorkspaces.valueList(); + + auto workspaceIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { + return m->name() == name; + }); + + if (workspaceIter != mList.end()) { + return *workspaceIter; + } else if (createIfMissing) { + qCDebug(logHyprlandIpc) << "Workspace" << name + << "requested before creation, performing early init"; + auto* workspace = new HyprlandWorkspace(this); + workspace->updateInitial(id, name); + this->mWorkspaces.insertObject(workspace); + return workspace; + } else { + return nullptr; + } +} + +void HyprlandIpc::refreshWorkspaces(bool canCreate) { + if (this->requestingWorkspaces) return; + this->requestingWorkspaces = true; + + this->makeRequest("j/workspaces", [this, canCreate](bool success, const QByteArray& resp) { + this->requestingWorkspaces = false; + if (!success) return; + + qCDebug(logHyprlandIpc) << "parsing workspaces response"; + auto json = QJsonDocument::fromJson(resp).array(); + + const auto& mList = this->mWorkspaces.valueList(); + auto names = QVector(); + + for (auto entry: json) { + auto object = entry.toObject().toVariantMap(); + auto name = object.value("name").toString(); + + auto workspaceIter = + std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { + return m->name() == name; + }); + + auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; + auto existed = workspace != nullptr; + + if (workspace == nullptr) { + if (!canCreate) continue; + workspace = new HyprlandWorkspace(this); + } + + workspace->updateFromObject(object); + + if (!existed) { + this->mWorkspaces.insertObject(workspace); + } + + names.push_back(name); + } + + auto removedWorkspaces = QVector(); + + for (auto* workspace: mList) { + if (!names.contains(workspace->name())) { + removedWorkspaces.push_back(workspace); + } + } + + for (auto* workspace: removedWorkspaces) { + this->mWorkspaces.removeObject(workspace); + delete workspace; + } + }); +} + +HyprlandMonitor* +HyprlandIpc::findMonitorByName(const QString& name, bool createIfMissing, qint32 id) { + const auto& mList = this->mMonitors.valueList(); + + auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { + return m->name() == name; + }); + + if (monitorIter != mList.end()) { + return *monitorIter; + } else if (createIfMissing) { + qCDebug(logHyprlandIpc) << "Monitor" << name + << "requested before creation, performing early init"; + auto* monitor = new HyprlandMonitor(this); + monitor->updateInitial(id, name, ""); + this->mMonitors.insertObject(monitor); + return monitor; + } else { + return nullptr; + } +} + +HyprlandMonitor* HyprlandIpc::focusedMonitor() const { return this->mFocusedMonitor; } + +HyprlandMonitor* HyprlandIpc::monitorFor(QuickshellScreenInfo* screen) { + // Wayland monitors appear after hyprland ones are created and disappear after destruction + // so simply not doing any preemptive creation is enough. + + if (screen == nullptr) return nullptr; + return this->findMonitorByName(screen->name(), false); +} + +void HyprlandIpc::setFocusedMonitor(HyprlandMonitor* monitor) { + if (monitor == this->mFocusedMonitor) return; + + if (this->mFocusedMonitor != nullptr) { + QObject::disconnect(this->mFocusedMonitor, nullptr, this, nullptr); + } + + this->mFocusedMonitor = monitor; + + if (monitor != nullptr) { + QObject::connect(monitor, &QObject::destroyed, this, &HyprlandIpc::onFocusedMonitorDestroyed); + } + emit this->focusedMonitorChanged(); +} + +void HyprlandIpc::onFocusedMonitorDestroyed() { + this->mFocusedMonitor = nullptr; + emit this->focusedMonitorChanged(); +} + +void HyprlandIpc::refreshMonitors(bool canCreate) { + if (this->requestingMonitors) return; + this->requestingMonitors = true; + + this->makeRequest("j/monitors", [this, canCreate](bool success, const QByteArray& resp) { + this->requestingMonitors = false; + if (!success) return; + + qCDebug(logHyprlandIpc) << "parsing monitors response"; + auto json = QJsonDocument::fromJson(resp).array(); + + const auto& mList = this->mMonitors.valueList(); + auto ids = QVector(); + + for (auto entry: json) { + auto object = entry.toObject().toVariantMap(); + auto id = object.value("id").toInt(); + + auto monitorIter = std::find_if(mList.begin(), mList.end(), [id](const HyprlandMonitor* m) { + return m->id() == id; + }); + + auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; + auto existed = monitor != nullptr; + + if (monitor == nullptr) { + if (!canCreate) continue; + monitor = new HyprlandMonitor(this); + } + + monitor->updateFromObject(object); + + if (!existed) { + this->mMonitors.insertObject(monitor); + } + + ids.push_back(id); + } + + auto removedMonitors = QVector(); + + for (auto* monitor: mList) { + if (!ids.contains(monitor->id())) { + removedMonitors.push_back(monitor); + } + } + + for (auto* monitor: removedMonitors) { + this->mMonitors.removeObject(monitor); + // see comment in onEvent + monitor->deleteLater(); + } + }); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/connection.hpp b/src/wayland/hyprland/ipc/connection.hpp new file mode 100644 index 00000000..d566a866 --- /dev/null +++ b/src/wayland/hyprland/ipc/connection.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandMonitor; +class HyprlandWorkspace; + +} // namespace qs::hyprland::ipc + +Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandWorkspace*); +Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandMonitor*); + +namespace qs::hyprland::ipc { + +///! Live Hyprland IPC event. +/// Live Hyprland IPC event. Holding this object after the +/// signal handler exits is undefined as the event instance +/// is reused. +class HyprlandIpcEvent: public QObject { + Q_OBJECT; + /// The name of the event. + Q_PROPERTY(QString name READ nameStr CONSTANT); + /// The unparsed data of the event. + Q_PROPERTY(QString data READ dataStr CONSTANT); + QML_NAMED_ELEMENT(HyprlandEvent); + QML_UNCREATABLE("HyprlandIpcEvents cannot be created."); + +public: + HyprlandIpcEvent(QObject* parent): QObject(parent) {} + + /// Parse this event with a known number of arguments. + /// + /// Argument count is required as some events can contain commas + /// in the last argument, which can be ignored as long as the count is known. + Q_INVOKABLE [[nodiscard]] QVector parse(qint32 argumentCount) const; + [[nodiscard]] QVector parseView(qint32 argumentCount) const; + + [[nodiscard]] QString nameStr() const; + [[nodiscard]] QString dataStr() const; + + void reset(); + QByteArrayView name; + QByteArrayView data; +}; + +class HyprlandIpc: public QObject { + Q_OBJECT; + +public: + static HyprlandIpc* instance(); + + [[nodiscard]] QString requestSocketPath() const; + [[nodiscard]] QString eventSocketPath() const; + + void + makeRequest(const QByteArray& request, const std::function& callback); + void dispatch(const QString& request); + + [[nodiscard]] HyprlandMonitor* monitorFor(QuickshellScreenInfo* screen); + [[nodiscard]] HyprlandMonitor* focusedMonitor() const; + void setFocusedMonitor(HyprlandMonitor* monitor); + + [[nodiscard]] ObjectModel* monitors(); + [[nodiscard]] ObjectModel* workspaces(); + + // No byId because these preemptively create objects. The given id is set if created. + HyprlandWorkspace* findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id = 0); + HyprlandMonitor* findMonitorByName(const QString& name, bool createIfMissing, qint32 id = -1); + + // canCreate avoids making ghost workspaces when the connection races + void refreshWorkspaces(bool canCreate); + void refreshMonitors(bool canCreate); + + // The last argument may contain commas, so the count is required. + [[nodiscard]] static QVector parseEventArgs(QByteArrayView event, quint16 count); + +signals: + void connected(); + void rawEvent(HyprlandIpcEvent* event); + + void focusedMonitorChanged(); + +private slots: + void eventSocketError(QLocalSocket::LocalSocketError error) const; + void eventSocketStateChanged(QLocalSocket::LocalSocketState state); + void eventSocketReady(); + + void onFocusedMonitorDestroyed(); + +private: + explicit HyprlandIpc(); + + void onEvent(HyprlandIpcEvent* event); + + QLocalSocket eventSocket; + QString mRequestSocketPath; + QString mEventSocketPath; + bool valid = false; + bool requestingMonitors = false; + bool requestingWorkspaces = false; + + ObjectModel mMonitors {this}; + ObjectModel mWorkspaces {this}; + HyprlandMonitor* mFocusedMonitor = nullptr; + //HyprlandWorkspace* activeWorkspace = nullptr; + + HyprlandIpcEvent event {this}; +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/monitor.cpp b/src/wayland/hyprland/ipc/monitor.cpp new file mode 100644 index 00000000..8ee5e207 --- /dev/null +++ b/src/wayland/hyprland/ipc/monitor.cpp @@ -0,0 +1,136 @@ +#include "monitor.hpp" +#include + +#include +#include +#include +#include + +#include "workspace.hpp" + +namespace qs::hyprland::ipc { + +qint32 HyprlandMonitor::id() const { return this->mId; } +QString HyprlandMonitor::name() const { return this->mName; } +QString HyprlandMonitor::description() const { return this->mDescription; } +qint32 HyprlandMonitor::x() const { return this->mX; } +qint32 HyprlandMonitor::y() const { return this->mY; } +qint32 HyprlandMonitor::width() const { return this->mWidth; } +qint32 HyprlandMonitor::height() const { return this->mHeight; } +qreal HyprlandMonitor::scale() const { return this->mScale; } +QVariantMap HyprlandMonitor::lastIpcObject() const { return this->mLastIpcObject; } + +void HyprlandMonitor::updateInitial(qint32 id, QString name, QString description) { + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } + + if (description != this->mDescription) { + this->mDescription = std::move(description); + emit this->descriptionChanged(); + } +} + +void HyprlandMonitor::updateFromObject(QVariantMap object) { + auto id = object.value("id").value(); + auto name = object.value("name").value(); + auto description = object.value("description").value(); + auto x = object.value("x").value(); + auto y = object.value("y").value(); + auto width = object.value("width").value(); + auto height = object.value("height").value(); + auto scale = object.value("height").value(); + auto activeWorkspaceObj = object.value("activeWorkspace").value(); + auto activeWorkspaceId = activeWorkspaceObj.value("id").value(); + auto activeWorkspaceName = activeWorkspaceObj.value("name").value(); + auto focused = object.value("focused").value(); + + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } + + if (description != this->mDescription) { + this->mDescription = std::move(description); + emit this->descriptionChanged(); + } + + if (x != this->mX) { + this->mX = x; + emit this->xChanged(); + } + + if (y != this->mY) { + this->mY = y; + emit this->yChanged(); + } + + if (width != this->mWidth) { + this->mWidth = width; + emit this->widthChanged(); + } + + if (height != this->mHeight) { + this->mHeight = height; + emit this->heightChanged(); + } + + if (scale != this->mScale) { + this->mScale = scale; + emit this->scaleChanged(); + } + + if (this->mActiveWorkspace == nullptr || this->mActiveWorkspace->name() != activeWorkspaceName) { + auto* workspace = this->ipc->findWorkspaceByName(activeWorkspaceName, true, activeWorkspaceId); + workspace->setMonitor(this); + this->setActiveWorkspace(workspace); + } + + this->mLastIpcObject = std::move(object); + emit this->lastIpcObjectChanged(); + + if (focused) { + this->ipc->setFocusedMonitor(this); + } +} + +HyprlandWorkspace* HyprlandMonitor::activeWorkspace() const { return this->mActiveWorkspace; } + +void HyprlandMonitor::setActiveWorkspace(HyprlandWorkspace* workspace) { + if (workspace == this->mActiveWorkspace) return; + + if (this->mActiveWorkspace != nullptr) { + QObject::disconnect(this->mActiveWorkspace, nullptr, this, nullptr); + } + + this->mActiveWorkspace = workspace; + + if (workspace != nullptr) { + QObject::connect( + workspace, + &QObject::destroyed, + this, + &HyprlandMonitor::onActiveWorkspaceDestroyed + ); + } + + emit this->activeWorkspaceChanged(); +} + +void HyprlandMonitor::onActiveWorkspaceDestroyed() { + this->mActiveWorkspace = nullptr; + emit this->activeWorkspaceChanged(); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/monitor.hpp b/src/wayland/hyprland/ipc/monitor.hpp new file mode 100644 index 00000000..6b5d2ecc --- /dev/null +++ b/src/wayland/hyprland/ipc/monitor.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "connection.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandMonitor: public QObject { + Q_OBJECT; + Q_PROPERTY(qint32 id READ id NOTIFY idChanged); + Q_PROPERTY(QString name READ name NOTIFY nameChanged); + Q_PROPERTY(QString description READ description NOTIFY descriptionChanged); + Q_PROPERTY(qint32 x READ x NOTIFY xChanged); + Q_PROPERTY(qint32 y READ y NOTIFY yChanged); + Q_PROPERTY(qint32 width READ width NOTIFY widthChanged); + Q_PROPERTY(qint32 height READ height NOTIFY heightChanged); + Q_PROPERTY(qreal scale READ scale NOTIFY scaleChanged); + /// Last json returned for this monitor, as a javascript object. + /// + /// > [!WARNING] This is *not* updated unless the monitor object is fetched again from + /// > Hyprland. If you need a value that is subject to change and does not have a dedicated + /// > property, run `HyprlandIpc.refreshMonitors()` and wait for this property to update. + Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); + /// The currently active workspace on this monitor. May be null. + Q_PROPERTY(HyprlandWorkspace* activeWorkspace READ activeWorkspace NOTIFY activeWorkspaceChanged); + QML_ELEMENT; + QML_UNCREATABLE("HyprlandMonitors must be retrieved from the HyprlandIpc object."); + +public: + explicit HyprlandMonitor(HyprlandIpc* ipc): QObject(ipc), ipc(ipc) {} + + void updateInitial(qint32 id, QString name, QString description); + void updateFromObject(QVariantMap object); + + [[nodiscard]] qint32 id() const; + [[nodiscard]] QString name() const; + [[nodiscard]] QString description() const; + [[nodiscard]] qint32 x() const; + [[nodiscard]] qint32 y() const; + [[nodiscard]] qint32 width() const; + [[nodiscard]] qint32 height() const; + [[nodiscard]] qreal scale() const; + [[nodiscard]] QVariantMap lastIpcObject() const; + + void setActiveWorkspace(HyprlandWorkspace* workspace); + [[nodiscard]] HyprlandWorkspace* activeWorkspace() const; + +signals: + void idChanged(); + void nameChanged(); + void descriptionChanged(); + void xChanged(); + void yChanged(); + void widthChanged(); + void heightChanged(); + void scaleChanged(); + void lastIpcObjectChanged(); + void activeWorkspaceChanged(); + +private slots: + void onActiveWorkspaceDestroyed(); + +private: + HyprlandIpc* ipc; + + qint32 mId = -1; + QString mName; + QString mDescription; + qint32 mX = 0; + qint32 mY = 0; + qint32 mWidth = 0; + qint32 mHeight = 0; + qreal mScale = 0; + QVariantMap mLastIpcObject; + + HyprlandWorkspace* mActiveWorkspace = nullptr; +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/qml.cpp b/src/wayland/hyprland/ipc/qml.cpp new file mode 100644 index 00000000..1e75ee9c --- /dev/null +++ b/src/wayland/hyprland/ipc/qml.cpp @@ -0,0 +1,52 @@ +#include "qml.hpp" + +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" +#include "monitor.hpp" + +namespace qs::hyprland::ipc { + +HyprlandIpcQml::HyprlandIpcQml() { + auto* instance = HyprlandIpc::instance(); + + QObject::connect(instance, &HyprlandIpc::rawEvent, this, &HyprlandIpcQml::rawEvent); + QObject::connect( + instance, + &HyprlandIpc::focusedMonitorChanged, + this, + &HyprlandIpcQml::focusedMonitorChanged + ); +} + +void HyprlandIpcQml::dispatch(const QString& request) { + HyprlandIpc::instance()->dispatch(request); +} + +HyprlandMonitor* HyprlandIpcQml::monitorFor(QuickshellScreenInfo* screen) { + return HyprlandIpc::instance()->monitorFor(screen); +} + +void HyprlandIpcQml::refreshMonitors() { HyprlandIpc::instance()->refreshMonitors(false); } + +void HyprlandIpcQml::refreshWorkspaces() { HyprlandIpc::instance()->refreshWorkspaces(false); } + +QString HyprlandIpcQml::requestSocketPath() { return HyprlandIpc::instance()->requestSocketPath(); } + +QString HyprlandIpcQml::eventSocketPath() { return HyprlandIpc::instance()->eventSocketPath(); } + +HyprlandMonitor* HyprlandIpcQml::focusedMonitor() { + return HyprlandIpc::instance()->focusedMonitor(); +} + +ObjectModel* HyprlandIpcQml::monitors() { + return HyprlandIpc::instance()->monitors(); +} + +ObjectModel* HyprlandIpcQml::workspaces() { + return HyprlandIpc::instance()->workspaces(); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/qml.hpp b/src/wayland/hyprland/ipc/qml.hpp new file mode 100644 index 00000000..2d39623f --- /dev/null +++ b/src/wayland/hyprland/ipc/qml.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" +#include "monitor.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandIpcQml: public QObject { + Q_OBJECT; + /// Path to the request socket (.socket.sock) + Q_PROPERTY(QString requestSocketPath READ requestSocketPath CONSTANT); + /// Path to the event socket (.socket2.sock) + Q_PROPERTY(QString eventSocketPath READ eventSocketPath CONSTANT); + /// The currently focused hyprland monitor. May be null. + Q_PROPERTY(HyprlandMonitor* focusedMonitor READ focusedMonitor NOTIFY focusedMonitorChanged); + /// All hyprland monitors. + Q_PROPERTY(ObjectModel* monitors READ monitors CONSTANT); + /// All hyprland workspaces. + Q_PROPERTY(ObjectModel* workspaces READ workspaces CONSTANT); + QML_NAMED_ELEMENT(Hyprland); + QML_SINGLETON; + +public: + explicit HyprlandIpcQml(); + + /// Execute a hyprland [dispatcher](https://wiki.hyprland.org/Configuring/Dispatchers). + Q_INVOKABLE static void dispatch(const QString& request); + + /// Get the HyprlandMonitor object that corrosponds to a quickshell screen. + Q_INVOKABLE static HyprlandMonitor* monitorFor(QuickshellScreenInfo* screen); + + /// Refresh monitor information. + /// + /// Many actions that will invalidate monitor state don't send events, + /// so this function is available if required. + Q_INVOKABLE static void refreshMonitors(); + + /// Refresh workspace information. + /// + /// Many actions that will invalidate workspace state don't send events, + /// so this function is available if required. + Q_INVOKABLE static void refreshWorkspaces(); + + [[nodiscard]] static QString requestSocketPath(); + [[nodiscard]] static QString eventSocketPath(); + [[nodiscard]] static HyprlandMonitor* focusedMonitor(); + [[nodiscard]] static ObjectModel* monitors(); + [[nodiscard]] static ObjectModel* workspaces(); + +signals: + /// Emitted for every event that comes in through the hyprland event socket (socket2). + /// + /// See [Hyprland Wiki: IPC](https://wiki.hyprland.org/IPC/) for a list of events. + void rawEvent(HyprlandIpcEvent* event); + + void focusedMonitorChanged(); +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/workspace.cpp b/src/wayland/hyprland/ipc/workspace.cpp new file mode 100644 index 00000000..fbf8477f --- /dev/null +++ b/src/wayland/hyprland/ipc/workspace.cpp @@ -0,0 +1,79 @@ +#include "workspace.hpp" +#include + +#include +#include +#include +#include + +#include "monitor.hpp" + +namespace qs::hyprland::ipc { + +qint32 HyprlandWorkspace::id() const { return this->mId; } +QString HyprlandWorkspace::name() const { return this->mName; } +QVariantMap HyprlandWorkspace::lastIpcObject() const { return this->mLastIpcObject; } + +void HyprlandWorkspace::updateInitial(qint32 id, QString name) { + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } +} + +void HyprlandWorkspace::updateFromObject(QVariantMap object) { + auto id = object.value("id").value(); + auto name = object.value("name").value(); + auto monitorId = object.value("monitorID").value(); + auto monitorName = object.value("monitor").value(); + + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } + + if (!monitorName.isEmpty() + && (this->mMonitor == nullptr || this->mMonitor->name() != monitorName)) + { + auto* monitor = this->ipc->findMonitorByName(monitorName, true, monitorId); + this->setMonitor(monitor); + } + + this->mLastIpcObject = std::move(object); + emit this->lastIpcObjectChanged(); +} + +HyprlandMonitor* HyprlandWorkspace::monitor() const { return this->mMonitor; } + +void HyprlandWorkspace::setMonitor(HyprlandMonitor* monitor) { + if (monitor == this->mMonitor) return; + + if (this->mMonitor != nullptr) { + QObject::disconnect(this->mMonitor, nullptr, this, nullptr); + } + + this->mMonitor = monitor; + + if (monitor != nullptr) { + QObject::connect(monitor, &QObject::destroyed, this, &HyprlandWorkspace::onMonitorDestroyed); + } + + emit this->monitorChanged(); +} + +void HyprlandWorkspace::onMonitorDestroyed() { + this->mMonitor = nullptr; + emit this->monitorChanged(); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/workspace.hpp b/src/wayland/hyprland/ipc/workspace.hpp new file mode 100644 index 00000000..a63901e6 --- /dev/null +++ b/src/wayland/hyprland/ipc/workspace.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "connection.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandWorkspace: public QObject { + Q_OBJECT; + Q_PROPERTY(qint32 id READ id NOTIFY idChanged); + Q_PROPERTY(QString name READ name NOTIFY nameChanged); + /// Last json returned for this workspace, as a javascript object. + /// + /// > [!WARNING] This is *not* updated unless the workspace object is fetched again from + /// > Hyprland. If you need a value that is subject to change and does not have a dedicated + /// > property, run `HyprlandIpc.refreshWorkspaces()` and wait for this property to update. + Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); + Q_PROPERTY(HyprlandMonitor* monitor READ monitor NOTIFY monitorChanged); + QML_ELEMENT; + QML_UNCREATABLE("HyprlandWorkspaces must be retrieved from the HyprlandIpc object."); + +public: + explicit HyprlandWorkspace(HyprlandIpc* ipc): QObject(ipc), ipc(ipc) {} + + void updateInitial(qint32 id, QString name); + void updateFromObject(QVariantMap object); + + [[nodiscard]] qint32 id() const; + [[nodiscard]] QString name() const; + [[nodiscard]] QVariantMap lastIpcObject() const; + + void setMonitor(HyprlandMonitor* monitor); + [[nodiscard]] HyprlandMonitor* monitor() const; + +signals: + void idChanged(); + void nameChanged(); + void lastIpcObjectChanged(); + void monitorChanged(); + +private slots: + void onMonitorDestroyed(); + +private: + HyprlandIpc* ipc; + + qint32 mId = -1; + QString mName; + QVariantMap mLastIpcObject; + HyprlandMonitor* mMonitor = nullptr; +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/module.md b/src/wayland/hyprland/module.md index 1b3e2fbf..6c2de249 100644 --- a/src/wayland/hyprland/module.md +++ b/src/wayland/hyprland/module.md @@ -1,6 +1,10 @@ name = "Quickshell.Hyprland" description = "Hyprland specific Quickshell types" headers = [ + "ipc/connection.hpp", + "ipc/monitor.hpp", + "ipc/workspace.hpp", + "ipc/qml.hpp", "focus_grab/qml.hpp", "global_shortcuts/qml.hpp", ] From ef1a4134f052998c15369e03cb3b2cb18adfb553 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 6 Jun 2024 00:46:38 -0700 Subject: [PATCH 035/305] hyprland/ipc: re-request monitors and workspaces on fail --- src/wayland/hyprland/ipc/connection.cpp | 167 +++++++++++++----------- src/wayland/hyprland/ipc/connection.hpp | 4 +- 2 files changed, 93 insertions(+), 78 deletions(-) diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index e7265c70..dcb57654 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -377,59 +377,66 @@ HyprlandIpc::findWorkspaceByName(const QString& name, bool createIfMissing, qint } } -void HyprlandIpc::refreshWorkspaces(bool canCreate) { +void HyprlandIpc::refreshWorkspaces(bool canCreate, bool tryAgain) { if (this->requestingWorkspaces) return; this->requestingWorkspaces = true; - this->makeRequest("j/workspaces", [this, canCreate](bool success, const QByteArray& resp) { - this->requestingWorkspaces = false; - if (!success) return; + this->makeRequest( + "j/workspaces", + [this, canCreate, tryAgain](bool success, const QByteArray& resp) { + this->requestingWorkspaces = false; + if (!success) { + // sometimes fails randomly, so we give it another shot. + if (tryAgain) this->refreshWorkspaces(canCreate, false); + return; + } - qCDebug(logHyprlandIpc) << "parsing workspaces response"; - auto json = QJsonDocument::fromJson(resp).array(); + qCDebug(logHyprlandIpc) << "parsing workspaces response"; + auto json = QJsonDocument::fromJson(resp).array(); - const auto& mList = this->mWorkspaces.valueList(); - auto names = QVector(); + const auto& mList = this->mWorkspaces.valueList(); + auto names = QVector(); - for (auto entry: json) { - auto object = entry.toObject().toVariantMap(); - auto name = object.value("name").toString(); + for (auto entry: json) { + auto object = entry.toObject().toVariantMap(); + auto name = object.value("name").toString(); - auto workspaceIter = - std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { - return m->name() == name; - }); + auto workspaceIter = + std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { + return m->name() == name; + }); - auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; - auto existed = workspace != nullptr; + auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; + auto existed = workspace != nullptr; - if (workspace == nullptr) { - if (!canCreate) continue; - workspace = new HyprlandWorkspace(this); - } + if (workspace == nullptr) { + if (!canCreate) continue; + workspace = new HyprlandWorkspace(this); + } - workspace->updateFromObject(object); + workspace->updateFromObject(object); - if (!existed) { - this->mWorkspaces.insertObject(workspace); - } + if (!existed) { + this->mWorkspaces.insertObject(workspace); + } - names.push_back(name); - } + names.push_back(name); + } - auto removedWorkspaces = QVector(); + auto removedWorkspaces = QVector(); - for (auto* workspace: mList) { - if (!names.contains(workspace->name())) { - removedWorkspaces.push_back(workspace); - } - } + for (auto* workspace: mList) { + if (!names.contains(workspace->name())) { + removedWorkspaces.push_back(workspace); + } + } - for (auto* workspace: removedWorkspaces) { - this->mWorkspaces.removeObject(workspace); - delete workspace; - } - }); + for (auto* workspace: removedWorkspaces) { + this->mWorkspaces.removeObject(workspace); + delete workspace; + } + } + ); } HyprlandMonitor* @@ -484,59 +491,67 @@ void HyprlandIpc::onFocusedMonitorDestroyed() { emit this->focusedMonitorChanged(); } -void HyprlandIpc::refreshMonitors(bool canCreate) { +void HyprlandIpc::refreshMonitors(bool canCreate, bool tryAgain) { if (this->requestingMonitors) return; this->requestingMonitors = true; - this->makeRequest("j/monitors", [this, canCreate](bool success, const QByteArray& resp) { - this->requestingMonitors = false; - if (!success) return; + this->makeRequest( + "j/monitors", + [this, canCreate, tryAgain](bool success, const QByteArray& resp) { + this->requestingMonitors = false; + if (!success) { + // sometimes fails randomly, so we give it another shot. + if (tryAgain) this->refreshMonitors(canCreate, false); + return; + } - qCDebug(logHyprlandIpc) << "parsing monitors response"; - auto json = QJsonDocument::fromJson(resp).array(); + qCDebug(logHyprlandIpc) << "parsing monitors response"; + auto json = QJsonDocument::fromJson(resp).array(); - const auto& mList = this->mMonitors.valueList(); - auto ids = QVector(); + const auto& mList = this->mMonitors.valueList(); + auto ids = QVector(); - for (auto entry: json) { - auto object = entry.toObject().toVariantMap(); - auto id = object.value("id").toInt(); + for (auto entry: json) { + auto object = entry.toObject().toVariantMap(); + auto id = object.value("id").toInt(); - auto monitorIter = std::find_if(mList.begin(), mList.end(), [id](const HyprlandMonitor* m) { - return m->id() == id; - }); + auto monitorIter = + std::find_if(mList.begin(), mList.end(), [id](const HyprlandMonitor* m) { + return m->id() == id; + }); - auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; - auto existed = monitor != nullptr; + auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; + auto existed = monitor != nullptr; - if (monitor == nullptr) { - if (!canCreate) continue; - monitor = new HyprlandMonitor(this); - } + if (monitor == nullptr) { + if (!canCreate) continue; + monitor = new HyprlandMonitor(this); + } - monitor->updateFromObject(object); + monitor->updateFromObject(object); - if (!existed) { - this->mMonitors.insertObject(monitor); - } + if (!existed) { + this->mMonitors.insertObject(monitor); + } - ids.push_back(id); - } + ids.push_back(id); + } - auto removedMonitors = QVector(); + auto removedMonitors = QVector(); - for (auto* monitor: mList) { - if (!ids.contains(monitor->id())) { - removedMonitors.push_back(monitor); - } - } + for (auto* monitor: mList) { + if (!ids.contains(monitor->id())) { + removedMonitors.push_back(monitor); + } + } - for (auto* monitor: removedMonitors) { - this->mMonitors.removeObject(monitor); - // see comment in onEvent - monitor->deleteLater(); - } - }); + for (auto* monitor: removedMonitors) { + this->mMonitors.removeObject(monitor); + // see comment in onEvent + monitor->deleteLater(); + } + } + ); } } // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/connection.hpp b/src/wayland/hyprland/ipc/connection.hpp index d566a866..0144ab43 100644 --- a/src/wayland/hyprland/ipc/connection.hpp +++ b/src/wayland/hyprland/ipc/connection.hpp @@ -81,8 +81,8 @@ public: HyprlandMonitor* findMonitorByName(const QString& name, bool createIfMissing, qint32 id = -1); // canCreate avoids making ghost workspaces when the connection races - void refreshWorkspaces(bool canCreate); - void refreshMonitors(bool canCreate); + void refreshWorkspaces(bool canCreate, bool tryAgain = true); + void refreshMonitors(bool canCreate, bool tryAgain = true); // The last argument may contain commas, so the count is required. [[nodiscard]] static QVector parseEventArgs(QByteArrayView event, quint16 count); From bc349998dfcd155951cde962cb09dae0548b8508 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 6 Jun 2024 00:58:10 -0700 Subject: [PATCH 036/305] hyprland/ipc: match by name in refreshMonitors instead of id Was causing ghost/duplicate monitors from usages where the id was not known. --- src/wayland/hyprland/ipc/connection.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index dcb57654..6dcba3ea 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -509,15 +509,15 @@ void HyprlandIpc::refreshMonitors(bool canCreate, bool tryAgain) { auto json = QJsonDocument::fromJson(resp).array(); const auto& mList = this->mMonitors.valueList(); - auto ids = QVector(); + auto names = QVector(); for (auto entry: json) { auto object = entry.toObject().toVariantMap(); - auto id = object.value("id").toInt(); + auto name = object.value("name").toString(); auto monitorIter = - std::find_if(mList.begin(), mList.end(), [id](const HyprlandMonitor* m) { - return m->id() == id; + std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { + return m->name() == name; }); auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; @@ -534,13 +534,13 @@ void HyprlandIpc::refreshMonitors(bool canCreate, bool tryAgain) { this->mMonitors.insertObject(monitor); } - ids.push_back(id); + names.push_back(name); } auto removedMonitors = QVector(); for (auto* monitor: mList) { - if (!ids.contains(monitor->id())) { + if (!names.contains(monitor->name())) { removedMonitors.push_back(monitor); } } From 5d1def3e49be3ed4abc611247d0e9fb084bfdba8 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 6 Jun 2024 00:59:17 -0700 Subject: [PATCH 037/305] hyprland/ipc: fix monitorFor returning null during HyprlandIpc init --- src/wayland/hyprland/ipc/connection.cpp | 8 ++++++-- src/wayland/hyprland/ipc/connection.hpp | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index 6dcba3ea..5ee8fffe 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -465,10 +465,12 @@ HyprlandMonitor* HyprlandIpc::focusedMonitor() const { return this->mFocusedMoni HyprlandMonitor* HyprlandIpc::monitorFor(QuickshellScreenInfo* screen) { // Wayland monitors appear after hyprland ones are created and disappear after destruction - // so simply not doing any preemptive creation is enough. + // so simply not doing any preemptive creation is enough, however if this call creates + // the HyprlandIpc singleton then monitors won't be initialized, in which case we + // preemptively create one. if (screen == nullptr) return nullptr; - return this->findMonitorByName(screen->name(), false); + return this->findMonitorByName(screen->name(), !this->monitorsRequested); } void HyprlandIpc::setFocusedMonitor(HyprlandMonitor* monitor) { @@ -505,6 +507,8 @@ void HyprlandIpc::refreshMonitors(bool canCreate, bool tryAgain) { return; } + this->monitorsRequested = true; + qCDebug(logHyprlandIpc) << "parsing monitors response"; auto json = QJsonDocument::fromJson(resp).array(); diff --git a/src/wayland/hyprland/ipc/connection.hpp b/src/wayland/hyprland/ipc/connection.hpp index 0144ab43..1778460a 100644 --- a/src/wayland/hyprland/ipc/connection.hpp +++ b/src/wayland/hyprland/ipc/connection.hpp @@ -111,6 +111,7 @@ private: bool valid = false; bool requestingMonitors = false; bool requestingWorkspaces = false; + bool monitorsRequested = false; ObjectModel mMonitors {this}; ObjectModel mWorkspaces {this}; From b5b9c1f6c352f5e495f580618f5d176497f7814b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 7 Jun 2024 04:31:20 -0700 Subject: [PATCH 038/305] wayland/toplevel_management: add foreign toplevel management --- .clang-tidy | 1 + BUILD.md | 15 +- CMakeLists.txt | 2 + src/wayland/CMakeLists.txt | 5 + src/wayland/module.md | 1 + .../toplevel_management/CMakeLists.txt | 22 ++ src/wayland/toplevel_management/handle.cpp | 228 +++++++++++++++ src/wayland/toplevel_management/handle.hpp | 77 +++++ src/wayland/toplevel_management/manager.cpp | 67 +++++ src/wayland/toplevel_management/manager.hpp | 47 +++ src/wayland/toplevel_management/qml.cpp | 153 ++++++++++ src/wayland/toplevel_management/qml.hpp | 140 +++++++++ ...oreign-toplevel-management-unstable-v1.xml | 270 ++++++++++++++++++ 13 files changed, 1026 insertions(+), 2 deletions(-) create mode 100644 src/wayland/toplevel_management/CMakeLists.txt create mode 100644 src/wayland/toplevel_management/handle.cpp create mode 100644 src/wayland/toplevel_management/handle.hpp create mode 100644 src/wayland/toplevel_management/manager.cpp create mode 100644 src/wayland/toplevel_management/manager.hpp create mode 100644 src/wayland/toplevel_management/qml.cpp create mode 100644 src/wayland/toplevel_management/qml.hpp create mode 100644 src/wayland/toplevel_management/wlr-foreign-toplevel-management-unstable-v1.xml diff --git a/.clang-tidy b/.clang-tidy index 6362e662..1da445cd 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -5,6 +5,7 @@ Checks: > -*, bugprone-*, -bugprone-easily-swappable-parameters, + -bugprone-forward-declararion-namespace, concurrency-*, cppcoreguidelines-*, -cppcoreguidelines-owning-memory, diff --git a/BUILD.md b/BUILD.md index c9909598..3c3e7125 100644 --- a/BUILD.md +++ b/BUILD.md @@ -59,20 +59,31 @@ Dependencies: - `wayland-protocols` #### Wlroots Layershell -Enables wlroots layershell integration through the [wlr-layer-shell-unstable-v1] protocol, +Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol, enabling use cases such as bars overlays and backgrounds. This feature has no extra dependencies. To disable: `-DWAYLAND_WLR_LAYERSHELL=OFF` -[wlr-layer-shell-unstable-v1]: https://wayland.app/protocols/wlr-layer-shell-unstable-v1 +[zwlr-layer-shell-v1]: https://wayland.app/protocols/wlr-layer-shell-unstable-v1 #### Session Lock Enables session lock support through the [ext-session-lock-v1] protocol, which allows quickshell to be used as a session lock under compatible wayland compositors. +To disable: `-DWAYLAND_SESSION_LOCK=OFF` + [ext-session-lock-v1]: https://wayland.app/protocols/ext-session-lock-v1 + +#### Foreign Toplevel Management +Enables management of windows of other clients through the [zwlr-foreign-toplevel-management-v1] protocol, +which allows quickshell to be used as a session lock under compatible wayland compositors. + +[zwlr-foreign-toplevel-management-v1]: https://wayland.app/protocols/wlr-foreign-toplevel-management-unstable-v1 + +To disable: `-DWAYLAND_TOPLEVEL_MANAGEMENT=OFF` + ### X11 This feature enables x11 support. Currently this implements panel windows for X11 similarly to the wlroots layershell above. diff --git a/CMakeLists.txt b/CMakeLists.txt index 246428ec..7af6b6cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ option(SOCKETS "Enable unix socket support" ON) option(WAYLAND "Enable wayland support" ON) option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON) option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON) +option(WAYLAND_TOPLEVEL_MANAGEMENT "Support the zwlr_foreign_toplevel_management_v1 wayland protocol" ON) option(X11 "Enable X11 support" ON) option(HYPRLAND "Support hyprland specific features" ON) option(HYPRLAND_IPC "Hyprland IPC" ON) @@ -31,6 +32,7 @@ message(STATUS " Wayland: ${WAYLAND}") if (WAYLAND) message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}") message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}") + message(STATUS " Toplevel Management: ${WAYLAND_TOPLEVEL_MANAGEMENT}") endif () message(STATUS " X11: ${X11}") message(STATUS " Services") diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index f20bc11d..ac8f42bb 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -71,6 +71,11 @@ if (WAYLAND_SESSION_LOCK) add_subdirectory(session_lock) endif() +if (WAYLAND_TOPLEVEL_MANAGEMENT) + add_subdirectory(toplevel_management) + list(APPEND WAYLAND_MODULES Quickshell.Wayland._ToplevelManagement) +endif() + if (HYPRLAND) add_subdirectory(hyprland) endif() diff --git a/src/wayland/module.md b/src/wayland/module.md index 7a427df9..d6376e39 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -4,5 +4,6 @@ headers = [ "wlr_layershell/window.hpp", "wlr_layershell.hpp", "session_lock.hpp", + "toplevel_management/qml.hpp", ] ----- diff --git a/src/wayland/toplevel_management/CMakeLists.txt b/src/wayland/toplevel_management/CMakeLists.txt new file mode 100644 index 00000000..4537c201 --- /dev/null +++ b/src/wayland/toplevel_management/CMakeLists.txt @@ -0,0 +1,22 @@ +qt_add_library(quickshell-wayland-toplevel-management STATIC + manager.cpp + handle.cpp + qml.cpp +) + +qt_add_qml_module(quickshell-wayland-toplevel-management + URI Quickshell.Wayland._ToplevelManagement + VERSION 0.1 +) + +wl_proto(quickshell-wayland-toplevel-management + wlr-foreign-toplevel-management-unstable-v1 + "${CMAKE_CURRENT_SOURCE_DIR}/wlr-foreign-toplevel-management-unstable-v1.xml" +) + +target_link_libraries(quickshell-wayland-toplevel-management PRIVATE ${QT_DEPS} wayland-client) + +qs_pch(quickshell-wayland-toplevel-management) +qs_pch(quickshell-wayland-toplevel-managementplugin) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-toplevel-managementplugin) diff --git a/src/wayland/toplevel_management/handle.cpp b/src/wayland/toplevel_management/handle.cpp new file mode 100644 index 00000000..8c2886b4 --- /dev/null +++ b/src/wayland/toplevel_management/handle.cpp @@ -0,0 +1,228 @@ +#include "handle.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" +#include "qwayland-wlr-foreign-toplevel-management-unstable-v1.h" +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +QString ToplevelHandle::appId() const { return this->mAppId; } +QString ToplevelHandle::title() const { return this->mTitle; } +QVector ToplevelHandle::visibleScreens() const { return this->mVisibleScreens; } +ToplevelHandle* ToplevelHandle::parent() const { return this->mParent; } +bool ToplevelHandle::activated() const { return this->mActivated; } +bool ToplevelHandle::maximized() const { return this->mMaximized; } +bool ToplevelHandle::minimized() const { return this->mMinimized; } +bool ToplevelHandle::fullscreen() const { return this->mFullscreen; } + +void ToplevelHandle::activate() { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* inputDevice = display->lastInputDevice(); + if (inputDevice == nullptr) return; + this->QtWayland::zwlr_foreign_toplevel_handle_v1::activate(inputDevice->object()); +} + +void ToplevelHandle::setMaximized(bool maximized) { + if (maximized) this->set_maximized(); + else this->unset_maximized(); +} + +void ToplevelHandle::setMinimized(bool minimized) { + if (minimized) this->set_minimized(); + else this->unset_minimized(); +} + +void ToplevelHandle::setFullscreen(bool fullscreen) { + if (fullscreen) this->set_fullscreen(nullptr); + else this->unset_fullscreen(); +} + +void ToplevelHandle::fullscreenOn(QScreen* screen) { + auto* waylandScreen = dynamic_cast(screen->handle()); + this->set_fullscreen(waylandScreen != nullptr ? waylandScreen->output() : nullptr); +} + +void ToplevelHandle::setRectangle(QWindow* window, QRect rect) { + if (window == nullptr) { + // will be cleared by the compositor if the surface is destroyed + if (this->rectWindow != nullptr) { + auto* waylandWindow = + dynamic_cast(this->rectWindow->handle()); + + if (waylandWindow != nullptr) { + this->set_rectangle(waylandWindow->surface(), 0, 0, 0, 0); + } + } + + QObject::disconnect(this->rectWindow, nullptr, this, nullptr); + this->rectWindow = nullptr; + return; + } + + if (this->rectWindow != window) { + if (this->rectWindow != nullptr) { + QObject::disconnect(this->rectWindow, nullptr, this, nullptr); + } + + this->rectWindow = window; + QObject::connect(window, &QObject::destroyed, this, &ToplevelHandle::onRectWindowDestroyed); + } + + if (auto* waylandWindow = dynamic_cast(window->handle())) { + this->set_rectangle(waylandWindow->surface(), rect.x(), rect.y(), rect.width(), rect.height()); + } else { + QObject::connect(window, &QWindow::visibleChanged, this, [this, window, rect]() { + if (window->isVisible()) { + if (window->handle() == nullptr) { + window->create(); + } + + auto* waylandWindow = dynamic_cast(window->handle()); + this->set_rectangle( + waylandWindow->surface(), + rect.x(), + rect.y(), + rect.width(), + rect.height() + ); + } + }); + } +} + +void ToplevelHandle::onRectWindowDestroyed() { this->rectWindow = nullptr; } + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_done() { + qCDebug(logToplevelManagement) << this << "got done"; + auto wasReady = this->isReady; + this->isReady = true; + + if (!wasReady) { + emit this->ready(); + } +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_closed() { + qCDebug(logToplevelManagement) << this << "closed"; + this->destroy(); + emit this->closed(); + delete this; +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_app_id(const QString& appId) { + qCDebug(logToplevelManagement) << this << "got appid" << appId; + this->mAppId = appId; + emit this->appIdChanged(); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_title(const QString& title) { + qCDebug(logToplevelManagement) << this << "got toplevel" << title; + this->mTitle = title; + emit this->titleChanged(); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_state(wl_array* stateArray) { + auto activated = false; + auto maximized = false; + auto minimized = false; + auto fullscreen = false; + + // wl_array_for_each is illegal in C++ so it is manually expanded. + auto* state = static_cast<::zwlr_foreign_toplevel_handle_v1_state*>(stateArray->data); + auto size = stateArray->size / sizeof(::zwlr_foreign_toplevel_handle_v1_state); + for (size_t i = 0; i < size; i++) { + auto flag = state[i]; // NOLINT + switch (flag) { + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED: activated = true; break; + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED: maximized = true; break; + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED: minimized = true; break; + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN: fullscreen = true; break; + } + } + + qCDebug(logToplevelManagement) << this << "got state update - activated:" << activated + << "maximized:" << maximized << "minimized:" << minimized + << "fullscreen:" << fullscreen; + + if (activated != this->mActivated) { + this->mActivated = activated; + emit this->activatedChanged(); + } + + if (maximized != this->mMaximized) { + this->mMaximized = maximized; + emit this->maximizedChanged(); + } + + if (minimized != this->mMinimized) { + this->mMinimized = minimized; + emit this->minimizedChanged(); + } + + if (fullscreen != this->mFullscreen) { + this->mFullscreen = fullscreen; + emit this->fullscreenChanged(); + } +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_output_enter(wl_output* output) { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* screen = display->screenForOutput(output)->screen(); + + qCDebug(logToplevelManagement) << this << "got output enter" << screen; + + this->mVisibleScreens.push_back(screen); + emit this->visibleScreenAdded(screen); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_output_leave(wl_output* output) { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* screen = display->screenForOutput(output)->screen(); + + qCDebug(logToplevelManagement) << this << "got output leave" << screen; + + emit this->visibleScreenRemoved(screen); + this->mVisibleScreens.removeOne(screen); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_parent( + ::zwlr_foreign_toplevel_handle_v1* parent +) { + auto* handle = ToplevelManager::instance()->handleFor(parent); + qCDebug(logToplevelManagement) << this << "got parent" << handle; + + if (handle != this->mParent) { + if (this->mParent != nullptr) { + QObject::disconnect(this->mParent, nullptr, this, nullptr); + } + + this->mParent = handle; + + if (handle != nullptr) { + QObject::connect(handle, &ToplevelHandle::closed, this, &ToplevelHandle::onParentClosed); + } + + emit this->parentChanged(); + } +} + +void ToplevelHandle::onParentClosed() { + this->mParent = nullptr; + emit this->parentChanged(); +} + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/handle.hpp b/src/wayland/toplevel_management/handle.hpp new file mode 100644 index 00000000..a49afe82 --- /dev/null +++ b/src/wayland/toplevel_management/handle.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +class ToplevelHandle + : public QObject + , public QtWayland::zwlr_foreign_toplevel_handle_v1 { + Q_OBJECT; + +public: + [[nodiscard]] QString appId() const; + [[nodiscard]] QString title() const; + [[nodiscard]] QVector visibleScreens() const; + [[nodiscard]] ToplevelHandle* parent() const; + [[nodiscard]] bool activated() const; + [[nodiscard]] bool maximized() const; + [[nodiscard]] bool minimized() const; + [[nodiscard]] bool fullscreen() const; + + void activate(); + void setMaximized(bool maximized); + void setMinimized(bool minimized); + void setFullscreen(bool fullscreen); + void fullscreenOn(QScreen* screen); + void setRectangle(QWindow* window, QRect rect); + +signals: + // sent after the first done event. + void ready(); + // sent right before delete this. + void closed(); + + void appIdChanged(); + void titleChanged(); + void visibleScreenAdded(QScreen* screen); + void visibleScreenRemoved(QScreen* screen); + void parentChanged(); + void activatedChanged(); + void maximizedChanged(); + void minimizedChanged(); + void fullscreenChanged(); + +private slots: + void onParentClosed(); + void onRectWindowDestroyed(); + +private: + void zwlr_foreign_toplevel_handle_v1_done() override; + void zwlr_foreign_toplevel_handle_v1_closed() override; + void zwlr_foreign_toplevel_handle_v1_app_id(const QString& appId) override; + void zwlr_foreign_toplevel_handle_v1_title(const QString& title) override; + void zwlr_foreign_toplevel_handle_v1_state(wl_array* stateArray) override; + void zwlr_foreign_toplevel_handle_v1_output_enter(wl_output* output) override; + void zwlr_foreign_toplevel_handle_v1_output_leave(wl_output* output) override; + void zwlr_foreign_toplevel_handle_v1_parent(::zwlr_foreign_toplevel_handle_v1* parent) override; + + bool isReady = false; + QString mAppId; + QString mTitle; + QVector mVisibleScreens; + ToplevelHandle* mParent = nullptr; + bool mActivated = false; + bool mMaximized = false; + bool mMinimized = false; + bool mFullscreen = false; + QWindow* rectWindow = nullptr; +}; + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/manager.cpp b/src/wayland/toplevel_management/manager.cpp new file mode 100644 index 00000000..bd477b49 --- /dev/null +++ b/src/wayland/toplevel_management/manager.cpp @@ -0,0 +1,67 @@ +#include "manager.hpp" + +#include +#include +#include +#include +#include +#include + +#include "handle.hpp" +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +Q_LOGGING_CATEGORY(logToplevelManagement, "quickshell.wayland.toplevelManagement", QtWarningMsg); + +ToplevelManager::ToplevelManager(): QWaylandClientExtensionTemplate(3) { this->initialize(); } + +bool ToplevelManager::available() const { return this->isActive(); } + +const QVector& ToplevelManager::readyToplevels() const { + return this->mReadyToplevels; +} + +ToplevelHandle* ToplevelManager::handleFor(::zwlr_foreign_toplevel_handle_v1* toplevel) { + if (toplevel == nullptr) return nullptr; + + for (auto* other: this->mToplevels) { + if (other->object() == toplevel) return other; + } + + return nullptr; +} + +ToplevelManager* ToplevelManager::instance() { + static auto* instance = new ToplevelManager(); // NOLINT + return instance; +} + +void ToplevelManager::zwlr_foreign_toplevel_manager_v1_toplevel( + ::zwlr_foreign_toplevel_handle_v1* toplevel +) { + auto* handle = new ToplevelHandle(); + QObject::connect(handle, &ToplevelHandle::closed, this, &ToplevelManager::onToplevelClosed); + QObject::connect(handle, &ToplevelHandle::ready, this, &ToplevelManager::onToplevelReady); + + qCDebug(logToplevelManagement) << "Toplevel handle created" << handle; + this->mToplevels.push_back(handle); + + // Not done in constructor as a close could technically be picked up immediately on init, + // making touching the handle a UAF. + handle->init(toplevel); +} + +void ToplevelManager::onToplevelReady() { + auto* handle = qobject_cast(this->sender()); + this->mReadyToplevels.push_back(handle); + emit this->toplevelReady(handle); +} + +void ToplevelManager::onToplevelClosed() { + auto* handle = qobject_cast(this->sender()); + this->mReadyToplevels.removeOne(handle); + this->mToplevels.removeOne(handle); +} + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/manager.hpp b/src/wayland/toplevel_management/manager.hpp new file mode 100644 index 00000000..41848de1 --- /dev/null +++ b/src/wayland/toplevel_management/manager.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +class ToplevelHandle; + +Q_DECLARE_LOGGING_CATEGORY(logToplevelManagement); + +class ToplevelManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zwlr_foreign_toplevel_manager_v1 { + Q_OBJECT; + +public: + [[nodiscard]] bool available() const; + [[nodiscard]] const QVector& readyToplevels() const; + [[nodiscard]] ToplevelHandle* handleFor(::zwlr_foreign_toplevel_handle_v1* toplevel); + + static ToplevelManager* instance(); + +signals: + void toplevelReady(ToplevelHandle* toplevel); + +protected: + explicit ToplevelManager(); + + void zwlr_foreign_toplevel_manager_v1_toplevel(::zwlr_foreign_toplevel_handle_v1* toplevel + ) override; + +private slots: + void onToplevelReady(); + void onToplevelClosed(); + +private: + QVector mToplevels; + QVector mReadyToplevels; +}; + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/qml.cpp b/src/wayland/toplevel_management/qml.cpp new file mode 100644 index 00000000..2042262b --- /dev/null +++ b/src/wayland/toplevel_management/qml.cpp @@ -0,0 +1,153 @@ +#include "qml.hpp" + +#include +#include + +#include "../../core/model.hpp" +#include "../../core/proxywindow.hpp" +#include "../../core/qmlscreen.hpp" +#include "../../core/windowinterface.hpp" +#include "handle.hpp" +#include "manager.hpp" + +namespace qs::wayland::toplevel_management { + +Toplevel::Toplevel(impl::ToplevelHandle* handle, QObject* parent): QObject(parent), handle(handle) { + // clang-format off + QObject::connect(handle, &impl::ToplevelHandle::closed, this, &Toplevel::onClosed); + QObject::connect(handle, &impl::ToplevelHandle::appIdChanged, this, &Toplevel::appIdChanged); + QObject::connect(handle, &impl::ToplevelHandle::titleChanged, this, &Toplevel::titleChanged); + QObject::connect(handle, &impl::ToplevelHandle::parentChanged, this, &Toplevel::parentChanged); + QObject::connect(handle, &impl::ToplevelHandle::activatedChanged, this, &Toplevel::activatedChanged); + QObject::connect(handle, &impl::ToplevelHandle::maximizedChanged, this, &Toplevel::maximizedChanged); + QObject::connect(handle, &impl::ToplevelHandle::minimizedChanged, this, &Toplevel::minimizedChanged); + QObject::connect(handle, &impl::ToplevelHandle::fullscreenChanged, this, &Toplevel::fullscreenChanged); + // clang-format on +} + +void Toplevel::onClosed() { + emit this->closed(); + delete this; +} + +void Toplevel::activate() { this->handle->activate(); } + +QString Toplevel::appId() const { return this->handle->appId(); } +QString Toplevel::title() const { return this->handle->title(); } + +Toplevel* Toplevel::parent() const { + return ToplevelManager::instance()->forImpl(this->handle->parent()); +} + +bool Toplevel::activated() const { return this->handle->activated(); } + +bool Toplevel::maximized() const { return this->handle->maximized(); } +void Toplevel::setMaximized(bool maximized) { this->handle->setMaximized(maximized); } + +bool Toplevel::minimized() const { return this->handle->minimized(); } +void Toplevel::setMinimized(bool minimized) { this->handle->setMinimized(minimized); } + +bool Toplevel::fullscreen() const { return this->handle->fullscreen(); } +void Toplevel::setFullscreen(bool fullscreen) { this->handle->setFullscreen(fullscreen); } + +void Toplevel::fullscreenOn(QuickshellScreenInfo* screen) { + auto* qscreen = screen != nullptr ? screen->screen : nullptr; + this->handle->fullscreenOn(qscreen); +} + +void Toplevel::setRectangle(QObject* window, QRect rect) { + auto* proxyWindow = qobject_cast(window); + + if (proxyWindow == nullptr) { + if (auto* iface = qobject_cast(window)) { + proxyWindow = iface->proxyWindow(); + } + } + + if (proxyWindow != this->rectWindow) { + if (this->rectWindow != nullptr) { + QObject::disconnect(this->rectWindow, nullptr, this, nullptr); + } + + this->rectWindow = proxyWindow; + + if (proxyWindow != nullptr) { + QObject::connect( + proxyWindow, + &QObject::destroyed, + this, + &Toplevel::onRectangleProxyDestroyed + ); + + QObject::connect( + proxyWindow, + &ProxyWindowBase::windowConnected, + this, + &Toplevel::onRectangleProxyConnected + ); + } + } + + this->rectangle = rect; + this->handle->setRectangle(proxyWindow->backingWindow(), rect); +} + +void Toplevel::unsetRectangle() { this->setRectangle(nullptr, QRect()); } + +void Toplevel::onRectangleProxyConnected() { + this->handle->setRectangle(this->rectWindow->backingWindow(), this->rectangle); +} + +void Toplevel::onRectangleProxyDestroyed() { + this->rectWindow = nullptr; + this->rectangle = QRect(); +} + +ToplevelManager::ToplevelManager() { + auto* manager = impl::ToplevelManager::instance(); + + QObject::connect( + manager, + &impl::ToplevelManager::toplevelReady, + this, + &ToplevelManager::onToplevelReady + ); + + for (auto* handle: manager->readyToplevels()) { + this->onToplevelReady(handle); + } +} + +Toplevel* ToplevelManager::forImpl(impl::ToplevelHandle* impl) const { + if (impl == nullptr) return nullptr; + + for (auto* toplevel: this->mToplevels.valueList()) { + if (toplevel->handle == impl) return toplevel; + } + + return nullptr; +} + +ObjectModel* ToplevelManager::toplevels() { return &this->mToplevels; } + +void ToplevelManager::onToplevelReady(impl::ToplevelHandle* handle) { + auto* toplevel = new Toplevel(handle, this); + QObject::connect(toplevel, &Toplevel::closed, this, &ToplevelManager::onToplevelClosed); + this->mToplevels.insertObject(toplevel); +} + +void ToplevelManager::onToplevelClosed() { + auto* toplevel = qobject_cast(this->sender()); + this->mToplevels.removeObject(toplevel); +} + +ToplevelManager* ToplevelManager::instance() { + static auto* instance = new ToplevelManager(); // NOLINT + return instance; +} + +ObjectModel* ToplevelManagerQml::toplevels() { + return ToplevelManager::instance()->toplevels(); +} + +} // namespace qs::wayland::toplevel_management diff --git a/src/wayland/toplevel_management/qml.hpp b/src/wayland/toplevel_management/qml.hpp new file mode 100644 index 00000000..8bb1d551 --- /dev/null +++ b/src/wayland/toplevel_management/qml.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#include + +#include "../../core/model.hpp" +#include "../../core/proxywindow.hpp" +#include "../../core/qmlscreen.hpp" + +namespace qs::wayland::toplevel_management { + +namespace impl { +class ToplevelManager; +class ToplevelHandle; +} // namespace impl + +///! Window from another application. +/// A window/toplevel from another application, retrievable from +/// the [ToplevelManager](../toplevelmanager). +class Toplevel: public QObject { + Q_OBJECT; + Q_PROPERTY(QString appId READ appId NOTIFY appIdChanged); + Q_PROPERTY(QString title READ title NOTIFY titleChanged); + /// Parent toplevel if this toplevel is a modal/dialog, otherwise null. + Q_PROPERTY(Toplevel* parent READ parent NOTIFY parentChanged); + /// If the window is currently activated or focused. + /// + /// Activation can be requested with the `activate()` function. + Q_PROPERTY(bool activated READ activated NOTIFY activatedChanged); + /// If the window is currently maximized. + /// + /// Maximization can be requested by setting this property, though it may + /// be ignored by the compositor. + Q_PROPERTY(bool maximized READ maximized WRITE setMaximized NOTIFY maximizedChanged); + /// If the window is currently minimized. + /// + /// Minimization can be requested by setting this property, though it may + /// be ignored by the compositor. + Q_PROPERTY(bool minimized READ minimized WRITE setMinimized NOTIFY minimizedChanged); + /// If the window is currently fullscreen. + /// + /// Fullscreen can be requested by setting this property, though it may + /// be ignored by the compositor. + /// Fullscreen can be requested on a specific screen with the `fullscreenOn()` function. + Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged); + QML_ELEMENT; + QML_UNCREATABLE("Toplevels must be acquired from the ToplevelManager."); + +public: + explicit Toplevel(impl::ToplevelHandle* handle, QObject* parent); + + /// Request that this toplevel is activated. + /// The request may be ignored by the compositor. + Q_INVOKABLE void activate(); + + /// Request that this toplevel is fullscreened on a specific screen. + /// The request may be ignored by the compositor. + Q_INVOKABLE void fullscreenOn(QuickshellScreenInfo* screen); + + /// Provide a hint to the compositor where the visual representation + /// of this toplevel is relative to a quickshell window. + /// This hint can be used visually in operations like minimization. + Q_INVOKABLE void setRectangle(QObject* window, QRect rect); + Q_INVOKABLE void unsetRectangle(); + + [[nodiscard]] QString appId() const; + [[nodiscard]] QString title() const; + [[nodiscard]] Toplevel* parent() const; + [[nodiscard]] bool activated() const; + + [[nodiscard]] bool maximized() const; + void setMaximized(bool maximized); + + [[nodiscard]] bool minimized() const; + void setMinimized(bool minimized); + + [[nodiscard]] bool fullscreen() const; + void setFullscreen(bool fullscreen); + +signals: + void closed(); + void appIdChanged(); + void titleChanged(); + void parentChanged(); + void activatedChanged(); + void maximizedChanged(); + void minimizedChanged(); + void fullscreenChanged(); + +private slots: + void onClosed(); + void onRectangleProxyConnected(); + void onRectangleProxyDestroyed(); + +private: + impl::ToplevelHandle* handle; + ProxyWindowBase* rectWindow = nullptr; + QRect rectangle; + + friend class ToplevelManager; +}; + +class ToplevelManager: public QObject { + Q_OBJECT; + +public: + Toplevel* forImpl(impl::ToplevelHandle* impl) const; + + [[nodiscard]] ObjectModel* toplevels(); + + static ToplevelManager* instance(); + +private slots: + void onToplevelReady(impl::ToplevelHandle* handle); + void onToplevelClosed(); + +private: + explicit ToplevelManager(); + + ObjectModel mToplevels {this}; +}; + +///! Exposes a list of Toplevels. +/// Exposes a list of windows from other applications as [Toplevel](../toplevel)s via the +/// [zwlr-foreign-toplevel-management-v1](https://wayland.app/protocols/wlr-foreign-toplevel-management-unstable-v1) +/// wayland protocol. +class ToplevelManagerQml: public QObject { + Q_OBJECT; + Q_PROPERTY(ObjectModel* toplevels READ toplevels CONSTANT); + QML_NAMED_ELEMENT(ToplevelManager); + QML_SINGLETON; + +public: + explicit ToplevelManagerQml(QObject* parent = nullptr): QObject(parent) {} + + [[nodiscard]] static ObjectModel* toplevels(); +}; + +} // namespace qs::wayland::toplevel_management diff --git a/src/wayland/toplevel_management/wlr-foreign-toplevel-management-unstable-v1.xml b/src/wayland/toplevel_management/wlr-foreign-toplevel-management-unstable-v1.xml new file mode 100644 index 00000000..44505bbb --- /dev/null +++ b/src/wayland/toplevel_management/wlr-foreign-toplevel-management-unstable-v1.xml @@ -0,0 +1,270 @@ + + + + Copyright © 2018 Ilia Bozhinov + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + The purpose of this protocol is to enable the creation of taskbars + and docks by providing them with a list of opened applications and + letting them request certain actions on them, like maximizing, etc. + + After a client binds the zwlr_foreign_toplevel_manager_v1, each opened + toplevel window will be sent via the toplevel event + + + + + This event is emitted whenever a new toplevel window is created. It + is emitted for all toplevels, regardless of the app that has created + them. + + All initial details of the toplevel(title, app_id, states, etc.) will + be sent immediately after this event via the corresponding events in + zwlr_foreign_toplevel_handle_v1. + + + + + + + Indicates the client no longer wishes to receive events for new toplevels. + However the compositor may emit further toplevel_created events, until + the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + This event indicates that the compositor is done sending events to the + zwlr_foreign_toplevel_manager_v1. The server will destroy the object + immediately after sending this request, so it will become invalid and + the client should free any resources associated with it. + + + + + + + A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel + window. Each app may have multiple opened toplevels. + + Each toplevel has a list of outputs it is visible on, conveyed to the + client with the output_enter and output_leave events. + + + + + This event is emitted whenever the title of the toplevel changes. + + + + + + + This event is emitted whenever the app-id of the toplevel changes. + + + + + + + This event is emitted whenever the toplevel becomes visible on + the given output. A toplevel may be visible on multiple outputs. + + + + + + + This event is emitted whenever the toplevel stops being visible on + the given output. It is guaranteed that an entered-output event + with the same output has been emitted before this event. + + + + + + + Requests that the toplevel be maximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unmaximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be minimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unminimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Request that this toplevel be activated on the given seat. + There is no guarantee the toplevel will be actually activated. + + + + + + + The different states that a toplevel can have. These have the same meaning + as the states with the same names defined in xdg-toplevel + + + + + + + + + + + This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 + is created and each time the toplevel state changes, either because of a + compositor action or because of a request in this protocol. + + + + + + + + This event is sent after all changes in the toplevel state have been + sent. + + This allows changes to the zwlr_foreign_toplevel_handle_v1 properties + to be seen as atomic, even if they happen via multiple events. + + + + + + Send a request to the toplevel to close itself. The compositor would + typically use a shell-specific method to carry out this request, for + example by sending the xdg_toplevel.close event. However, this gives + no guarantees the toplevel will actually be destroyed. If and when + this happens, the zwlr_foreign_toplevel_handle_v1.closed event will + be emitted. + + + + + + The rectangle of the surface specified in this request corresponds to + the place where the app using this protocol represents the given toplevel. + It can be used by the compositor as a hint for some operations, e.g + minimizing. The client is however not required to set this, in which + case the compositor is free to decide some default value. + + If the client specifies more than one rectangle, only the last one is + considered. + + The dimensions are given in surface-local coordinates. + Setting width=height=0 removes the already-set rectangle. + + + + + + + + + + + + + + + + This event means the toplevel has been destroyed. It is guaranteed there + won't be any more events for this zwlr_foreign_toplevel_handle_v1. The + toplevel itself becomes inert so any requests will be ignored except the + destroy request. + + + + + + Destroys the zwlr_foreign_toplevel_handle_v1 object. + + This request should be called either when the client does not want to + use the toplevel anymore or after the closed event to finalize the + destruction of the object. + + + + + + + + Requests that the toplevel be fullscreened on the given output. If the + fullscreen state and/or the outputs the toplevel is visible on actually + change, this will be indicated by the state and output_enter/leave + events. + + The output parameter is only a hint to the compositor. Also, if output + is NULL, the compositor should decide which output the toplevel will be + fullscreened on, if at all. + + + + + + + Requests that the toplevel be unfullscreened. If the fullscreen state + actually changes, this will be indicated by the state event. + + + + + + + + This event is emitted whenever the parent of the toplevel changes. + + No event is emitted when the parent handle is destroyed by the client. + + + + + From 67783ec24c61030abff8ba5008a5bbd2822c6eca Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 9 Jun 2024 15:42:38 -0700 Subject: [PATCH 039/305] core/transformwatcher: fix crash when a or b is destroyed Usually happens during reload. --- src/core/transformwatcher.cpp | 50 +++++++++++++++++++++++++++++++++-- src/core/transformwatcher.hpp | 3 +++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/core/transformwatcher.cpp b/src/core/transformwatcher.cpp index 697dfc56..2a33bad0 100644 --- a/src/core/transformwatcher.cpp +++ b/src/core/transformwatcher.cpp @@ -82,7 +82,10 @@ void TransformWatcher::linkItem(QQuickItem* item) const { QObject::connect(item, &QQuickItem::parentChanged, this, &TransformWatcher::recalcChains); QObject::connect(item, &QQuickItem::windowChanged, this, &TransformWatcher::recalcChains); - QObject::connect(item, &QObject::destroyed, this, &TransformWatcher::recalcChains); + + if (item != this->mA && item != this->mB) { + QObject::connect(item, &QObject::destroyed, this, &TransformWatcher::itemDestroyed); + } } void TransformWatcher::linkChains() { @@ -103,6 +106,18 @@ void TransformWatcher::unlinkChains() { for (auto* item: this->childChain) { QObject::disconnect(item, nullptr, this, nullptr); } + + // relink a and b destruction notifications + if (this->mA != nullptr) { + QObject::connect(this->mA, &QObject::destroyed, this, &TransformWatcher::aDestroyed); + } + + if (this->mB != nullptr) { + QObject::connect(this->mB, &QObject::destroyed, this, &TransformWatcher::bDestroyed); + } + + this->parentChain.clear(); + this->childChain.clear(); } void TransformWatcher::recalcChains() { @@ -111,26 +126,57 @@ void TransformWatcher::recalcChains() { this->linkChains(); } +void TransformWatcher::itemDestroyed() { + auto destroyed = + this->parentChain.removeOne(this->sender()) || this->childChain.removeOne(this->sender()); + + if (destroyed) this->recalcChains(); +} + QQuickItem* TransformWatcher::a() const { return this->mA; } void TransformWatcher::setA(QQuickItem* a) { if (this->mA == a) return; + if (this->mA != nullptr) QObject::disconnect(this->mA, nullptr, this, nullptr); this->mA = a; + + if (this->mA != nullptr) { + QObject::connect(this->mA, &QObject::destroyed, this, &TransformWatcher::aDestroyed); + } + this->recalcChains(); } +void TransformWatcher::aDestroyed() { + this->mA = nullptr; + this->unlinkChains(); + emit this->aChanged(); +} + QQuickItem* TransformWatcher::b() const { return this->mB; } void TransformWatcher::setB(QQuickItem* b) { if (this->mB == b) return; + if (this->mB != nullptr) QObject::disconnect(this->mB, nullptr, this, nullptr); this->mB = b; + + if (this->mB != nullptr) { + QObject::connect(this->mB, &QObject::destroyed, this, &TransformWatcher::bDestroyed); + } + this->recalcChains(); } +void TransformWatcher::bDestroyed() { + this->mB = nullptr; + this->unlinkChains(); + emit this->bChanged(); +} + QQuickItem* TransformWatcher::commonParent() const { return this->mCommonParent; } void TransformWatcher::setCommonParent(QQuickItem* commonParent) { if (this->mCommonParent == commonParent) return; this->mCommonParent = commonParent; - this->resolveChains(); + this->recalcChains(); } diff --git a/src/core/transformwatcher.hpp b/src/core/transformwatcher.hpp index d7174e4c..64bac4a1 100644 --- a/src/core/transformwatcher.hpp +++ b/src/core/transformwatcher.hpp @@ -60,6 +60,9 @@ signals: private slots: void recalcChains(); + void itemDestroyed(); + void aDestroyed(); + void bDestroyed(); private: void resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent); From 523de78796c0f3ff3a98f737b8056589df0c7c79 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 13 Jun 2024 16:23:28 -0700 Subject: [PATCH 040/305] wayland/layershell: ensure state changes are comitted without render Previously they were not comitted and did not apply until the next rendered frame. --- src/wayland/wlr_layershell/surface.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/wayland/wlr_layershell/surface.cpp b/src/wayland/wlr_layershell/surface.cpp index 5c369f2b..695ecc48 100644 --- a/src/wayland/wlr_layershell/surface.cpp +++ b/src/wayland/wlr_layershell/surface.cpp @@ -119,24 +119,31 @@ void QSWaylandLayerSurface::setWindowGeometry(const QRect& geometry) { QWindow* QSWaylandLayerSurface::qwindow() { return this->window()->window(); } -void QSWaylandLayerSurface::updateLayer() { this->set_layer(toWaylandLayer(this->ext->mLayer)); } +void QSWaylandLayerSurface::updateLayer() { + this->set_layer(toWaylandLayer(this->ext->mLayer)); + this->window()->waylandSurface()->commit(); +} void QSWaylandLayerSurface::updateAnchors() { this->set_anchor(toWaylandAnchors(this->ext->mAnchors)); this->setWindowGeometry(this->window()->windowContentGeometry()); + this->window()->waylandSurface()->commit(); } void QSWaylandLayerSurface::updateMargins() { auto& margins = this->ext->mMargins; this->set_margin(margins.mTop, margins.mRight, margins.mBottom, margins.mLeft); + this->window()->waylandSurface()->commit(); } void QSWaylandLayerSurface::updateExclusiveZone() { this->set_exclusive_zone(this->ext->mExclusiveZone); + this->window()->waylandSurface()->commit(); } void QSWaylandLayerSurface::updateKeyboardFocus() { this->set_keyboard_interactivity(toWaylandKeyboardFocus(this->ext->mKeyboardFocus)); + this->window()->waylandSurface()->commit(); } QtWayland::zwlr_layer_shell_v1::layer toWaylandLayer(const WlrLayer::Enum& layer) noexcept { From d8b72b4c31ea471ab56895671506d710f960189f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 13 Jun 2024 16:25:07 -0700 Subject: [PATCH 041/305] wayland/lock: notify on screen change --- src/wayland/session_lock.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wayland/session_lock.cpp b/src/wayland/session_lock.cpp index ac1cf7d6..bb5ed132 100644 --- a/src/wayland/session_lock.cpp +++ b/src/wayland/session_lock.cpp @@ -257,10 +257,10 @@ void WlSessionLockSurface::setScreen(QScreen* qscreen) { QObject::connect(qscreen, &QObject::destroyed, this, &WlSessionLockSurface::onScreenDestroyed); } - if (this->window == nullptr) { - this->mScreen = qscreen; - emit this->screenChanged(); - } else this->window->setScreen(qscreen); + if (this->window == nullptr) this->mScreen = qscreen; + else this->window->setScreen(qscreen); + + emit this->screenChanged(); } void WlSessionLockSurface::onScreenDestroyed() { this->mScreen = nullptr; } From ce5ddbf8ba3ca8a0f2edc06c2923ed8346604dfa Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 14 Jun 2024 19:18:43 -0700 Subject: [PATCH 042/305] core: add $XDG_DATA_DIRS/pixmaps to QIcon fallback path Picks up some missing app icons. --- src/core/main.cpp | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/core/main.cpp b/src/core/main.cpp index 220bde30..3daf09ad 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -333,6 +334,32 @@ int qs_main(int argc, char** argv) { qputenv("QSG_USE_SIMPLE_ANIMATION_DRIVER", "1"); } + // Some programs place icons in the pixmaps folder instead of the icons folder. + // This seems to be controlled by the QPA and qt6ct does not provide it. + { + QList dataPaths; + + 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"); + } + + auto fallbackPaths = QIcon::fallbackSearchPaths(); + + for (auto& path: dataPaths) { + auto newPath = QDir(path).filePath("pixmaps"); + + if (!fallbackPaths.contains(newPath)) { + fallbackPaths.push_back(newPath); + } + } + + QIcon::setFallbackSearchPaths(fallbackPaths); + } + QGuiApplication::setDesktopSettingsAware(desktopSettingsAware); QGuiApplication* app = nullptr; From f655875547b71afdd643e158c62c2b290f7e3ee7 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 16 Jun 2024 01:58:24 -0700 Subject: [PATCH 043/305] core/desktopentry: add limited desktop entry api --- src/core/CMakeLists.txt | 1 + src/core/desktopentry.cpp | 367 ++++++++++++++++++++++++++++++++++++++ src/core/desktopentry.hpp | 151 ++++++++++++++++ src/core/module.md | 1 + 4 files changed, 520 insertions(+) create mode 100644 src/core/desktopentry.cpp create mode 100644 src/core/desktopentry.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 24d2e685..b76c7aab 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -28,6 +28,7 @@ qt_add_library(quickshell-core STATIC boundcomponent.cpp model.cpp elapsedtimer.cpp + desktopentry.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp new file mode 100644 index 00000000..a5ecef83 --- /dev/null +++ b/src/core/desktopentry.cpp @@ -0,0 +1,367 @@ +#include "desktopentry.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "model.hpp" + +Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg); + +struct Locale { + explicit Locale() = default; + + explicit Locale(const QString& string) { + auto territoryIdx = string.indexOf('_'); + auto codesetIdx = string.indexOf('.'); + auto modifierIdx = string.indexOf('@'); + + auto parseEnd = string.length(); + + if (modifierIdx != -1) { + this->modifier = string.sliced(modifierIdx + 1, parseEnd - modifierIdx - 1); + parseEnd = modifierIdx; + } + + if (codesetIdx != -1) { + parseEnd = codesetIdx; + } + + if (territoryIdx != -1) { + this->territory = string.sliced(territoryIdx + 1, parseEnd - territoryIdx - 1); + parseEnd = territoryIdx; + } + + this->language = string.sliced(0, parseEnd); + } + + [[nodiscard]] bool isValid() const { return !this->language.isEmpty(); } + + [[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; + + auto score = 1; + if (territoryMatches) score += 2; + if (modifierMatches) score += 1; + + return score; + } + + static const Locale& system() { + static Locale* locale = nullptr; // NOLINT + + if (locale == nullptr) { + auto lstr = qEnvironmentVariable("LC_MESSAGES"); + if (lstr.isEmpty()) lstr = qEnvironmentVariable("LANG"); + locale = new Locale(lstr); + } + + return *locale; + } + + QString language; + QString territory; + QString modifier; +}; + +QDebug operator<<(QDebug debug, const Locale& locale) { + auto saver = QDebugStateSaver(debug); + debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory + << ", modifier" << locale.modifier << ')'; + + return debug; +} + +void DesktopEntry::parseEntry(const QString& text) { + const auto& system = Locale::system(); + + auto groupName = QString(); + auto entries = QHash>(); + + auto finishCategory = [&]() { + if (groupName == "Desktop Entry") { + if (entries["Type"].second != "Application") return; + if (entries.contains("Hidden") && entries["Hidden"].second == "true") return; + + for (const auto& [key, pair]: entries.asKeyValueRange()) { + auto& [_, value] = pair; + this->mEntries.insert(key, value); + + if (key == "Name") this->mName = value; + else if (key == "GenericName") this->mGenericName = value; + else if (key == "NoDisplay") this->mNoDisplay = value == "true"; + else if (key == "Comment") this->mComment = value; + else if (key == "Icon") this->mIcon = value; + else if (key == "Exec") this->mExecString = value; + else if (key == "Path") this->mWorkingDirectory = value; + else if (key == "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); + } + } else if (groupName.startsWith("Desktop Action ")) { + auto actionName = groupName.sliced(16); + auto* action = new DesktopAction(actionName, this); + + for (const auto& [key, pair]: entries.asKeyValueRange()) { + const auto& [_, value] = pair; + action->mEntries.insert(key, value); + + if (key == "Name") action->mName = value; + else if (key == "Icon") action->mIcon = value; + else if (key == "Exec") action->mExecString = value; + } + + this->mActions.insert(actionName, action); + } + + entries.clear(); + }; + + for (auto& line: text.split(u'\n', Qt::SkipEmptyParts)) { + if (line.startsWith(u'#')) continue; + + if (line.startsWith(u'[') && line.endsWith(u']')) { + finishCategory(); + groupName = line.sliced(1, line.length() - 2); + continue; + } + + auto splitIdx = line.indexOf(u'='); + if (splitIdx == -1) { + qCDebug(logDesktopEntry) << "Encountered invalid line in desktop entry (no =)" << line; + continue; + } + + auto key = line.sliced(0, splitIdx); + const auto& value = line.sliced(splitIdx + 1); + + auto localeIdx = key.indexOf('['); + Locale locale; + if (localeIdx != -1 && localeIdx != key.length() - 1) { + locale = Locale(key.sliced(localeIdx + 1, key.length() - localeIdx - 2)); + key = key.sliced(0, localeIdx); + } + + if (entries.contains(key)) { + const auto& old = entries.value(key); + + if (system.matchScore(locale) > system.matchScore(old.first)) { + entries.insert(key, qMakePair(locale, value)); + } + } else { + entries.insert(key, qMakePair(locale, value)); + } + } + + finishCategory(); +} + +void DesktopEntry::execute() const { + DesktopEntry::doExec(this->mExecString, this->mWorkingDirectory); +} + +bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); } +bool DesktopEntry::noDisplay() const { return this->mNoDisplay; } + +QVector DesktopEntry::actions() const { return this->mActions.values(); } + +QVector DesktopEntry::parseExecString(const QString& execString) { + QVector arguments; + QString currentArgument; + auto parsingString = false; + auto escape = 0; + auto percent = false; + + for (auto c: execString) { + if (escape == 0 && c == u'\\') { + escape = 1; + } else if (parsingString) { + if (c == '\\') { + escape++; + if (escape == 4) { + currentArgument += '\\'; + 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. + qCWarning(logDesktopEntry).noquote() + << "Illegal escape sequence in desktop entry exec string:" << execString; + } + + currentArgument += c; + escape = 0; + } else if (c == u'"') { + parsingString = false; + } else { + currentArgument += c; + } + } else if (escape != 0) { + currentArgument += c; + escape = 0; + } else if (percent) { + if (c == '%') { + currentArgument += '%'; + } // else discard + + percent = false; + } else if (c == '%') { + percent = true; + } else if (c == u'"') { + parsingString = true; + } else if (c == u' ') { + if (!currentArgument.isEmpty()) { + arguments.push_back(currentArgument); + currentArgument.clear(); + } + } else { + currentArgument += c; + } + } + + if (!currentArgument.isEmpty()) { + arguments.push_back(currentArgument); + currentArgument.clear(); + } + + return arguments; +} + +void DesktopEntry::doExec(const QString& execString, const QString& workingDirectory) { + auto args = DesktopEntry::parseExecString(execString); + if (args.isEmpty()) { + qCWarning(logDesktopEntry) << "Tried to exec string" << execString << "which parsed as empty."; + return; + } + + auto process = QProcess(); + process.setProgram(args.at(0)); + process.setArguments(args.sliced(1)); + if (!workingDirectory.isEmpty()) process.setWorkingDirectory(workingDirectory); + process.startDetached(); +} + +void DesktopAction::execute() const { + DesktopEntry::doExec(this->mExecString, this->entry->mWorkingDirectory); +} + +DesktopEntryManager::DesktopEntryManager() { + this->scanDesktopEntries(); + this->populateApplications(); +} + +void DesktopEntryManager::scanDesktopEntries() { + QList dataPaths; + + 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); + } +} + +void DesktopEntryManager::populateApplications() { + for (auto& entry: this->desktopEntries.values()) { + if (!entry->noDisplay()) this->mApplications.insertObject(entry); + } +} + +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.path(), prefix + dir.dirName() + "-"); + else if (entry.isFile()) { + auto path = entry.filePath(); + if (!path.endsWith(".desktop")) { + qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension"; + continue; + } + + auto* file = new QFile(path); + + if (!file->open(QFile::ReadOnly)) { + qCDebug(logDesktopEntry) << "Could not open file" << path; + continue; + } + + auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8); + + 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; + + if (desktopEntries.contains(id)) { + qCDebug(logDesktopEntry) << "Replacing old entry for" << id; + delete desktopEntries.value(id); + desktopEntries.remove(id); + } + + desktopEntries.insert(id, dentry); + } + } +} + +DesktopEntryManager* DesktopEntryManager::instance() { + static auto* instance = new DesktopEntryManager(); // NOLINT + return instance; +} + +DesktopEntry* DesktopEntryManager::byId(const QString& id) { + return this->desktopEntries.value(id); +} + +ObjectModel* DesktopEntryManager::applications() { return &this->mApplications; } + +DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); } + +DesktopEntry* DesktopEntries::byId(const QString& id) { + return DesktopEntryManager::instance()->byId(id); +} + +ObjectModel* DesktopEntries::applications() { + return DesktopEntryManager::instance()->applications(); +} diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp new file mode 100644 index 00000000..b9399d41 --- /dev/null +++ b/src/core/desktopentry.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "model.hpp" + +class DesktopAction; + +/// A desktop entry. See [DesktopEntries](../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); + /// Short description of the application, such as "Web Browser". May be empty. + Q_PROPERTY(QString genericName MEMBER mGenericName 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. + Q_PROPERTY(QString comment MEMBER mComment CONSTANT); + /// Name of the icon associated with this application. May be empty. + Q_PROPERTY(QString icon MEMBER mIcon CONSTANT); + /// The raw `Exec` string from the desktop entry. You probably want `execute()`. + Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); + /// The working directory to execute from. + Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT); + /// 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(QVector actions READ actions CONSTANT); + QML_ELEMENT; + QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries"); + +public: + explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {} + + void parseEntry(const QString& text); + + /// Run the application. Currently ignores `runInTerminal` and field codes. + Q_INVOKABLE void execute() const; + + [[nodiscard]] bool isValid() const; + [[nodiscard]] bool noDisplay() const; + [[nodiscard]] QVector actions() const; + + // currently ignores all field codes. + static QVector parseExecString(const QString& execString); + static void doExec(const QString& execString, const QString& workingDirectory); + +private: + QHash mEntries; + QHash mActions; + + QString mId; + QString mName; + QString mGenericName; + bool mNoDisplay = false; + QString mComment; + QString mIcon; + QString mExecString; + QString mWorkingDirectory; + bool mTerminal = false; + QVector mCategories; + QVector mKeywords; + + friend class DesktopAction; +}; + +/// An action of a [DesktopEntry](../desktopentry). +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); + /// The raw `Exec` string from the desktop entry. You probably want `execute()`. + Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); + QML_ELEMENT; + QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry"); + +public: + explicit DesktopAction(QString id, DesktopEntry* entry) + : QObject(entry) + , entry(entry) + , mId(std::move(id)) {} + + /// Run the application. Currently ignores `runInTerminal` and field codes. + Q_INVOKABLE void execute() const; + +private: + DesktopEntry* entry; + QString mId; + QString mName; + QString mIcon; + QString mExecString; + QHash mEntries; + + friend class DesktopEntry; +}; + +class DesktopEntryManager: public QObject { + Q_OBJECT; + +public: + void scanDesktopEntries(); + + [[nodiscard]] DesktopEntry* byId(const QString& id); + + [[nodiscard]] ObjectModel* applications(); + + static DesktopEntryManager* instance(); + +private: + explicit DesktopEntryManager(); + + void populateApplications(); + void scanPath(const QDir& dir, const QString& prefix = QString()); + + QHash desktopEntries; + ObjectModel mApplications {this}; +}; + +///! Desktop entry index. +/// Index of desktop entries according to the [desktop entry specification]. +/// +/// Primarily useful for looking up icons and metadata from an id, as there is +/// currently no mechanism for usage based sorting of entries and other launcher niceties. +/// +/// [desktop entry specification]: https://specifications.freedesktop.org/desktop-entry-spec/latest/ +class DesktopEntries: public QObject { + Q_OBJECT; + /// All desktop entries of type Application that are not Hidden or NoDisplay. + Q_PROPERTY(ObjectModel* applications READ applications CONSTANT); + QML_ELEMENT; + QML_SINGLETON; + +public: + explicit DesktopEntries(); + + /// Look up a desktop entry by name. Includes NoDisplay entries. May return null. + Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id); + + [[nodiscard]] static ObjectModel* applications(); +}; diff --git a/src/core/module.md b/src/core/module.md index 13218610..315eb25f 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -20,5 +20,6 @@ headers = [ "boundcomponent.hpp", "model.hpp", "elapsedtimer.hpp", + "desktopentry.hpp", ] ----- From 7e5d128a91a2c6f21639d3ff4afeb1ee639e2003 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 17 Jun 2024 18:32:13 -0700 Subject: [PATCH 044/305] service/pam: add pam service --- .clang-tidy | 1 + CMakeLists.txt | 2 + README.md | 1 + default.nix | 4 + src/services/CMakeLists.txt | 4 + src/services/pam/CMakeLists.txt | 17 +++ src/services/pam/conversation.cpp | 185 +++++++++++++++++++++++ src/services/pam/conversation.hpp | 95 ++++++++++++ src/services/pam/module.md | 67 +++++++++ src/services/pam/qml.cpp | 238 ++++++++++++++++++++++++++++++ src/services/pam/qml.hpp | 126 ++++++++++++++++ 11 files changed, 740 insertions(+) create mode 100644 src/services/pam/CMakeLists.txt create mode 100644 src/services/pam/conversation.cpp create mode 100644 src/services/pam/conversation.hpp create mode 100644 src/services/pam/module.md create mode 100644 src/services/pam/qml.cpp create mode 100644 src/services/pam/qml.hpp diff --git a/.clang-tidy b/.clang-tidy index 1da445cd..41de1fd7 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -14,6 +14,7 @@ Checks: > -cppcoreguidelines-avoid-const-or-ref-data-members, -cppcoreguidelines-non-private-member-variables-in-classes, -cppcoreguidelines-avoid-goto, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, google-build-using-namespace. google-explicit-constructor, google-global-names-in-headers, diff --git a/CMakeLists.txt b/CMakeLists.txt index 7af6b6cf..606256bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON) option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) option(SERVICE_PIPEWIRE "PipeWire service" ON) option(SERVICE_MPRIS "Mpris service" ON) +option(SERVICE_PAM "Pam service" ON) message(STATUS "Quickshell configuration") message(STATUS " Jemalloc: ${USE_JEMALLOC}") @@ -39,6 +40,7 @@ message(STATUS " Services") message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") message(STATUS " Mpris: ${SERVICE_MPRIS}") +message(STATUS " Pam: ${SERVICE_PAM}") message(STATUS " Hyprland: ${HYPRLAND}") if (HYPRLAND) message(STATUS " IPC: ${HYPRLAND_IPC}") diff --git a/README.md b/README.md index 4def09ed..bf494c3c 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ quickshell.packages..default.override { withWayland = true; withX11 = true; withPipewire = true; + withPam = true; withHyprland = true; } ``` diff --git a/default.nix b/default.nix index 01624c4a..e77109f7 100644 --- a/default.nix +++ b/default.nix @@ -13,6 +13,7 @@ wayland-protocols, xorg, pipewire, + pam, gitRev ? (let headExists = builtins.pathExists ./.git/HEAD; @@ -31,6 +32,7 @@ withWayland ? true, withX11 ? true, withPipewire ? true, + withPam ? true, withHyprland ? true, }: buildStdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; @@ -55,6 +57,7 @@ ++ (lib.optional withQtSvg qt6.qtsvg) ++ (lib.optionals withWayland [ qt6.qtwayland wayland ]) ++ (lib.optional withX11 xorg.libxcb) + ++ (lib.optional withPam pam) ++ (lib.optional withPipewire pipewire); QTWAYLANDSCANNER = lib.optionalString withWayland "${qt6.qtwayland}/libexec/qtwaylandscanner"; @@ -74,6 +77,7 @@ ++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF" ++ lib.optional (!withWayland) "-DWAYLAND=OFF" ++ lib.optional (!withPipewire) "-DSERVICE_PIPEWIRE=OFF" + ++ lib.optional (!withPam) "-DSERVICE_PAM=OFF" ++ lib.optional (!withHyprland) "-DHYPRLAND=OFF"; buildPhase = "ninjaBuildPhase"; diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt index 4915762c..e8c05f4c 100644 --- a/src/services/CMakeLists.txt +++ b/src/services/CMakeLists.txt @@ -9,3 +9,7 @@ endif() if (SERVICE_MPRIS) add_subdirectory(mpris) endif() + +if (SERVICE_PAM) + add_subdirectory(pam) +endif() diff --git a/src/services/pam/CMakeLists.txt b/src/services/pam/CMakeLists.txt new file mode 100644 index 00000000..1a7b29b8 --- /dev/null +++ b/src/services/pam/CMakeLists.txt @@ -0,0 +1,17 @@ +#find_package(PAM REQUIRED) + +qt_add_library(quickshell-service-pam STATIC + qml.cpp + conversation.cpp +) +qt_add_qml_module(quickshell-service-pam + URI Quickshell.Services.Pam + VERSION 0.1 +) + +target_link_libraries(quickshell-service-pam PRIVATE ${QT_DEPS} pam ${PAM_LIBRARIES}) + +qs_pch(quickshell-service-pam) +qs_pch(quickshell-service-pamplugin) + +target_link_libraries(quickshell PRIVATE quickshell-service-pamplugin) diff --git a/src/services/pam/conversation.cpp b/src/services/pam/conversation.cpp new file mode 100644 index 00000000..d73c4264 --- /dev/null +++ b/src/services/pam/conversation.cpp @@ -0,0 +1,185 @@ +#include "conversation.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(logPam, "quickshell.service.pam", QtWarningMsg); + +QString PamError::toString(PamError::Enum value) { + switch (value) { + case ConnectionFailed: return "Failed to connect to pam"; + case TryAuthFailed: return "Failed to try authenticating"; + default: return "Invalid error"; + } +} + +QString PamResult::toString(PamResult::Enum value) { + switch (value) { + case Success: return "Success"; + case Failed: return "Failed"; + case Error: return "Error occurred while authenticating"; + case MaxTries: return "The authentication method has no more attempts available"; + // case Expired: return "The account has expired"; + // case PermissionDenied: return "Permission denied"; + default: return "Invalid result"; + } +} + +void PamConversation::run() { + auto conv = pam_conv { + .conv = &PamConversation::conversation, + .appdata_ptr = this, + }; + + pam_handle_t* handle = nullptr; + + qCInfo(logPam) << this << "Starting pam session for user" << this->user << "with config" + << this->config << "in configdir" << this->configDir; + + auto result = pam_start_confdir( + this->config.toStdString().c_str(), + this->user.toStdString().c_str(), + &conv, + this->configDir.toStdString().c_str(), + &handle + ); + + if (result != PAM_SUCCESS) { + qCCritical(logPam) << this << "Unable to start pam conversation with error" + << QString(pam_strerror(handle, result)); + emit this->error(PamError::ConnectionFailed); + this->deleteLater(); + return; + } + + result = pam_authenticate(handle, 0); + + // Seems to require root and quickshell should not run as root. + // if (result == PAM_SUCCESS) { + // result = pam_acct_mgmt(handle, 0); + // } + + switch (result) { + case PAM_SUCCESS: + qCInfo(logPam) << this << "ended with successful authentication."; + emit this->completed(PamResult::Success); + break; + case PAM_AUTH_ERR: + qCInfo(logPam) << this << "ended with failed authentication."; + emit this->completed(PamResult::Failed); + break; + case PAM_MAXTRIES: + qCInfo(logPam) << this << "ended with failure: max tries."; + emit this->completed(PamResult::MaxTries); + break; + /*case PAM_ACCT_EXPIRED: + qCInfo(logPam) << this << "ended with failure: account expiration."; + emit this->completed(PamResult::Expired); + break; + case PAM_PERM_DENIED: + qCInfo(logPam) << this << "ended with failure: permission denied."; + emit this->completed(PamResult::PermissionDenied); + break;*/ + default: + qCCritical(logPam) << this << "ended with error:" << QString(pam_strerror(handle, result)); + emit this->error(PamError::TryAuthFailed); + break; + } + + result = pam_end(handle, result); + if (result != PAM_SUCCESS) { + qCCritical(logPam) << this << "Failed to end pam conversation with error code" + << QString(pam_strerror(handle, result)); + } + + this->deleteLater(); +} + +void PamConversation::abort() { + qCDebug(logPam) << "Abort requested for" << this; + auto locker = QMutexLocker(&this->wakeMutex); + this->mAbort = true; + this->waker.wakeOne(); +} + +void PamConversation::respond(QString response) { + qCDebug(logPam) << "Set response for" << this; + auto locker = QMutexLocker(&this->wakeMutex); + this->response = std::move(response); + this->hasResponse = true; + this->waker.wakeOne(); +} + +int PamConversation::conversation( + int msgCount, + const pam_message** msgArray, + pam_response** responseArray, + void* appdata +) { + auto* delegate = static_cast(appdata); + + { + auto locker = QMutexLocker(&delegate->wakeMutex); + if (delegate->mAbort) { + return PAM_ERROR_MSG; + } + } + + // freed by libc so must be alloc'd by it. + auto* responses = static_cast(calloc(msgCount, sizeof(pam_response))); // NOLINT + + for (auto i = 0; i < msgCount; i++) { + const auto* message = msgArray[i]; // NOLINT + auto& response = responses[i]; // NOLINT + + auto msgString = QString(message->msg); + auto messageChanged = true; // message->msg_style != PAM_PROMPT_ECHO_OFF; + auto isError = message->msg_style == PAM_ERROR_MSG; + auto responseRequired = + message->msg_style == PAM_PROMPT_ECHO_OFF || message->msg_style == PAM_PROMPT_ECHO_ON; + + qCDebug(logPam) << delegate << "got new message message:" << msgString + << "messageChanged:" << messageChanged << "isError:" << isError + << "responseRequired" << responseRequired; + + delegate->hasResponse = false; + emit delegate->message(msgString, messageChanged, isError, responseRequired); + + { + auto locker = QMutexLocker(&delegate->wakeMutex); + + if (delegate->mAbort) { + free(responses); // NOLINT + return PAM_ERROR_MSG; + } + + if (responseRequired) { + if (!delegate->hasResponse) { + delegate->waker.wait(locker.mutex()); + + if (delegate->mAbort) { + free(responses); // NOLINT + return PAM_ERROR_MSG; + } + } + + if (!delegate->hasResponse) { + qCCritical(logPam + ) << "Pam conversation requires response and does not have one. This should not happen."; + } + + response.resp = strdup(delegate->response.toStdString().c_str()); // NOLINT (include error) + } + } + } + + *responseArray = responses; + return PAM_SUCCESS; +} diff --git a/src/services/pam/conversation.hpp b/src/services/pam/conversation.hpp new file mode 100644 index 00000000..ff589806 --- /dev/null +++ b/src/services/pam/conversation.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +/// The result of an authentication. +class PamResult: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum { + /// Authentication was successful. + Success = 0, + /// Authentication failed. + Failed = 1, + /// An error occurred while trying to authenticate. + Error = 2, + /// The authentication method ran out of tries and should not be used again. + MaxTries = 3, + // The account has expired. + // Expired 4, + // Permission denied. + // PermissionDenied 5, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(PamResult::Enum value); +}; + +/// An error that occurred during an authentication. +class PamError: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum { + /// Failed to initiate the pam connection. + ConnectionFailed = 1, + /// Failed to try to authenticate the user. + /// This is not the same as the user failing to authenticate. + TryAuthFailed = 2, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(PamError::Enum value); +}; + +class PamConversation: public QThread { + Q_OBJECT; + +public: + explicit PamConversation(QString config, QString configDir, QString user) + : config(std::move(config)) + , configDir(std::move(configDir)) + , user(std::move(user)) {} + +public: + void run() override; + + void abort(); + void respond(QString response); + +signals: + void completed(PamResult::Enum result); + void error(PamError::Enum error); + void message(QString message, bool messageChanged, bool isError, bool responseRequired); + +private: + static int conversation( + int msgCount, + const pam_message** msgArray, + pam_response** responseArray, + void* appdata + ); + + QString config; + QString configDir; + QString user; + + QMutex wakeMutex; + QWaitCondition waker; + bool mAbort = false; + bool hasResponse = false; + QString response; +}; diff --git a/src/services/pam/module.md b/src/services/pam/module.md new file mode 100644 index 00000000..2f99400f --- /dev/null +++ b/src/services/pam/module.md @@ -0,0 +1,67 @@ +name = "Quickshell.Services.Pam" +description = "Pam authentication" +headers = [ + "qml.hpp", + "conversation.hpp", +] +----- + +## Writing pam configurations + +It is a good idea to write pam configurations specifically for quickshell +if you want to do anything other than match the default login flow. + +A good example of this is having a configuration that allows entering a password +or fingerprint in any order. + +### Structure of a pam configuration. +Pam configuration files are a list of rules, each on a new line in the following form: +``` + [options] +``` + +Each line runs in order. + +PamContext currently only works with the `auth` type, as other types require root +access to check. + +#### Control flags +The control flags you're likely to use are `required` and `sufficient`. +- `required` rules must pass for authentication to succeed. +- `sufficient` rules will bypass any remaining rules and return on success. + +Note that you should have at least one required rule or pam will fail with an undocumented error. + +#### Modules +Pam works with a set of modules that handle various authentication mechanisms. +Some common ones include `pam_unix.so` which handles passwords and `pam_fprintd.so` +which handles fingerprints. + +These modules have options but none are required for basic functionality. + +### Examples + +Authenticate with only a password: +``` +auth required pam_unix.so +``` + +Authenticate with only a fingerprint: +``` +auth required pam_fprintd.so +``` + +Try to authenticate with a fingerprint first, but if that fails fall back to a password: +``` +auth sufficient pam_fprintd.so +auth required pam_unix.so +``` + +Require both a fingerprint and a password: +``` +auth required pam_fprintd.so +auth required pam_unix.so +``` + + +See also: [Oracle: PAM configuration file](https://docs.oracle.com/cd/E19683-01/816-4883/pam-32/index.html) diff --git a/src/services/pam/qml.cpp b/src/services/pam/qml.cpp new file mode 100644 index 00000000..be34410a --- /dev/null +++ b/src/services/pam/qml.cpp @@ -0,0 +1,238 @@ +#include "qml.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "conversation.hpp" + +PamContext::~PamContext() { + if (this->conversation != nullptr && this->conversation->isRunning()) { + this->conversation->abort(); + } +} + +void PamContext::componentComplete() { + this->postInit = true; + + if (this->mTargetActive) { + this->startConversation(); + } +} + +void PamContext::startConversation() { + if (!this->postInit || this->conversation != nullptr) return; + + QString user; + + { + auto configDirInfo = QFileInfo(this->mConfigDirectory); + if (!configDirInfo.isDir()) { + qCritical() << "Cannot start" << this << "because specified config directory" + << this->mConfigDirectory << "is not a directory."; + this->mTargetActive = false; + return; + } + + auto configFilePath = QDir(this->mConfigDirectory).filePath(this->mConfig); + auto configFileInfo = QFileInfo(configFilePath); + if (!configFileInfo.isFile()) { + qCritical() << "Cannot start" << this << "because specified config file" << configFilePath + << "is not a file."; + this->mTargetActive = false; + return; + } + + auto pwuidbufSize = sysconf(_SC_GETPW_R_SIZE_MAX); + if (pwuidbufSize == -1) pwuidbufSize = 8192; + char pwuidbuf[pwuidbufSize]; // NOLINT + + passwd pwuid {}; + passwd* pwuidResult = nullptr; + + if (this->mUser.isEmpty()) { + auto r = getpwuid_r(getuid(), &pwuid, pwuidbuf, pwuidbufSize, &pwuidResult); + if (pwuidResult == nullptr) { + qCritical() << "Cannot start" << this << "due to error in getpwuid_r: " << r; + this->mTargetActive = false; + return; + } + + user = pwuid.pw_name; + } else { + auto r = getpwnam_r( + this->mUser.toStdString().c_str(), + &pwuid, + pwuidbuf, + pwuidbufSize, + &pwuidResult + ); + + if (pwuidResult == nullptr) { + if (r == 0) { + qCritical() << "Cannot start" << this + << "because specified user was not found: " << this->mUser; + } else { + qCritical() << "Cannot start" << this << "due to error in getpwnam_r: " << r; + } + + this->mTargetActive = false; + return; + } + + user = pwuid.pw_name; + } + } + + this->conversation = new PamConversation(this->mConfig, this->mConfigDirectory, user); + QObject::connect(this->conversation, &PamConversation::completed, this, &PamContext::onCompleted); + QObject::connect(this->conversation, &PamConversation::error, this, &PamContext::onError); + QObject::connect(this->conversation, &PamConversation::message, this, &PamContext::onMessage); + emit this->activeChanged(); + this->conversation->start(); +} + +void PamContext::abortConversation() { + if (this->conversation == nullptr) return; + this->mTargetActive = false; + + QObject::disconnect(this->conversation, nullptr, this, nullptr); + if (this->conversation->isRunning()) this->conversation->abort(); + this->conversation = nullptr; + emit this->activeChanged(); + + if (!this->mMessage.isEmpty()) { + this->mMessage.clear(); + emit this->messageChanged(); + } + + if (this->mMessageIsError) { + this->mMessageIsError = false; + emit this->messageIsErrorChanged(); + } + + if (this->mIsResponseRequired) { + this->mIsResponseRequired = false; + emit this->responseRequiredChanged(); + } +} + +void PamContext::respond(QString response) { + if (this->isActive() && this->mIsResponseRequired) { + this->conversation->respond(std::move(response)); + } else { + qWarning() << "PamContext response was ignored as this context does not require one."; + } +} + +bool PamContext::start() { + this->setActive(true); + return this->isActive(); +} + +void PamContext::abort() { this->setActive(false); } + +bool PamContext::isActive() const { return this->conversation != nullptr; } + +void PamContext::setActive(bool active) { + if (active == this->mTargetActive) return; + this->mTargetActive = active; + + if (active) this->startConversation(); + else this->abortConversation(); +} + +QString PamContext::config() const { return this->mConfig; } + +void PamContext::setConfig(QString config) { + if (config == this->mConfig) return; + + if (this->isActive()) { + qCritical() << "Cannot set config on PamContext while it is active."; + return; + } + + this->mConfig = std::move(config); + emit this->configChanged(); +} + +QString PamContext::configDirectory() const { return this->mConfigDirectory; } + +void PamContext::setConfigDirectory(QString configDirectory) { + if (configDirectory == this->mConfigDirectory) return; + + if (this->isActive()) { + qCritical() << "Cannot set configDirectory on PamContext while it is active."; + return; + } + + auto* context = QQmlEngine::contextForObject(this); + if (context != nullptr) { + configDirectory = context->resolvedUrl(configDirectory).path(); + } + + this->mConfigDirectory = std::move(configDirectory); + emit this->configDirectoryChanged(); +} + +QString PamContext::user() const { return this->mUser; } + +void PamContext::setUser(QString user) { + if (user == this->mUser) return; + + if (this->isActive()) { + qCritical() << "Cannot set user on PamContext while it is active."; + return; + } + + this->mUser = std::move(user); + emit this->userChanged(); +} + +QString PamContext::message() const { return this->mMessage; } +bool PamContext::messageIsError() const { return this->mMessageIsError; } +bool PamContext::isResponseRequired() const { return this->mIsResponseRequired; } + +void PamContext::onCompleted(PamResult::Enum result) { + emit this->completed(result); + this->abortConversation(); +} + +void PamContext::onError(PamError::Enum error) { + emit this->error(error); + emit this->completed(PamResult::Error); + this->abortConversation(); +} + +void PamContext::onMessage( + QString message, + bool messageChanged, + bool isError, + bool responseRequired +) { + if (messageChanged) { + if (message != this->mMessage) { + this->mMessage = std::move(message); + emit this->messageChanged(); + } + + if (isError != this->mMessageIsError) { + this->mMessageIsError = isError; + emit this->messageIsErrorChanged(); + } + } + + if (responseRequired != this->mIsResponseRequired) { + this->mIsResponseRequired = responseRequired; + emit this->responseRequiredChanged(); + } + + emit this->pamMessage(); +} diff --git a/src/services/pam/qml.hpp b/src/services/pam/qml.hpp new file mode 100644 index 00000000..6f9df4d6 --- /dev/null +++ b/src/services/pam/qml.hpp @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "conversation.hpp" + +///! Connection to pam. +/// Connection to pam. See [the module documentation](../) for pam configuration advice. +class PamContext + : public QObject + , public QQmlParserStatus { + Q_OBJECT; + // clang-format off + /// If the pam context is actively performing an authentication. + /// + /// Setting this value behaves exactly the same as calling `start()` and `abort()`. + Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged); + /// The pam configuration to use. Defaults to "login". + /// + /// The configuration should name a file inside `configDirectory`. + /// + /// This property may not be set while `active` is true. + Q_PROPERTY(QString config READ config WRITE setConfig NOTIFY configChanged); + /// The pam configuration directory to use. Defaults to "/etc/pam.d". + /// + /// The configuration directory is resolved relative to the current file if not an absolute path. + /// + /// 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. + /// + /// This property may not be set while `active` is true. + Q_PROPERTY(QString user READ user WRITE setUser NOTIFY userChanged); + /// The last message sent by pam. + Q_PROPERTY(QString message READ message NOTIFY messageChanged); + /// If the last message should be shown as an error. + Q_PROPERTY(bool messageIsError READ messageIsError NOTIFY messageIsErrorChanged); + /// If pam currently wants a response. + /// + /// Responses can be returned with the `respond()` function. + Q_PROPERTY(bool responseRequired READ isResponseRequired NOTIFY responseRequiredChanged); + // clang-format on + QML_ELEMENT; + +public: + explicit PamContext(QObject* parent = nullptr): QObject(parent) {} + ~PamContext() override; + Q_DISABLE_COPY_MOVE(PamContext); + + void classBegin() override {} + void componentComplete() override; + + void startConversation(); + void abortConversation(); + + /// Start an authentication session. Returns if the session was started successfully. + Q_INVOKABLE bool start(); + + /// Abort a running authentication session. + Q_INVOKABLE void abort(); + + /// Respond to pam. + /// + /// May not be called unless `responseRequired` is true. + Q_INVOKABLE void respond(QString response); + + [[nodiscard]] bool isActive() const; + void setActive(bool active); + + [[nodiscard]] QString config() const; + void setConfig(QString config); + + [[nodiscard]] QString configDirectory() const; + void setConfigDirectory(QString configDirectory); + + [[nodiscard]] QString user() const; + void setUser(QString user); + + [[nodiscard]] QString message() const; + [[nodiscard]] bool messageIsError() const; + [[nodiscard]] bool isResponseRequired() const; + +signals: + /// Emitted whenever authentication completes. + void completed(PamResult::Enum result); + /// Emitted if pam fails to perform authentication normally. + /// + /// A `completed(false)` will be emitted after this event. + void error(PamError::Enum error); + + /// Emitted whenever pam sends a new message, after the change signals for + /// `message`, `messageIsError`, and `responseRequired`. + void pamMessage(); + + void activeChanged(); + void configChanged(); + void configDirectoryChanged(); + void userChanged(); + void messageChanged(); + void messageIsErrorChanged(); + void responseRequiredChanged(); + +private slots: + void onCompleted(PamResult::Enum result); + void onError(PamError::Enum error); + void onMessage(QString message, bool messageChanged, bool isError, bool responseRequired); + +private: + PamConversation* conversation = nullptr; + + bool postInit = false; + bool mTargetActive = false; + QString mConfig = "login"; + QString mConfigDirectory = "/etc/pam.d"; + QString mUser; + QString mMessage; + bool mMessageIsError = false; + bool mIsResponseRequired = false; +}; From b5c8774a797bf2f18fea7fac6ebd4c0da37d7a13 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 17 Jun 2024 20:30:23 -0700 Subject: [PATCH 045/305] service/pam: send completed messages after destroying pam conv Allows context to be restarted in a complete handler. --- src/services/pam/qml.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/pam/qml.cpp b/src/services/pam/qml.cpp index be34410a..ee8b45f1 100644 --- a/src/services/pam/qml.cpp +++ b/src/services/pam/qml.cpp @@ -201,14 +201,14 @@ bool PamContext::messageIsError() const { return this->mMessageIsError; } bool PamContext::isResponseRequired() const { return this->mIsResponseRequired; } void PamContext::onCompleted(PamResult::Enum result) { - emit this->completed(result); this->abortConversation(); + emit this->completed(result); } void PamContext::onError(PamError::Enum error) { + this->abortConversation(); emit this->error(error); emit this->completed(PamResult::Error); - this->abortConversation(); } void PamContext::onMessage( From e89035b18c80d6514068403b8fd9bd89e07827f5 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 18 Jun 2024 03:29:25 -0700 Subject: [PATCH 046/305] service/pam: move pam execution to subprocess to allow killing it Many pam modules can't be aborted well without this. --- .clang-tidy | 1 + src/services/pam/CMakeLists.txt | 2 + src/services/pam/conversation.cpp | 215 ++++++++++++------------------ src/services/pam/conversation.hpp | 63 ++++----- src/services/pam/ipc.cpp | 69 ++++++++++ src/services/pam/ipc.hpp | 43 ++++++ src/services/pam/qml.cpp | 16 +-- src/services/pam/qml.hpp | 4 +- src/services/pam/subprocess.cpp | 209 +++++++++++++++++++++++++++++ src/services/pam/subprocess.hpp | 31 +++++ 10 files changed, 480 insertions(+), 173 deletions(-) create mode 100644 src/services/pam/ipc.cpp create mode 100644 src/services/pam/ipc.hpp create mode 100644 src/services/pam/subprocess.cpp create mode 100644 src/services/pam/subprocess.hpp diff --git a/.clang-tidy b/.clang-tidy index 41de1fd7..19c3547c 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -28,6 +28,7 @@ Checks: > -modernize-return-braced-init-list, -modernize-use-trailing-return-type, performance-*, + -performance-avoid-endl, portability-std-allocator-const, readability-*, -readability-function-cognitive-complexity, diff --git a/src/services/pam/CMakeLists.txt b/src/services/pam/CMakeLists.txt index 1a7b29b8..3de9b5d8 100644 --- a/src/services/pam/CMakeLists.txt +++ b/src/services/pam/CMakeLists.txt @@ -3,6 +3,8 @@ qt_add_library(quickshell-service-pam STATIC qml.cpp conversation.cpp + ipc.cpp + subprocess.cpp ) qt_add_qml_module(quickshell-service-pam URI Quickshell.Services.Pam diff --git a/src/services/pam/conversation.cpp b/src/services/pam/conversation.cpp index d73c4264..a06ee880 100644 --- a/src/services/pam/conversation.cpp +++ b/src/services/pam/conversation.cpp @@ -1,21 +1,22 @@ #include "conversation.hpp" -#include #include #include -#include #include +#include #include #include -#include -#include +#include + +#include "ipc.hpp" Q_LOGGING_CATEGORY(logPam, "quickshell.service.pam", QtWarningMsg); QString PamError::toString(PamError::Enum value) { switch (value) { - case ConnectionFailed: return "Failed to connect to pam"; + case StartFailed: return "Failed to start the PAM session"; case TryAuthFailed: return "Failed to try authenticating"; + case InternalError: return "Internal error occurred"; default: return "Invalid error"; } } @@ -26,160 +27,116 @@ QString PamResult::toString(PamResult::Enum value) { case Failed: return "Failed"; case Error: return "Error occurred while authenticating"; case MaxTries: return "The authentication method has no more attempts available"; - // case Expired: return "The account has expired"; - // case PermissionDenied: return "Permission denied"; default: return "Invalid result"; } } -void PamConversation::run() { - auto conv = pam_conv { - .conv = &PamConversation::conversation, - .appdata_ptr = this, - }; +PamConversation::~PamConversation() { this->abort(); } - pam_handle_t* handle = nullptr; - - qCInfo(logPam) << this << "Starting pam session for user" << this->user << "with config" - << this->config << "in configdir" << this->configDir; - - auto result = pam_start_confdir( - this->config.toStdString().c_str(), - this->user.toStdString().c_str(), - &conv, - this->configDir.toStdString().c_str(), - &handle - ); - - if (result != PAM_SUCCESS) { - qCCritical(logPam) << this << "Unable to start pam conversation with error" - << QString(pam_strerror(handle, result)); - emit this->error(PamError::ConnectionFailed); - this->deleteLater(); +void PamConversation::start(const QString& configDir, const QString& config, const QString& user) { + this->childPid = PamConversation::createSubprocess(&this->pipes, configDir, config, user); + if (this->childPid == 0) { + qCCritical(logPam) << "Failed to create pam subprocess."; + emit this->error(PamError::InternalError); return; } - result = pam_authenticate(handle, 0); - - // Seems to require root and quickshell should not run as root. - // if (result == PAM_SUCCESS) { - // result = pam_acct_mgmt(handle, 0); - // } - - switch (result) { - case PAM_SUCCESS: - qCInfo(logPam) << this << "ended with successful authentication."; - emit this->completed(PamResult::Success); - break; - case PAM_AUTH_ERR: - qCInfo(logPam) << this << "ended with failed authentication."; - emit this->completed(PamResult::Failed); - break; - case PAM_MAXTRIES: - qCInfo(logPam) << this << "ended with failure: max tries."; - emit this->completed(PamResult::MaxTries); - break; - /*case PAM_ACCT_EXPIRED: - qCInfo(logPam) << this << "ended with failure: account expiration."; - emit this->completed(PamResult::Expired); - break; - case PAM_PERM_DENIED: - qCInfo(logPam) << this << "ended with failure: permission denied."; - emit this->completed(PamResult::PermissionDenied); - break;*/ - default: - qCCritical(logPam) << this << "ended with error:" << QString(pam_strerror(handle, result)); - emit this->error(PamError::TryAuthFailed); - break; - } - - result = pam_end(handle, result); - if (result != PAM_SUCCESS) { - qCCritical(logPam) << this << "Failed to end pam conversation with error code" - << QString(pam_strerror(handle, result)); - } - - this->deleteLater(); + QObject::connect(&this->notifier, &QSocketNotifier::activated, this, &PamConversation::onMessage); + this->notifier.setSocket(this->pipes.fdIn); + this->notifier.setEnabled(true); } void PamConversation::abort() { - qCDebug(logPam) << "Abort requested for" << this; - auto locker = QMutexLocker(&this->wakeMutex); - this->mAbort = true; - this->waker.wakeOne(); + if (this->childPid != 0) { + qCDebug(logPam) << "Killing subprocess for" << this; + kill(this->childPid, SIGKILL); // NOLINT (include) + waitpid(this->childPid, nullptr, 0); + this->childPid = 0; + } } -void PamConversation::respond(QString response) { - qCDebug(logPam) << "Set response for" << this; - auto locker = QMutexLocker(&this->wakeMutex); - this->response = std::move(response); - this->hasResponse = true; - this->waker.wakeOne(); +void PamConversation::internalError() { + if (this->childPid != 0) { + qCDebug(logPam) << "Killing subprocess for" << this; + kill(this->childPid, SIGKILL); // NOLINT (include) + waitpid(this->childPid, nullptr, 0); + this->childPid = 0; + emit this->error(PamError::InternalError); + } } -int PamConversation::conversation( - int msgCount, - const pam_message** msgArray, - pam_response** responseArray, - void* appdata -) { - auto* delegate = static_cast(appdata); +void PamConversation::respond(const QString& response) { + qCDebug(logPam) << "Sending response for" << this; + if (!this->pipes.writeString(response.toStdString())) { + qCCritical(logPam) << "Failed to write response to subprocess."; + this->internalError(); + } +} +void PamConversation::onMessage() { { - auto locker = QMutexLocker(&delegate->wakeMutex); - if (delegate->mAbort) { - return PAM_ERROR_MSG; - } - } + qCDebug(logPam) << "Got message from subprocess."; - // freed by libc so must be alloc'd by it. - auto* responses = static_cast(calloc(msgCount, sizeof(pam_response))); // NOLINT + auto type = PamIpcEvent::Exit; - for (auto i = 0; i < msgCount; i++) { - const auto* message = msgArray[i]; // NOLINT - auto& response = responses[i]; // NOLINT + auto ok = this->pipes.readBytes( + reinterpret_cast(&type), // NOLINT + sizeof(PamIpcEvent) + ); - auto msgString = QString(message->msg); - auto messageChanged = true; // message->msg_style != PAM_PROMPT_ECHO_OFF; - auto isError = message->msg_style == PAM_ERROR_MSG; - auto responseRequired = - message->msg_style == PAM_PROMPT_ECHO_OFF || message->msg_style == PAM_PROMPT_ECHO_ON; + if (!ok) goto fail; - qCDebug(logPam) << delegate << "got new message message:" << msgString - << "messageChanged:" << messageChanged << "isError:" << isError - << "responseRequired" << responseRequired; + if (type == PamIpcEvent::Exit) { + auto code = PamIpcExitCode::OtherError; - delegate->hasResponse = false; - emit delegate->message(msgString, messageChanged, isError, responseRequired); + ok = this->pipes.readBytes( + reinterpret_cast(&code), // NOLINT + sizeof(PamIpcExitCode) + ); - { - auto locker = QMutexLocker(&delegate->wakeMutex); + if (!ok) goto fail; - if (delegate->mAbort) { - free(responses); // NOLINT - return PAM_ERROR_MSG; + qCDebug(logPam) << "Subprocess exited with code" << static_cast(code); + + switch (code) { + case PamIpcExitCode::Success: emit this->completed(PamResult::Success); break; + case PamIpcExitCode::AuthFailed: emit this->completed(PamResult::Failed); break; + case PamIpcExitCode::StartFailed: emit this->error(PamError::StartFailed); break; + case PamIpcExitCode::MaxTries: emit this->completed(PamResult::MaxTries); break; + case PamIpcExitCode::PamError: emit this->error(PamError::TryAuthFailed); break; + case PamIpcExitCode::OtherError: emit this->error(PamError::InternalError); break; } - if (responseRequired) { - if (!delegate->hasResponse) { - delegate->waker.wait(locker.mutex()); + waitpid(this->childPid, nullptr, 0); + this->childPid = 0; + } else if (type == PamIpcEvent::Request) { + PamIpcRequestFlags flags {}; - if (delegate->mAbort) { - free(responses); // NOLINT - return PAM_ERROR_MSG; - } - } + ok = this->pipes.readBytes( + reinterpret_cast(&flags), // NOLINT + sizeof(PamIpcRequestFlags) + ); - if (!delegate->hasResponse) { - qCCritical(logPam - ) << "Pam conversation requires response and does not have one. This should not happen."; - } + if (!ok) goto fail; - response.resp = strdup(delegate->response.toStdString().c_str()); // NOLINT (include error) - } + auto message = this->pipes.readString(&ok); + + if (!ok) goto fail; + + this->message( + QString::fromUtf8(message), + /*flags.echo*/ true, + flags.error, + flags.responseRequired + ); + } else { + qCCritical(logPam) << "Unexpected message from subprocess."; + goto fail; } } + return; - *responseArray = responses; - return PAM_SUCCESS; +fail: + qCCritical(logPam) << "Failed to read subprocess request."; + this->internalError(); } diff --git a/src/services/pam/conversation.hpp b/src/services/pam/conversation.hpp index ff589806..9719d16a 100644 --- a/src/services/pam/conversation.hpp +++ b/src/services/pam/conversation.hpp @@ -2,13 +2,16 @@ #include -#include +#include #include #include -#include +#include +#include #include -#include -#include + +#include "ipc.hpp" + +Q_DECLARE_LOGGING_CATEGORY(logPam); /// The result of an authentication. class PamResult: public QObject { @@ -26,10 +29,6 @@ public: Error = 2, /// The authentication method ran out of tries and should not be used again. MaxTries = 3, - // The account has expired. - // Expired 4, - // Permission denied. - // PermissionDenied 5, }; Q_ENUM(Enum); @@ -44,52 +43,56 @@ class PamError: public QObject { public: enum Enum { - /// Failed to initiate the pam connection. - ConnectionFailed = 1, + /// Failed to start the pam session. + StartFailed = 1, /// Failed to try to authenticate the user. /// This is not the same as the user failing to authenticate. TryAuthFailed = 2, + /// An error occurred inside quickshell's pam interface. + InternalError = 3, }; Q_ENUM(Enum); Q_INVOKABLE static QString toString(PamError::Enum value); }; -class PamConversation: public QThread { +// PAM has no way to abort a running module except when it sends a message, +// meaning aborts for things like fingerprint scanners +// and hardware keys don't actually work without aborting the process... +// so we have a subprocess. +class PamConversation: public QObject { Q_OBJECT; public: - explicit PamConversation(QString config, QString configDir, QString user) - : config(std::move(config)) - , configDir(std::move(configDir)) - , user(std::move(user)) {} + explicit PamConversation(QObject* parent): QObject(parent) {} + ~PamConversation() override; + Q_DISABLE_COPY_MOVE(PamConversation); public: - void run() override; + void start(const QString& configDir, const QString& config, const QString& user); void abort(); - void respond(QString response); + void respond(const QString& response); signals: void completed(PamResult::Enum result); void error(PamError::Enum error); void message(QString message, bool messageChanged, bool isError, bool responseRequired); +private slots: + void onMessage(); + private: - static int conversation( - int msgCount, - const pam_message** msgArray, - pam_response** responseArray, - void* appdata + static pid_t createSubprocess( + PamIpcPipes* pipes, + const QString& configDir, + const QString& config, + const QString& user ); - QString config; - QString configDir; - QString user; + void internalError(); - QMutex wakeMutex; - QWaitCondition waker; - bool mAbort = false; - bool hasResponse = false; - QString response; + pid_t childPid = 0; + PamIpcPipes pipes; + QSocketNotifier notifier {QSocketNotifier::Read}; }; diff --git a/src/services/pam/ipc.cpp b/src/services/pam/ipc.cpp new file mode 100644 index 00000000..2b0e00b1 --- /dev/null +++ b/src/services/pam/ipc.cpp @@ -0,0 +1,69 @@ +#include "ipc.hpp" +#include +#include +#include + +#include + +PamIpcPipes::~PamIpcPipes() { + if (this->fdIn != 0) close(this->fdIn); + if (this->fdOut != 0) close(this->fdOut); +} + +bool PamIpcPipes::readBytes(char* buffer, size_t length) const { + size_t i = 0; + + while (i < length) { + auto count = read(this->fdIn, buffer + i, length - i); // NOLINT + + if (count == -1 || count == 0) { + return false; + } + + i += count; + } + + return true; +} + +bool PamIpcPipes::writeBytes(const char* buffer, size_t length) const { + size_t i = 0; + while (i < length) { + auto count = write(this->fdOut, buffer + i, length - i); // NOLINT + + if (count == -1 || count == 0) { + return false; + } + + i += count; + } + + return true; +} + +std::string PamIpcPipes::readString(bool* ok) const { + if (ok != nullptr) *ok = false; + + uint32_t length = 0; + if (!this->readBytes(reinterpret_cast(&length), sizeof(length))) { // NOLINT + return ""; + } + + char data[length]; // NOLINT + if (!this->readBytes(data, length)) { + return ""; + } + + if (ok != nullptr) *ok = true; + + return std::string(data, length); +} + +bool PamIpcPipes::writeString(const std::string& string) const { + uint32_t length = string.length(); + if (!this->writeBytes(reinterpret_cast(&length), sizeof(length))) { // NOLINT + return false; + } + + return this->writeBytes(string.data(), string.length()); +} diff --git a/src/services/pam/ipc.hpp b/src/services/pam/ipc.hpp new file mode 100644 index 00000000..7bfcc6f3 --- /dev/null +++ b/src/services/pam/ipc.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +#include + +enum class PamIpcEvent : uint8_t { + Request, + Exit, +}; + +enum class PamIpcExitCode : uint8_t { + Success, + StartFailed, + AuthFailed, + MaxTries, + PamError, + OtherError, +}; + +struct PamIpcRequestFlags { + bool echo; + bool error; + bool responseRequired; +}; + +class PamIpcPipes { +public: + explicit PamIpcPipes() = default; + explicit PamIpcPipes(int fdIn, int fdOut): fdIn(fdIn), fdOut(fdOut) {} + ~PamIpcPipes(); + Q_DISABLE_COPY_MOVE(PamIpcPipes); + + [[nodiscard]] bool readBytes(char* buffer, size_t length) const; + [[nodiscard]] bool writeBytes(const char* buffer, size_t length) const; + [[nodiscard]] std::string readString(bool* ok) const; + [[nodiscard]] bool writeString(const std::string& string) const; + + int fdIn = 0; + int fdOut = 0; +}; diff --git a/src/services/pam/qml.cpp b/src/services/pam/qml.cpp index ee8b45f1..c849f358 100644 --- a/src/services/pam/qml.cpp +++ b/src/services/pam/qml.cpp @@ -13,12 +13,6 @@ #include "conversation.hpp" -PamContext::~PamContext() { - if (this->conversation != nullptr && this->conversation->isRunning()) { - this->conversation->abort(); - } -} - void PamContext::componentComplete() { this->postInit = true; @@ -91,12 +85,12 @@ void PamContext::startConversation() { } } - this->conversation = new PamConversation(this->mConfig, this->mConfigDirectory, user); + this->conversation = new PamConversation(this); QObject::connect(this->conversation, &PamConversation::completed, this, &PamContext::onCompleted); QObject::connect(this->conversation, &PamConversation::error, this, &PamContext::onError); QObject::connect(this->conversation, &PamConversation::message, this, &PamContext::onMessage); emit this->activeChanged(); - this->conversation->start(); + this->conversation->start(this->mConfigDirectory, this->mConfig, user); } void PamContext::abortConversation() { @@ -104,7 +98,7 @@ void PamContext::abortConversation() { this->mTargetActive = false; QObject::disconnect(this->conversation, nullptr, this, nullptr); - if (this->conversation->isRunning()) this->conversation->abort(); + this->conversation->deleteLater(); this->conversation = nullptr; emit this->activeChanged(); @@ -124,9 +118,9 @@ void PamContext::abortConversation() { } } -void PamContext::respond(QString response) { +void PamContext::respond(const QString& response) { if (this->isActive() && this->mIsResponseRequired) { - this->conversation->respond(std::move(response)); + this->conversation->respond(response); } else { qWarning() << "PamContext response was ignored as this context does not require one."; } diff --git a/src/services/pam/qml.hpp b/src/services/pam/qml.hpp index 6f9df4d6..5d741f84 100644 --- a/src/services/pam/qml.hpp +++ b/src/services/pam/qml.hpp @@ -51,8 +51,6 @@ class PamContext public: explicit PamContext(QObject* parent = nullptr): QObject(parent) {} - ~PamContext() override; - Q_DISABLE_COPY_MOVE(PamContext); void classBegin() override {} void componentComplete() override; @@ -69,7 +67,7 @@ public: /// Respond to pam. /// /// May not be called unless `responseRequired` is true. - Q_INVOKABLE void respond(QString response); + Q_INVOKABLE void respond(const QString& response); [[nodiscard]] bool isActive() const; void setActive(bool active); diff --git a/src/services/pam/subprocess.cpp b/src/services/pam/subprocess.cpp new file mode 100644 index 00000000..276e8b94 --- /dev/null +++ b/src/services/pam/subprocess.cpp @@ -0,0 +1,209 @@ +#include "subprocess.hpp" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "conversation.hpp" +#include "ipc.hpp" + +pid_t PamConversation::createSubprocess( + PamIpcPipes* pipes, + const QString& configDir, + const QString& config, + const QString& user +) { + auto toSubprocess = std::array(); + auto fromSubprocess = std::array(); + + if (pipe(toSubprocess.data()) == -1 || pipe(fromSubprocess.data()) == -1) { + qCDebug(logPam) << "Failed to create pipes for subprocess."; + return 0; + } + + auto* configDirF = strdup(configDir.toStdString().c_str()); // NOLINT (include) + auto* configF = strdup(config.toStdString().c_str()); // NOLINT (include) + auto* userF = strdup(user.toStdString().c_str()); // NOLINT (include) + auto log = logPam().isDebugEnabled(); + + auto pid = fork(); + + if (pid < 0) { + qCDebug(logPam) << "Failed to fork for subprocess."; + } else if (pid == 0) { + close(toSubprocess[1]); // close w + close(fromSubprocess[0]); // close r + + { + auto subprocess = PamSubprocess(log, toSubprocess[0], fromSubprocess[1]); + auto code = subprocess.exec(configDirF, configF, userF); + subprocess.sendCode(code); + } + + free(configDirF); // NOLINT + free(configF); // NOLINT + free(userF); // NOLINT + + // do not do cleanup that may affect the parent + _exit(0); + } else { + close(toSubprocess[0]); // close r + close(fromSubprocess[1]); // close w + + pipes->fdIn = fromSubprocess[0]; + pipes->fdOut = toSubprocess[1]; + + free(configDirF); // NOLINT + free(configF); // NOLINT + free(userF); // NOLINT + + return pid; + } + + return -1; // should never happen but lint +} + +PamIpcExitCode PamSubprocess::exec(const char* configDir, const char* config, const char* user) { + logIf(this->log) << "Waiting for parent confirmation..." << std::endl; + + auto conv = pam_conv { + .conv = &PamSubprocess::conversation, + .appdata_ptr = this, + }; + + pam_handle_t* handle = nullptr; + + logIf(this->log) << "Starting pam session for user \"" << user << "\" with config \"" << config + << "\" in dir \"" << configDir << "\"" << std::endl; + + auto result = pam_start_confdir(config, user, &conv, configDir, &handle); + + if (result != PAM_SUCCESS) { + logIf(true) << "Unable to start pam conversation with error \"" << pam_strerror(handle, result) + << "\" (code " << result << ")" << std::endl; + return PamIpcExitCode::StartFailed; + } + + result = pam_authenticate(handle, 0); + PamIpcExitCode code = PamIpcExitCode::OtherError; + + switch (result) { + case PAM_SUCCESS: + logIf(this->log) << "Authenticated successfully." << std::endl; + code = PamIpcExitCode::Success; + break; + case PAM_AUTH_ERR: + logIf(this->log) << "Failed to authenticate." << std::endl; + code = PamIpcExitCode::AuthFailed; + break; + case PAM_MAXTRIES: + logIf(this->log) << "Failed to authenticate due to hitting max tries." << std::endl; + code = PamIpcExitCode::MaxTries; + break; + default: + logIf(true) << "Error while authenticating: \"" << pam_strerror(handle, result) << "\" (code " + << result << ")" << std::endl; + code = PamIpcExitCode::PamError; + break; + } + + result = pam_end(handle, result); + if (result != PAM_SUCCESS) { + logIf(true) << "Error in pam_end: \"" << pam_strerror(handle, result) << "\" (code " << result + << ")" << std::endl; + } + + return code; +} + +void PamSubprocess::sendCode(PamIpcExitCode code) { + { + auto eventType = PamIpcEvent::Exit; + auto ok = this->pipes.writeBytes( + reinterpret_cast(&eventType), // NOLINT + sizeof(PamIpcEvent) + ); + + if (!ok) goto fail; + + ok = this->pipes.writeBytes( + reinterpret_cast(&code), // NOLINT + sizeof(PamIpcExitCode) + ); + + if (!ok) goto fail; + + return; + } + +fail: + _exit(1); +} + +int PamSubprocess::conversation( + int msgCount, + const pam_message** msgArray, + pam_response** responseArray, + void* appdata +) { + auto* delegate = static_cast(appdata); + + // freed by libc so must be alloc'd by it. + auto* responses = static_cast(calloc(msgCount, sizeof(pam_response))); // NOLINT + + for (auto i = 0; i < msgCount; i++) { + const auto* message = msgArray[i]; // NOLINT + auto& response = responses[i]; // NOLINT + + auto msgString = std::string(message->msg); + auto req = PamIpcRequestFlags { + .echo = message->msg_style != PAM_PROMPT_ECHO_OFF, + .error = message->msg_style == PAM_ERROR_MSG, + .responseRequired = + message->msg_style == PAM_PROMPT_ECHO_OFF || message->msg_style == PAM_PROMPT_ECHO_ON, + }; + + logIf(delegate->log) << "Relaying pam message: \"" << msgString << "\" echo: " << req.echo + << " error: " << req.error << " responseRequired: " << req.responseRequired + << std::endl; + + auto eventType = PamIpcEvent::Request; + auto ok = delegate->pipes.writeBytes( + reinterpret_cast(&eventType), // NOLINT + sizeof(PamIpcEvent) + ); + + if (!ok) goto fail; + + ok = delegate->pipes.writeBytes( + reinterpret_cast(&req), // NOLINT + sizeof(PamIpcRequestFlags) + ); + + if (!ok) goto fail; + if (!delegate->pipes.writeString(msgString)) goto fail; + + if (req.responseRequired) { + auto ok = false; + auto resp = delegate->pipes.readString(&ok); + if (!ok) _exit(static_cast(PamIpcExitCode::OtherError)); + logIf(delegate->log) << "Got response for request."; + + response.resp = strdup(resp.c_str()); // NOLINT (include) + } + } + + *responseArray = responses; + return PAM_SUCCESS; + +fail: + free(responseArray); // NOLINT + _exit(1); +} diff --git a/src/services/pam/subprocess.hpp b/src/services/pam/subprocess.hpp new file mode 100644 index 00000000..11f738fd --- /dev/null +++ b/src/services/pam/subprocess.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include + +#include "ipc.hpp" + +// endls are intentional as it makes debugging much easier when the buffer actually flushes. +// NOLINTBEGIN +#define logIf(log) \ + if (log) std::cout << "quickshell.service.pam.subprocess: " +// NOLINTEND + +class PamSubprocess { +public: + explicit PamSubprocess(bool log, int fdIn, int fdOut): log(log), pipes(fdIn, fdOut) {} + PamIpcExitCode exec(const char* configDir, const char* config, const char* user); + void sendCode(PamIpcExitCode code); + +private: + static int conversation( + int msgCount, + const pam_message** msgArray, + pam_response** responseArray, + void* appdata + ); + + bool log; + PamIpcPipes pipes; +}; From ae762f5c6e0cda17708a25825774ac2415616683 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 18 Jun 2024 12:26:23 -0700 Subject: [PATCH 047/305] hyprland/ipc: ensure requests are flushed --- src/wayland/hyprland/ipc/connection.cpp | 32 ++++++++----------------- src/wayland/hyprland/ipc/connection.hpp | 4 ++-- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index 5ee8fffe..060d6fd0 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include #include @@ -58,13 +57,9 @@ HyprlandIpc::HyprlandIpc() { QObject::connect(&this->eventSocket, &QLocalSocket::readyRead, this, &HyprlandIpc::eventSocketReady); // clang-format on - // Sockets don't appear to be able to send data in the first event loop - // cycle of the program, so delay it by one. No idea why this is the case. - QTimer::singleShot(0, [this]() { - this->eventSocket.connectToServer(this->mEventSocketPath, QLocalSocket::ReadOnly); - this->refreshMonitors(true); - this->refreshWorkspaces(true); - }); + this->eventSocket.connectToServer(this->mEventSocketPath, QLocalSocket::ReadOnly); + this->refreshMonitors(true); + this->refreshWorkspaces(true); } QString HyprlandIpc::requestSocketPath() const { return this->mRequestSocketPath; } @@ -128,6 +123,7 @@ void HyprlandIpc::makeRequest( QObject::connect(requestSocket, &QLocalSocket::readyRead, this, responseCallback); requestSocket->write(request); + requestSocket->flush(); }; auto errorCallback = [=](QLocalSocket::LocalSocketError error) { @@ -377,19 +373,15 @@ HyprlandIpc::findWorkspaceByName(const QString& name, bool createIfMissing, qint } } -void HyprlandIpc::refreshWorkspaces(bool canCreate, bool tryAgain) { +void HyprlandIpc::refreshWorkspaces(bool canCreate) { if (this->requestingWorkspaces) return; this->requestingWorkspaces = true; this->makeRequest( "j/workspaces", - [this, canCreate, tryAgain](bool success, const QByteArray& resp) { + [this, canCreate](bool success, const QByteArray& resp) { this->requestingWorkspaces = false; - if (!success) { - // sometimes fails randomly, so we give it another shot. - if (tryAgain) this->refreshWorkspaces(canCreate, false); - return; - } + if (!success) return; qCDebug(logHyprlandIpc) << "parsing workspaces response"; auto json = QJsonDocument::fromJson(resp).array(); @@ -493,19 +485,15 @@ void HyprlandIpc::onFocusedMonitorDestroyed() { emit this->focusedMonitorChanged(); } -void HyprlandIpc::refreshMonitors(bool canCreate, bool tryAgain) { +void HyprlandIpc::refreshMonitors(bool canCreate) { if (this->requestingMonitors) return; this->requestingMonitors = true; this->makeRequest( "j/monitors", - [this, canCreate, tryAgain](bool success, const QByteArray& resp) { + [this, canCreate](bool success, const QByteArray& resp) { this->requestingMonitors = false; - if (!success) { - // sometimes fails randomly, so we give it another shot. - if (tryAgain) this->refreshMonitors(canCreate, false); - return; - } + if (!success) return; this->monitorsRequested = true; diff --git a/src/wayland/hyprland/ipc/connection.hpp b/src/wayland/hyprland/ipc/connection.hpp index 1778460a..635918d8 100644 --- a/src/wayland/hyprland/ipc/connection.hpp +++ b/src/wayland/hyprland/ipc/connection.hpp @@ -81,8 +81,8 @@ public: HyprlandMonitor* findMonitorByName(const QString& name, bool createIfMissing, qint32 id = -1); // canCreate avoids making ghost workspaces when the connection races - void refreshWorkspaces(bool canCreate, bool tryAgain = true); - void refreshMonitors(bool canCreate, bool tryAgain = true); + void refreshWorkspaces(bool canCreate); + void refreshMonitors(bool canCreate); // The last argument may contain commas, so the count is required. [[nodiscard]] static QVector parseEventArgs(QByteArrayView event, quint16 count); From 3991726b9b0ddca86e99675fcd15df9e6dc194ab Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 18 Jun 2024 15:25:10 -0700 Subject: [PATCH 048/305] docs: document PAM feature in build instructions --- BUILD.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/BUILD.md b/BUILD.md index 3c3e7125..ea69cbd6 100644 --- a/BUILD.md +++ b/BUILD.md @@ -113,6 +113,13 @@ To disable: `-DSERVICE_MPRIS=OFF` Dependencies: `qt6dbus` (usually part of qt6base) +### PAM +This feature enables PAM integration for user authentication. + +To disable: `-DSERVICE_PAM=OFF` + +Dependencies: `pam` + ### Hyprland This feature enables hyprland specific integrations. It requires wayland support but has no extra dependencies. From 9e58077c61fce106d613abbfe47e659185e38bf8 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 18 Jun 2024 17:03:38 -0700 Subject: [PATCH 049/305] core: fix shutdown sequence crashing --- src/core/generation.cpp | 8 ++++++++ src/core/generation.hpp | 1 + src/core/rootwrapper.cpp | 5 +---- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index e43db6ee..aef12049 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -84,6 +84,14 @@ void EngineGeneration::destroy() { } } +void EngineGeneration::shutdown() { + delete this->root; + this->root = nullptr; + delete this->engine; + this->engine = nullptr; + delete this; +} + void EngineGeneration::onReload(EngineGeneration* old) { if (old != nullptr) { // if the old generation holds the window incubation controller as the diff --git a/src/core/generation.hpp b/src/core/generation.hpp index f757113e..760c19ce 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -51,6 +51,7 @@ public: QuickshellGlobal* qsgInstance = nullptr; void destroy(); + void shutdown(); signals: void filesChanged(); diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index 1afb30cf..096ac4de 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -35,11 +35,8 @@ RootWrapper::RootWrapper(QString rootPath) RootWrapper::~RootWrapper() { // event loop may no longer be running so deleteLater is not an option if (this->generation != nullptr) { - delete this->generation->root; - this->generation->root = nullptr; + this->generation->shutdown(); } - - delete this->generation; } void RootWrapper::reloadGraph(bool hard) { From 71a65c4d3c233a640e0bdc0f731019f44d3b3783 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 18 Jun 2024 17:54:08 -0700 Subject: [PATCH 050/305] docs: mention Fedora COPR package --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bf494c3c..99c3a60f 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,16 @@ Note: by default this package is built with clang as it is significantly faster. ## Arch (AUR) Quickshell has a third party [AUR package] available under the same name. -As is usual with the AUR it is not maintained by us and should be looked over before use. +It is not managed by us and should be looked over before use. [AUR package]: https://aur.archlinux.org/packages/quickshell +## Fedora (COPR) +Quickshell has a third party [Fedora COPR package] available under the same name. +It is not managed by us and should be looked over before use. + +[Fedora COPR package]: https://copr.fedorainfracloud.org/coprs/errornointernet/quickshell + ## Anything else See [BUILD.md](BUILD.md) for instructions on building and packaging quickshell. From 8ec245ac667f2066a65b6fe89a9a2a7eff6df81b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 18 Jun 2024 20:34:16 -0700 Subject: [PATCH 051/305] wayland/lock: initialize lock content before starting lock Reduces any chances of the compositor displaying a blank frame first. --- src/wayland/session_lock.cpp | 127 +++++++++++++--------- src/wayland/session_lock.hpp | 3 +- src/wayland/session_lock/session_lock.cpp | 2 + src/wayland/session_lock/session_lock.hpp | 4 +- 4 files changed, 79 insertions(+), 57 deletions(-) diff --git a/src/wayland/session_lock.cpp b/src/wayland/session_lock.cpp index bb5ed132..251d7b8f 100644 --- a/src/wayland/session_lock.cpp +++ b/src/wayland/session_lock.cpp @@ -46,60 +46,72 @@ void WlSessionLock::onReload(QObject* oldInstance) { } // clang-format on - if (this->lockTarget) { - if (!this->manager->lock()) this->lockTarget = false; - this->updateSurfaces(old); - } else { - this->setLocked(false); + this->realizeLockTarget(old); +} + +void WlSessionLock::updateSurfaces(bool show, WlSessionLock* old) { + auto screens = QGuiApplication::screens(); + + auto map = this->surfaces.toStdMap(); + for (auto& [screen, surface]: map) { + if (!screens.contains(screen)) { + this->surfaces.remove(screen); + surface->deleteLater(); + } + } + + for (auto* screen: screens) { + if (!this->surfaces.contains(screen)) { + auto* instanceObj = + this->mSurfaceComponent->create(QQmlEngine::contextForObject(this->mSurfaceComponent)); + auto* instance = qobject_cast(instanceObj); + + if (instance == nullptr) { + qWarning( + ) << "WlSessionLock.surface does not create a WlSessionLockSurface. Aborting lock."; + if (instanceObj != nullptr) instanceObj->deleteLater(); + this->unlock(); + return; + } + + instance->setParent(this); + instance->setScreen(screen); + + auto* oldInstance = old == nullptr ? nullptr : old->surfaces.value(screen, nullptr); + instance->reload(oldInstance); + + this->surfaces[screen] = instance; + } + } + + if (show) { + if (!this->manager->isLocked()) { + qFatal() << "Tried to show lockscreen surfaces without active lock"; + } + + for (auto* surface: this->surfaces.values()) { + surface->show(); + } } } -void WlSessionLock::updateSurfaces(WlSessionLock* old) { - if (this->manager->isLocked()) { - auto screens = QGuiApplication::screens(); - - auto map = this->surfaces.toStdMap(); - for (auto& [screen, surface]: map) { - if (!screens.contains(screen)) { - this->surfaces.remove(screen); - surface->deleteLater(); - } - } - +void WlSessionLock::realizeLockTarget(WlSessionLock* old) { + if (this->lockTarget) { if (this->mSurfaceComponent == nullptr) { qWarning() << "WlSessionLock.surface is null. Aborting lock."; this->unlock(); return; } - for (auto* screen: screens) { - if (!this->surfaces.contains(screen)) { - auto* instanceObj = - this->mSurfaceComponent->create(QQmlEngine::contextForObject(this->mSurfaceComponent)); - auto* instance = qobject_cast(instanceObj); + // preload initial surfaces to make the chance of the compositor displaying a blank + // frame before the lock surfaces are shown as low as possible. + this->updateSurfaces(false); - if (instance == nullptr) { - qWarning( - ) << "WlSessionLock.surface does not create a WlSessionLockSurface. Aborting lock."; - if (instanceObj != nullptr) instanceObj->deleteLater(); - this->unlock(); - return; - } + if (!this->manager->lock()) this->lockTarget = false; - instance->setParent(this); - instance->setScreen(screen); - - auto* oldInstance = old == nullptr ? nullptr : old->surfaces.value(screen, nullptr); - instance->reload(oldInstance); - instance->attach(); - - this->surfaces[screen] = instance; - } - - for (auto* surface: this->surfaces.values()) { - surface->show(); - } - } + this->updateSurfaces(true, old); + } else { + this->unlock(); // emits lockStateChanged } } @@ -118,7 +130,7 @@ void WlSessionLock::unlock() { } } -void WlSessionLock::onScreensChanged() { this->updateSurfaces(); } +void WlSessionLock::onScreensChanged() { this->updateSurfaces(true); } bool WlSessionLock::isLocked() const { return this->manager == nullptr ? this->lockTarget : this->manager->isLocked(); @@ -137,18 +149,17 @@ void WlSessionLock::setLocked(bool locked) { return; } - if (locked) { - if (!this->manager->lock()) this->lockTarget = false; - this->updateSurfaces(); - if (this->lockTarget) emit this->lockStateChanged(); - } else { - this->unlock(); // emits lockStateChanged - } + this->realizeLockTarget(); } QQmlComponent* WlSessionLock::surfaceComponent() const { return this->mSurfaceComponent; } void WlSessionLock::setSurfaceComponent(QQmlComponent* surfaceComponent) { + if (this->manager != nullptr && this->manager->isLocked()) { + qCritical() << "WlSessionLock.surfaceComponent cannot be changed while the lock is active."; + return; + } + if (this->mSurfaceComponent != nullptr) this->mSurfaceComponent->deleteLater(); if (surfaceComponent != nullptr) surfaceComponent->setParent(this); @@ -202,6 +213,8 @@ void WlSessionLockSurface::onReload(QObject* oldInstance) { } void WlSessionLockSurface::attach() { + if (this->ext->isAttached()) return; + if (auto* parent = qobject_cast(this->parent())) { if (!this->ext->attach(this->window, parent->manager)) { qFatal() << "Failed to attach WlSessionLockSurface"; @@ -220,7 +233,10 @@ QQuickWindow* WlSessionLockSurface::disownWindow() { return window; } -void WlSessionLockSurface::show() { this->ext->setVisible(); } +void WlSessionLockSurface::show() { + this->attach(); + this->ext->setVisible(); +} QQuickItem* WlSessionLockSurface::contentItem() const { return this->mContentItem; } @@ -257,8 +273,11 @@ void WlSessionLockSurface::setScreen(QScreen* qscreen) { QObject::connect(qscreen, &QObject::destroyed, this, &WlSessionLockSurface::onScreenDestroyed); } - if (this->window == nullptr) this->mScreen = qscreen; - else this->window->setScreen(qscreen); + if (this->window == nullptr) { + this->mScreen = qscreen; + } else { + this->window->setScreen(qscreen); + } emit this->screenChanged(); } diff --git a/src/wayland/session_lock.hpp b/src/wayland/session_lock.hpp index 9e35cd49..a44dae0c 100644 --- a/src/wayland/session_lock.hpp +++ b/src/wayland/session_lock.hpp @@ -97,7 +97,8 @@ private slots: void onScreensChanged(); private: - void updateSurfaces(WlSessionLock* old = nullptr); + void updateSurfaces(bool show, WlSessionLock* old = nullptr); + void realizeLockTarget(WlSessionLock* old = nullptr); SessionLockManager* manager = nullptr; QQmlComponent* mSurfaceComponent = nullptr; diff --git a/src/wayland/session_lock/session_lock.cpp b/src/wayland/session_lock/session_lock.cpp index 1f2f1cec..50e88185 100644 --- a/src/wayland/session_lock/session_lock.cpp +++ b/src/wayland/session_lock/session_lock.cpp @@ -63,6 +63,8 @@ LockWindowExtension* LockWindowExtension::get(QWindow* window) { } } +bool LockWindowExtension::isAttached() const { return this->surface != nullptr; } + bool LockWindowExtension::attach(QWindow* window, SessionLockManager* manager) { if (this->surface != nullptr) qFatal() << "Cannot change the attached window of a LockWindowExtension"; diff --git a/src/wayland/session_lock/session_lock.hpp b/src/wayland/session_lock/session_lock.hpp index 7e3968bd..1ad6ae90 100644 --- a/src/wayland/session_lock/session_lock.hpp +++ b/src/wayland/session_lock/session_lock.hpp @@ -61,6 +61,8 @@ public: ~LockWindowExtension() override; Q_DISABLE_COPY_MOVE(LockWindowExtension); + [[nodiscard]] bool isAttached() const; + // Attach this lock extension to the given window. // The extension is reparented to the window and replaces any existing lock extension. // Returns false if the window cannot be used. @@ -70,8 +72,6 @@ public: // To make a window invisible, destroy it as it cannot be recovered. void setVisible(); - [[nodiscard]] bool isLocked() const; - static LockWindowExtension* get(QWindow* window); signals: From 3033cba52d89b8bcb2202f87ea03c5e4d5d75328 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 18 Jun 2024 20:46:58 -0700 Subject: [PATCH 052/305] all: fix failing lints --- src/core/desktopentry.cpp | 12 +- src/core/main.cpp | 2 + src/core/transformwatcher.cpp | 1 + src/wayland/hyprland/ipc/connection.cpp | 157 +++++++++++------------- 4 files changed, 83 insertions(+), 89 deletions(-) diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index a5ecef83..9c887b6c 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -4,10 +4,8 @@ #include #include #include -#include #include #include -#include #include #include #include @@ -94,7 +92,7 @@ void DesktopEntry::parseEntry(const QString& text) { auto groupName = QString(); auto entries = QHash>(); - auto finishCategory = [&]() { + auto finishCategory = [this, &groupName, &entries]() { if (groupName == "Desktop Entry") { if (entries["Type"].second != "Application") return; if (entries.contains("Hidden") && entries["Hidden"].second == "true") return; @@ -334,13 +332,13 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) { qCDebug(logDesktopEntry) << "Found desktop entry" << id << "at" << path; - if (desktopEntries.contains(id)) { + if (this->desktopEntries.contains(id)) { qCDebug(logDesktopEntry) << "Replacing old entry for" << id; - delete desktopEntries.value(id); - desktopEntries.remove(id); + delete this->desktopEntries.value(id); + this->desktopEntries.remove(id); } - desktopEntries.insert(id, dentry); + this->desktopEntries.insert(id, dentry); } } } diff --git a/src/core/main.cpp b/src/core/main.cpp index 3daf09ad..b893ab9f 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include #include #include #include diff --git a/src/core/transformwatcher.cpp b/src/core/transformwatcher.cpp index 2a33bad0..6fc7c34a 100644 --- a/src/core/transformwatcher.cpp +++ b/src/core/transformwatcher.cpp @@ -7,6 +7,7 @@ #include #include #include +#include void TransformWatcher::resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent) { if (a == nullptr || b == nullptr) return; diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index 060d6fd0..cb2bf203 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -377,58 +377,55 @@ void HyprlandIpc::refreshWorkspaces(bool canCreate) { if (this->requestingWorkspaces) return; this->requestingWorkspaces = true; - this->makeRequest( - "j/workspaces", - [this, canCreate](bool success, const QByteArray& resp) { - this->requestingWorkspaces = false; - if (!success) return; + this->makeRequest("j/workspaces", [this, canCreate](bool success, const QByteArray& resp) { + this->requestingWorkspaces = false; + if (!success) return; - qCDebug(logHyprlandIpc) << "parsing workspaces response"; - auto json = QJsonDocument::fromJson(resp).array(); + qCDebug(logHyprlandIpc) << "parsing workspaces response"; + auto json = QJsonDocument::fromJson(resp).array(); - const auto& mList = this->mWorkspaces.valueList(); - auto names = QVector(); + const auto& mList = this->mWorkspaces.valueList(); + auto names = QVector(); - for (auto entry: json) { - auto object = entry.toObject().toVariantMap(); - auto name = object.value("name").toString(); + for (auto entry: json) { + auto object = entry.toObject().toVariantMap(); + auto name = object.value("name").toString(); - auto workspaceIter = - std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { - return m->name() == name; - }); + auto workspaceIter = + std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { + return m->name() == name; + }); - auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; - auto existed = workspace != nullptr; + auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; + auto existed = workspace != nullptr; - if (workspace == nullptr) { - if (!canCreate) continue; - workspace = new HyprlandWorkspace(this); - } + if (workspace == nullptr) { + if (!canCreate) continue; + workspace = new HyprlandWorkspace(this); + } - workspace->updateFromObject(object); + workspace->updateFromObject(object); - if (!existed) { - this->mWorkspaces.insertObject(workspace); - } + if (!existed) { + this->mWorkspaces.insertObject(workspace); + } - names.push_back(name); - } + names.push_back(name); + } - auto removedWorkspaces = QVector(); + auto removedWorkspaces = QVector(); - for (auto* workspace: mList) { - if (!names.contains(workspace->name())) { - removedWorkspaces.push_back(workspace); - } - } + for (auto* workspace: mList) { + if (!names.contains(workspace->name())) { + removedWorkspaces.push_back(workspace); + } + } - for (auto* workspace: removedWorkspaces) { - this->mWorkspaces.removeObject(workspace); - delete workspace; - } - } - ); + for (auto* workspace: removedWorkspaces) { + this->mWorkspaces.removeObject(workspace); + delete workspace; + } + }); } HyprlandMonitor* @@ -489,61 +486,57 @@ void HyprlandIpc::refreshMonitors(bool canCreate) { if (this->requestingMonitors) return; this->requestingMonitors = true; - this->makeRequest( - "j/monitors", - [this, canCreate](bool success, const QByteArray& resp) { - this->requestingMonitors = false; - if (!success) return; + this->makeRequest("j/monitors", [this, canCreate](bool success, const QByteArray& resp) { + this->requestingMonitors = false; + if (!success) return; - this->monitorsRequested = true; + this->monitorsRequested = true; - qCDebug(logHyprlandIpc) << "parsing monitors response"; - auto json = QJsonDocument::fromJson(resp).array(); + qCDebug(logHyprlandIpc) << "parsing monitors response"; + auto json = QJsonDocument::fromJson(resp).array(); - const auto& mList = this->mMonitors.valueList(); - auto names = QVector(); + const auto& mList = this->mMonitors.valueList(); + auto names = QVector(); - for (auto entry: json) { - auto object = entry.toObject().toVariantMap(); - auto name = object.value("name").toString(); + for (auto entry: json) { + auto object = entry.toObject().toVariantMap(); + auto name = object.value("name").toString(); - auto monitorIter = - std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { - return m->name() == name; - }); + auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { + return m->name() == name; + }); - auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; - auto existed = monitor != nullptr; + auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; + auto existed = monitor != nullptr; - if (monitor == nullptr) { - if (!canCreate) continue; - monitor = new HyprlandMonitor(this); - } + if (monitor == nullptr) { + if (!canCreate) continue; + monitor = new HyprlandMonitor(this); + } - monitor->updateFromObject(object); + monitor->updateFromObject(object); - if (!existed) { - this->mMonitors.insertObject(monitor); - } + if (!existed) { + this->mMonitors.insertObject(monitor); + } - names.push_back(name); - } + names.push_back(name); + } - auto removedMonitors = QVector(); + auto removedMonitors = QVector(); - for (auto* monitor: mList) { - if (!names.contains(monitor->name())) { - removedMonitors.push_back(monitor); - } - } + for (auto* monitor: mList) { + if (!names.contains(monitor->name())) { + removedMonitors.push_back(monitor); + } + } - for (auto* monitor: removedMonitors) { - this->mMonitors.removeObject(monitor); - // see comment in onEvent - monitor->deleteLater(); - } - } - ); + for (auto* monitor: removedMonitors) { + this->mMonitors.removeObject(monitor); + // see comment in onEvent + monitor->deleteLater(); + } + }); } } // namespace qs::hyprland::ipc From 6efa05a8eb3f8e5239b9c379b29a135005ca8cd8 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 18 Jun 2024 20:58:33 -0700 Subject: [PATCH 053/305] core: run full destruction sequence before exiting Fixes QTimer messages. --- src/core/generation.cpp | 31 ++++++++++++++++++++++++++++--- src/core/generation.hpp | 6 ++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index aef12049..0a62432e 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -52,6 +52,9 @@ EngineGeneration::~EngineGeneration() { } void EngineGeneration::destroy() { + if (this->destroying) return; + this->destroying = true; + if (this->watcher != nullptr) { // Multiple generations can detect a reload at the same time. QObject::disconnect(this->watcher, nullptr, this, nullptr); @@ -71,7 +74,12 @@ void EngineGeneration::destroy() { delete this->engine; this->engine = nullptr; + + auto terminate = this->shouldTerminate; + auto code = this->exitCode; delete this; + + if (terminate) QCoreApplication::exit(code); }); this->root->deleteLater(); @@ -80,11 +88,18 @@ void EngineGeneration::destroy() { // the engine has never been used, no need to clean up delete this->engine; this->engine = nullptr; + + auto terminate = this->shouldTerminate; + auto code = this->exitCode; delete this; + + if (terminate) QCoreApplication::exit(code); } } void EngineGeneration::shutdown() { + if (this->destroying) return; + delete this->root; this->root = nullptr; delete this->engine; @@ -100,9 +115,8 @@ void EngineGeneration::onReload(EngineGeneration* old) { old->assignIncubationController(); } - auto* app = QCoreApplication::instance(); - QObject::connect(this->engine, &QQmlEngine::quit, app, &QCoreApplication::quit); - QObject::connect(this->engine, &QQmlEngine::exit, app, &QCoreApplication::exit); + QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit); + QObject::connect(this->engine, &QQmlEngine::exit, this, &EngineGeneration::exit); this->root->reload(old == nullptr ? nullptr : old->root); this->singletonRegistry.onReload(old == nullptr ? nullptr : &old->singletonRegistry); @@ -266,6 +280,17 @@ void EngineGeneration::incubationControllerDestroyed() { } } +void EngineGeneration::quit() { + this->shouldTerminate = true; + this->destroy(); +} + +void EngineGeneration::exit(int code) { + this->shouldTerminate = true; + this->exitCode = code; + this->destroy(); +} + void EngineGeneration::assignIncubationController() { QQmlIncubationController* controller = nullptr; if (this->incubationControllers.isEmpty()) controller = &this->delayedIncubationController; diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 760c19ce..2f2fc5ff 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -61,9 +61,15 @@ private slots: void onFileChanged(const QString& name); void onDirectoryChanged(); void incubationControllerDestroyed(); + void quit(); + void exit(int code); private: void postReload(); void assignIncubationController(); QVector> incubationControllers; + + bool destroying = false; + bool shouldTerminate = false; + int exitCode = 0; }; From 59cf60d83ece304e02411eee1f66fdf2a320d658 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 19 Jun 2024 00:31:09 -0700 Subject: [PATCH 054/305] service/pam: add responseVisible Fixes misunderstanding of "echo". --- src/services/pam/conversation.cpp | 7 +------ src/services/pam/qml.cpp | 26 +++++++++++++++----------- src/services/pam/qml.hpp | 7 ++++++- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/services/pam/conversation.cpp b/src/services/pam/conversation.cpp index a06ee880..c58f4fef 100644 --- a/src/services/pam/conversation.cpp +++ b/src/services/pam/conversation.cpp @@ -123,12 +123,7 @@ void PamConversation::onMessage() { if (!ok) goto fail; - this->message( - QString::fromUtf8(message), - /*flags.echo*/ true, - flags.error, - flags.responseRequired - ); + this->message(QString::fromUtf8(message), flags.error, flags.responseRequired, flags.echo); } else { qCCritical(logPam) << "Unexpected message from subprocess."; goto fail; diff --git a/src/services/pam/qml.cpp b/src/services/pam/qml.cpp index c849f358..01a02953 100644 --- a/src/services/pam/qml.cpp +++ b/src/services/pam/qml.cpp @@ -193,6 +193,7 @@ void PamContext::setUser(QString user) { QString PamContext::message() const { return this->mMessage; } bool PamContext::messageIsError() const { return this->mMessageIsError; } bool PamContext::isResponseRequired() const { return this->mIsResponseRequired; } +bool PamContext::isResponseVisible() const { return this->mIsResponseVisible; } void PamContext::onCompleted(PamResult::Enum result) { this->abortConversation(); @@ -207,20 +208,23 @@ void PamContext::onError(PamError::Enum error) { void PamContext::onMessage( QString message, - bool messageChanged, bool isError, - bool responseRequired + bool responseRequired, + bool responseVisible ) { - if (messageChanged) { - if (message != this->mMessage) { - this->mMessage = std::move(message); - emit this->messageChanged(); - } + if (message != this->mMessage) { + this->mMessage = std::move(message); + emit this->messageChanged(); + } - if (isError != this->mMessageIsError) { - this->mMessageIsError = isError; - emit this->messageIsErrorChanged(); - } + if (isError != this->mMessageIsError) { + this->mMessageIsError = isError; + emit this->messageIsErrorChanged(); + } + + if (responseVisible != this->mIsResponseVisible) { + this->mIsResponseVisible = responseVisible; + emit this->responseVisibleChanged(); } if (responseRequired != this->mIsResponseRequired) { diff --git a/src/services/pam/qml.hpp b/src/services/pam/qml.hpp index 5d741f84..c6e3509e 100644 --- a/src/services/pam/qml.hpp +++ b/src/services/pam/qml.hpp @@ -46,6 +46,8 @@ class PamContext /// /// Responses can be returned with the `respond()` function. Q_PROPERTY(bool responseRequired READ isResponseRequired NOTIFY responseRequiredChanged); + /// 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; @@ -84,6 +86,7 @@ public: [[nodiscard]] QString message() const; [[nodiscard]] bool messageIsError() const; [[nodiscard]] bool isResponseRequired() const; + [[nodiscard]] bool isResponseVisible() const; signals: /// Emitted whenever authentication completes. @@ -104,11 +107,12 @@ signals: void messageChanged(); void messageIsErrorChanged(); void responseRequiredChanged(); + void responseVisibleChanged(); private slots: void onCompleted(PamResult::Enum result); void onError(PamError::Enum error); - void onMessage(QString message, bool messageChanged, bool isError, bool responseRequired); + void onMessage(QString message, bool isError, bool responseRequired, bool responseVisible); private: PamConversation* conversation = nullptr; @@ -121,4 +125,5 @@ private: QString mMessage; bool mMessageIsError = false; bool mIsResponseRequired = false; + bool mIsResponseVisible = false; }; From 72956185bd64cabb07af990419e23ae8b8c56f6e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 19 Jun 2024 11:16:51 -0700 Subject: [PATCH 055/305] wayland/lock: only update surfaces on screens changed if locked Fixes crash when a WlSessionLock object exists but is unlocked and a screen is added or removed. --- src/wayland/session_lock.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/wayland/session_lock.cpp b/src/wayland/session_lock.cpp index 251d7b8f..5f0bb673 100644 --- a/src/wayland/session_lock.cpp +++ b/src/wayland/session_lock.cpp @@ -130,7 +130,11 @@ void WlSessionLock::unlock() { } } -void WlSessionLock::onScreensChanged() { this->updateSurfaces(true); } +void WlSessionLock::onScreensChanged() { + if (this->manager != nullptr && this->manager->isLocked()) { + this->updateSurfaces(true); + } +} bool WlSessionLock::isLocked() const { return this->manager == nullptr ? this->lockTarget : this->manager->isLocked(); From 3573663ab648ff793e6ad545b63eaa5979633e51 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 20 Jun 2024 15:39:49 -0700 Subject: [PATCH 056/305] service/greetd: add greetd service --- CMakeLists.txt | 2 + src/core/generation.cpp | 6 + src/core/generation.hpp | 10 +- src/services/CMakeLists.txt | 4 + src/services/greetd/CMakeLists.txt | 16 ++ src/services/greetd/connection.cpp | 263 +++++++++++++++++++++++++++++ src/services/greetd/connection.hpp | 74 ++++++++ src/services/greetd/module.md | 7 + src/services/greetd/qml.cpp | 46 +++++ src/services/greetd/qml.hpp | 95 +++++++++++ src/services/pam/CMakeLists.txt | 3 +- 11 files changed, 522 insertions(+), 4 deletions(-) create mode 100644 src/services/greetd/CMakeLists.txt create mode 100644 src/services/greetd/connection.cpp create mode 100644 src/services/greetd/connection.hpp create mode 100644 src/services/greetd/module.md create mode 100644 src/services/greetd/qml.cpp create mode 100644 src/services/greetd/qml.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 606256bd..48654909 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) option(SERVICE_PIPEWIRE "PipeWire service" ON) option(SERVICE_MPRIS "Mpris service" ON) option(SERVICE_PAM "Pam service" ON) +option(SERVICE_GREETD "Greet service" ON) message(STATUS "Quickshell configuration") message(STATUS " Jemalloc: ${USE_JEMALLOC}") @@ -41,6 +42,7 @@ message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") message(STATUS " Mpris: ${SERVICE_MPRIS}") message(STATUS " Pam: ${SERVICE_PAM}") +message(STATUS " Greetd: ${SERVICE_GREETD}") message(STATUS " Hyprland: ${HYPRLAND}") if (HYPRLAND) message(STATUS " IPC: ${HYPRLAND_IPC}") diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 0a62432e..2ca7a77b 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -302,6 +302,12 @@ void EngineGeneration::assignIncubationController() { this->engine->setIncubationController(controller); } +EngineGeneration* EngineGeneration::currentGeneration() { + if (g_generations.size() == 1) { + return *g_generations.begin(); + } else return nullptr; +} + EngineGeneration* EngineGeneration::findEngineGeneration(QQmlEngine* engine) { return g_generations.value(engine); } diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 2f2fc5ff..9bcb8b60 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -36,6 +36,10 @@ public: static EngineGeneration* findEngineGeneration(QQmlEngine* engine); static EngineGeneration* findObjectGeneration(QObject* object); + // Returns the current generation if there is only one generation, + // otherwise null. + static EngineGeneration* currentGeneration(); + RootWrapper* wrapper = nullptr; QDir rootPath; QmlScanner scanner; @@ -57,12 +61,14 @@ signals: void filesChanged(); void reloadFinished(); +public slots: + void quit(); + void exit(int code); + private slots: void onFileChanged(const QString& name); void onDirectoryChanged(); void incubationControllerDestroyed(); - void quit(); - void exit(int code); private: void postReload(); diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt index e8c05f4c..8005f074 100644 --- a/src/services/CMakeLists.txt +++ b/src/services/CMakeLists.txt @@ -13,3 +13,7 @@ endif() if (SERVICE_PAM) add_subdirectory(pam) endif() + +if (SERVICE_GREETD) + add_subdirectory(greetd) +endif() diff --git a/src/services/greetd/CMakeLists.txt b/src/services/greetd/CMakeLists.txt new file mode 100644 index 00000000..3c8fcf3b --- /dev/null +++ b/src/services/greetd/CMakeLists.txt @@ -0,0 +1,16 @@ +qt_add_library(quickshell-service-greetd STATIC + qml.cpp + connection.cpp +) + +qt_add_qml_module(quickshell-service-greetd + URI Quickshell.Services.Greetd + VERSION 0.1 +) + +target_link_libraries(quickshell-service-greetd PRIVATE ${QT_DEPS}) + +qs_pch(quickshell-service-greetd) +qs_pch(quickshell-service-greetdplugin) + +target_link_libraries(quickshell PRIVATE quickshell-service-greetdplugin) diff --git a/src/services/greetd/connection.cpp b/src/services/greetd/connection.cpp new file mode 100644 index 00000000..4b59d793 --- /dev/null +++ b/src/services/greetd/connection.cpp @@ -0,0 +1,263 @@ +#include "connection.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/generation.hpp" + +Q_LOGGING_CATEGORY(logGreetd, "quickshell.service.greetd"); + +QString GreetdState::toString(GreetdState::Enum value) { + switch (value) { + case GreetdState::Inactive: return "Inactive"; + case GreetdState::Authenticating: return "Authenticating"; + case GreetdState::ReadyToLaunch: return "Ready to Launch"; + case GreetdState::Launching: return "Launching"; + case GreetdState::Launched: return "Launched"; + default: return "Invalid"; + } +} + +GreetdConnection::GreetdConnection() { + auto socket = qEnvironmentVariable("GREETD_SOCK"); + + if (socket.isEmpty()) { + this->mAvailable = false; + return; + } + + this->mAvailable = true; + + // clang-format off + QObject::connect(&this->socket, &QLocalSocket::connected, this, &GreetdConnection::onSocketConnected); + QObject::connect(&this->socket, &QLocalSocket::readyRead, this, &GreetdConnection::onSocketReady); + // clang-format on + + this->socket.connectToServer(socket, QLocalSocket::ReadWrite); +} + +void GreetdConnection::createSession(QString user) { + if (!this->mAvailable) { + qCCritical(logGreetd) << "Greetd is not available."; + return; + } + + if (user != this->mUser) { + this->mUser = std::move(user); + emit this->userChanged(); + } + + this->setActive(true); +} + +void GreetdConnection::cancelSession() { this->setActive(false); } + +void GreetdConnection::respond(QString response) { + if (!this->mResponseRequired) { + qCCritical(logGreetd) << "Cannot respond to greetd as a response is not currently required."; + return; + } + + this->sendRequest({ + {"type", "post_auth_message_response"}, + {"response", response}, + }); + + this->mResponseRequired = false; +} + +void GreetdConnection::launch( + const QList& command, + const QList& environment, + bool exit +) { + if (this->mState != GreetdState::ReadyToLaunch) { + qCCritical(logGreetd) << "Cannot call launch() as state is not currently ReadyToLaunch."; + return; + } + + this->mState = GreetdState::Launching; + this->mExitAfterLaunch = exit; + + this->sendRequest({ + {"type", "start_session"}, + {"cmd", QJsonArray::fromStringList(command)}, + {"env", QJsonArray::fromStringList(environment)}, + }); +} + +bool GreetdConnection::isAvailable() const { return this->mAvailable; } +GreetdState::Enum GreetdConnection::state() const { return this->mState; } + +void GreetdConnection::setActive(bool active) { + if (this->socket.state() == QLocalSocket::ConnectedState) { + this->mTargetActive = active; + if (active == (this->mState != GreetdState::Inactive)) return; + + if (active) { + if (this->mUser.isEmpty()) { + qCCritical(logGreetd) << "Cannot activate greetd with unset user."; + this->setActive(false); + return; + } + + this->sendRequest({ + {"type", "create_session"}, + {"username", this->mUser}, + }); + + this->mState = GreetdState::Authenticating; + emit this->stateChanged(); + } else { + this->sendRequest({ + {"type", "cancel_session"}, + }); + + this->setInactive(); + } + } else { + if (active != this->mTargetActive) { + this->mTargetActive = active; + } + } +} + +void GreetdConnection::setInactive() { + this->mTargetActive = false; + this->mResponseRequired = false; + this->mState = GreetdState::Inactive; + emit this->stateChanged(); +} + +QString GreetdConnection::user() const { return this->mUser; } + +void GreetdConnection::onSocketConnected() { + qCDebug(logGreetd) << "Connected to greetd socket."; + + if (this->mTargetActive) { + this->setActive(true); + } +} + +void GreetdConnection::onSocketError(QLocalSocket::LocalSocketError error) { + qCCritical(logGreetd) << "Greetd socket encountered an error and cannot continue:" << error; + + this->mAvailable = false; + this->setActive(false); +} + +void GreetdConnection::onSocketReady() { + qint32 length = 0; + + this->socket.read( + reinterpret_cast(&length), // NOLINT + sizeof(qint32) + ); + + auto text = this->socket.read(length); + auto json = QJsonDocument::fromJson(text).object(); + auto type = json.value("type").toString(); + + qCDebug(logGreetd).noquote() << "Received greetd response:" << text; + + if (type == "success") { + switch (this->mState) { + case GreetdState::Authenticating: + qCDebug(logGreetd) << "Authentication complete."; + this->mState = GreetdState::ReadyToLaunch; + emit this->stateChanged(); + emit this->readyToLaunch(); + break; + case GreetdState::Launching: + qCDebug(logGreetd) << "Target session set successfully."; + this->mState = GreetdState::Launched; + emit this->stateChanged(); + emit this->launched(); + + if (this->mExitAfterLaunch) { + qCDebug(logGreetd) << "Quitting."; + EngineGeneration::currentGeneration()->quit(); + } + + break; + default: goto unexpected; + } + } else if (type == "error") { + auto errorType = json.value("error_type").toString(); + auto desc = json.value("description").toString(); + + // Special case this error in case a session was already running. + // This cancels and restarts the session. + if (errorType == "error" && desc == "a session is already being configured") { + qCDebug(logGreetd + ) << "A session was already in progress, cancelling it and starting a new one."; + this->setActive(false); + this->setActive(true); + return; + } + + if (errorType == "auth_error") { + emit this->authFailure(desc); + this->setActive(false); + } else if (errorType == "error") { + qCWarning(logGreetd) << "Greetd error occurred" << desc; + emit this->error(desc); + } else goto unexpected; + + // errors terminate the session + this->setInactive(); + } else if (type == "auth_message") { + auto message = json.value("auth_message").toString(); + auto type = json.value("auth_message_type").toString(); + auto error = type == "error"; + auto responseRequired = type == "visible" || type == "secret"; + auto echoResponse = type != "secret"; + + this->mResponseRequired = responseRequired; + emit this->authMessage(message, error, responseRequired, echoResponse); + } else goto unexpected; + + return; +unexpected: + qCCritical(logGreetd) << "Received unexpected greetd response" << text; + this->setActive(false); +} + +void GreetdConnection::sendRequest(const QJsonObject& json) { + auto text = QJsonDocument(json).toJson(QJsonDocument::Compact); + auto length = static_cast(text.length()); + + if (logGreetd().isDebugEnabled()) { + auto debugJson = json; + + if (json.value("type").toString() == "post_auth_message_response") { + debugJson["response"] = ""; + } + + qCDebug(logGreetd).noquote() << "Sending greetd request:" + << QJsonDocument(debugJson).toJson(QJsonDocument::Compact); + } + + this->socket.write( + reinterpret_cast(&length), // NOLINT + sizeof(qint32) + ); + + this->socket.write(text); + this->socket.flush(); +} + +GreetdConnection* GreetdConnection::instance() { + static auto* instance = new GreetdConnection(); // NOLINT + return instance; +} diff --git a/src/services/greetd/connection.hpp b/src/services/greetd/connection.hpp new file mode 100644 index 00000000..76b75146 --- /dev/null +++ b/src/services/greetd/connection.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class GreetdState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum { + Inactive = 0, + Authenticating = 1, + ReadyToLaunch = 2, + Launching = 3, + Launched = 4, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(GreetdState::Enum value); +}; + +class GreetdConnection: public QObject { + Q_OBJECT; + +public: + void createSession(QString user); + void cancelSession(); + void respond(QString response); + void launch(const QList& command, const QList& environment, bool exit); + + [[nodiscard]] bool isAvailable() const; + [[nodiscard]] GreetdState::Enum state() const; + + [[nodiscard]] QString user() const; + + static GreetdConnection* instance(); + +signals: + void authMessage(QString message, bool error, bool responseRequired, bool echoResponse); + void authFailure(QString message); + void readyToLaunch(); + void launched(); + void error(QString error); + + void stateChanged(); + void userChanged(); + +private slots: + void onSocketConnected(); + void onSocketError(QLocalSocket::LocalSocketError error); + void onSocketReady(); + +private: + explicit GreetdConnection(); + + void sendRequest(const QJsonObject& json); + void setActive(bool active); + void setInactive(); + + bool mAvailable = false; + GreetdState::Enum mState = GreetdState::Inactive; + bool mTargetActive = false; + bool mExitAfterLaunch = false; + QString mMessage; + bool mResponseRequired = false; + QString mUser; + QLocalSocket socket; +}; diff --git a/src/services/greetd/module.md b/src/services/greetd/module.md new file mode 100644 index 00000000..a3ec540d --- /dev/null +++ b/src/services/greetd/module.md @@ -0,0 +1,7 @@ +name = "Quickshell.Services.Greetd" +description = "Greetd integration" +headers = [ + "qml.hpp", + "connection.hpp", +] +----- diff --git a/src/services/greetd/qml.cpp b/src/services/greetd/qml.cpp new file mode 100644 index 00000000..faebaa17 --- /dev/null +++ b/src/services/greetd/qml.cpp @@ -0,0 +1,46 @@ +#include "qml.hpp" +#include + +#include +#include + +#include "connection.hpp" + +Greetd::Greetd(QObject* parent): QObject(parent) { + auto* connection = GreetdConnection::instance(); + + QObject::connect(connection, &GreetdConnection::authMessage, this, &Greetd::authMessage); + QObject::connect(connection, &GreetdConnection::authFailure, this, &Greetd::authFailure); + QObject::connect(connection, &GreetdConnection::readyToLaunch, this, &Greetd::readyToLaunch); + QObject::connect(connection, &GreetdConnection::launched, this, &Greetd::launched); + QObject::connect(connection, &GreetdConnection::error, this, &Greetd::error); + + QObject::connect(connection, &GreetdConnection::stateChanged, this, &Greetd::stateChanged); + QObject::connect(connection, &GreetdConnection::userChanged, this, &Greetd::userChanged); +} + +void Greetd::createSession(QString user) { + GreetdConnection::instance()->createSession(std::move(user)); +} + +void Greetd::cancelSession() { GreetdConnection::instance()->cancelSession(); } + +void Greetd::respond(QString response) { + GreetdConnection::instance()->respond(std::move(response)); +} + +void Greetd::launch(const QList& command) { + GreetdConnection::instance()->launch(command, {}, true); +} + +void Greetd::launch(const QList& command, const QList& environment) { + GreetdConnection::instance()->launch(command, environment, true); +} + +void Greetd::launch(const QList& command, const QList& environment, bool quit) { + GreetdConnection::instance()->launch(command, environment, quit); +} + +bool Greetd::isAvailable() { return GreetdConnection::instance()->isAvailable(); } +GreetdState::Enum Greetd::state() { return GreetdConnection::instance()->state(); } +QString Greetd::user() { return GreetdConnection::instance()->user(); } diff --git a/src/services/greetd/qml.hpp b/src/services/greetd/qml.hpp new file mode 100644 index 00000000..b03d181e --- /dev/null +++ b/src/services/greetd/qml.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include + +#include "connection.hpp" + +/// This object provides access to a running greetd instance if present. +/// With it you can authenticate a user and launch a session. +/// +/// See [the greetd wiki] for instructions on how to set up a graphical greeter. +/// +/// [the greetd wiki]: https://man.sr.ht/~kennylevinsen/greetd/#setting-up-greetd-with-gtkgreet +class Greetd: public QObject { + Q_OBJECT; + /// If the greetd socket is available. + Q_PROPERTY(bool available READ isAvailable CONSTANT); + /// The current state of the greetd connection. + Q_PROPERTY(GreetdState::Enum state READ state NOTIFY stateChanged); + /// The currently authenticating user. + Q_PROPERTY(QString user READ user NOTIFY userChanged); + QML_ELEMENT; + QML_SINGLETON; + +public: + explicit Greetd(QObject* parent = nullptr); + + /// Create a greetd session for the given user. + Q_INVOKABLE static void createSession(QString user); + /// Cancel the active greetd session. + Q_INVOKABLE static void cancelSession(); + /// Respond to an authentication message. + /// + /// May only be called in response to an `authMessage` with responseRequired set to true. + Q_INVOKABLE static void respond(QString response); + + // docgen currently can't handle default params + + // clang-format off + /// Launch the session, exiting quickshell. + /// `readyToLaunch` must be true to call this function. + Q_INVOKABLE static void launch(const QList& command); + /// Launch the session, exiting quickshell. + /// `readyToLaunch` must be true to call this function. + Q_INVOKABLE static void launch(const QList& command, const QList& environment); + /// Launch the session, exiting quickshell if `quit` is true. + /// `readyToLaunch` must be true to call this function. + /// + /// The `launched` signal can be used to perform an action after greetd has acknowledged + /// the desired session. + /// + /// > [!WARNING] Note that greetd expects the greeter to terminate as soon as possible + /// > after setting a target session, and waiting too long may lead to unexpected behavior + /// > such as the greeter restarting. + /// > + /// > Performing animations and such should be done *before* calling `launch`. + Q_INVOKABLE static void launch(const QList& command, const QList& environment, bool quit); + // clang-format on + + [[nodiscard]] static bool isAvailable(); + [[nodiscard]] static GreetdState::Enum state(); + [[nodiscard]] static QString user(); + +signals: + /// An authentication message has been sent by greetd. + /// - `message` - the text of the message + /// - `error` - if the message should be displayed as an error + /// - `responseRequired` - if a response via `respond()` is required for this message + /// - `echoResponse` - if the response should be displayed in clear text to the user + /// + /// Note that `error` and `responseRequired` are mutually exclusive. + /// + /// Errors are sent through `authMessage` when they are recoverable, such as a fingerprint scanner + /// not being able to read a finger correctly, while definite failures such as a bad password are + /// sent through `authFailure`. + void authMessage(QString message, bool error, bool responseRequired, bool echoResponse); + /// Authentication has failed an the session has terminated. + /// + /// Usually this is something like a timeout or a failed password entry. + void authFailure(QString message); + /// Authentication has finished successfully and greetd can now launch a session. + void readyToLaunch(); + /// Greetd has acknowledged the launch request and the greeter should quit as soon as possible. + /// + /// This signal is sent right before quickshell exits automatically if the launch was not specifically + /// requested not to exit. You usually don't need to use this signal. + void launched(); + /// Greetd has encountered an error. + void error(QString error); + + void stateChanged(); + void userChanged(); +}; diff --git a/src/services/pam/CMakeLists.txt b/src/services/pam/CMakeLists.txt index 3de9b5d8..fef23578 100644 --- a/src/services/pam/CMakeLists.txt +++ b/src/services/pam/CMakeLists.txt @@ -1,11 +1,10 @@ -#find_package(PAM REQUIRED) - qt_add_library(quickshell-service-pam STATIC qml.cpp conversation.cpp ipc.cpp subprocess.cpp ) + qt_add_qml_module(quickshell-service-pam URI Quickshell.Services.Pam VERSION 0.1 From b6612bd56ce0db04c8106162edccce21406bd330 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 21 Jun 2024 10:11:57 -0700 Subject: [PATCH 057/305] core/panelwindow: remove QSDOC_HIDE for above and focusable props --- src/core/panelinterface.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/panelinterface.hpp b/src/core/panelinterface.hpp index e7ae0322..c3853122 100644 --- a/src/core/panelinterface.hpp +++ b/src/core/panelinterface.hpp @@ -118,9 +118,9 @@ class PanelWindowInterface: public WindowInterface { /// Defaults to `ExclusionMode.Auto`. Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged); /// If the panel should render above standard windows. Defaults to true. - QSDOC_HIDE Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY aboveWindowsChanged); + Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY aboveWindowsChanged); /// Defaults to false. - QSDOC_HIDE Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged); + Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged); // clang-format on QSDOC_NAMED_ELEMENT(PanelWindow); From c56a3ec9663db03125a126e49dd3c8ea6d85fedf Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 21 Jun 2024 16:31:02 -0700 Subject: [PATCH 058/305] service/mpris: add shorthand for playback state changes --- src/services/mpris/player.cpp | 24 ++++++++++++++++++++++++ src/services/mpris/player.hpp | 14 ++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index b659badf..31ef5c60 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -71,6 +71,10 @@ MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(paren QObject::connect(&this->pCanGoPrevious, &AbstractDBusProperty::changed, this, &MprisPlayer::canGoPreviousChanged); QObject::connect(&this->pCanPlay, &AbstractDBusProperty::changed, this, &MprisPlayer::canPlayChanged); QObject::connect(&this->pCanPause, &AbstractDBusProperty::changed, this, &MprisPlayer::canPauseChanged); + + QObject::connect(&this->pCanPlay, &AbstractDBusProperty::changed, this, &MprisPlayer::canTogglePlayingChanged); + QObject::connect(&this->pCanPause, &AbstractDBusProperty::changed, this, &MprisPlayer::canTogglePlayingChanged); + QObject::connect(&this->pPosition, &AbstractDBusProperty::changed, this, &MprisPlayer::onPositionChanged); QObject::connect(this->player, &DBusMprisPlayer::Seeked, this, &MprisPlayer::onSeek); QObject::connect(&this->pVolume, &AbstractDBusProperty::changed, this, &MprisPlayer::volumeChanged); @@ -148,6 +152,12 @@ QString MprisPlayer::address() const { return this->player->service(); } bool MprisPlayer::canControl() const { return this->pCanControl.get(); } bool MprisPlayer::canPlay() const { return this->canControl() && this->pCanPlay.get(); } bool MprisPlayer::canPause() const { return this->canControl() && this->pCanPause.get(); } + +bool MprisPlayer::canTogglePlaying() const { + if (this->mPlaybackState == MprisPlaybackState::Playing) return this->canPlay(); + else return this->canPause(); +} + bool MprisPlayer::canSeek() const { return this->canControl() && this->pCanSeek.get(); } bool MprisPlayer::canGoNext() const { return this->canControl() && this->pCanGoNext.get(); } bool MprisPlayer::canGoPrevious() const { return this->canControl() && this->pCanGoPrevious.get(); } @@ -327,6 +337,20 @@ void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) { } } +void MprisPlayer::play() { this->setPlaybackState(MprisPlaybackState::Playing); } + +void MprisPlayer::pause() { this->setPlaybackState(MprisPlaybackState::Paused); } + +void MprisPlayer::stop() { this->setPlaybackState(MprisPlaybackState::Stopped); } + +void MprisPlayer::togglePlaying() { + if (this->mPlaybackState == MprisPlaybackState::Playing) { + this->pause(); + } else { + this->play(); + } +} + void MprisPlayer::onPlaybackStatusChanged() { const auto& status = this->pPlaybackStatus.get(); diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 1172505a..94062bcc 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -60,6 +60,7 @@ class MprisPlayer: public QObject { Q_PROPERTY(bool canControl READ canControl NOTIFY canControlChanged); Q_PROPERTY(bool canPlay READ canPlay NOTIFY canPlayChanged); Q_PROPERTY(bool canPause READ canPause NOTIFY canPauseChanged); + Q_PROPERTY(bool canTogglePlaying READ canTogglePlaying NOTIFY canTogglePlayingChanged); Q_PROPERTY(bool canSeek READ canSeek NOTIFY canSeekChanged); Q_PROPERTY(bool canGoNext READ canGoNext NOTIFY canGoNextChanged); Q_PROPERTY(bool canGoPrevious READ canGoPrevious NOTIFY canGoPreviousChanged); @@ -191,6 +192,17 @@ public: /// /// May only be called if `canSeek` is true. Q_INVOKABLE void seek(qreal offset); + /// Equivalent to setting `playbackState` to `Playing`. + Q_INVOKABLE void play(); + /// Equivalent to setting `playbackState` to `Paused`. + Q_INVOKABLE void pause(); + /// Equivalent to setting `playbackState` to `Stopped`. + Q_INVOKABLE void stop(); + /// Equivalent to calling `play()` if not playing or `pause()` if playing. + /// + /// May only be called if `canTogglePlaying` is true, which is equivalent to + /// `canPlay` or `canPause` depending on the current playback state. + Q_INVOKABLE void togglePlaying(); [[nodiscard]] bool isValid() const; [[nodiscard]] QString address() const; @@ -201,6 +213,7 @@ public: [[nodiscard]] bool canGoPrevious() const; [[nodiscard]] bool canPlay() const; [[nodiscard]] bool canPause() const; + [[nodiscard]] bool canTogglePlaying() const; [[nodiscard]] bool canQuit() const; [[nodiscard]] bool canRaise() const; [[nodiscard]] bool canSetFullscreen() const; @@ -251,6 +264,7 @@ signals: void canControlChanged(); void canPlayChanged(); void canPauseChanged(); + void canTogglePlayingChanged(); void canSeekChanged(); void canGoNextChanged(); void canGoPreviousChanged(); From d8fa9e7bb393212ea40dc3332ef2e0568052b630 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 21 Jun 2024 19:00:04 -0700 Subject: [PATCH 059/305] service/mpris: add properties for common track metadata This was done to work around bad player implementations sending weird metadata, such as removing the art url halfway through a song. --- src/services/mpris/player.cpp | 84 +++++++++++++++++++++++++++++++++-- src/services/mpris/player.hpp | 37 ++++++++++++++- 2 files changed, 116 insertions(+), 5 deletions(-) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 31ef5c60..3c221c25 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -1,4 +1,5 @@ #include "player.hpp" +#include #include #include @@ -255,6 +256,11 @@ void MprisPlayer::setVolume(qreal volume) { } QVariantMap MprisPlayer::metadata() const { return this->pMetadata.get(); } +QString MprisPlayer::trackTitle() const { return this->mTrackTitle; } +QString MprisPlayer::trackAlbum() const { return this->mTrackAlbum; } +QString MprisPlayer::trackAlbumArtist() const { return this->mTrackAlbumArtist; } +QVector MprisPlayer::trackArtists() const { return this->mTrackArtists; } +QString MprisPlayer::trackArtUrl() const { return this->mTrackArtUrl; } void MprisPlayer::onMetadataChanged() { emit this->metadataChanged(); @@ -274,7 +280,7 @@ void MprisPlayer::onMetadataChanged() { auto trackidVariant = this->pMetadata.get().value("mpris:trackid"); if (trackidVariant.isValid() && trackidVariant.canConvert()) { - auto trackId = trackidVariant.value(); + auto trackId = trackidVariant.toString(); if (trackId != this->mTrackId) { this->mTrackId = trackId; @@ -285,14 +291,49 @@ void MprisPlayer::onMetadataChanged() { // Helps to catch players without trackid. auto urlVariant = this->pMetadata.get().value("xesam:url"); if (urlVariant.isValid() && urlVariant.canConvert()) { - auto url = urlVariant.value(); + auto url = urlVariant.toString(); - if (url != this->mUrl) { - this->mUrl = url; + if (url != this->mTrackUrl) { + this->mTrackUrl = url; trackChanged = true; } } + auto trackTitle = this->pMetadata.get().value("xesam:title"); + if (trackTitle.isValid() && trackTitle.canConvert()) { + this->setTrackTitle(trackTitle.toString()); + } else if (trackChanged) { + this->setTrackTitle("Unknown Track"); + } + + auto trackAlbum = this->pMetadata.get().value("xesam:album"); + if (trackAlbum.isValid() && trackAlbum.canConvert()) { + this->setTrackAlbum(trackAlbum.toString()); + } else if (trackChanged) { + this->setTrackAlbum("Unknown Album"); + } + + auto trackAlbumArtist = this->pMetadata.get().value("xesam:albumArtist"); + if (trackAlbumArtist.isValid() && trackAlbumArtist.canConvert()) { + this->setTrackAlbumArtist(trackAlbumArtist.toString()); + } else if (trackChanged) { + this->setTrackAlbumArtist("Unknown Artist"); + } + + auto trackArtists = this->pMetadata.get().value("xesam:artist"); + if (trackArtists.isValid() && trackArtists.canConvert>()) { + this->setTrackArtists(trackArtists.value>()); + } else if (trackChanged) { + this->setTrackArtists({}); + } + + auto trackArtUrl = this->pMetadata.get().value("mpris:artUrl"); + if (trackArtUrl.isValid() && trackArtUrl.canConvert()) { + this->setTrackArtUrl(trackArtUrl.toString()); + } else if (trackChanged) { + this->setTrackArtUrl(""); + } + if (trackChanged) { // Some players don't seem to send position updates or seeks on track change. this->pPosition.update(); @@ -300,6 +341,41 @@ void MprisPlayer::onMetadataChanged() { } } +void MprisPlayer::setTrackTitle(QString title) { + if (title == this->mTrackTitle) return; + + this->mTrackTitle = std::move(title); + emit this->trackTitleChanged(); +} + +void MprisPlayer::setTrackAlbum(QString album) { + if (album == this->mTrackAlbum) return; + + this->mTrackAlbum = std::move(album); + emit this->trackAlbumChanged(); +} + +void MprisPlayer::setTrackAlbumArtist(QString albumArtist) { + if (albumArtist == this->mTrackAlbumArtist) return; + + this->mTrackAlbumArtist = std::move(albumArtist); + emit this->trackAlbumArtistChanged(); +} + +void MprisPlayer::setTrackArtists(QVector artists) { + if (artists == this->mTrackArtists) return; + + this->mTrackArtists = std::move(artists); + emit this->trackArtistsChanged(); +} + +void MprisPlayer::setTrackArtUrl(QString artUrl) { + if (artUrl == this->mTrackArtUrl) return; + + this->mTrackArtUrl = std::move(artUrl); + emit this->trackArtUrlChanged(); +} + MprisPlaybackState::Enum MprisPlayer::playbackState() const { return this->mPlaybackState; } void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) { diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 94062bcc..5de69091 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -122,7 +122,21 @@ class MprisPlayer: public QObject { /// /// A map of common properties is available [here](https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata). /// Do not count on any of them actually being present. + /// + /// Note that the `trackTitle`, `trackAlbum`, `trackAlbumArtist`, `trackArtists` and `trackArtUrl` + /// properties have extra logic to guard against bad players sending weird metadata, and should + /// be used over grabbing the properties directly from the metadata. Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged); + /// The title of the current track, or "Unknown Track" if none was provided. + Q_PROPERTY(QString trackTitle READ trackTitle NOTIFY trackTitleChanged); + /// The current track's album, or "Unknown Album" if none was provided. + Q_PROPERTY(QString trackAlbum READ trackAlbum NOTIFY trackAlbumChanged); + /// The current track's album artist, or "Unknown Artist" if none was provided. + Q_PROPERTY(QString trackAlbumArtist READ trackAlbumArtist NOTIFY trackAlbumArtistChanged); + /// The current track's artists, or an empty list if none were provided. + Q_PROPERTY(QVector trackArtists READ trackArtists NOTIFY trackArtistsChanged); + /// The current track's art url, or `""` if none was provided. + Q_PROPERTY(QString trackArtUrl READ trackArtUrl NOTIFY trackArtUrlChanged); /// The playback state of the media player. /// /// - If `canPlay` is false, you cannot assign the `Playing` state. @@ -234,6 +248,11 @@ public: void setVolume(qreal volume); [[nodiscard]] QVariantMap metadata() const; + [[nodiscard]] QString trackTitle() const; + [[nodiscard]] QString trackAlbum() const; + [[nodiscard]] QString trackAlbumArtist() const; + [[nodiscard]] QVector trackArtists() const; + [[nodiscard]] QString trackArtUrl() const; [[nodiscard]] MprisPlaybackState::Enum playbackState() const; void setPlaybackState(MprisPlaybackState::Enum playbackState); @@ -280,6 +299,11 @@ signals: void volumeChanged(); void volumeSupportedChanged(); void metadataChanged(); + void trackTitleChanged(); + void trackAlbumChanged(); + void trackAlbumArtistChanged(); + void trackArtistsChanged(); + void trackArtUrlChanged(); void playbackStateChanged(); void loopStateChanged(); void loopSupportedChanged(); @@ -302,6 +326,12 @@ private slots: void onLoopStatusChanged(); private: + void setTrackTitle(QString title); + void setTrackAlbum(QString album); + void setTrackAlbumArtist(QString albumArtist); + void setTrackArtists(QVector artists); + void setTrackArtUrl(QString artUrl); + // clang-format off dbus::DBusPropertyGroup appProperties; dbus::DBusProperty pIdentity {this->appProperties, "Identity"}; @@ -340,7 +370,12 @@ private: DBusMprisPlayerApp* app = nullptr; DBusMprisPlayer* player = nullptr; QString mTrackId; - QString mUrl; + QString mTrackUrl; + QString mTrackTitle; + QString mTrackAlbum; + QString mTrackAlbumArtist; + QVector mTrackArtists; + QString mTrackArtUrl; }; } // namespace qs::service::mpris From 09d8a7a07d9d431e1e2e41f574a1079f0341ffe5 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 22 Jun 2024 01:57:48 -0700 Subject: [PATCH 060/305] core/objectrepeater: add ObjectRepeater --- src/core/CMakeLists.txt | 1 + src/core/module.md | 1 + src/core/objectrepeater.cpp | 191 ++++++++++++++++++++++++++++++++++++ src/core/objectrepeater.hpp | 86 ++++++++++++++++ 4 files changed, 279 insertions(+) create mode 100644 src/core/objectrepeater.cpp create mode 100644 src/core/objectrepeater.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b76c7aab..3d912459 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -29,6 +29,7 @@ qt_add_library(quickshell-core STATIC model.cpp elapsedtimer.cpp desktopentry.cpp + objectrepeater.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/module.md b/src/core/module.md index 315eb25f..73ede34a 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -21,5 +21,6 @@ headers = [ "model.hpp", "elapsedtimer.hpp", "desktopentry.hpp", + "objectrepeater.hpp", ] ----- diff --git a/src/core/objectrepeater.cpp b/src/core/objectrepeater.cpp new file mode 100644 index 00000000..014a5e29 --- /dev/null +++ b/src/core/objectrepeater.cpp @@ -0,0 +1,191 @@ +#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::rowsAboutToBeRemoved, this, &ObjectRepeater::onModelRowsAboutToBeRemoved); + 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::onModelRowsAboutToBeRemoved(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->onModelRowsAboutToBeRemoved(sourceParent, sourceStart, sourceEnd); + } + + if (hasDest) { + this->onModelRowsInserted(destParent, destStart, destStart + (sourceEnd - sourceStart)); + } +} + +void ObjectRepeater::onModelAboutToBeReset() { + auto last = static_cast(this->valuesList.length() - 1); + this->onModelRowsAboutToBeRemoved(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); + delete instance; + + this->removeAt(index); +} diff --git a/src/core/objectrepeater.hpp b/src/core/objectrepeater.hpp new file mode 100644 index 00000000..891666d3 --- /dev/null +++ b/src/core/objectrepeater.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "model.hpp" + +///! A Repeater / for loop / map for non Item derived objects. +/// The ObjectRepeater creates instances of the provided delegate for every entry in the +/// given model, similarly to a [Repeater] but for non visual types. +/// +/// [Repeater]: https://doc.qt.io/qt-6/qml-qtquick-repeater.html +class ObjectRepeater: public ObjectModel { + Q_OBJECT; + /// The model providing data to the ObjectRepeater. + /// + /// Currently accepted model types are QML `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 + /// [ObjectModel]: ../objectmodel + 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 + /// [ObjectModel]: ../objectmodel + Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged); + Q_CLASSINFO("DefaultProperty", "delegate"); + QML_ELEMENT; + +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 onModelRowsAboutToBeRemoved(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; +}; From c78c86425dd1691579f2873bd351d3e31eb76097 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 23 Jun 2024 03:18:27 -0700 Subject: [PATCH 061/305] core/objectrepeater: delete delegate instances after removal --- src/core/objectrepeater.cpp | 11 +++++------ src/core/objectrepeater.hpp | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/core/objectrepeater.cpp b/src/core/objectrepeater.cpp index 014a5e29..7971952c 100644 --- a/src/core/objectrepeater.cpp +++ b/src/core/objectrepeater.cpp @@ -81,7 +81,7 @@ void ObjectRepeater::reloadElements() { // clang-format off QObject::connect(model, &QObject::destroyed, this, &ObjectRepeater::onModelDestroyed); QObject::connect(model, &QAbstractItemModel::rowsInserted, this, &ObjectRepeater::onModelRowsInserted); - QObject::connect(model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ObjectRepeater::onModelRowsAboutToBeRemoved); + 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 @@ -134,7 +134,7 @@ void ObjectRepeater::onModelRowsInserted(const QModelIndex& parent, int first, i this->insertModelElements(this->itemModel, first, last); } -void ObjectRepeater::onModelRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) { +void ObjectRepeater::onModelRowsRemoved(const QModelIndex& parent, int first, int last) { if (parent != QModelIndex()) return; for (auto i = last; i != first - 1; i--) { @@ -155,7 +155,7 @@ void ObjectRepeater::onModelRowsMoved( if (!hasSource && !hasDest) return; if (hasSource) { - this->onModelRowsAboutToBeRemoved(sourceParent, sourceStart, sourceEnd); + this->onModelRowsRemoved(sourceParent, sourceStart, sourceEnd); } if (hasDest) { @@ -165,7 +165,7 @@ void ObjectRepeater::onModelRowsMoved( void ObjectRepeater::onModelAboutToBeReset() { auto last = static_cast(this->valuesList.length() - 1); - this->onModelRowsAboutToBeRemoved(QModelIndex(), 0, last); // -1 is fine + this->onModelRowsRemoved(QModelIndex(), 0, last); // -1 is fine } void ObjectRepeater::insertComponent(qsizetype index, const QVariantMap& properties) { @@ -185,7 +185,6 @@ void ObjectRepeater::insertComponent(qsizetype index, const QVariantMap& propert void ObjectRepeater::removeComponent(qsizetype index) { auto* instance = this->valuesList.at(index); - delete instance; - this->removeAt(index); + delete instance; } diff --git a/src/core/objectrepeater.hpp b/src/core/objectrepeater.hpp index 891666d3..d1c482cc 100644 --- a/src/core/objectrepeater.hpp +++ b/src/core/objectrepeater.hpp @@ -62,7 +62,7 @@ private slots: void onDelegateDestroyed(); void onModelDestroyed(); void onModelRowsInserted(const QModelIndex& parent, int first, int last); - void onModelRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last); + void onModelRowsRemoved(const QModelIndex& parent, int first, int last); void onModelRowsMoved( const QModelIndex& sourceParent, From d7149d564117e3cbb8075ecdc39b073b8f11d32e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 23 Jun 2024 14:05:34 -0700 Subject: [PATCH 062/305] core/objectrepeater: soft-remove in favor of Instantiator RIP my time. --- src/core/objectrepeater.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/objectrepeater.hpp b/src/core/objectrepeater.hpp index d1c482cc..0361636f 100644 --- a/src/core/objectrepeater.hpp +++ b/src/core/objectrepeater.hpp @@ -11,6 +11,8 @@ #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 [Repeater] but for non visual types. /// @@ -44,6 +46,7 @@ class ObjectRepeater: public ObjectModel { 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) {} From 8547d123965c532b1e8e7a6178e2e18d268799d3 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 27 Jun 2024 20:45:27 -0700 Subject: [PATCH 063/305] service/pipewire: make binding warnings in docs more obvious --- src/services/pipewire/module.md | 2 +- src/services/pipewire/qml.hpp | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/services/pipewire/module.md b/src/services/pipewire/module.md index e10809ef..d109f05a 100644 --- a/src/services/pipewire/module.md +++ b/src/services/pipewire/module.md @@ -1,4 +1,4 @@ -name = "Quickshell.Services.PipeWire" +name = "Quickshell.Services.Pipewire" description = "Pipewire API" headers = [ "qml.hpp", diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 8d456419..78e75633 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -140,22 +140,22 @@ class PwNodeAudioIface: public QObject { Q_OBJECT; /// If the node is currently muted. Setting this property changes the mute state. /// - /// **This property is invalid unless the node is [bound](../pwobjecttracker).** + /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged); /// The average volume over all channels of the node. /// Setting this property modifies the volume of all channels proportionately. /// - /// **This property is invalid unless the node is [bound](../pwobjecttracker).** + /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). Q_PROPERTY(float volume READ averageVolume WRITE setAverageVolume NOTIFY volumesChanged); /// The audio channels present on the node. /// - /// **This property is invalid unless the node is [bound](../pwobjecttracker).** + /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). Q_PROPERTY(QVector channels READ channels NOTIFY channelsChanged); /// The volumes of each audio channel individually. Each entry corrosponds to /// the channel at the same index in `channels`. `volumes` and `channels` will always be /// the same length. /// - /// **This property is invalid unless the node is [bound](../pwobjecttracker).** + /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). Q_PROPERTY(QVector volumes READ volumes WRITE setVolumes NOTIFY volumesChanged); QML_NAMED_ELEMENT(PwNodeAudio); QML_UNCREATABLE("PwNodeAudio cannot be created directly"); @@ -217,7 +217,7 @@ class PwNodeIface: public PwObjectIface { /// - `media.title` - The title of the currently playing media. /// - `media.artist` - The artist of the currently playing media. /// - /// **This property is invalid unless the node is [bound](../pwobjecttracker).** + /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). Q_PROPERTY(QVariantMap properties READ properties NOTIFY propertiesChanged); /// Extra information present only if the node sends or receives audio. Q_PROPERTY(PwNodeAudioIface* audio READ audio CONSTANT); @@ -263,7 +263,7 @@ class PwLinkIface: public PwObjectIface { Q_PROPERTY(PwNodeIface* source READ source CONSTANT); /// The current state of the link. /// - /// **This property is invalid unless the link is [bound](../pwobjecttracker).** + /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). Q_PROPERTY(PwLinkState::Enum state READ state NOTIFY stateChanged); QML_NAMED_ELEMENT(PwLink); QML_UNCREATABLE("PwLinks cannot be created directly"); @@ -298,7 +298,7 @@ class PwLinkGroupIface Q_PROPERTY(PwNodeIface* source READ source CONSTANT); /// The current state of the link group. /// - /// **This property is invalid unless the link is [bound](../pwobjecttracker).** + /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). Q_PROPERTY(PwLinkState::Enum state READ state NOTIFY stateChanged); QML_NAMED_ELEMENT(PwLinkGroup); QML_UNCREATABLE("PwLinkGroups cannot be created directly"); From d8b900ed0b6c3a52a9fee121580e2904d0969ab6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 28 Jun 2024 01:05:59 -0700 Subject: [PATCH 064/305] lint: allow implicit bool conversions --- .clang-tidy | 1 + 1 file changed, 1 insertion(+) diff --git a/.clang-tidy b/.clang-tidy index 19c3547c..14e9b9ae 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -40,6 +40,7 @@ Checks: > -readability-redundant-access-specifiers, -readability-else-after-return, -readability-container-data-pointer, + -readability-implicit-bool-conversion, tidyfox-*, CheckOptions: performance-for-range-copy.WarnOnAllAutoCopies: true From c31bbea837ba0396940f3dba4a75f0666bbec742 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 1 Jul 2024 20:50:07 -0700 Subject: [PATCH 065/305] docs: add breaking change notice --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 99c3a60f..0cc3d1b1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # quickshell -Simple and flexbile QtQuick based desktop shell toolkit. +Flexbile QtQuick based desktop shell toolkit. Hosted on: [outfoxxed's gitea], [github] @@ -14,6 +14,12 @@ can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quick Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples) repo. +# Breaking Changes +Quickshell is still in alpha and there will be breaking changes. + +Commits with breaking qml api changes will contain a `!` at the end of the scope +(`thing!: foo`) and the commit description will contain details about the broken api. + # Installation ## Nix From ec362637b879baa817e4df7c96e27adef35b89a0 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 1 Jul 2024 20:50:30 -0700 Subject: [PATCH 066/305] service/tray!: redesign menus / dbusmenu and add native menu support Reworks dbusmenu menus to be displayable with a system context menu. Breaks the entire DBusMenu api. --- src/core/CMakeLists.txt | 2 + src/core/generation.cpp | 89 +++++++++ src/core/generation.hpp | 4 + src/core/module.md | 1 + src/core/platformmenu.cpp | 276 ++++++++++++++++++++++++++ src/core/platformmenu.hpp | 72 +++++++ src/core/qsmenu.cpp | 95 +++++++++ src/core/qsmenu.hpp | 140 +++++++++++++ src/dbus/dbusmenu/dbusmenu.cpp | 112 +++++------ src/dbus/dbusmenu/dbusmenu.hpp | 135 +++---------- src/services/status_notifier/item.cpp | 39 +++- src/services/status_notifier/item.hpp | 11 +- src/services/status_notifier/qml.cpp | 54 +++-- src/services/status_notifier/qml.hpp | 17 +- src/wayland/CMakeLists.txt | 4 +- src/wayland/init.cpp | 3 + src/wayland/platformmenu.cpp | 32 +++ src/wayland/platformmenu.hpp | 3 + 18 files changed, 898 insertions(+), 191 deletions(-) create mode 100644 src/core/platformmenu.cpp create mode 100644 src/core/platformmenu.hpp create mode 100644 src/core/qsmenu.cpp create mode 100644 src/core/qsmenu.hpp create mode 100644 src/wayland/platformmenu.cpp create mode 100644 src/wayland/platformmenu.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 3d912459..a7ed6d1e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -30,6 +30,8 @@ qt_add_library(quickshell-core STATIC elapsedtimer.cpp desktopentry.cpp objectrepeater.cpp + platformmenu.cpp + qsmenu.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 2ca7a77b..5f21a19a 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -8,12 +8,17 @@ #include #include #include +#include +#include #include #include #include +#include #include #include #include +#include +#include #include #include "iconimageprovider.hpp" @@ -308,6 +313,90 @@ EngineGeneration* EngineGeneration::currentGeneration() { } else return nullptr; } +// QMenu re-calls pixmap() every time the mouse moves so its important to cache it. +class PixmapCacheIconEngine: public QIconEngine { + void paint( + QPainter* /*unused*/, + const QRect& /*unused*/, + QIcon::Mode /*unused*/, + QIcon::State /*unused*/ + ) override { + 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 { + if (this->lastPixmap.isNull() || size != this->lastSize) { + this->lastPixmap = this->createPixmap(size); + this->lastSize = size; + } + + return this->lastPixmap; + } + + virtual QPixmap createPixmap(const QSize& size) = 0; + +private: + QSize lastSize; + QPixmap lastPixmap; +}; + +class ImageProviderIconEngine: public PixmapCacheIconEngine { +public: + explicit ImageProviderIconEngine(QQuickImageProvider* provider, QString id) + : provider(provider) + , id(std::move(id)) {} + + QPixmap createPixmap(const QSize& size) override { + if (this->provider->imageType() == QQmlImageProviderBase::Pixmap) { + return this->provider->requestPixmap(this->id, nullptr, size); + } else if (this->provider->imageType() == QQmlImageProviderBase::Image) { + auto image = this->provider->requestImage(this->id, nullptr, size); + return QPixmap::fromImage(image); + } else { + qFatal() << "Unexpected ImageProviderIconEngine image type" << this->provider->imageType(); + return QPixmap(); // never reached, satisfies lint + } + } + + [[nodiscard]] QIconEngine* clone() const override { + return new ImageProviderIconEngine(this->provider, this->id); + } + +private: + QQuickImageProvider* provider; + QString id; +}; + +QIcon EngineGeneration::iconByUrl(const QUrl& url) const { + if (url.isEmpty()) return QIcon(); + + auto scheme = url.scheme(); + if (scheme == "image") { + auto providerName = url.authority(); + auto path = url.path(); + if (!path.isEmpty()) path = path.sliced(1); + + auto* provider = qobject_cast(this->engine->imageProvider(providerName)); + + if (provider == nullptr) { + qWarning() << "iconByUrl failed: no provider found for" << url; + return QIcon(); + } + + if (provider->imageType() == QQmlImageProviderBase::Pixmap + || provider->imageType() == QQmlImageProviderBase::Image) + { + return QIcon(new ImageProviderIconEngine(provider, path)); + } + + } else { + qWarning() << "iconByUrl failed: unsupported scheme" << scheme << "in path" << url; + } + + return QIcon(); +} + EngineGeneration* EngineGeneration::findEngineGeneration(QQmlEngine* engine) { return g_generations.value(engine); } diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 9bcb8b60..54863752 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -3,11 +3,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include "incubator.hpp" #include "qsintercept.hpp" @@ -40,6 +42,8 @@ public: // otherwise null. static EngineGeneration* currentGeneration(); + [[nodiscard]] QIcon iconByUrl(const QUrl& url) const; + RootWrapper* wrapper = nullptr; QDir rootPath; QmlScanner scanner; diff --git a/src/core/module.md b/src/core/module.md index 73ede34a..c70b4876 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -22,5 +22,6 @@ headers = [ "elapsedtimer.hpp", "desktopentry.hpp", "objectrepeater.hpp", + "qsmenu.hpp" ] ----- diff --git a/src/core/platformmenu.cpp b/src/core/platformmenu.cpp new file mode 100644 index 00000000..a2f8f813 --- /dev/null +++ b/src/core/platformmenu.cpp @@ -0,0 +1,276 @@ +#include "platformmenu.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "generation.hpp" +#include "proxywindow.hpp" +#include "qsmenu.hpp" +#include "windowinterface.hpp" + +namespace qs::menu::platform { + +namespace { +QVector> CREATION_HOOKS; // NOLINT +PlatformMenuQMenu* ACTIVE_MENU = nullptr; // NOLINT +} // namespace + +PlatformMenuQMenu::~PlatformMenuQMenu() { + if (this == ACTIVE_MENU) { + ACTIVE_MENU = nullptr; + } +} + +void PlatformMenuQMenu::setVisible(bool visible) { + if (visible) { + for (auto& hook: CREATION_HOOKS) { + hook(this); + } + } else { + if (this == ACTIVE_MENU) { + ACTIVE_MENU = nullptr; + } + } + + this->QMenu::setVisible(visible); +} + +PlatformMenuEntry::PlatformMenuEntry(QsMenuEntry* menu): QObject(menu), menu(menu) { + this->relayout(); + + // clang-format off + QObject::connect(menu, &QsMenuEntry::enabledChanged, this, &PlatformMenuEntry::onEnabledChanged); + QObject::connect(menu, &QsMenuEntry::textChanged, this, &PlatformMenuEntry::onTextChanged); + QObject::connect(menu, &QsMenuEntry::iconChanged, this, &PlatformMenuEntry::onIconChanged); + QObject::connect(menu, &QsMenuEntry::buttonTypeChanged, this, &PlatformMenuEntry::onButtonTypeChanged); + QObject::connect(menu, &QsMenuEntry::checkStateChanged, this, &PlatformMenuEntry::onCheckStateChanged); + QObject::connect(menu, &QsMenuEntry::hasChildrenChanged, this, &PlatformMenuEntry::relayoutParent); + // clang-format on +} + +PlatformMenuEntry::~PlatformMenuEntry() { + this->clearChildren(); + delete this->qaction; + delete this->qmenu; +} + +void PlatformMenuEntry::registerCreationHook(std::function hook) { + CREATION_HOOKS.push_back(std::move(hook)); +} + +bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) { + QWindow* window = nullptr; + + if (this->qmenu == nullptr) { + qCritical() << "Cannot display PlatformMenuEntry as it is not a menu."; + return false; + } else if (parentWindow == nullptr) { + qCritical() << "Cannot display PlatformMenuEntry with null parent window."; + return false; + } else if (auto* proxy = qobject_cast(parentWindow)) { + window = proxy->backingWindow(); + } else if (auto* interface = qobject_cast(parentWindow)) { + window = interface->proxyWindow()->backingWindow(); + } else { + qCritical() << "PlatformMenuEntry.display() must be called with a window."; + return false; + } + + if (window == nullptr) { + qCritical() << "Cannot display PlatformMenuEntry from a parent window that is not visible."; + return false; + } + + if (ACTIVE_MENU && this->qmenu != ACTIVE_MENU) { + ACTIVE_MENU->close(); + } + + ACTIVE_MENU = this->qmenu; + + auto point = window->mapToGlobal(QPoint(relativeX, relativeY)); + + this->qmenu->createWinId(); + this->qmenu->windowHandle()->setTransientParent(window); + + this->qmenu->popup(point); + + return true; +} + +void PlatformMenuEntry::relayout() { + if (this->menu->hasChildren()) { + delete this->qaction; + this->qaction = nullptr; + + if (this->qmenu == nullptr) { + this->qmenu = new PlatformMenuQMenu(); + QObject::connect(this->qmenu, &QMenu::aboutToShow, this, &PlatformMenuEntry::onAboutToShow); + QObject::connect(this->qmenu, &QMenu::aboutToHide, this, &PlatformMenuEntry::onAboutToHide); + } else { + this->clearChildren(); + } + + this->qmenu->setTitle(this->menu->text()); + + auto icon = this->menu->icon(); + if (!icon.isEmpty()) { + auto* generation = EngineGeneration::currentGeneration(); + this->qmenu->setIcon(generation->iconByUrl(this->menu->icon())); + } + + auto children = this->menu->children(); + auto len = children.count(&children); + for (auto i = 0; i < len; i++) { + auto* child = children.at(&children, i); + + auto* instance = new PlatformMenuEntry(child); + QObject::connect(instance, &QObject::destroyed, this, &PlatformMenuEntry::onChildDestroyed); + + QObject::connect( + instance, + &PlatformMenuEntry::relayoutParent, + this, + &PlatformMenuEntry::relayout + ); + + this->childEntries.push_back(instance); + instance->addToQMenu(this->qmenu); + } + } else if (!this->menu->isSeparator()) { + this->clearChildren(); + delete this->qmenu; + this->qmenu = nullptr; + + if (this->qaction == nullptr) { + this->qaction = new QAction(this); + + QObject::connect( + this->qaction, + &QAction::triggered, + this, + &PlatformMenuEntry::onActionTriggered + ); + } + + this->qaction->setText(this->menu->text()); + + auto icon = this->menu->icon(); + if (!icon.isEmpty()) { + auto* generation = EngineGeneration::currentGeneration(); + this->qaction->setIcon(generation->iconByUrl(this->menu->icon())); + } + + this->qaction->setEnabled(this->menu->enabled()); + this->qaction->setCheckable(this->menu->buttonType() != QsMenuButtonType::None); + + if (this->menu->buttonType() == QsMenuButtonType::RadioButton) { + if (!this->qactiongroup) this->qactiongroup = new QActionGroup(this); + this->qaction->setActionGroup(this->qactiongroup); + } + + this->qaction->setChecked(this->menu->checkState() != Qt::Unchecked); + } else { + delete this->qmenu; + delete this->qaction; + this->qmenu = nullptr; + this->qaction = nullptr; + } +} + +void PlatformMenuEntry::onAboutToShow() { this->menu->ref(); } + +void PlatformMenuEntry::onAboutToHide() { + this->menu->unref(); + emit this->closed(); +} + +void PlatformMenuEntry::onActionTriggered() { + auto* action = qobject_cast(this->sender()->parent()); + emit action->menu->triggered(); +} + +void PlatformMenuEntry::onChildDestroyed() { this->childEntries.removeOne(this->sender()); } + +void PlatformMenuEntry::onEnabledChanged() { + if (this->qaction != nullptr) { + this->qaction->setEnabled(this->menu->enabled()); + } +} + +void PlatformMenuEntry::onTextChanged() { + if (this->qmenu != nullptr) { + this->qmenu->setTitle(this->menu->text()); + } else if (this->qaction != nullptr) { + this->qaction->setText(this->menu->text()); + } +} + +void PlatformMenuEntry::onIconChanged() { + if (this->qmenu == nullptr && this->qaction == nullptr) return; + + auto iconName = this->menu->icon(); + QIcon icon; + + if (!iconName.isEmpty()) { + auto* generation = EngineGeneration::currentGeneration(); + icon = generation->iconByUrl(iconName); + } + + if (this->qmenu != nullptr) { + this->qmenu->setIcon(icon); + } else if (this->qaction != nullptr) { + this->qaction->setIcon(icon); + } +} + +void PlatformMenuEntry::onButtonTypeChanged() { + if (this->qaction != nullptr) { + QActionGroup* group = nullptr; + + if (this->menu->buttonType() == QsMenuButtonType::RadioButton) { + if (!this->qactiongroup) this->qactiongroup = new QActionGroup(this); + group = this->qactiongroup; + } + + this->qaction->setActionGroup(group); + } +} + +void PlatformMenuEntry::onCheckStateChanged() { + if (this->qaction != nullptr) { + this->qaction->setChecked(this->menu->checkState() != Qt::Unchecked); + } +} + +void PlatformMenuEntry::clearChildren() { + for (auto* child: this->childEntries) { + delete child; + } + + this->childEntries.clear(); +} + +void PlatformMenuEntry::addToQMenu(PlatformMenuQMenu* menu) { + if (this->qmenu != nullptr) { + menu->addMenu(this->qmenu); + this->qmenu->containingMenu = menu; + } else if (this->qaction != nullptr) { + menu->addAction(this->qaction); + } else { + menu->addSeparator(); + } +} + +} // namespace qs::menu::platform diff --git a/src/core/platformmenu.hpp b/src/core/platformmenu.hpp new file mode 100644 index 00000000..5c18a57e --- /dev/null +++ b/src/core/platformmenu.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "qsmenu.hpp" + +namespace qs::menu::platform { + +class PlatformMenuQMenu: public QMenu { +public: + explicit PlatformMenuQMenu() = default; + ~PlatformMenuQMenu() override; + Q_DISABLE_COPY_MOVE(PlatformMenuQMenu); + + void setVisible(bool visible) override; + + PlatformMenuQMenu* containingMenu = nullptr; +}; + +class PlatformMenuEntry: public QObject { + Q_OBJECT; + +public: + explicit PlatformMenuEntry(QsMenuEntry* menu); + ~PlatformMenuEntry() override; + Q_DISABLE_COPY_MOVE(PlatformMenuEntry); + + bool display(QObject* parentWindow, int relativeX, int relativeY); + + static void registerCreationHook(std::function hook); + +signals: + void closed(); + void relayoutParent(); + +public slots: + void relayout(); + +private slots: + void onAboutToShow(); + void onAboutToHide(); + void onActionTriggered(); + void onChildDestroyed(); + void onEnabledChanged(); + void onTextChanged(); + void onIconChanged(); + void onButtonTypeChanged(); + void onCheckStateChanged(); + +private: + void clearChildren(); + void addToQMenu(PlatformMenuQMenu* menu); + + QsMenuEntry* menu; + PlatformMenuQMenu* qmenu = nullptr; + QAction* qaction = nullptr; + QActionGroup* qactiongroup = nullptr; + QVector childEntries; +}; + +} // namespace qs::menu::platform diff --git a/src/core/qsmenu.cpp b/src/core/qsmenu.cpp new file mode 100644 index 00000000..e7eed3c3 --- /dev/null +++ b/src/core/qsmenu.cpp @@ -0,0 +1,95 @@ +#include "qsmenu.hpp" + +#include +#include +#include +#include +#include + +#include "platformmenu.hpp" + +using namespace qs::menu::platform; + +namespace qs::menu { + +QString QsMenuButtonType::toString(QsMenuButtonType::Enum value) { + switch (value) { + case QsMenuButtonType::None: return "None"; + case QsMenuButtonType::CheckBox: return "CheckBox"; + case QsMenuButtonType::RadioButton: return "RadioButton"; + default: return "Invalid button type"; + } +} + +void QsMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) { + auto* platform = new PlatformMenuEntry(this); + + QObject::connect(platform, &PlatformMenuEntry::closed, platform, [=]() { + platform->deleteLater(); + }); + + auto success = platform->display(parentWindow, relativeX, relativeY); + if (!success) delete platform; +} + +QQmlListProperty QsMenuEntry::emptyChildren(QObject* parent) { + return QQmlListProperty( + parent, + nullptr, + &QsMenuEntry::childCount, + &QsMenuEntry::childAt + ); +} + +void QsMenuEntry::ref() { + this->refcount++; + if (this->refcount == 1) emit this->opened(); +} + +void QsMenuEntry::unref() { + this->refcount--; + if (this->refcount == 0) emit this->closed(); +} + +QQmlListProperty QsMenuEntry::children() { return QsMenuEntry::emptyChildren(this); } + +QsMenuEntry* QsMenuOpener::menu() const { return this->mMenu; } + +void QsMenuOpener::setMenu(QsMenuEntry* menu) { + if (menu == this->mMenu) return; + + if (this->mMenu != nullptr) { + this->mMenu->unref(); + QObject::disconnect(this->mMenu, nullptr, this, nullptr); + } + + this->mMenu = menu; + + if (menu != nullptr) { + QObject::connect(menu, &QObject::destroyed, this, &QsMenuOpener::onMenuDestroyed); + QObject::connect(menu, &QsMenuEntry::childrenChanged, this, &QsMenuOpener::childrenChanged); + menu->ref(); + } + + emit this->menuChanged(); + emit this->childrenChanged(); +} + +void QsMenuOpener::onMenuDestroyed() { + this->mMenu = nullptr; + emit this->menuChanged(); + emit this->childrenChanged(); +} + +QQmlListProperty QsMenuOpener::children() { + return this->mMenu ? this->mMenu->children() : QsMenuEntry::emptyChildren(this); +} + +qsizetype QsMenuEntry::childCount(QQmlListProperty* /*property*/) { return 0; } + +QsMenuEntry* +QsMenuEntry::childAt(QQmlListProperty* /*property*/, qsizetype /*index*/) { + return nullptr; +} + +} // namespace qs::menu diff --git a/src/core/qsmenu.hpp b/src/core/qsmenu.hpp new file mode 100644 index 00000000..a5f38225 --- /dev/null +++ b/src/core/qsmenu.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "doc.hpp" + +namespace qs::menu { + +class QsMenuButtonType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum { + /// This menu item does not have a checkbox or a radiobutton associated with it. + None = 0, + /// This menu item should draw a checkbox. + CheckBox = 1, + /// This menu item should draw a radiobutton. + RadioButton = 2, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(QsMenuButtonType::Enum value); +}; + +class QsMenuEntry: public QObject { + Q_OBJECT; + /// If this menu item should be rendered as a separator between other items. + /// + /// No other properties have a meaningful value when `isSeparator` is true. + Q_PROPERTY(bool isSeparator READ isSeparator NOTIFY isSeparatorChanged); + Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged); + /// Text of the menu item. + Q_PROPERTY(QString text READ text NOTIFY textChanged); + /// Url of the menu item's icon or `""` if it doesn't have one. + /// + /// This can be passed to [Image.source](https://doc.qt.io/qt-6/qml-qtquick-image.html#source-prop) + /// as shown below. + /// + /// ```qml + /// Image { + /// source: menuItem.icon + /// // To get the best image quality, set the image source size to the same size + /// // as the rendered image. + /// sourceSize.width: width + /// sourceSize.height: height + /// } + /// ``` + Q_PROPERTY(QString icon READ icon NOTIFY iconChanged); + /// If this menu item has an associated checkbox or radiobutton. + Q_PROPERTY(QsMenuButtonType::Enum buttonType READ buttonType NOTIFY buttonTypeChanged); + /// The check state of the checkbox or radiobutton if applicable, as a + /// [Qt.CheckState](https://doc.qt.io/qt-6/qt.html#CheckState-enum). + Q_PROPERTY(Qt::CheckState checkState READ checkState NOTIFY checkStateChanged); + /// If this menu item has children that can be accessed through a [QsMenuOpener](../qsmenuopener). + Q_PROPERTY(bool hasChildren READ hasChildren NOTIFY hasChildrenChanged); + QML_ELEMENT; + QML_UNCREATABLE("QsMenuEntry cannot be directly created"); + +public: + explicit QsMenuEntry(QObject* parent = nullptr): QObject(parent) {} + + /// Display a platform menu at the given location relative to the parent window. + Q_INVOKABLE void display(QObject* parentWindow, qint32 relativeX, qint32 relativeY); + + [[nodiscard]] virtual bool isSeparator() const { return false; } + [[nodiscard]] virtual bool enabled() const { return true; } + [[nodiscard]] virtual QString text() const { return ""; } + [[nodiscard]] virtual QString icon() const { return ""; } + [[nodiscard]] virtual QsMenuButtonType::Enum buttonType() const { return QsMenuButtonType::None; } + [[nodiscard]] virtual Qt::CheckState checkState() const { return Qt::Unchecked; } + [[nodiscard]] virtual bool hasChildren() const { return false; } + + void ref(); + void unref(); + + [[nodiscard]] virtual QQmlListProperty children(); + + static QQmlListProperty emptyChildren(QObject* parent); + +signals: + /// Send a trigger/click signal to the menu entry. + void triggered(); + + QSDOC_HIDE void opened(); + QSDOC_HIDE void closed(); + + void isSeparatorChanged(); + void enabledChanged(); + void textChanged(); + void iconChanged(); + void buttonTypeChanged(); + void checkStateChanged(); + void hasChildrenChanged(); + QSDOC_HIDE void childrenChanged(); + +private: + static qsizetype childCount(QQmlListProperty* property); + static QsMenuEntry* childAt(QQmlListProperty* property, qsizetype index); + + qsizetype refcount = 0; +}; + +///! Provides access to children of a QsMenuEntry +class QsMenuOpener: public QObject { + Q_OBJECT; + /// The menu to retrieve children from. + Q_PROPERTY(QsMenuEntry* menu READ menu WRITE setMenu NOTIFY menuChanged); + /// The children of the given menu. + Q_PROPERTY(QQmlListProperty children READ children NOTIFY childrenChanged); + QML_ELEMENT; + +public: + explicit QsMenuOpener(QObject* parent = nullptr): QObject(parent) {} + + [[nodiscard]] QsMenuEntry* menu() const; + void setMenu(QsMenuEntry* menu); + + [[nodiscard]] QQmlListProperty children(); + +signals: + void menuChanged(); + void childrenChanged(); + +private slots: + void onMenuDestroyed(); + +private: + QsMenuEntry* mMenu = nullptr; +}; + +} // namespace qs::menu diff --git a/src/dbus/dbusmenu/dbusmenu.cpp b/src/dbus/dbusmenu/dbusmenu.cpp index 7484d849..ae68ecd2 100644 --- a/src/dbus/dbusmenu/dbusmenu.cpp +++ b/src/dbus/dbusmenu/dbusmenu.cpp @@ -21,19 +21,26 @@ #include #include "../../core/iconimageprovider.hpp" +#include "../../core/qsmenu.hpp" #include "../../dbus/properties.hpp" #include "dbus_menu.h" #include "dbus_menu_types.hpp" Q_LOGGING_CATEGORY(logDbusMenu, "quickshell.dbus.dbusmenu", QtWarningMsg); +using namespace qs::menu; + namespace qs::dbus::dbusmenu { DBusMenuItem::DBusMenuItem(qint32 id, DBusMenu* menu, DBusMenuItem* parentMenu) - : QObject(menu) + : QsMenuEntry(menu) , id(id) , menu(menu) , parentMenu(parentMenu) { + QObject::connect(this, &QsMenuEntry::opened, this, &DBusMenuItem::sendOpened); + QObject::connect(this, &QsMenuEntry::closed, this, &DBusMenuItem::sendClosed); + QObject::connect(this, &QsMenuEntry::triggered, this, &DBusMenuItem::sendTriggered); + QObject::connect( &this->menu->iconThemePath, &AbstractDBusProperty::changed, @@ -42,20 +49,13 @@ DBusMenuItem::DBusMenuItem(qint32 id, DBusMenu* menu, DBusMenuItem* parentMenu) ); } -void DBusMenuItem::click() { - if (this->displayChildren) { - this->setShowChildren(!this->mShowChildren); - } else { - this->menu->sendEvent(this->id, "clicked"); - } -} - -void DBusMenuItem::hover() const { this->menu->sendEvent(this->id, "hovered"); } +void DBusMenuItem::sendOpened() const { this->menu->sendEvent(this->id, "opened"); } +void DBusMenuItem::sendClosed() const { this->menu->sendEvent(this->id, "closed"); } +void DBusMenuItem::sendTriggered() const { this->menu->sendEvent(this->id, "clicked"); } DBusMenu* DBusMenuItem::menuHandle() const { return this->menu; } -QString DBusMenuItem::label() const { return this->mLabel; } -QString DBusMenuItem::cleanLabel() const { return this->mCleanLabel; } bool DBusMenuItem::enabled() const { return this->mEnabled; } +QString DBusMenuItem::text() const { return this->mCleanLabel; } QString DBusMenuItem::icon() const { if (!this->iconName.isEmpty()) { @@ -68,23 +68,20 @@ QString DBusMenuItem::icon() const { } else return nullptr; } -ToggleButtonType::Enum DBusMenuItem::toggleType() const { return this->mToggleType; }; +QsMenuButtonType::Enum DBusMenuItem::buttonType() const { return this->mButtonType; }; Qt::CheckState DBusMenuItem::checkState() const { return this->mCheckState; } bool DBusMenuItem::isSeparator() const { return this->mSeparator; } bool DBusMenuItem::isShowingChildren() const { return this->mShowChildren && this->childrenLoaded; } -void DBusMenuItem::setShowChildren(bool showChildren) { +void DBusMenuItem::setShowChildrenRecursive(bool showChildren) { if (showChildren == this->mShowChildren) return; this->mShowChildren = showChildren; this->childrenLoaded = false; if (showChildren) { - this->menu->prepareToShow(this->id, true); + this->menu->prepareToShow(this->id, -1); } else { - this->menu->sendEvent(this->id, "closed"); - emit this->showingChildrenChanged(); - if (!this->mChildren.isEmpty()) { for (auto child: this->mChildren) { this->menu->removeRecursive(child); @@ -96,10 +93,15 @@ void DBusMenuItem::setShowChildren(bool showChildren) { } } +void DBusMenuItem::updateLayout() const { + if (!this->isShowingChildren()) return; + this->menu->updateLayout(this->id, -1); +} + bool DBusMenuItem::hasChildren() const { return this->displayChildren; } -QQmlListProperty DBusMenuItem::children() { - return QQmlListProperty( +QQmlListProperty DBusMenuItem::children() { + return QQmlListProperty( this, nullptr, &DBusMenuItem::childrenCount, @@ -107,11 +109,11 @@ QQmlListProperty DBusMenuItem::children() { ); } -qsizetype DBusMenuItem::childrenCount(QQmlListProperty* property) { +qsizetype DBusMenuItem::childrenCount(QQmlListProperty* property) { return reinterpret_cast(property->object)->enabledChildren.count(); // NOLINT } -DBusMenuItem* DBusMenuItem::childAt(QQmlListProperty* property, qsizetype index) { +QsMenuEntry* DBusMenuItem::childAt(QQmlListProperty* property, qsizetype index) { auto* item = reinterpret_cast(property->object); // NOLINT return item->menu->items.value(item->enabledChildren.at(index)); } @@ -124,30 +126,30 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString return; } - auto originalLabel = this->mLabel; + auto originalText = this->mText; //auto originalMnemonic = this->mnemonic; auto originalEnabled = this->mEnabled; auto originalVisible = this->visible; auto originalIconName = this->iconName; auto* originalImage = this->image; auto originalIsSeparator = this->mSeparator; - auto originalToggleType = this->mToggleType; + auto originalButtonType = this->mButtonType; auto originalToggleState = this->mCheckState; auto originalDisplayChildren = this->displayChildren; auto label = properties.value("label"); if (label.canConvert()) { auto text = label.value(); - this->mLabel = text; + this->mText = text; this->mCleanLabel = text; //this->mnemonic = QChar(); - for (auto i = 0; i < this->mLabel.length() - 1;) { - if (this->mLabel.at(i) == '_') { + for (auto i = 0; i < this->mText.length() - 1;) { + if (this->mText.at(i) == '_') { //if (this->mnemonic == QChar()) this->mnemonic = this->mLabel.at(i + 1); - this->mLabel.remove(i, 1); - this->mLabel.insert(i + 1, ""); - this->mLabel.insert(i, ""); + this->mText.remove(i, 1); + this->mText.insert(i + 1, ""); + this->mText.insert(i, ""); i += 8; } else { i++; @@ -160,7 +162,7 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString } } } else if (removed.isEmpty() || removed.contains("label")) { - this->mLabel = ""; + this->mText = ""; //this->mnemonic = QChar(); } @@ -208,15 +210,15 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString if (toggleType.canConvert()) { auto toggleTypeStr = toggleType.value(); - if (toggleTypeStr == "") this->mToggleType = ToggleButtonType::None; - else if (toggleTypeStr == "checkmark") this->mToggleType = ToggleButtonType::CheckBox; - else if (toggleTypeStr == "radio") this->mToggleType = ToggleButtonType::RadioButton; + if (toggleTypeStr == "") this->mButtonType = QsMenuButtonType::None; + else if (toggleTypeStr == "checkmark") this->mButtonType = QsMenuButtonType::CheckBox; + else if (toggleTypeStr == "radio") this->mButtonType = QsMenuButtonType::RadioButton; else { qCWarning(logDbusMenu) << "Unrecognized toggle type" << toggleTypeStr << "for" << this; - this->mToggleType = ToggleButtonType::None; + this->mButtonType = QsMenuButtonType::None; } } else if (removed.isEmpty() || removed.contains("toggle-type")) { - this->mToggleType = ToggleButtonType::None; + this->mButtonType = QsMenuButtonType::None; } auto toggleState = properties.value("toggle-state"); @@ -227,7 +229,7 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString else if (toggleStateInt == 1) this->mCheckState = Qt::Checked; else this->mCheckState = Qt::PartiallyChecked; } else if (removed.isEmpty() || removed.contains("toggle-state")) { - this->mCheckState = Qt::PartiallyChecked; + this->mCheckState = Qt::Unchecked; } auto childrenDisplay = properties.value("children-display"); @@ -245,14 +247,14 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString this->displayChildren = false; } - if (this->mLabel != originalLabel) emit this->labelChanged(); + if (this->mText != originalText) emit this->textChanged(); //if (this->mnemonic != originalMnemonic) emit this->labelChanged(); if (this->mEnabled != originalEnabled) emit this->enabledChanged(); if (this->visible != originalVisible && this->parentMenu != nullptr) this->parentMenu->onChildrenUpdated(); - if (this->mToggleType != originalToggleType) emit this->toggleTypeChanged(); + if (this->mButtonType != originalButtonType) emit this->buttonTypeChanged(); if (this->mCheckState != originalToggleState) emit this->checkStateChanged(); - if (this->mSeparator != originalIsSeparator) emit this->separatorChanged(); + if (this->mSeparator != originalIsSeparator) emit this->isSeparatorChanged(); if (this->displayChildren != originalDisplayChildren) emit this->hasChildrenChanged(); if (this->iconName != originalIconName || this->image != originalImage) { @@ -263,11 +265,11 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString emit this->iconChanged(); } - qCDebug(logDbusMenu).nospace() << "Updated properties of " << this << " { label=" << this->mLabel + qCDebug(logDbusMenu).nospace() << "Updated properties of " << this << " { label=" << this->mText << ", enabled=" << this->mEnabled << ", visible=" << this->visible << ", iconName=" << this->iconName << ", iconData=" << this->image << ", separator=" << this->mSeparator - << ", toggleType=" << this->mToggleType + << ", toggleType=" << this->mButtonType << ", toggleState=" << this->mCheckState << ", displayChildren=" << this->displayChildren << " }"; } @@ -291,20 +293,7 @@ QDebug operator<<(QDebug debug, DBusMenuItem* item) { auto saver = QDebugStateSaver(debug); debug.nospace() << "DBusMenuItem(" << static_cast(item) << ", id=" << item->id - << ", label=" << item->mLabel << ", menu=" << item->menu << ")"; - return debug; -} - -QDebug operator<<(QDebug debug, const ToggleButtonType::Enum& toggleType) { - auto saver = QDebugStateSaver(debug); - debug.nospace() << "ToggleType::"; - - switch (toggleType) { - case ToggleButtonType::None: debug << "None"; break; - case ToggleButtonType::CheckBox: debug << "Checkbox"; break; - case ToggleButtonType::RadioButton: debug << "Radiobutton"; break; - } - + << ", label=" << item->mText << ", menu=" << item->menu << ")"; return debug; } @@ -334,19 +323,18 @@ DBusMenu::DBusMenu(const QString& service, const QString& path, QObject* parent) this->properties.updateAllViaGetAll(); } -void DBusMenu::prepareToShow(qint32 item, bool sendOpened) { +void DBusMenu::prepareToShow(qint32 item, qint32 depth) { auto pending = this->interface->AboutToShow(item); auto* call = new QDBusPendingCallWatcher(pending, this); - auto responseCallback = [this, item, sendOpened](QDBusPendingCallWatcher* call) { + 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(); } - this->updateLayout(item, 1); - if (sendOpened) this->sendEvent(item, "opened"); + this->updateLayout(item, depth); delete call; }; @@ -385,6 +373,7 @@ void DBusMenu::updateLayoutRecursive( // there is an actual nullptr in the map and not no entry if (this->items.contains(layout.id)) { item = new DBusMenuItem(layout.id, this, parent); + item->mShowChildren = parent != nullptr && parent->mShowChildren; this->items.insert(layout.id, item); } } @@ -431,8 +420,9 @@ void DBusMenu::updateLayoutRecursive( if (item->mShowChildren && !item->childrenLoaded) { item->childrenLoaded = true; - emit item->showingChildrenChanged(); } + + emit item->layoutUpdated(); } void DBusMenu::removeRecursive(qint32 id) { diff --git a/src/dbus/dbusmenu/dbusmenu.hpp b/src/dbus/dbusmenu/dbusmenu.hpp index ab485c45..bf2f09fa 100644 --- a/src/dbus/dbusmenu/dbusmenu.hpp +++ b/src/dbus/dbusmenu/dbusmenu.hpp @@ -14,32 +14,18 @@ #include #include "../../core/imageprovider.hpp" +#include "../../core/qsmenu.hpp" #include "../properties.hpp" #include "dbus_menu_types.hpp" Q_DECLARE_LOGGING_CATEGORY(logDbusMenu); -namespace ToggleButtonType { // NOLINT -Q_NAMESPACE; -QML_ELEMENT; - -enum Enum { - /// This menu item does not have a checkbox or a radiobutton associated with it. - None = 0, - /// This menu item should draw a checkbox. - CheckBox = 1, - /// This menu item should draw a radiobutton. - RadioButton = 2, -}; -Q_ENUM_NS(Enum); - -} // namespace ToggleButtonType - class DBusMenuInterface; namespace qs::dbus::dbusmenu { -QDebug operator<<(QDebug debug, const ToggleButtonType::Enum& toggleType); +// hack because docgen can't take namespaces in superclasses +using menu::QsMenuEntry; class DBusMenu; class DBusMenuPngImage; @@ -47,113 +33,56 @@ class DBusMenuPngImage; ///! Menu item shared by an external program. /// Menu item shared by an external program via the /// [DBusMenu specification](https://github.com/AyatanaIndicators/libdbusmenu/blob/master/libdbusmenu-glib/dbus-menu.xml). -class DBusMenuItem: public QObject { +class DBusMenuItem: public QsMenuEntry { Q_OBJECT; - // clang-format off /// Handle to the root of this menu. Q_PROPERTY(DBusMenu* menuHandle READ menuHandle CONSTANT); - /// Text of the menu item, including hotkey markup. - Q_PROPERTY(QString label READ label NOTIFY labelChanged); - /// Text of the menu item without hotkey markup. - Q_PROPERTY(QString cleanLabel READ cleanLabel NOTIFY labelChanged); - /// If the menu item should be shown as enabled. - /// - /// > [!INFO] Disabled menu items are often used as headers in addition - /// > to actual disabled entries. - Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged); - /// Url of the menu item's icon or `""` if it doesn't have one. - /// - /// This can be passed to [Image.source](https://doc.qt.io/qt-6/qml-qtquick-image.html#source-prop) - /// as shown below. - /// - /// ```qml - /// Image { - /// source: menuItem.icon - /// // To get the best image quality, set the image source size to the same size - /// // as the rendered image. - /// sourceSize.width: width - /// sourceSize.height: height - /// } - /// ``` - Q_PROPERTY(QString icon READ icon NOTIFY iconChanged); - /// If this menu item has an associated checkbox or radiobutton. - /// - /// > [!INFO] It is the responsibility of the remote application to update the state of - /// > checkboxes and radiobuttons via [checkState](#prop.checkState). - Q_PROPERTY(ToggleButtonType::Enum toggleType READ toggleType NOTIFY toggleTypeChanged); - /// The check state of the checkbox or radiobutton if applicable, as a - /// [Qt.CheckState](https://doc.qt.io/qt-6/qt.html#CheckState-enum). - Q_PROPERTY(Qt::CheckState checkState READ checkState NOTIFY checkStateChanged); - /// If this menu item should be rendered as a separator between other items. - /// - /// No other properties have a meaningful value when `isSeparator` is true. - Q_PROPERTY(bool isSeparator READ isSeparator NOTIFY separatorChanged); - /// If this menu item reveals a submenu containing more items. - /// - /// Any submenu items must be requested by setting [showChildren](#prop.showChildren). - Q_PROPERTY(bool hasChildren READ hasChildren NOTIFY hasChildrenChanged); - /// If submenu entries of this item should be shown. - /// - /// When true, children of this menu item will be exposed via [children](#prop.children). - /// Setting this property will additionally send the `opened` and `closed` events to the - /// process that provided the menu. - Q_PROPERTY(bool showChildren READ isShowingChildren WRITE setShowChildren NOTIFY showingChildrenChanged); - /// Children of this menu item. Only populated when [showChildren](#prop.showChildren) is true. - /// - /// > [!INFO] Use [hasChildren](#prop.hasChildren) to check if this item should reveal a submenu - /// > instead of checking if `children` is empty. - Q_PROPERTY(QQmlListProperty children READ children NOTIFY childrenChanged); - // clang-format on QML_ELEMENT; QML_UNCREATABLE("DBusMenus can only be acquired from a DBusMenuHandle"); public: explicit DBusMenuItem(qint32 id, DBusMenu* menu, DBusMenuItem* parentMenu); - /// Send a `clicked` event to the remote application for this menu item. - Q_INVOKABLE void click(); - - /// Send a `hovered` event to the remote application for this menu item. + /// Refreshes the menu contents. /// - /// Note: we are not aware of any programs that use this in any meaningful way. - Q_INVOKABLE void hover() const; + /// Usually you shouldn't need to call this manually but some applications providing + /// menus do not update them correctly. Call this if menus don't update their state. + /// + /// The `layoutUpdated` signal will be sent when a response is received. + Q_INVOKABLE void updateLayout() const; [[nodiscard]] DBusMenu* menuHandle() const; - [[nodiscard]] QString label() const; - [[nodiscard]] QString cleanLabel() const; - [[nodiscard]] bool enabled() const; - [[nodiscard]] QString icon() const; - [[nodiscard]] ToggleButtonType::Enum toggleType() const; - [[nodiscard]] Qt::CheckState checkState() const; - [[nodiscard]] bool isSeparator() const; - [[nodiscard]] bool hasChildren() const; + + [[nodiscard]] bool isSeparator() const override; + [[nodiscard]] bool enabled() const override; + [[nodiscard]] QString text() const override; + [[nodiscard]] QString icon() const override; + [[nodiscard]] menu::QsMenuButtonType::Enum buttonType() const override; + [[nodiscard]] Qt::CheckState checkState() const override; + [[nodiscard]] bool hasChildren() const override; [[nodiscard]] bool isShowingChildren() const; - void setShowChildren(bool showChildren); + void setShowChildrenRecursive(bool showChildren); - [[nodiscard]] QQmlListProperty children(); + [[nodiscard]] QQmlListProperty children() override; void updateProperties(const QVariantMap& properties, const QStringList& removed = {}); void onChildrenUpdated(); qint32 id = 0; - QString mLabel; + QString mText; QVector mChildren; bool mShowChildren = false; bool childrenLoaded = false; DBusMenu* menu = nullptr; signals: - void labelChanged(); - //void mnemonicChanged(); - void enabledChanged(); - void iconChanged(); - void separatorChanged(); - void toggleTypeChanged(); - void checkStateChanged(); - void hasChildrenChanged(); - void showingChildrenChanged(); - void childrenChanged(); + void layoutUpdated(); + +private slots: + void sendOpened() const; + void sendClosed() const; + void sendTriggered() const; private: QString mCleanLabel; @@ -163,14 +92,14 @@ private: bool mSeparator = false; QString iconName; DBusMenuPngImage* image = nullptr; - ToggleButtonType::Enum mToggleType = ToggleButtonType::None; - Qt::CheckState mCheckState = Qt::Checked; + menu::QsMenuButtonType::Enum mButtonType = menu::QsMenuButtonType::None; + Qt::CheckState mCheckState = Qt::Unchecked; bool displayChildren = false; QVector enabledChildren; DBusMenuItem* parentMenu = nullptr; - static qsizetype childrenCount(QQmlListProperty* property); - static DBusMenuItem* childAt(QQmlListProperty* property, qsizetype index); + static qsizetype childrenCount(QQmlListProperty* property); + static menu::QsMenuEntry* childAt(QQmlListProperty* property, qsizetype index); }; QDebug operator<<(QDebug debug, DBusMenuItem* item); @@ -192,7 +121,7 @@ public: dbus::DBusProperty status {this->properties, "Status"}; dbus::DBusProperty iconThemePath {this->properties, "IconThemePath", {}, false}; - void prepareToShow(qint32 item, bool sendOpened); + void prepareToShow(qint32 item, qint32 depth); void updateLayout(qint32 parent, qint32 depth); void removeRecursive(qint32 id); void sendEvent(qint32 item, const QString& event); diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index b8359ee8..a5a9aa9b 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -75,6 +75,7 @@ StatusNotifierItem::StatusNotifierItem(const QString& address, QObject* parent) QObject::connect(&this->overlayIconPixmaps, &AbstractDBusProperty::changed, this, &StatusNotifierItem::updateIcon); QObject::connect(&this->properties, &DBusPropertyGroup::getAllFinished, this, &StatusNotifierItem::onGetAllFinished); + QObject::connect(&this->menuPath, &AbstractDBusProperty::changed, this, &StatusNotifierItem::onMenuPathChanged); // clang-format on QObject::connect(this->item, &DBusStatusNotifierItem::NewStatus, this, [this](QString value) { @@ -230,13 +231,41 @@ void StatusNotifierItem::updateIcon() { emit this->iconChanged(); } -DBusMenu* StatusNotifierItem::createMenu() const { - auto path = this->menuPath.get().path(); - if (!path.isEmpty()) { - return new DBusMenu(this->item->service(), this->menuPath.get().path()); +DBusMenu* StatusNotifierItem::menu() const { return this->mMenu; } + +void StatusNotifierItem::refMenu() { + this->menuRefcount++; + + if (this->menuRefcount == 1) { + this->onMenuPathChanged(); + } else { + // Refresh the layout when opening a menu in case a bad client isn't updating it + // and another ref is open somewhere. + this->mMenu->rootItem.updateLayout(); + } +} + +void StatusNotifierItem::unrefMenu() { + this->menuRefcount--; + + if (this->menuRefcount == 0) { + this->onMenuPathChanged(); + } +} + +void StatusNotifierItem::onMenuPathChanged() { + if (this->mMenu) { + this->mMenu->deleteLater(); + this->mMenu = nullptr; } - return nullptr; + if (this->menuRefcount > 0 && !this->menuPath.get().path().isEmpty()) { + this->mMenu = new DBusMenu(this->item->service(), this->menuPath.get().path()); + this->mMenu->setParent(this); + this->mMenu->rootItem.setShowChildrenRecursive(true); + } + + emit this->menuChanged(); } void StatusNotifierItem::onGetAllFinished() { diff --git a/src/services/status_notifier/item.hpp b/src/services/status_notifier/item.hpp index aa411909..04cceeff 100644 --- a/src/services/status_notifier/item.hpp +++ b/src/services/status_notifier/item.hpp @@ -41,7 +41,10 @@ public: [[nodiscard]] bool isReady() const; [[nodiscard]] QString iconId() const; [[nodiscard]] QPixmap createPixmap(const QSize& size) const; - [[nodiscard]] qs::dbus::dbusmenu::DBusMenu* createMenu() const; + + [[nodiscard]] qs::dbus::dbusmenu::DBusMenu* menu() const; + void refMenu(); + void unrefMenu(); void activate(); void secondaryActivate(); @@ -70,16 +73,22 @@ public: signals: void iconChanged(); void ready(); + void menuChanged(); private slots: void updateIcon(); void onGetAllFinished(); + void onMenuPathChanged(); private: + void updateMenuState(); + DBusStatusNotifierItem* item = nullptr; TrayImageHandle imageHandle {this}; bool mReady = false; + dbus::dbusmenu::DBusMenu* mMenu = nullptr; + quint32 menuRefcount = 0; // bumped to inhibit caching quint32 iconIndex = 0; diff --git a/src/services/status_notifier/qml.cpp b/src/services/status_notifier/qml.cpp index f81a6381..e5c64d2a 100644 --- a/src/services/status_notifier/qml.cpp +++ b/src/services/status_notifier/qml.cpp @@ -10,6 +10,7 @@ #include #include "../../core/model.hpp" +#include "../../core/platformmenu.hpp" #include "../../dbus/dbusmenu/dbusmenu.hpp" #include "../../dbus/properties.hpp" #include "host.hpp" @@ -18,6 +19,7 @@ using namespace qs::dbus; using namespace qs::dbus::dbusmenu; using namespace qs::service::sni; +using namespace qs::menu::platform; SystemTrayItem::SystemTrayItem(qs::service::sni::StatusNotifierItem* item, QObject* parent) : QObject(parent) @@ -30,6 +32,7 @@ SystemTrayItem::SystemTrayItem(qs::service::sni::StatusNotifierItem* item, QObje QObject::connect(this->item, &StatusNotifierItem::iconChanged, this, &SystemTrayItem::iconChanged); QObject::connect(&this->item->tooltip, &AbstractDBusProperty::changed, this, &SystemTrayItem::tooltipTitleChanged); QObject::connect(&this->item->tooltip, &AbstractDBusProperty::changed, this, &SystemTrayItem::tooltipDescriptionChanged); + QObject::connect(&this->item->menuPath, &AbstractDBusProperty::changed, this, &SystemTrayItem::hasMenuChanged); QObject::connect(&this->item->isMenu, &AbstractDBusProperty::changed, this, &SystemTrayItem::onlyMenuChanged); // clang-format on } @@ -87,6 +90,11 @@ QString SystemTrayItem::tooltipDescription() const { return this->item->tooltip.get().description; } +bool SystemTrayItem::hasMenu() const { + if (this->item == nullptr) return false; + return !this->item->menuPath.get().path().isEmpty(); +} + bool SystemTrayItem::onlyMenu() const { if (this->item == nullptr) return false; return this->item->isMenu.get(); @@ -94,10 +102,27 @@ bool SystemTrayItem::onlyMenu() const { void SystemTrayItem::activate() const { this->item->activate(); } void SystemTrayItem::secondaryActivate() const { this->item->secondaryActivate(); } + void SystemTrayItem::scroll(qint32 delta, bool horizontal) const { this->item->scroll(delta, horizontal); } +void SystemTrayItem::display(QObject* parentWindow, qint32 relativeX, qint32 relativeY) { + this->item->refMenu(); + auto* platform = new PlatformMenuEntry(&this->item->menu()->rootItem); + + QObject::connect(&this->item->menu()->rootItem, &DBusMenuItem::layoutUpdated, platform, [=]() { + platform->relayout(); + auto success = platform->display(parentWindow, relativeX, relativeY); + + // calls destroy which also unrefs + if (!success) delete platform; + }); + + QObject::connect(platform, &PlatformMenuEntry::closed, this, [=]() { platform->deleteLater(); }); + QObject::connect(platform, &QObject::destroyed, this, [this]() { this->item->unrefMenu(); }); +} + SystemTray::SystemTray(QObject* parent): QObject(parent) { auto* host = StatusNotifierHost::instance(); @@ -129,46 +154,45 @@ ObjectModel* SystemTray::items() { return &this->mItems; } SystemTrayItem* SystemTrayMenuWatcher::trayItem() const { return this->item; } +SystemTrayMenuWatcher::~SystemTrayMenuWatcher() { + if (this->item != nullptr) { + this->item->item->unrefMenu(); + } +} + void SystemTrayMenuWatcher::setTrayItem(SystemTrayItem* item) { if (item == this->item) return; if (this->item != nullptr) { + this->item->item->unrefMenu(); QObject::disconnect(this->item, nullptr, this, nullptr); } this->item = item; if (item != nullptr) { + this->item->item->refMenu(); + QObject::connect(item, &QObject::destroyed, this, &SystemTrayMenuWatcher::onItemDestroyed); QObject::connect( - &item->item->menuPath, - &AbstractDBusProperty::changed, + item->item, + &StatusNotifierItem::menuChanged, this, - &SystemTrayMenuWatcher::onMenuPathChanged + &SystemTrayMenuWatcher::menuChanged ); } - this->onMenuPathChanged(); emit this->trayItemChanged(); + emit this->menuChanged(); } DBusMenuItem* SystemTrayMenuWatcher::menu() const { - if (this->mMenu == nullptr) return nullptr; - return &this->mMenu->rootItem; + return this->item ? &this->item->item->menu()->rootItem : nullptr; } void SystemTrayMenuWatcher::onItemDestroyed() { this->item = nullptr; - this->onMenuPathChanged(); emit this->trayItemChanged(); -} - -void SystemTrayMenuWatcher::onMenuPathChanged() { - if (this->mMenu != nullptr) { - this->mMenu->deleteLater(); - } - - this->mMenu = this->item == nullptr ? nullptr : this->item->item->createMenu(); emit this->menuChanged(); } diff --git a/src/services/status_notifier/qml.hpp b/src/services/status_notifier/qml.hpp index e55509df..9510285a 100644 --- a/src/services/status_notifier/qml.hpp +++ b/src/services/status_notifier/qml.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include "../../core/model.hpp" @@ -44,8 +45,6 @@ Q_ENUM_NS(Enum); /// A system tray item, roughly conforming to the [kde/freedesktop spec] /// (there is no real spec, we just implemented whatever seemed to actually be used). /// -/// The associated context menu can be retrieved using a [SystemTrayMenuWatcher](../systemtraymenuwatcher). -/// /// [kde/freedesktop spec]: https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem/ class SystemTrayItem: public QObject { using DBusMenuItem = qs::dbus::dbusmenu::DBusMenuItem; @@ -61,6 +60,9 @@ class SystemTrayItem: public QObject { Q_PROPERTY(QString icon READ icon NOTIFY iconChanged); Q_PROPERTY(QString tooltipTitle READ tooltipTitle NOTIFY tooltipTitleChanged); Q_PROPERTY(QString tooltipDescription READ tooltipDescription NOTIFY tooltipDescriptionChanged); + /// If this tray item has an associated menu accessible via `display` + /// or a [SystemTrayMenuWatcher](../systemtraymenuwatcher). + Q_PROPERTY(bool hasMenu READ hasMenu NOTIFY hasMenuChanged); /// If this tray item only offers a menu and activation will do nothing. Q_PROPERTY(bool onlyMenu READ onlyMenu NOTIFY onlyMenuChanged); QML_ELEMENT; @@ -78,6 +80,9 @@ public: /// Scroll action, such as changing volume on a mixer. Q_INVOKABLE void scroll(qint32 delta, bool horizontal) const; + /// Display a platform menu at the given location relative to the parent window. + Q_INVOKABLE void display(QObject* parentWindow, qint32 relativeX, qint32 relativeY); + [[nodiscard]] QString id() const; [[nodiscard]] QString title() const; [[nodiscard]] SystemTrayStatus::Enum status() const; @@ -85,6 +90,7 @@ public: [[nodiscard]] QString icon() const; [[nodiscard]] QString tooltipTitle() const; [[nodiscard]] QString tooltipDescription() const; + [[nodiscard]] bool hasMenu() const; [[nodiscard]] bool onlyMenu() const; qs::service::sni::StatusNotifierItem* item = nullptr; @@ -97,6 +103,7 @@ signals: void iconChanged(); void tooltipTitleChanged(); void tooltipDescriptionChanged(); + void hasMenuChanged(); void onlyMenuChanged(); }; @@ -141,6 +148,8 @@ class SystemTrayMenuWatcher: public QObject { public: explicit SystemTrayMenuWatcher(QObject* parent = nullptr): QObject(parent) {} + ~SystemTrayMenuWatcher() override; + Q_DISABLE_COPY_MOVE(SystemTrayMenuWatcher); [[nodiscard]] SystemTrayItem* trayItem() const; void setTrayItem(SystemTrayItem* item); @@ -148,14 +157,12 @@ public: [[nodiscard]] DBusMenuItem* menu() const; signals: - void menuChanged(); void trayItemChanged(); + void menuChanged(); private slots: void onItemDestroyed(); - void onMenuPathChanged(); private: SystemTrayItem* item = nullptr; - DBusMenu* mMenu = nullptr; }; diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index ac8f42bb..a57c5579 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -50,7 +50,9 @@ endfunction() # ----- -qt_add_library(quickshell-wayland STATIC) +qt_add_library(quickshell-wayland STATIC + platformmenu.cpp +) # required to make sure the constructor is linked add_library(quickshell-wayland-init OBJECT init.cpp) diff --git a/src/wayland/init.cpp b/src/wayland/init.cpp index 95adb248..b43179f7 100644 --- a/src/wayland/init.cpp +++ b/src/wayland/init.cpp @@ -4,6 +4,7 @@ #include #include "../core/plugin.hpp" +#include "platformmenu.hpp" #ifdef QS_WAYLAND_WLR_LAYERSHELL #include "wlr_layershell.hpp" @@ -26,6 +27,8 @@ class WaylandPlugin: public QuickshellPlugin { return isWayland; } + void init() override { installPlatformMenuHook(); } + void registerTypes() override { #ifdef QS_WAYLAND_WLR_LAYERSHELL qmlRegisterType("Quickshell._WaylandOverlay", 1, 0, "PanelWindow"); diff --git a/src/wayland/platformmenu.cpp b/src/wayland/platformmenu.cpp new file mode 100644 index 00000000..b7ae3d07 --- /dev/null +++ b/src/wayland/platformmenu.cpp @@ -0,0 +1,32 @@ +#include "platformmenu.hpp" + +#include +#include +#include + +#include "../core/platformmenu.hpp" + +using namespace qs::menu::platform; +using namespace QtWayland; + +// fixes positioning of submenus when hitting screen edges +void platformMenuHook(PlatformMenuQMenu* menu) { + auto* window = menu->windowHandle(); + + auto constraintAdjustment = QtWayland::xdg_positioner::constraint_adjustment_flip_x + | QtWayland::xdg_positioner::constraint_adjustment_flip_y; + + window->setProperty("_q_waylandPopupConstraintAdjustment", constraintAdjustment); + + if (auto* containingMenu = menu->containingMenu) { + auto geom = containingMenu->actionGeometry(menu->menuAction()); + + // use the first action to find the offsets relative to the containing window + auto baseGeom = containingMenu->actionGeometry(containingMenu->actions().first()); + geom += QMargins(0, baseGeom.top(), 0, baseGeom.top()); + + window->setProperty("_q_waylandPopupAnchorRect", geom); + } +} + +void installPlatformMenuHook() { PlatformMenuEntry::registerCreationHook(&platformMenuHook); } diff --git a/src/wayland/platformmenu.hpp b/src/wayland/platformmenu.hpp new file mode 100644 index 00000000..0362932e --- /dev/null +++ b/src/wayland/platformmenu.hpp @@ -0,0 +1,3 @@ +#pragma once + +void installPlatformMenuHook(); From b4be3836952aa68afe9aa32b8dede2c63cbaa0c8 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 2 Jul 2024 10:50:07 -0700 Subject: [PATCH 067/305] service/tray: log menu refcount updates --- src/services/status_notifier/item.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index a5a9aa9b..85383a0b 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -31,6 +31,7 @@ using namespace qs::dbus; using namespace qs::dbus::dbusmenu; Q_LOGGING_CATEGORY(logStatusNotifierItem, "quickshell.service.sni.item", QtWarningMsg); +Q_LOGGING_CATEGORY(logSniMenu, "quickshell.service.sni.item.menu", QtWarningMsg); namespace qs::service::sni { @@ -235,6 +236,7 @@ DBusMenu* StatusNotifierItem::menu() const { return this->mMenu; } void StatusNotifierItem::refMenu() { this->menuRefcount++; + qCDebug(logSniMenu) << "Menu of" << this << "gained a reference. Refcount is now" << this->menuRefcount; if (this->menuRefcount == 1) { this->onMenuPathChanged(); @@ -247,6 +249,7 @@ void StatusNotifierItem::refMenu() { void StatusNotifierItem::unrefMenu() { this->menuRefcount--; + qCDebug(logSniMenu) << "Menu of" << this << "lost a reference. Refcount is now" << this->menuRefcount; if (this->menuRefcount == 0) { this->onMenuPathChanged(); @@ -254,6 +257,9 @@ void StatusNotifierItem::unrefMenu() { } void StatusNotifierItem::onMenuPathChanged() { + qCDebug(logSniMenu) << "Updating menu of" << this << "with refcount" << this->menuRefcount + << "path" << this->menuPath.get().path(); + if (this->mMenu) { this->mMenu->deleteLater(); this->mMenu = nullptr; From fdbb490537b9e70f9cff7555bd5e176ecd1644bd Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 2 Jul 2024 10:52:11 -0700 Subject: [PATCH 068/305] service/tray: fix crash when display is called on a menuless item --- src/services/status_notifier/qml.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/services/status_notifier/qml.cpp b/src/services/status_notifier/qml.cpp index e5c64d2a..e9d1c0ea 100644 --- a/src/services/status_notifier/qml.cpp +++ b/src/services/status_notifier/qml.cpp @@ -109,6 +109,12 @@ void SystemTrayItem::scroll(qint32 delta, bool horizontal) const { void SystemTrayItem::display(QObject* parentWindow, qint32 relativeX, qint32 relativeY) { this->item->refMenu(); + if (!this->item->menu()) { + this->item->unrefMenu(); + qCritical() << "No menu present for" << this; + return; + } + auto* platform = new PlatformMenuEntry(&this->item->menu()->rootItem); QObject::connect(&this->item->menu()->rootItem, &DBusMenuItem::layoutUpdated, platform, [=]() { From db23c0264a1c277b33698e115a9768caf4da4628 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 8 Jul 2024 15:37:49 -0700 Subject: [PATCH 069/305] core/desktopentry: paper over id casing issues --- src/core/desktopentry.cpp | 26 ++++++++++++++++++++++++-- src/core/desktopentry.hpp | 9 ++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 9c887b6c..abcb29fd 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -319,6 +319,7 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) { } auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8); + auto lowerId = id.toLower(); auto text = QString::fromUtf8(file->readAll()); auto* dentry = new DesktopEntry(id, this); @@ -332,13 +333,28 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) { qCDebug(logDesktopEntry) << "Found desktop entry" << id << "at" << path; - if (this->desktopEntries.contains(id)) { + 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); } } } @@ -349,7 +365,13 @@ DesktopEntryManager* DesktopEntryManager::instance() { } DesktopEntry* DesktopEntryManager::byId(const QString& id) { - return this->desktopEntries.value(id); + if (auto* entry = this->desktopEntries.value(id)) { + return entry; + } else if (auto* entry = this->lowercaseDesktopEntries.value(id.toLower())) { + return entry; + } else { + return nullptr; + } } ObjectModel* DesktopEntryManager::applications() { return &this->mApplications; } diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index b9399d41..f268ec51 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -55,10 +55,8 @@ public: static QVector parseExecString(const QString& execString); static void doExec(const QString& execString, const QString& workingDirectory); -private: - QHash mEntries; - QHash mActions; +public: QString mId; QString mName; QString mGenericName; @@ -71,6 +69,10 @@ private: QVector mCategories; QVector mKeywords; +private: + QHash mEntries; + QHash mActions; + friend class DesktopAction; }; @@ -124,6 +126,7 @@ private: void scanPath(const QDir& dir, const QString& prefix = QString()); QHash desktopEntries; + QHash lowercaseDesktopEntries; ObjectModel mApplications {this}; }; From 497c9c4e50dcaa2bf36985de6259f5f24a23d04a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 10 Jul 2024 03:44:55 -0700 Subject: [PATCH 070/305] core/window: ensure items are polished before setting window visible Hacks around a bug in layouts that commonly results in popups being wrongly sized for at least a frame. --- src/core/CMakeLists.txt | 2 +- src/core/proxywindow.cpp | 13 +++++++++++++ src/core/proxywindow.hpp | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index a7ed6d1e..d730d1dd 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -37,7 +37,7 @@ qt_add_library(quickshell-core STATIC set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1) -target_link_libraries(quickshell-core PRIVATE ${QT_DEPS}) +target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} Qt6::QuickPrivate) qs_pch(quickshell-core) target_link_libraries(quickshell PRIVATE quickshell-coreplugin) diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index 4eef5f38..41d539ce 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "generation.hpp" #include "qmlglobal.hpp" @@ -182,6 +183,7 @@ void ProxyWindowBase::setVisibleDirect(bool visible) { if (visible) { this->createWindow(); + this->polishItems(); this->window->setVisible(true); emit this->backerVisibilityChanged(); } else { @@ -192,11 +194,22 @@ void ProxyWindowBase::setVisibleDirect(bool visible) { } } } else if (this->window != nullptr) { + if (visible) this->polishItems(); this->window->setVisible(visible); emit this->backerVisibilityChanged(); } } +void ProxyWindowBase::polishItems() { + // Due to QTBUG-126704, layouts in invisible windows don't update their dimensions. + // Usually this isn't an issue, but it is when the size of a window is based on the size + // of its content, and that content is in a layout. + // + // This hack manually polishes the item tree right before showing the window so it will + // always be created with the correct size. + QQuickWindowPrivate::get(this->window)->polishItems(); +} + qint32 ProxyWindowBase::x() const { if (this->window == nullptr) return 0; else return this->window->x(); diff --git a/src/core/proxywindow.hpp b/src/core/proxywindow.hpp index 40f14c4a..1c62f029 100644 --- a/src/core/proxywindow.hpp +++ b/src/core/proxywindow.hpp @@ -133,5 +133,6 @@ protected: bool reloadComplete = false; private: + void polishItems(); void updateMask(); }; From 24f54f579f28ac4d48bcd3782a3ecc507dffe806 Mon Sep 17 00:00:00 2001 From: Ben <66143154+bdebiase@users.noreply.github.com> Date: Fri, 5 Jul 2024 23:00:39 -0400 Subject: [PATCH 071/305] service/upower: add upower service --- CMakeLists.txt | 2 + src/services/CMakeLists.txt | 4 + src/services/upower/CMakeLists.txt | 39 ++++ src/services/upower/core.cpp | 165 ++++++++++++++++ src/services/upower/core.hpp | 77 ++++++++ src/services/upower/device.cpp | 134 +++++++++++++ src/services/upower/device.hpp | 187 ++++++++++++++++++ src/services/upower/module.md | 7 + .../upower/org.freedesktop.UPower.Device.xml | 16 ++ .../upower/org.freedesktop.UPower.xml | 7 + 10 files changed, 638 insertions(+) create mode 100644 src/services/upower/CMakeLists.txt create mode 100644 src/services/upower/core.cpp create mode 100644 src/services/upower/core.hpp create mode 100644 src/services/upower/device.cpp create mode 100644 src/services/upower/device.hpp create mode 100644 src/services/upower/module.md create mode 100644 src/services/upower/org.freedesktop.UPower.Device.xml create mode 100644 src/services/upower/org.freedesktop.UPower.xml diff --git a/CMakeLists.txt b/CMakeLists.txt index 48654909..9ec58d3d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,7 @@ option(SERVICE_PIPEWIRE "PipeWire service" ON) option(SERVICE_MPRIS "Mpris service" ON) option(SERVICE_PAM "Pam service" ON) option(SERVICE_GREETD "Greet service" ON) +option(SERVICE_UPOWER "UPower service" ON) message(STATUS "Quickshell configuration") message(STATUS " Jemalloc: ${USE_JEMALLOC}") @@ -43,6 +44,7 @@ message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") message(STATUS " Mpris: ${SERVICE_MPRIS}") message(STATUS " Pam: ${SERVICE_PAM}") message(STATUS " Greetd: ${SERVICE_GREETD}") +message(STATUS " UPower: ${SERVICE_UPOWER}") message(STATUS " Hyprland: ${HYPRLAND}") if (HYPRLAND) message(STATUS " IPC: ${HYPRLAND_IPC}") diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt index 8005f074..089f5fd7 100644 --- a/src/services/CMakeLists.txt +++ b/src/services/CMakeLists.txt @@ -17,3 +17,7 @@ endif() if (SERVICE_GREETD) add_subdirectory(greetd) endif() + +if (SERVICE_UPOWER) + add_subdirectory(upower) +endif() diff --git a/src/services/upower/CMakeLists.txt b/src/services/upower/CMakeLists.txt new file mode 100644 index 00000000..59987c40 --- /dev/null +++ b/src/services/upower/CMakeLists.txt @@ -0,0 +1,39 @@ +set_source_files_properties(org.freedesktop.UPower.xml PROPERTIES + CLASSNAME DBusUPowerService + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(DBUS_INTERFACES + org.freedesktop.UPower.xml + dbus_service +) + +set_source_files_properties(org.freedesktop.UPower.Device.xml PROPERTIES + CLASSNAME DBusUPowerDevice + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(DBUS_INTERFACES + org.freedesktop.UPower.Device.xml + dbus_device +) + +qt_add_library(quickshell-service-upower STATIC + core.cpp + device.cpp + ${DBUS_INTERFACES} +) + +# dbus headers +target_include_directories(quickshell-service-upower PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +qt_add_qml_module(quickshell-service-upower + URI Quickshell.Services.UPower + VERSION 0.1 +) + +target_link_libraries(quickshell-service-upower PRIVATE ${QT_DEPS} quickshell-dbus) +target_link_libraries(quickshell PRIVATE quickshell-service-upowerplugin) + +qs_pch(quickshell-service-upower) +qs_pch(quickshell-service-upowerplugin) diff --git a/src/services/upower/core.cpp b/src/services/upower/core.cpp new file mode 100644 index 00000000..de952e00 --- /dev/null +++ b/src/services/upower/core.cpp @@ -0,0 +1,165 @@ +#include "core.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/model.hpp" +#include "../../dbus/properties.hpp" +#include "dbus_service.h" +#include "device.hpp" + +namespace qs::service::upower { + +Q_LOGGING_CATEGORY(logUPower, "quickshell.service.upower", QtWarningMsg); + +UPower::UPower() { + qCDebug(logUPower) << "Starting UPower"; + + auto bus = QDBusConnection::systemBus(); + + if (!bus.isConnected()) { + qCWarning(logUPower) << "Could not connect to DBus. UPower service will not work."; + return; + } + + this->service = + new DBusUPowerService("org.freedesktop.UPower", "/org/freedesktop/UPower", bus, this); + + if (!this->service->isValid()) { + qCWarning(logUPower) << "Cannot connect to the UPower service."; + return; + } + + QObject::connect( + &this->pOnBattery, + &dbus::AbstractDBusProperty::changed, + this, + &UPower::onBatteryChanged + ); + + this->serviceProperties.setInterface(this->service); + this->serviceProperties.updateAllViaGetAll(); + + this->registerExisting(); +} + +void UPower::registerExisting() { + this->registerDevice("/org/freedesktop/UPower/devices/DisplayDevice"); + + auto pending = this->service->EnumerateDevices(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply> reply = *call; + + if (reply.isError()) { + qCWarning(logUPower) << "Failed to enumerate 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 UPower::onDeviceReady() { + auto* device = qobject_cast(this->sender()); + + if (device->path() == "/org/freedesktop/UPower/devices/DisplayDevice") { + this->mDisplayDevice = device; + emit this->displayDeviceChanged(); + qCDebug(logUPower) << "Display UPowerDevice" << device->path() << "ready"; + return; + } + + this->readyDevices.insertObject(device); + qCDebug(logUPower) << "UPowerDevice" << device->path() << "ready"; +} + +void UPower::onDeviceDestroyed(QObject* object) { + auto* device = static_cast(object); // NOLINT + + this->mDevices.remove(device->path()); + + if (device == this->mDisplayDevice) { + this->mDisplayDevice = nullptr; + emit this->displayDeviceChanged(); + qCDebug(logUPower) << "Display UPowerDevice" << device->path() << "destroyed"; + return; + } + + this->readyDevices.removeObject(device); + qCDebug(logUPower) << "UPowerDevice" << device->path() << "destroyed"; +} + +void UPower::registerDevice(const QString& path) { + if (this->mDevices.contains(path)) { + qCDebug(logUPower) << "Skipping duplicate registration of UPowerDevice" << path; + return; + } + + auto* device = new UPowerDevice(path, this); + if (!device->isValid()) { + qCWarning(logUPower) << "Ignoring invalid UPowerDevice registration of" << path; + delete device; + return; + } + + this->mDevices.insert(path, device); + QObject::connect(device, &UPowerDevice::ready, this, &UPower::onDeviceReady); + QObject::connect(device, &QObject::destroyed, this, &UPower::onDeviceDestroyed); + + qCDebug(logUPower) << "Registered UPowerDevice" << path; +} + +UPowerDevice* UPower::displayDevice() { return this->mDisplayDevice; } + +ObjectModel* UPower::devices() { return &this->readyDevices; } + +bool UPower::onBattery() const { return this->pOnBattery.get(); } + +UPower* UPower::instance() { + static UPower* instance = new UPower(); // NOLINT + return instance; +} + +UPowerQml::UPowerQml(QObject* parent): QObject(parent) { + QObject::connect( + UPower::instance(), + &UPower::displayDeviceChanged, + this, + &UPowerQml::displayDeviceChanged + ); + QObject::connect( + UPower::instance(), + &UPower::onBatteryChanged, + this, + &UPowerQml::onBatteryChanged + ); +} + +UPowerDevice* UPowerQml::displayDevice() { // NOLINT + return UPower::instance()->displayDevice(); +} + +ObjectModel* UPowerQml::devices() { // NOLINT + return UPower::instance()->devices(); +} + +bool UPowerQml::onBattery() { return UPower::instance()->onBattery(); } + +} // namespace qs::service::upower diff --git a/src/services/upower/core.hpp b/src/services/upower/core.hpp new file mode 100644 index 00000000..3b2f8602 --- /dev/null +++ b/src/services/upower/core.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../../core/model.hpp" +#include "../../dbus/properties.hpp" +#include "dbus_service.h" +#include "device.hpp" + +namespace qs::service::upower { + +class UPower: public QObject { + Q_OBJECT; + +public: + [[nodiscard]] UPowerDevice* displayDevice(); + [[nodiscard]] ObjectModel* devices(); + [[nodiscard]] bool onBattery() const; + + static UPower* instance(); + +signals: + void displayDeviceChanged(); + void onBatteryChanged(); + +private slots: + void onDeviceReady(); + void onDeviceDestroyed(QObject* object); + +private: + explicit UPower(); + + void registerExisting(); + void registerDevice(const QString& path); + + UPowerDevice* mDisplayDevice = nullptr; + QHash mDevices; + ObjectModel readyDevices {this}; + + dbus::DBusPropertyGroup serviceProperties; + dbus::DBusProperty pOnBattery {this->serviceProperties, "OnBattery"}; + + DBusUPowerService* service = nullptr; +}; + +class UPowerQml: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(UPower); + QML_SINGLETON; + /// UPower's DisplayDevice for your system. Can be `null`. + /// + /// This is an aggregate device and not a physical one, meaning you will not find it in `devices`. + /// It is typically the device that is used for displaying information in desktop environments. + Q_PROPERTY(UPowerDevice* displayDevice READ displayDevice NOTIFY displayDeviceChanged); + /// All connected UPower devices. + Q_PROPERTY(ObjectModel* devices READ devices CONSTANT); + /// If the system is currently running on battery power, or discharging. + Q_PROPERTY(bool onBattery READ onBattery NOTIFY onBatteryChanged); + +public: + explicit UPowerQml(QObject* parent = nullptr); + + [[nodiscard]] UPowerDevice* displayDevice(); + [[nodiscard]] ObjectModel* devices(); + [[nodiscard]] static bool onBattery(); + +signals: + void displayDeviceChanged(); + void onBatteryChanged(); +}; + +} // namespace qs::service::upower diff --git a/src/services/upower/device.cpp b/src/services/upower/device.cpp new file mode 100644 index 00000000..fb7b6064 --- /dev/null +++ b/src/services/upower/device.cpp @@ -0,0 +1,134 @@ +#include "device.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "dbus_device.h" + +using namespace qs::dbus; + +namespace qs::service::upower { + +Q_LOGGING_CATEGORY(logUPowerDevice, "quickshell.service.upower.device", QtWarningMsg); + +QString UPowerDeviceState::toString(UPowerDeviceState::Enum status) { + switch (status) { + case UPowerDeviceState::Unknown: return "Unknown"; + case UPowerDeviceState::Charging: return "Charging"; + case UPowerDeviceState::Discharging: return "Discharging"; + case UPowerDeviceState::Empty: return "Empty"; + case UPowerDeviceState::FullyCharged: return "Fully Charged"; + case UPowerDeviceState::PendingCharge: return "Pending Charge"; + case UPowerDeviceState::PendingDischarge: return "Pending Discharge"; + default: return "Invalid Status"; + } +} + +QString UPowerDeviceType::toString(UPowerDeviceType::Enum type) { + switch (type) { + case UPowerDeviceType::Unknown: return "Unknown"; + case UPowerDeviceType::LinePower: return "Line Power"; + case UPowerDeviceType::Battery: return "Battery"; + case UPowerDeviceType::Ups: return "Ups"; + case UPowerDeviceType::Monitor: return "Monitor"; + case UPowerDeviceType::Mouse: return "Mouse"; + case UPowerDeviceType::Keyboard: return "Keyboard"; + case UPowerDeviceType::Pda: return "Pda"; + case UPowerDeviceType::Phone: return "Phone"; + case UPowerDeviceType::MediaPlayer: return "Media Player"; + case UPowerDeviceType::Tablet: return "Tablet"; + case UPowerDeviceType::Computer: return "Computer"; + case UPowerDeviceType::GamingInput: return "Gaming Input"; + case UPowerDeviceType::Pen: return "Pen"; + case UPowerDeviceType::Touchpad: return "Touchpad"; + case UPowerDeviceType::Modem: return "Modem"; + case UPowerDeviceType::Network: return "Network"; + case UPowerDeviceType::Headset: return "Headset"; + case UPowerDeviceType::Speakers: return "Speakers"; + case UPowerDeviceType::Headphones: return "Headphones"; + case UPowerDeviceType::Video: return "Video"; + case UPowerDeviceType::OtherAudio: return "Other Audio"; + case UPowerDeviceType::RemoteControl: return "Remote Control"; + case UPowerDeviceType::Printer: return "Printer"; + case UPowerDeviceType::Scanner: return "Scanner"; + case UPowerDeviceType::Camera: return "Camera"; + case UPowerDeviceType::Wearable: return "Wearable"; + case UPowerDeviceType::Toy: return "Toy"; + case UPowerDeviceType::BluetoothGeneric: return "Bluetooth Generic"; + default: return "Invalid Type"; + } +} + +UPowerDevice::UPowerDevice(const QString& path, QObject* parent): QObject(parent) { + this->device = + new DBusUPowerDevice("org.freedesktop.UPower", path, QDBusConnection::systemBus(), this); + + if (!this->device->isValid()) { + qCWarning(logUPowerDevice) << "Cannot create UPowerDevice for" << path; + return; + } + + // clang-format off + QObject::connect(&this->pType, &AbstractDBusProperty::changed, this, &UPowerDevice::typeChanged); + QObject::connect(&this->pPowerSupply, &AbstractDBusProperty::changed, this, &UPowerDevice::powerSupplyChanged); + QObject::connect(&this->pEnergy, &AbstractDBusProperty::changed, this, &UPowerDevice::energyChanged); + QObject::connect(&this->pEnergyCapacity, &AbstractDBusProperty::changed, this, &UPowerDevice::energyCapacityChanged); + QObject::connect(&this->pChangeRate, &AbstractDBusProperty::changed, this, &UPowerDevice::changeRateChanged); + QObject::connect(&this->pTimeToEmpty, &AbstractDBusProperty::changed, this, &UPowerDevice::timeToEmptyChanged); + QObject::connect(&this->pTimeToFull, &AbstractDBusProperty::changed, this, &UPowerDevice::timeToFullChanged); + QObject::connect(&this->pPercentage, &AbstractDBusProperty::changed, this, &UPowerDevice::percentageChanged); + QObject::connect(&this->pIsPresent, &AbstractDBusProperty::changed, this, &UPowerDevice::isPresentChanged); + QObject::connect(&this->pState, &AbstractDBusProperty::changed, this, &UPowerDevice::stateChanged); + QObject::connect(&this->pHealthPercentage, &AbstractDBusProperty::changed, this, &UPowerDevice::healthPercentageChanged); + QObject::connect(&this->pHealthPercentage, &AbstractDBusProperty::changed, this, &UPowerDevice::healthSupportedChanged); + QObject::connect(&this->pIconName, &AbstractDBusProperty::changed, this, &UPowerDevice::iconNameChanged); + QObject::connect(&this->pType, &AbstractDBusProperty::changed, this, &UPowerDevice::isLaptopBatteryChanged); + QObject::connect(&this->pNativePath, &AbstractDBusProperty::changed, this, &UPowerDevice::nativePathChanged); + + QObject::connect(&this->deviceProperties, &DBusPropertyGroup::getAllFinished, this, &UPowerDevice::ready); + // clang-format on + + this->deviceProperties.setInterface(this->device); + this->deviceProperties.updateAllViaGetAll(); +} + +bool UPowerDevice::isValid() const { return this->device->isValid(); } +QString UPowerDevice::address() const { return this->device->service(); } +QString UPowerDevice::path() const { return this->device->path(); } + +UPowerDeviceType::Enum UPowerDevice::type() const { + return static_cast(this->pType.get()); +} + +bool UPowerDevice::powerSupply() const { return this->pPowerSupply.get(); } +qreal UPowerDevice::energy() const { return this->pEnergy.get(); } +qreal UPowerDevice::energyCapacity() const { return this->pEnergyCapacity.get(); } +qreal UPowerDevice::changeRate() const { return this->pChangeRate.get(); } +qlonglong UPowerDevice::timeToEmpty() const { return this->pTimeToEmpty.get(); } +qlonglong UPowerDevice::timeToFull() const { return this->pTimeToFull.get(); } +qreal UPowerDevice::percentage() const { return this->pPercentage.get(); } +bool UPowerDevice::isPresent() const { return this->pIsPresent.get(); } + +UPowerDeviceState::Enum UPowerDevice::state() const { + return static_cast(this->pState.get()); +} + +qreal UPowerDevice::healthPercentage() const { return this->pHealthPercentage.get(); } + +bool UPowerDevice::healthSupported() const { return this->healthPercentage() != 0; } + +QString UPowerDevice::iconName() const { return this->pIconName.get(); } + +bool UPowerDevice::isLaptopBattery() const { + return this->pType.get() == UPowerDeviceType::Battery && this->pPowerSupply.get(); +} + +QString UPowerDevice::nativePath() const { return this->pNativePath.get(); } + +} // namespace qs::service::upower diff --git a/src/services/upower/device.hpp b/src/services/upower/device.hpp new file mode 100644 index 00000000..47a71a55 --- /dev/null +++ b/src/services/upower/device.hpp @@ -0,0 +1,187 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../../core/doc.hpp" +#include "../../dbus/properties.hpp" +#include "dbus_device.h" + +namespace qs::service::upower { + +class UPowerDeviceState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum { + Unknown = 0, + Charging = 1, + Discharging = 2, + Empty = 3, + FullyCharged = 4, + /// The device is waiting to be charged after it was plugged in. + PendingCharge = 5, + /// The device is waiting to be discharged after being unplugged. + PendingDischarge = 6, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(UPowerDeviceState::Enum status); +}; + +class UPowerDeviceType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum { + Unknown = 0, + LinePower = 1, + Battery = 2, + Ups = 3, + Monitor = 4, + Mouse = 5, + Keyboard = 6, + Pda = 7, + Phone = 8, + MediaPlayer = 9, + Tablet = 10, + Computer = 11, + GamingInput = 12, + Pen = 13, + Touchpad = 14, + Modem = 15, + Network = 16, + Headset = 17, + Speakers = 18, + Headphones = 19, + Video = 20, + OtherAudio = 21, + RemoteControl = 22, + Printer = 23, + Scanner = 24, + Camera = 25, + Wearable = 26, + Toy = 27, + BluetoothGeneric = 28, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(UPowerDeviceType::Enum type); +}; + +///! A device exposed through the UPower system service. +class UPowerDevice: public QObject { + Q_OBJECT; + // clang-format off + /// The type of device. + Q_PROPERTY(UPowerDeviceType::Enum type READ type NOTIFY typeChanged); + /// If the device is a power supply for your computer and can provide charge. + Q_PROPERTY(bool powerSupply READ powerSupply NOTIFY powerSupplyChanged); + /// Current energy level of the device in watt-hours. + Q_PROPERTY(qreal energy READ energy NOTIFY energyChanged); + /// Maximum energy capacity of the device in watt-hours + Q_PROPERTY(qreal energyCapacity READ energyCapacity NOTIFY energyCapacityChanged); + /// Rate of energy change in watts (positive when charging, negative when discharging). + Q_PROPERTY(qreal changeRate READ changeRate NOTIFY changeRateChanged); + /// Estimated time until the device is fully discharged, in seconds. + /// + /// Will be set to `0` if charging. + Q_PROPERTY(qreal timeToEmpty READ timeToEmpty NOTIFY timeToEmptyChanged); + /// Estimated time until the device is fully charged, in seconds. + /// + /// Will be set to `0` if discharging. + Q_PROPERTY(qreal timeToFull READ timeToFull NOTIFY timeToFullChanged); + /// Current charge level as a percentage. + /// + /// This would be equivalent to `energy / energyCapacity`. + Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged); + /// If the power source is present in the bay or slot, useful for hot-removable batteries. + /// + /// If the device `type` is not `Battery`, then the property will be invalid. + Q_PROPERTY(bool isPresent READ isPresent NOTIFY isPresentChanged); + /// Current state of the device. + Q_PROPERTY(UPowerDeviceState::Enum state READ state NOTIFY stateChanged); + /// Health of the device as a percentage of its original health. + Q_PROPERTY(qreal healthPercentage READ healthPercentage NOTIFY healthPercentageChanged); + Q_PROPERTY(bool healthSupported READ healthSupported NOTIFY healthSupportedChanged); + /// Name of the icon representing the current state of the device, or an empty string if not provided. + Q_PROPERTY(QString iconName READ iconName NOTIFY iconNameChanged); + /// If the device is a laptop battery or not. Use this to check if your device is a valid battery. + /// + /// This will be equivalent to `type == Battery && powerSupply == true`. + Q_PROPERTY(bool isLaptopBattery READ isLaptopBattery NOTIFY isLaptopBatteryChanged); + /// Native path of the device specific to your OS. + Q_PROPERTY(QString nativePath READ nativePath NOTIFY nativePathChanged); + // clang-format on + QML_ELEMENT; + QML_UNCREATABLE("UPowerDevices can only be acquired from UPower"); + +public: + explicit UPowerDevice(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QString path() const; + + [[nodiscard]] UPowerDeviceType::Enum type() const; + [[nodiscard]] bool powerSupply() const; + [[nodiscard]] qreal energy() const; + [[nodiscard]] qreal energyCapacity() const; + [[nodiscard]] qreal changeRate() const; + [[nodiscard]] qlonglong timeToEmpty() const; + [[nodiscard]] qlonglong timeToFull() const; + [[nodiscard]] qreal percentage() const; + [[nodiscard]] bool isPresent() const; + [[nodiscard]] UPowerDeviceState::Enum state() const; + [[nodiscard]] qreal healthPercentage() const; + [[nodiscard]] bool healthSupported() const; + [[nodiscard]] QString iconName() const; + [[nodiscard]] bool isLaptopBattery() const; + [[nodiscard]] QString nativePath() const; + +signals: + QSDOC_HIDE void ready(); + + void typeChanged(); + void powerSupplyChanged(); + void energyChanged(); + void energyCapacityChanged(); + void changeRateChanged(); + void timeToEmptyChanged(); + void timeToFullChanged(); + void percentageChanged(); + void isPresentChanged(); + void stateChanged(); + void healthPercentageChanged(); + void healthSupportedChanged(); + void iconNameChanged(); + void isLaptopBatteryChanged(); + void nativePathChanged(); + +private: + dbus::DBusPropertyGroup deviceProperties; + dbus::DBusProperty pType {this->deviceProperties, "Type"}; + dbus::DBusProperty pPowerSupply {this->deviceProperties, "PowerSupply"}; + dbus::DBusProperty pEnergy {this->deviceProperties, "Energy"}; + dbus::DBusProperty pEnergyCapacity {this->deviceProperties, "EnergyFull"}; + dbus::DBusProperty pChangeRate {this->deviceProperties, "EnergyRate"}; + dbus::DBusProperty pTimeToEmpty {this->deviceProperties, "TimeToEmpty"}; + dbus::DBusProperty pTimeToFull {this->deviceProperties, "TimeToFull"}; + dbus::DBusProperty pPercentage {this->deviceProperties, "Percentage"}; + dbus::DBusProperty pIsPresent {this->deviceProperties, "IsPresent"}; + dbus::DBusProperty pState {this->deviceProperties, "State"}; + dbus::DBusProperty pHealthPercentage {this->deviceProperties, "Capacity"}; + dbus::DBusProperty pIconName {this->deviceProperties, "IconName"}; + dbus::DBusProperty pNativePath {this->deviceProperties, "NativePath"}; + + DBusUPowerDevice* device = nullptr; +}; + +} // namespace qs::service::upower diff --git a/src/services/upower/module.md b/src/services/upower/module.md new file mode 100644 index 00000000..99c7ece4 --- /dev/null +++ b/src/services/upower/module.md @@ -0,0 +1,7 @@ +name = "Quickshell.Services.UPower" +description = "UPower Service" +headers = [ + "core.hpp", + "device.hpp", +] +----- diff --git a/src/services/upower/org.freedesktop.UPower.Device.xml b/src/services/upower/org.freedesktop.UPower.Device.xml new file mode 100644 index 00000000..ef98a30a --- /dev/null +++ b/src/services/upower/org.freedesktop.UPower.Device.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/services/upower/org.freedesktop.UPower.xml b/src/services/upower/org.freedesktop.UPower.xml new file mode 100644 index 00000000..a7522ee5 --- /dev/null +++ b/src/services/upower/org.freedesktop.UPower.xml @@ -0,0 +1,7 @@ + + + + + + + From bb33c9a0c4c16ad05220d948f63c88e1ce8bcf2a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 11 Jul 2024 00:09:34 -0700 Subject: [PATCH 072/305] core/global: add Quickshell.iconPath Replaces "image://icon/" in user facing code. --- src/core/qmlglobal.cpp | 5 +++++ src/core/qmlglobal.hpp | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 05197f26..3321fcf3 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -19,6 +19,7 @@ #include #include "generation.hpp" +#include "iconimageprovider.hpp" #include "qmlscreen.hpp" #include "rootwrapper.hpp" @@ -188,6 +189,10 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT return qEnvironmentVariable(vstr.data()); } +QString QuickshellGlobal::iconPath(const QString& icon) { + return IconImageProvider::requestString(icon, ""); +} + 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 8de55fc2..14d99c52 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -125,6 +125,11 @@ public: /// Returns the string value of an environment variable or null if it is not set. Q_INVOKABLE QVariant env(const QString& variable); + /// Returns a source string usable in an [Image] for a given system icon. + /// + /// [Image]: https://doc.qt.io/qt-6/qml-qtquick-image.html + Q_INVOKABLE static QString iconPath(const QString& icon); + [[nodiscard]] QString workingDirectory() const; void setWorkingDirectory(QString workingDirectory); From 49b309247d5938e4f7b14cec06e19061f7072347 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 11 Jul 2024 00:16:44 -0700 Subject: [PATCH 073/305] all: fix formatting --- src/core/desktopentry.cpp | 7 +++---- src/core/desktopentry.hpp | 1 - src/core/proxywindow.cpp | 2 +- src/services/status_notifier/item.cpp | 8 +++++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index abcb29fd..0fa2d8f3 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -344,12 +344,11 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) { 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."; + << "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); } diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index f268ec51..81cfa8f0 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -55,7 +55,6 @@ public: static QVector parseExecString(const QString& execString); static void doExec(const QString& execString, const QString& workingDirectory); - public: QString mId; QString mName; diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index 41d539ce..fc78f168 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -1,5 +1,6 @@ #include "proxywindow.hpp" +#include #include #include #include @@ -11,7 +12,6 @@ #include #include #include -#include #include "generation.hpp" #include "qmlglobal.hpp" diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index 85383a0b..9a82198b 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -236,7 +236,8 @@ DBusMenu* StatusNotifierItem::menu() const { return this->mMenu; } void StatusNotifierItem::refMenu() { this->menuRefcount++; - qCDebug(logSniMenu) << "Menu of" << this << "gained a reference. Refcount is now" << this->menuRefcount; + qCDebug(logSniMenu) << "Menu of" << this << "gained a reference. Refcount is now" + << this->menuRefcount; if (this->menuRefcount == 1) { this->onMenuPathChanged(); @@ -249,7 +250,8 @@ void StatusNotifierItem::refMenu() { void StatusNotifierItem::unrefMenu() { this->menuRefcount--; - qCDebug(logSniMenu) << "Menu of" << this << "lost a reference. Refcount is now" << this->menuRefcount; + qCDebug(logSniMenu) << "Menu of" << this << "lost a reference. Refcount is now" + << this->menuRefcount; if (this->menuRefcount == 0) { this->onMenuPathChanged(); @@ -258,7 +260,7 @@ void StatusNotifierItem::unrefMenu() { void StatusNotifierItem::onMenuPathChanged() { qCDebug(logSniMenu) << "Updating menu of" << this << "with refcount" << this->menuRefcount - << "path" << this->menuPath.get().path(); + << "path" << this->menuPath.get().path(); if (this->mMenu) { this->mMenu->deleteLater(); From c758421af60842219d2b04b4024103c48d5db32c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 11 Jul 2024 01:43:54 -0700 Subject: [PATCH 074/305] core/reloader: fix Reloadable::onReload being called multiple times onReload was called multiple times due to Reloadable::reloadRecursive calling onReload instead of reload, which didn't set reloadComplete. This allowed the componentComplete fallback to call reload later. --- src/core/reload.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/reload.cpp b/src/core/reload.cpp index 4940ddc8..c62706ab 100644 --- a/src/core/reload.cpp +++ b/src/core/reload.cpp @@ -86,7 +86,7 @@ void Reloadable::reloadRecursive(QObject* newObj, QObject* oldRoot) { // pass handling to the child's onReload, which should call back into reloadRecursive, // with its oldInstance becoming the new oldRoot. - reloadable->onReload(oldInstance); + reloadable->reload(oldInstance); } else if (newObj != nullptr) { Reloadable::reloadChildrenRecursive(newObj, oldRoot); } From 79cbfba48a549e9beaaeead4797dc9319025f3f4 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 11 Jul 2024 22:32:21 -0700 Subject: [PATCH 075/305] wayland/layershell: add warning that exclusive focus is not a lock Apparently this needed to be said. --- src/wayland/wlr_layershell/window.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/wayland/wlr_layershell/window.hpp b/src/wayland/wlr_layershell/window.hpp index 37092a6a..8ce08b5c 100644 --- a/src/wayland/wlr_layershell/window.hpp +++ b/src/wayland/wlr_layershell/window.hpp @@ -38,6 +38,10 @@ enum Enum { /// No keyboard input will be accepted. None = 0, /// Exclusive access to the keyboard, locking out all other windows. + /// + /// > [!WARNING] You **CANNOT** use this to make a secure lock screen. + /// > + /// > If you want to make a lock screen, use [WlSessionLock](../wlsessionlock). Exclusive = 1, /// Access to the keyboard as determined by the operating system. /// From d630cc7f76b56ad3ebe084159df6beab48db9866 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 10 Jul 2024 12:27:10 -0700 Subject: [PATCH 076/305] service/notifications: add notifications service --- CMakeLists.txt | 2 + src/core/model.cpp | 5 + src/core/model.hpp | 10 + src/services/CMakeLists.txt | 4 + src/services/notifications/CMakeLists.txt | 29 ++ src/services/notifications/dbusimage.cpp | 72 +++++ src/services/notifications/dbusimage.hpp | 36 +++ src/services/notifications/module.md | 4 + src/services/notifications/notification.cpp | 252 ++++++++++++++++++ src/services/notifications/notification.hpp | 225 ++++++++++++++++ .../org.freedesktop.Notifications.xml | 46 ++++ src/services/notifications/qml.cpp | 141 ++++++++++ src/services/notifications/qml.hpp | 139 ++++++++++ src/services/notifications/server.cpp | 205 ++++++++++++++ src/services/notifications/server.hpp | 79 ++++++ 15 files changed, 1249 insertions(+) create mode 100644 src/services/notifications/CMakeLists.txt create mode 100644 src/services/notifications/dbusimage.cpp create mode 100644 src/services/notifications/dbusimage.hpp create mode 100644 src/services/notifications/module.md create mode 100644 src/services/notifications/notification.cpp create mode 100644 src/services/notifications/notification.hpp create mode 100644 src/services/notifications/org.freedesktop.Notifications.xml create mode 100644 src/services/notifications/qml.cpp create mode 100644 src/services/notifications/qml.hpp create mode 100644 src/services/notifications/server.cpp create mode 100644 src/services/notifications/server.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ec58d3d..c3b37603 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,7 @@ option(SERVICE_MPRIS "Mpris service" ON) option(SERVICE_PAM "Pam service" ON) option(SERVICE_GREETD "Greet service" ON) option(SERVICE_UPOWER "UPower service" ON) +option(SERVICE_NOTIFICATIONS "Notification server" ON) message(STATUS "Quickshell configuration") message(STATUS " Jemalloc: ${USE_JEMALLOC}") @@ -45,6 +46,7 @@ message(STATUS " Mpris: ${SERVICE_MPRIS}") message(STATUS " Pam: ${SERVICE_PAM}") message(STATUS " Greetd: ${SERVICE_GREETD}") message(STATUS " UPower: ${SERVICE_UPOWER}") +message(STATUS " Notifications: ${SERVICE_NOTIFICATIONS}") message(STATUS " Hyprland: ${HYPRLAND}") if (HYPRLAND) message(STATUS " IPC: ${HYPRLAND_IPC}") diff --git a/src/core/model.cpp b/src/core/model.cpp index 64f7d765..9aaa1472 100644 --- a/src/core/model.cpp +++ b/src/core/model.cpp @@ -72,3 +72,8 @@ bool UntypedObjectModel::removeObject(const QObject* object) { } qsizetype UntypedObjectModel::indexOf(QObject* object) { return this->valuesList.indexOf(object); } + +UntypedObjectModel* UntypedObjectModel::emptyInstance() { + static auto* instance = new UntypedObjectModel(nullptr); // NOLINT + return instance; +} diff --git a/src/core/model.hpp b/src/core/model.hpp index 10465bba..ab58f270 100644 --- a/src/core/model.hpp +++ b/src/core/model.hpp @@ -55,6 +55,8 @@ public: Q_INVOKABLE qsizetype indexOf(QObject* object); + static UntypedObjectModel* emptyInstance(); + signals: void valuesChanged(); /// Sent immediately before an object is inserted into the list. @@ -82,6 +84,10 @@ class ObjectModel: public UntypedObjectModel { public: explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {} + [[nodiscard]] QVector& valueList() { + return *reinterpret_cast*>(&this->valuesList); // NOLINT + } + [[nodiscard]] const QVector& valueList() const { return *reinterpret_cast*>(&this->valuesList); // NOLINT } @@ -91,4 +97,8 @@ public: } void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); } + + static ObjectModel* emptyInstance() { + return static_cast*>(UntypedObjectModel::emptyInstance()); + } }; diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt index 089f5fd7..5ab5c550 100644 --- a/src/services/CMakeLists.txt +++ b/src/services/CMakeLists.txt @@ -21,3 +21,7 @@ endif() if (SERVICE_UPOWER) add_subdirectory(upower) endif() + +if (SERVICE_NOTIFICATIONS) + add_subdirectory(notifications) +endif() diff --git a/src/services/notifications/CMakeLists.txt b/src/services/notifications/CMakeLists.txt new file mode 100644 index 00000000..cc986a84 --- /dev/null +++ b/src/services/notifications/CMakeLists.txt @@ -0,0 +1,29 @@ +qt_add_dbus_adaptor(DBUS_INTERFACES + org.freedesktop.Notifications.xml + server.hpp + qs::service::notifications::NotificationServer + dbus_notifications + DBusNotificationServer +) + +qt_add_library(quickshell-service-notifications STATIC + server.cpp + notification.cpp + dbusimage.cpp + qml.cpp + ${DBUS_INTERFACES} +) + +# dbus headers +target_include_directories(quickshell-service-notifications PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +qt_add_qml_module(quickshell-service-notifications + URI Quickshell.Services.Notifications + VERSION 0.1 +) + +target_link_libraries(quickshell-service-notifications PRIVATE ${QT_DEPS} quickshell-dbus) +target_link_libraries(quickshell PRIVATE quickshell-service-notificationsplugin) + +qs_pch(quickshell-service-notifications) +qs_pch(quickshell-service-notificationsplugin) diff --git a/src/services/notifications/dbusimage.cpp b/src/services/notifications/dbusimage.cpp new file mode 100644 index 00000000..b292f02f --- /dev/null +++ b/src/services/notifications/dbusimage.cpp @@ -0,0 +1,72 @@ +#include "dbusimage.hpp" + +#include +#include +#include +#include +#include +#include + +namespace qs::service::notifications { + +Q_DECLARE_LOGGING_CATEGORY(logNotifications); // server.cpp + +QImage DBusNotificationImage::createImage() const { + auto format = this->hasAlpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888; + + return QImage( + reinterpret_cast(this->data.data()), // NOLINT + this->width, + this->height, + format + ); +} + +const QDBusArgument& operator>>(const QDBusArgument& argument, DBusNotificationImage& pixmap) { + argument.beginStructure(); + argument >> pixmap.width; + argument >> pixmap.height; + auto rowstride = qdbus_cast(argument); + argument >> pixmap.hasAlpha; + auto sampleBits = qdbus_cast(argument); + auto channels = qdbus_cast(argument); + argument >> pixmap.data; + argument.endStructure(); + + if (sampleBits != 8) { + qCWarning(logNotifications) << "Unable to parse pixmap as sample count is incorrect. Got" + << sampleBits << "expected" << 8; + } 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) { + qCWarning(logNotifications) << "Unable to parse pixmap as rowstride is incorrect. Got" + << rowstride << "expected" + << (pixmap.width * sampleBits * channels); + } + + return argument; +} + +const QDBusArgument& operator<<(QDBusArgument& argument, const DBusNotificationImage& pixmap) { + argument.beginStructure(); + argument << pixmap.width; + argument << pixmap.height; + argument << pixmap.width * (pixmap.hasAlpha ? 4 : 3) * 8; + argument << pixmap.hasAlpha; + argument << 8; + argument << (pixmap.hasAlpha ? 4 : 3); + argument << pixmap.data; + argument.endStructure(); + return argument; +} + +QImage +NotificationImage::requestImage(const QString& /*unused*/, QSize* size, const QSize& /*unused*/) { + auto image = this->image.createImage(); + + if (size != nullptr) *size = image.size(); + return image; +} + +} // namespace qs::service::notifications diff --git a/src/services/notifications/dbusimage.hpp b/src/services/notifications/dbusimage.hpp new file mode 100644 index 00000000..d81d1e74 --- /dev/null +++ b/src/services/notifications/dbusimage.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include +#include +#include + +#include "../../core/imageprovider.hpp" + +namespace qs::service::notifications { + +struct DBusNotificationImage { + qint32 width = 0; + qint32 height = 0; + bool hasAlpha = false; + QByteArray data; + + // valid only for the lifetime of the pixmap + [[nodiscard]] QImage createImage() const; +}; + +const QDBusArgument& operator>>(const QDBusArgument& argument, DBusNotificationImage& pixmap); +const QDBusArgument& operator<<(QDBusArgument& argument, const DBusNotificationImage& pixmap); + +class NotificationImage: public QsImageHandle { +public: + explicit NotificationImage(DBusNotificationImage image, QObject* parent) + : QsImageHandle(QQuickAsyncImageProvider::Image, parent) + , image(std::move(image)) {} + + QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; + + DBusNotificationImage image; +}; +} // namespace qs::service::notifications diff --git a/src/services/notifications/module.md b/src/services/notifications/module.md new file mode 100644 index 00000000..8e9ed2c1 --- /dev/null +++ b/src/services/notifications/module.md @@ -0,0 +1,4 @@ +name = "Quickshell.Services.Notifications" +description = "Types for implementing a notification daemon" +headers = [ "qml.hpp", "notification.hpp" ] +----- diff --git a/src/services/notifications/notification.cpp b/src/services/notifications/notification.cpp new file mode 100644 index 00000000..e267699b --- /dev/null +++ b/src/services/notifications/notification.cpp @@ -0,0 +1,252 @@ +#include "notification.hpp" +#include + +#include +#include +#include +#include +#include +#include + +#include "../../core/desktopentry.hpp" +#include "../../core/iconimageprovider.hpp" +#include "dbusimage.hpp" +#include "server.hpp" + +namespace qs::service::notifications { + +Q_DECLARE_LOGGING_CATEGORY(logNotifications); // server.cpp + +QString NotificationUrgency::toString(NotificationUrgency::Enum value) { + switch (value) { + case NotificationUrgency::Low: return "Low"; + case NotificationUrgency::Normal: return "Normal"; + case NotificationUrgency::Critical: return "Critical"; + default: return "Invalid notification urgency"; + } +} + +QString NotificationCloseReason::toString(NotificationCloseReason::Enum value) { + switch (value) { + case NotificationCloseReason::Expired: return "Expired"; + case NotificationCloseReason::Dismissed: return "Dismissed"; + case NotificationCloseReason::CloseRequested: return "CloseRequested"; + default: return "Invalid notification close reason"; + } +} + +QString NotificationAction::identifier() const { return this->mIdentifier; } +QString NotificationAction::text() const { return this->mText; } + +void NotificationAction::invoke() { + NotificationServer::instance()->ActionInvoked(this->notification->id(), this->mIdentifier); + + if (!this->notification->isResident()) { + this->notification->close(NotificationCloseReason::Dismissed); + } +} + +void NotificationAction::setText(const QString& text) { + if (text != this->mText) return; + + this->mText = text; + emit this->textChanged(); +} + +void Notification::expire() { this->close(NotificationCloseReason::Expired); } +void Notification::dismiss() { this->close(NotificationCloseReason::Dismissed); } + +void Notification::close(NotificationCloseReason::Enum reason) { + this->mCloseReason = reason; + + if (reason != 0) { + NotificationServer::instance()->deleteNotification(this, reason); + } +} + +void Notification::updateProperties( + const QString& appName, + QString appIcon, + const QString& summary, + const QString& body, + const QStringList& actions, + QVariantMap hints, + qint32 expireTimeout +) { + auto urgency = hints.contains("urgency") ? hints.value("urgency").value() + : NotificationUrgency::Normal; + + auto hasActionIcons = hints.value("action-icons").value(); + auto isResident = hints.value("resident").value(); + auto isTransient = hints.value("transient").value(); + auto desktopEntry = hints.value("desktop-entry").value(); + + QString imageDataName; + if (hints.contains("image-data")) imageDataName = "image-data"; + else if (hints.contains("image_data")) imageDataName = "image_data"; + else if (hints.contains("icon_data")) imageDataName = "icon_data"; + + NotificationImage* imagePixmap = nullptr; + if (!imageDataName.isEmpty()) { + auto value = hints.value(imageDataName).value(); + DBusNotificationImage image; + value >> image; + imagePixmap = new NotificationImage(std::move(image), this); + } + + // don't store giant byte arrays more than necessary + hints.remove("image-data"); + hints.remove("image_data"); + hints.remove("icon_data"); + + QString imagePath; + if (!imagePixmap) { + QString imagePathName; + if (hints.contains("image-path")) imagePathName = "image-path"; + else if (hints.contains("image_path")) imagePathName = "image_path"; + + if (!imagePathName.isEmpty()) { + imagePath = hints.value(imagePathName).value(); + + if (!imagePath.startsWith("file:")) { + imagePath = IconImageProvider::requestString(imagePath, ""); + } + } + } + + if (appIcon.isEmpty() && !desktopEntry.isEmpty()) { + if (auto* entry = DesktopEntryManager::instance()->byId(desktopEntry)) { + appIcon = entry->mIcon; + } + } + + auto appNameChanged = appName != this->mAppName; + auto appIconChanged = appIcon != this->mAppIcon; + auto summaryChanged = summary != this->mSummary; + auto bodyChanged = body != this->mBody; + auto expireTimeoutChanged = expireTimeout != this->mExpireTimeout; + auto urgencyChanged = urgency != this->mUrgency; + auto hasActionIconsChanged = hasActionIcons != this->mHasActionIcons; + auto isResidentChanged = isResident != this->mIsResident; + auto isTransientChanged = isTransient != this->mIsTransient; + auto desktopEntryChanged = desktopEntry != this->mDesktopEntry; + auto imageChanged = imagePixmap || imagePath != this->mImagePath; + auto hintsChanged = hints != this->mHints; + + if (appNameChanged) this->mAppName = appName; + if (appIconChanged) this->mAppIcon = appIcon; + if (summaryChanged) this->mSummary = summary; + if (bodyChanged) this->mBody = body; + if (expireTimeoutChanged) this->mExpireTimeout = expireTimeout; + if (urgencyChanged) this->mUrgency = static_cast(urgency); + if (hasActionIcons) this->mHasActionIcons = hasActionIcons; + if (isResidentChanged) this->mIsResident = isResident; + if (isTransientChanged) this->mIsTransient = isTransient; + if (desktopEntryChanged) this->mDesktopEntry = desktopEntry; + + NotificationImage* oldImage = nullptr; + + if (imageChanged) { + oldImage = this->mImagePixmap; + this->mImagePixmap = imagePixmap; + this->mImagePath = imagePath; + } + + if (hintsChanged) this->mHints = hints; + + 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); + auto* action = ai < this->mActions.length() ? this->mActions.at(ai) : nullptr; + + if (action && identifier == action->identifier()) { + action->setText(text); + } else { + auto* newAction = new NotificationAction(identifier, text, this); + + if (action) { + deletedActions.push_back(action); + this->mActions.replace(ai, newAction); + } else { + this->mActions.push_back(newAction); + } + + actionsChanged = true; + } + + ai++; + } + + for (auto i = this->mActions.length(); i > ai; i--) { + deletedActions.push_back(this->mActions.at(i - 1)); + this->mActions.remove(i - 1); + actionsChanged = true; + } + } else { + qCWarning(logNotifications) << this << '(' << appName << ')' + << "sent an action set of an invalid length."; + } + + if (appNameChanged) emit this->appNameChanged(); + if (appIconChanged) emit this->appIconChanged(); + if (summaryChanged) emit this->summaryChanged(); + if (bodyChanged) emit this->bodyChanged(); + if (expireTimeoutChanged) emit this->expireTimeoutChanged(); + if (urgencyChanged) emit this->urgencyChanged(); + if (actionsChanged) emit this->actionsChanged(); + if (hasActionIconsChanged) emit this->hasActionIconsChanged(); + if (isResidentChanged) emit this->isResidentChanged(); + if (isTransientChanged) emit this->isTransientChanged(); + if (desktopEntryChanged) emit this->desktopEntryChanged(); + if (imageChanged) emit this->imageChanged(); + if (hintsChanged) emit this->hintsChanged(); + + for (auto* action: deletedActions) { + delete action; + } + + delete oldImage; +} + +quint32 Notification::id() const { return this->mId; } +bool Notification::isTracked() const { return this->mCloseReason == 0; } +NotificationCloseReason::Enum Notification::closeReason() const { return this->mCloseReason; } + +void Notification::setTracked(bool tracked) { + this->close( + tracked ? static_cast(0) : NotificationCloseReason::Dismissed + ); +} + +bool Notification::isLastGeneration() const { return this->mLastGeneration; } +void Notification::setLastGeneration() { this->mLastGeneration = true; } + +qreal Notification::expireTimeout() const { return this->mExpireTimeout; } +QString Notification::appName() const { return this->mAppName; } +QString Notification::appIcon() const { return this->mAppIcon; } +QString Notification::summary() const { return this->mSummary; } +QString Notification::body() const { return this->mBody; } +NotificationUrgency::Enum Notification::urgency() const { return this->mUrgency; } +QVector Notification::actions() const { return this->mActions; } +bool Notification::hasActionIcons() const { return this->mHasActionIcons; } +bool Notification::isResident() const { return this->mIsResident; } +bool Notification::isTransient() const { return this->mIsTransient; } +QString Notification::desktopEntry() const { return this->mDesktopEntry; } + +QString Notification::image() const { + if (this->mImagePixmap) { + return this->mImagePixmap->url(); + } else { + return this->mImagePath; + } +} + +QVariantMap Notification::hints() const { return this->mHints; } + +} // namespace qs::service::notifications diff --git a/src/services/notifications/notification.hpp b/src/services/notifications/notification.hpp new file mode 100644 index 00000000..5be56172 --- /dev/null +++ b/src/services/notifications/notification.hpp @@ -0,0 +1,225 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace qs::service::notifications { + +class NotificationImage; + +class NotificationUrgency: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum { + Low = 0, + Normal = 1, + Critical = 2, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(NotificationUrgency::Enum value); +}; + +class NotificationCloseReason: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum { + /// The notification expired due to a timeout. + Expired = 1, + /// The notification was explicitly dismissed by the user. + Dismissed = 2, + /// The remote application requested the notification be removed. + CloseRequested = 3, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(NotificationCloseReason::Enum value); +}; + +class NotificationAction; + +///! A notification emitted by a NotificationServer. +class Notification: public QObject { + Q_OBJECT; + /// Id of the notification as given to the client. + Q_PROPERTY(quint32 id READ id CONSTANT); + /// If the notification is tracked by the notification server. + /// + /// Setting this property to false is equivalent to calling `dismiss()`. + Q_PROPERTY(bool tracked READ isTracked WRITE setTracked NOTIFY trackedChanged); + /// If this notification was carried over from the last generation + /// when quickshell reloaded. + /// + /// Notifications from the last generation will only be emitted if + /// [NotificationServer.keepOnReload](../notificationserver#prop.keepOnReload) is true. + Q_PROPERTY(bool lastGeneration READ isLastGeneration CONSTANT); + /// Time in seconds the notification should be valid for + Q_PROPERTY(qreal expireTimeout READ expireTimeout NOTIFY expireTimeoutChanged); + /// The sending application's name. + Q_PROPERTY(QString appName READ appName NOTIFY appNameChanged); + /// The sending application's icon. If none was provided, then the icon from an associated + /// desktop entry will be retrieved. If none was found then "". + Q_PROPERTY(QString appIcon READ appIcon NOTIFY appIconChanged); + /// The image associated with this notification, or "" if none. + Q_PROPERTY(QString summary READ summary NOTIFY summaryChanged); + Q_PROPERTY(QString body READ body NOTIFY bodyChanged); + Q_PROPERTY(NotificationUrgency::Enum urgency READ urgency NOTIFY urgencyChanged); + /// Actions that can be taken for this notification. + Q_PROPERTY(QVector actions READ actions NOTIFY actionsChanged); + /// If actions associated with this notification have icons available. + /// + /// See [NotificationAction.identifier](../notificationaction#prop.identifier) for details. + Q_PROPERTY(bool hasActionIcons READ hasActionIcons NOTIFY hasActionIconsChanged); + /// If true, the notification will not be destroyed after an action is invoked. + Q_PROPERTY(bool resident READ isResident NOTIFY isResidentChanged); + /// If true, the notification should skip any kind of persistence function like a notification area. + Q_PROPERTY(bool transient READ isTransient NOTIFY isTransientChanged); + /// The name of the sender's desktop entry or "" if none was supplied. + Q_PROPERTY(QString desktopEntry READ desktopEntry NOTIFY desktopEntryChanged); + /// An image associated with the notification. + /// + /// This image is often something like a profile picture in instant messaging applications. + Q_PROPERTY(QString image READ image NOTIFY imageChanged); + /// All hints sent by the client application as a javascript object. + /// Many common hints are exposed via other properties. + Q_PROPERTY(QVariantMap hints READ hints NOTIFY hintsChanged); + QML_ELEMENT; + QML_UNCREATABLE("Notifications must be acquired from a NotificationServer"); + +public: + explicit Notification(quint32 id, QObject* parent): QObject(parent), mId(id) {} + + /// Destroy the notification and hint to the remote application that it has + /// timed out an expired. + Q_INVOKABLE void expire(); + /// Destroy the notification and hint to the remote application that it was + /// explicitly closed by the user. + Q_INVOKABLE void dismiss(); + + void updateProperties( + const QString& appName, + QString appIcon, + const QString& summary, + const QString& body, + const QStringList& actions, + QVariantMap hints, + qint32 expireTimeout + ); + + void close(NotificationCloseReason::Enum reason); + + [[nodiscard]] quint32 id() const; + + [[nodiscard]] bool isTracked() const; + [[nodiscard]] NotificationCloseReason::Enum closeReason() const; + void setTracked(bool tracked); + + [[nodiscard]] bool isLastGeneration() const; + void setLastGeneration(); + + [[nodiscard]] qreal expireTimeout() const; + [[nodiscard]] QString appName() const; + [[nodiscard]] QString appIcon() const; + [[nodiscard]] QString summary() const; + [[nodiscard]] QString body() const; + [[nodiscard]] NotificationUrgency::Enum urgency() const; + [[nodiscard]] QVector actions() const; + [[nodiscard]] bool hasActionIcons() const; + [[nodiscard]] bool isResident() const; + [[nodiscard]] bool isTransient() const; + [[nodiscard]] QString desktopEntry() const; + [[nodiscard]] QString image() const; + [[nodiscard]] QVariantMap hints() const; + +signals: + /// Sent when a notification has been closed. + /// + /// The notification object will be destroyed as soon as all signal handlers exit. + void closed(NotificationCloseReason::Enum reason); + + void trackedChanged(); + void expireTimeoutChanged(); + void appNameChanged(); + void appIconChanged(); + void summaryChanged(); + void bodyChanged(); + void urgencyChanged(); + void actionsChanged(); + void hasActionIconsChanged(); + void isResidentChanged(); + void isTransientChanged(); + void desktopEntryChanged(); + void imageChanged(); + void hintsChanged(); + +private: + quint32 mId; + NotificationCloseReason::Enum mCloseReason = NotificationCloseReason::Dismissed; + bool mLastGeneration = false; + qreal mExpireTimeout = 0; + QString mAppName; + QString mAppIcon; + QString mSummary; + QString mBody; + NotificationUrgency::Enum mUrgency = NotificationUrgency::Normal; + QVector mActions; + bool mHasActionIcons = false; + bool mIsResident = false; + bool mIsTransient = false; + QString mImagePath; + NotificationImage* mImagePixmap = nullptr; + QString mDesktopEntry; + QVariantMap mHints; +}; + +class NotificationAction: public QObject { + Q_OBJECT; + /// The identifier of the action. + /// + /// When [Notification.hasActionIcons] is true, this property will be an icon name. + /// When it is false, this property is irrelevant. + /// + /// [Notification.hasActionIcons]: ../notification#prop.hasActionIcons + Q_PROPERTY(QString identifier READ identifier CONSTANT); + /// The localized text that should be displayed on a button. + Q_PROPERTY(QString text READ text NOTIFY textChanged); + QML_ELEMENT; + QML_UNCREATABLE("NotificationActions must be acquired from a Notification"); + +public: + explicit NotificationAction(QString identifier, QString text, Notification* notification) + : QObject(notification) + , notification(notification) + , mIdentifier(std::move(identifier)) + , mText(std::move(text)) {} + + /// Invoke the action. If [Notification.resident] is false it will be dismissed. + /// + /// [Notification.resident]: ../notification#prop.resident + Q_INVOKABLE void invoke(); + + [[nodiscard]] QString identifier() const; + [[nodiscard]] QString text() const; + void setText(const QString& text); + +signals: + void textChanged(); + +private: + Notification* notification; + QString mIdentifier; + QString mText; +}; + +} // namespace qs::service::notifications diff --git a/src/services/notifications/org.freedesktop.Notifications.xml b/src/services/notifications/org.freedesktop.Notifications.xml new file mode 100644 index 00000000..1a2001fe --- /dev/null +++ b/src/services/notifications/org.freedesktop.Notifications.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/services/notifications/qml.cpp b/src/services/notifications/qml.cpp new file mode 100644 index 00000000..99818214 --- /dev/null +++ b/src/services/notifications/qml.cpp @@ -0,0 +1,141 @@ +#include "qml.hpp" +#include + +#include +#include +#include +#include + +#include "../../core/model.hpp" +#include "notification.hpp" +#include "server.hpp" + +namespace qs::service::notifications { + +void NotificationServerQml::onPostReload() { + auto* instance = NotificationServer::instance(); + instance->support = this->support; + + QObject::connect( + instance, + &NotificationServer::notification, + this, + &NotificationServerQml::notification + ); + + instance->switchGeneration(this->mKeepOnReload, [this]() { + this->live = true; + emit this->trackedNotificationsChanged(); + }); +} + +bool NotificationServerQml::keepOnReload() const { return this->mKeepOnReload; } + +void NotificationServerQml::setKeepOnReload(bool keepOnReload) { + if (keepOnReload == this->mKeepOnReload) return; + + if (this->live) { + qCritical() << "Cannot set NotificationServer.keepOnReload after the server has been started."; + return; + } + + this->mKeepOnReload = keepOnReload; + emit this->keepOnReloadChanged(); +} + +bool NotificationServerQml::persistenceSupported() const { return this->support.persistence; } + +void NotificationServerQml::setPersistenceSupported(bool persistenceSupported) { + if (persistenceSupported == this->support.persistence) return; + this->support.persistence = persistenceSupported; + this->updateSupported(); + emit this->persistenceSupportedChanged(); +} + +bool NotificationServerQml::bodySupported() const { return this->support.body; } + +void NotificationServerQml::setBodySupported(bool bodySupported) { + if (bodySupported == this->support.body) return; + this->support.body = bodySupported; + this->updateSupported(); + emit this->bodySupportedChanged(); +} + +bool NotificationServerQml::bodyMarkupSupported() const { return this->support.bodyMarkup; } + +void NotificationServerQml::setBodyMarkupSupported(bool bodyMarkupSupported) { + if (bodyMarkupSupported == this->support.bodyMarkup) return; + this->support.bodyMarkup = bodyMarkupSupported; + this->updateSupported(); + emit this->bodyMarkupSupportedChanged(); +} + +bool NotificationServerQml::bodyHyperlinksSupported() const { return this->support.bodyHyperlinks; } + +void NotificationServerQml::setBodyHyperlinksSupported(bool bodyHyperlinksSupported) { + if (bodyHyperlinksSupported == this->support.bodyHyperlinks) return; + this->support.bodyHyperlinks = bodyHyperlinksSupported; + this->updateSupported(); + emit this->bodyHyperlinksSupportedChanged(); +} + +bool NotificationServerQml::bodyImagesSupported() const { return this->support.bodyImages; } + +void NotificationServerQml::setBodyImagesSupported(bool bodyImagesSupported) { + if (bodyImagesSupported == this->support.bodyImages) return; + this->support.bodyImages = bodyImagesSupported; + this->updateSupported(); + emit this->bodyImagesSupportedChanged(); +} + +bool NotificationServerQml::actionsSupported() const { return this->support.actions; } + +void NotificationServerQml::setActionsSupported(bool actionsSupported) { + if (actionsSupported == this->support.actions) return; + this->support.actions = actionsSupported; + this->updateSupported(); + emit this->actionsSupportedChanged(); +} + +bool NotificationServerQml::actionIconsSupported() const { return this->support.actionIcons; } + +void NotificationServerQml::setActionIconsSupported(bool actionIconsSupported) { + if (actionIconsSupported == this->support.actionIcons) return; + this->support.actionIcons = actionIconsSupported; + this->updateSupported(); + emit this->actionIconsSupportedChanged(); +} + +bool NotificationServerQml::imageSupported() const { return this->support.image; } + +void NotificationServerQml::setImageSupported(bool imageSupported) { + if (imageSupported == this->support.image) return; + this->support.image = imageSupported; + this->updateSupported(); + emit this->imageSupportedChanged(); +} + +QVector NotificationServerQml::extraHints() const { return this->support.extraHints; } + +void NotificationServerQml::setExtraHints(QVector extraHints) { + if (extraHints == this->support.extraHints) return; + this->support.extraHints = std::move(extraHints); + this->updateSupported(); + emit this->extraHintsChanged(); +} + +ObjectModel* NotificationServerQml::trackedNotifications() const { + if (this->live) { + return NotificationServer::instance()->trackedNotifications(); + } else { + return ObjectModel::emptyInstance(); + } +} + +void NotificationServerQml::updateSupported() { + if (this->live) { + NotificationServer::instance()->support = this->support; + } +} + +} // namespace qs::service::notifications diff --git a/src/services/notifications/qml.hpp b/src/services/notifications/qml.hpp new file mode 100644 index 00000000..341eaf2a --- /dev/null +++ b/src/services/notifications/qml.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../../core/model.hpp" +#include "../../core/reload.hpp" +#include "notification.hpp" +#include "server.hpp" + +namespace qs::service::notifications { + +///! Desktop Notifications Server. +/// An implementation of the [Desktop Notifications Specification] for receiving notifications +/// from external applications. +/// +/// The server does not advertise most capabilities by default. See the individual properties for details. +/// +/// [Desktop Notifications Specification]: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html +class NotificationServerQml + : public QObject + , public PostReloadHook { + Q_OBJECT; + // clang-format off + /// If notifications should be re-emitted when quickshell reloads. Defaults to true. + /// + /// The [lastGeneration](../notification#prop.lastGeneration) flag will be + /// set on notifications from the prior generation for further filtering/handling. + Q_PROPERTY(bool keepOnReload READ keepOnReload WRITE setKeepOnReload NOTIFY keepOnReloadChanged); + /// If the notification server should advertise that it can persist notifications in the background + /// after going offscreen. Defaults to false. + Q_PROPERTY(bool persistenceSupported READ persistenceSupported WRITE setPersistenceSupported NOTIFY persistenceSupportedChanged); + /// If notification body text should be advertised as supported by the notification server. + /// Defaults to true. + /// + /// Note that returned notifications are likely to return body text even if this property is false, + /// as it is only a hint. + Q_PROPERTY(bool bodySupported READ bodySupported WRITE setBodySupported NOTIFY bodySupportedChanged); + /// If notification body text should be advertised as supporting markup as described in [the specification] + /// Defaults to false. + /// + /// Note that returned notifications may still contain markup if this property is false, as it is only a hint. + /// By default Text objects will try to render markup. To avoid this if any is sent, change [Text.textFormat] to `PlainText`. + /// + /// [the specification]: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#markup + /// [Text.textFormat]: https://doc.qt.io/qt-6/qml-qtquick-text.html#textFormat-prop + Q_PROPERTY(bool bodyMarkupSupported READ bodyMarkupSupported WRITE setBodyMarkupSupported NOTIFY bodyMarkupSupportedChanged); + /// If notification body text should be advertised as supporting hyperlinks as described in [the specification] + /// Defaults to false. + /// + /// Note that returned notifications may still contain hyperlinks if this property is false, as it is only a hint. + /// + /// [the specification]: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#hyperlinks + Q_PROPERTY(bool bodyHyperlinksSupported READ bodyHyperlinksSupported WRITE setBodyHyperlinksSupported NOTIFY bodyHyperlinksSupportedChanged); + /// If notification body text should be advertised as supporting images as described in [the specification] + /// Defaults to false. + /// + /// Note that returned notifications may still contain images if this property is false, as it is only a hint. + /// + /// [the specification]: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#images + Q_PROPERTY(bool bodyImagesSupported READ bodyImagesSupported WRITE setBodyImagesSupported NOTIFY bodyImagesSupportedChanged); + /// If notification actions should be advertised as supported by the notification server. Defaults to false. + Q_PROPERTY(bool actionsSupported READ actionsSupported WRITE setActionsSupported NOTIFY actionsSupportedChanged); + /// If notification actions should be advertised as supporting the display of icons. Defaults to false. + 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); + /// All notifications currently tracked by the server. + Q_PROPERTY(ObjectModel* trackedNotifications READ trackedNotifications NOTIFY trackedNotificationsChanged); + /// Extra hints to expose to notification clients. + Q_PROPERTY(QVector extraHints READ extraHints WRITE setExtraHints NOTIFY extraHintsChanged); + // clang-format on + QML_NAMED_ELEMENT(NotificationServer); + +public: + void onPostReload() override; + + [[nodiscard]] bool keepOnReload() const; + void setKeepOnReload(bool keepOnReload); + + [[nodiscard]] bool persistenceSupported() const; + void setPersistenceSupported(bool persistenceSupported); + + [[nodiscard]] bool bodySupported() const; + void setBodySupported(bool bodySupported); + + [[nodiscard]] bool bodyMarkupSupported() const; + void setBodyMarkupSupported(bool bodyMarkupSupported); + + [[nodiscard]] bool bodyHyperlinksSupported() const; + void setBodyHyperlinksSupported(bool bodyHyperlinksSupported); + + [[nodiscard]] bool bodyImagesSupported() const; + void setBodyImagesSupported(bool bodyImagesSupported); + + [[nodiscard]] bool actionsSupported() const; + void setActionsSupported(bool actionsSupported); + + [[nodiscard]] bool actionIconsSupported() const; + void setActionIconsSupported(bool actionIconsSupported); + + [[nodiscard]] bool imageSupported() const; + void setImageSupported(bool imageSupported); + + [[nodiscard]] QVector extraHints() const; + void setExtraHints(QVector extraHints); + + [[nodiscard]] ObjectModel* trackedNotifications() const; + +signals: + /// Sent when a notification is received by the server. + /// + /// If this notification should not be discarded, set its `tracked` property to true. + void notification(Notification* notification); + + void keepOnReloadChanged(); + void persistenceSupportedChanged(); + void bodySupportedChanged(); + void bodyMarkupSupportedChanged(); + void bodyHyperlinksSupportedChanged(); + void bodyImagesSupportedChanged(); + void actionsSupportedChanged(); + void actionIconsSupportedChanged(); + void imageSupportedChanged(); + void extraHintsChanged(); + void trackedNotificationsChanged(); + +private: + void updateSupported(); + + bool live = false; + bool mKeepOnReload = true; + NotificationServerSupport support; +}; + +} // namespace qs::service::notifications diff --git a/src/services/notifications/server.cpp b/src/services/notifications/server.cpp new file mode 100644 index 00000000..5b89a894 --- /dev/null +++ b/src/services/notifications/server.cpp @@ -0,0 +1,205 @@ +#include "server.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/model.hpp" +#include "dbus_notifications.h" +#include "dbusimage.hpp" +#include "notification.hpp" + +namespace qs::service::notifications { + +Q_LOGGING_CATEGORY(logNotifications, "quickshell.service.notifications"); + +NotificationServer::NotificationServer() { + qDBusRegisterMetaType(); + + new DBusNotificationServer(this); + + qCInfo(logNotifications) << "Starting notification server"; + + auto bus = QDBusConnection::sessionBus(); + + if (!bus.isConnected()) { + qCWarning(logNotifications) << "Could not connect to DBus. Notification service will not work."; + return; + } + + if (!bus.registerObject("/org/freedesktop/Notifications", this)) { + qCWarning(logNotifications) << "Could not register Notification server object with DBus. " + "Notification server will not work."; + return; + } + + QObject::connect( + &this->serviceWatcher, + &QDBusServiceWatcher::serviceUnregistered, + this, + &NotificationServer::onServiceUnregistered + ); + + this->serviceWatcher.setWatchMode(QDBusServiceWatcher::WatchForUnregistration); + this->serviceWatcher.addWatchedService("org.freedesktop.Notifications"); + this->serviceWatcher.setConnection(bus); + + NotificationServer::tryRegister(); +} + +NotificationServer* NotificationServer::instance() { + static auto* instance = new NotificationServer(); // NOLINT + return instance; +} + +void NotificationServer::switchGeneration(bool reEmit, const std::function& clearHook) { + auto notifications = this->mNotifications.valueList(); + this->mNotifications.valueList().clear(); + this->idMap.clear(); + + clearHook(); + + if (reEmit) { + for (auto* notification: notifications) { + notification->setLastGeneration(); + notification->setTracked(false); + emit this->notification(notification); + + if (!notification->isTracked()) { + emit this->NotificationClosed(notification->id(), notification->closeReason()); + delete notification; + } else { + this->idMap.insert(notification->id(), notification); + this->mNotifications.insertObject(notification); + } + } + } else { + for (auto* notification: notifications) { + emit this->NotificationClosed(notification->id(), NotificationCloseReason::Expired); + delete notification; + } + } +} + +ObjectModel* NotificationServer::trackedNotifications() { + return &this->mNotifications; +} + +void NotificationServer::deleteNotification( + Notification* notification, + NotificationCloseReason::Enum reason +) { + if (!this->idMap.contains(notification->id())) return; + + emit notification->closed(reason); + + this->mNotifications.removeObject(notification); + this->idMap.remove(notification->id()); + + emit this->NotificationClosed(notification->id(), reason); + delete notification; +} + +void NotificationServer::tryRegister() { + auto bus = QDBusConnection::sessionBus(); + auto success = bus.registerService("org.freedesktop.Notifications"); + + if (success) { + qCInfo(logNotifications) << "Registered notification server with dbus."; + } else { + qCWarning(logNotifications + ) << "Could not register notification server at org.freedesktop.Notifications, presumably " + "because one is already registered."; + qCWarning(logNotifications + ) << "Registration will be attempted again if the active service is unregistered."; + } +} +void NotificationServer::onServiceUnregistered(const QString& /*unused*/) { + qCDebug(logNotifications) << "Active notification server unregistered, attempting registration"; + this->tryRegister(); +} + +void NotificationServer::CloseNotification(uint id) { + auto* notification = this->idMap.value(id); + + if (notification) { + this->deleteNotification(notification, NotificationCloseReason::CloseRequested); + } +} + +QStringList NotificationServer::GetCapabilities() const { + auto capabilities = QStringList(); + + if (this->support.persistence) capabilities += "persistence"; + + if (this->support.body) { + capabilities += "body"; + if (this->support.bodyMarkup) capabilities += "body-markup"; + if (this->support.bodyHyperlinks) capabilities += "body-hyperlinks"; + if (this->support.bodyImages) capabilities += "body-images"; + } + + if (this->support.actions) { + capabilities += "actions"; + if (this->support.actionIcons) capabilities += "action-icons"; + } + + if (this->support.image) capabilities += "icon-static"; + + capabilities += this->support.extraHints; + + return capabilities; +} + +QString +NotificationServer::GetServerInformation(QString& vendor, QString& version, QString& specVersion) { + vendor = "quickshell"; + version = QCoreApplication::applicationVersion(); + specVersion = "1.2"; + return "quickshell"; +} + +uint NotificationServer::Notify( + const QString& appName, + uint replacesId, + const QString& appIcon, + const QString& summary, + const QString& body, + const QStringList& actions, + const QVariantMap& hints, + int expireTimeout +) { + auto* notification = replacesId == 0 ? nullptr : this->idMap.value(replacesId); + auto old = notification != nullptr; + + if (!notification) { + notification = new Notification(this->nextId++, this); + QQmlEngine::setObjectOwnership(notification, QQmlEngine::CppOwnership); + } + + notification->updateProperties(appName, appIcon, summary, body, actions, hints, expireTimeout); + + if (!old) { + emit this->notification(notification); + + if (!notification->isTracked()) { + emit this->NotificationClosed(notification->id(), notification->closeReason()); + delete notification; + } else { + this->idMap.insert(notification->id(), notification); + this->mNotifications.insertObject(notification); + } + } + + return notification->id(); +} + +} // namespace qs::service::notifications diff --git a/src/services/notifications/server.hpp b/src/services/notifications/server.hpp new file mode 100644 index 00000000..de140f8c --- /dev/null +++ b/src/services/notifications/server.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "../../core/model.hpp" +#include "notification.hpp" + +namespace qs::service::notifications { + +struct NotificationServerSupport { + bool persistence = false; + bool body = true; + bool bodyMarkup = false; + bool bodyHyperlinks = false; + bool bodyImages = false; + bool actions = false; + bool actionIcons = false; + bool image = false; + QVector extraHints; +}; + +class NotificationServer: public QObject { + Q_OBJECT; + +public: + static NotificationServer* instance(); + + void switchGeneration(bool reEmit, const std::function& clearHook); + ObjectModel* trackedNotifications(); + void deleteNotification(Notification* notification, NotificationCloseReason::Enum reason); + + // NOLINTBEGIN + void CloseNotification(uint id); + QStringList GetCapabilities() const; + static QString GetServerInformation(QString& vendor, QString& version, QString& specVersion); + uint Notify( + const QString& appName, + uint replacesId, + const QString& appIcon, + const QString& summary, + const QString& body, + const QStringList& actions, + const QVariantMap& hints, + int expireTimeout + ); + // NOLINTEND + + NotificationServerSupport support; + +signals: + void notification(Notification* notification); + + // NOLINTBEGIN + void NotificationClosed(quint32 id, quint32 reason); + void ActionInvoked(quint32 id, QString action); + // NOLINTEND + +private slots: + void onServiceUnregistered(const QString& service); + +private: + explicit NotificationServer(); + + static void tryRegister(); + + QDBusServiceWatcher serviceWatcher; + quint32 nextId = 1; + QHash idMap; + ObjectModel mNotifications {this}; +}; + +} // namespace qs::service::notifications From 7c5632ef5fb73c6c02c841de675f6f1f8469a23b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 12 Jul 2024 13:44:09 -0700 Subject: [PATCH 077/305] service/upower: start upower dbus service if inactive --- src/dbus/CMakeLists.txt | 1 + src/dbus/bus.cpp | 57 ++++++++++++++++++++++++++++++++++++ src/dbus/bus.hpp | 18 ++++++++++++ src/services/upower/core.cpp | 21 ++++++++++--- src/services/upower/core.hpp | 1 + 5 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 src/dbus/bus.cpp create mode 100644 src/dbus/bus.hpp diff --git a/src/dbus/CMakeLists.txt b/src/dbus/CMakeLists.txt index ee6df30a..49a4a06b 100644 --- a/src/dbus/CMakeLists.txt +++ b/src/dbus/CMakeLists.txt @@ -9,6 +9,7 @@ qt_add_dbus_interface(DBUS_INTERFACES qt_add_library(quickshell-dbus STATIC properties.cpp + bus.cpp ${DBUS_INTERFACES} ) diff --git a/src/dbus/bus.cpp b/src/dbus/bus.cpp new file mode 100644 index 00000000..6f560e9e --- /dev/null +++ b/src/dbus/bus.cpp @@ -0,0 +1,57 @@ +#include "bus.hpp" // NOLINT +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qs::dbus { + +Q_LOGGING_CATEGORY(logDbus, "quickshell.dbus", QtWarningMsg); + +void tryLaunchService( + QObject* parent, + QDBusConnection& connection, + const QString& serviceName, + const std::function& callback +) { + qCDebug(logDbus) << "Attempting to launch service" << serviceName; + + auto message = QDBusMessage::createMethodCall( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "StartServiceByName" + ); + + message << serviceName << 0u; + auto pendingCall = connection.asyncCall(message); + + auto* call = new QDBusPendingCallWatcher(pendingCall, parent); + + auto responseCallback = [callback, serviceName](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logDbus).noquote().nospace() + << "Could not launch service " << serviceName << ": " << reply.error(); + callback(false); + } else { + qCDebug(logDbus) << "Service launch successful for" << serviceName; + callback(true); + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, parent, responseCallback); +} + +} // namespace qs::dbus diff --git a/src/dbus/bus.hpp b/src/dbus/bus.hpp new file mode 100644 index 00000000..1c4c71e4 --- /dev/null +++ b/src/dbus/bus.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include +#include +#include + +namespace qs::dbus { + +void tryLaunchService( + QObject* parent, + QDBusConnection& connection, + const QString& serviceName, + const std::function& callback +); + +} diff --git a/src/services/upower/core.cpp b/src/services/upower/core.cpp index de952e00..b8ba9abe 100644 --- a/src/services/upower/core.cpp +++ b/src/services/upower/core.cpp @@ -14,6 +14,7 @@ #include #include "../../core/model.hpp" +#include "../../dbus/bus.hpp" #include "../../dbus/properties.hpp" #include "dbus_service.h" #include "device.hpp" @@ -23,7 +24,7 @@ namespace qs::service::upower { Q_LOGGING_CATEGORY(logUPower, "quickshell.service.upower", QtWarningMsg); UPower::UPower() { - qCDebug(logUPower) << "Starting UPower"; + qCDebug(logUPower) << "Starting UPower Service"; auto bus = QDBusConnection::systemBus(); @@ -36,10 +37,22 @@ UPower::UPower() { new DBusUPowerService("org.freedesktop.UPower", "/org/freedesktop/UPower", bus, this); if (!this->service->isValid()) { - qCWarning(logUPower) << "Cannot connect to the UPower service."; - return; - } + qCDebug(logUPower) << "UPower service is not currently running, attempting to start it."; + dbus::tryLaunchService(this, bus, "org.freedesktop.UPower", [this](bool success) { + if (success) { + qCDebug(logUPower) << "Successfully launched UPower service."; + this->init(); + } else { + qCWarning(logUPower) << "Could not start UPower. The UPower service will not work."; + } + }); + } else { + this->init(); + } +} + +void UPower::init() { QObject::connect( &this->pOnBattery, &dbus::AbstractDBusProperty::changed, diff --git a/src/services/upower/core.hpp b/src/services/upower/core.hpp index 3b2f8602..aaeed5ac 100644 --- a/src/services/upower/core.hpp +++ b/src/services/upower/core.hpp @@ -35,6 +35,7 @@ private slots: private: explicit UPower(); + void init(); void registerExisting(); void registerDevice(const QString& path); From 609834d8f2902202afd5353e4fe80b928e473c11 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 12 Jul 2024 21:21:35 -0700 Subject: [PATCH 078/305] core/retainable: add Retainable and RetainableLock --- src/core/CMakeLists.txt | 1 + src/core/module.md | 3 +- src/core/retainable.cpp | 163 ++++++++++++++++++++++++++++++++++++++++ src/core/retainable.hpp | 162 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 src/core/retainable.cpp create mode 100644 src/core/retainable.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index d730d1dd..6eace03b 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -32,6 +32,7 @@ qt_add_library(quickshell-core STATIC objectrepeater.cpp platformmenu.cpp qsmenu.cpp + retainable.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/module.md b/src/core/module.md index c70b4876..f0d296a5 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -22,6 +22,7 @@ headers = [ "elapsedtimer.hpp", "desktopentry.hpp", "objectrepeater.hpp", - "qsmenu.hpp" + "qsmenu.hpp", + "retainable.hpp", ] ----- diff --git a/src/core/retainable.cpp b/src/core/retainable.cpp new file mode 100644 index 00000000..4e77e051 --- /dev/null +++ b/src/core/retainable.cpp @@ -0,0 +1,163 @@ +#include "retainable.hpp" + +#include +#include +#include +#include + +RetainableHook* RetainableHook::getHook(QObject* object, bool create) { + auto v = object->property("__qs_retainable"); + + if (v.canConvert()) { + return v.value(); + } else if (create) { + auto* retainable = dynamic_cast(object); + if (!retainable) return nullptr; + + auto* hook = new RetainableHook(object); + hook->retainableFacet = retainable; + retainable->hook = hook; + + object->setProperty("__qs_retainable", QVariant::fromValue(hook)); + + return hook; + } else return nullptr; +} + +RetainableHook* RetainableHook::qmlAttachedProperties(QObject* object) { + return RetainableHook::getHook(object, true); +} + +void RetainableHook::ref() { this->refcount++; } + +void RetainableHook::unref() { + this->refcount--; + if (this->refcount == 0) this->unlocked(); +} + +void RetainableHook::lock() { + this->explicitRefcount++; + this->ref(); +} + +void RetainableHook::unlock() { + if (this->explicitRefcount < 1) { + qWarning() << "Retainable object" << this->parent() + << "unlocked more times than it was locked!"; + } else { + this->explicitRefcount--; + this->unref(); + } +} + +void RetainableHook::forceUnlock() { this->unlocked(); } + +bool RetainableHook::isRetained() const { return !this->inactive; } + +void RetainableHook::unlocked() { + if (this->inactive) return; + + emit this->aboutToDestroy(); + this->retainableFacet->retainFinished(); +} + +void Retainable::retainedDestroy() { + this->retaining = true; + + auto* hook = RetainableHook::getHook(dynamic_cast(this), false); + + if (hook) { + // let all signal handlers run before acting on changes + emit hook->dropped(); + hook->inactive = false; + + if (hook->refcount == 0) hook->unlocked(); + else emit hook->retainedChanged(); + } else { + this->retainFinished(); + } +} + +bool Retainable::isRetained() const { return this->retaining; } + +void Retainable::retainFinished() { + // a normal delete tends to cause deref errors in a listview. + dynamic_cast(this)->deleteLater(); +} + +RetainableLock::~RetainableLock() { + if (this->mEnabled && this->mObject) { + this->hook->unref(); + } +} + +QObject* RetainableLock::object() const { return this->mObject; } + +void RetainableLock::setObject(QObject* object) { + if (object == this->mObject) return; + + if (this->mObject) { + QObject::disconnect(this->mObject, nullptr, this, nullptr); + if (this->hook->isRetained()) emit this->retainedChanged(); + this->hook->unref(); + } + + this->mObject = nullptr; + this->hook = nullptr; + + if (object) { + if (auto* hook = RetainableHook::getHook(object, true)) { + this->mObject = object; + this->hook = hook; + + QObject::connect(object, &QObject::destroyed, this, &RetainableLock::onObjectDestroyed); + QObject::connect(hook, &RetainableHook::dropped, this, &RetainableLock::dropped); + QObject::connect( + hook, + &RetainableHook::aboutToDestroy, + this, + &RetainableLock::aboutToDestroy + ); + QObject::connect( + hook, + &RetainableHook::retainedChanged, + this, + &RetainableLock::retainedChanged + ); + if (hook->isRetained()) emit this->retainedChanged(); + + hook->ref(); + } else { + qCritical() << "Tried to set non retainable object" << object << "as the target of" << this; + } + } + + emit this->objectChanged(); +} + +void RetainableLock::onObjectDestroyed() { + this->mObject = nullptr; + this->hook = nullptr; + + emit this->objectChanged(); +} + +bool RetainableLock::locked() const { return this->mEnabled; } + +void RetainableLock::setLocked(bool locked) { + if (locked == this->mEnabled) return; + + this->mEnabled = locked; + + if (this->mObject) { + if (locked) this->hook->ref(); + else { + if (this->hook->isRetained()) emit this->retainedChanged(); + this->hook->unref(); + } + } + + emit this->lockedChanged(); +} + +bool RetainableLock::isRetained() const { return this->mObject && this->hook->isRetained(); } diff --git a/src/core/retainable.hpp b/src/core/retainable.hpp new file mode 100644 index 00000000..598d6f95 --- /dev/null +++ b/src/core/retainable.hpp @@ -0,0 +1,162 @@ +#pragma once + +#include +#include +#include +#include + +class Retainable; + +///! Attached object for types that can have delayed destruction. +/// Retainable works as an attached property that allows objects to be +/// kept around (retained) after they would normally be destroyed, which +/// is especially useful for things like exit transitions. +/// +/// An object that is retainable will have `Retainable` as an attached property. +/// All retainable objects will say that they are retainable on their respective +/// typeinfo pages. +/// +/// > [!INFO] Working directly with Retainable is often overly complicated and +/// > error prone. For this reason [RetainableLock](../retainablelock) should +/// > usually be used instead. +class RetainableHook: public QObject { + Q_OBJECT; + /// If the object is currently in a retained state. + Q_PROPERTY(bool retained READ isRetained NOTIFY retainedChanged); + QML_ATTACHED(RetainableHook); + QML_NAMED_ELEMENT(Retainable); + QML_UNCREATABLE("Retainable can only be used as an attached object."); + +public: + static RetainableHook* getHook(QObject* object, bool create = false); + + void destroyOnRelease(); + + void ref(); + void unref(); + + /// Hold a lock on the object so it cannot be destroyed. + /// + /// A counter is used to ensure you can lock the object from multiple places + /// and it will not be unlocked until the same number of unlocks as locks have occurred. + /// + /// > [!WARNING] It is easy to forget to unlock a locked object. + /// > Doing so will create what is effectively a memory leak. + /// > + /// > Using [RetainableLock](../retainablelock) is recommended as it will help + /// > avoid this scenario and make misuse more obvious. + Q_INVOKABLE void lock(); + /// Remove a lock on the object. See `lock()` for more information. + Q_INVOKABLE void unlock(); + /// Forcibly remove all locks, destroying the object. + /// + /// `unlock()` should usually be preferred. + Q_INVOKABLE void forceUnlock(); + + [[nodiscard]] bool isRetained() const; + + static RetainableHook* qmlAttachedProperties(QObject* object); + +signals: + /// This signal is sent when the object would normally be destroyed. + /// + /// If all signal handlers return and no locks are in place, the object will be destroyed. + /// If at least one lock is present the object will be retained until all are removed. + void dropped(); + /// This signal is sent immediately before the object is destroyed. + /// At this point destruction cannot be interrupted. + void aboutToDestroy(); + + void retainedChanged(); + +private: + explicit RetainableHook(QObject* parent): QObject(parent) {} + + void unlocked(); + + uint refcount = 0; + // tracked separately so a warning can be given when unlock is called too many times, + // without affecting other lock sources such as RetainableLock. + uint explicitRefcount = 0; + Retainable* retainableFacet = nullptr; + bool inactive = true; + + friend class Retainable; +}; + +class Retainable { +public: + Retainable() = default; + virtual ~Retainable() = default; + Q_DISABLE_COPY_MOVE(Retainable); + + void retainedDestroy(); + [[nodiscard]] bool isRetained() const; + +protected: + virtual void retainFinished(); + +private: + RetainableHook* hook = nullptr; + bool retaining = false; + + friend class RetainableHook; +}; + +///! A helper for easily using Retainable. +/// A RetainableLock provides extra safety and ease of use for locking +/// [Retainable](../retainable) objects. A retainable object can be locked +/// by multiple locks at once, and each lock re-exposes relevant properties +/// of the retained objects. +/// +/// #### Example +/// The code below will keep a retainable object alive for as long as the +/// RetainableLock exists. +/// +/// ```qml +/// RetainableLock { +/// object: aRetainableObject +/// locked: true +/// } +/// ``` +class RetainableLock: public QObject { + Q_OBJECT; + /// The object to lock. Must be [Retainable](../retainable). + Q_PROPERTY(QObject* object READ object WRITE setObject NOTIFY objectChanged); + /// If the object should be locked. + Q_PROPERTY(bool locked READ locked WRITE setLocked NOTIFY lockedChanged); + /// If the object is currently in a retained state. + Q_PROPERTY(bool retained READ isRetained NOTIFY retainedChanged); + QML_ELEMENT; + +public: + explicit RetainableLock(QObject* parent = nullptr): QObject(parent) {} + ~RetainableLock() override; + Q_DISABLE_COPY_MOVE(RetainableLock); + + [[nodiscard]] QObject* object() const; + void setObject(QObject* object); + + [[nodiscard]] bool locked() const; + void setLocked(bool locked); + + [[nodiscard]] bool isRetained() const; + +signals: + /// Rebroadcast of the object's `dropped()` signal. + void dropped(); + /// Rebroadcast of the object's `aboutToDestroy()` signal. + void aboutToDestroy(); + void retainedChanged(); + + void objectChanged(); + void lockedChanged(); + +private slots: + void onObjectDestroyed(); + +private: + QObject* mObject = nullptr; + RetainableHook* hook = nullptr; + bool mEnabled = false; +}; From e23923d9a234e023ae7f2e69f04fb32514a6b5df Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 12 Jul 2024 21:25:46 -0700 Subject: [PATCH 079/305] service/notifications: make notifications Retainable --- src/services/notifications/notification.cpp | 10 ++++++++++ src/services/notifications/notification.hpp | 9 ++++++++- src/services/notifications/server.cpp | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/services/notifications/notification.cpp b/src/services/notifications/notification.cpp index e267699b..46a337aa 100644 --- a/src/services/notifications/notification.cpp +++ b/src/services/notifications/notification.cpp @@ -39,6 +39,11 @@ QString NotificationAction::identifier() const { return this->mIdentifier; } QString NotificationAction::text() const { return this->mText; } void NotificationAction::invoke() { + if (this->notification->isRetained()) { + qCritical() << "Cannot invoke destroyed notification" << this; + return; + } + NotificationServer::instance()->ActionInvoked(this->notification->id(), this->mIdentifier); if (!this->notification->isResident()) { @@ -57,6 +62,11 @@ void Notification::expire() { this->close(NotificationCloseReason::Expired); } void Notification::dismiss() { this->close(NotificationCloseReason::Dismissed); } void Notification::close(NotificationCloseReason::Enum reason) { + if (this->isRetained()) { + qCritical() << "Cannot close destroyed notification" << this; + return; + } + this->mCloseReason = reason; if (reason != 0) { diff --git a/src/services/notifications/notification.hpp b/src/services/notifications/notification.hpp index 5be56172..2dab36dc 100644 --- a/src/services/notifications/notification.hpp +++ b/src/services/notifications/notification.hpp @@ -8,6 +8,8 @@ #include #include +#include "../../core/retainable.hpp" + namespace qs::service::notifications { class NotificationImage; @@ -50,7 +52,12 @@ public: class NotificationAction; ///! A notification emitted by a NotificationServer. -class Notification: public QObject { +/// A notification emitted by a NotificationServer. +/// > [!INFO] This type is [Retainable](/docs/types/quickshell/retainable). It +/// > can be retained after destruction if necessary. +class Notification + : public QObject + , public Retainable { Q_OBJECT; /// Id of the notification as given to the client. Q_PROPERTY(quint32 id READ id CONSTANT); diff --git a/src/services/notifications/server.cpp b/src/services/notifications/server.cpp index 5b89a894..3e035e0c 100644 --- a/src/services/notifications/server.cpp +++ b/src/services/notifications/server.cpp @@ -105,7 +105,7 @@ void NotificationServer::deleteNotification( this->idMap.remove(notification->id()); emit this->NotificationClosed(notification->id(), reason); - delete notification; + notification->retainedDestroy(); } void NotificationServer::tryRegister() { From c4cc662bccbe03ee58ee2c4a7146ef802b555f74 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 12 Jul 2024 22:52:40 -0700 Subject: [PATCH 080/305] core/objectmodel: fix objectInserted signal indexes --- src/core/model.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/model.cpp b/src/core/model.cpp index 9aaa1472..bcba6a15 100644 --- a/src/core/model.cpp +++ b/src/core/model.cpp @@ -39,7 +39,7 @@ QObject* UntypedObjectModel::valueAt(QQmlListProperty* property, qsizet void UntypedObjectModel::insertObject(QObject* object, qsizetype index) { auto iindex = index == -1 ? this->valuesList.length() : index; - emit this->objectInsertedPre(object, index); + emit this->objectInsertedPre(object, iindex); auto intIndex = static_cast(iindex); this->beginInsertRows(QModelIndex(), intIndex, intIndex); @@ -47,7 +47,7 @@ void UntypedObjectModel::insertObject(QObject* object, qsizetype index) { this->endInsertRows(); emit this->valuesChanged(); - emit this->objectInsertedPost(object, index); + emit this->objectInsertedPost(object, iindex); } void UntypedObjectModel::removeAt(qsizetype index) { From e9cacbd92daf19c92f083037f6e0ef70dc687218 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 14 Jul 2024 16:17:51 -0700 Subject: [PATCH 081/305] all: use type/prop shorthand in docs --- src/core/desktopentry.hpp | 4 ++-- src/core/lazyloader.hpp | 2 +- src/core/objectrepeater.hpp | 10 +++------- src/core/qmlscreen.hpp | 7 ++----- src/core/qsmenu.hpp | 2 +- src/core/reload.hpp | 5 +---- src/core/retainable.hpp | 8 ++++---- src/core/variants.hpp | 7 ++----- src/io/datastream.hpp | 4 ++-- src/services/notifications/notification.hpp | 16 ++++++---------- src/services/notifications/qml.hpp | 10 ++++------ src/services/pipewire/qml.hpp | 2 +- src/services/status_notifier/qml.hpp | 4 ++-- src/wayland/session_lock.hpp | 11 +++-------- src/wayland/toplevel_management/qml.hpp | 4 ++-- src/wayland/wlr_layershell.hpp | 3 +-- src/wayland/wlr_layershell/window.hpp | 2 +- 17 files changed, 38 insertions(+), 63 deletions(-) diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index 81cfa8f0..e227eb19 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -13,7 +13,7 @@ class DesktopAction; -/// A desktop entry. See [DesktopEntries](../desktopentries) for details. +/// A desktop entry. See @@DesktopEntries for details. class DesktopEntry: public QObject { Q_OBJECT; Q_PROPERTY(QString id MEMBER mId CONSTANT); @@ -75,7 +75,7 @@ private: friend class DesktopAction; }; -/// An action of a [DesktopEntry](../desktopentry). +/// An action of a @@DesktopEntry$. class DesktopAction: public QObject { Q_OBJECT; Q_PROPERTY(QString id MEMBER mId CONSTANT); diff --git a/src/core/lazyloader.hpp b/src/core/lazyloader.hpp index 8ef935f6..34bd2a70 100644 --- a/src/core/lazyloader.hpp +++ b/src/core/lazyloader.hpp @@ -79,7 +79,7 @@ /// > [!WARNING] Components that internally load other components must explicitly /// > support asynchronous loading to avoid blocking. /// > -/// > Notably, [Variants](../variants) does not corrently support asynchronous +/// > 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. /// diff --git a/src/core/objectrepeater.hpp b/src/core/objectrepeater.hpp index 0361636f..2350971c 100644 --- a/src/core/objectrepeater.hpp +++ b/src/core/objectrepeater.hpp @@ -14,9 +14,7 @@ /// > [!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 [Repeater] but for non visual types. -/// -/// [Repeater]: https://doc.qt.io/qt-6/qml-qtquick-repeater.html +/// 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. @@ -25,10 +23,9 @@ class ObjectRepeater: public ObjectModel { /// and [QAbstractListModel] derived models, though only one column will be repeated /// from the latter. /// - /// Note: [ObjectModel] is a [QAbstractListModel] with a single column. + /// Note: @@ObjectModel is a [QAbstractListModel] with a single column. /// /// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html - /// [ObjectModel]: ../objectmodel Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged); /// The delegate component to repeat. /// @@ -39,10 +36,9 @@ class ObjectRepeater: public ObjectModel { /// 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. + /// Note: @@ObjectModel has a single role named `modelData` for compatibility with normal lists. /// /// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html - /// [ObjectModel]: ../objectmodel Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged); Q_CLASSINFO("DefaultProperty", "delegate"); QML_ELEMENT; diff --git a/src/core/qmlscreen.hpp b/src/core/qmlscreen.hpp index dfebf331..69c0762d 100644 --- a/src/core/qmlscreen.hpp +++ b/src/core/qmlscreen.hpp @@ -12,17 +12,14 @@ // unfortunately QQuickScreenInfo is private. -/// Monitor object useful for setting the monitor for a [ShellWindow] +/// Monitor object useful for setting the monitor for a @@QsWindow /// or querying information about the monitor. /// /// > [!WARNING] If the monitor is disconnected than any stored copies of its ShellMonitor will /// > be marked as dangling and all properties will return default values. /// > Reconnecting the monitor will not reconnect it to the ShellMonitor object. /// -/// Due to some technical limitations, it was not possible to reuse the native qml [Screen] type. -/// -/// [ShellWindow]: ../shellwindow -/// [Screen]: https://doc.qt.io/qt-6/qml-qtquick-screen.html +/// Due to some technical limitations, it was not possible to reuse the native qml @@QtQuick.Screen type. class QuickshellScreenInfo: public QObject { Q_OBJECT; QML_NAMED_ELEMENT(ShellScreen); diff --git a/src/core/qsmenu.hpp b/src/core/qsmenu.hpp index a5f38225..2d2413a4 100644 --- a/src/core/qsmenu.hpp +++ b/src/core/qsmenu.hpp @@ -60,7 +60,7 @@ class QsMenuEntry: public QObject { /// The check state of the checkbox or radiobutton if applicable, as a /// [Qt.CheckState](https://doc.qt.io/qt-6/qt.html#CheckState-enum). Q_PROPERTY(Qt::CheckState checkState READ checkState NOTIFY checkStateChanged); - /// If this menu item has children that can be accessed through a [QsMenuOpener](../qsmenuopener). + /// If this menu item has children that can be accessed through a @@QsMenuOpener$. Q_PROPERTY(bool hasChildren READ hasChildren NOTIFY hasChildrenChanged); QML_ELEMENT; QML_UNCREATABLE("QsMenuEntry cannot be directly created"); diff --git a/src/core/reload.hpp b/src/core/reload.hpp index 0d33e2b6..36956f5a 100644 --- a/src/core/reload.hpp +++ b/src/core/reload.hpp @@ -11,10 +11,7 @@ class EngineGeneration; ///! The base class of all types that can be reloaded. /// Reloadables will attempt to take specific state from previous config revisions if possible. -/// Some examples are [ProxyWindowBase] and [PersistentProperties] -/// -/// [ProxyWindowBase]: ../proxywindowbase -/// [PersistentProperties]: ../persistentproperties +/// Some examples are @@ProxyWindowBase and @@PersistentProperties class Reloadable : public QObject , public QQmlParserStatus { diff --git a/src/core/retainable.hpp b/src/core/retainable.hpp index 598d6f95..5ef02f80 100644 --- a/src/core/retainable.hpp +++ b/src/core/retainable.hpp @@ -17,7 +17,7 @@ class Retainable; /// typeinfo pages. /// /// > [!INFO] Working directly with Retainable is often overly complicated and -/// > error prone. For this reason [RetainableLock](../retainablelock) should +/// > error prone. For this reason @@RetainableLock should /// > usually be used instead. class RetainableHook: public QObject { Q_OBJECT; @@ -43,7 +43,7 @@ public: /// > [!WARNING] It is easy to forget to unlock a locked object. /// > Doing so will create what is effectively a memory leak. /// > - /// > Using [RetainableLock](../retainablelock) is recommended as it will help + /// > Using @@RetainableLock is recommended as it will help /// > avoid this scenario and make misuse more obvious. Q_INVOKABLE void lock(); /// Remove a lock on the object. See `lock()` for more information. @@ -105,8 +105,8 @@ private: ///! A helper for easily using Retainable. /// A RetainableLock provides extra safety and ease of use for locking -/// [Retainable](../retainable) objects. A retainable object can be locked -/// by multiple locks at once, and each lock re-exposes relevant properties +/// @@Retainable objects. A retainable object can be locked by multiple +/// locks at once, and each lock re-exposes relevant properties /// of the retained objects. /// /// #### Example diff --git a/src/core/variants.hpp b/src/core/variants.hpp index a9071cf8..f0f3c6f1 100644 --- a/src/core/variants.hpp +++ b/src/core/variants.hpp @@ -28,20 +28,17 @@ public: ///! Creates instances of a component based on a given model. /// Creates and destroys instances of the given component when the given property changes. /// -/// `Variants` is similar to [Repeater] except it is for *non Item* objects, and acts as +/// `Variants` is similar to @@QtQuick.Repeater except it is for *non Item* objects, and acts as /// a reload scope. /// /// Each non duplicate value passed to [model](#prop.model) will create a new instance of /// [delegate](#prop.delegate) with its `modelData` property set to that value. /// -/// See [Quickshell.screens] for an example of using `Variants` to create copies of a window per +/// See @@Quickshell.screens for an example of using `Variants` to create copies of a window per /// screen. /// /// > [!WARNING] BUG: Variants currently fails to reload children if the variant set is changed as /// > it is instantiated. (usually due to a mutation during variant creation) -/// -/// [Repeater]: https://doc.qt.io/qt-6/qml-qtquick-repeater.html -/// [Quickshell.screens]: ../quickshell#prop.screens class Variants: public Reloadable { Q_OBJECT; /// The component to create instances of. diff --git a/src/io/datastream.hpp b/src/io/datastream.hpp index 24aa54a4..76a25f2b 100644 --- a/src/io/datastream.hpp +++ b/src/io/datastream.hpp @@ -10,7 +10,7 @@ class DataStreamParser; ///! Data source that can be streamed into a parser. -/// See also: [DataStreamParser](../datastreamparser) +/// See also: @@DataStreamParser class DataStream: public QObject { Q_OBJECT; /// The parser to stream data from this source into. @@ -43,7 +43,7 @@ protected: }; ///! Parser for streamed input data. -/// See also: [DataStream](../datastream), [SplitParser](../splitparser) +/// See also: @@DataStream$, @@SplitParser class DataStreamParser: public QObject { Q_OBJECT; QML_ELEMENT; diff --git a/src/services/notifications/notification.hpp b/src/services/notifications/notification.hpp index 2dab36dc..4f34f7b6 100644 --- a/src/services/notifications/notification.hpp +++ b/src/services/notifications/notification.hpp @@ -53,7 +53,7 @@ class NotificationAction; ///! A notification emitted by a NotificationServer. /// A notification emitted by a NotificationServer. -/// > [!INFO] This type is [Retainable](/docs/types/quickshell/retainable). It +/// > [!INFO] This type is @@Quickshell.Retainable$. It /// > can be retained after destruction if necessary. class Notification : public QObject @@ -68,8 +68,8 @@ class Notification /// If this notification was carried over from the last generation /// when quickshell reloaded. /// - /// Notifications from the last generation will only be emitted if - /// [NotificationServer.keepOnReload](../notificationserver#prop.keepOnReload) is true. + /// Notifications from the last generation will only be emitted + /// if @@NotificationServer.keepOnReloadis true. Q_PROPERTY(bool lastGeneration READ isLastGeneration CONSTANT); /// Time in seconds the notification should be valid for Q_PROPERTY(qreal expireTimeout READ expireTimeout NOTIFY expireTimeoutChanged); @@ -86,7 +86,7 @@ class Notification Q_PROPERTY(QVector actions READ actions NOTIFY actionsChanged); /// If actions associated with this notification have icons available. /// - /// See [NotificationAction.identifier](../notificationaction#prop.identifier) for details. + /// See @@NotificationAction.identifier for details. Q_PROPERTY(bool hasActionIcons READ hasActionIcons NOTIFY hasActionIconsChanged); /// If true, the notification will not be destroyed after an action is invoked. Q_PROPERTY(bool resident READ isResident NOTIFY isResidentChanged); @@ -194,10 +194,8 @@ class NotificationAction: public QObject { Q_OBJECT; /// The identifier of the action. /// - /// When [Notification.hasActionIcons] is true, this property will be an icon name. + /// When @@Notification.hasActionIcons is true, this property will be an icon name. /// When it is false, this property is irrelevant. - /// - /// [Notification.hasActionIcons]: ../notification#prop.hasActionIcons Q_PROPERTY(QString identifier READ identifier CONSTANT); /// The localized text that should be displayed on a button. Q_PROPERTY(QString text READ text NOTIFY textChanged); @@ -211,9 +209,7 @@ public: , mIdentifier(std::move(identifier)) , mText(std::move(text)) {} - /// Invoke the action. If [Notification.resident] is false it will be dismissed. - /// - /// [Notification.resident]: ../notification#prop.resident + /// Invoke the action. If @@Notification.resident is false it will be dismissed. Q_INVOKABLE void invoke(); [[nodiscard]] QString identifier() const; diff --git a/src/services/notifications/qml.hpp b/src/services/notifications/qml.hpp index 341eaf2a..86851030 100644 --- a/src/services/notifications/qml.hpp +++ b/src/services/notifications/qml.hpp @@ -27,7 +27,7 @@ class NotificationServerQml // clang-format off /// If notifications should be re-emitted when quickshell reloads. Defaults to true. /// - /// The [lastGeneration](../notification#prop.lastGeneration) flag will be + /// The @@Notification.lastGeneration flag will be /// set on notifications from the prior generation for further filtering/handling. Q_PROPERTY(bool keepOnReload READ keepOnReload WRITE setKeepOnReload NOTIFY keepOnReloadChanged); /// If the notification server should advertise that it can persist notifications in the background @@ -42,11 +42,9 @@ class NotificationServerQml /// If notification body text should be advertised as supporting markup as described in [the specification] /// Defaults to false. /// - /// Note that returned notifications may still contain markup if this property is false, as it is only a hint. - /// By default Text objects will try to render markup. To avoid this if any is sent, change [Text.textFormat] to `PlainText`. - /// - /// [the specification]: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#markup - /// [Text.textFormat]: https://doc.qt.io/qt-6/qml-qtquick-text.html#textFormat-prop + /// Note that returned notifications may still contain markup if this property is false, + /// as it is only a hint. By default Text objects will try to render markup. To avoid this + /// if any is sent, change @@QtQuick.Text.textFormat to `PlainText`. Q_PROPERTY(bool bodyMarkupSupported READ bodyMarkupSupported WRITE setBodyMarkupSupported NOTIFY bodyMarkupSupportedChanged); /// If notification body text should be advertised as supporting hyperlinks as described in [the specification] /// Defaults to false. diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 78e75633..708b7282 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -249,7 +249,7 @@ private: ///! A connection between pipewire nodes. /// Note that there is one link per *channel* of a connection between nodes. -/// You usually want [PwLinkGroup](../pwlinkgroup). +/// You usually want @@PwLinkGroup$. class PwLinkIface: public PwObjectIface { Q_OBJECT; /// The pipewire object id of the link. diff --git a/src/services/status_notifier/qml.hpp b/src/services/status_notifier/qml.hpp index 9510285a..7c89055a 100644 --- a/src/services/status_notifier/qml.hpp +++ b/src/services/status_notifier/qml.hpp @@ -61,7 +61,7 @@ class SystemTrayItem: public QObject { Q_PROPERTY(QString tooltipTitle READ tooltipTitle NOTIFY tooltipTitleChanged); Q_PROPERTY(QString tooltipDescription READ tooltipDescription NOTIFY tooltipDescriptionChanged); /// If this tray item has an associated menu accessible via `display` - /// or a [SystemTrayMenuWatcher](../systemtraymenuwatcher). + /// or a @@SystemTrayMenuWatcher$. Q_PROPERTY(bool hasMenu READ hasMenu NOTIFY hasMenuChanged); /// If this tray item only offers a menu and activation will do nothing. Q_PROPERTY(bool onlyMenu READ onlyMenu NOTIFY onlyMenuChanged); @@ -133,7 +133,7 @@ private: ///! Accessor for SystemTrayItem menus. /// SystemTrayMenuWatcher provides access to the associated -/// [DBusMenuItem](../../quickshell.dbusmenu/dbusmenuitem) for a tray item. +/// @@Quickshell.DBusMenu.DBusMenuItem for a tray item. class SystemTrayMenuWatcher: public QObject { using DBusMenu = qs::dbus::dbusmenu::DBusMenu; using DBusMenuItem = qs::dbus::dbusmenu::DBusMenuItem; diff --git a/src/wayland/session_lock.hpp b/src/wayland/session_lock.hpp index a44dae0c..9a34eafa 100644 --- a/src/wayland/session_lock.hpp +++ b/src/wayland/session_lock.hpp @@ -25,7 +25,7 @@ class WlSessionLockSurface; /// Wayland session lock implemented using the [ext_session_lock_v1] protocol. /// /// WlSessionLock will create an instance of its `surface` component for every screen when -/// `locked` is set to true. The `surface` component must create a [WlSessionLockSurface] +/// `locked` is set to true. The `surface` component must create a @@WlSessionLockSurface /// which will be displayed on each screen. /// /// The below example will create a session lock that disappears when the button is clicked. @@ -53,7 +53,6 @@ class WlSessionLockSurface; /// > but it will render it inoperable. /// /// [ext_session_lock_v1]: https://wayland.app/protocols/ext-session-lock-v1 -/// [WlSessionLockSurface]: ../wlsessionlocksurface class WlSessionLock: public Reloadable { Q_OBJECT; // clang-format off @@ -66,9 +65,7 @@ class WlSessionLock: public Reloadable { /// /// This is set to true once the compositor has confirmed all screens are covered with locks. Q_PROPERTY(bool secure READ isSecure NOTIFY secureStateChanged); - /// The surface that will be created for each screen. Must create a [WlSessionLockSurface]. - /// - /// [WlSessionLockSurface]: ../wlsessionlocksurface + /// The surface that will be created for each screen. Must create a @@WlSessionLockSurface$. Q_PROPERTY(QQmlComponent* surface READ surfaceComponent WRITE setSurfaceComponent NOTIFY surfaceComponentChanged); // clang-format on QML_ELEMENT; @@ -109,9 +106,7 @@ private: }; ///! Surface to display with a `WlSessionLock`. -/// Surface displayed by a [WlSessionLock] when it is locked. -/// -/// [WlSessionLock]: ../wlsessionlock +/// Surface displayed by a @@WlSessionLock when it is locked. class WlSessionLockSurface: public Reloadable { Q_OBJECT; // clang-format off diff --git a/src/wayland/toplevel_management/qml.hpp b/src/wayland/toplevel_management/qml.hpp index 8bb1d551..19d64dfa 100644 --- a/src/wayland/toplevel_management/qml.hpp +++ b/src/wayland/toplevel_management/qml.hpp @@ -17,7 +17,7 @@ class ToplevelHandle; ///! Window from another application. /// A window/toplevel from another application, retrievable from -/// the [ToplevelManager](../toplevelmanager). +/// the @@ToplevelManager$. class Toplevel: public QObject { Q_OBJECT; Q_PROPERTY(QString appId READ appId NOTIFY appIdChanged); @@ -122,7 +122,7 @@ private: }; ///! Exposes a list of Toplevels. -/// Exposes a list of windows from other applications as [Toplevel](../toplevel)s via the +/// Exposes a list of windows from other applications as @@Toplevel$s via the /// [zwlr-foreign-toplevel-management-v1](https://wayland.app/protocols/wlr-foreign-toplevel-management-unstable-v1) /// wayland protocol. class ToplevelManagerQml: public QObject { diff --git a/src/wayland/wlr_layershell.hpp b/src/wayland/wlr_layershell.hpp index cf9abe4f..b289bbe4 100644 --- a/src/wayland/wlr_layershell.hpp +++ b/src/wayland/wlr_layershell.hpp @@ -16,7 +16,7 @@ /// Decorationless window that can be attached to the screen edges using the [zwlr_layer_shell_v1] protocol. /// /// #### Attached property -/// `WlrLayershell` works as an attached property of [PanelWindow] which you should use instead if you can, +/// `WlrLayershell` works as an attached property of @@Quickshell.PanelWindow which you should use instead if you can, /// as it is platform independent. /// ```qml /// PanelWindow { @@ -37,7 +37,6 @@ /// ``` /// /// [zwlr_layer_shell_v1]: https://wayland.app/protocols/wlr-layer-shell-unstable-v1 -/// [PanelWindow]: ../../quickshell/panelwindow class WlrLayershell: public ProxyWindowBase { QSDOC_BASECLASS(PanelWindowInterface); // clang-format off diff --git a/src/wayland/wlr_layershell/window.hpp b/src/wayland/wlr_layershell/window.hpp index 8ce08b5c..b73e8a78 100644 --- a/src/wayland/wlr_layershell/window.hpp +++ b/src/wayland/wlr_layershell/window.hpp @@ -41,7 +41,7 @@ enum Enum { /// /// > [!WARNING] You **CANNOT** use this to make a secure lock screen. /// > - /// > If you want to make a lock screen, use [WlSessionLock](../wlsessionlock). + /// > If you want to make a lock screen, use @@WlSessionLock$. Exclusive = 1, /// Access to the keyboard as determined by the operating system. /// From d1c33d48cde52ce9416662052cb5136406d094f7 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 14 Jul 2024 16:22:01 -0700 Subject: [PATCH 082/305] docs: explain type reference shorthand in CONTRIBUTING --- CONTRIBUTING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6fdef09c..9c781462 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,7 +64,9 @@ Before submitting an MR, if adding new features please make sure the documentati reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo. Doc comments take the form `///` or `///!` (summary) and work with markdown. -Look at existing code for how it works. +You can reference other types using the `@@[Module]..[property]` shorthand +where module and property are optional. If module is not specified it will +be inferred as the current module. Look at existing code for how it works. Quickshell modules additionally have a `module.md` file which contains a summary, description, and list of headers to scan for documentation. From e48af446070cf99613a0d0aa5d1ee455bd40b6e9 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 17 Jul 2024 20:54:29 -0700 Subject: [PATCH 083/305] core/window: add QsWindow attached object to contained Items --- src/core/proxywindow.cpp | 6 ++++++ src/core/proxywindow.hpp | 15 +++++++++++++++ src/core/windowinterface.cpp | 30 +++++++++++++++++++++++++++++- src/core/windowinterface.hpp | 25 +++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index fc78f168..8f8ab0dc 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include "generation.hpp" @@ -123,6 +124,8 @@ void ProxyWindowBase::connectWindow() { generation->registerIncubationController(this->window->incubationController()); } + this->window->setProperty("__qs_proxywindow", QVariant::fromValue(this)); + // clang-format off QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged); QObject::connect(this->window, &QWindow::xChanged, this, &ProxyWindowBase::xChanged); @@ -344,3 +347,6 @@ QQmlListProperty ProxyWindowBase::data() { void ProxyWindowBase::onWidthChanged() { this->mContentItem->setWidth(this->width()); } void ProxyWindowBase::onHeightChanged() { this->mContentItem->setHeight(this->height()); } + +QObject* ProxyWindowAttached::window() const { return this->mWindow; } +QQuickItem* ProxyWindowAttached::contentItem() const { return this->mWindow->contentItem(); } diff --git a/src/core/proxywindow.hpp b/src/core/proxywindow.hpp index 1c62f029..ce8228fe 100644 --- a/src/core/proxywindow.hpp +++ b/src/core/proxywindow.hpp @@ -136,3 +136,18 @@ private: void polishItems(); void updateMask(); }; + +class ProxyWindowAttached: public QsWindowAttached { + Q_OBJECT; + +public: + explicit ProxyWindowAttached(ProxyWindowBase* window) + : QsWindowAttached(window) + , mWindow(window) {} + + [[nodiscard]] QObject* window() const override; + [[nodiscard]] QQuickItem* contentItem() const override; + +private: + ProxyWindowBase* mWindow; +}; diff --git a/src/core/windowinterface.cpp b/src/core/windowinterface.cpp index a29bd599..48f6f2ae 100644 --- a/src/core/windowinterface.cpp +++ b/src/core/windowinterface.cpp @@ -1 +1,29 @@ -#include "windowinterface.hpp" // NOLINT +#include "windowinterface.hpp" + +#include +#include +#include + +#include "proxywindow.hpp" + +QsWindowAttached* WindowInterface::qmlAttachedProperties(QObject* object) { + auto* item = qobject_cast(object); + if (!item) return nullptr; + auto* window = item->window(); + if (!window) return nullptr; + auto* proxy = window->property("__qs_proxywindow").value(); + if (!proxy) return nullptr; + + auto v = proxy->property("__qs_window_attached"); + if (auto* attached = v.value()) { + return attached; + } + + auto* attached = new ProxyWindowAttached(proxy); + + if (attached) { + proxy->setProperty("__qs_window_attached", QVariant::fromValue(attached)); + } + + return attached; +} diff --git a/src/core/windowinterface.hpp b/src/core/windowinterface.hpp index ec50dfd8..ac72a791 100644 --- a/src/core/windowinterface.hpp +++ b/src/core/windowinterface.hpp @@ -13,7 +13,15 @@ #include "reload.hpp" class ProxyWindowBase; +class QsWindowAttached; +///! Base class of Quickshell windows +/// Base class of Quickshell windows +/// ### Attached properties +/// `QSWindow` can be used as an attached object of anything that subclasses @@QtQuick.Item$. +/// It provides the following properties +/// - `window` - the `QSWindow` object. +/// - `contentItem` - the `contentItem` property of the window. class WindowInterface: public Reloadable { Q_OBJECT; // clang-format off @@ -101,6 +109,7 @@ class WindowInterface: public Reloadable { Q_CLASSINFO("DefaultProperty", "data"); QML_NAMED_ELEMENT(QSWindow); QML_UNCREATABLE("uncreatable base class"); + QML_ATTACHED(QsWindowAttached); public: explicit WindowInterface(QObject* parent = nullptr): Reloadable(parent) {} @@ -131,6 +140,8 @@ public: [[nodiscard]] virtual QQmlListProperty data() = 0; + static QsWindowAttached* qmlAttachedProperties(QObject* object); + signals: void windowConnected(); void visibleChanged(); @@ -142,3 +153,17 @@ signals: void colorChanged(); void maskChanged(); }; + +class QsWindowAttached: public QObject { + Q_OBJECT; + Q_PROPERTY(QObject* window READ window CONSTANT); + Q_PROPERTY(QQuickItem* contentItem READ contentItem CONSTANT); + QML_ANONYMOUS; + +public: + [[nodiscard]] virtual QObject* window() const = 0; + [[nodiscard]] virtual QQuickItem* contentItem() const = 0; + +protected: + explicit QsWindowAttached(QObject* parent): QObject(parent) {} +}; From 6367b56f5548a86e9b541f871870a3bb1dc69392 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 18 Jul 2024 01:57:40 -0700 Subject: [PATCH 084/305] core/window: fix attached property prior to backer creation --- src/core/proxywindow.cpp | 3 +-- src/core/windowinterface.cpp | 15 ++++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index 8f8ab0dc..05fbff0a 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -26,6 +26,7 @@ ProxyWindowBase::ProxyWindowBase(QObject* parent) , mContentItem(new QQuickItem()) { QQmlEngine::setObjectOwnership(this->mContentItem, QQmlEngine::CppOwnership); this->mContentItem->setParent(this); + this->mContentItem->setProperty("__qs_proxywindow", QVariant::fromValue(this)); // clang-format off QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onWidthChanged); @@ -124,8 +125,6 @@ void ProxyWindowBase::connectWindow() { generation->registerIncubationController(this->window->incubationController()); } - this->window->setProperty("__qs_proxywindow", QVariant::fromValue(this)); - // clang-format off QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged); QObject::connect(this->window, &QWindow::xChanged, this, &ProxyWindowBase::xChanged); diff --git a/src/core/windowinterface.cpp b/src/core/windowinterface.cpp index 48f6f2ae..d941d0ec 100644 --- a/src/core/windowinterface.cpp +++ b/src/core/windowinterface.cpp @@ -7,11 +7,16 @@ #include "proxywindow.hpp" QsWindowAttached* WindowInterface::qmlAttachedProperties(QObject* object) { - auto* item = qobject_cast(object); - if (!item) return nullptr; - auto* window = item->window(); - if (!window) return nullptr; - auto* proxy = window->property("__qs_proxywindow").value(); + auto* visualRoot = qobject_cast(object); + + ProxyWindowBase* proxy = nullptr; + while (visualRoot != nullptr) { + proxy = visualRoot->property("__qs_proxywindow").value(); + + if (proxy) break; + visualRoot = visualRoot->parentItem(); + }; + if (!proxy) return nullptr; auto v = proxy->property("__qs_window_attached"); From aa3f7daea2a14fffc98a38c41f83c990911deb39 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 19 Jul 2024 02:55:38 -0700 Subject: [PATCH 085/305] wayland/platformmenu: fix flipped positions and submenu y positions --- src/core/platformmenu.cpp | 2 ++ src/core/platformmenu.hpp | 1 + src/wayland/platformmenu.cpp | 33 +++++++++++++++++++++++++++++++-- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/core/platformmenu.cpp b/src/core/platformmenu.cpp index a2f8f813..7b31c871 100644 --- a/src/core/platformmenu.cpp +++ b/src/core/platformmenu.cpp @@ -104,6 +104,8 @@ bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relati this->qmenu->createWinId(); this->qmenu->windowHandle()->setTransientParent(window); + // Skips screen edge repositioning so it can be left to the compositor on wayland. + this->qmenu->targetPosition = point; this->qmenu->popup(point); return true; diff --git a/src/core/platformmenu.hpp b/src/core/platformmenu.hpp index 5c18a57e..c1e39096 100644 --- a/src/core/platformmenu.hpp +++ b/src/core/platformmenu.hpp @@ -26,6 +26,7 @@ public: void setVisible(bool visible) override; PlatformMenuQMenu* containingMenu = nullptr; + QPoint targetPosition; }; class PlatformMenuEntry: public QObject { diff --git a/src/wayland/platformmenu.cpp b/src/wayland/platformmenu.cpp index b7ae3d07..ffe9548d 100644 --- a/src/wayland/platformmenu.cpp +++ b/src/wayland/platformmenu.cpp @@ -2,31 +2,60 @@ #include #include +#include +#include +#include #include #include "../core/platformmenu.hpp" using namespace qs::menu::platform; -using namespace QtWayland; // fixes positioning of submenus when hitting screen edges void platformMenuHook(PlatformMenuQMenu* menu) { auto* window = menu->windowHandle(); auto constraintAdjustment = QtWayland::xdg_positioner::constraint_adjustment_flip_x - | QtWayland::xdg_positioner::constraint_adjustment_flip_y; + | QtWayland::xdg_positioner::constraint_adjustment_flip_y + | QtWayland::xdg_positioner::constraint_adjustment_slide_x + | QtWayland::xdg_positioner::constraint_adjustment_slide_y + | QtWayland::xdg_positioner::constraint_adjustment_resize_x + | QtWayland::xdg_positioner::constraint_adjustment_resize_y; window->setProperty("_q_waylandPopupConstraintAdjustment", constraintAdjustment); + Qt::Edges anchor; + Qt::Edges gravity; + if (auto* containingMenu = menu->containingMenu) { auto geom = containingMenu->actionGeometry(menu->menuAction()); + // Forces action rects to be refreshed. Without this the geometry is intermittently null. + containingMenu->sizeHint(); + // use the first action to find the offsets relative to the containing window auto baseGeom = containingMenu->actionGeometry(containingMenu->actions().first()); geom += QMargins(0, baseGeom.top(), 0, baseGeom.top()); window->setProperty("_q_waylandPopupAnchorRect", geom); + + auto sideEdge = menu->isRightToLeft() ? Qt::LeftEdge : Qt::RightEdge; + anchor = Qt::TopEdge | sideEdge; + gravity = Qt::BottomEdge | sideEdge; + } else if (auto* parent = window->transientParent()) { + // The menu geometry will be adjusted to flip internally by qt already, but it ends up off by + // one pixel which causes the compositor to also flip which results in the menu being placed + // left of the edge by its own width. To work around this the intended position is stored prior + // to tampering by qt. + auto anchorRect = QRect(menu->targetPosition - parent->geometry().topLeft(), QSize(1, 1)); + window->setProperty("_q_waylandPopupAnchorRect", anchorRect); + + anchor = Qt::BottomEdge | Qt::RightEdge; + gravity = Qt::BottomEdge | Qt::RightEdge; } + + window->setProperty("_q_waylandPopupAnchor", QVariant::fromValue(anchor)); + window->setProperty("_q_waylandPopupGravity", QVariant::fromValue(gravity)); } void installPlatformMenuHook() { PlatformMenuEntry::registerCreationHook(&platformMenuHook); } From dfcf533424a5b5a9847f5786c721f9fcb6bdeba9 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 21 Jul 2024 16:15:11 -0700 Subject: [PATCH 086/305] core/window!: rename QSWindow to QsWindow --- src/core/windowinterface.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/windowinterface.hpp b/src/core/windowinterface.hpp index ac72a791..3970d9da 100644 --- a/src/core/windowinterface.hpp +++ b/src/core/windowinterface.hpp @@ -107,7 +107,7 @@ class WindowInterface: public Reloadable { Q_PROPERTY(QQmlListProperty data READ data); // clang-format on Q_CLASSINFO("DefaultProperty", "data"); - QML_NAMED_ELEMENT(QSWindow); + QML_NAMED_ELEMENT(QsWindow); QML_UNCREATABLE("uncreatable base class"); QML_ATTACHED(QsWindowAttached); From a9e4720fae20854f959577594e0a5f87f633c409 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 21 Jul 2024 17:41:49 -0700 Subject: [PATCH 087/305] docs: use new member reference shorthand --- src/core/desktopentry.hpp | 8 +-- src/core/floatingwindow.hpp | 2 +- src/core/lazyloader.hpp | 18 ++--- src/core/model.hpp | 2 +- src/core/objectrepeater.hpp | 4 +- src/core/panelinterface.hpp | 10 ++- src/core/qmlglobal.hpp | 10 +-- src/core/qsmenu.hpp | 4 +- src/core/region.hpp | 15 +++-- src/core/reload.hpp | 7 +- src/core/retainable.hpp | 14 ++-- src/core/transformwatcher.hpp | 2 +- src/core/variants.hpp | 8 +-- src/dbus/dbusmenu/dbusmenu.hpp | 2 +- src/io/datastream.hpp | 6 +- src/io/process.hpp | 20 +++--- src/io/socket.hpp | 6 +- src/services/greetd/connection.hpp | 2 + src/services/greetd/qml.hpp | 14 ++-- src/services/mpris/player.hpp | 73 +++++++++++---------- src/services/notifications/notification.hpp | 13 +++- src/services/pam/conversation.hpp | 6 +- src/services/pam/qml.hpp | 18 ++--- src/services/pipewire/link.hpp | 2 + src/services/pipewire/node.hpp | 2 + src/services/pipewire/qml.hpp | 13 ++-- src/services/status_notifier/qml.hpp | 12 ++-- src/services/upower/core.hpp | 2 +- src/services/upower/device.hpp | 8 ++- src/wayland/hyprland/ipc/connection.hpp | 4 ++ src/wayland/hyprland/ipc/monitor.hpp | 2 +- src/wayland/hyprland/ipc/workspace.hpp | 2 +- src/wayland/toplevel_management/qml.hpp | 6 +- src/wayland/wlr_layershell.hpp | 5 +- src/wayland/wlr_layershell/window.hpp | 6 +- 35 files changed, 182 insertions(+), 146 deletions(-) diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index e227eb19..57bc3bc7 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -27,7 +27,7 @@ class DesktopEntry: public QObject { Q_PROPERTY(QString comment MEMBER mComment CONSTANT); /// Name of the icon associated with this application. May be empty. Q_PROPERTY(QString icon MEMBER mIcon CONSTANT); - /// The raw `Exec` string from the desktop entry. You probably want `execute()`. + /// The raw `Exec` string from the desktop entry. You probably want @@execute(). Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); /// The working directory to execute from. Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT); @@ -44,7 +44,7 @@ public: void parseEntry(const QString& text); - /// Run the application. Currently ignores `runInTerminal` and field codes. + /// Run the application. Currently ignores @@runInTerminal and field codes. Q_INVOKABLE void execute() const; [[nodiscard]] bool isValid() const; @@ -81,7 +81,7 @@ class DesktopAction: public QObject { Q_PROPERTY(QString id MEMBER mId CONSTANT); Q_PROPERTY(QString name MEMBER mName CONSTANT); Q_PROPERTY(QString icon MEMBER mIcon CONSTANT); - /// The raw `Exec` string from the desktop entry. You probably want `execute()`. + /// The raw `Exec` string from the desktop entry. You probably want @@execute(). Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); QML_ELEMENT; QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry"); @@ -92,7 +92,7 @@ public: , entry(entry) , mId(std::move(id)) {} - /// Run the application. Currently ignores `runInTerminal` and field codes. + /// Run the application. Currently ignores @@DesktopEntry.runInTerminal and field codes. Q_INVOKABLE void execute() const; private: diff --git a/src/core/floatingwindow.hpp b/src/core/floatingwindow.hpp index 93b5723d..def1183a 100644 --- a/src/core/floatingwindow.hpp +++ b/src/core/floatingwindow.hpp @@ -17,7 +17,7 @@ public: void setHeight(qint32 height) override; }; -///! Standard floating window. +///! Standard toplevel operating system window that looks like any other application. class FloatingWindowInterface: public WindowInterface { Q_OBJECT; QML_NAMED_ELEMENT(FloatingWindow); diff --git a/src/core/lazyloader.hpp b/src/core/lazyloader.hpp index 34bd2a70..dbaad4b5 100644 --- a/src/core/lazyloader.hpp +++ b/src/core/lazyloader.hpp @@ -87,8 +87,8 @@ /// > 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` - /// if neither `loading` or `active`. + /// The fully loaded item if the loader is @@loading or @@active, or `null` + /// if neither @@loading nor @@active. /// /// Note that the item is owned by the LazyLoader, and destroying the LazyLoader /// will destroy the item. @@ -96,7 +96,7 @@ class LazyLoader: public Reloadable { /// > [!WARNING] If you access the `item` of a loader that is currently loading, /// > it will block as if you had set `active` to true immediately beforehand. /// > - /// > You can instead set `loading` and listen to the `activeChanged` signal to + /// > You can instead set @@loading and listen to @@activeChanged(s) signal to /// > ensure loading happens asynchronously. Q_PROPERTY(QObject* item READ item NOTIFY itemChanged); /// If the loader is actively loading. @@ -105,7 +105,7 @@ class LazyLoader: public Reloadable { /// loading it asynchronously. If the component is already loaded, setting /// this property has no effect. /// - /// See also: [activeAsync](#prop.activeAsync). + /// See also: @@activeAsync. Q_PROPERTY(bool loading READ isLoading WRITE setLoading NOTIFY loadingChanged); /// If the component is fully loaded. /// @@ -113,17 +113,17 @@ class LazyLoader: public Reloadable { /// blocking the UI, and setting it to `false` will destroy the component, requiring /// it to be loaded again. /// - /// See also: [activeAsync](#prop.activeAsync). + /// See also: @@activeAsync. Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged); /// If the component is fully loaded. /// /// Setting this property to true will asynchronously load the component similarly to - /// [loading](#prop.loading). Reading it or setting it to false will behanve - /// the same as [active](#prop.active). + /// @@loading. Reading it or setting it to false will behanve + /// the same as @@active. Q_PROPERTY(bool activeAsync READ isActive WRITE setActiveAsync NOTIFY activeChanged); - /// The component to load. Mutually exclusive to `source`. + /// The component to load. Mutually exclusive to @@source. Q_PROPERTY(QQmlComponent* component READ component WRITE setComponent NOTIFY componentChanged); - /// The URI to load the component from. Mutually exclusive to `component`. + /// The URI to load the component from. Mutually exclusive to @@component. Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged); Q_CLASSINFO("DefaultProperty", "component"); QML_ELEMENT; diff --git a/src/core/model.hpp b/src/core/model.hpp index ab58f270..5ab3e79f 100644 --- a/src/core/model.hpp +++ b/src/core/model.hpp @@ -27,7 +27,7 @@ /// property var foo: model[3] /// ``` /// -/// You can work around this limitation using the `values` property of the model to view it as a list. +/// You can work around this limitation using the @@values property of the model to view it as a list. /// ```qml /// // will update reactively /// property var foo: model.values[3] diff --git a/src/core/objectrepeater.hpp b/src/core/objectrepeater.hpp index 2350971c..409b12dc 100644 --- a/src/core/objectrepeater.hpp +++ b/src/core/objectrepeater.hpp @@ -11,7 +11,7 @@ #include "model.hpp" ///! A Repeater / for loop / map for non Item derived objects. -/// > [!ERROR] Removed in favor of QtQml.Models.Instantiator +/// > [!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. @@ -19,7 +19,7 @@ class ObjectRepeater: public ObjectModel { Q_OBJECT; /// The model providing data to the ObjectRepeater. /// - /// Currently accepted model types are QML `list` lists, javascript arrays, + /// Currently accepted model types are `list` lists, javascript arrays, /// and [QAbstractListModel] derived models, though only one column will be repeated /// from the latter. /// diff --git a/src/core/panelinterface.hpp b/src/core/panelinterface.hpp index c3853122..78665df3 100644 --- a/src/core/panelinterface.hpp +++ b/src/core/panelinterface.hpp @@ -57,6 +57,8 @@ public: qint32 mBottom = 0; }; +///! Panel exclusion mode +/// See @@PanelWindow.exclusionMode. namespace ExclusionMode { // NOLINT Q_NAMESPACE; QML_ELEMENT; @@ -111,15 +113,19 @@ class PanelWindowInterface: public WindowInterface { /// > [!INFO] Only applies to edges with anchors Q_PROPERTY(Margins margins READ margins WRITE setMargins NOTIFY marginsChanged); /// The amount of space reserved for the shell layer relative to its anchors. - /// Setting this property sets `exclusionMode` to `Normal`. + /// Setting this property sets @@exclusionMode to `ExclusionMode.Normal`. /// /// > [!INFO] Either 1 or 3 anchors are required for the zone to take effect. Q_PROPERTY(qint32 exclusiveZone READ exclusiveZone WRITE setExclusiveZone NOTIFY exclusiveZoneChanged); /// Defaults to `ExclusionMode.Auto`. Q_PROPERTY(ExclusionMode::Enum exclusionMode READ exclusionMode WRITE setExclusionMode NOTIFY exclusionModeChanged); /// If the panel should render above standard windows. Defaults to true. + /// + /// Note: On Wayland this property corrosponds to @@Quickshell.Wayland.WlrLayershell.layer. Q_PROPERTY(bool aboveWindows READ aboveWindows WRITE setAboveWindows NOTIFY aboveWindowsChanged); - /// Defaults to false. + /// If the panel should accept keyboard focus. Defaults to false. + /// + /// Note: On Wayland this property corrosponds to @@Quickshell.Wayland.WlrLayershell.keyboardFocus. Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged); // clang-format on QSDOC_NAMED_ELEMENT(PanelWindow); diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 14d99c52..ae797d62 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -112,22 +112,18 @@ public: QQmlListProperty screens(); - /// Reload the shell from the [ShellRoot]. + /// Reload the shell. /// /// `hard` - perform a hard reload. If this is false, Quickshell will attempt to reuse windows /// that already exist. If true windows will be recreated. /// - /// See [Reloadable] for more information on what can be reloaded and how. - /// - /// [Reloadable]: ../reloadable + /// See @@Reloadable for more information on what can be reloaded and how. Q_INVOKABLE void reload(bool hard); /// Returns the string value of an environment variable or null if it is not set. Q_INVOKABLE QVariant env(const QString& variable); - /// Returns a source string usable in an [Image] for a given system icon. - /// - /// [Image]: https://doc.qt.io/qt-6/qml-qtquick-image.html + /// Returns a string usable for a @@QtQuick.Image.source for a given system icon. Q_INVOKABLE static QString iconPath(const QString& icon); [[nodiscard]] QString workingDirectory() const; diff --git a/src/core/qsmenu.hpp b/src/core/qsmenu.hpp index 2d2413a4..b1c9b5a5 100644 --- a/src/core/qsmenu.hpp +++ b/src/core/qsmenu.hpp @@ -12,6 +12,8 @@ namespace qs::menu { +///! Button type associated with a QsMenuEntry. +/// See @@QsMenuEntry.buttonType. class QsMenuButtonType: public QObject { Q_OBJECT; QML_ELEMENT; @@ -35,7 +37,7 @@ class QsMenuEntry: public QObject { Q_OBJECT; /// If this menu item should be rendered as a separator between other items. /// - /// No other properties have a meaningful value when `isSeparator` is true. + /// No other properties have a meaningful value when @@isSeparator is true. Q_PROPERTY(bool isSeparator READ isSeparator NOTIFY isSeparatorChanged); Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged); /// Text of the menu item. diff --git a/src/core/region.hpp b/src/core/region.hpp index 35f2736c..a512085b 100644 --- a/src/core/region.hpp +++ b/src/core/region.hpp @@ -9,7 +9,8 @@ #include #include -/// Shape of a Region. +///! Shape of a Region. +/// See @@Region.shape. namespace RegionShape { // NOLINT Q_NAMESPACE; QML_ELEMENT; @@ -23,6 +24,7 @@ Q_ENUM_NS(Enum); } // namespace RegionShape ///! Intersection strategy for Regions. +/// See @@Region.intersection. namespace Intersection { // NOLINT Q_NAMESPACE; QML_ELEMENT; @@ -44,6 +46,7 @@ Q_ENUM_NS(Enum); } // namespace Intersection ///! A composable region used as a mask. +/// See @@QsWindow.mask. class PendingRegion: public QObject { Q_OBJECT; /// Defaults to `Rect`. @@ -52,16 +55,16 @@ class PendingRegion: public QObject { Q_PROPERTY(Intersection::Enum intersection MEMBER mIntersection NOTIFY intersectionChanged); /// The item that determines the geometry of the region. - /// `item` overrides `x`, `y`, `width` and `height`. + /// `item` overrides @@x, @@y, @@width and @@height. Q_PROPERTY(QQuickItem* item MEMBER mItem WRITE setItem NOTIFY itemChanged); - /// Defaults to 0. Does nothing if `item` is set. + /// Defaults to 0. Does nothing if @@item is set. Q_PROPERTY(qint32 x MEMBER mX NOTIFY xChanged); - /// Defaults to 0. Does nothing if `item` is set. + /// Defaults to 0. Does nothing if @@item is set. Q_PROPERTY(qint32 y MEMBER mY NOTIFY yChanged); - /// Defaults to 0. Does nothing if `item` is set. + /// Defaults to 0. Does nothing if @@item is set. Q_PROPERTY(qint32 width MEMBER mWidth NOTIFY widthChanged); - /// Defaults to 0. Does nothing if `item` is set. + /// Defaults to 0. Does nothing if @@item is set. Q_PROPERTY(qint32 height MEMBER mHeight NOTIFY heightChanged); /// Regions to apply on top of this region. diff --git a/src/core/reload.hpp b/src/core/reload.hpp index 36956f5a..378a9520 100644 --- a/src/core/reload.hpp +++ b/src/core/reload.hpp @@ -25,7 +25,7 @@ class Reloadable /// this object in the current revision, and facilitate smoother reloading. /// /// Note that identifiers are scoped, and will try to do the right thing in context. - /// For example if you have a `Variants` wrapping an object with an identified element inside, + /// For example if you have a @@Variants wrapping an object with an identified element inside, /// a scope is created at the variant level. /// /// ```qml @@ -83,10 +83,9 @@ private: }; ///! Scope that propagates reloads to child items in order. -/// Convenience type equivalent to setting `reloadableId` on properties in a -/// QtObject instance. +/// Convenience type equivalent to setting @@Reloadable.reloadableId for all children. /// -/// Note that this does not work for visible `Item`s (all widgets). +/// Note that this does not work for visible @@QtQuick.Item$s (all widgets). /// /// ```qml /// ShellRoot { diff --git a/src/core/retainable.hpp b/src/core/retainable.hpp index 5ef02f80..dfe2e794 100644 --- a/src/core/retainable.hpp +++ b/src/core/retainable.hpp @@ -12,11 +12,11 @@ class Retainable; /// kept around (retained) after they would normally be destroyed, which /// is especially useful for things like exit transitions. /// -/// An object that is retainable will have `Retainable` as an attached property. +/// An object that is retainable will have @@Retainable as an attached property. /// All retainable objects will say that they are retainable on their respective /// typeinfo pages. /// -/// > [!INFO] Working directly with Retainable is often overly complicated and +/// > [!INFO] Working directly with @@Retainable is often overly complicated and /// > error prone. For this reason @@RetainableLock should /// > usually be used instead. class RetainableHook: public QObject { @@ -46,11 +46,11 @@ public: /// > Using @@RetainableLock is recommended as it will help /// > avoid this scenario and make misuse more obvious. Q_INVOKABLE void lock(); - /// Remove a lock on the object. See `lock()` for more information. + /// Remove a lock on the object. See @@lock() for more information. Q_INVOKABLE void unlock(); /// Forcibly remove all locks, destroying the object. /// - /// `unlock()` should usually be preferred. + /// @@unlock() should usually be preferred. Q_INVOKABLE void forceUnlock(); [[nodiscard]] bool isRetained() const; @@ -121,7 +121,7 @@ private: /// ``` class RetainableLock: public QObject { Q_OBJECT; - /// The object to lock. Must be [Retainable](../retainable). + /// The object to lock. Must be @@Retainable. Q_PROPERTY(QObject* object READ object WRITE setObject NOTIFY objectChanged); /// If the object should be locked. Q_PROPERTY(bool locked READ locked WRITE setLocked NOTIFY lockedChanged); @@ -143,9 +143,9 @@ public: [[nodiscard]] bool isRetained() const; signals: - /// Rebroadcast of the object's `dropped()` signal. + /// Rebroadcast of the object's @@Retainable.dropped(s). void dropped(); - /// Rebroadcast of the object's `aboutToDestroy()` signal. + /// Rebroadcast of the object's @@Retainable.aboutToDestroy(s). void aboutToDestroy(); void retainedChanged(); diff --git a/src/core/transformwatcher.hpp b/src/core/transformwatcher.hpp index 64bac4a1..8efa9399 100644 --- a/src/core/transformwatcher.hpp +++ b/src/core/transformwatcher.hpp @@ -13,7 +13,7 @@ class TestTransformWatcher; ///! Monitor of all geometry changes between two objects. /// The TransformWatcher monitors all properties that affect the geometry -/// of two `Item`s relative to eachother. +/// of two @@QtQuick.Item$s relative to eachother. /// /// > [!INFO] The algorithm responsible for determining the relationship /// > between `a` and `b` is biased towards `a` being a parent of `b`, diff --git a/src/core/variants.hpp b/src/core/variants.hpp index f0f3c6f1..ebf87ae1 100644 --- a/src/core/variants.hpp +++ b/src/core/variants.hpp @@ -28,11 +28,11 @@ public: ///! Creates instances of a component based on a given model. /// Creates and destroys instances of the given component when the given property changes. /// -/// `Variants` is similar to @@QtQuick.Repeater except it is for *non Item* objects, and acts as +/// `Variants` is similar to @@QtQuick.Repeater except it is for *non @@QtQuick.Item$* objects, and acts as /// a reload scope. /// -/// Each non duplicate value passed to [model](#prop.model) will create a new instance of -/// [delegate](#prop.delegate) with its `modelData` property set to that value. +/// Each non duplicate value passed to @@model will create a new instance of +/// @@delegate with a `modelData` property set to that value. /// /// See @@Quickshell.screens for an example of using `Variants` to create copies of a window per /// screen. @@ -44,7 +44,7 @@ class Variants: public Reloadable { /// The component to create instances of. /// /// The delegate should define a `modelData` property that will be popuplated with a value - /// from the [model](#prop.model). + /// from the @@model. Q_PROPERTY(QQmlComponent* delegate MEMBER mDelegate); /// The list of sets of properties to create instances with. /// Each set creates an instance of the component, which are updated when the input sets update. diff --git a/src/dbus/dbusmenu/dbusmenu.hpp b/src/dbus/dbusmenu/dbusmenu.hpp index bf2f09fa..b49f666a 100644 --- a/src/dbus/dbusmenu/dbusmenu.hpp +++ b/src/dbus/dbusmenu/dbusmenu.hpp @@ -48,7 +48,7 @@ public: /// Usually you shouldn't need to call this manually but some applications providing /// menus do not update them correctly. Call this if menus don't update their state. /// - /// The `layoutUpdated` signal will be sent when a response is received. + /// The @@layoutUpdated(s) signal will be sent when a response is received. Q_INVOKABLE void updateLayout() const; [[nodiscard]] DBusMenu* menuHandle() const; diff --git a/src/io/datastream.hpp b/src/io/datastream.hpp index 76a25f2b..b30800ac 100644 --- a/src/io/datastream.hpp +++ b/src/io/datastream.hpp @@ -43,7 +43,7 @@ protected: }; ///! Parser for streamed input data. -/// See also: @@DataStream$, @@SplitParser +/// See also: @@DataStream, @@SplitParser. class DataStreamParser: public QObject { Q_OBJECT; QML_ELEMENT; @@ -61,9 +61,7 @@ signals: }; ///! Parser for delimited data streams. -/// Parser for delimited data streams. [read()] is emitted once per delimited chunk of the stream. -/// -/// [read()]: ../datastreamparser#sig.read +/// Parser for delimited data streams. @@read() 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/process.hpp b/src/io/process.hpp index 10e47002..521ee2ca 100644 --- a/src/io/process.hpp +++ b/src/io/process.hpp @@ -30,7 +30,7 @@ class Process: public QObject { /// Setting this property to true will start the process if command has at least /// one element. /// Setting it to false will send SIGTERM. To immediately kill the process, - /// use [signal](#func.signal) with SIGKILL. The process will be killed when + /// use @@signal() with SIGKILL. The process will be killed when /// quickshell dies. /// /// If you want to run the process in a loop, use the onRunningChanged signal handler @@ -42,7 +42,7 @@ class Process: public QObject { /// } /// ``` Q_PROPERTY(bool running READ isRunning WRITE setRunning NOTIFY runningChanged); - /// The process ID of the running process or `null` if `running` is false. + /// The process ID of the running process or `null` if @@running is false. Q_PROPERTY(QVariant processId READ processId NOTIFY processIdChanged); /// The command to execute. Each argument is its own string, which means you don't have /// to deal with quoting anything. @@ -65,8 +65,8 @@ class Process: public QObject { /// Environment of the executed process. /// /// This is a javascript object (json). Environment variables can be added by setting - /// them to a string and removed by setting them to null (except when [clearEnvironment] is true, - /// in which case this behavior is inverted, see [clearEnvironment] for details). + /// them to a string and removed by setting them to null (except when @@clearEnvironment is true, + /// in which case this behavior is inverted, see @@clearEnvironment for details). /// /// /// ```qml @@ -82,13 +82,11 @@ class Process: public QObject { /// If the process is already running changing this property will affect the next /// started process. If the property has been changed after starting a process it will /// return the new value, not the one for the currently running process. - /// - /// [clearEnvironment]: #prop.clearEnvironment Q_PROPERTY(QMap environment READ environment WRITE setEnvironment NOTIFY environmentChanged); - /// If the process's environment should be cleared prior to applying [environment](#prop.environment). + /// If the process's environment should be cleared prior to applying @@environment. /// Defaults to false. /// - /// If true, all environment variables will be removed before the [environment](#prop.environment) + /// If true, all environment variables will be removed before the @@environment /// object is applied, meaning the variables listed will be the only ones visible to the process. /// This changes the behavior of `null` to pass in the system value of the variable if present instead /// of removing it. @@ -112,7 +110,7 @@ class Process: public QObject { /// and no further data will be read, even if a new parser is attached. Q_PROPERTY(DataStreamParser* stderr READ stderrParser WRITE setStderrParser NOTIFY stderrParserChanged); /// If stdin is enabled. Defaults to false. If this property is false the process's stdin channel - /// will be closed and [write](#func.write) will do nothing, even if set back to true. + /// will be closed and @@write() will do nothing, even if set back to true. Q_PROPERTY(bool stdinEnabled READ stdinEnabled WRITE setStdinEnabled NOTIFY stdinEnabledChanged); /// If the process should be killed when the Process object is destroyed or quickshell exits. /// Defaults to true. @@ -130,10 +128,10 @@ public: ~Process() override; Q_DISABLE_COPY_MOVE(Process); - /// Sends a signal to the process if `running` is true, otherwise does nothing. + /// Sends a signal to the process if @@running is true, otherwise does nothing. Q_INVOKABLE void signal(qint32 signal); - /// Writes to the process's stdin. Does nothing if `running` is false. + /// Writes to the process's stdin. Does nothing if @@running is false. Q_INVOKABLE void write(const QString& data); [[nodiscard]] bool isRunning() const; diff --git a/src/io/socket.hpp b/src/io/socket.hpp index 75902441..c710dbdc 100644 --- a/src/io/socket.hpp +++ b/src/io/socket.hpp @@ -23,7 +23,7 @@ class Socket: public DataStream { /// update the property immediately. Setting the property to false will begin disconnecting /// the socket, and setting it to true will begin connecting the socket if path is not empty. Q_PROPERTY(bool connected READ isConnected WRITE setConnected NOTIFY connectionStateChanged); - /// The path to connect this socket to when `connected` is set to true. + /// The path to connect this socket to when @@connected is set to true. /// /// Changing this property will have no effect while the connection is active. Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged); @@ -105,9 +105,9 @@ class SocketServer /// /// Setting this property while the server is active will have no effect. Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged); - /// Connection handler component. Must creeate a `Socket`. + /// Connection handler component. Must creeate a @@Socket. /// - /// The created socket should not set `connected` or `path` or the incoming + /// The created socket should not set @@connected or @@path or the incoming /// socket connection will be dropped (they will be set by the socket server.) /// Setting `connected` to false on the created socket after connection will /// close and delete it. diff --git a/src/services/greetd/connection.hpp b/src/services/greetd/connection.hpp index 76b75146..fd619a1e 100644 --- a/src/services/greetd/connection.hpp +++ b/src/services/greetd/connection.hpp @@ -7,6 +7,8 @@ #include #include +///! State of the Greetd connection. +/// See @@Greetd.state. class GreetdState: public QObject { Q_OBJECT; QML_ELEMENT; diff --git a/src/services/greetd/qml.hpp b/src/services/greetd/qml.hpp index b03d181e..9a42e763 100644 --- a/src/services/greetd/qml.hpp +++ b/src/services/greetd/qml.hpp @@ -33,29 +33,29 @@ public: Q_INVOKABLE static void cancelSession(); /// Respond to an authentication message. /// - /// May only be called in response to an `authMessage` with responseRequired set to true. + /// May only be called in response to an @@authMessage(s) with `responseRequired` set to true. Q_INVOKABLE static void respond(QString response); // docgen currently can't handle default params // clang-format off /// Launch the session, exiting quickshell. - /// `readyToLaunch` must be true to call this function. + /// @@state must be `GreetdState.ReadyToLaunch` to call this function. Q_INVOKABLE static void launch(const QList& command); /// Launch the session, exiting quickshell. - /// `readyToLaunch` must be true to call this function. + /// @@state must be `GreetdState.ReadyToLaunch` to call this function. Q_INVOKABLE static void launch(const QList& command, const QList& environment); - /// Launch the session, exiting quickshell if `quit` is true. - /// `readyToLaunch` must be true to call this function. + /// Launch the session, exiting quickshell if @@quit is true. + /// @@state must be `GreetdState.ReadyToLaunch` to call this function. /// - /// The `launched` signal can be used to perform an action after greetd has acknowledged + /// The @@launched signal can be used to perform an action after greetd has acknowledged /// the desired session. /// /// > [!WARNING] Note that greetd expects the greeter to terminate as soon as possible /// > after setting a target session, and waiting too long may lead to unexpected behavior /// > such as the greeter restarting. /// > - /// > Performing animations and such should be done *before* calling `launch`. + /// > Performing animations and such should be done *before* calling @@launch. Q_INVOKABLE static void launch(const QList& command, const QList& environment, bool quit); // clang-format on diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 5de69091..4f3154d5 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -13,6 +13,8 @@ namespace qs::service::mpris { +///! Playback state of an MprisPlayer +/// See @@MprisPlayer.playbackState. class MprisPlaybackState: public QObject { Q_OBJECT; QML_ELEMENT; @@ -29,6 +31,8 @@ public: Q_INVOKABLE static QString toString(MprisPlaybackState::Enum status); }; +///! Loop state of an MprisPlayer +/// See @@MprisPlayer.loopState. class MprisLoopState: public QObject { Q_OBJECT; QML_ELEMENT; @@ -72,17 +76,17 @@ class MprisPlayer: public QObject { /// The name of the desktop entry for the media player, or an empty string if not provided. Q_PROPERTY(QString desktopEntry READ desktopEntry NOTIFY desktopEntryChanged); /// The current position in the playing track, as seconds, with millisecond precision, - /// or `0` if `positionSupported` is false. + /// or `0` if @@positionSupported is false. /// - /// May only be written to if `canSeek` and `positionSupported` are true. + /// May only be written to if @@canSeek and @@positionSupported are true. /// /// > [!WARNING] To avoid excessive property updates wasting CPU while `position` is not /// > actively monitored, `position` usually will not update reactively, unless a nonlinear /// > change in position occurs, however reading it will always return the current position. /// > - /// > If you want to actively monitor the position, the simplest way it to emit the `positionChanged` - /// > signal manually for the duration you are monitoring it, Using a [FrameAnimation] if you need - /// > the value to update smoothly, such as on a slider, or a [Timer] if not, as shown below. + /// > If you want to actively monitor the position, the simplest way it to emit the @@positionChanged(s) + /// > signal manually for the duration you are monitoring it, Using a @@QtQuick.FrameAnimation if you need + /// > the value to update smoothly, such as on a slider, or a @@QtQuick.Timer if not, as shown below. /// > /// > ```qml {filename="Using a FrameAnimation"} /// > FrameAnimation { @@ -104,18 +108,15 @@ class MprisPlayer: public QObject { /// > onTriggered: player.positionChanged() /// > } /// > ``` - /// - /// [FrameAnimation]: https://doc.qt.io/qt-6/qml-qtquick-frameanimation.html - /// [Timer]: https://doc.qt.io/qt-6/qml-qtqml-timer.html Q_PROPERTY(qreal position READ position WRITE setPosition NOTIFY positionChanged); Q_PROPERTY(bool positionSupported READ positionSupported NOTIFY positionSupportedChanged); /// The length of the playing track, as seconds, with millisecond precision, - /// or the value of `position` if `lengthSupported` is false. + /// or the value of @@position if @@lengthSupported is false. Q_PROPERTY(qreal length READ length NOTIFY lengthChanged); Q_PROPERTY(bool lengthSupported READ lengthSupported NOTIFY lengthSupportedChanged); - /// The volume of the playing track from 0.0 to 1.0, or 1.0 if `volumeSupported` is false. + /// The volume of the playing track from 0.0 to 1.0, or 1.0 if @@volumeSupported is false. /// - /// May only be written to if `canControl` and `volumeSupported` are true. + /// May only be written to if @@canControl and @@volumeSupported are true. Q_PROPERTY(qreal volume READ volume WRITE setVolume NOTIFY volumeChanged); Q_PROPERTY(bool volumeSupported READ volumeSupported NOTIFY volumeSupportedChanged); /// Metadata of the current track. @@ -123,7 +124,7 @@ class MprisPlayer: public QObject { /// A map of common properties is available [here](https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata). /// Do not count on any of them actually being present. /// - /// Note that the `trackTitle`, `trackAlbum`, `trackAlbumArtist`, `trackArtists` and `trackArtUrl` + /// Note that the @@trackTitle, @@trackAlbum, @@trackAlbumArtist, @@trackArtists and @@trackArtUrl /// properties have extra logic to guard against bad players sending weird metadata, and should /// be used over grabbing the properties directly from the metadata. Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged); @@ -139,37 +140,37 @@ class MprisPlayer: public QObject { Q_PROPERTY(QString trackArtUrl READ trackArtUrl NOTIFY trackArtUrlChanged); /// The playback state of the media player. /// - /// - If `canPlay` is false, you cannot assign the `Playing` state. - /// - If `canPause` is false, you cannot assign the `Paused` state. - /// - If `canControl` is false, you cannot assign the `Stopped` state. + /// - If @@canPlay is false, you cannot assign the `Playing` state. + /// - If @@canPause is false, you cannot assign the `Paused` state. + /// - If @@canControl is false, you cannot assign the `Stopped` state. /// (or any of the others, though their repsective properties will also be false) Q_PROPERTY(MprisPlaybackState::Enum playbackState READ playbackState WRITE setPlaybackState NOTIFY playbackStateChanged); - /// The loop state of the media player, or `None` if `loopSupported` is false. + /// The loop state of the media player, or `None` if @@loopSupported is false. /// - /// May only be written to if `canControl` and `loopSupported` are true. + /// May only be written to if @@canControl and @@loopSupported are true. Q_PROPERTY(MprisLoopState::Enum loopState READ loopState WRITE setLoopState NOTIFY loopStateChanged); Q_PROPERTY(bool loopSupported READ loopSupported NOTIFY loopSupportedChanged); /// The speed the song is playing at, as a multiplier. /// - /// Only values between `minRate` and `maxRate` (inclusive) may be written to the property. + /// Only values between @@minRate and @@maxRate (inclusive) may be written to the property. /// Additionally, It is recommended that you only write common values such as `0.25`, `0.5`, `1.0`, `2.0` /// to the property, as media players are free to ignore the value, and are more likely to /// accept common ones. Q_PROPERTY(qreal rate READ rate WRITE setRate NOTIFY rateChanged); Q_PROPERTY(qreal minRate READ minRate NOTIFY minRateChanged); Q_PROPERTY(qreal maxRate READ maxRate NOTIFY maxRateChanged); - /// If the play queue is currently being shuffled, or false if `shuffleSupported` is false. + /// If the play queue is currently being shuffled, or false if @@shuffleSupported is false. /// - /// May only be written if `canControl` and `shuffleSupported` are true. + /// May only be written if @@canControl and @@shuffleSupported are true. Q_PROPERTY(bool shuffle READ shuffle WRITE setShuffle NOTIFY shuffleChanged); Q_PROPERTY(bool shuffleSupported READ shuffleSupported NOTIFY shuffleSupportedChanged); /// If the player is currently shown in fullscreen. /// - /// May only be written to if `canSetFullscreen` is true. + /// May only be written to if @@canSetFullscreen is true. Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged); - /// Uri schemes supported by `openUri`. + /// Uri schemes supported by @@openUri(). Q_PROPERTY(QList supportedUriSchemes READ supportedUriSchemes NOTIFY supportedUriSchemesChanged); - /// Mime types supported by `openUri`. + /// Mime types supported by @@openUri(). Q_PROPERTY(QList supportedMimeTypes READ supportedMimeTypes NOTIFY supportedMimeTypesChanged); // clang-format on QML_ELEMENT; @@ -180,42 +181,42 @@ public: /// Bring the media player to the front of the window stack. /// - /// May only be called if `canRaise` is true. + /// May only be called if @@canRaise is true. Q_INVOKABLE void raise(); /// Quit the media player. /// - /// May only be called if `canQuit` is true. + /// May only be called if @@canQuit is true. Q_INVOKABLE void quit(); /// Open the given URI in the media player. /// /// Many players will silently ignore this, especially if the uri - /// does not match `supportedUriSchemes` and `supportedMimeTypes`. + /// does not match @@supportedUriSchemes and @@supportedMimeTypes. Q_INVOKABLE void openUri(const QString& uri); /// Play the next song. /// - /// May only be called if `canGoNext` is true. + /// May only be called if @@canGoNext is true. Q_INVOKABLE void next(); /// Play the previous song, or go back to the beginning of the current one. /// - /// May only be called if `canGoPrevious` is true. + /// May only be called if @@canGoPrevious is true. Q_INVOKABLE void previous(); /// Change `position` by an offset. /// - /// Even if `positionSupported` is false and you cannot set `position`, + /// Even if @@positionSupported is false and you cannot set `position`, /// this function may work. /// - /// May only be called if `canSeek` is true. + /// May only be called if @@canSeek is true. Q_INVOKABLE void seek(qreal offset); - /// Equivalent to setting `playbackState` to `Playing`. + /// Equivalent to setting @@playbackState to `Playing`. Q_INVOKABLE void play(); - /// Equivalent to setting `playbackState` to `Paused`. + /// Equivalent to setting @@playbackState to `Paused`. Q_INVOKABLE void pause(); - /// Equivalent to setting `playbackState` to `Stopped`. + /// Equivalent to setting @@playbackState to `Stopped`. Q_INVOKABLE void stop(); - /// Equivalent to calling `play()` if not playing or `pause()` if playing. + /// Equivalent to calling @@play() if not playing or @@pause() if playing. /// - /// May only be called if `canTogglePlaying` is true, which is equivalent to - /// `canPlay` or `canPause` depending on the current playback state. + /// May only be called if @@canTogglePlaying is true, which is equivalent to + /// @@canPlay or @@canPause() depending on the current playback state. Q_INVOKABLE void togglePlaying(); [[nodiscard]] bool isValid() const; diff --git a/src/services/notifications/notification.hpp b/src/services/notifications/notification.hpp index 4f34f7b6..e87cde9a 100644 --- a/src/services/notifications/notification.hpp +++ b/src/services/notifications/notification.hpp @@ -14,6 +14,8 @@ namespace qs::service::notifications { class NotificationImage; +///! The urgency level of a Notification. +/// See @@Notification.urgency. class NotificationUrgency: public QObject { Q_OBJECT; QML_ELEMENT; @@ -30,6 +32,8 @@ public: Q_INVOKABLE static QString toString(NotificationUrgency::Enum value); }; +///! The reason a Notification was closed. +/// See @@Notification.closed(s). class NotificationCloseReason: public QObject { Q_OBJECT; QML_ELEMENT; @@ -53,7 +57,8 @@ class NotificationAction; ///! A notification emitted by a NotificationServer. /// A notification emitted by a NotificationServer. -/// > [!INFO] This type is @@Quickshell.Retainable$. It +/// +/// > [!INFO] This type is @@Quickshell.Retainable. It /// > can be retained after destruction if necessary. class Notification : public QObject @@ -63,13 +68,13 @@ class Notification Q_PROPERTY(quint32 id READ id CONSTANT); /// If the notification is tracked by the notification server. /// - /// Setting this property to false is equivalent to calling `dismiss()`. + /// Setting this property to false is equivalent to calling @@dismiss(). Q_PROPERTY(bool tracked READ isTracked WRITE setTracked NOTIFY trackedChanged); /// If this notification was carried over from the last generation /// when quickshell reloaded. /// /// Notifications from the last generation will only be emitted - /// if @@NotificationServer.keepOnReloadis true. + /// if @@NotificationServer.keepOnReload is true. Q_PROPERTY(bool lastGeneration READ isLastGeneration CONSTANT); /// Time in seconds the notification should be valid for Q_PROPERTY(qreal expireTimeout READ expireTimeout NOTIFY expireTimeoutChanged); @@ -190,6 +195,8 @@ private: QVariantMap mHints; }; +///! An action associated with a Notification. +/// See @@Notification.actions. class NotificationAction: public QObject { Q_OBJECT; /// The identifier of the action. diff --git a/src/services/pam/conversation.hpp b/src/services/pam/conversation.hpp index 9719d16a..2ba4e8e1 100644 --- a/src/services/pam/conversation.hpp +++ b/src/services/pam/conversation.hpp @@ -13,7 +13,8 @@ Q_DECLARE_LOGGING_CATEGORY(logPam); -/// The result of an authentication. +///! The result of an authentication. +/// See @@PamContext.completed(s). class PamResult: public QObject { Q_OBJECT; QML_ELEMENT; @@ -35,7 +36,8 @@ public: Q_INVOKABLE static QString toString(PamResult::Enum value); }; -/// An error that occurred during an authentication. +///! An error that occurred during an authentication. +/// See @@PamContext.error(s). class PamError: public QObject { Q_OBJECT; QML_ELEMENT; diff --git a/src/services/pam/qml.hpp b/src/services/pam/qml.hpp index c6e3509e..805e04c2 100644 --- a/src/services/pam/qml.hpp +++ b/src/services/pam/qml.hpp @@ -20,23 +20,23 @@ class PamContext // clang-format off /// If the pam context is actively performing an authentication. /// - /// Setting this value behaves exactly the same as calling `start()` and `abort()`. + /// Setting this value behaves exactly the same as calling @@start() and @@abort(). Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged); /// The pam configuration to use. Defaults to "login". /// - /// The configuration should name a file inside `configDirectory`. + /// The configuration should name a file inside @@configDirectory. /// - /// This property may not be set while `active` is true. + /// This property may not be set while @@active is true. Q_PROPERTY(QString config READ config WRITE setConfig NOTIFY configChanged); /// The pam configuration directory to use. Defaults to "/etc/pam.d". /// /// The configuration directory is resolved relative to the current file if not an absolute path. /// - /// This property may not be set while `active` is true. + /// 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. /// - /// This property may not be set while `active` is true. + /// This property may not be set while @@active is true. Q_PROPERTY(QString user READ user WRITE setUser NOTIFY userChanged); /// The last message sent by pam. Q_PROPERTY(QString message READ message NOTIFY messageChanged); @@ -44,9 +44,9 @@ class PamContext Q_PROPERTY(bool messageIsError READ messageIsError NOTIFY messageIsErrorChanged); /// If pam currently wants a response. /// - /// Responses can be returned with the `respond()` function. + /// Responses can be returned with the @@respond() function. Q_PROPERTY(bool responseRequired READ isResponseRequired NOTIFY responseRequiredChanged); - /// If the user's response should be visible. Only valid when `responseRequired` is true. + /// 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; @@ -68,7 +68,7 @@ public: /// Respond to pam. /// - /// May not be called unless `responseRequired` is true. + /// May not be called unless @@responseRequired is true. Q_INVOKABLE void respond(const QString& response); [[nodiscard]] bool isActive() const; @@ -93,7 +93,7 @@ signals: void completed(PamResult::Enum result); /// Emitted if pam fails to perform authentication normally. /// - /// A `completed(false)` will be emitted after this event. + /// A `completed(PamResult.Error)` will be emitted after this event. void error(PamError::Enum error); /// Emitted whenever pam sends a new message, after the change signals for diff --git a/src/services/pipewire/link.hpp b/src/services/pipewire/link.hpp index e5ff2ce9..01c9e60f 100644 --- a/src/services/pipewire/link.hpp +++ b/src/services/pipewire/link.hpp @@ -13,6 +13,8 @@ namespace qs::service::pipewire { +///! State of a pipewire link. +/// See @@PwLink.state. class PwLinkState: public QObject { Q_OBJECT; QML_ELEMENT; diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index a1a60c93..75c93d0a 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -18,6 +18,8 @@ namespace qs::service::pipewire { +///! Audio channel of a pipewire node. +/// See @@PwNodeAudio.channels. class PwAudioChannel: public QObject { Q_OBJECT; QML_ELEMENT; diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 708b7282..2c2c1d60 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -136,6 +136,9 @@ private: }; ///! Audio specific properties of pipewire nodes. +/// Extra properties of a @@PwNode if the node is an audio node. +/// +/// See @@PwNode.audio. class PwNodeAudioIface: public QObject { Q_OBJECT; /// If the node is currently muted. Setting this property changes the mute state. @@ -152,8 +155,8 @@ class PwNodeAudioIface: public QObject { /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). Q_PROPERTY(QVector channels READ channels NOTIFY channelsChanged); /// The volumes of each audio channel individually. Each entry corrosponds to - /// the channel at the same index in `channels`. `volumes` and `channels` will always be - /// the same length. + /// the volume of the channel at the same index in @@channels. @@volumes and @@channels + /// will always be the same length. /// /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). Q_PROPERTY(QVector volumes READ volumes WRITE setVolumes NOTIFY volumesChanged); @@ -195,11 +198,11 @@ class PwNodeIface: public PwObjectIface { Q_PROPERTY(QString name READ name CONSTANT); /// The node's description, corrosponding to the object's `node.description` property. /// - /// May be empty. Generally more human readable than `name`. + /// May be empty. Generally more human readable than @@name. Q_PROPERTY(QString description READ description CONSTANT); /// The node's nickname, corrosponding to the object's `node.nickname` property. /// - /// May be empty. Generally but not always more human readable than `description`. + /// May be empty. Generally but not always more human readable than @@description. Q_PROPERTY(QString nickname READ nickname CONSTANT); /// If `true`, then the node accepts audio input from other nodes, /// if `false` the node outputs audio to other nodes. @@ -249,7 +252,7 @@ private: ///! A connection between pipewire nodes. /// Note that there is one link per *channel* of a connection between nodes. -/// You usually want @@PwLinkGroup$. +/// You usually want @@PwLinkGroup. class PwLinkIface: public PwObjectIface { Q_OBJECT; /// The pipewire object id of the link. diff --git a/src/services/status_notifier/qml.hpp b/src/services/status_notifier/qml.hpp index 7c89055a..0d61e2ad 100644 --- a/src/services/status_notifier/qml.hpp +++ b/src/services/status_notifier/qml.hpp @@ -8,6 +8,8 @@ #include "../../core/model.hpp" #include "item.hpp" +///! Statis of a SystemTrayItem. +/// See @@SystemTrayItem.status. namespace SystemTrayStatus { // NOLINT Q_NAMESPACE; QML_ELEMENT; @@ -24,6 +26,8 @@ Q_ENUM_NS(Enum); } // namespace SystemTrayStatus +///! Category of a SystemTrayItem. +/// See @@SystemTrayItem.category. namespace SystemTrayCategory { // NOLINT Q_NAMESPACE; QML_ELEMENT; @@ -60,8 +64,8 @@ class SystemTrayItem: public QObject { Q_PROPERTY(QString icon READ icon NOTIFY iconChanged); Q_PROPERTY(QString tooltipTitle READ tooltipTitle NOTIFY tooltipTitleChanged); Q_PROPERTY(QString tooltipDescription READ tooltipDescription NOTIFY tooltipDescriptionChanged); - /// If this tray item has an associated menu accessible via `display` - /// or a @@SystemTrayMenuWatcher$. + /// If this tray item has an associated menu accessible via @@display() + /// or a @@SystemTrayMenuWatcher. Q_PROPERTY(bool hasMenu READ hasMenu NOTIFY hasMenuChanged); /// If this tray item only offers a menu and activation will do nothing. Q_PROPERTY(bool onlyMenu READ onlyMenu NOTIFY onlyMenuChanged); @@ -110,7 +114,7 @@ signals: ///! System tray /// Referencing the SystemTray singleton will make quickshell start tracking /// system tray contents, which are updated as the tray changes, and can be -/// accessed via the `items` property. +/// accessed via the @@items property. class SystemTray: public QObject { Q_OBJECT; /// List of all system tray icons. @@ -141,7 +145,7 @@ class SystemTrayMenuWatcher: public QObject { Q_OBJECT; /// The tray item to watch. Q_PROPERTY(SystemTrayItem* trayItem READ trayItem WRITE setTrayItem NOTIFY trayItemChanged); - /// The menu associated with the tray item. Will be null if `trayItem` is null + /// The menu associated with the tray item. Will be null if @@trayItem is null /// or has no associated menu. Q_PROPERTY(DBusMenuItem* menu READ menu NOTIFY menuChanged); QML_ELEMENT; diff --git a/src/services/upower/core.hpp b/src/services/upower/core.hpp index aaeed5ac..0a2367c0 100644 --- a/src/services/upower/core.hpp +++ b/src/services/upower/core.hpp @@ -55,7 +55,7 @@ class UPowerQml: public QObject { QML_SINGLETON; /// UPower's DisplayDevice for your system. Can be `null`. /// - /// This is an aggregate device and not a physical one, meaning you will not find it in `devices`. + /// This is an aggregate device and not a physical one, meaning you will not find it in @@devices. /// It is typically the device that is used for displaying information in desktop environments. Q_PROPERTY(UPowerDevice* displayDevice READ displayDevice NOTIFY displayDeviceChanged); /// All connected UPower devices. diff --git a/src/services/upower/device.hpp b/src/services/upower/device.hpp index 47a71a55..aef9efd6 100644 --- a/src/services/upower/device.hpp +++ b/src/services/upower/device.hpp @@ -12,6 +12,8 @@ namespace qs::service::upower { +///! Power state of a UPower device. +/// See @@UPowerDevice.state. class UPowerDeviceState: public QObject { Q_OBJECT; QML_ELEMENT; @@ -34,6 +36,8 @@ public: Q_INVOKABLE static QString toString(UPowerDeviceState::Enum status); }; +///! Type of a UPower device. +/// See @@UPowerDevice.type. class UPowerDeviceType: public QObject { Q_OBJECT; QML_ELEMENT; @@ -100,7 +104,7 @@ class UPowerDevice: public QObject { Q_PROPERTY(qreal timeToFull READ timeToFull NOTIFY timeToFullChanged); /// Current charge level as a percentage. /// - /// This would be equivalent to `energy / energyCapacity`. + /// This would be equivalent to @@energy / @@energyCapacity. Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged); /// If the power source is present in the bay or slot, useful for hot-removable batteries. /// @@ -115,7 +119,7 @@ class UPowerDevice: public QObject { Q_PROPERTY(QString iconName READ iconName NOTIFY iconNameChanged); /// If the device is a laptop battery or not. Use this to check if your device is a valid battery. /// - /// This will be equivalent to `type == Battery && powerSupply == true`. + /// This will be equivalent to @@type == Battery && @@powerSupply == true. Q_PROPERTY(bool isLaptopBattery READ isLaptopBattery NOTIFY isLaptopBatteryChanged); /// Native path of the device specific to your OS. Q_PROPERTY(QString nativePath READ nativePath NOTIFY nativePathChanged); diff --git a/src/wayland/hyprland/ipc/connection.hpp b/src/wayland/hyprland/ipc/connection.hpp index 635918d8..856d4173 100644 --- a/src/wayland/hyprland/ipc/connection.hpp +++ b/src/wayland/hyprland/ipc/connection.hpp @@ -29,9 +29,13 @@ namespace qs::hyprland::ipc { /// Live Hyprland IPC event. Holding this object after the /// signal handler exits is undefined as the event instance /// is reused. +/// +/// Emitted by @@Hyprland.rawEvent(s). class HyprlandIpcEvent: public QObject { Q_OBJECT; /// The name of the event. + /// + /// See [Hyprland Wiki: IPC](https://wiki.hyprland.org/IPC/) for a list of events. Q_PROPERTY(QString name READ nameStr CONSTANT); /// The unparsed data of the event. Q_PROPERTY(QString data READ dataStr CONSTANT); diff --git a/src/wayland/hyprland/ipc/monitor.hpp b/src/wayland/hyprland/ipc/monitor.hpp index 6b5d2ecc..e5a5eddf 100644 --- a/src/wayland/hyprland/ipc/monitor.hpp +++ b/src/wayland/hyprland/ipc/monitor.hpp @@ -25,7 +25,7 @@ class HyprlandMonitor: public QObject { /// /// > [!WARNING] This is *not* updated unless the monitor object is fetched again from /// > Hyprland. If you need a value that is subject to change and does not have a dedicated - /// > property, run `HyprlandIpc.refreshMonitors()` and wait for this property to update. + /// > property, run @@Hyprland.refreshMonitors() and wait for this property to update. Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); /// The currently active workspace on this monitor. May be null. Q_PROPERTY(HyprlandWorkspace* activeWorkspace READ activeWorkspace NOTIFY activeWorkspaceChanged); diff --git a/src/wayland/hyprland/ipc/workspace.hpp b/src/wayland/hyprland/ipc/workspace.hpp index a63901e6..dab01eb3 100644 --- a/src/wayland/hyprland/ipc/workspace.hpp +++ b/src/wayland/hyprland/ipc/workspace.hpp @@ -19,7 +19,7 @@ class HyprlandWorkspace: public QObject { /// /// > [!WARNING] This is *not* updated unless the workspace object is fetched again from /// > Hyprland. If you need a value that is subject to change and does not have a dedicated - /// > property, run `HyprlandIpc.refreshWorkspaces()` and wait for this property to update. + /// > property, run @@Hyprland.refreshWorkspaces() and wait for this property to update. Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); Q_PROPERTY(HyprlandMonitor* monitor READ monitor NOTIFY monitorChanged); QML_ELEMENT; diff --git a/src/wayland/toplevel_management/qml.hpp b/src/wayland/toplevel_management/qml.hpp index 19d64dfa..64951b63 100644 --- a/src/wayland/toplevel_management/qml.hpp +++ b/src/wayland/toplevel_management/qml.hpp @@ -17,7 +17,7 @@ class ToplevelHandle; ///! Window from another application. /// A window/toplevel from another application, retrievable from -/// the @@ToplevelManager$. +/// the @@ToplevelManager. class Toplevel: public QObject { Q_OBJECT; Q_PROPERTY(QString appId READ appId NOTIFY appIdChanged); @@ -26,7 +26,7 @@ class Toplevel: public QObject { Q_PROPERTY(Toplevel* parent READ parent NOTIFY parentChanged); /// If the window is currently activated or focused. /// - /// Activation can be requested with the `activate()` function. + /// Activation can be requested with the @@activate() function. Q_PROPERTY(bool activated READ activated NOTIFY activatedChanged); /// If the window is currently maximized. /// @@ -42,7 +42,7 @@ class Toplevel: public QObject { /// /// Fullscreen can be requested by setting this property, though it may /// be ignored by the compositor. - /// Fullscreen can be requested on a specific screen with the `fullscreenOn()` function. + /// Fullscreen can be requested on a specific screen with the @@fullscreenOn() function. Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged); QML_ELEMENT; QML_UNCREATABLE("Toplevels must be acquired from the ToplevelManager."); diff --git a/src/wayland/wlr_layershell.hpp b/src/wayland/wlr_layershell.hpp index b289bbe4..7687c4f3 100644 --- a/src/wayland/wlr_layershell.hpp +++ b/src/wayland/wlr_layershell.hpp @@ -15,9 +15,10 @@ ///! Wlroots layershell window /// Decorationless window that can be attached to the screen edges using the [zwlr_layer_shell_v1] protocol. /// -/// #### Attached property -/// `WlrLayershell` works as an attached property of @@Quickshell.PanelWindow which you should use instead if you can, +/// #### Attached object +/// `WlrLayershell` works as an attached object of @@Quickshell.PanelWindow which you should use instead if you can, /// as it is platform independent. +/// /// ```qml /// PanelWindow { /// // When PanelWindow is backed with WlrLayershell this will work diff --git a/src/wayland/wlr_layershell/window.hpp b/src/wayland/wlr_layershell/window.hpp index b73e8a78..ea38e6e9 100644 --- a/src/wayland/wlr_layershell/window.hpp +++ b/src/wayland/wlr_layershell/window.hpp @@ -9,7 +9,8 @@ #include "../../core/panelinterface.hpp" -///! WlrLayershell layer +///! WlrLayershell layer. +/// See @@WlrLayershell.layer. namespace WlrLayer { // NOLINT Q_NAMESPACE; QML_ELEMENT; @@ -30,6 +31,7 @@ Q_ENUM_NS(Enum); } // namespace WlrLayer ///! WlrLayershell keyboard focus mode +/// See @@WlrLayershell.keyboardFocus. namespace WlrKeyboardFocus { // NOLINT Q_NAMESPACE; QML_ELEMENT; @@ -41,7 +43,7 @@ enum Enum { /// /// > [!WARNING] You **CANNOT** use this to make a secure lock screen. /// > - /// > If you want to make a lock screen, use @@WlSessionLock$. + /// > If you want to make a lock screen, use @@WlSessionLock. Exclusive = 1, /// Access to the keyboard as determined by the operating system. /// From 14910b1b60713da40315554d66d2065d27f45a75 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 21 Jul 2024 17:44:09 -0700 Subject: [PATCH 088/305] docs: mention member reference syntax in CONTRIBUTING --- CONTRIBUTING.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c781462..feeb746b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,9 +64,10 @@ Before submitting an MR, if adding new features please make sure the documentati reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo. Doc comments take the form `///` or `///!` (summary) and work with markdown. -You can reference other types using the `@@[Module]..[property]` shorthand -where module and property are optional. If module is not specified it will -be inferred as the current module. Look at existing code for how it works. +You can reference other types using the `@@[Module.][Type.][member]` shorthand +where all parts are optional. If module or type are not specified they will +be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`. +Look at existing code for how it works. Quickshell modules additionally have a `module.md` file which contains a summary, description, and list of headers to scan for documentation. From ebfa8ec448c7d4ee2f1bf6b463f1dbf2e65db060 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 23 Jul 2024 22:12:27 -0700 Subject: [PATCH 089/305] core/popupanchor: rework popup anchoring and add PopupAnchor --- src/core/CMakeLists.txt | 2 + src/core/module.md | 2 + src/core/popupanchor.cpp | 275 ++++++++++++++++++++++++++++++++++++ src/core/popupanchor.hpp | 157 ++++++++++++++++++++ src/core/popupwindow.cpp | 167 ++++++++++------------ src/core/popupwindow.hpp | 38 +++-- src/core/types.cpp | 23 +++ src/core/types.hpp | 54 +++++++ src/wayland/CMakeLists.txt | 2 + src/wayland/init.cpp | 9 +- src/wayland/popupanchor.cpp | 99 +++++++++++++ src/wayland/popupanchor.hpp | 16 +++ src/wayland/xdgshell.cpp | 14 ++ src/wayland/xdgshell.hpp | 20 +++ 14 files changed, 770 insertions(+), 108 deletions(-) create mode 100644 src/core/popupanchor.cpp create mode 100644 src/core/popupanchor.hpp create mode 100644 src/core/types.cpp create mode 100644 src/core/types.hpp create mode 100644 src/wayland/popupanchor.cpp create mode 100644 src/wayland/popupanchor.hpp create mode 100644 src/wayland/xdgshell.cpp create mode 100644 src/wayland/xdgshell.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6eace03b..b70681bc 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -33,6 +33,8 @@ qt_add_library(quickshell-core STATIC platformmenu.cpp qsmenu.cpp retainable.cpp + popupanchor.cpp + types.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/module.md b/src/core/module.md index f0d296a5..9bf7bf25 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -24,5 +24,7 @@ headers = [ "objectrepeater.hpp", "qsmenu.hpp", "retainable.hpp", + "popupanchor.hpp", + "types.hpp", ] ----- diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp new file mode 100644 index 00000000..5700224f --- /dev/null +++ b/src/core/popupanchor.cpp @@ -0,0 +1,275 @@ +#include "popupanchor.hpp" + +#include +#include +#include +#include + +#include "proxywindow.hpp" +#include "types.hpp" +#include "windowinterface.hpp" + +bool PopupAnchorState::operator==(const PopupAnchorState& other) const { + return this->rect == other.rect && this->edges == other.edges && this->gravity == other.gravity + && this->adjustment == other.adjustment && this->anchorpoint == other.anchorpoint; +} + +bool PopupAnchor::isDirty() const { + return !this->lastState.has_value() || this->state != this->lastState.value(); +} + +void PopupAnchor::markClean() { this->lastState = this->state; } +void PopupAnchor::markDirty() { this->lastState.reset(); } + +QObject* PopupAnchor::window() const { return this->mWindow; } +ProxyWindowBase* PopupAnchor::proxyWindow() const { return this->mProxyWindow; } + +QWindow* PopupAnchor::backingWindow() const { + return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr; +} + +void PopupAnchor::setWindow(QObject* window) { + if (window == this->mWindow) return; + + if (this->mWindow) { + QObject::disconnect(this->mWindow, nullptr, this, nullptr); + QObject::disconnect(this->mProxyWindow, nullptr, this, nullptr); + } + + if (window) { + if (auto* proxy = qobject_cast(window)) { + this->mProxyWindow = proxy; + } else if (auto* interface = qobject_cast(window)) { + this->mProxyWindow = interface->proxyWindow(); + } else { + qWarning() << "Tried to set popup anchor window to" << window + << "which is not a quickshell window."; + goto setnull; + } + + this->mWindow = window; + + QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed); + + QObject::connect( + this->mProxyWindow, + &ProxyWindowBase::backerVisibilityChanged, + this, + &PopupAnchor::backingWindowVisibilityChanged + ); + + emit this->windowChanged(); + emit this->backingWindowVisibilityChanged(); + + return; + } + +setnull: + if (this->mWindow) { + this->mWindow = nullptr; + this->mProxyWindow = nullptr; + + emit this->windowChanged(); + emit this->backingWindowVisibilityChanged(); + } +} + +void PopupAnchor::onWindowDestroyed() { + this->mWindow = nullptr; + this->mProxyWindow = nullptr; + emit this->windowChanged(); + emit this->backingWindowVisibilityChanged(); +} + +Box PopupAnchor::rect() const { return this->state.rect; } + +void PopupAnchor::setRect(Box rect) { + if (rect == this->state.rect) return; + if (rect.w <= 0) rect.w = 1; + if (rect.h <= 0) rect.h = 1; + + this->state.rect = rect; + emit this->rectChanged(); +} + +Edges::Flags PopupAnchor::edges() const { return this->state.edges; } + +void PopupAnchor::setEdges(Edges::Flags edges) { + if (edges == this->state.edges) return; + + if (Edges::isOpposing(edges)) { + qWarning() << "Cannot set opposing edges for anchor edges. Tried to set" << edges; + return; + } + + this->state.edges = edges; + emit this->edgesChanged(); +} + +Edges::Flags PopupAnchor::gravity() const { return this->state.gravity; } + +void PopupAnchor::setGravity(Edges::Flags gravity) { + if (gravity == this->state.gravity) return; + + if (Edges::isOpposing(gravity)) { + qWarning() << "Cannot set opposing edges for anchor gravity. Tried to set" << gravity; + return; + } + + this->state.gravity = gravity; + emit this->gravityChanged(); +} + +PopupAdjustment::Flags PopupAnchor::adjustment() const { return this->state.adjustment; } + +void PopupAnchor::setAdjustment(PopupAdjustment::Flags adjustment) { + if (adjustment == this->state.adjustment) return; + this->state.adjustment = adjustment; + emit this->adjustmentChanged(); +} + +void PopupAnchor::updateAnchorpoint(const QPoint& anchorpoint) { + this->state.anchorpoint = anchorpoint; +} + +static PopupPositioner* POSITIONER = nullptr; // NOLINT + +void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty) { + auto* parentWindow = window->transientParent(); + if (!parentWindow) { + qFatal() << "Cannot reposition popup that does not have a transient parent."; + } + + auto adjustment = anchor->adjustment(); + auto screenGeometry = parentWindow->screen()->geometry(); + auto parentGeometry = parentWindow->geometry(); + auto windowGeometry = window->geometry(); + auto anchorRectGeometry = anchor->rect().qrect().translated(parentGeometry.topLeft()); + + auto anchorEdges = anchor->edges(); + auto anchorGravity = anchor->gravity(); + + auto width = windowGeometry.width(); + auto height = windowGeometry.height(); + + auto anchorX = anchorEdges.testFlag(Edges::Left) ? anchorRectGeometry.left() + : anchorEdges.testFlag(Edges::Right) ? anchorRectGeometry.right() + : anchorRectGeometry.center().x(); + + auto anchorY = anchorEdges.testFlag(Edges::Top) ? anchorRectGeometry.top() + : anchorEdges.testFlag(Edges::Bottom) ? anchorRectGeometry.bottom() + : anchorRectGeometry.center().y(); + + anchor->updateAnchorpoint({anchorX, anchorY}); + if (onlyIfDirty && !anchor->isDirty()) return; + anchor->markClean(); + + auto calcEffectiveX = [&]() { + return anchorGravity.testFlag(Edges::Left) ? anchorX - windowGeometry.width() + 1 + : anchorGravity.testFlag(Edges::Right) ? anchorX + : anchorX - windowGeometry.width() / 2; + }; + + auto calcEffectiveY = [&]() { + return anchorGravity.testFlag(Edges::Top) ? anchorY - windowGeometry.height() + 1 + : anchorGravity.testFlag(Edges::Bottom) ? anchorY + : anchorY - windowGeometry.height() / 2; + }; + + auto effectiveX = calcEffectiveX(); + auto effectiveY = calcEffectiveY(); + + if (adjustment.testFlag(PopupAdjustment::FlipX)) { + if (anchorGravity.testFlag(Edges::Left)) { + if (effectiveX < screenGeometry.left()) { + anchorGravity = anchorGravity ^ Edges::Left | Edges::Right; + anchorX = anchorRectGeometry.right(); + effectiveX = calcEffectiveX(); + } + } else if (anchorGravity.testFlag(Edges::Right)) { + if (effectiveX + windowGeometry.width() > screenGeometry.right()) { + anchorGravity = anchorGravity ^ Edges::Right | Edges::Left; + anchorX = anchorRectGeometry.left(); + effectiveX = calcEffectiveX(); + } + } + } + + if (adjustment.testFlag(PopupAdjustment::FlipY)) { + if (anchorGravity.testFlag(Edges::Top)) { + if (effectiveY < screenGeometry.top()) { + anchorGravity = anchorGravity ^ Edges::Top | Edges::Bottom; + effectiveY = calcEffectiveY(); + } + } else if (anchorGravity.testFlag(Edges::Bottom)) { + if (effectiveY + windowGeometry.height() > screenGeometry.bottom()) { + anchorGravity = anchorGravity ^ Edges::Bottom | Edges::Top; + effectiveY = calcEffectiveY(); + } + } + } + + // Slide order is important for the case where the window is too large to fit on screen. + if (adjustment.testFlag(PopupAdjustment::SlideX)) { + if (effectiveX + windowGeometry.width() > screenGeometry.right()) { + effectiveX = screenGeometry.right() - windowGeometry.width() + 1; + } + + if (effectiveX < screenGeometry.left()) { + effectiveX = screenGeometry.left(); + } + } + + if (adjustment.testFlag(PopupAdjustment::SlideY)) { + if (effectiveY + windowGeometry.height() > screenGeometry.bottom()) { + effectiveY = screenGeometry.bottom() - windowGeometry.height() + 1; + } + + if (effectiveY < screenGeometry.top()) { + effectiveY = screenGeometry.top(); + } + } + + if (adjustment.testFlag(PopupAdjustment::ResizeX)) { + if (effectiveX < screenGeometry.left()) { + auto diff = screenGeometry.left() - effectiveX; + effectiveX = screenGeometry.left(); + width -= diff; + } + + auto effectiveX2 = effectiveX + windowGeometry.width(); + if (effectiveX2 > screenGeometry.right()) { + width -= effectiveX2 - screenGeometry.right() - 1; + } + } + + if (adjustment.testFlag(PopupAdjustment::ResizeY)) { + if (effectiveY < screenGeometry.top()) { + auto diff = screenGeometry.top() - effectiveY; + effectiveY = screenGeometry.top(); + height -= diff; + } + + auto effectiveY2 = effectiveY + windowGeometry.height(); + if (effectiveY2 > screenGeometry.bottom()) { + height -= effectiveY2 - screenGeometry.bottom() - 1; + } + } + + window->setGeometry({effectiveX, effectiveY, width, height}); +} + +bool PopupPositioner::shouldRepositionOnMove() const { return true; } + +PopupPositioner* PopupPositioner::instance() { + if (POSITIONER == nullptr) { + POSITIONER = new PopupPositioner(); + } + + return POSITIONER; +} + +void PopupPositioner::setInstance(PopupPositioner* instance) { + delete POSITIONER; + POSITIONER = instance; +} diff --git a/src/core/popupanchor.hpp b/src/core/popupanchor.hpp new file mode 100644 index 00000000..04d89f47 --- /dev/null +++ b/src/core/popupanchor.hpp @@ -0,0 +1,157 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "doc.hpp" +#include "proxywindow.hpp" +#include "types.hpp" + +///! Adjustment strategy for popups that do not fit on screen. +/// Adjustment strategy for popups. See @@PopupAnchor.adjustment. +/// +/// Adjustment flags can be combined with the `|` operator. +/// +/// `Flip` will be applied first, then `Slide`, then `Resize`. +namespace PopupAdjustment { // NOLINT +Q_NAMESPACE; +QML_ELEMENT; + +enum Enum { + None = 0, + /// If the X axis is constrained, the popup will slide along the X axis until it fits onscreen. + SlideX = 1, + /// If the Y axis is constrained, the popup will slide along the Y axis until it fits onscreen. + SlideY = 2, + /// Alias for `SlideX | SlideY`. + Slide = SlideX | SlideY, + /// If the X axis is constrained, the popup will invert its horizontal gravity if any. + FlipX = 4, + /// If the Y axis is constrained, the popup will invert its vertical gravity if any. + FlipY = 8, + /// Alias for `FlipX | FlipY`. + Flip = FlipX | FlipY, + /// If the X axis is constrained, the width of the popup will be reduced to fit on screen. + ResizeX = 16, + /// If the Y axis is constrained, the height of the popup will be reduced to fit on screen. + ResizeY = 32, + /// Alias for `ResizeX | ResizeY` + Resize = ResizeX | ResizeY, + /// Alias for `Flip | Slide | Resize`. + All = Slide | Flip | Resize, +}; +Q_ENUM_NS(Enum); +Q_DECLARE_FLAGS(Flags, Enum); + +} // namespace PopupAdjustment + +Q_DECLARE_OPERATORS_FOR_FLAGS(PopupAdjustment::Flags); + +struct PopupAnchorState { + bool operator==(const PopupAnchorState& other) const; + + Box rect = {0, 0, 1, 1}; + Edges::Flags edges = Edges::Top | Edges::Left; + Edges::Flags gravity = Edges::Bottom | Edges::Right; + PopupAdjustment::Flags adjustment = PopupAdjustment::Slide; + QPoint anchorpoint; +}; + +///! Anchorpoint or positioner for popup windows. +class PopupAnchor: public QObject { + Q_OBJECT; + // clang-format off + /// The window to anchor / attach the popup to. + Q_PROPERTY(QObject* window READ window WRITE setWindow NOTIFY windowChanged); + /// The anchorpoints the popup will attach to. Which anchors will be used is + /// determined by the @@edges, @@gravity, and @@adjustment. + /// + /// If you leave @@edges, @@gravity and @@adjustment at their default values, + /// setting more than `x` and `y` does not matter. + /// + /// > [!INFO] The anchor rect cannot be smaller than 1x1 pixels. + Q_PROPERTY(Box rect READ rect WRITE setRect NOTIFY rectChanged); + /// The point on the anchor rectangle the popup should anchor to. + /// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed. + /// + /// Defaults to `Edges.Top | Edges.Left`. + Q_PROPERTY(Edges::Flags edges READ edges WRITE setEdges NOTIFY edgesChanged); + /// The direction the popup should expand towards, relative to the anchorpoint. + /// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed. + /// + /// Defaults to `Edges.Bottom | Edges.Right`. + Q_PROPERTY(Edges::Flags gravity READ gravity WRITE setGravity NOTIFY gravityChanged); + /// The strategy used to adjust the popup's position if it would otherwise not fit on screen, + /// based on the anchor @@rect, preferred @@edges, and @@gravity. + /// + /// See the documentation for @@PopupAdjustment for details. + Q_PROPERTY(PopupAdjustment::Flags adjustment READ adjustment WRITE setAdjustment NOTIFY adjustmentChanged); + // clang-format on + QML_ELEMENT; + QML_UNCREATABLE(""); + +public: + explicit PopupAnchor(QObject* parent): QObject(parent) {} + + [[nodiscard]] bool isDirty() const; + void markClean(); + void markDirty(); + + [[nodiscard]] QObject* window() const; + [[nodiscard]] ProxyWindowBase* proxyWindow() const; + [[nodiscard]] QWindow* backingWindow() const; + void setWindow(QObject* window); + + [[nodiscard]] Box rect() const; + void setRect(Box rect); + + [[nodiscard]] Edges::Flags edges() const; + void setEdges(Edges::Flags edges); + + [[nodiscard]] Edges::Flags gravity() const; + void setGravity(Edges::Flags gravity); + + [[nodiscard]] PopupAdjustment::Flags adjustment() const; + void setAdjustment(PopupAdjustment::Flags adjustment); + + void updateAnchorpoint(const QPoint& anchorpoint); + +signals: + void windowChanged(); + QSDOC_HIDE void backingWindowVisibilityChanged(); + void rectChanged(); + void edgesChanged(); + void gravityChanged(); + void adjustmentChanged(); + +private slots: + void onWindowDestroyed(); + +private: + QObject* mWindow = nullptr; + ProxyWindowBase* mProxyWindow = nullptr; + PopupAnchorState state; + std::optional lastState; +}; + +class PopupPositioner { +public: + explicit PopupPositioner() = default; + virtual ~PopupPositioner() = default; + Q_DISABLE_COPY_MOVE(PopupPositioner); + + virtual void reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty = true); + [[nodiscard]] virtual bool shouldRepositionOnMove() const; + + static PopupPositioner* instance(); + static void setInstance(PopupPositioner* instance); +}; diff --git a/src/core/popupwindow.cpp b/src/core/popupwindow.cpp index 547bbe36..fa5d7892 100644 --- a/src/core/popupwindow.cpp +++ b/src/core/popupwindow.cpp @@ -4,86 +4,66 @@ #include #include #include -#include #include +#include +#include "popupanchor.hpp" #include "proxywindow.hpp" #include "qmlscreen.hpp" #include "windowinterface.hpp" 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::rectChanged, 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 } void ProxyPopupWindow::completeWindow() { this->ProxyWindowBase::completeWindow(); + QObject::connect( + this->window, + &QWindow::visibleChanged, + this, + &ProxyPopupWindow::onVisibleChanged + ); this->window->setFlag(Qt::ToolTip); - this->updateTransientParent(); } -void ProxyPopupWindow::postCompleteWindow() { this->ProxyWindowBase::setVisible(this->mVisible); } - -bool ProxyPopupWindow::deleteOnInvisible() const { - // Currently crashes in normal mode, do not have the time to debug it now. - return true; -} - -qint32 ProxyPopupWindow::x() const { - // QTBUG-121550 - auto basepos = this->mParentProxyWindow == nullptr ? 0 : this->mParentProxyWindow->x(); - return basepos + this->mRelativeX; -} +void ProxyPopupWindow::postCompleteWindow() { this->updateTransientParent(); } void ProxyPopupWindow::setParentWindow(QObject* parent) { - if (parent == this->mParentWindow) return; - - if (this->mParentWindow != nullptr) { - QObject::disconnect(this->mParentWindow, nullptr, this, nullptr); - QObject::disconnect(this->mParentProxyWindow, nullptr, this, nullptr); - } - - if (parent == nullptr) { - this->mParentWindow = nullptr; - this->mParentProxyWindow = nullptr; - } else { - if (auto* proxy = qobject_cast(parent)) { - this->mParentProxyWindow = proxy; - } else if (auto* interface = qobject_cast(parent)) { - this->mParentProxyWindow = interface->proxyWindow(); - } else { - qWarning() << "Tried to set popup parent window to something that is not a quickshell window:" - << parent; - this->mParentWindow = nullptr; - this->mParentProxyWindow = nullptr; - this->updateTransientParent(); - return; - } - - this->mParentWindow = parent; - - // clang-format off - QObject::connect(this->mParentWindow, &QObject::destroyed, this, &ProxyPopupWindow::onParentDestroyed); - - QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::xChanged, this, &ProxyPopupWindow::updateX); - QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::yChanged, this, &ProxyPopupWindow::updateY); - QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::backerVisibilityChanged, this, &ProxyPopupWindow::onParentUpdated); - // clang-format on - } - - this->updateTransientParent(); + qWarning() << "PopupWindow.parentWindow is deprecated. Use PopupWindow.anchor.window."; + this->mAnchor.setWindow(parent); } -QObject* ProxyPopupWindow::parentWindow() const { return this->mParentWindow; } +QObject* ProxyPopupWindow::parentWindow() const { + qWarning() << "PopupWindow.parentWindow is deprecated. Use PopupWindow.anchor.window."; + return this->mAnchor.window(); +} void ProxyPopupWindow::updateTransientParent() { - this->updateX(); - this->updateY(); + auto* bw = this->mAnchor.backingWindow(); - if (this->window != nullptr) { - this->window->setTransientParent( - this->mParentProxyWindow == nullptr ? nullptr : this->mParentProxyWindow->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(); @@ -91,13 +71,6 @@ void ProxyPopupWindow::updateTransientParent() { void ProxyPopupWindow::onParentUpdated() { this->updateTransientParent(); } -void ProxyPopupWindow::onParentDestroyed() { - this->mParentWindow = nullptr; - this->mParentProxyWindow = nullptr; - this->updateVisible(); - emit this->parentWindowChanged(); -} - void ProxyPopupWindow::setScreen(QuickshellScreenInfo* /*unused*/) { qWarning() << "Cannot set screen of popup window, as that is controlled by the parent window"; } @@ -109,53 +82,55 @@ void ProxyPopupWindow::setVisible(bool visible) { } void ProxyPopupWindow::updateVisible() { - auto target = this->wantsVisible && this->mParentWindow != nullptr - && this->mParentProxyWindow->isVisibleDirect(); + auto target = this->wantsVisible && this->mAnchor.window() != nullptr + && this->mAnchor.proxyWindow()->isVisibleDirect(); if (target && this->window != nullptr && !this->window->isVisible()) { - this->updateX(); // QTBUG-121550 + PopupPositioner::instance()->reposition(&this->mAnchor, this->window); } this->ProxyWindowBase::setVisible(target); } -void ProxyPopupWindow::setRelativeX(qint32 x) { - if (x == this->mRelativeX) return; - this->mRelativeX = x; - this->updateX(); +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(); + } } -qint32 ProxyPopupWindow::relativeX() const { return this->mRelativeX; } +void ProxyPopupWindow::setRelativeX(qint32 x) { + qWarning() << "PopupWindow.relativeX is deprecated. Use PopupWindow.anchor.rect.x."; + auto rect = this->mAnchor.rect(); + if (x == rect.x) return; + rect.x = x; + this->mAnchor.setRect(rect); +} + +qint32 ProxyPopupWindow::relativeX() const { + qWarning() << "PopupWindow.relativeX is deprecated. Use PopupWindow.anchor.rect.x."; + return this->mAnchor.rect().x; +} void ProxyPopupWindow::setRelativeY(qint32 y) { - if (y == this->mRelativeY) return; - this->mRelativeY = y; - this->updateY(); + qWarning() << "PopupWindow.relativeY is deprecated. Use PopupWindow.anchor.rect.y."; + auto rect = this->mAnchor.rect(); + if (y == rect.y) return; + rect.y = y; + this->mAnchor.setRect(rect); } -qint32 ProxyPopupWindow::relativeY() const { return this->mRelativeY; } - -void ProxyPopupWindow::updateX() { - if (this->mParentWindow == nullptr || this->window == nullptr) return; - - auto target = this->x() - 1; // QTBUG-121550 - - auto reshow = this->isVisibleDirect() && (this->window->x() != target && this->x() != target); - if (reshow) this->setVisibleDirect(false); - if (this->window != nullptr) this->window->setX(target); - if (reshow && this->wantsVisible) this->setVisibleDirect(true); +qint32 ProxyPopupWindow::relativeY() const { + qWarning() << "PopupWindow.relativeY is deprecated. Use PopupWindow.anchor.rect.y."; + return this->mAnchor.rect().y; } -void ProxyPopupWindow::updateY() { - if (this->mParentWindow == nullptr || this->window == nullptr) return; +PopupAnchor* ProxyPopupWindow::anchor() { return &this->mAnchor; } - auto target = this->mParentProxyWindow->y() + this->relativeY(); - - auto reshow = this->isVisibleDirect() && this->window->y() != target; - if (reshow) { - this->setVisibleDirect(false); - this->updateX(); // QTBUG-121550 +void ProxyPopupWindow::reposition() { + if (this->window != nullptr) { + PopupPositioner::instance()->reposition(&this->mAnchor, this->window); } - if (this->window != nullptr) this->window->setY(target); - if (reshow && this->wantsVisible) this->setVisibleDirect(true); } diff --git a/src/core/popupwindow.hpp b/src/core/popupwindow.hpp index 7815d400..47db4038 100644 --- a/src/core/popupwindow.hpp +++ b/src/core/popupwindow.hpp @@ -7,6 +7,7 @@ #include #include "doc.hpp" +#include "popupanchor.hpp" #include "proxywindow.hpp" #include "qmlscreen.hpp" #include "windowinterface.hpp" @@ -42,15 +43,37 @@ class ProxyPopupWindow: public ProxyWindowBase { QSDOC_BASECLASS(WindowInterface); Q_OBJECT; // clang-format off + /// > [!ERROR] Deprecated in favor of `anchor.window`. + /// /// The parent window of this popup. /// /// Changing this property reparents the popup. Q_PROPERTY(QObject* parentWindow READ parentWindow WRITE setParentWindow NOTIFY parentWindowChanged); + /// > [!ERROR] Deprecated in favor of `anchor.rect.x`. + /// /// The X position of the popup relative to the parent window. Q_PROPERTY(qint32 relativeX READ relativeX WRITE setRelativeX NOTIFY relativeXChanged); + /// > [!ERROR] Deprecated in favor of `anchor.rect.y`. + /// /// The Y position of the popup relative to the parent window. Q_PROPERTY(qint32 relativeY READ relativeY WRITE setRelativeY NOTIFY relativeYChanged); + /// The popup's anchor / positioner relative to another window. The popup will not be + /// shown until it has a valid anchor relative to a window and @@visible is true. + /// + /// You can set properties of the anchor like so: + /// ```qml + /// PopupWindow { + /// anchor.window: parentwindow + /// // or + /// anchor { + /// window: parentwindow + /// } + /// } + /// ``` + Q_PROPERTY(PopupAnchor* anchor READ anchor CONSTANT); /// If the window is shown or hidden. Defaults to false. + /// + /// 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); /// The screen that the window currently occupies. /// @@ -64,13 +87,10 @@ public: void completeWindow() override; void postCompleteWindow() override; - [[nodiscard]] bool deleteOnInvisible() const override; void setScreen(QuickshellScreenInfo* screen) override; void setVisible(bool visible) override; - [[nodiscard]] qint32 x() const override; - [[nodiscard]] QObject* parentWindow() const; void setParentWindow(QObject* parent); @@ -80,25 +100,23 @@ public: [[nodiscard]] qint32 relativeY() const; void setRelativeY(qint32 y); + [[nodiscard]] PopupAnchor* anchor(); + signals: void parentWindowChanged(); void relativeXChanged(); void relativeYChanged(); private slots: + void onVisibleChanged(); void onParentUpdated(); - void onParentDestroyed(); - void updateX(); - void updateY(); + void reposition(); private: QQuickWindow* parentBackingWindow(); void updateTransientParent(); void updateVisible(); - QObject* mParentWindow = nullptr; - ProxyWindowBase* mParentProxyWindow = nullptr; - qint32 mRelativeX = 0; - qint32 mRelativeY = 0; + PopupAnchor mAnchor {this}; bool wantsVisible = false; }; diff --git a/src/core/types.cpp b/src/core/types.cpp new file mode 100644 index 00000000..5ed63a02 --- /dev/null +++ b/src/core/types.cpp @@ -0,0 +1,23 @@ +#include "types.hpp" + +#include +#include +#include + +QRect Box::qrect() const { return {this->x, this->y, this->w, this->h}; } + +bool Box::operator==(const Box& other) const { + return this->x == other.x && this->y == other.y && this->w == other.w && this->h == other.h; +} + +QDebug operator<<(QDebug debug, const Box& box) { + auto saver = QDebugStateSaver(debug); + debug.nospace() << "Box(" << box.x << ',' << box.y << ' ' << box.w << 'x' << box.h << ')'; + return debug; +} + +Qt::Edges Edges::toQt(Edges::Flags edges) { return Qt::Edges(edges.toInt()); } + +bool Edges::isOpposing(Edges::Flags edges) { + return edges.testFlags(Edges::Top | Edges::Bottom) || edges.testFlags(Edges::Left | Edges::Right); +} diff --git a/src/core/types.hpp b/src/core/types.hpp new file mode 100644 index 00000000..11474f3d --- /dev/null +++ b/src/core/types.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include + +class Box { + Q_GADGET; + Q_PROPERTY(qint32 x MEMBER x); + Q_PROPERTY(qint32 y MEMBER y); + Q_PROPERTY(qint32 w MEMBER w); + Q_PROPERTY(qint32 h MEMBER h); + Q_PROPERTY(qint32 width MEMBER w); + Q_PROPERTY(qint32 height MEMBER h); + QML_VALUE_TYPE(box); + +public: + explicit Box() = default; + Box(qint32 x, qint32 y, qint32 w, qint32 h): x(x), y(y), w(w), h(h) {} + bool operator==(const Box& other) const; + + qint32 x = 0; + qint32 y = 0; + qint32 w = 0; + qint32 h = 0; + + [[nodiscard]] QRect qrect() const; +}; + +QDebug operator<<(QDebug debug, const Box& box); + +///! Top Left Right Bottom flags. +/// Edge flags can be combined with the `|` operator. +namespace Edges { // NOLINT +Q_NAMESPACE; +QML_NAMED_ELEMENT(Edges); + +enum Enum { + None = 0, + Top = Qt::TopEdge, + Left = Qt::LeftEdge, + Right = Qt::RightEdge, + Bottom = Qt::BottomEdge, +}; +Q_ENUM_NS(Enum); +Q_DECLARE_FLAGS(Flags, Enum); + +Qt::Edges toQt(Flags edges); +bool isOpposing(Flags edges); + +}; // namespace Edges + +Q_DECLARE_OPERATORS_FOR_FLAGS(Edges::Flags); diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index a57c5579..d8702b7a 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -52,6 +52,8 @@ endfunction() qt_add_library(quickshell-wayland STATIC platformmenu.cpp + popupanchor.cpp + xdgshell.cpp ) # required to make sure the constructor is linked diff --git a/src/wayland/init.cpp b/src/wayland/init.cpp index b43179f7..1ad51cea 100644 --- a/src/wayland/init.cpp +++ b/src/wayland/init.cpp @@ -4,12 +4,14 @@ #include #include "../core/plugin.hpp" -#include "platformmenu.hpp" #ifdef QS_WAYLAND_WLR_LAYERSHELL #include "wlr_layershell.hpp" #endif +void installPlatformMenuHook(); +void installPopupPositioner(); + namespace { class WaylandPlugin: public QuickshellPlugin { @@ -27,7 +29,10 @@ class WaylandPlugin: public QuickshellPlugin { return isWayland; } - void init() override { installPlatformMenuHook(); } + void init() override { + installPlatformMenuHook(); + installPopupPositioner(); + } void registerTypes() override { #ifdef QS_WAYLAND_WLR_LAYERSHELL diff --git a/src/wayland/popupanchor.cpp b/src/wayland/popupanchor.cpp new file mode 100644 index 00000000..bf6f9850 --- /dev/null +++ b/src/wayland/popupanchor.cpp @@ -0,0 +1,99 @@ +#include "popupanchor.hpp" + +#include +#include +#include +#include +#include + +#include "../core/popupanchor.hpp" +#include "../core/types.hpp" +#include "xdgshell.hpp" + +using QtWaylandClient::QWaylandWindow; +using XdgPositioner = QtWayland::xdg_positioner; +using qs::wayland::xdg_shell::XdgWmBase; + +void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty) { + if (onlyIfDirty && !anchor->isDirty()) return; + + auto* waylandWindow = dynamic_cast(window->handle()); + auto* popupRole = waylandWindow ? waylandWindow->surfaceRole<::xdg_popup>() : nullptr; + + anchor->markClean(); + + if (popupRole) { + auto* xdgWmBase = XdgWmBase::instance(); + + if (xdgWmBase->QtWayland::xdg_wm_base::version() < XDG_POPUP_REPOSITION_SINCE_VERSION) { + window->setVisible(false); + WaylandPopupPositioner::setFlags(anchor, window); + window->setVisible(true); + return; + } + + auto positioner = XdgPositioner(xdgWmBase->create_positioner()); + + positioner.set_constraint_adjustment(anchor->adjustment().toInt()); + + auto anchorRect = anchor->rect(); + positioner.set_anchor_rect(anchorRect.x, anchorRect.y, anchorRect.w, anchorRect.h); + + XdgPositioner::anchor anchorFlag = XdgPositioner::anchor_none; + switch (anchor->edges()) { + case Edges::Top: anchorFlag = XdgPositioner::anchor_top; break; + case Edges::Top | Edges::Right: anchorFlag = XdgPositioner::anchor_top_right; break; + case Edges::Right: anchorFlag = XdgPositioner::anchor_right; break; + case Edges::Bottom | Edges::Right: anchorFlag = XdgPositioner::anchor_bottom_right; break; + case Edges::Bottom: anchorFlag = XdgPositioner::anchor_bottom; break; + case Edges::Bottom | Edges::Left: anchorFlag = XdgPositioner::anchor_bottom_left; break; + case Edges::Left: anchorFlag = XdgPositioner::anchor_left; break; + case Edges::Top | Edges::Left: anchorFlag = XdgPositioner::anchor_top_left; break; + default: break; + } + + positioner.set_anchor(anchorFlag); + + XdgPositioner::gravity gravity = XdgPositioner::gravity_none; + switch (anchor->gravity()) { + case Edges::Top: gravity = XdgPositioner::gravity_top; break; + case Edges::Top | Edges::Right: gravity = XdgPositioner::gravity_top_right; break; + case Edges::Right: gravity = XdgPositioner::gravity_right; break; + case Edges::Bottom | Edges::Right: gravity = XdgPositioner::gravity_bottom_right; break; + case Edges::Bottom: gravity = XdgPositioner::gravity_bottom; break; + case Edges::Bottom | Edges::Left: gravity = XdgPositioner::gravity_bottom_left; break; + case Edges::Left: gravity = XdgPositioner::gravity_left; break; + case Edges::Top | Edges::Left: gravity = XdgPositioner::gravity_top_left; break; + default: break; + } + + positioner.set_gravity(gravity); + auto geometry = waylandWindow->geometry(); + positioner.set_size(geometry.width(), geometry.height()); + + // Note: this needs to be set for the initial position as well but no compositor + // supports it enough to test + positioner.set_reactive(); + + xdg_popup_reposition(popupRole, positioner.object(), 0); + + positioner.destroy(); + } else { + WaylandPopupPositioner::setFlags(anchor, window); + } +} + +// Should be false but nobody supports set_reactive. +// This just tries its best when something like a bar gets resized. +bool WaylandPopupPositioner::shouldRepositionOnMove() const { return true; } + +void WaylandPopupPositioner::setFlags(PopupAnchor* anchor, QWindow* window) { + // clang-format off + window->setProperty("_q_waylandPopupConstraintAdjustment", anchor->adjustment().toInt()); + window->setProperty("_q_waylandPopupAnchorRect", anchor->rect().qrect()); + window->setProperty("_q_waylandPopupAnchor", QVariant::fromValue(Edges::toQt(anchor->edges()))); + window->setProperty("_q_waylandPopupGravity", QVariant::fromValue(Edges::toQt(anchor->gravity()))); + // clang-format on +} + +void installPopupPositioner() { PopupPositioner::setInstance(new WaylandPopupPositioner()); } diff --git a/src/wayland/popupanchor.hpp b/src/wayland/popupanchor.hpp new file mode 100644 index 00000000..3e84e4b8 --- /dev/null +++ b/src/wayland/popupanchor.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "../core/popupanchor.hpp" + +class WaylandPopupPositioner: public PopupPositioner { +public: + void reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty = true) override; + [[nodiscard]] bool shouldRepositionOnMove() const override; + +private: + static void setFlags(PopupAnchor* anchor, QWindow* window); +}; + +void installPopupPositioner(); diff --git a/src/wayland/xdgshell.cpp b/src/wayland/xdgshell.cpp new file mode 100644 index 00000000..8677d1b5 --- /dev/null +++ b/src/wayland/xdgshell.cpp @@ -0,0 +1,14 @@ +#include "xdgshell.hpp" + +#include + +namespace qs::wayland::xdg_shell { + +XdgWmBase::XdgWmBase(): QWaylandClientExtensionTemplate(6) { this->initialize(); } + +XdgWmBase* XdgWmBase::instance() { + static auto* instance = new XdgWmBase(); // NOLINT + return instance; +} + +} // namespace qs::wayland::xdg_shell diff --git a/src/wayland/xdgshell.hpp b/src/wayland/xdgshell.hpp new file mode 100644 index 00000000..735ba679 --- /dev/null +++ b/src/wayland/xdgshell.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace qs::wayland::xdg_shell { + +// Hack that binds xdg_wm_base twice as QtWaylandXdgShell headers are not exported anywhere. + +class XdgWmBase + : public QWaylandClientExtensionTemplate + , public QtWayland::xdg_wm_base { +public: + static XdgWmBase* instance(); + +private: + explicit XdgWmBase(); +}; + +} // namespace qs::wayland::xdg_shell From 60388f10ca0a3755f46292f23ef5bba1956771fd Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 24 Jul 2024 00:44:42 -0700 Subject: [PATCH 090/305] core/popupanchor: reposition on popup size change --- src/core/popupanchor.cpp | 20 ++++++++++++-------- src/core/popupanchor.hpp | 4 +++- src/core/popupwindow.cpp | 12 ++++++------ 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index 5700224f..594ec4af 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -11,7 +12,8 @@ bool PopupAnchorState::operator==(const PopupAnchorState& other) const { return this->rect == other.rect && this->edges == other.edges && this->gravity == other.gravity - && this->adjustment == other.adjustment && this->anchorpoint == other.anchorpoint; + && this->adjustment == other.adjustment && this->anchorpoint == other.anchorpoint + && this->size == other.size; } bool PopupAnchor::isDirty() const { @@ -128,8 +130,9 @@ void PopupAnchor::setAdjustment(PopupAdjustment::Flags adjustment) { emit this->adjustmentChanged(); } -void PopupAnchor::updateAnchorpoint(const QPoint& anchorpoint) { +void PopupAnchor::updatePlacement(const QPoint& anchorpoint, const QSize& size) { this->state.anchorpoint = anchorpoint; + this->state.size = size; } static PopupPositioner* POSITIONER = nullptr; // NOLINT @@ -140,10 +143,15 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only qFatal() << "Cannot reposition popup that does not have a transient parent."; } - auto adjustment = anchor->adjustment(); - auto screenGeometry = parentWindow->screen()->geometry(); auto parentGeometry = parentWindow->geometry(); auto windowGeometry = window->geometry(); + + anchor->updatePlacement(parentGeometry.topLeft(), windowGeometry.size()); + if (onlyIfDirty && !anchor->isDirty()) return; + anchor->markClean(); + + auto adjustment = anchor->adjustment(); + auto screenGeometry = parentWindow->screen()->geometry(); auto anchorRectGeometry = anchor->rect().qrect().translated(parentGeometry.topLeft()); auto anchorEdges = anchor->edges(); @@ -160,10 +168,6 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only : anchorEdges.testFlag(Edges::Bottom) ? anchorRectGeometry.bottom() : anchorRectGeometry.center().y(); - anchor->updateAnchorpoint({anchorX, anchorY}); - if (onlyIfDirty && !anchor->isDirty()) return; - anchor->markClean(); - auto calcEffectiveX = [&]() { return anchorGravity.testFlag(Edges::Left) ? anchorX - windowGeometry.width() + 1 : anchorGravity.testFlag(Edges::Right) ? anchorX diff --git a/src/core/popupanchor.hpp b/src/core/popupanchor.hpp index 04d89f47..11b2ba20 100644 --- a/src/core/popupanchor.hpp +++ b/src/core/popupanchor.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -64,6 +65,7 @@ struct PopupAnchorState { Edges::Flags gravity = Edges::Bottom | Edges::Right; PopupAdjustment::Flags adjustment = PopupAdjustment::Slide; QPoint anchorpoint; + QSize size; }; ///! Anchorpoint or positioner for popup windows. @@ -123,7 +125,7 @@ public: [[nodiscard]] PopupAdjustment::Flags adjustment() const; void setAdjustment(PopupAdjustment::Flags adjustment); - void updateAnchorpoint(const QPoint& anchorpoint); + void updatePlacement(const QPoint& anchorpoint, const QSize& size); signals: void windowChanged(); diff --git a/src/core/popupwindow.cpp b/src/core/popupwindow.cpp index fa5d7892..7a3d9316 100644 --- a/src/core/popupwindow.cpp +++ b/src/core/popupwindow.cpp @@ -26,12 +26,12 @@ ProxyPopupWindow::ProxyPopupWindow(QObject* parent): ProxyWindowBase(parent) { void ProxyPopupWindow::completeWindow() { this->ProxyWindowBase::completeWindow(); - QObject::connect( - this->window, - &QWindow::visibleChanged, - this, - &ProxyPopupWindow::onVisibleChanged - ); + + // clang-format off + QObject::connect(this->window, &QWindow::visibleChanged, this, &ProxyPopupWindow::onVisibleChanged); + QObject::connect(this->window, &QWindow::widthChanged, this, &ProxyPopupWindow::reposition); + QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyPopupWindow::reposition); + // clang-format on this->window->setFlag(Qt::ToolTip); } From a71a6fb3ac484ee05f3f159f60ade5f738cca8c4 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 24 Jul 2024 01:18:16 -0700 Subject: [PATCH 091/305] core/popupanchor: fix flip with opposite anchors and gravity Flips into the anchor rect instead of over it when anchors and gravity are opposite. --- src/core/popupanchor.cpp | 50 +++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index 594ec4af..c0e60ca6 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -12,8 +12,8 @@ bool PopupAnchorState::operator==(const PopupAnchorState& other) const { return this->rect == other.rect && this->edges == other.edges && this->gravity == other.gravity - && this->adjustment == other.adjustment && this->anchorpoint == other.anchorpoint - && this->size == other.size; + && this->adjustment == other.adjustment && this->anchorpoint == other.anchorpoint + && this->size == other.size; } bool PopupAnchor::isDirty() const { @@ -184,32 +184,34 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only auto effectiveY = calcEffectiveY(); if (adjustment.testFlag(PopupAdjustment::FlipX)) { - if (anchorGravity.testFlag(Edges::Left)) { - if (effectiveX < screenGeometry.left()) { - anchorGravity = anchorGravity ^ Edges::Left | Edges::Right; - anchorX = anchorRectGeometry.right(); - effectiveX = calcEffectiveX(); - } - } else if (anchorGravity.testFlag(Edges::Right)) { - if (effectiveX + windowGeometry.width() > screenGeometry.right()) { - anchorGravity = anchorGravity ^ Edges::Right | Edges::Left; - anchorX = anchorRectGeometry.left(); - effectiveX = calcEffectiveX(); - } + bool flip = (anchorGravity.testFlag(Edges::Left) && effectiveX < screenGeometry.left()) + || (anchorGravity.testFlag(Edges::Right) + && effectiveX + windowGeometry.width() > screenGeometry.right()); + + if (flip) { + anchorGravity ^= Edges::Left | Edges::Right; + + anchorX = anchorEdges.testFlags(Edges::Left) ? anchorRectGeometry.right() + : anchorEdges.testFlags(Edges::Right) ? anchorRectGeometry.left() + : anchorX; + + effectiveX = calcEffectiveX(); } } if (adjustment.testFlag(PopupAdjustment::FlipY)) { - if (anchorGravity.testFlag(Edges::Top)) { - if (effectiveY < screenGeometry.top()) { - anchorGravity = anchorGravity ^ Edges::Top | Edges::Bottom; - effectiveY = calcEffectiveY(); - } - } else if (anchorGravity.testFlag(Edges::Bottom)) { - if (effectiveY + windowGeometry.height() > screenGeometry.bottom()) { - anchorGravity = anchorGravity ^ Edges::Bottom | Edges::Top; - effectiveY = calcEffectiveY(); - } + bool flip = (anchorGravity.testFlag(Edges::Top) && effectiveY < screenGeometry.top()) + || (anchorGravity.testFlag(Edges::Bottom) + && effectiveY + windowGeometry.height() > screenGeometry.bottom()); + + if (flip) { + anchorGravity ^= Edges::Top | Edges::Bottom; + + anchorY = anchorEdges.testFlags(Edges::Top) ? anchorRectGeometry.bottom() + : anchorEdges.testFlags(Edges::Bottom) ? anchorRectGeometry.top() + : anchorY; + + effectiveY = calcEffectiveY(); } } From acdbe73c10266871b85ca574c594e5d05aa6f0a7 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 25 Jul 2024 01:30:23 -0700 Subject: [PATCH 092/305] dbus/dbusmenu: separate menu handles from status notifier items No api changes yet. --- src/core/qsmenu.hpp | 17 +++++++ src/dbus/dbusmenu/dbusmenu.cpp | 68 +++++++++++++++++++++++++++ src/dbus/dbusmenu/dbusmenu.hpp | 31 ++++++++++++ src/services/status_notifier/item.cpp | 42 ++--------------- src/services/status_notifier/item.hpp | 8 +--- src/services/status_notifier/qml.cpp | 51 ++++++++++++++------ 6 files changed, 157 insertions(+), 60 deletions(-) diff --git a/src/core/qsmenu.hpp b/src/core/qsmenu.hpp index b1c9b5a5..9c2f168d 100644 --- a/src/core/qsmenu.hpp +++ b/src/core/qsmenu.hpp @@ -111,6 +111,23 @@ private: qsizetype refcount = 0; }; +class QsMenuHandle: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + +public: + explicit QsMenuHandle(QObject* parent): QObject(parent) {} + + virtual void ref() {}; + virtual void unref() {}; + + [[nodiscard]] virtual QsMenuEntry* menu() const = 0; + +signals: + void menuChanged(); +}; + ///! Provides access to children of a QsMenuEntry class QsMenuOpener: public QObject { Q_OBJECT; diff --git a/src/dbus/dbusmenu/dbusmenu.cpp b/src/dbus/dbusmenu/dbusmenu.cpp index ae68ecd2..1539500d 100644 --- a/src/dbus/dbusmenu/dbusmenu.cpp +++ b/src/dbus/dbusmenu/dbusmenu.cpp @@ -512,4 +512,72 @@ DBusMenuPngImage::requestImage(const QString& /*unused*/, QSize* size, const QSi return image; } +void DBusMenuHandle::setAddress(const QString& service, const QString& path) { + if (service == this->service && path == this->path) return; + this->service = service; + this->path = path; + this->onMenuPathChanged(); +} + +void DBusMenuHandle::ref() { + this->refcount++; + qCDebug(logDbusMenu) << this << "gained a reference. Refcount is now" << this->refcount; + + if (this->refcount == 1 || !this->mMenu) { + this->onMenuPathChanged(); + } else { + // Refresh the layout when opening a menu in case a bad client isn't updating it + // and another ref is open somewhere. + this->mMenu->rootItem.updateLayout(); + } +} + +void DBusMenuHandle::unref() { + this->refcount--; + qCDebug(logDbusMenu) << this << "lost a reference. Refcount is now" << this->refcount; + + if (this->refcount == 0) { + this->onMenuPathChanged(); + } +} + +void DBusMenuHandle::onMenuPathChanged() { + qCDebug(logDbusMenu) << "Updating" << this << "with refcount" << this->refcount; + + if (this->mMenu) { + this->mMenu->deleteLater(); + this->mMenu = nullptr; + this->loaded = false; + emit this->menuChanged(); + } + + if (this->refcount > 0 && !this->service.isEmpty() && !this->path.isEmpty()) { + this->mMenu = new DBusMenu(this->service, this->path); + this->mMenu->setParent(this); + + QObject::connect(&this->mMenu->rootItem, &DBusMenuItem::layoutUpdated, this, [this]() { + this->loaded = true; + emit this->menuChanged(); + }); + + this->mMenu->rootItem.setShowChildrenRecursive(true); + } +} + +QsMenuEntry* DBusMenuHandle::menu() const { + return this->loaded ? &this->mMenu->rootItem : nullptr; +} + +QDebug operator<<(QDebug debug, const DBusMenuHandle* handle) { + if (handle) { + auto saver = QDebugStateSaver(debug); + debug.nospace() << "DBusMenuHandle(" << static_cast(handle) + << ", service=" << handle->service << ", path=" << handle->path << ')'; + } else { + debug << "DBusMenuHandle(nullptr)"; + } + + return debug; +} + } // namespace qs::dbus::dbusmenu diff --git a/src/dbus/dbusmenu/dbusmenu.hpp b/src/dbus/dbusmenu/dbusmenu.hpp index b49f666a..cbfa61f4 100644 --- a/src/dbus/dbusmenu/dbusmenu.hpp +++ b/src/dbus/dbusmenu/dbusmenu.hpp @@ -157,4 +157,35 @@ public: QByteArray data; }; +class DBusMenuHandle; + +QDebug operator<<(QDebug debug, const DBusMenuHandle* handle); + +class DBusMenuHandle: public menu::QsMenuHandle { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + +public: + explicit DBusMenuHandle(QObject* parent): menu::QsMenuHandle(parent) {} + + void setAddress(const QString& service, const QString& path); + + void ref() override; + void unref() override; + + [[nodiscard]] QsMenuEntry* menu() const override; + +private: + void onMenuPathChanged(); + + QString service; + QString path; + DBusMenu* mMenu = nullptr; + bool loaded = false; + quint32 refcount = 0; + + friend QDebug operator<<(QDebug debug, const DBusMenuHandle* handle); +}; + } // namespace qs::dbus::dbusmenu diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index 9a82198b..7f990a9f 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -232,48 +232,12 @@ void StatusNotifierItem::updateIcon() { emit this->iconChanged(); } -DBusMenu* StatusNotifierItem::menu() const { return this->mMenu; } - -void StatusNotifierItem::refMenu() { - this->menuRefcount++; - qCDebug(logSniMenu) << "Menu of" << this << "gained a reference. Refcount is now" - << this->menuRefcount; - - if (this->menuRefcount == 1) { - this->onMenuPathChanged(); - } else { - // Refresh the layout when opening a menu in case a bad client isn't updating it - // and another ref is open somewhere. - this->mMenu->rootItem.updateLayout(); - } -} - -void StatusNotifierItem::unrefMenu() { - this->menuRefcount--; - qCDebug(logSniMenu) << "Menu of" << this << "lost a reference. Refcount is now" - << this->menuRefcount; - - if (this->menuRefcount == 0) { - this->onMenuPathChanged(); - } +DBusMenuHandle* StatusNotifierItem::menuHandle() { + return this->menuPath.get().path().isEmpty() ? nullptr : &this->mMenuHandle; } void StatusNotifierItem::onMenuPathChanged() { - qCDebug(logSniMenu) << "Updating menu of" << this << "with refcount" << this->menuRefcount - << "path" << this->menuPath.get().path(); - - if (this->mMenu) { - this->mMenu->deleteLater(); - this->mMenu = nullptr; - } - - if (this->menuRefcount > 0 && !this->menuPath.get().path().isEmpty()) { - this->mMenu = new DBusMenu(this->item->service(), this->menuPath.get().path()); - this->mMenu->setParent(this); - this->mMenu->rootItem.setShowChildrenRecursive(true); - } - - emit this->menuChanged(); + this->mMenuHandle.setAddress(this->item->service(), this->menuPath.get().path()); } void StatusNotifierItem::onGetAllFinished() { diff --git a/src/services/status_notifier/item.hpp b/src/services/status_notifier/item.hpp index 04cceeff..efe31591 100644 --- a/src/services/status_notifier/item.hpp +++ b/src/services/status_notifier/item.hpp @@ -42,9 +42,7 @@ public: [[nodiscard]] QString iconId() const; [[nodiscard]] QPixmap createPixmap(const QSize& size) const; - [[nodiscard]] qs::dbus::dbusmenu::DBusMenu* menu() const; - void refMenu(); - void unrefMenu(); + [[nodiscard]] dbus::dbusmenu::DBusMenuHandle* menuHandle(); void activate(); void secondaryActivate(); @@ -73,7 +71,6 @@ public: signals: void iconChanged(); void ready(); - void menuChanged(); private slots: void updateIcon(); @@ -87,8 +84,7 @@ private: TrayImageHandle imageHandle {this}; bool mReady = false; - dbus::dbusmenu::DBusMenu* mMenu = nullptr; - quint32 menuRefcount = 0; + dbus::dbusmenu::DBusMenuHandle mMenuHandle {this}; // bumped to inhibit caching quint32 iconIndex = 0; diff --git a/src/services/status_notifier/qml.cpp b/src/services/status_notifier/qml.cpp index e9d1c0ea..854f4d27 100644 --- a/src/services/status_notifier/qml.cpp +++ b/src/services/status_notifier/qml.cpp @@ -20,6 +20,7 @@ using namespace qs::dbus; using namespace qs::dbus::dbusmenu; using namespace qs::service::sni; using namespace qs::menu::platform; +using qs::menu::QsMenuHandle; SystemTrayItem::SystemTrayItem(qs::service::sni::StatusNotifierItem* item, QObject* parent) : QObject(parent) @@ -108,25 +109,41 @@ void SystemTrayItem::scroll(qint32 delta, bool horizontal) const { } void SystemTrayItem::display(QObject* parentWindow, qint32 relativeX, qint32 relativeY) { - this->item->refMenu(); - if (!this->item->menu()) { - this->item->unrefMenu(); + if (!this->item->menuHandle()) { qCritical() << "No menu present for" << this; return; } - auto* platform = new PlatformMenuEntry(&this->item->menu()->rootItem); + auto* handle = this->item->menuHandle(); + + auto onMenuChanged = [this, parentWindow, relativeX, relativeY, handle]() { + QObject::disconnect(handle, nullptr, this, nullptr); + + if (!handle->menu()) { + handle->unref(); + return; + } + + auto* platform = new PlatformMenuEntry(handle->menu()); + + // clang-format off + QObject::connect(platform, &PlatformMenuEntry::closed, this, [=]() { platform->deleteLater(); }); + QObject::connect(platform, &QObject::destroyed, this, [=]() { handle->unref(); }); + // clang-format on - QObject::connect(&this->item->menu()->rootItem, &DBusMenuItem::layoutUpdated, platform, [=]() { - platform->relayout(); auto success = platform->display(parentWindow, relativeX, relativeY); // calls destroy which also unrefs if (!success) delete platform; - }); + }; - QObject::connect(platform, &PlatformMenuEntry::closed, this, [=]() { platform->deleteLater(); }); - QObject::connect(platform, &QObject::destroyed, this, [this]() { this->item->unrefMenu(); }); + if (handle->menu()) { + onMenuChanged(); + } else { + QObject::connect(handle, &QsMenuHandle::menuChanged, this, onMenuChanged); + } + + handle->ref(); } SystemTray::SystemTray(QObject* parent): QObject(parent) { @@ -162,7 +179,7 @@ SystemTrayItem* SystemTrayMenuWatcher::trayItem() const { return this->item; } SystemTrayMenuWatcher::~SystemTrayMenuWatcher() { if (this->item != nullptr) { - this->item->item->unrefMenu(); + this->item->item->menuHandle()->unref(); } } @@ -170,20 +187,20 @@ void SystemTrayMenuWatcher::setTrayItem(SystemTrayItem* item) { if (item == this->item) return; if (this->item != nullptr) { - this->item->item->unrefMenu(); + this->item->item->menuHandle()->unref(); QObject::disconnect(this->item, nullptr, this, nullptr); } this->item = item; if (item != nullptr) { - this->item->item->refMenu(); + this->item->item->menuHandle()->ref(); QObject::connect(item, &QObject::destroyed, this, &SystemTrayMenuWatcher::onItemDestroyed); QObject::connect( - item->item, - &StatusNotifierItem::menuChanged, + item->item->menuHandle(), + &DBusMenuHandle::menuChanged, this, &SystemTrayMenuWatcher::menuChanged ); @@ -194,7 +211,11 @@ void SystemTrayMenuWatcher::setTrayItem(SystemTrayItem* item) { } DBusMenuItem* SystemTrayMenuWatcher::menu() const { - return this->item ? &this->item->item->menu()->rootItem : nullptr; + if (this->item) { + return static_cast(this->item->item->menuHandle()->menu()); // NOLINT + } else { + return nullptr; + } } void SystemTrayMenuWatcher::onItemDestroyed() { From 54350277beafaa0d1e400cf6f74c5322971dbdac Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 25 Jul 2024 02:51:17 -0700 Subject: [PATCH 093/305] core/menu: add handle support to QsMenuOpener + add handle to tray --- src/core/qsmenu.cpp | 48 +++++++++++++++++++++---- src/core/qsmenu.hpp | 54 ++++++++++++++++------------ src/dbus/dbusmenu/dbusmenu.cpp | 8 ++--- src/dbus/dbusmenu/dbusmenu.hpp | 10 ++---- src/services/status_notifier/qml.cpp | 20 ++++++----- src/services/status_notifier/qml.hpp | 12 +++++-- 6 files changed, 101 insertions(+), 51 deletions(-) diff --git a/src/core/qsmenu.cpp b/src/core/qsmenu.cpp index e7eed3c3..1587912d 100644 --- a/src/core/qsmenu.cpp +++ b/src/core/qsmenu.cpp @@ -21,6 +21,8 @@ QString QsMenuButtonType::toString(QsMenuButtonType::Enum value) { } } +QsMenuEntry* QsMenuEntry::menu() { return this; } + void QsMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) { auto* platform = new PlatformMenuEntry(this); @@ -53,22 +55,52 @@ void QsMenuEntry::unref() { QQmlListProperty QsMenuEntry::children() { return QsMenuEntry::emptyChildren(this); } -QsMenuEntry* QsMenuOpener::menu() const { return this->mMenu; } +QsMenuOpener::~QsMenuOpener() { + if (this->mMenu) { + if (this->mMenu->menu()) this->mMenu->menu()->unref(); + this->mMenu->unrefHandle(); + } +} -void QsMenuOpener::setMenu(QsMenuEntry* menu) { +QsMenuHandle* QsMenuOpener::menu() const { return this->mMenu; } + +void QsMenuOpener::setMenu(QsMenuHandle* menu) { if (menu == this->mMenu) return; if (this->mMenu != nullptr) { - this->mMenu->unref(); QObject::disconnect(this->mMenu, nullptr, this, nullptr); + + if (this->mMenu->menu()) { + this->mMenu->menu()->unref(); + QObject::disconnect(this->mMenu->menu(), nullptr, this, nullptr); + } + + this->mMenu->unrefHandle(); } this->mMenu = menu; if (menu != nullptr) { + auto onMenuChanged = [this, menu]() { + if (menu->menu()) { + QObject::connect( + menu->menu(), + &QsMenuEntry::childrenChanged, + this, + &QsMenuOpener::childrenChanged + ); + + menu->menu()->ref(); + } + + emit this->childrenChanged(); + }; + QObject::connect(menu, &QObject::destroyed, this, &QsMenuOpener::onMenuDestroyed); - QObject::connect(menu, &QsMenuEntry::childrenChanged, this, &QsMenuOpener::childrenChanged); - menu->ref(); + QObject::connect(menu, &QsMenuHandle::menuChanged, this, onMenuChanged); + + if (menu->menu()) onMenuChanged(); + menu->refHandle(); } emit this->menuChanged(); @@ -82,7 +114,11 @@ void QsMenuOpener::onMenuDestroyed() { } QQmlListProperty QsMenuOpener::children() { - return this->mMenu ? this->mMenu->children() : QsMenuEntry::emptyChildren(this); + if (this->mMenu && this->mMenu->menu()) { + return this->mMenu->menu()->children(); + } else { + return QsMenuEntry::emptyChildren(this); + } } qsizetype QsMenuEntry::childCount(QQmlListProperty* /*property*/) { return 0; } diff --git a/src/core/qsmenu.hpp b/src/core/qsmenu.hpp index 9c2f168d..f0e81edd 100644 --- a/src/core/qsmenu.hpp +++ b/src/core/qsmenu.hpp @@ -33,7 +33,28 @@ public: Q_INVOKABLE static QString toString(QsMenuButtonType::Enum value); }; -class QsMenuEntry: public QObject { +class QsMenuEntry; + +///! Menu handle for QsMenuOpener +/// See @@QsMenuOpener. +class QsMenuHandle: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + +public: + explicit QsMenuHandle(QObject* parent): QObject(parent) {} + + virtual void refHandle() {}; + virtual void unrefHandle() {}; + + [[nodiscard]] virtual QsMenuEntry* menu() = 0; + +signals: + void menuChanged(); +}; + +class QsMenuEntry: public QsMenuHandle { Q_OBJECT; /// If this menu item should be rendered as a separator between other items. /// @@ -68,7 +89,9 @@ class QsMenuEntry: public QObject { QML_UNCREATABLE("QsMenuEntry cannot be directly created"); public: - explicit QsMenuEntry(QObject* parent = nullptr): QObject(parent) {} + explicit QsMenuEntry(QObject* parent): QsMenuHandle(parent) {} + + [[nodiscard]] QsMenuEntry* menu() override; /// Display a platform menu at the given location relative to the parent window. Q_INVOKABLE void display(QObject* parentWindow, qint32 relativeX, qint32 relativeY); @@ -111,37 +134,22 @@ private: qsizetype refcount = 0; }; -class QsMenuHandle: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_UNCREATABLE(""); - -public: - explicit QsMenuHandle(QObject* parent): QObject(parent) {} - - virtual void ref() {}; - virtual void unref() {}; - - [[nodiscard]] virtual QsMenuEntry* menu() const = 0; - -signals: - void menuChanged(); -}; - ///! Provides access to children of a QsMenuEntry class QsMenuOpener: public QObject { Q_OBJECT; /// The menu to retrieve children from. - Q_PROPERTY(QsMenuEntry* menu READ menu WRITE setMenu NOTIFY menuChanged); + Q_PROPERTY(QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged); /// The children of the given menu. Q_PROPERTY(QQmlListProperty children READ children NOTIFY childrenChanged); QML_ELEMENT; public: explicit QsMenuOpener(QObject* parent = nullptr): QObject(parent) {} + ~QsMenuOpener() override; + Q_DISABLE_COPY_MOVE(QsMenuOpener); - [[nodiscard]] QsMenuEntry* menu() const; - void setMenu(QsMenuEntry* menu); + [[nodiscard]] QsMenuHandle* menu() const; + void setMenu(QsMenuHandle* menu); [[nodiscard]] QQmlListProperty children(); @@ -153,7 +161,7 @@ private slots: void onMenuDestroyed(); private: - QsMenuEntry* mMenu = nullptr; + QsMenuHandle* mMenu = nullptr; }; } // namespace qs::menu diff --git a/src/dbus/dbusmenu/dbusmenu.cpp b/src/dbus/dbusmenu/dbusmenu.cpp index 1539500d..0d966610 100644 --- a/src/dbus/dbusmenu/dbusmenu.cpp +++ b/src/dbus/dbusmenu/dbusmenu.cpp @@ -519,7 +519,7 @@ void DBusMenuHandle::setAddress(const QString& service, const QString& path) { this->onMenuPathChanged(); } -void DBusMenuHandle::ref() { +void DBusMenuHandle::refHandle() { this->refcount++; qCDebug(logDbusMenu) << this << "gained a reference. Refcount is now" << this->refcount; @@ -532,7 +532,7 @@ void DBusMenuHandle::ref() { } } -void DBusMenuHandle::unref() { +void DBusMenuHandle::unrefHandle() { this->refcount--; qCDebug(logDbusMenu) << this << "lost a reference. Refcount is now" << this->refcount; @@ -564,9 +564,7 @@ void DBusMenuHandle::onMenuPathChanged() { } } -QsMenuEntry* DBusMenuHandle::menu() const { - return this->loaded ? &this->mMenu->rootItem : nullptr; -} +QsMenuEntry* DBusMenuHandle::menu() { return this->loaded ? &this->mMenu->rootItem : nullptr; } QDebug operator<<(QDebug debug, const DBusMenuHandle* handle) { if (handle) { diff --git a/src/dbus/dbusmenu/dbusmenu.hpp b/src/dbus/dbusmenu/dbusmenu.hpp index cbfa61f4..0687761f 100644 --- a/src/dbus/dbusmenu/dbusmenu.hpp +++ b/src/dbus/dbusmenu/dbusmenu.hpp @@ -162,19 +162,15 @@ class DBusMenuHandle; QDebug operator<<(QDebug debug, const DBusMenuHandle* handle); class DBusMenuHandle: public menu::QsMenuHandle { - Q_OBJECT; - QML_ELEMENT; - QML_UNCREATABLE(""); - public: explicit DBusMenuHandle(QObject* parent): menu::QsMenuHandle(parent) {} void setAddress(const QString& service, const QString& path); - void ref() override; - void unref() override; + void refHandle() override; + void unrefHandle() override; - [[nodiscard]] QsMenuEntry* menu() const override; + [[nodiscard]] QsMenuEntry* menu() override; private: void onMenuPathChanged(); diff --git a/src/services/status_notifier/qml.cpp b/src/services/status_notifier/qml.cpp index 854f4d27..1530e5f9 100644 --- a/src/services/status_notifier/qml.cpp +++ b/src/services/status_notifier/qml.cpp @@ -20,7 +20,6 @@ using namespace qs::dbus; using namespace qs::dbus::dbusmenu; using namespace qs::service::sni; using namespace qs::menu::platform; -using qs::menu::QsMenuHandle; SystemTrayItem::SystemTrayItem(qs::service::sni::StatusNotifierItem* item, QObject* parent) : QObject(parent) @@ -96,6 +95,11 @@ bool SystemTrayItem::hasMenu() const { return !this->item->menuPath.get().path().isEmpty(); } +DBusMenuHandle* SystemTrayItem::menu() const { + if (this->item == nullptr) return nullptr; + return this->item->menuHandle(); +} + bool SystemTrayItem::onlyMenu() const { if (this->item == nullptr) return false; return this->item->isMenu.get(); @@ -120,7 +124,7 @@ void SystemTrayItem::display(QObject* parentWindow, qint32 relativeX, qint32 rel QObject::disconnect(handle, nullptr, this, nullptr); if (!handle->menu()) { - handle->unref(); + handle->unrefHandle(); return; } @@ -128,7 +132,7 @@ void SystemTrayItem::display(QObject* parentWindow, qint32 relativeX, qint32 rel // clang-format off QObject::connect(platform, &PlatformMenuEntry::closed, this, [=]() { platform->deleteLater(); }); - QObject::connect(platform, &QObject::destroyed, this, [=]() { handle->unref(); }); + QObject::connect(platform, &QObject::destroyed, this, [=]() { handle->unrefHandle(); }); // clang-format on auto success = platform->display(parentWindow, relativeX, relativeY); @@ -140,10 +144,10 @@ void SystemTrayItem::display(QObject* parentWindow, qint32 relativeX, qint32 rel if (handle->menu()) { onMenuChanged(); } else { - QObject::connect(handle, &QsMenuHandle::menuChanged, this, onMenuChanged); + QObject::connect(handle, &DBusMenuHandle::menuChanged, this, onMenuChanged); } - handle->ref(); + handle->refHandle(); } SystemTray::SystemTray(QObject* parent): QObject(parent) { @@ -179,7 +183,7 @@ SystemTrayItem* SystemTrayMenuWatcher::trayItem() const { return this->item; } SystemTrayMenuWatcher::~SystemTrayMenuWatcher() { if (this->item != nullptr) { - this->item->item->menuHandle()->unref(); + this->item->item->menuHandle()->unrefHandle(); } } @@ -187,14 +191,14 @@ void SystemTrayMenuWatcher::setTrayItem(SystemTrayItem* item) { if (item == this->item) return; if (this->item != nullptr) { - this->item->item->menuHandle()->unref(); + this->item->item->menuHandle()->unrefHandle(); QObject::disconnect(this->item, nullptr, this, nullptr); } this->item = item; if (item != nullptr) { - this->item->item->menuHandle()->ref(); + this->item->item->menuHandle()->refHandle(); QObject::connect(item, &QObject::destroyed, this, &SystemTrayMenuWatcher::onItemDestroyed); diff --git a/src/services/status_notifier/qml.hpp b/src/services/status_notifier/qml.hpp index 0d61e2ad..b6aa8366 100644 --- a/src/services/status_notifier/qml.hpp +++ b/src/services/status_notifier/qml.hpp @@ -53,6 +53,9 @@ Q_ENUM_NS(Enum); class SystemTrayItem: public QObject { using DBusMenuItem = qs::dbus::dbusmenu::DBusMenuItem; + // intentionally wrongly aliased to temporarily hack around a docgen issue + using QsMenuHandle = qs::dbus::dbusmenu::DBusMenuHandle; + Q_OBJECT; /// A name unique to the application, such as its name. Q_PROPERTY(QString id READ id NOTIFY idChanged); @@ -64,9 +67,10 @@ class SystemTrayItem: public QObject { Q_PROPERTY(QString icon READ icon NOTIFY iconChanged); Q_PROPERTY(QString tooltipTitle READ tooltipTitle NOTIFY tooltipTitleChanged); Q_PROPERTY(QString tooltipDescription READ tooltipDescription NOTIFY tooltipDescriptionChanged); - /// If this tray item has an associated menu accessible via @@display() - /// or a @@SystemTrayMenuWatcher. + /// If this tray item has an associated menu accessible via @@display() or @@menu. Q_PROPERTY(bool hasMenu READ hasMenu NOTIFY hasMenuChanged); + /// A handle to the menu associated with this tray item, if any. + Q_PROPERTY(QsMenuHandle* menu READ menu NOTIFY hasMenuChanged); /// If this tray item only offers a menu and activation will do nothing. Q_PROPERTY(bool onlyMenu READ onlyMenu NOTIFY onlyMenuChanged); QML_ELEMENT; @@ -95,6 +99,7 @@ public: [[nodiscard]] QString tooltipTitle() const; [[nodiscard]] QString tooltipDescription() const; [[nodiscard]] bool hasMenu() const; + [[nodiscard]] QsMenuHandle* menu() const; [[nodiscard]] bool onlyMenu() const; qs::service::sni::StatusNotifierItem* item = nullptr; @@ -136,6 +141,9 @@ private: }; ///! Accessor for SystemTrayItem menus. +/// > [!ERROR] Deprecated in favor of @@Quickshell.QsMenuOpener.menu, +/// > which now supports directly accessing a tray menu via @@SystemTrayItem.menu. +/// /// SystemTrayMenuWatcher provides access to the associated /// @@Quickshell.DBusMenu.DBusMenuItem for a tray item. class SystemTrayMenuWatcher: public QObject { From 6b9b1fcb53a1ff5c2e8bf8b6e55e02a4b24827f8 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 25 Jul 2024 20:44:26 -0700 Subject: [PATCH 094/305] core/menu: add QsMenuAnchor for more control of platform menus --- src/core/CMakeLists.txt | 1 + src/core/module.md | 1 + src/core/platformmenu.cpp | 28 +++++++ src/core/platformmenu.hpp | 2 + src/core/popupanchor.cpp | 12 +-- src/core/qsmenuanchor.cpp | 105 +++++++++++++++++++++++++++ src/core/qsmenuanchor.hpp | 86 ++++++++++++++++++++++ src/services/status_notifier/qml.cpp | 8 +- src/services/status_notifier/qml.hpp | 2 + src/wayland/platformmenu.cpp | 21 +++--- 10 files changed, 245 insertions(+), 21 deletions(-) create mode 100644 src/core/qsmenuanchor.cpp create mode 100644 src/core/qsmenuanchor.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b70681bc..fbf006b2 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -35,6 +35,7 @@ qt_add_library(quickshell-core STATIC retainable.cpp popupanchor.cpp types.cpp + qsmenuanchor.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/module.md b/src/core/module.md index 9bf7bf25..411b1d49 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -26,5 +26,6 @@ headers = [ "retainable.hpp", "popupanchor.hpp", "types.hpp", + "qsmenuanchor.hpp", ] ----- diff --git a/src/core/platformmenu.cpp b/src/core/platformmenu.cpp index 7b31c871..0f416a20 100644 --- a/src/core/platformmenu.cpp +++ b/src/core/platformmenu.cpp @@ -16,6 +16,7 @@ #include #include "generation.hpp" +#include "popupanchor.hpp" #include "proxywindow.hpp" #include "qsmenu.hpp" #include "windowinterface.hpp" @@ -111,6 +112,33 @@ bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relati return true; } +bool PlatformMenuEntry::display(PopupAnchor* anchor) { + if (!anchor->backingWindow() || !anchor->backingWindow()->isVisible()) { + qCritical() << "Cannot display PlatformMenuEntry on anchor without visible window."; + return false; + } + + if (ACTIVE_MENU && this->qmenu != ACTIVE_MENU) { + ACTIVE_MENU->close(); + } + + ACTIVE_MENU = this->qmenu; + + this->qmenu->createWinId(); + this->qmenu->windowHandle()->setTransientParent(anchor->backingWindow()); + + // Update the window geometry to the menu's actual dimensions so reposition + // can accurately adjust it if applicable for the current platform. + this->qmenu->windowHandle()->setGeometry({{0, 0}, this->qmenu->sizeHint()}); + + PopupPositioner::instance()->reposition(anchor, this->qmenu->windowHandle(), false); + + // Open the menu at the position determined by the popup positioner. + this->qmenu->popup(this->qmenu->windowHandle()->position()); + + return true; +} + void PlatformMenuEntry::relayout() { if (this->menu->hasChildren()) { delete this->qaction; diff --git a/src/core/platformmenu.hpp b/src/core/platformmenu.hpp index c1e39096..85aaffac 100644 --- a/src/core/platformmenu.hpp +++ b/src/core/platformmenu.hpp @@ -13,6 +13,7 @@ #include #include +#include "popupanchor.hpp" #include "qsmenu.hpp" namespace qs::menu::platform { @@ -38,6 +39,7 @@ public: Q_DISABLE_COPY_MOVE(PlatformMenuEntry); bool display(QObject* parentWindow, int relativeX, int relativeY); + bool display(PopupAnchor* anchor); static void registerCreationHook(std::function hook); diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index c0e60ca6..2534c114 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -184,9 +184,9 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only auto effectiveY = calcEffectiveY(); if (adjustment.testFlag(PopupAdjustment::FlipX)) { - bool flip = (anchorGravity.testFlag(Edges::Left) && effectiveX < screenGeometry.left()) - || (anchorGravity.testFlag(Edges::Right) - && effectiveX + windowGeometry.width() > screenGeometry.right()); + const bool flip = (anchorGravity.testFlag(Edges::Left) && effectiveX < screenGeometry.left()) + || (anchorGravity.testFlag(Edges::Right) + && effectiveX + windowGeometry.width() > screenGeometry.right()); if (flip) { anchorGravity ^= Edges::Left | Edges::Right; @@ -200,9 +200,9 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only } if (adjustment.testFlag(PopupAdjustment::FlipY)) { - bool flip = (anchorGravity.testFlag(Edges::Top) && effectiveY < screenGeometry.top()) - || (anchorGravity.testFlag(Edges::Bottom) - && effectiveY + windowGeometry.height() > screenGeometry.bottom()); + const bool flip = (anchorGravity.testFlag(Edges::Top) && effectiveY < screenGeometry.top()) + || (anchorGravity.testFlag(Edges::Bottom) + && effectiveY + windowGeometry.height() > screenGeometry.bottom()); if (flip) { anchorGravity ^= Edges::Top | Edges::Bottom; diff --git a/src/core/qsmenuanchor.cpp b/src/core/qsmenuanchor.cpp new file mode 100644 index 00000000..e6af7865 --- /dev/null +++ b/src/core/qsmenuanchor.cpp @@ -0,0 +1,105 @@ +#include "qsmenuanchor.hpp" + +#include +#include +#include + +#include "platformmenu.hpp" +#include "popupanchor.hpp" +#include "qsmenu.hpp" + +using qs::menu::platform::PlatformMenuEntry; + +namespace qs::menu { + +QsMenuAnchor::~QsMenuAnchor() { this->onClosed(); } + +void QsMenuAnchor::open() { + if (this->mOpen) { + qCritical() << "Cannot call QsMenuAnchor.open() as it is already open."; + return; + } + + if (!this->mMenu) { + qCritical() << "Cannot open QsMenuAnchor with no menu attached."; + return; + } + + this->mOpen = true; + + if (this->mMenu->menu()) this->onMenuChanged(); + QObject::connect(this->mMenu, &QsMenuHandle::menuChanged, this, &QsMenuAnchor::onMenuChanged); + this->mMenu->refHandle(); + + emit this->visibleChanged(); +} + +void QsMenuAnchor::onMenuChanged() { + // close menu if the path changes + if (this->platformMenu || !this->mMenu->menu()) { + this->onClosed(); + return; + } + + this->platformMenu = new PlatformMenuEntry(this->mMenu->menu()); + QObject::connect(this->platformMenu, &PlatformMenuEntry::closed, this, &QsMenuAnchor::onClosed); + + auto success = this->platformMenu->display(&this->mAnchor); + if (!success) this->onClosed(); + else emit this->opened(); +} + +void QsMenuAnchor::close() { + if (!this->mOpen) { + qCritical() << "Cannot close QsMenuAnchor as it isn't open."; + return; + } + + this->onClosed(); +} + +void QsMenuAnchor::onClosed() { + if (!this->mOpen) return; + + this->mOpen = false; + + if (this->platformMenu) { + this->platformMenu->deleteLater(); + this->platformMenu = nullptr; + } + + QObject::disconnect(this->mMenu, &QsMenuHandle::menuChanged, this, &QsMenuAnchor::onMenuChanged); + this->mMenu->unrefHandle(); + emit this->closed(); + emit this->visibleChanged(); +} + +PopupAnchor* QsMenuAnchor::anchor() { return &this->mAnchor; } + +QsMenuHandle* QsMenuAnchor::menu() const { return this->mMenu; } + +void QsMenuAnchor::setMenu(QsMenuHandle* menu) { + if (menu == this->mMenu) return; + + if (this->mMenu != nullptr) { + if (this->platformMenu != nullptr) this->platformMenu->deleteLater(); + QObject::disconnect(this->mMenu, nullptr, this, nullptr); + } + + this->mMenu = menu; + + if (menu != nullptr) { + QObject::connect(menu, &QObject::destroyed, this, &QsMenuAnchor::onMenuDestroyed); + } + + emit this->menuChanged(); +} + +bool QsMenuAnchor::isVisible() const { return this->mOpen; } + +void QsMenuAnchor::onMenuDestroyed() { + this->mMenu = nullptr; + emit this->menuChanged(); +} + +} // namespace qs::menu diff --git a/src/core/qsmenuanchor.hpp b/src/core/qsmenuanchor.hpp new file mode 100644 index 00000000..683895ab --- /dev/null +++ b/src/core/qsmenuanchor.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include + +#include "platformmenu.hpp" +#include "popupanchor.hpp" +#include "qsmenu.hpp" + +namespace qs::menu { + +///! Display anchor for platform menus. +class QsMenuAnchor: public QObject { + Q_OBJECT; + /// The menu's anchor / positioner relative to another window. The menu will not be + /// shown until it has a valid anchor. + /// + /// > [!INFO] *The following is subject to change and NOT a guarantee of future behavior.* + /// > + /// > A snapshot of the anchor at the time @@opened(s) is emitted will be + /// > used to position the menu. Additional changes to the anchor after this point + /// > will not affect the placement of the menu. + /// + /// You can set properties of the anchor like so: + /// ```qml + /// QsMenuAnchor { + /// anchor.window: parentwindow + /// // or + /// anchor { + /// window: parentwindow + /// } + /// } + /// ``` + Q_PROPERTY(PopupAnchor* anchor READ anchor CONSTANT); + /// The menu that should be displayed on this anchor. + /// + /// See also: @@Quickshell.Services.SystemTray.SystemTrayItem.menu. + Q_PROPERTY(QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged); + /// If the menu is currently open and visible. + /// + /// See also: @@open(), @@close(). + Q_PROPERTY(bool visible READ isVisible NOTIFY visibleChanged); + QML_ELEMENT; + +public: + explicit QsMenuAnchor(QObject* parent = nullptr): QObject(parent) {} + ~QsMenuAnchor() override; + Q_DISABLE_COPY_MOVE(QsMenuAnchor); + + /// Open the given menu on this menu Requires that @@anchor is valid. + Q_INVOKABLE void open(); + /// Close the open menu. + Q_INVOKABLE void close(); + + [[nodiscard]] PopupAnchor* anchor(); + + [[nodiscard]] QsMenuHandle* menu() const; + void setMenu(QsMenuHandle* menu); + + [[nodiscard]] bool isVisible() const; + +signals: + /// Sent when the menu is displayed onscreen which may be after @@visible + /// becomes true. + void opened(); + /// Sent when the menu is closed. + void closed(); + + void menuChanged(); + void visibleChanged(); + +private slots: + void onMenuChanged(); + void onMenuDestroyed(); + +private: + void onClosed(); + + PopupAnchor mAnchor {this}; + QsMenuHandle* mMenu = nullptr; + bool mOpen = false; + platform::PlatformMenuEntry* platformMenu = nullptr; +}; + +} // namespace qs::menu diff --git a/src/services/status_notifier/qml.cpp b/src/services/status_notifier/qml.cpp index 1530e5f9..d39963f4 100644 --- a/src/services/status_notifier/qml.cpp +++ b/src/services/status_notifier/qml.cpp @@ -141,12 +141,8 @@ void SystemTrayItem::display(QObject* parentWindow, qint32 relativeX, qint32 rel if (!success) delete platform; }; - if (handle->menu()) { - onMenuChanged(); - } else { - QObject::connect(handle, &DBusMenuHandle::menuChanged, this, onMenuChanged); - } - + if (handle->menu()) onMenuChanged(); + QObject::connect(handle, &DBusMenuHandle::menuChanged, this, onMenuChanged); handle->refHandle(); } diff --git a/src/services/status_notifier/qml.hpp b/src/services/status_notifier/qml.hpp index b6aa8366..343fa5b6 100644 --- a/src/services/status_notifier/qml.hpp +++ b/src/services/status_notifier/qml.hpp @@ -70,6 +70,8 @@ class SystemTrayItem: public QObject { /// If this tray item has an associated menu accessible via @@display() or @@menu. Q_PROPERTY(bool hasMenu READ hasMenu NOTIFY hasMenuChanged); /// A handle to the menu associated with this tray item, if any. + /// + /// Can be displayed with @@Quickshell.QsMenuAnchor or @@Quickshell.QsMenuOpener. Q_PROPERTY(QsMenuHandle* menu READ menu NOTIFY hasMenuChanged); /// If this tray item only offers a menu and activation will do nothing. Q_PROPERTY(bool onlyMenu READ onlyMenu NOTIFY onlyMenuChanged); diff --git a/src/wayland/platformmenu.cpp b/src/wayland/platformmenu.cpp index ffe9548d..80f9854e 100644 --- a/src/wayland/platformmenu.cpp +++ b/src/wayland/platformmenu.cpp @@ -15,15 +15,6 @@ using namespace qs::menu::platform; void platformMenuHook(PlatformMenuQMenu* menu) { auto* window = menu->windowHandle(); - auto constraintAdjustment = QtWayland::xdg_positioner::constraint_adjustment_flip_x - | QtWayland::xdg_positioner::constraint_adjustment_flip_y - | QtWayland::xdg_positioner::constraint_adjustment_slide_x - | QtWayland::xdg_positioner::constraint_adjustment_slide_y - | QtWayland::xdg_positioner::constraint_adjustment_resize_x - | QtWayland::xdg_positioner::constraint_adjustment_resize_y; - - window->setProperty("_q_waylandPopupConstraintAdjustment", constraintAdjustment); - Qt::Edges anchor; Qt::Edges gravity; @@ -43,6 +34,9 @@ void platformMenuHook(PlatformMenuQMenu* menu) { anchor = Qt::TopEdge | sideEdge; gravity = Qt::BottomEdge | sideEdge; } else if (auto* parent = window->transientParent()) { + // abort if already set by a PopupAnchor + if (window->property("_q_waylandPopupAnchorRect").isValid()) return; + // The menu geometry will be adjusted to flip internally by qt already, but it ends up off by // one pixel which causes the compositor to also flip which results in the menu being placed // left of the edge by its own width. To work around this the intended position is stored prior @@ -56,6 +50,15 @@ void platformMenuHook(PlatformMenuQMenu* menu) { window->setProperty("_q_waylandPopupAnchor", QVariant::fromValue(anchor)); window->setProperty("_q_waylandPopupGravity", QVariant::fromValue(gravity)); + + auto constraintAdjustment = QtWayland::xdg_positioner::constraint_adjustment_flip_x + | QtWayland::xdg_positioner::constraint_adjustment_flip_y + | QtWayland::xdg_positioner::constraint_adjustment_slide_x + | QtWayland::xdg_positioner::constraint_adjustment_slide_y + | QtWayland::xdg_positioner::constraint_adjustment_resize_x + | QtWayland::xdg_positioner::constraint_adjustment_resize_y; + + window->setProperty("_q_waylandPopupConstraintAdjustment", constraintAdjustment); } void installPlatformMenuHook() { PlatformMenuEntry::registerCreationHook(&platformMenuHook); } From 58c37182875935079387cd07ab33e1bb598e384e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 26 Jul 2024 00:55:42 -0700 Subject: [PATCH 095/305] core/types: add implicit coversion from rect to box --- src/core/types.hpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/core/types.hpp b/src/core/types.hpp index 11474f3d..e2b43e59 100644 --- a/src/core/types.hpp +++ b/src/core/types.hpp @@ -13,11 +13,21 @@ class Box { Q_PROPERTY(qint32 h MEMBER h); Q_PROPERTY(qint32 width MEMBER w); Q_PROPERTY(qint32 height MEMBER h); + QML_CONSTRUCTIBLE_VALUE; QML_VALUE_TYPE(box); public: explicit Box() = default; Box(qint32 x, qint32 y, qint32 w, qint32 h): x(x), y(y), w(w), h(h) {} + + Q_INVOKABLE Box(const QRect& rect): x(rect.x()), y(rect.y()), w(rect.width()), h(rect.height()) {} + + Q_INVOKABLE Box(const QRectF& rect) + : x(static_cast(rect.x())) + , y(static_cast(rect.y())) + , w(static_cast(rect.width())) + , h(static_cast(rect.height())) {} + bool operator==(const Box& other) const; qint32 x = 0; From 4b2e569e942de2d52354d9e81951f6e31d5329dc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 26 Jul 2024 10:06:56 -0700 Subject: [PATCH 096/305] core/types: allow implicit conversion from point to box --- src/core/types.hpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/types.hpp b/src/core/types.hpp index e2b43e59..43224d82 100644 --- a/src/core/types.hpp +++ b/src/core/types.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -21,6 +22,7 @@ public: Box(qint32 x, qint32 y, qint32 w, qint32 h): x(x), y(y), w(w), h(h) {} Q_INVOKABLE Box(const QRect& rect): x(rect.x()), y(rect.y()), w(rect.width()), h(rect.height()) {} + Q_INVOKABLE Box(const QPoint& rect): x(rect.x()), y(rect.y()) {} Q_INVOKABLE Box(const QRectF& rect) : x(static_cast(rect.x())) @@ -28,6 +30,10 @@ public: , w(static_cast(rect.width())) , h(static_cast(rect.height())) {} + Q_INVOKABLE Box(const QPointF& rect) + : x(static_cast(rect.x())) + , y(static_cast(rect.y())) {} + bool operator==(const Box& other) const; qint32 x = 0; From 18563b1273166f6e5795e892642fd11f6b66e69a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 27 Jul 2024 02:28:21 -0700 Subject: [PATCH 097/305] wayland/popupanchor: fix anchor state breaking show after reposition If the popup was hidden and reposition was called to update qt's initial positioning properties it would be cancelled by the dirty marker being unset. This includes if the popup is shown or not into its dirty state. --- src/wayland/popupanchor.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/wayland/popupanchor.cpp b/src/wayland/popupanchor.cpp index bf6f9850..ec6e5dbe 100644 --- a/src/wayland/popupanchor.cpp +++ b/src/wayland/popupanchor.cpp @@ -15,11 +15,15 @@ using XdgPositioner = QtWayland::xdg_positioner; using qs::wayland::xdg_shell::XdgWmBase; void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty) { - if (onlyIfDirty && !anchor->isDirty()) return; auto* waylandWindow = dynamic_cast(window->handle()); auto* popupRole = waylandWindow ? waylandWindow->surfaceRole<::xdg_popup>() : nullptr; + // If a popup becomes invisble after creation ensure the _q properties will + // be set and not ignored because the rest is the same. + anchor->updatePlacement({popupRole != nullptr, 0}, {}); + + if (onlyIfDirty && !anchor->isDirty()) return; anchor->markClean(); if (popupRole) { From d9f66e63a3283ffbac179502171de726c48660fd Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 28 Jul 2024 20:28:45 -0700 Subject: [PATCH 098/305] service/upower!: divide percentage by 100 Brings range back to the expected 0-1 instead of 0-100. --- src/services/upower/device.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/upower/device.cpp b/src/services/upower/device.cpp index fb7b6064..8d205b6f 100644 --- a/src/services/upower/device.cpp +++ b/src/services/upower/device.cpp @@ -112,7 +112,7 @@ qreal UPowerDevice::energyCapacity() const { return this->pEnergyCapacity.get(); qreal UPowerDevice::changeRate() const { return this->pChangeRate.get(); } qlonglong UPowerDevice::timeToEmpty() const { return this->pTimeToEmpty.get(); } qlonglong UPowerDevice::timeToFull() const { return this->pTimeToFull.get(); } -qreal UPowerDevice::percentage() const { return this->pPercentage.get(); } +qreal UPowerDevice::percentage() const { return this->pPercentage.get() / 100; } bool UPowerDevice::isPresent() const { return this->pIsPresent.get(); } UPowerDeviceState::Enum UPowerDevice::state() const { From abc0201f6e3d2b751403521574beac455cbf8a2b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 29 Jul 2024 01:34:44 -0700 Subject: [PATCH 099/305] service/mpris: add uniqueId property to detect track changes Also emits all property changes after trackChanged --- src/core/types.cpp | 19 ++++++++++ src/core/types.hpp | 26 +++++++++++++ src/services/mpris/player.cpp | 70 ++++++++++++++++++++--------------- src/services/mpris/player.hpp | 23 +++++++++--- 4 files changed, 104 insertions(+), 34 deletions(-) diff --git a/src/core/types.cpp b/src/core/types.cpp index 5ed63a02..d9c025fb 100644 --- a/src/core/types.cpp +++ b/src/core/types.cpp @@ -21,3 +21,22 @@ Qt::Edges Edges::toQt(Edges::Flags edges) { return Qt::Edges(edges.toInt()); } bool Edges::isOpposing(Edges::Flags edges) { return edges.testFlags(Edges::Top | Edges::Bottom) || edges.testFlags(Edges::Left | Edges::Right); } + +DropEmitter::DropEmitter(DropEmitter&& other) noexcept: object(other.object), signal(other.signal) { + other.object = nullptr; +} + +DropEmitter& DropEmitter::operator=(DropEmitter&& other) noexcept { + this->object = other.object; + this->signal = other.signal; + other.object = nullptr; + return *this; +} + +DropEmitter::~DropEmitter() { this->call(); } + +void DropEmitter::call() { + if (!this->object) return; + this->signal(this->object); + this->object = nullptr; +} diff --git a/src/core/types.hpp b/src/core/types.hpp index 43224d82..a23ff085 100644 --- a/src/core/types.hpp +++ b/src/core/types.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -68,3 +69,28 @@ bool isOpposing(Flags edges); }; // namespace Edges Q_DECLARE_OPERATORS_FOR_FLAGS(Edges::Flags); + +// NOLINTBEGIN +#define DROP_EMIT(object, func) \ + DropEmitter(object, static_cast([](typeof(object) o) { o->func(); })) +// NOLINTEND + +class DropEmitter { +public: + template + DropEmitter(O* object, void (*signal)(O*)) + : object(object) + , signal(*reinterpret_cast(signal)) {} // NOLINT + + DropEmitter() = default; + DropEmitter(DropEmitter&& other) noexcept; + DropEmitter& operator=(DropEmitter&& other) noexcept; + ~DropEmitter(); + Q_DISABLE_COPY(DropEmitter); + + void call(); + +private: + void* object = nullptr; + void (*signal)(void*) = nullptr; +}; diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 3c221c25..8a06319c 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -12,6 +12,7 @@ #include #include +#include "../../core/types.hpp" #include "../../dbus/properties.hpp" #include "dbus_player.h" #include "dbus_player_app.h" @@ -255,6 +256,7 @@ void MprisPlayer::setVolume(qreal volume) { this->pVolume.write(); } +quint32 MprisPlayer::uniqueId() const { return this->mUniqueId; } QVariantMap MprisPlayer::metadata() const { return this->pMetadata.get(); } QString MprisPlayer::trackTitle() const { return this->mTrackTitle; } QString MprisPlayer::trackAlbum() const { return this->mTrackAlbum; } @@ -271,10 +273,7 @@ void MprisPlayer::onMetadataChanged() { length = lengthVariant.value(); } - if (length != this->mLength) { - this->mLength = length; - emit this->lengthChanged(); - } + auto emitLengthChanged = this->setLength(length); auto trackChanged = false; @@ -299,81 +298,94 @@ void MprisPlayer::onMetadataChanged() { } } + DropEmitter emitTrackTitle; auto trackTitle = this->pMetadata.get().value("xesam:title"); if (trackTitle.isValid() && trackTitle.canConvert()) { - this->setTrackTitle(trackTitle.toString()); + emitTrackTitle = this->setTrackTitle(trackTitle.toString()); } else if (trackChanged) { - this->setTrackTitle("Unknown Track"); + emitTrackTitle = this->setTrackTitle("Unknown Track"); } + DropEmitter emitTrackAlbum; auto trackAlbum = this->pMetadata.get().value("xesam:album"); if (trackAlbum.isValid() && trackAlbum.canConvert()) { - this->setTrackAlbum(trackAlbum.toString()); + emitTrackAlbum = this->setTrackAlbum(trackAlbum.toString()); } else if (trackChanged) { - this->setTrackAlbum("Unknown Album"); + emitTrackAlbum = this->setTrackAlbum("Unknown Album"); } + DropEmitter emitTrackAlbumArtist; auto trackAlbumArtist = this->pMetadata.get().value("xesam:albumArtist"); if (trackAlbumArtist.isValid() && trackAlbumArtist.canConvert()) { - this->setTrackAlbumArtist(trackAlbumArtist.toString()); + emitTrackAlbumArtist = this->setTrackAlbumArtist(trackAlbumArtist.toString()); } else if (trackChanged) { - this->setTrackAlbumArtist("Unknown Artist"); + emitTrackAlbumArtist = this->setTrackAlbumArtist("Unknown Artist"); } + DropEmitter emitTrackArtists; auto trackArtists = this->pMetadata.get().value("xesam:artist"); if (trackArtists.isValid() && trackArtists.canConvert>()) { - this->setTrackArtists(trackArtists.value>()); + emitTrackArtists = this->setTrackArtists(trackArtists.value>()); } else if (trackChanged) { - this->setTrackArtists({}); + emitTrackArtists = this->setTrackArtists({}); } + DropEmitter emitTrackArtUrl; auto trackArtUrl = this->pMetadata.get().value("mpris:artUrl"); if (trackArtUrl.isValid() && trackArtUrl.canConvert()) { - this->setTrackArtUrl(trackArtUrl.toString()); + emitTrackArtUrl = this->setTrackArtUrl(trackArtUrl.toString()); } else if (trackChanged) { - this->setTrackArtUrl(""); + emitTrackArtUrl = this->setTrackArtUrl(""); } if (trackChanged) { + this->mUniqueId++; // Some players don't seem to send position updates or seeks on track change. this->pPosition.update(); emit this->trackChanged(); } } -void MprisPlayer::setTrackTitle(QString title) { - if (title == this->mTrackTitle) return; +DropEmitter MprisPlayer::setLength(qlonglong length) { + if (length == this->mLength) return DropEmitter(); + + this->mLength = length; + return DROP_EMIT(this, lengthChanged); +} + +DropEmitter MprisPlayer::setTrackTitle(QString title) { + if (title == this->mTrackTitle) return DropEmitter(); this->mTrackTitle = std::move(title); - emit this->trackTitleChanged(); + return DROP_EMIT(this, trackTitleChanged); } -void MprisPlayer::setTrackAlbum(QString album) { - if (album == this->mTrackAlbum) return; +DropEmitter MprisPlayer::setTrackAlbum(QString album) { + if (album == this->mTrackAlbum) return DropEmitter(); this->mTrackAlbum = std::move(album); - emit this->trackAlbumChanged(); + return DROP_EMIT(this, trackAlbumChanged); } -void MprisPlayer::setTrackAlbumArtist(QString albumArtist) { - if (albumArtist == this->mTrackAlbumArtist) return; +DropEmitter MprisPlayer::setTrackAlbumArtist(QString albumArtist) { + if (albumArtist == this->mTrackAlbumArtist) return DropEmitter(); this->mTrackAlbumArtist = std::move(albumArtist); - emit this->trackAlbumArtistChanged(); + return DROP_EMIT(this, trackAlbumArtistChanged); } -void MprisPlayer::setTrackArtists(QVector artists) { - if (artists == this->mTrackArtists) return; +DropEmitter MprisPlayer::setTrackArtists(QVector artists) { + if (artists == this->mTrackArtists) return DropEmitter(); this->mTrackArtists = std::move(artists); - emit this->trackArtistsChanged(); + return DROP_EMIT(this, trackArtistsChanged); } -void MprisPlayer::setTrackArtUrl(QString artUrl) { - if (artUrl == this->mTrackArtUrl) return; +DropEmitter MprisPlayer::setTrackArtUrl(QString artUrl) { + if (artUrl == this->mTrackArtUrl) return DropEmitter(); this->mTrackArtUrl = std::move(artUrl); - emit this->trackArtUrlChanged(); + return DROP_EMIT(this, trackArtUrlChanged); } MprisPlaybackState::Enum MprisPlayer::playbackState() const { return this->mPlaybackState; } diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 4f3154d5..627512f1 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -7,6 +7,7 @@ #include #include "../../core/doc.hpp" +#include "../../core/types.hpp" #include "../../dbus/properties.hpp" #include "dbus_player.h" #include "dbus_player_app.h" @@ -128,6 +129,11 @@ class MprisPlayer: public QObject { /// properties have extra logic to guard against bad players sending weird metadata, and should /// be used over grabbing the properties directly from the metadata. Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged); + /// An opaque identifier for the current track unique within the current player. + /// + /// > [!WARNING] This is NOT `mpris:trackid` as that is sometimes missing or nonunique + /// > in some players. + Q_PROPERTY(quint32 uniqueId READ uniqueId NOTIFY trackChanged); /// The title of the current track, or "Unknown Track" if none was provided. Q_PROPERTY(QString trackTitle READ trackTitle NOTIFY trackTitleChanged); /// The current track's album, or "Unknown Album" if none was provided. @@ -248,6 +254,7 @@ public: [[nodiscard]] bool volumeSupported() const; void setVolume(qreal volume); + [[nodiscard]] quint32 uniqueId() const; [[nodiscard]] QVariantMap metadata() const; [[nodiscard]] QString trackTitle() const; [[nodiscard]] QString trackAlbum() const; @@ -278,6 +285,10 @@ public: [[nodiscard]] QList supportedMimeTypes() const; signals: + /// The track has changed. + /// + /// All track info change signalss will fire immediately after if applicable, + /// but their values will be updated before the signal fires. void trackChanged(); QSDOC_HIDE void ready(); @@ -327,11 +338,12 @@ private slots: void onLoopStatusChanged(); private: - void setTrackTitle(QString title); - void setTrackAlbum(QString album); - void setTrackAlbumArtist(QString albumArtist); - void setTrackArtists(QVector artists); - void setTrackArtUrl(QString artUrl); + DropEmitter setLength(qlonglong length); + DropEmitter setTrackTitle(QString title); + DropEmitter setTrackAlbum(QString album); + DropEmitter setTrackAlbumArtist(QString albumArtist); + DropEmitter setTrackArtists(QVector artists); + DropEmitter setTrackArtUrl(QString artUrl); // clang-format off dbus::DBusPropertyGroup appProperties; @@ -370,6 +382,7 @@ private: DBusMprisPlayerApp* app = nullptr; DBusMprisPlayer* player = nullptr; + quint32 mUniqueId = 0; QString mTrackId; QString mTrackUrl; QString mTrackTitle; From 3a8e67e8abe3b09d4099cebe91ad5af02a4d8939 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 30 Jul 2024 12:19:59 -0700 Subject: [PATCH 100/305] core/util: move DropEmitter to utils and add generic accessor macros --- src/core/types.cpp | 19 ----------- src/core/types.hpp | 25 -------------- src/core/util.hpp | 81 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 44 deletions(-) create mode 100644 src/core/util.hpp diff --git a/src/core/types.cpp b/src/core/types.cpp index d9c025fb..5ed63a02 100644 --- a/src/core/types.cpp +++ b/src/core/types.cpp @@ -21,22 +21,3 @@ Qt::Edges Edges::toQt(Edges::Flags edges) { return Qt::Edges(edges.toInt()); } bool Edges::isOpposing(Edges::Flags edges) { return edges.testFlags(Edges::Top | Edges::Bottom) || edges.testFlags(Edges::Left | Edges::Right); } - -DropEmitter::DropEmitter(DropEmitter&& other) noexcept: object(other.object), signal(other.signal) { - other.object = nullptr; -} - -DropEmitter& DropEmitter::operator=(DropEmitter&& other) noexcept { - this->object = other.object; - this->signal = other.signal; - other.object = nullptr; - return *this; -} - -DropEmitter::~DropEmitter() { this->call(); } - -void DropEmitter::call() { - if (!this->object) return; - this->signal(this->object); - this->object = nullptr; -} diff --git a/src/core/types.hpp b/src/core/types.hpp index a23ff085..13fce824 100644 --- a/src/core/types.hpp +++ b/src/core/types.hpp @@ -69,28 +69,3 @@ bool isOpposing(Flags edges); }; // namespace Edges Q_DECLARE_OPERATORS_FOR_FLAGS(Edges::Flags); - -// NOLINTBEGIN -#define DROP_EMIT(object, func) \ - DropEmitter(object, static_cast([](typeof(object) o) { o->func(); })) -// NOLINTEND - -class DropEmitter { -public: - template - DropEmitter(O* object, void (*signal)(O*)) - : object(object) - , signal(*reinterpret_cast(signal)) {} // NOLINT - - DropEmitter() = default; - DropEmitter(DropEmitter&& other) noexcept; - DropEmitter& operator=(DropEmitter&& other) noexcept; - ~DropEmitter(); - Q_DISABLE_COPY(DropEmitter); - - void call(); - -private: - void* object = nullptr; - void (*signal)(void*) = nullptr; -}; diff --git a/src/core/util.hpp b/src/core/util.hpp new file mode 100644 index 00000000..249ab49d --- /dev/null +++ b/src/core/util.hpp @@ -0,0 +1,81 @@ +#pragma once + +// NOLINTBEGIN +#define DROP_EMIT(object, func) \ + DropEmitter(object, static_cast([](typeof(object) o) { o->func(); })) + +#define DROP_EMIT_IF(cond, object, func) (cond) ? DROP_EMIT(object, func) : DropEmitter() + +#define DEFINE_DROP_EMIT_IF(cond, object, func) DropEmitter func = DROP_EMIT_IF(cond, object, func) + +#define DROP_EMIT_SET(object, local, member, signal) \ + auto signal = DropEmitter(); \ + if (local == object->member) { \ + object->member = local; \ + signal = DROP_EMIT(object, signal); \ + } + +// generic accessor declarations + +#define GDECL_GETTER(type, name) [[nodiscard]] type name() const + +#define GDEF_GETTER(class, type, member, name) \ + type class::name() const { return this->member; } + +#define GDECL_SETTER(type, name) DropEmitter name(type value) + +#define GDEF_SETTER(class, type, member, name, signal) \ + DropEmitter class ::name(type value) { \ + if (value == this->member) return DropEmitter(); \ + this->member = value; \ + return DROP_EMIT(this, signal); \ + } + +#define GDECL_MEMBER(type, getter, setter) \ + GDECL_GETTER(type, getter) \ + GDECL_SETTER(type, setter) + +#define GDEF_MEMBER(class, type, member, getter, setter, signal) \ + GDEF_GETTER(class, type, member, getter) \ + GDEF_SETTER(class, type, member, setter, signal) + +#define GDEF_MEMBER_S(class, type, lower, upper) \ + GDEF_MEMBER(class, type, m##upper, lower, set##upper, lower##Changed) + +// NOLINTEND + +class DropEmitter { +public: + Q_DISABLE_COPY(DropEmitter); + template + DropEmitter(O* object, void (*signal)(O*)) + : object(object) + , signal(*reinterpret_cast(signal)) {} // NOLINT + + DropEmitter() = default; + + DropEmitter(DropEmitter&& other) noexcept: object(other.object), signal(other.signal) { + other.object = nullptr; + } + + ~DropEmitter() { this->call(); } + + DropEmitter& operator=(DropEmitter&& other) noexcept { + this->object = other.object; + this->signal = other.signal; + other.object = nullptr; + return *this; + } + + explicit operator bool() const noexcept { return this->object; } + + void call() { + if (!this->object) return; + this->signal(this->object); + this->object = nullptr; + } + +private: + void* object = nullptr; + void (*signal)(void*) = nullptr; +}; From 8873a06962e7bf09bca9a2b68254c878278e3390 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 30 Jul 2024 12:20:39 -0700 Subject: [PATCH 101/305] service/notifications: use DROP_EMIT_SET for notification properties --- src/services/notifications/notification.cpp | 46 ++++++--------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/src/services/notifications/notification.cpp b/src/services/notifications/notification.cpp index 46a337aa..b7080918 100644 --- a/src/services/notifications/notification.cpp +++ b/src/services/notifications/notification.cpp @@ -8,6 +8,7 @@ #include #include +#include "../../core/util.hpp" #include "../../core/desktopentry.hpp" #include "../../core/iconimageprovider.hpp" #include "dbusimage.hpp" @@ -130,29 +131,20 @@ void Notification::updateProperties( } } - auto appNameChanged = appName != this->mAppName; - auto appIconChanged = appIcon != this->mAppIcon; - auto summaryChanged = summary != this->mSummary; - auto bodyChanged = body != this->mBody; - auto expireTimeoutChanged = expireTimeout != this->mExpireTimeout; - auto urgencyChanged = urgency != this->mUrgency; - auto hasActionIconsChanged = hasActionIcons != this->mHasActionIcons; - auto isResidentChanged = isResident != this->mIsResident; - auto isTransientChanged = isTransient != this->mIsTransient; - auto desktopEntryChanged = desktopEntry != this->mDesktopEntry; - auto imageChanged = imagePixmap || imagePath != this->mImagePath; - auto hintsChanged = hints != this->mHints; + DROP_EMIT_SET(this, appName, mAppName, appNameChanged); + DROP_EMIT_SET(this, appIcon, mAppIcon, appIconChanged); + DROP_EMIT_SET(this, summary, mSummary, summaryChanged); + DROP_EMIT_SET(this, body, mBody, bodyChanged); + DROP_EMIT_SET(this, expireTimeout, mExpireTimeout, expireTimeoutChanged); + DEFINE_DROP_EMIT_IF(urgency != this->mUrgency, this, urgencyChanged); + DROP_EMIT_SET(this, hasActionIcons, mHasActionIcons, hasActionIconsChanged); + DROP_EMIT_SET(this, isResident, mIsResident, isResidentChanged); + DROP_EMIT_SET(this, isTransient, mIsTransient, isTransientChanged); + DROP_EMIT_SET(this, desktopEntry, mDesktopEntry, desktopEntryChanged); + DEFINE_DROP_EMIT_IF(imagePixmap || imagePath != this->mImagePath, this, imageChanged); + DROP_EMIT_SET(this, hints, mHints, hintsChanged); - if (appNameChanged) this->mAppName = appName; - if (appIconChanged) this->mAppIcon = appIcon; - if (summaryChanged) this->mSummary = summary; - if (bodyChanged) this->mBody = body; - if (expireTimeoutChanged) this->mExpireTimeout = expireTimeout; if (urgencyChanged) this->mUrgency = static_cast(urgency); - if (hasActionIcons) this->mHasActionIcons = hasActionIcons; - if (isResidentChanged) this->mIsResident = isResident; - if (isTransientChanged) this->mIsTransient = isTransient; - if (desktopEntryChanged) this->mDesktopEntry = desktopEntry; NotificationImage* oldImage = nullptr; @@ -203,19 +195,7 @@ void Notification::updateProperties( << "sent an action set of an invalid length."; } - if (appNameChanged) emit this->appNameChanged(); - if (appIconChanged) emit this->appIconChanged(); - if (summaryChanged) emit this->summaryChanged(); - if (bodyChanged) emit this->bodyChanged(); - if (expireTimeoutChanged) emit this->expireTimeoutChanged(); - if (urgencyChanged) emit this->urgencyChanged(); if (actionsChanged) emit this->actionsChanged(); - if (hasActionIconsChanged) emit this->hasActionIconsChanged(); - if (isResidentChanged) emit this->isResidentChanged(); - if (isTransientChanged) emit this->isTransientChanged(); - if (desktopEntryChanged) emit this->desktopEntryChanged(); - if (imageChanged) emit this->imageChanged(); - if (hintsChanged) emit this->hintsChanged(); for (auto* action: deletedActions) { delete action; From ba1e535f9ce9148a51db2be6a68f8486070b75e6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 30 Jul 2024 20:23:57 -0700 Subject: [PATCH 102/305] core/util: add experimental member macros An experiment that should reduce boilerplate for properties that just access a backing value. Code also exists for using it as an interface for other properties as well, but isn't currently in use. --- src/core/types.hpp | 2 +- src/core/util.hpp | 147 ++++++++++++++++---- src/services/mpris/player.cpp | 120 +++++----------- src/services/mpris/player.hpp | 39 +++--- src/services/notifications/notification.cpp | 73 +++++----- src/services/notifications/notification.hpp | 40 +++--- 6 files changed, 236 insertions(+), 185 deletions(-) diff --git a/src/core/types.hpp b/src/core/types.hpp index 13fce824..cacd7b39 100644 --- a/src/core/types.hpp +++ b/src/core/types.hpp @@ -3,8 +3,8 @@ #include #include #include -#include #include +#include #include class Box { diff --git a/src/core/util.hpp b/src/core/util.hpp index 249ab49d..82a2082e 100644 --- a/src/core/util.hpp +++ b/src/core/util.hpp @@ -1,4 +1,5 @@ #pragma once +#include // NOLINTBEGIN #define DROP_EMIT(object, func) \ @@ -14,34 +15,6 @@ object->member = local; \ signal = DROP_EMIT(object, signal); \ } - -// generic accessor declarations - -#define GDECL_GETTER(type, name) [[nodiscard]] type name() const - -#define GDEF_GETTER(class, type, member, name) \ - type class::name() const { return this->member; } - -#define GDECL_SETTER(type, name) DropEmitter name(type value) - -#define GDEF_SETTER(class, type, member, name, signal) \ - DropEmitter class ::name(type value) { \ - if (value == this->member) return DropEmitter(); \ - this->member = value; \ - return DROP_EMIT(this, signal); \ - } - -#define GDECL_MEMBER(type, getter, setter) \ - GDECL_GETTER(type, getter) \ - GDECL_SETTER(type, setter) - -#define GDEF_MEMBER(class, type, member, getter, setter, signal) \ - GDEF_GETTER(class, type, member, getter) \ - GDEF_SETTER(class, type, member, setter, signal) - -#define GDEF_MEMBER_S(class, type, lower, upper) \ - GDEF_MEMBER(class, type, m##upper, lower, set##upper, lower##Changed) - // NOLINTEND class DropEmitter { @@ -75,7 +48,125 @@ public: this->object = nullptr; } + // orders calls for multiple emitters (instead of reverse definition order) + template + static void call(Args&... args) { + (args.call(), ...); + } + private: void* object = nullptr; void (*signal)(void*) = nullptr; }; + +// NOLINTBEGIN +#define DECLARE_MEMBER(class, name, member, signal) \ + using M_##name = MemberMetadata<&class ::member, &class ::signal> + +#define DECLARE_MEMBER_NS(class, name, member) using M_##name = MemberMetadata<&class ::member> + +#define DECLARE_MEMBER_GET(name) [[nodiscard]] M_##name::Ref name() const +#define DECLARE_MEMBER_SET(name, setter) M_##name::Ret setter(M_##name::Ref value) + +#define DECLARE_MEMBER_GETSET(name, setter) \ + DECLARE_MEMBER_GET(name); \ + DECLARE_MEMBER_SET(name, setter) + +#define DECLARE_MEMBER_FULL(class, name, setter, member, signal) \ + DECLARE_MEMBER(class, name, member, signal); \ + DECLARE_MEMBER_GETSET(name, setter) + +#define DECLARE_MEMBER_WITH_GET(class, name, member, signal) \ + DECLARE_MEMBER(class, name, member, signal); \ + \ +public: \ + DECLARE_MEMBER_GET(name); \ + \ +private: + +#define DECLARE_PRIVATE_MEMBER(class, name, setter, member, signal) \ + DECLARE_MEMBER_WITH_GET(class, name, member, signal); \ + DECLARE_MEMBER_SET(name, setter); + +#define DECLARE_PMEMBER(type, name) using M_##name = PseudomemberMetadata; +#define DECLARE_PMEMBER_NS(type, name) using M_##name = PseudomemberMetadata; + +#define DECLARE_PMEMBER_FULL(type, name, setter) \ + DECLARE_PMEMBER(type, name); \ + DECLARE_MEMBER_GETSET(name, setter) + +#define DECLARE_PMEMBER_WITH_GET(type, name) \ + DECLARE_PMEMBER(type, name); \ + \ +public: \ + DECLARE_MEMBER_GET(name); \ + \ +private: + +#define DECLARE_PRIVATE_PMEMBER(type, name, setter) \ + DECLARE_PMEMBER_WITH_GET(type, name); \ + DECLARE_MEMBER_SET(name, setter); + +#define DEFINE_PMEMBER_GET_M(Class, Member, name) Member::Ref Class::name() const +#define DEFINE_PMEMBER_GET(Class, name) DEFINE_PMEMBER_GET_M(Class, Class::M_##name, name) + +#define DEFINE_MEMBER_GET_M(Class, Member, name) \ + DEFINE_PMEMBER_GET_M(Class, Member, name) { return Member::get(this); } + +#define DEFINE_MEMBER_GET(Class, name) DEFINE_MEMBER_GET_M(Class, Class::M_##name, name) + +#define DEFINE_MEMBER_SET_M(Class, Member, setter) \ + Member::Ret Class::setter(Member::Ref value) { return Member::set(this, value); } + +#define DEFINE_MEMBER_SET(Class, name, setter) DEFINE_MEMBER_SET_M(Class, Class::M_##name, setter) + +#define DEFINE_MEMBER_GETSET(Class, name, setter) \ + DEFINE_MEMBER_GET(Class, name) \ + DEFINE_MEMBER_SET(Class, name, setter) +// NOLINTEND + +template +class MemberPointerTraits; + +template +class MemberPointerTraits { +public: + using Class = C; + using Type = T; +}; + +template +class MemberMetadata { + using Traits = MemberPointerTraits; + using Class = Traits::Class; + +public: + using Type = Traits::Type; + using Ref = const Type&; + using Ret = std::conditional_t; + + static Ref get(const Class* obj) { return obj->*member; } + + static Ret set(Class* obj, Ref value) { + if constexpr (signal == nullptr) { + if (MemberMetadata::get(obj) == value) return; + obj->*member = value; + } else { + if (MemberMetadata::get(obj) == value) return DropEmitter(); + obj->*member = value; + return DropEmitter(obj, &MemberMetadata::emitForObject); + } + } + +private: + static void emitForObject(Class* obj) { (obj->*signal)(); } +}; + +// allows use of member macros without an actual field backing them +template +class PseudomemberMetadata { +public: + using Type = T; + using Ref = const Type&; + using Ret = std::conditional_t; +}; diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 8a06319c..d17975b0 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -1,5 +1,4 @@ #include "player.hpp" -#include #include #include @@ -12,7 +11,7 @@ #include #include -#include "../../core/types.hpp" +#include "../../core/util.hpp" #include "../../dbus/properties.hpp" #include "dbus_player.h" #include "dbus_player_app.h" @@ -167,8 +166,8 @@ bool MprisPlayer::canQuit() const { return this->pCanQuit.get(); } bool MprisPlayer::canRaise() const { return this->pCanRaise.get(); } bool MprisPlayer::canSetFullscreen() const { return this->pCanSetFullscreen.get(); } -QString MprisPlayer::identity() const { return this->pIdentity.get(); } -QString MprisPlayer::desktopEntry() const { return this->pDesktopEntry.get(); } +const QString& MprisPlayer::identity() const { return this->pIdentity.get(); } +const QString& MprisPlayer::desktopEntry() const { return this->pDesktopEntry.get(); } qlonglong MprisPlayer::positionMs() const { if (!this->positionSupported()) return 0; // unsupported @@ -256,13 +255,7 @@ void MprisPlayer::setVolume(qreal volume) { this->pVolume.write(); } -quint32 MprisPlayer::uniqueId() const { return this->mUniqueId; } -QVariantMap MprisPlayer::metadata() const { return this->pMetadata.get(); } -QString MprisPlayer::trackTitle() const { return this->mTrackTitle; } -QString MprisPlayer::trackAlbum() const { return this->mTrackAlbum; } -QString MprisPlayer::trackAlbumArtist() const { return this->mTrackAlbumArtist; } -QVector MprisPlayer::trackArtists() const { return this->mTrackArtists; } -QString MprisPlayer::trackArtUrl() const { return this->mTrackArtUrl; } +const QVariantMap& MprisPlayer::metadata() const { return this->pMetadata.get(); } void MprisPlayer::onMetadataChanged() { emit this->metadataChanged(); @@ -273,7 +266,7 @@ void MprisPlayer::onMetadataChanged() { length = lengthVariant.value(); } - auto emitLengthChanged = this->setLength(length); + auto lengthChanged = this->setLength(length); auto trackChanged = false; @@ -298,45 +291,20 @@ void MprisPlayer::onMetadataChanged() { } } - DropEmitter emitTrackTitle; - auto trackTitle = this->pMetadata.get().value("xesam:title"); - if (trackTitle.isValid() && trackTitle.canConvert()) { - emitTrackTitle = this->setTrackTitle(trackTitle.toString()); - } else if (trackChanged) { - emitTrackTitle = this->setTrackTitle("Unknown Track"); - } + auto trackTitle = this->pMetadata.get().value("xesam:title").toString(); + auto trackTitleChanged = this->setTrackTitle(trackTitle.isNull() ? "Unknown Track" : trackTitle); - DropEmitter emitTrackAlbum; - auto trackAlbum = this->pMetadata.get().value("xesam:album"); - if (trackAlbum.isValid() && trackAlbum.canConvert()) { - emitTrackAlbum = this->setTrackAlbum(trackAlbum.toString()); - } else if (trackChanged) { - emitTrackAlbum = this->setTrackAlbum("Unknown Album"); - } + auto trackArtists = this->pMetadata.get().value("xesam:artist").value>(); + auto trackArtistsChanged = this->setTrackArtists(trackArtists); - DropEmitter emitTrackAlbumArtist; - auto trackAlbumArtist = this->pMetadata.get().value("xesam:albumArtist"); - if (trackAlbumArtist.isValid() && trackAlbumArtist.canConvert()) { - emitTrackAlbumArtist = this->setTrackAlbumArtist(trackAlbumArtist.toString()); - } else if (trackChanged) { - emitTrackAlbumArtist = this->setTrackAlbumArtist("Unknown Artist"); - } + auto trackAlbum = this->pMetadata.get().value("xesam:album").toString(); + auto trackAlbumChanged = this->setTrackAlbum(trackAlbum.isNull() ? "Unknown Album" : trackAlbum); - DropEmitter emitTrackArtists; - auto trackArtists = this->pMetadata.get().value("xesam:artist"); - if (trackArtists.isValid() && trackArtists.canConvert>()) { - emitTrackArtists = this->setTrackArtists(trackArtists.value>()); - } else if (trackChanged) { - emitTrackArtists = this->setTrackArtists({}); - } + auto trackAlbumArtist = this->pMetadata.get().value("xesam:albumArtist").toString(); + auto trackAlbumArtistChanged = this->setTrackAlbumArtist(trackAlbumArtist); - DropEmitter emitTrackArtUrl; - auto trackArtUrl = this->pMetadata.get().value("mpris:artUrl"); - if (trackArtUrl.isValid() && trackArtUrl.canConvert()) { - emitTrackArtUrl = this->setTrackArtUrl(trackArtUrl.toString()); - } else if (trackChanged) { - emitTrackArtUrl = this->setTrackArtUrl(""); - } + auto trackArtUrl = this->pMetadata.get().value("mpris:artUrl").toString(); + auto trackArtUrlChanged = this->setTrackArtUrl(trackArtUrl); if (trackChanged) { this->mUniqueId++; @@ -344,49 +312,25 @@ void MprisPlayer::onMetadataChanged() { this->pPosition.update(); emit this->trackChanged(); } + + DropEmitter::call( + trackTitleChanged, + trackArtistsChanged, + trackAlbumChanged, + trackAlbumArtistChanged, + trackArtUrlChanged, + lengthChanged + ); } -DropEmitter MprisPlayer::setLength(qlonglong length) { - if (length == this->mLength) return DropEmitter(); +DEFINE_MEMBER_GET(MprisPlayer, uniqueId); +DEFINE_MEMBER_SET(MprisPlayer, length, setLength); - this->mLength = length; - return DROP_EMIT(this, lengthChanged); -} - -DropEmitter MprisPlayer::setTrackTitle(QString title) { - if (title == this->mTrackTitle) return DropEmitter(); - - this->mTrackTitle = std::move(title); - return DROP_EMIT(this, trackTitleChanged); -} - -DropEmitter MprisPlayer::setTrackAlbum(QString album) { - if (album == this->mTrackAlbum) return DropEmitter(); - - this->mTrackAlbum = std::move(album); - return DROP_EMIT(this, trackAlbumChanged); -} - -DropEmitter MprisPlayer::setTrackAlbumArtist(QString albumArtist) { - if (albumArtist == this->mTrackAlbumArtist) return DropEmitter(); - - this->mTrackAlbumArtist = std::move(albumArtist); - return DROP_EMIT(this, trackAlbumArtistChanged); -} - -DropEmitter MprisPlayer::setTrackArtists(QVector artists) { - if (artists == this->mTrackArtists) return DropEmitter(); - - this->mTrackArtists = std::move(artists); - return DROP_EMIT(this, trackArtistsChanged); -} - -DropEmitter MprisPlayer::setTrackArtUrl(QString artUrl) { - if (artUrl == this->mTrackArtUrl) return DropEmitter(); - - this->mTrackArtUrl = std::move(artUrl); - return DROP_EMIT(this, trackArtUrlChanged); -} +DEFINE_MEMBER_GETSET(MprisPlayer, trackTitle, setTrackTitle); +DEFINE_MEMBER_GETSET(MprisPlayer, trackArtists, setTrackArtists); +DEFINE_MEMBER_GETSET(MprisPlayer, trackAlbum, setTrackAlbum); +DEFINE_MEMBER_GETSET(MprisPlayer, trackAlbumArtist, setTrackAlbumArtist); +DEFINE_MEMBER_GETSET(MprisPlayer, trackArtUrl, setTrackArtUrl); MprisPlaybackState::Enum MprisPlayer::playbackState() const { return this->mPlaybackState; } @@ -426,9 +370,7 @@ void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) { } void MprisPlayer::play() { this->setPlaybackState(MprisPlaybackState::Playing); } - void MprisPlayer::pause() { this->setPlaybackState(MprisPlaybackState::Paused); } - void MprisPlayer::stop() { this->setPlaybackState(MprisPlaybackState::Stopped); } void MprisPlayer::togglePlaying() { diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 627512f1..04e78208 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -7,7 +7,7 @@ #include #include "../../core/doc.hpp" -#include "../../core/types.hpp" +#include "../../core/util.hpp" #include "../../dbus/properties.hpp" #include "dbus_player.h" #include "dbus_player_app.h" @@ -239,8 +239,8 @@ public: [[nodiscard]] bool canRaise() const; [[nodiscard]] bool canSetFullscreen() const; - [[nodiscard]] QString identity() const; - [[nodiscard]] QString desktopEntry() const; + [[nodiscard]] const QString& identity() const; + [[nodiscard]] const QString& desktopEntry() const; [[nodiscard]] qlonglong positionMs() const; [[nodiscard]] qreal position() const; @@ -254,13 +254,7 @@ public: [[nodiscard]] bool volumeSupported() const; void setVolume(qreal volume); - [[nodiscard]] quint32 uniqueId() const; - [[nodiscard]] QVariantMap metadata() const; - [[nodiscard]] QString trackTitle() const; - [[nodiscard]] QString trackAlbum() const; - [[nodiscard]] QString trackAlbumArtist() const; - [[nodiscard]] QVector trackArtists() const; - [[nodiscard]] QString trackArtUrl() const; + [[nodiscard]] const QVariantMap& metadata() const; [[nodiscard]] MprisPlaybackState::Enum playbackState() const; void setPlaybackState(MprisPlaybackState::Enum playbackState); @@ -338,13 +332,6 @@ private slots: void onLoopStatusChanged(); private: - DropEmitter setLength(qlonglong length); - DropEmitter setTrackTitle(QString title); - DropEmitter setTrackAlbum(QString album); - DropEmitter setTrackAlbumArtist(QString albumArtist); - DropEmitter setTrackArtists(QVector artists); - DropEmitter setTrackArtUrl(QString artUrl); - // clang-format off dbus::DBusPropertyGroup appProperties; dbus::DBusProperty pIdentity {this->appProperties, "Identity"}; @@ -386,10 +373,26 @@ private: QString mTrackId; QString mTrackUrl; QString mTrackTitle; + QVector mTrackArtists; QString mTrackAlbum; QString mTrackAlbumArtist; - QVector mTrackArtists; QString mTrackArtUrl; + + DECLARE_MEMBER_NS(MprisPlayer, uniqueId, mUniqueId); + + DECLARE_MEMBER(MprisPlayer, length, mLength, lengthChanged); + DECLARE_MEMBER_SET(length, setLength); + + // clang-format off + DECLARE_PRIVATE_MEMBER(MprisPlayer, trackTitle, setTrackTitle, mTrackTitle, trackTitleChanged); + DECLARE_PRIVATE_MEMBER(MprisPlayer, trackArtists, setTrackArtists, mTrackArtists, trackArtistsChanged); + DECLARE_PRIVATE_MEMBER(MprisPlayer, trackAlbum, setTrackAlbum, mTrackAlbum, trackAlbumChanged); + DECLARE_PRIVATE_MEMBER(MprisPlayer, trackAlbumArtist, setTrackAlbumArtist, mTrackAlbumArtist, trackAlbumArtistChanged); + DECLARE_PRIVATE_MEMBER(MprisPlayer, trackArtUrl, setTrackArtUrl, mTrackArtUrl, trackArtUrlChanged); + // clang-format on + +public: + DECLARE_MEMBER_GET(uniqueId); }; } // namespace qs::service::mpris diff --git a/src/services/notifications/notification.cpp b/src/services/notifications/notification.cpp index b7080918..c090c135 100644 --- a/src/services/notifications/notification.cpp +++ b/src/services/notifications/notification.cpp @@ -8,9 +8,9 @@ #include #include -#include "../../core/util.hpp" #include "../../core/desktopentry.hpp" #include "../../core/iconimageprovider.hpp" +#include "../../core/util.hpp" #include "dbusimage.hpp" #include "server.hpp" @@ -47,7 +47,7 @@ void NotificationAction::invoke() { NotificationServer::instance()->ActionInvoked(this->notification->id(), this->mIdentifier); - if (!this->notification->isResident()) { + if (!this->notification->resident()) { this->notification->close(NotificationCloseReason::Dismissed); } } @@ -88,8 +88,8 @@ void Notification::updateProperties( : NotificationUrgency::Normal; auto hasActionIcons = hints.value("action-icons").value(); - auto isResident = hints.value("resident").value(); - auto isTransient = hints.value("transient").value(); + auto resident = hints.value("resident").value(); + auto transient = hints.value("transient").value(); auto desktopEntry = hints.value("desktop-entry").value(); QString imageDataName; @@ -131,20 +131,18 @@ void Notification::updateProperties( } } - DROP_EMIT_SET(this, appName, mAppName, appNameChanged); - DROP_EMIT_SET(this, appIcon, mAppIcon, appIconChanged); - DROP_EMIT_SET(this, summary, mSummary, summaryChanged); - DROP_EMIT_SET(this, body, mBody, bodyChanged); - DROP_EMIT_SET(this, expireTimeout, mExpireTimeout, expireTimeoutChanged); - DEFINE_DROP_EMIT_IF(urgency != this->mUrgency, this, urgencyChanged); - DROP_EMIT_SET(this, hasActionIcons, mHasActionIcons, hasActionIconsChanged); - DROP_EMIT_SET(this, isResident, mIsResident, isResidentChanged); - DROP_EMIT_SET(this, isTransient, mIsTransient, isTransientChanged); - DROP_EMIT_SET(this, desktopEntry, mDesktopEntry, desktopEntryChanged); + auto expireTimeoutChanged = this->setExpireTimeout(expireTimeout); + auto appNameChanged = this->setAppName(appName); + auto appIconChanged = this->setAppIcon(appIcon); + auto summaryChanged = this->setSummary(summary); + auto bodyChanged = this->setBody(body); + auto urgencyChanged = this->setUrgency(static_cast(urgency)); + auto hasActionIconsChanged = this->setHasActionIcons(hasActionIcons); + auto residentChanged = this->setResident(resident); + auto transientChanged = this->setTransient(transient); + auto desktopEntryChanged = this->setDesktopEntry(desktopEntry); DEFINE_DROP_EMIT_IF(imagePixmap || imagePath != this->mImagePath, this, imageChanged); - DROP_EMIT_SET(this, hints, mHints, hintsChanged); - - if (urgencyChanged) this->mUrgency = static_cast(urgency); + auto hintsChanged = this->setHints(hints); NotificationImage* oldImage = nullptr; @@ -154,8 +152,6 @@ void Notification::updateProperties( this->mImagePath = imagePath; } - if (hintsChanged) this->mHints = hints; - bool actionsChanged = false; auto deletedActions = QVector(); @@ -195,6 +191,21 @@ void Notification::updateProperties( << "sent an action set of an invalid length."; } + DropEmitter::call( + expireTimeoutChanged, + appNameChanged, + appIconChanged, + summaryChanged, + bodyChanged, + urgencyChanged, + hasActionIconsChanged, + residentChanged, + transientChanged, + desktopEntryChanged, + imageChanged, + hintsChanged + ); + if (actionsChanged) emit this->actionsChanged(); for (auto* action: deletedActions) { @@ -217,17 +228,17 @@ void Notification::setTracked(bool tracked) { bool Notification::isLastGeneration() const { return this->mLastGeneration; } void Notification::setLastGeneration() { this->mLastGeneration = true; } -qreal Notification::expireTimeout() const { return this->mExpireTimeout; } -QString Notification::appName() const { return this->mAppName; } -QString Notification::appIcon() const { return this->mAppIcon; } -QString Notification::summary() const { return this->mSummary; } -QString Notification::body() const { return this->mBody; } -NotificationUrgency::Enum Notification::urgency() const { return this->mUrgency; } -QVector Notification::actions() const { return this->mActions; } -bool Notification::hasActionIcons() const { return this->mHasActionIcons; } -bool Notification::isResident() const { return this->mIsResident; } -bool Notification::isTransient() const { return this->mIsTransient; } -QString Notification::desktopEntry() const { return this->mDesktopEntry; } +DEFINE_MEMBER_GETSET(Notification, expireTimeout, setExpireTimeout); +DEFINE_MEMBER_GETSET(Notification, appName, setAppName); +DEFINE_MEMBER_GETSET(Notification, appIcon, setAppIcon); +DEFINE_MEMBER_GETSET(Notification, summary, setSummary); +DEFINE_MEMBER_GETSET(Notification, body, setBody); +DEFINE_MEMBER_GETSET(Notification, urgency, setUrgency); +DEFINE_MEMBER_GET(Notification, actions); +DEFINE_MEMBER_GETSET(Notification, hasActionIcons, setHasActionIcons); +DEFINE_MEMBER_GETSET(Notification, resident, setResident); +DEFINE_MEMBER_GETSET(Notification, transient, setTransient); +DEFINE_MEMBER_GETSET(Notification, desktopEntry, setDesktopEntry); QString Notification::image() const { if (this->mImagePixmap) { @@ -237,6 +248,6 @@ QString Notification::image() const { } } -QVariantMap Notification::hints() const { return this->mHints; } +DEFINE_MEMBER_GETSET(Notification, hints, setHints); } // namespace qs::service::notifications diff --git a/src/services/notifications/notification.hpp b/src/services/notifications/notification.hpp index e87cde9a..d5280bb7 100644 --- a/src/services/notifications/notification.hpp +++ b/src/services/notifications/notification.hpp @@ -9,6 +9,7 @@ #include #include "../../core/retainable.hpp" +#include "../../core/util.hpp" namespace qs::service::notifications { @@ -94,9 +95,9 @@ class Notification /// See @@NotificationAction.identifier for details. Q_PROPERTY(bool hasActionIcons READ hasActionIcons NOTIFY hasActionIconsChanged); /// If true, the notification will not be destroyed after an action is invoked. - Q_PROPERTY(bool resident READ isResident NOTIFY isResidentChanged); + Q_PROPERTY(bool resident READ resident NOTIFY residentChanged); /// If true, the notification should skip any kind of persistence function like a notification area. - Q_PROPERTY(bool transient READ isTransient NOTIFY isTransientChanged); + Q_PROPERTY(bool transient READ transient NOTIFY transientChanged); /// The name of the sender's desktop entry or "" if none was supplied. Q_PROPERTY(QString desktopEntry READ desktopEntry NOTIFY desktopEntryChanged); /// An image associated with the notification. @@ -140,19 +141,7 @@ public: [[nodiscard]] bool isLastGeneration() const; void setLastGeneration(); - [[nodiscard]] qreal expireTimeout() const; - [[nodiscard]] QString appName() const; - [[nodiscard]] QString appIcon() const; - [[nodiscard]] QString summary() const; - [[nodiscard]] QString body() const; - [[nodiscard]] NotificationUrgency::Enum urgency() const; - [[nodiscard]] QVector actions() const; - [[nodiscard]] bool hasActionIcons() const; - [[nodiscard]] bool isResident() const; - [[nodiscard]] bool isTransient() const; - [[nodiscard]] QString desktopEntry() const; [[nodiscard]] QString image() const; - [[nodiscard]] QVariantMap hints() const; signals: /// Sent when a notification has been closed. @@ -169,8 +158,8 @@ signals: void urgencyChanged(); void actionsChanged(); void hasActionIconsChanged(); - void isResidentChanged(); - void isTransientChanged(); + void residentChanged(); + void transientChanged(); void desktopEntryChanged(); void imageChanged(); void hintsChanged(); @@ -187,12 +176,27 @@ private: NotificationUrgency::Enum mUrgency = NotificationUrgency::Normal; QVector mActions; bool mHasActionIcons = false; - bool mIsResident = false; - bool mIsTransient = false; + bool mResident = false; + bool mTransient = false; QString mImagePath; NotificationImage* mImagePixmap = nullptr; QString mDesktopEntry; QVariantMap mHints; + + // clang-format off + DECLARE_PRIVATE_MEMBER(Notification, expireTimeout, setExpireTimeout, mExpireTimeout, expireTimeoutChanged); + DECLARE_PRIVATE_MEMBER(Notification, appName, setAppName, mAppName, appNameChanged); + DECLARE_PRIVATE_MEMBER(Notification, appIcon, setAppIcon, mAppIcon, appIconChanged); + DECLARE_PRIVATE_MEMBER(Notification, summary, setSummary, mSummary, summaryChanged); + DECLARE_PRIVATE_MEMBER(Notification, body, setBody, mBody, bodyChanged); + DECLARE_PRIVATE_MEMBER(Notification, urgency, setUrgency, mUrgency, urgencyChanged); + DECLARE_MEMBER_WITH_GET(Notification, actions, mActions, actionsChanged); + DECLARE_PRIVATE_MEMBER(Notification, hasActionIcons, setHasActionIcons, mHasActionIcons, hasActionIconsChanged); + DECLARE_PRIVATE_MEMBER(Notification, resident, setResident, mResident, residentChanged); + DECLARE_PRIVATE_MEMBER(Notification, transient, setTransient, mTransient, transientChanged); + DECLARE_PRIVATE_MEMBER(Notification, desktopEntry, setDesktopEntry, mDesktopEntry, desktopEntryChanged); + DECLARE_PRIVATE_MEMBER(Notification, hints, setHints, mHints, hintsChanged); + // clang-format on }; ///! An action associated with a Notification. From 76744c903ac4c07e5dfe4b9e32d72af8237afff5 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 30 Jul 2024 23:24:54 -0700 Subject: [PATCH 103/305] core/clock: add SystemClock --- src/core/CMakeLists.txt | 1 + src/core/clock.cpp | 66 +++++++++++++++++++++++++++++++++++++++++ src/core/clock.hpp | 65 ++++++++++++++++++++++++++++++++++++++++ src/core/module.md | 1 + 4 files changed, 133 insertions(+) create mode 100644 src/core/clock.cpp create mode 100644 src/core/clock.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index fbf006b2..c53976ba 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -36,6 +36,7 @@ qt_add_library(quickshell-core STATIC popupanchor.cpp types.cpp qsmenuanchor.cpp + clock.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/clock.cpp b/src/core/clock.cpp new file mode 100644 index 00000000..b232d0b1 --- /dev/null +++ b/src/core/clock.cpp @@ -0,0 +1,66 @@ +#include "clock.hpp" + +#include +#include +#include +#include + +#include "util.hpp" + +SystemClock::SystemClock(QObject* parent): QObject(parent) { + QObject::connect(&this->timer, &QTimer::timeout, this, &SystemClock::update); + this->update(); +} + +bool SystemClock::enabled() const { return this->mEnabled; } + +void SystemClock::setEnabled(bool enabled) { + if (enabled == this->mEnabled) return; + this->mEnabled = enabled; + emit this->enabledChanged(); + this->update(); +} + +SystemClock::Enum SystemClock::precision() const { return this->mPrecision; } + +void SystemClock::setPrecision(SystemClock::Enum precision) { + if (precision == this->mPrecision) return; + this->mPrecision = precision; + emit this->precisionChanged(); + this->update(); +} + +void SystemClock::update() { + auto time = QTime::currentTime(); + + if (this->mEnabled) { + auto secondPrecision = this->mPrecision >= SystemClock::Seconds; + auto secondChanged = this->setSeconds(secondPrecision ? time.second() : 0); + + auto minutePrecision = this->mPrecision >= SystemClock::Minutes; + auto minuteChanged = this->setMinutes(minutePrecision ? time.minute() : 0); + + auto hourPrecision = this->mPrecision >= SystemClock::Hours; + auto hourChanged = this->setHours(hourPrecision ? time.hour() : 0); + + DropEmitter::call(secondChanged, minuteChanged, hourChanged); + + auto nextTime = QTime( + hourPrecision ? time.hour() : 0, + minutePrecision ? time.minute() : 0, + secondPrecision ? time.second() : 0 + ); + + if (secondPrecision) nextTime = nextTime.addSecs(1); + else if (minutePrecision) nextTime = nextTime.addSecs(60); + else if (hourPrecision) nextTime = nextTime.addSecs(3600); + + this->timer.start(time.msecsTo(nextTime)); + } else { + this->timer.stop(); + } +} + +DEFINE_MEMBER_GETSET(SystemClock, hours, setHours); +DEFINE_MEMBER_GETSET(SystemClock, minutes, setMinutes); +DEFINE_MEMBER_GETSET(SystemClock, seconds, setSeconds); diff --git a/src/core/clock.hpp b/src/core/clock.hpp new file mode 100644 index 00000000..b3ac1228 --- /dev/null +++ b/src/core/clock.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include +#include + +#include "util.hpp" + +///! System clock accessor. +class SystemClock: public QObject { + Q_OBJECT; + /// If the clock should update. Defaults to true. + /// + /// Setting enabled to false pauses the clock. + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged); + /// The precision the clock should measure at. Defaults to `SystemClock.Seconds`. + Q_PROPERTY(SystemClock::Enum precision READ precision WRITE setPrecision NOTIFY precisionChanged); + /// The current hour. + Q_PROPERTY(quint32 hours READ hours NOTIFY hoursChanged); + /// The current minute, or 0 if @@precision is `SystemClock.Hours`. + Q_PROPERTY(quint32 minutes READ minutes NOTIFY minutesChanged); + /// The current second, or 0 if @@precision is `SystemClock.Hours` or `SystemClock.Minutes`. + Q_PROPERTY(quint32 seconds READ seconds NOTIFY secondsChanged); + QML_ELEMENT; + +public: + // must be named enum until docgen is ready to handle member enums better + enum Enum { + Hours = 1, + Minutes = 2, + Seconds = 3, + }; + Q_ENUM(Enum); + + explicit SystemClock(QObject* parent = nullptr); + + [[nodiscard]] bool enabled() const; + void setEnabled(bool enabled); + + [[nodiscard]] SystemClock::Enum precision() const; + void setPrecision(SystemClock::Enum precision); + +signals: + void enabledChanged(); + void precisionChanged(); + void hoursChanged(); + void minutesChanged(); + void secondsChanged(); + +private slots: + void update(); + +private: + bool mEnabled = true; + SystemClock::Enum mPrecision = SystemClock::Seconds; + quint32 mHours = 0; + quint32 mMinutes = 0; + quint32 mSeconds = 0; + QTimer timer; + + DECLARE_PRIVATE_MEMBER(SystemClock, hours, setHours, mHours, hoursChanged); + DECLARE_PRIVATE_MEMBER(SystemClock, minutes, setMinutes, mMinutes, minutesChanged); + DECLARE_PRIVATE_MEMBER(SystemClock, seconds, setSeconds, mSeconds, secondsChanged); +}; diff --git a/src/core/module.md b/src/core/module.md index 411b1d49..060aca9f 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -27,5 +27,6 @@ headers = [ "popupanchor.hpp", "types.hpp", "qsmenuanchor.hpp", + "clock.hpp", ] ----- From a4903eaefc3d195bae35ace6a01291a2cd237fa0 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 31 Jul 2024 01:51:53 -0700 Subject: [PATCH 104/305] core/clock: fix breakage at midnight The difference between 23:59 and 00:00 is -23:59, not 00:01. --- src/core/clock.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/clock.cpp b/src/core/clock.cpp index b232d0b1..0af9762e 100644 --- a/src/core/clock.cpp +++ b/src/core/clock.cpp @@ -55,7 +55,11 @@ void SystemClock::update() { else if (minutePrecision) nextTime = nextTime.addSecs(60); else if (hourPrecision) nextTime = nextTime.addSecs(3600); - this->timer.start(time.msecsTo(nextTime)); + auto delay = time.msecsTo(nextTime); + // day rollover + if (delay < 0) delay += 86400000; + + this->timer.start(delay); } else { this->timer.stop(); } From 9555b201fe47b4a74a432d3cb4fe7dba20cf894c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 31 Jul 2024 02:37:05 -0700 Subject: [PATCH 105/305] core/clock: fix instability causing timer to fire multiple times If the signal was fired slightly before the scheduled time, it would schedule itself again a couple ms in the future. --- src/core/clock.cpp | 80 +++++++++++++++++++++++++++++----------------- src/core/clock.hpp | 8 ++++- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/src/core/clock.cpp b/src/core/clock.cpp index 0af9762e..ee396ac8 100644 --- a/src/core/clock.cpp +++ b/src/core/clock.cpp @@ -8,7 +8,7 @@ #include "util.hpp" SystemClock::SystemClock(QObject* parent): QObject(parent) { - QObject::connect(&this->timer, &QTimer::timeout, this, &SystemClock::update); + QObject::connect(&this->timer, &QTimer::timeout, this, &SystemClock::onTimeout); this->update(); } @@ -30,41 +30,63 @@ void SystemClock::setPrecision(SystemClock::Enum precision) { this->update(); } +void SystemClock::onTimeout() { + this->setTime(this->nextTime); + this->schedule(this->nextTime); +} + void SystemClock::update() { - auto time = QTime::currentTime(); - if (this->mEnabled) { - auto secondPrecision = this->mPrecision >= SystemClock::Seconds; - auto secondChanged = this->setSeconds(secondPrecision ? time.second() : 0); - - auto minutePrecision = this->mPrecision >= SystemClock::Minutes; - auto minuteChanged = this->setMinutes(minutePrecision ? time.minute() : 0); - - auto hourPrecision = this->mPrecision >= SystemClock::Hours; - auto hourChanged = this->setHours(hourPrecision ? time.hour() : 0); - - DropEmitter::call(secondChanged, minuteChanged, hourChanged); - - auto nextTime = QTime( - hourPrecision ? time.hour() : 0, - minutePrecision ? time.minute() : 0, - secondPrecision ? time.second() : 0 - ); - - if (secondPrecision) nextTime = nextTime.addSecs(1); - else if (minutePrecision) nextTime = nextTime.addSecs(60); - else if (hourPrecision) nextTime = nextTime.addSecs(3600); - - auto delay = time.msecsTo(nextTime); - // day rollover - if (delay < 0) delay += 86400000; - - this->timer.start(delay); + this->setTime(QTime::currentTime()); + this->schedule(QTime::currentTime()); } else { this->timer.stop(); } } +void SystemClock::setTime(QTime time) { + auto secondPrecision = this->mPrecision >= SystemClock::Seconds; + auto secondChanged = this->setSeconds(secondPrecision ? time.second() : 0); + + auto minutePrecision = this->mPrecision >= SystemClock::Minutes; + auto minuteChanged = this->setMinutes(minutePrecision ? time.minute() : 0); + + auto hourPrecision = this->mPrecision >= SystemClock::Hours; + auto hourChanged = this->setHours(hourPrecision ? time.hour() : 0); + + DropEmitter::call(secondChanged, minuteChanged, hourChanged); +} + +void SystemClock::schedule(QTime floor) { + auto secondPrecision = this->mPrecision >= SystemClock::Seconds; + auto minutePrecision = this->mPrecision >= SystemClock::Minutes; + auto hourPrecision = this->mPrecision >= SystemClock::Hours; + +setnext: + auto nextTime = QTime( + hourPrecision ? floor.hour() : 0, + minutePrecision ? floor.minute() : 0, + secondPrecision ? floor.second() : 0 + ); + + if (secondPrecision) nextTime = nextTime.addSecs(1); + else if (minutePrecision) nextTime = nextTime.addSecs(60); + else if (hourPrecision) nextTime = nextTime.addSecs(3600); + + auto delay = QTime::currentTime().msecsTo(nextTime); + + // If off by more than 2 hours we likely wrapped around midnight. + if (delay < -7200000) delay += 86400000; + else if (delay < 0) { + // Otherwise its just the timer being unstable. + floor = QTime::currentTime(); + goto setnext; + } + + this->timer.start(delay); + this->nextTime = nextTime; +} + DEFINE_MEMBER_GETSET(SystemClock, hours, setHours); DEFINE_MEMBER_GETSET(SystemClock, minutes, setMinutes); DEFINE_MEMBER_GETSET(SystemClock, seconds, setSeconds); diff --git a/src/core/clock.hpp b/src/core/clock.hpp index b3ac1228..7f0dbf74 100644 --- a/src/core/clock.hpp +++ b/src/core/clock.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -49,7 +50,7 @@ signals: void secondsChanged(); private slots: - void update(); + void onTimeout(); private: bool mEnabled = true; @@ -58,6 +59,11 @@ private: quint32 mMinutes = 0; quint32 mSeconds = 0; QTimer timer; + QTime nextTime; + + void update(); + void setTime(QTime time); + void schedule(QTime floor); DECLARE_PRIVATE_MEMBER(SystemClock, hours, setHours, mHours, hoursChanged); DECLARE_PRIVATE_MEMBER(SystemClock, minutes, setMinutes, mMinutes, minutesChanged); From cb2862eca912e8115b60ee2c3a0e02a52299cc85 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 31 Jul 2024 23:10:08 -0700 Subject: [PATCH 106/305] wayland/toplevel_management: add ToplevelManager.activeToplevel --- src/wayland/toplevel_management/qml.cpp | 28 +++++++++++++++++++++++++ src/wayland/toplevel_management/qml.hpp | 26 ++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/wayland/toplevel_management/qml.cpp b/src/wayland/toplevel_management/qml.cpp index 2042262b..1f0d1fe8 100644 --- a/src/wayland/toplevel_management/qml.cpp +++ b/src/wayland/toplevel_management/qml.cpp @@ -3,6 +3,7 @@ #include #include +#include "../../core/util.hpp" #include "../../core/model.hpp" #include "../../core/proxywindow.hpp" #include "../../core/qmlscreen.hpp" @@ -132,22 +133,49 @@ ObjectModel* ToplevelManager::toplevels() { return &this->mToplevels; void ToplevelManager::onToplevelReady(impl::ToplevelHandle* handle) { auto* toplevel = new Toplevel(handle, this); + + // clang-format off QObject::connect(toplevel, &Toplevel::closed, this, &ToplevelManager::onToplevelClosed); + QObject::connect(toplevel, &Toplevel::activatedChanged, this, &ToplevelManager::onToplevelActiveChanged); + // clang-format on + + if (toplevel->activated()) this->setActiveToplevel(toplevel); this->mToplevels.insertObject(toplevel); } +void ToplevelManager::onToplevelActiveChanged() { + auto* toplevel = qobject_cast(this->sender()); + if (toplevel->activated()) this->setActiveToplevel(toplevel); +} + void ToplevelManager::onToplevelClosed() { auto* toplevel = qobject_cast(this->sender()); + if (toplevel == this->mActiveToplevel) this->setActiveToplevel(nullptr); this->mToplevels.removeObject(toplevel); } +DEFINE_MEMBER_GETSET(ToplevelManager, activeToplevel, setActiveToplevel); + ToplevelManager* ToplevelManager::instance() { static auto* instance = new ToplevelManager(); // NOLINT return instance; } +ToplevelManagerQml::ToplevelManagerQml(QObject* parent): QObject(parent) { + QObject::connect( + ToplevelManager::instance(), + &ToplevelManager::activeToplevelChanged, + this, + &ToplevelManagerQml::activeToplevelChanged + ); +} + ObjectModel* ToplevelManagerQml::toplevels() { return ToplevelManager::instance()->toplevels(); } +Toplevel* ToplevelManagerQml::activeToplevel() { + return ToplevelManager::instance()->activeToplevel(); +} + } // namespace qs::wayland::toplevel_management diff --git a/src/wayland/toplevel_management/qml.hpp b/src/wayland/toplevel_management/qml.hpp index 64951b63..d50713c5 100644 --- a/src/wayland/toplevel_management/qml.hpp +++ b/src/wayland/toplevel_management/qml.hpp @@ -7,6 +7,7 @@ #include "../../core/model.hpp" #include "../../core/proxywindow.hpp" #include "../../core/qmlscreen.hpp" +#include "../../core/util.hpp" namespace qs::wayland::toplevel_management { @@ -111,14 +112,27 @@ public: static ToplevelManager* instance(); +signals: + void activeToplevelChanged(); + private slots: void onToplevelReady(impl::ToplevelHandle* handle); + void onToplevelActiveChanged(); void onToplevelClosed(); private: explicit ToplevelManager(); ObjectModel mToplevels {this}; + Toplevel* mActiveToplevel = nullptr; + + DECLARE_PRIVATE_MEMBER( + ToplevelManager, + activeToplevel, + setActiveToplevel, + mActiveToplevel, + activeToplevelChanged + ); }; ///! Exposes a list of Toplevels. @@ -127,14 +141,24 @@ private: /// wayland protocol. class ToplevelManagerQml: public QObject { Q_OBJECT; + /// All toplevel windows exposed by the compositor. Q_PROPERTY(ObjectModel* toplevels READ toplevels CONSTANT); + /// Active toplevel or null. + /// + /// > [!INFO] If multiple are active, this will be the most recently activated one. + /// > Usually compositors will not report more than one toplevel as active at a time. + Q_PROPERTY(Toplevel* activeToplevel READ activeToplevel NOTIFY activeToplevelChanged); QML_NAMED_ELEMENT(ToplevelManager); QML_SINGLETON; public: - explicit ToplevelManagerQml(QObject* parent = nullptr): QObject(parent) {} + explicit ToplevelManagerQml(QObject* parent = nullptr); [[nodiscard]] static ObjectModel* toplevels(); + [[nodiscard]] static Toplevel* activeToplevel(); + +signals: + void activeToplevelChanged(); }; } // namespace qs::wayland::toplevel_management From 2c87cc3803229a8f5fa80e6e5a4961373050b5df Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 1 Aug 2024 21:47:18 -0700 Subject: [PATCH 107/305] core: stop using the simple animation driver by default --- src/core/main.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/core/main.cpp b/src/core/main.cpp index b893ab9f..36717db0 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -329,12 +329,15 @@ int qs_main(int argc, char** argv) { qputenv(var.toUtf8(), val.toUtf8()); } - // The simple animation driver seems to work far better than the default one - // when more than one window is in use, and even with a single window appears - // to improve animation quality. - if (!qEnvironmentVariableIsSet("QSG_USE_SIMPLE_ANIMATION_DRIVER")) { - qputenv("QSG_USE_SIMPLE_ANIMATION_DRIVER", "1"); - } + // While the simple animation driver can lead to better animations in some cases, + // it also can cause excessive repainting at excessively high framerates which can + // lead to noticeable amounts of gpu usage, including overheating on some systems. + // This gets worse the more windows are open, as repaints trigger on all of them for + // some reason. See QTBUG-126099 for details. + + // if (!qEnvironmentVariableIsSet("QSG_USE_SIMPLE_ANIMATION_DRIVER")) { + // qputenv("QSG_USE_SIMPLE_ANIMATION_DRIVER", "1"); + // } // Some programs place icons in the pixmaps folder instead of the icons folder. // This seems to be controlled by the QPA and qt6ct does not provide it. From 79b2fea52e6d46c888a30296c7bc07a92be6f7bb Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 2 Aug 2024 01:32:12 -0700 Subject: [PATCH 108/305] core/util: fix MemberMetadata compile on gcc --- src/core/util.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/util.hpp b/src/core/util.hpp index 82a2082e..b2599234 100644 --- a/src/core/util.hpp +++ b/src/core/util.hpp @@ -143,7 +143,7 @@ class MemberMetadata { public: using Type = Traits::Type; using Ref = const Type&; - using Ret = std::conditional_t; + using Ret = std::conditional_t, void, DropEmitter>; static Ref get(const Class* obj) { return obj->*member; } From d582bb7b578c54bf413a55c6b817e52cfb1e977e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 2 Aug 2024 02:09:55 -0700 Subject: [PATCH 109/305] core: add per-config shell id Will be useful for future functionality such as IPC and caching. --- src/core/main.cpp | 19 +++++++++++++++---- src/core/rootwrapper.cpp | 9 +++++---- src/core/rootwrapper.hpp | 3 ++- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/core/main.cpp b/src/core/main.cpp index 36717db0..f3fef81d 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -31,10 +32,12 @@ int qs_main(int argc, char** argv) { auto useQApplication = false; auto nativeTextRendering = false; auto desktopSettingsAware = true; + auto shellId = QString(); QHash envOverrides; int debugPort = -1; bool waitForDebug = false; + bool printCurrent = false; { const auto app = QCoreApplication(argc, argv); @@ -85,7 +88,7 @@ int qs_main(int argc, char** argv) { } { - auto printCurrent = parser.isSet(currentOption); + printCurrent = parser.isSet(currentOption); // NOLINTBEGIN #define CHECK(rname, name, level, label, expr) \ @@ -274,9 +277,9 @@ int qs_main(int argc, char** argv) { #undef CHECK #undef OPTSTR - qInfo() << "config file path:" << configFilePath; + shellId = QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); - if (printCurrent) return 0; + qInfo() << "config file path:" << configFilePath; } if (!QFile(configFilePath).exists()) { @@ -315,6 +318,8 @@ int qs_main(int argc, char** argv) { auto var = envPragma.sliced(0, splitIdx).trimmed(); auto val = envPragma.sliced(splitIdx + 1).trimmed(); envOverrides.insert(var, val); + } else if (pragma.startsWith("ShellId ")) { + shellId = pragma.sliced(8).trimmed(); } else { qCritical() << "Unrecognized pragma" << pragma; return -1; @@ -325,6 +330,12 @@ int qs_main(int argc, char** argv) { file.close(); } + + if (printCurrent) { + qInfo() << "shell id:" << shellId; + return 0; + } + for (auto [var, val]: envOverrides.asKeyValueRange()) { qputenv(var.toUtf8(), val.toUtf8()); } @@ -396,7 +407,7 @@ int qs_main(int argc, char** argv) { QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); } - auto root = RootWrapper(configFilePath); + auto root = RootWrapper(configFilePath, shellId); QGuiApplication::setQuitOnLastWindowClosed(false); auto code = QGuiApplication::exec(); diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index 096ac4de..c4c5e711 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -16,10 +16,11 @@ #include "scan.hpp" #include "shell.hpp" -RootWrapper::RootWrapper(QString rootPath) - : QObject(nullptr) - , rootPath(std::move(rootPath)) - , originalWorkingDirectory(QDir::current().absolutePath()) { +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 diff --git a/src/core/rootwrapper.hpp b/src/core/rootwrapper.hpp index 7958ee5c..46603097 100644 --- a/src/core/rootwrapper.hpp +++ b/src/core/rootwrapper.hpp @@ -12,7 +12,7 @@ class RootWrapper: public QObject { Q_OBJECT; public: - explicit RootWrapper(QString rootPath); + explicit RootWrapper(QString rootPath, QString shellId); ~RootWrapper() override; Q_DISABLE_COPY_MOVE(RootWrapper); @@ -24,6 +24,7 @@ private slots: private: QString rootPath; + QString shellId; EngineGeneration* generation = nullptr; QString originalWorkingDirectory; }; From 533b389742f44e4e96c29b91a94fbd1352e2d516 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 2 Aug 2024 13:56:30 -0700 Subject: [PATCH 110/305] nix: build with split debuginfo in release mode --- default.nix | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/default.nix b/default.nix index e77109f7..2393d8ab 100644 --- a/default.nix +++ b/default.nix @@ -62,18 +62,9 @@ QTWAYLANDSCANNER = lib.optionalString withWayland "${qt6.qtwayland}/libexec/qtwaylandscanner"; - configurePhase = let - cmakeBuildType = if debug - then "Debug" - else "RelWithDebInfo"; - in '' - cmakeBuildType=${cmakeBuildType} # qt6 setup hook resets this for some godforsaken reason - cmakeConfigurePhase - ''; + cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; - cmakeFlags = [ - "-DGIT_REVISION=${gitRev}" - ] + cmakeFlags = [ "-DGIT_REVISION=${gitRev}" ] ++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF" ++ lib.optional (!withWayland) "-DWAYLAND=OFF" ++ lib.optional (!withPipewire) "-DSERVICE_PIPEWIRE=OFF" @@ -82,7 +73,13 @@ buildPhase = "ninjaBuildPhase"; enableParallelBuilding = true; - dontStrip = true; + + # 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://git.outfoxxed.me/outfoxxed/quickshell"; From 46f48f2f875bbef1178e76c15e4ddcb47c22ae8c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 2 Aug 2024 18:48:09 -0700 Subject: [PATCH 111/305] core/log: add fancy logger --- src/core/CMakeLists.txt | 1 + src/core/logging.cpp | 74 +++++++++++++++++++++++++++++++++++++++++ src/core/logging.hpp | 6 ++++ src/core/main.cpp | 8 ++--- 4 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 src/core/logging.cpp create mode 100644 src/core/logging.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index c53976ba..eedfca99 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -37,6 +37,7 @@ qt_add_library(quickshell-core STATIC types.cpp qsmenuanchor.cpp clock.cpp + logging.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/logging.cpp b/src/core/logging.cpp new file mode 100644 index 00000000..d216a980 --- /dev/null +++ b/src/core/logging.cpp @@ -0,0 +1,74 @@ +#include "logging.hpp" +#include +#include + +#include +#include +#include + +namespace { + +bool COLOR_LOGS = false; // NOLINT + +void formatMessage( + QtMsgType type, + const QMessageLogContext& context, + const QString& msg, + bool color +) { + const auto* typeString = "[log error]"; + + if (color) { + switch (type) { + case QtDebugMsg: typeString = "\033[34m DEBUG"; break; + case QtInfoMsg: typeString = "\033[32m INFO"; break; + case QtWarningMsg: typeString = "\033[33m WARN"; break; + case QtCriticalMsg: typeString = "\033[31m ERROR"; break; + case QtFatalMsg: typeString = "\033[31m FATAL"; break; + } + } else { + switch (type) { + case QtDebugMsg: typeString = " DEBUG"; break; + case QtInfoMsg: typeString = " INFO"; break; + case QtWarningMsg: typeString = " WARN"; break; + case QtCriticalMsg: typeString = " ERROR"; break; + case QtFatalMsg: typeString = " FATAL"; break; + } + } + + const auto isDefault = strcmp(context.category, "default") == 0; + + const char* format = nullptr; + + if (color) { + if (type == QtFatalMsg) { + if (isDefault) format = "%s: %s\033[0m\n"; + else format = "%s %s: %s\033[0m\n"; + } else { + if (isDefault) format = "%s\033[0m: %s\n"; + else format = "%s \033[97m%s\033[0m: %s\n"; + } + } else { + if (isDefault) format = "%s: %s\n"; + else format = "%s %s: %s\n"; + } + + if (isDefault) { + printf(format, typeString, msg.toStdString().c_str()); + } else { + printf(format, typeString, context.category, msg.toStdString().c_str()); + } + + fflush(stdout); +} + +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg) { + formatMessage(type, context, msg, COLOR_LOGS); +} + +} // namespace + +void LogManager::setup() { + COLOR_LOGS = qEnvironmentVariableIsEmpty("NO_COLOR"); + qInstallMessageHandler(&messageHandler); +} diff --git a/src/core/logging.hpp b/src/core/logging.hpp new file mode 100644 index 00000000..c2d8d13f --- /dev/null +++ b/src/core/logging.hpp @@ -0,0 +1,6 @@ +#pragma once + +class LogManager { +public: + static void setup(); +}; diff --git a/src/core/main.cpp b/src/core/main.cpp index f3fef81d..12f3eb38 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -22,10 +22,12 @@ #include #include +#include "logging.hpp" #include "plugin.hpp" #include "rootwrapper.hpp" int qs_main(int argc, char** argv) { + LogManager::setup(); QString configFilePath; QString workingDirectory; @@ -330,11 +332,9 @@ int qs_main(int argc, char** argv) { file.close(); } + qInfo() << "shell id:" << shellId; - if (printCurrent) { - qInfo() << "shell id:" << shellId; - return 0; - } + if (printCurrent) return 0; for (auto [var, val]: envOverrides.asKeyValueRange()) { qputenv(var.toUtf8(), val.toUtf8()); From 6bf4826ae74b143bf2e9c7017fa9cfb5e18d4dea Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 2 Aug 2024 21:37:52 -0700 Subject: [PATCH 112/305] core/log: add filesystem logger --- src/core/CMakeLists.txt | 2 + src/core/filelogger.cpp | 60 ++++++++++++++++++++++++++ src/core/filelogger.hpp | 13 ++++++ src/core/filelogger_p.hpp | 24 +++++++++++ src/core/logging.cpp | 89 +++++++++++++++++--------------------- src/core/logging.hpp | 38 ++++++++++++++-- src/core/main.cpp | 8 +++- src/core/paths.cpp | 91 +++++++++++++++++++++++++++++++++++++++ src/core/paths.hpp | 27 ++++++++++++ 9 files changed, 299 insertions(+), 53 deletions(-) create mode 100644 src/core/filelogger.cpp create mode 100644 src/core/filelogger.hpp create mode 100644 src/core/filelogger_p.hpp create mode 100644 src/core/paths.cpp create mode 100644 src/core/paths.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index eedfca99..83013907 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -38,6 +38,8 @@ qt_add_library(quickshell-core STATIC qsmenuanchor.cpp clock.cpp logging.cpp + paths.cpp + filelogger.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/filelogger.cpp b/src/core/filelogger.cpp new file mode 100644 index 00000000..279d25a7 --- /dev/null +++ b/src/core/filelogger.cpp @@ -0,0 +1,60 @@ +#include "filelogger.hpp" + +#include +#include +#include +#include +#include +#include + +#include "filelogger_p.hpp" +#include "logging.hpp" +#include "paths.hpp" + +Q_LOGGING_CATEGORY(logLogger, "quickshell.logger", QtWarningMsg); + +void FileLoggerThread::init() { + auto* thread = new FileLoggerThread(); + auto* logger = new FileLogger(); + logger->moveToThread(thread); + thread->start(); + QMetaObject::invokeMethod(logger, "init", Qt::BlockingQueuedConnection); +} + +void FileLogger::init() { + qCDebug(logLogger) << "Initializing filesystem logger..."; + auto* runDir = QsPaths::instance()->instanceRunDir(); + + if (!runDir) { + qCCritical(logLogger + ) << "Could not start filesystem logger as the runtime directory could not be created."; + return; + } + + auto path = runDir->filePath("log.log"); + auto* file = new QFile(path); + + if (!file->open(QFile::WriteOnly | QFile::Truncate)) { + qCCritical(logLogger + ) << "Could not start filesystem logger as the log file could not be created:" + << path; + return; + } + + this->fileStream.setDevice(file); + + QObject::connect( + LogManager::instance(), + &LogManager::logMessage, + this, + &FileLogger::onMessage, + Qt::QueuedConnection + ); + + qDebug(logLogger) << "Initialized filesystem logger"; +} + +void FileLogger::onMessage(const LogMessage& msg) { + LogManager::formatMessage(this->fileStream, msg, false); + this->fileStream << Qt::endl; +} diff --git a/src/core/filelogger.hpp b/src/core/filelogger.hpp new file mode 100644 index 00000000..dba7aaa2 --- /dev/null +++ b/src/core/filelogger.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include +#include +class FileLoggerThread: public QThread { + Q_OBJECT; + +public: + static void init(); + +private: + explicit FileLoggerThread() = default; +}; diff --git a/src/core/filelogger_p.hpp b/src/core/filelogger_p.hpp new file mode 100644 index 00000000..e6d7b633 --- /dev/null +++ b/src/core/filelogger_p.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include +#include + +#include "logging.hpp" + +class FileLogger: public QObject { + Q_OBJECT; + +public: + explicit FileLogger() = default; + +public slots: + void init(); + +private slots: + void onMessage(const LogMessage& msg); + +private: + QTextStream fileStream; +}; diff --git a/src/core/logging.cpp b/src/core/logging.cpp index d216a980..982ee3f0 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -5,70 +5,61 @@ #include #include #include +#include +#include -namespace { +LogManager::LogManager(): colorLogs(qEnvironmentVariableIsEmpty("NO_COLOR")), stdoutStream(stdout) { + qInstallMessageHandler(&LogManager::messageHandler); +} -bool COLOR_LOGS = false; // NOLINT - -void formatMessage( +void LogManager::messageHandler( QtMsgType type, const QMessageLogContext& context, - const QString& msg, - bool color + const QString& msg ) { - const auto* typeString = "[log error]"; + auto message = LogMessage(type, context.category, msg.toUtf8()); + auto* self = LogManager::instance(); + + LogManager::formatMessage(self->stdoutStream, message, self->colorLogs); + self->stdoutStream << Qt::endl; + + emit self->logMessage(message); +} + +LogManager* LogManager::instance() { + static auto* instance = new LogManager(); // NOLINT + return instance; +} + +void LogManager::formatMessage(QTextStream& stream, const LogMessage& msg, bool color) { if (color) { - switch (type) { - case QtDebugMsg: typeString = "\033[34m DEBUG"; break; - case QtInfoMsg: typeString = "\033[32m INFO"; break; - case QtWarningMsg: typeString = "\033[33m WARN"; break; - case QtCriticalMsg: typeString = "\033[31m ERROR"; break; - case QtFatalMsg: typeString = "\033[31m FATAL"; break; + switch (msg.type) { + case QtDebugMsg: stream << "\033[34m DEBUG"; break; + case QtInfoMsg: stream << "\033[32m INFO"; break; + case QtWarningMsg: stream << "\033[33m WARN"; break; + case QtCriticalMsg: stream << "\033[31m ERROR"; break; + case QtFatalMsg: stream << "\033[31m FATAL"; break; } } else { - switch (type) { - case QtDebugMsg: typeString = " DEBUG"; break; - case QtInfoMsg: typeString = " INFO"; break; - case QtWarningMsg: typeString = " WARN"; break; - case QtCriticalMsg: typeString = " ERROR"; break; - case QtFatalMsg: typeString = " FATAL"; break; + switch (msg.type) { + case QtDebugMsg: stream << " DEBUG"; break; + case QtInfoMsg: stream << " INFO"; break; + case QtWarningMsg: stream << " WARN"; break; + case QtCriticalMsg: stream << " ERROR"; break; + case QtFatalMsg: stream << " FATAL"; break; } } - const auto isDefault = strcmp(context.category, "default") == 0; + const auto isDefault = strcmp(msg.category, "default") == 0; - const char* format = nullptr; + if (color && !isDefault && msg.type != QtFatalMsg) stream << "\033[97m"; - if (color) { - if (type == QtFatalMsg) { - if (isDefault) format = "%s: %s\033[0m\n"; - else format = "%s %s: %s\033[0m\n"; - } else { - if (isDefault) format = "%s\033[0m: %s\n"; - else format = "%s \033[97m%s\033[0m: %s\n"; - } - } else { - if (isDefault) format = "%s: %s\n"; - else format = "%s %s: %s\n"; + if (!isDefault) { + stream << ' ' << msg.category; } - if (isDefault) { - printf(format, typeString, msg.toStdString().c_str()); - } else { - printf(format, typeString, context.category, msg.toStdString().c_str()); - } + if (color && msg.type != QtFatalMsg) stream << "\033[0m"; - fflush(stdout); -} - -void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg) { - formatMessage(type, context, msg, COLOR_LOGS); -} - -} // namespace - -void LogManager::setup() { - COLOR_LOGS = qEnvironmentVariableIsEmpty("NO_COLOR"); - qInstallMessageHandler(&messageHandler); + stream << ": " << msg.body; } diff --git a/src/core/logging.hpp b/src/core/logging.hpp index c2d8d13f..03a880a1 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -1,6 +1,38 @@ #pragma once -class LogManager { -public: - static void setup(); +#include + +#include +#include +#include +#include + +struct LogMessage { + explicit LogMessage(QtMsgType type, const char* category, QByteArray body) + : type(type) + , category(category) + , body(std::move(body)) {} + + QtMsgType type; + const char* category; + QByteArray body; +}; + +class LogManager: public QObject { + Q_OBJECT; + +public: + static LogManager* instance(); + + static void formatMessage(QTextStream& stream, const LogMessage& msg, bool color); + +signals: + void logMessage(LogMessage msg); + +private: + explicit LogManager(); + static void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg); + + bool colorLogs; + QTextStream stdoutStream; }; diff --git a/src/core/main.cpp b/src/core/main.cpp index 12f3eb38..e2bbdcbe 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -22,12 +22,14 @@ #include #include +#include "filelogger.hpp" #include "logging.hpp" +#include "paths.hpp" #include "plugin.hpp" #include "rootwrapper.hpp" int qs_main(int argc, char** argv) { - LogManager::setup(); + LogManager::instance(); QString configFilePath; QString workingDirectory; @@ -340,6 +342,8 @@ int qs_main(int argc, char** argv) { qputenv(var.toUtf8(), val.toUtf8()); } + QsPaths::init(shellId); + // While the simple animation driver can lead to better animations in some cases, // it also can cause excessive repainting at excessively high framerates which can // lead to noticeable amounts of gpu usage, including overheating on some systems. @@ -386,6 +390,8 @@ int qs_main(int argc, char** argv) { app = new QGuiApplication(argc, argv); } + FileLoggerThread::init(); + if (debugPort != -1) { QQmlDebuggingEnabler::enableDebugging(true); auto wait = waitForDebug ? QQmlDebuggingEnabler::WaitForClient diff --git a/src/core/paths.cpp b/src/core/paths.cpp new file mode 100644 index 00000000..b204f94f --- /dev/null +++ b/src/core/paths.cpp @@ -0,0 +1,91 @@ +#include "paths.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg); + +QsPaths* QsPaths::instance() { + static auto* instance = new QsPaths(); // NOLINT + return instance; +} + +void QsPaths::init(QString shellId) { QsPaths::instance()->shellId = std::move(shellId); } + +QDir* QsPaths::cacheDir() { + if (this->cacheState == DirState::Unknown) { + auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + dir = QDir(dir.filePath("quickshell")); + dir = QDir(dir.filePath(this->shellId)); + this->mCacheDir = dir; + + qCDebug(logPaths) << "Initialized cache path:" << dir.path(); + + if (!dir.mkpath(".")) { + qCCritical(logPaths) << "Cannot create cache directory at" << dir.path(); + + this->cacheState = DirState::Failed; + } + } + + if (this->cacheState == DirState::Failed) return nullptr; + else return &this->mCacheDir; +} + +QDir* QsPaths::runDir() { + if (this->runState == DirState::Unknown) { + auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR"); + if (runtimeDir.isEmpty()) { + runtimeDir = QString("/run/user/$1").arg(getuid()); + qCInfo(logPaths) << "XDG_RUNTIME_DIR was not set, defaulting to" << runtimeDir; + } + + auto dir = QDir(runtimeDir); + dir = QDir(dir.filePath("quickshell")); + dir = QDir(dir.filePath(this->shellId)); + this->mRunDir = dir; + + qCDebug(logPaths) << "Initialized runtime path:" << dir.path(); + + if (!dir.mkpath(".")) { + qCCritical(logPaths) << "Cannot create runtime directory at" << dir.path(); + + this->runState = DirState::Failed; + } + } + + if (this->runState == DirState::Failed) return nullptr; + else return &this->mRunDir; +} + +QDir* QsPaths::instanceRunDir() { + if (this->instanceRunState == DirState::Unknown) { + auto* runtimeDir = this->runDir(); + + if (!runtimeDir) { + qCCritical(logPaths) << "Cannot create instance runtime directory as main runtim directory " + "could not be created."; + this->instanceRunState = DirState::Failed; + } else { + this->mInstanceRunDir = + runtimeDir->filePath(QString("run-%1").arg(QDateTime::currentMSecsSinceEpoch())); + + qCDebug(logPaths) << "Initialized instance runtime path:" << this->mInstanceRunDir.path(); + + if (!this->mInstanceRunDir.mkpath(".")) { + qCCritical(logPaths) << "Cannot create instance runtime directory at" + << this->mInstanceRunDir.path(); + this->instanceRunState = DirState::Failed; + } + } + } + + if (this->runState == DirState::Failed) return nullptr; + else return &this->mInstanceRunDir; +} diff --git a/src/core/paths.hpp b/src/core/paths.hpp new file mode 100644 index 00000000..b2a1c193 --- /dev/null +++ b/src/core/paths.hpp @@ -0,0 +1,27 @@ +#pragma once +#include + +class QsPaths { +public: + static QsPaths* instance(); + static void init(QString shellId); + + QDir* cacheDir(); + QDir* runDir(); + QDir* instanceRunDir(); + +private: + enum class DirState { + Unknown = 0, + Ready = 1, + Failed = 2, + }; + + QString shellId; + QDir mCacheDir; + QDir mRunDir; + QDir mInstanceRunDir; + DirState cacheState = DirState::Unknown; + DirState runState = DirState::Unknown; + DirState instanceRunState = DirState::Unknown; +}; From 38ba3fff242c41a7f648d1fede4cf9a36b2b7e61 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 6 Aug 2024 22:24:31 -0700 Subject: [PATCH 113/305] core/popupanchor: pick flip direction based on available width --- src/core/popupanchor.cpp | 119 ++++++++++++++++++++++++++------------- 1 file changed, 79 insertions(+), 40 deletions(-) diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index 2534c114..1f4c5a76 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -1,5 +1,6 @@ #include "popupanchor.hpp" +#include #include #include #include @@ -168,20 +169,56 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only : anchorEdges.testFlag(Edges::Bottom) ? anchorRectGeometry.bottom() : anchorRectGeometry.center().y(); - auto calcEffectiveX = [&]() { - return anchorGravity.testFlag(Edges::Left) ? anchorX - windowGeometry.width() + 1 - : anchorGravity.testFlag(Edges::Right) ? anchorX - : anchorX - windowGeometry.width() / 2; + auto calcEffectiveX = [&](Edges::Flags anchorGravity, int anchorX) { + auto ex = anchorGravity.testFlag(Edges::Left) ? anchorX - windowGeometry.width() + : anchorGravity.testFlag(Edges::Right) ? anchorX - 1 + : anchorX - windowGeometry.width() / 2; + + return ex + 1; }; - auto calcEffectiveY = [&]() { - return anchorGravity.testFlag(Edges::Top) ? anchorY - windowGeometry.height() + 1 - : anchorGravity.testFlag(Edges::Bottom) ? anchorY - : anchorY - windowGeometry.height() / 2; + auto calcEffectiveY = [&](Edges::Flags anchorGravity, int anchorY) { + auto ey = anchorGravity.testFlag(Edges::Top) ? anchorY - windowGeometry.height() + : anchorGravity.testFlag(Edges::Bottom) ? anchorY - 1 + : anchorY - windowGeometry.height() / 2; + + return ey + 1; }; - auto effectiveX = calcEffectiveX(); - auto effectiveY = calcEffectiveY(); + auto calcRemainingWidth = [&](int effectiveX) { + auto width = windowGeometry.width(); + if (effectiveX < screenGeometry.left()) { + auto diff = screenGeometry.left() - effectiveX; + effectiveX = screenGeometry.left(); + width -= diff; + } + + auto effectiveX2 = effectiveX + width; + if (effectiveX2 > screenGeometry.right()) { + width -= effectiveX2 - screenGeometry.right() - 1; + } + + return QPair(effectiveX, width); + }; + + auto calcRemainingHeight = [&](int effectiveY) { + auto height = windowGeometry.height(); + if (effectiveY < screenGeometry.left()) { + auto diff = screenGeometry.top() - effectiveY; + effectiveY = screenGeometry.top(); + height -= diff; + } + + auto effectiveY2 = effectiveY + height; + if (effectiveY2 > screenGeometry.bottom()) { + height -= effectiveY2 - screenGeometry.bottom() - 1; + } + + return QPair(effectiveY, height); + }; + + auto effectiveX = calcEffectiveX(anchorGravity, anchorX); + auto effectiveY = calcEffectiveY(anchorGravity, anchorY); if (adjustment.testFlag(PopupAdjustment::FlipX)) { const bool flip = (anchorGravity.testFlag(Edges::Left) && effectiveX < screenGeometry.left()) @@ -189,13 +226,22 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only && effectiveX + windowGeometry.width() > screenGeometry.right()); if (flip) { - anchorGravity ^= Edges::Left | Edges::Right; + auto newAnchorGravity = anchorGravity ^ (Edges::Left | Edges::Right); - anchorX = anchorEdges.testFlags(Edges::Left) ? anchorRectGeometry.right() - : anchorEdges.testFlags(Edges::Right) ? anchorRectGeometry.left() - : anchorX; + auto newAnchorX = anchorEdges.testFlags(Edges::Left) ? anchorRectGeometry.right() + : anchorEdges.testFlags(Edges::Right) ? anchorRectGeometry.left() + : anchorX; - effectiveX = calcEffectiveX(); + auto newEffectiveX = calcEffectiveX(newAnchorGravity, newAnchorX); + + // TODO IN HL: pick constraint monitor based on anchor rect position in window + + // if the available width when flipped is more than the available width without flipping then flip + if (calcRemainingWidth(newEffectiveX).second > calcRemainingWidth(effectiveX).second) { + anchorGravity = newAnchorGravity; + anchorX = newAnchorX; + effectiveX = newEffectiveX; + } } } @@ -205,13 +251,20 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only && effectiveY + windowGeometry.height() > screenGeometry.bottom()); if (flip) { - anchorGravity ^= Edges::Top | Edges::Bottom; + auto newAnchorGravity = anchorGravity ^ (Edges::Top | Edges::Bottom); - anchorY = anchorEdges.testFlags(Edges::Top) ? anchorRectGeometry.bottom() - : anchorEdges.testFlags(Edges::Bottom) ? anchorRectGeometry.top() - : anchorY; + auto newAnchorY = anchorEdges.testFlags(Edges::Top) ? anchorRectGeometry.bottom() + : anchorEdges.testFlags(Edges::Bottom) ? anchorRectGeometry.top() + : anchorY; - effectiveY = calcEffectiveY(); + auto newEffectiveY = calcEffectiveY(newAnchorGravity, newAnchorY); + + // if the available width when flipped is more than the available width without flipping then flip + if (calcRemainingHeight(newEffectiveY).second > calcRemainingHeight(effectiveY).second) { + anchorGravity = newAnchorGravity; + anchorY = newAnchorY; + effectiveY = newEffectiveY; + } } } @@ -237,29 +290,15 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only } if (adjustment.testFlag(PopupAdjustment::ResizeX)) { - if (effectiveX < screenGeometry.left()) { - auto diff = screenGeometry.left() - effectiveX; - effectiveX = screenGeometry.left(); - width -= diff; - } - - auto effectiveX2 = effectiveX + windowGeometry.width(); - if (effectiveX2 > screenGeometry.right()) { - width -= effectiveX2 - screenGeometry.right() - 1; - } + auto [newX, newWidth] = calcRemainingWidth(effectiveX); + effectiveX = newX; + width = newWidth; } if (adjustment.testFlag(PopupAdjustment::ResizeY)) { - if (effectiveY < screenGeometry.top()) { - auto diff = screenGeometry.top() - effectiveY; - effectiveY = screenGeometry.top(); - height -= diff; - } - - auto effectiveY2 = effectiveY + windowGeometry.height(); - if (effectiveY2 > screenGeometry.bottom()) { - height -= effectiveY2 - screenGeometry.bottom() - 1; - } + auto [newY, newHeight] = calcRemainingHeight(effectiveY); + effectiveY = newY; + height = newHeight; } window->setGeometry({effectiveX, effectiveY, width, height}); From 7c7326ec528a688ba7e4e9d927802443dd0e4e04 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 7 Aug 2024 13:40:37 -0700 Subject: [PATCH 114/305] core/log: add timestamps to log files --- src/core/filelogger.cpp | 2 +- src/core/logging.cpp | 16 ++++++++++++++-- src/core/logging.hpp | 12 ++++++++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/core/filelogger.cpp b/src/core/filelogger.cpp index 279d25a7..7dcada9e 100644 --- a/src/core/filelogger.cpp +++ b/src/core/filelogger.cpp @@ -55,6 +55,6 @@ void FileLogger::init() { } void FileLogger::onMessage(const LogMessage& msg) { - LogManager::formatMessage(this->fileStream, msg, false); + LogManager::formatMessage(this->fileStream, msg, false, true); this->fileStream << Qt::endl; } diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 982ee3f0..8b567e70 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -21,7 +21,7 @@ void LogManager::messageHandler( auto* self = LogManager::instance(); - LogManager::formatMessage(self->stdoutStream, message, self->colorLogs); + LogManager::formatMessage(self->stdoutStream, message, self->colorLogs, false); self->stdoutStream << Qt::endl; emit self->logMessage(message); @@ -32,7 +32,17 @@ LogManager* LogManager::instance() { return instance; } -void LogManager::formatMessage(QTextStream& stream, const LogMessage& msg, bool color) { +void LogManager::formatMessage( + QTextStream& stream, + const LogMessage& msg, + bool color, + bool timestamp +) { + if (timestamp) { + if (color) stream << "\033[90m"; + stream << msg.time.toString("yyyy-MM-dd hh:mm:ss.zzz"); + } + if (color) { switch (msg.type) { case QtDebugMsg: stream << "\033[34m DEBUG"; break; @@ -62,4 +72,6 @@ void LogManager::formatMessage(QTextStream& stream, const LogMessage& msg, bool if (color && msg.type != QtFatalMsg) stream << "\033[0m"; stream << ": " << msg.body; + + if (color && msg.type == QtFatalMsg) stream << "\033[0m"; } diff --git a/src/core/logging.hpp b/src/core/logging.hpp index 03a880a1..ae9e596e 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -2,18 +2,26 @@ #include +#include #include #include #include #include struct LogMessage { - explicit LogMessage(QtMsgType type, const char* category, QByteArray body) + explicit LogMessage( + QtMsgType type, + const char* category, + QByteArray body, + QDateTime time = QDateTime::currentDateTime() + ) : type(type) + , time(std::move(time)) , category(category) , body(std::move(body)) {} QtMsgType type; + QDateTime time; const char* category; QByteArray body; }; @@ -24,7 +32,7 @@ class LogManager: public QObject { public: static LogManager* instance(); - static void formatMessage(QTextStream& stream, const LogMessage& msg, bool color); + static void formatMessage(QTextStream& stream, const LogMessage& msg, bool color, bool timestamp); signals: void logMessage(LogMessage msg); From 8364e94d266bbde6e1a13f901e60e62fdbfefed2 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 7 Aug 2024 15:53:11 -0700 Subject: [PATCH 115/305] core/log: capture early logs in fs logger --- src/core/CMakeLists.txt | 1 - src/core/filelogger.cpp | 60 ------------------- src/core/filelogger.hpp | 13 ---- src/core/filelogger_p.hpp | 24 -------- src/core/logging.cpp | 123 +++++++++++++++++++++++++++++++++++++- src/core/logging.hpp | 36 +++++++++++ src/core/main.cpp | 7 ++- src/core/paths.cpp | 1 - 8 files changed, 160 insertions(+), 105 deletions(-) delete mode 100644 src/core/filelogger.cpp delete mode 100644 src/core/filelogger.hpp delete mode 100644 src/core/filelogger_p.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 83013907..5ced5410 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -39,7 +39,6 @@ qt_add_library(quickshell-core STATIC clock.cpp logging.cpp paths.cpp - filelogger.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/filelogger.cpp b/src/core/filelogger.cpp deleted file mode 100644 index 7dcada9e..00000000 --- a/src/core/filelogger.cpp +++ /dev/null @@ -1,60 +0,0 @@ -#include "filelogger.hpp" - -#include -#include -#include -#include -#include -#include - -#include "filelogger_p.hpp" -#include "logging.hpp" -#include "paths.hpp" - -Q_LOGGING_CATEGORY(logLogger, "quickshell.logger", QtWarningMsg); - -void FileLoggerThread::init() { - auto* thread = new FileLoggerThread(); - auto* logger = new FileLogger(); - logger->moveToThread(thread); - thread->start(); - QMetaObject::invokeMethod(logger, "init", Qt::BlockingQueuedConnection); -} - -void FileLogger::init() { - qCDebug(logLogger) << "Initializing filesystem logger..."; - auto* runDir = QsPaths::instance()->instanceRunDir(); - - if (!runDir) { - qCCritical(logLogger - ) << "Could not start filesystem logger as the runtime directory could not be created."; - return; - } - - auto path = runDir->filePath("log.log"); - auto* file = new QFile(path); - - if (!file->open(QFile::WriteOnly | QFile::Truncate)) { - qCCritical(logLogger - ) << "Could not start filesystem logger as the log file could not be created:" - << path; - return; - } - - this->fileStream.setDevice(file); - - QObject::connect( - LogManager::instance(), - &LogManager::logMessage, - this, - &FileLogger::onMessage, - Qt::QueuedConnection - ); - - qDebug(logLogger) << "Initialized filesystem logger"; -} - -void FileLogger::onMessage(const LogMessage& msg) { - LogManager::formatMessage(this->fileStream, msg, false, true); - this->fileStream << Qt::endl; -} diff --git a/src/core/filelogger.hpp b/src/core/filelogger.hpp deleted file mode 100644 index dba7aaa2..00000000 --- a/src/core/filelogger.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include -#include -class FileLoggerThread: public QThread { - Q_OBJECT; - -public: - static void init(); - -private: - explicit FileLoggerThread() = default; -}; diff --git a/src/core/filelogger_p.hpp b/src/core/filelogger_p.hpp deleted file mode 100644 index e6d7b633..00000000 --- a/src/core/filelogger_p.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "logging.hpp" - -class FileLogger: public QObject { - Q_OBJECT; - -public: - explicit FileLogger() = default; - -public slots: - void init(); - -private slots: - void onMessage(const LogMessage& msg); - -private: - QTextStream fileStream; -}; diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 8b567e70..642d3fc6 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -3,14 +3,25 @@ #include #include +#include +#include +#include +#include #include #include #include +#include #include +#include +#include -LogManager::LogManager(): colorLogs(qEnvironmentVariableIsEmpty("NO_COLOR")), stdoutStream(stdout) { - qInstallMessageHandler(&LogManager::messageHandler); -} +#include "paths.hpp" + +Q_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg); + +LogManager::LogManager() + : colorLogs(qEnvironmentVariableIsEmpty("NO_COLOR")) + , stdoutStream(stdout) {} void LogManager::messageHandler( QtMsgType type, @@ -32,6 +43,27 @@ LogManager* LogManager::instance() { return instance; } +void LogManager::init() { + auto* instance = LogManager::instance(); + + qInstallMessageHandler(&LogManager::messageHandler); + + qCDebug(logLogging) << "Creating offthread logger..."; + auto* thread = new QThread(); + instance->threadProxy.moveToThread(thread); + thread->start(); + QMetaObject::invokeMethod(&instance->threadProxy, "initInThread", Qt::BlockingQueuedConnection); + qCDebug(logLogging) << "Logger initialized."; +} + +void LogManager::initFs() { + QMetaObject::invokeMethod( + &LogManager::instance()->threadProxy, + "initFs", + Qt::BlockingQueuedConnection + ); +} + void LogManager::formatMessage( QTextStream& stream, const LogMessage& msg, @@ -75,3 +107,88 @@ void LogManager::formatMessage( if (color && msg.type == QtFatalMsg) stream << "\033[0m"; } + +void LoggingThreadProxy::initInThread() { + this->logging = new ThreadLogging(this); + this->logging->init(); +} + +void LoggingThreadProxy::initFs() { this->logging->initFs(); } + +void ThreadLogging::init() { + auto mfd = memfd_create("quickshell:logs", 0); + + if (mfd == -1) { + qCCritical(logLogging) << "Failed to create memfd for initial log storage" + << qt_error_string(-1); + return; + } + + this->file = new QFile(); + this->file->open(mfd, QFile::WriteOnly, QFile::AutoCloseHandle); + this->fileStream.setDevice(this->file); + + // This connection is direct so it works while the event loop is destroyed between + // QCoreApplication delete and Q(Gui)Application launch. + QObject::connect( + LogManager::instance(), + &LogManager::logMessage, + this, + &ThreadLogging::onMessage, + Qt::DirectConnection + ); + + qCDebug(logLogging) << "Created memfd" << mfd << "for early logs."; +} + +void ThreadLogging::initFs() { + + qCDebug(logLogging) << "Starting filesystem logging..."; + auto* runDir = QsPaths::instance()->instanceRunDir(); + + if (!runDir) { + qCCritical(logLogging + ) << "Could not start filesystem logging as the runtime directory could not be created."; + return; + } + + auto path = runDir->filePath("log.log"); + auto* file = new QFile(path); + + if (!file->open(QFile::WriteOnly | QFile::Truncate)) { + qCCritical(logLogging + ) << "Could not start filesystem logger as the log file could not be created:" + << path; + return; + } + + qCDebug(logLogging) << "Copying memfd logs to log file..."; + + auto* oldFile = this->file; + oldFile->seek(0); + sendfile(file->handle(), oldFile->handle(), nullptr, oldFile->size()); + this->file = file; + this->fileStream.setDevice(file); + delete oldFile; + + qCDebug(logLogging) << "Switched logging to disk logs."; + + auto* logManager = LogManager::instance(); + QObject::disconnect(logManager, &LogManager::logMessage, this, &ThreadLogging::onMessage); + + QObject::connect( + logManager, + &LogManager::logMessage, + this, + &ThreadLogging::onMessage, + Qt::QueuedConnection + ); + + qCDebug(logLogging) << "Switched threaded logger to queued eventloop connection."; +} + +void ThreadLogging::onMessage(const LogMessage& msg) { + if (this->fileStream.device() == nullptr) return; + LogManager::formatMessage(this->fileStream, msg, false, true); + this->fileStream << Qt::endl; +} diff --git a/src/core/logging.hpp b/src/core/logging.hpp index ae9e596e..f831fcfa 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -26,10 +27,44 @@ struct LogMessage { QByteArray body; }; +class ThreadLogging: public QObject { + Q_OBJECT; + +public: + explicit ThreadLogging(QObject* parent): QObject(parent) {} + + void init(); + void initFs(); + void setupFileLogging(); + +private slots: + void onMessage(const LogMessage& msg); + +private: + QFile* file = nullptr; + QTextStream fileStream; +}; + +class LoggingThreadProxy: public QObject { + Q_OBJECT; + +public: + explicit LoggingThreadProxy() = default; + +public slots: + void initInThread(); + void initFs(); + +private: + ThreadLogging* logging = nullptr; +}; + class LogManager: public QObject { Q_OBJECT; public: + static void init(); + static void initFs(); static LogManager* instance(); static void formatMessage(QTextStream& stream, const LogMessage& msg, bool color, bool timestamp); @@ -43,4 +78,5 @@ private: bool colorLogs; QTextStream stdoutStream; + LoggingThreadProxy threadProxy; }; diff --git a/src/core/main.cpp b/src/core/main.cpp index e2bbdcbe..cc71f9fb 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -22,14 +22,12 @@ #include #include -#include "filelogger.hpp" #include "logging.hpp" #include "paths.hpp" #include "plugin.hpp" #include "rootwrapper.hpp" int qs_main(int argc, char** argv) { - LogManager::instance(); QString configFilePath; QString workingDirectory; @@ -48,6 +46,9 @@ int qs_main(int argc, char** argv) { QCoreApplication::setApplicationName("quickshell"); QCoreApplication::setApplicationVersion("0.1.0 (" GIT_REVISION ")"); + // Start log manager - has to happen with an active event loop or offthread can't be started. + LogManager::init(); + QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); @@ -390,7 +391,7 @@ int qs_main(int argc, char** argv) { app = new QGuiApplication(argc, argv); } - FileLoggerThread::init(); + LogManager::initFs(); if (debugPort != -1) { QQmlDebuggingEnabler::enableDebugging(true); diff --git a/src/core/paths.cpp b/src/core/paths.cpp index b204f94f..7e05530d 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -21,7 +21,6 @@ void QsPaths::init(QString shellId) { QsPaths::instance()->shellId = std::move(s QDir* QsPaths::cacheDir() { if (this->cacheState == DirState::Unknown) { auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); - dir = QDir(dir.filePath("quickshell")); dir = QDir(dir.filePath(this->shellId)); this->mCacheDir = dir; From bdbf5b9af998000773d49bfe00c3b52aa259229a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 9 Aug 2024 14:43:18 -0700 Subject: [PATCH 116/305] core/log: add custom log encoder for smaller log storage Will be used to store more detailed logs in the future without using as much disk space. --- CMakeLists.txt | 2 +- src/core/logging.cpp | 559 +++++++++++++++++++++++++++++++---- src/core/logging.hpp | 46 +-- src/core/logging_p.hpp | 123 ++++++++ src/core/main.cpp | 15 + src/core/ringbuf.hpp | 169 +++++++++++ src/core/test/CMakeLists.txt | 1 + src/core/test/ringbuf.cpp | 125 ++++++++ src/core/test/ringbuf.hpp | 27 ++ 9 files changed, 980 insertions(+), 87 deletions(-) create mode 100644 src/core/logging_p.hpp create mode 100644 src/core/ringbuf.hpp create mode 100644 src/core/test/ringbuf.cpp create mode 100644 src/core/test/ringbuf.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c3b37603..b55c751f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) option(BUILD_TESTING "Build tests" OFF) -option(ASAN "Enable ASAN" OFF) +option(ASAN "Enable ASAN" OFF) # note: better output with gcc than clang option(FRAME_POINTERS "Always keep frame pointers" ${ASAN}) option(USE_JEMALLOC "Use jemalloc over the system malloc implementation" ON) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 642d3fc6..9271c07f 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -1,24 +1,86 @@ #include "logging.hpp" +#include #include -#include +#include +#include +#include +#include #include #include #include #include #include #include +#include #include #include #include #include +#include #include #include +#include "logging_p.hpp" #include "paths.hpp" +namespace qs::log { + Q_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg); +bool LogMessage::operator==(const LogMessage& other) const { + // note: not including time + return this->type == other.type && this->category == other.category && this->body == other.body; +} + +size_t qHash(const LogMessage& message) { + return qHash(message.type) ^ qHash(message.category) ^ qHash(message.body); +} + +void LogMessage::formatMessage( + QTextStream& stream, + const LogMessage& msg, + bool color, + bool timestamp +) { + if (timestamp) { + if (color) stream << "\033[90m"; + stream << msg.time.toString("yyyy-MM-dd hh:mm:ss.zzz"); + } + + if (color) { + switch (msg.type) { + case QtDebugMsg: stream << "\033[34m DEBUG"; break; + case QtInfoMsg: stream << "\033[32m INFO"; break; + case QtWarningMsg: stream << "\033[33m WARN"; break; + case QtCriticalMsg: stream << "\033[31m ERROR"; break; + case QtFatalMsg: stream << "\033[31m FATAL"; break; + } + } else { + switch (msg.type) { + case QtDebugMsg: stream << " DEBUG"; break; + case QtInfoMsg: stream << " INFO"; break; + case QtWarningMsg: stream << " WARN"; break; + case QtCriticalMsg: stream << " ERROR"; break; + case QtFatalMsg: stream << " FATAL"; break; + } + } + + const auto isDefault = msg.category == "default"; + + if (color && !isDefault && msg.type != QtFatalMsg) stream << "\033[97m"; + + if (!isDefault) { + stream << ' ' << msg.category; + } + + if (color && msg.type != QtFatalMsg) stream << "\033[0m"; + + stream << ": " << msg.body; + + if (color && msg.type == QtFatalMsg) stream << "\033[0m"; +} + LogManager::LogManager() : colorLogs(qEnvironmentVariableIsEmpty("NO_COLOR")) , stdoutStream(stdout) {} @@ -28,11 +90,11 @@ void LogManager::messageHandler( const QMessageLogContext& context, const QString& msg ) { - auto message = LogMessage(type, context.category, msg.toUtf8()); + auto message = LogMessage(type, QLatin1StringView(context.category), msg.toUtf8()); auto* self = LogManager::instance(); - LogManager::formatMessage(self->stdoutStream, message, self->colorLogs, false); + LogMessage::formatMessage(self->stdoutStream, message, self->colorLogs, false); self->stdoutStream << Qt::endl; emit self->logMessage(message); @@ -64,50 +126,6 @@ void LogManager::initFs() { ); } -void LogManager::formatMessage( - QTextStream& stream, - const LogMessage& msg, - bool color, - bool timestamp -) { - if (timestamp) { - if (color) stream << "\033[90m"; - stream << msg.time.toString("yyyy-MM-dd hh:mm:ss.zzz"); - } - - if (color) { - switch (msg.type) { - case QtDebugMsg: stream << "\033[34m DEBUG"; break; - case QtInfoMsg: stream << "\033[32m INFO"; break; - case QtWarningMsg: stream << "\033[33m WARN"; break; - case QtCriticalMsg: stream << "\033[31m ERROR"; break; - case QtFatalMsg: stream << "\033[31m FATAL"; break; - } - } else { - switch (msg.type) { - case QtDebugMsg: stream << " DEBUG"; break; - case QtInfoMsg: stream << " INFO"; break; - case QtWarningMsg: stream << " WARN"; break; - case QtCriticalMsg: stream << " ERROR"; break; - case QtFatalMsg: stream << " FATAL"; break; - } - } - - const auto isDefault = strcmp(msg.category, "default") == 0; - - if (color && !isDefault && msg.type != QtFatalMsg) stream << "\033[97m"; - - if (!isDefault) { - stream << ' ' << msg.category; - } - - if (color && msg.type != QtFatalMsg) stream << "\033[0m"; - - stream << ": " << msg.body; - - if (color && msg.type == QtFatalMsg) stream << "\033[0m"; -} - void LoggingThreadProxy::initInThread() { this->logging = new ThreadLogging(this); this->logging->init(); @@ -116,17 +134,39 @@ void LoggingThreadProxy::initInThread() { void LoggingThreadProxy::initFs() { this->logging->initFs(); } void ThreadLogging::init() { - auto mfd = memfd_create("quickshell:logs", 0); + auto logMfd = memfd_create("quickshell:logs", 0); - if (mfd == -1) { + if (logMfd == -1) { qCCritical(logLogging) << "Failed to create memfd for initial log storage" << qt_error_string(-1); - return; } - this->file = new QFile(); - this->file->open(mfd, QFile::WriteOnly, QFile::AutoCloseHandle); - this->fileStream.setDevice(this->file); + auto dlogMfd = memfd_create("quickshell:detailedlogs", 0); + + if (dlogMfd == -1) { + qCCritical(logLogging) << "Failed to create memfd for initial detailed log storage" + << qt_error_string(-1); + } + + if (logMfd != -1) { + this->file = new QFile(); + this->file->open(logMfd, QFile::WriteOnly, QFile::AutoCloseHandle); + this->fileStream.setDevice(this->file); + } + + if (dlogMfd != -1) { + this->detailedFile = new QFile(); + // buffered by WriteBuffer + this->detailedFile->open(dlogMfd, QFile::WriteOnly | 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; + } + } // This connection is direct so it works while the event loop is destroyed between // QCoreApplication delete and Q(Gui)Application launch. @@ -138,11 +178,11 @@ void ThreadLogging::init() { Qt::DirectConnection ); - qCDebug(logLogging) << "Created memfd" << mfd << "for early logs."; + qCDebug(logLogging) << "Created memfd" << logMfd << "for early logs."; + qCDebug(logLogging) << "Created memfd" << dlogMfd << "for early detailed logs."; } void ThreadLogging::initFs() { - qCDebug(logLogging) << "Starting filesystem logging..."; auto* runDir = QsPaths::instance()->instanceRunDir(); @@ -153,23 +193,62 @@ void ThreadLogging::initFs() { } auto path = runDir->filePath("log.log"); + auto detailedPath = runDir->filePath("log.qslog"); auto* file = new QFile(path); + auto* detailedFile = new QFile(detailedPath); if (!file->open(QFile::WriteOnly | QFile::Truncate)) { qCCritical(logLogging ) << "Could not start filesystem logger as the log file could not be created:" << path; - return; + delete file; + file = nullptr; + } + + // buffered by WriteBuffer + if (!detailedFile->open(QFile::WriteOnly | QFile::Truncate | QFile::Unbuffered)) { + qCCritical(logLogging + ) << "Could not start detailed filesystem logger as the log file could not be created:" + << detailedPath; + delete detailedFile; + detailedFile = nullptr; } qCDebug(logLogging) << "Copying memfd logs to log file..."; - auto* oldFile = this->file; - oldFile->seek(0); - sendfile(file->handle(), oldFile->handle(), nullptr, oldFile->size()); - this->file = file; - this->fileStream.setDevice(file); - delete oldFile; + if (file) { + auto* oldFile = this->file; + if (oldFile) { + oldFile->seek(0); + sendfile(file->handle(), oldFile->handle(), nullptr, oldFile->size()); + } + + this->file = file; + this->fileStream.setDevice(file); + delete oldFile; + } + + if (detailedFile) { + auto* oldFile = this->detailedFile; + if (oldFile) { + oldFile->seek(0); + sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size()); + } + + this->detailedFile = detailedFile; + this->detailedWriter.setDevice(detailedFile); + + if (!oldFile) { + if (!this->detailedWriter.writeHeader()) { + qCCritical(logLogging) << "Could not write header for detailed logs."; + this->detailedWriter.setDevice(nullptr); + delete this->detailedFile; + this->detailedFile = nullptr; + } + } + + delete oldFile; + } qCDebug(logLogging) << "Switched logging to disk logs."; @@ -189,6 +268,360 @@ void ThreadLogging::initFs() { void ThreadLogging::onMessage(const LogMessage& msg) { if (this->fileStream.device() == nullptr) return; - LogManager::formatMessage(this->fileStream, msg, false, true); + LogMessage::formatMessage(this->fileStream, msg, false, true); 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."; + } } + +CompressedLogType compressedTypeOf(QtMsgType type) { + switch (type) { + case QtDebugMsg: return CompressedLogType::Debug; + case QtInfoMsg: return CompressedLogType::Info; + case QtWarningMsg: return CompressedLogType::Warn; + case QtCriticalMsg: + case QtFatalMsg: return CompressedLogType::Critical; + } +} + +QtMsgType typeOfCompressed(CompressedLogType type) { + switch (type) { + case CompressedLogType::Debug: return QtDebugMsg; + case CompressedLogType::Info: return QtInfoMsg; + case CompressedLogType::Warn: return QtWarningMsg; + case CompressedLogType::Critical: return QtCriticalMsg; + } +} + +void WriteBuffer::setDevice(QIODevice* device) { this->device = device; } +bool WriteBuffer::hasDevice() const { return this->device; } + +bool WriteBuffer::flush() { + auto written = this->device->write(this->buffer); + auto success = written == this->buffer.length(); + this->buffer.clear(); + return success; +} + +void WriteBuffer::writeBytes(const char* data, qsizetype length) { + this->buffer.append(data, length); +} + +void WriteBuffer::writeU8(quint8 data) { + this->writeBytes(reinterpret_cast(&data), 1); // NOLINT +} + +void WriteBuffer::writeU16(quint16 data) { + data = qToLittleEndian(data); + this->writeBytes(reinterpret_cast(&data), 2); // NOLINT +} + +void WriteBuffer::writeU32(quint32 data) { + data = qToLittleEndian(data); + this->writeBytes(reinterpret_cast(&data), 4); // NOLINT +} + +void WriteBuffer::writeU64(quint64 data) { + data = qToLittleEndian(data); + this->writeBytes(reinterpret_cast(&data), 8); // NOLINT +} + +void DeviceReader::setDevice(QIODevice* device) { this->device = device; } +bool DeviceReader::hasDevice() const { return this->device; } + +bool DeviceReader::readBytes(char* data, qsizetype length) { + return this->device->read(data, length) == length; +} + +qsizetype DeviceReader::peekBytes(char* data, qsizetype length) { + return this->device->peek(data, length); +} + +bool DeviceReader::skip(qsizetype length) { return this->device->skip(length) == length; } + +bool DeviceReader::readU8(quint8* data) { + return this->readBytes(reinterpret_cast(data), 1); // NOLINT +} + +bool DeviceReader::readU16(quint16* data) { + return this->readBytes(reinterpret_cast(data), 2); // NOLINT +} + +bool DeviceReader::readU32(quint32* data) { + return this->readBytes(reinterpret_cast(data), 4); // NOLINT +} + +bool DeviceReader::readU64(quint64* data) { + return this->readBytes(reinterpret_cast(data), 8); // NOLINT +} + +void EncodedLogWriter::setDevice(QIODevice* target) { this->buffer.setDevice(target); } +void EncodedLogReader::setDevice(QIODevice* source) { this->reader.setDevice(source); } + +constexpr quint8 LOG_VERSION = 1; + +bool EncodedLogWriter::writeHeader() { + this->buffer.writeU8(LOG_VERSION); + return this->buffer.flush(); +} + +bool EncodedLogReader::readHeader(bool* success, quint8* version, quint8* readerVersion) { + if (!this->reader.readU8(version)) return false; + *success = *version == LOG_VERSION; + *readerVersion = LOG_VERSION; + return true; +} + +bool EncodedLogWriter::write(const LogMessage& message) { + if (!this->buffer.hasDevice()) return false; + + LogMessage* prevMessage = nullptr; + auto index = this->recentMessages.indexOf(message, &prevMessage); + + // If its a dupe, save memory by reusing the buffer of the first message and letting + // the new one be deallocated. + auto body = prevMessage ? prevMessage->body : message.body; + this->recentMessages.emplace(message.type, message.category, body, message.time); + + if (index != -1) { + auto secondDelta = this->lastMessageTime.secsTo(message.time); + + if (secondDelta < 16 && index < 16) { + this->writeOp(EncodedLogOpcode::RecentMessageShort); + this->buffer.writeU8(index | (secondDelta << 4)); + } else { + this->writeOp(EncodedLogOpcode::RecentMessageLong); + this->buffer.writeU8(index); + this->writeVarInt(secondDelta); + } + + goto finish; + } else { + auto categoryId = this->getOrCreateCategory(message.category); + this->writeVarInt(categoryId); + + auto writeFullTimestamp = [this, &message]() { + this->buffer.writeU64(message.time.toSecsSinceEpoch()); + }; + + if (message.type == QtFatalMsg) { + this->buffer.writeU8(0xff); + writeFullTimestamp(); + } else { + quint8 field = compressedTypeOf(message.type); + + auto secondDelta = this->lastMessageTime.secsTo(message.time); + if (secondDelta > 29) { + // 0x1d = followed by delta int + // 0x1e = followed by epoch delta int + field |= (secondDelta < 0xffff ? 0x1d : 0x1e) << 3; + } else { + field |= secondDelta << 3; + } + + this->buffer.writeU8(field); + + if (secondDelta > 29) { + if (secondDelta > 0xffff) { + writeFullTimestamp(); + } else { + this->writeVarInt(secondDelta); + } + } + } + + this->writeString(message.body); + } + +finish: + // copy with second precision + this->lastMessageTime = QDateTime::fromSecsSinceEpoch(message.time.toSecsSinceEpoch()); + return this->buffer.flush(); +} + +bool EncodedLogReader::read(LogMessage* slot) { +start: + quint32 next = 0; + if (!this->readVarInt(&next)) return false; + + if (next < EncodedLogOpcode::BeginCategories) { + if (next == EncodedLogOpcode::RegisterCategory) { + if (!this->registerCategory()) return false; + goto start; + } else if (next == EncodedLogOpcode::RecentMessageShort || next == EncodedLogOpcode::RecentMessageLong) + { + quint8 index = 0; + quint32 secondDelta = 0; + + if (next == EncodedLogOpcode::RecentMessageShort) { + quint8 field = 0; + if (!this->reader.readU8(&field)) return false; + index = field & 0xf; + secondDelta = field >> 4; + } else { + if (!this->reader.readU8(&index)) return false; + if (!this->readVarInt(&secondDelta)) return false; + } + + *slot = this->recentMessages.at(index); + this->lastMessageTime = this->lastMessageTime.addSecs(static_cast(secondDelta)); + slot->time = this->lastMessageTime; + } + } else { + auto category = this->categories.value(next - EncodedLogOpcode::BeginCategories); + + quint8 field = 0; + if (!this->reader.readU8(&field)) return false; + + auto msgType = QtDebugMsg; + quint64 secondDelta = 0; + auto needsTimeRead = false; + + if (field == 0xff) { + msgType = QtFatalMsg; + needsTimeRead = true; + } else { + msgType = typeOfCompressed(static_cast(field & 0x07)); + secondDelta = field >> 3; + + if (secondDelta == 0x1d) { + quint32 slot = 0; + if (!this->readVarInt(&slot)) return false; + secondDelta = slot; + } else if (secondDelta == 0x1e) { + needsTimeRead = true; + } + } + + if (needsTimeRead) { + if (!this->reader.readU64(&secondDelta)) return false; + } + + this->lastMessageTime = this->lastMessageTime.addSecs(static_cast(secondDelta)); + + QByteArray body; + if (!this->readString(&body)) return false; + + *slot = LogMessage(msgType, QLatin1StringView(category), body, this->lastMessageTime); + } + + this->recentMessages.emplace(*slot); + return true; +} + +void EncodedLogWriter::writeOp(EncodedLogOpcode opcode) { this->buffer.writeU8(opcode); } + +void EncodedLogWriter::writeVarInt(quint32 n) { + if (n < 0xff) { + this->buffer.writeU8(n); + } else if (n < 0xffff) { + this->buffer.writeU8(0xff); + this->buffer.writeU16(n); + } else { + this->buffer.writeU8(0xff); + this->buffer.writeU16(0xffff); + this->buffer.writeU32(n); + } +} + +bool EncodedLogReader::readVarInt(quint32* slot) { + auto bytes = std::array(); + auto readLength = this->reader.peekBytes(reinterpret_cast(bytes.data()), 7); // NOLINT + + if (bytes[0] != 0xff && readLength >= 1) { + auto n = *reinterpret_cast(bytes.data()); // NOLINT + 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); // NOLINT + if (!this->reader.skip(3)) return false; + *slot = qFromLittleEndian(n); + } else if (readLength == 7) { + auto n = *reinterpret_cast(bytes.data() + 3); // NOLINT + if (!this->reader.skip(7)) return false; + *slot = qFromLittleEndian(n); + } else return false; + + return true; +} + +void EncodedLogWriter::writeString(QByteArrayView bytes) { + this->writeVarInt(bytes.length()); + this->buffer.writeBytes(bytes.constData(), bytes.length()); +} + +bool EncodedLogReader::readString(QByteArray* slot) { + quint32 length = 0; + if (!this->readVarInt(&length)) return false; + + *slot = QByteArray(length, Qt::Uninitialized); + auto r = this->reader.readBytes(slot->data(), slot->size()); + return r; +} + +quint16 EncodedLogWriter::getOrCreateCategory(QLatin1StringView category) { + if (this->categories.contains(category)) { + return this->categories.value(category); + } else { + this->writeOp(EncodedLogOpcode::RegisterCategory); + // id is implicitly the next available id + this->writeString(category); + + auto id = this->nextCategory++; + this->categories.insert(category, id); + + return id; + } +} + +bool EncodedLogReader::registerCategory() { + QByteArray name; + if (!this->readString(&name)) return false; + this->categories.append(name); + return true; +} + +bool readEncodedLogs(QIODevice* device) { + auto reader = EncodedLogReader(); + reader.setDevice(device); + + bool readable = false; + quint8 logVersion = 0; + quint8 readerVersion = 0; + if (!reader.readHeader(&readable, &logVersion, &readerVersion)) { + qCritical() << "Failed to read log header."; + return false; + } + + if (!readable) { + qCritical() << "This log was encoded with version" << logVersion + << "of the quickshell log encoder, which cannot be decoded by the current " + "version of quickshell, with log version" + << readerVersion; + return false; + } + + auto color = LogManager::instance()->colorLogs; + + LogMessage message; + auto stream = QTextStream(stdout); + while (reader.read(&message)) { + LogMessage::formatMessage(stream, message, color, true); + stream << '\n'; + } + + stream << Qt::flush; + + if (!device->atEnd()) { + qCritical() << "An error occurred parsing the end of this log file."; + qCritical() << "Remaining data:" << device->readAll(); + } + + return true; +} + +} // namespace qs::log diff --git a/src/core/logging.hpp b/src/core/logging.hpp index f831fcfa..6f4c970f 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -2,17 +2,22 @@ #include +#include #include -#include +#include +#include #include #include -#include #include +namespace qs::log { + struct LogMessage { + explicit LogMessage() = default; + explicit LogMessage( QtMsgType type, - const char* category, + QLatin1StringView category, QByteArray body, QDateTime time = QDateTime::currentDateTime() ) @@ -21,29 +26,19 @@ struct LogMessage { , category(category) , body(std::move(body)) {} - QtMsgType type; + bool operator==(const LogMessage& other) const; + + QtMsgType type = QtDebugMsg; QDateTime time; - const char* category; + QLatin1StringView category; QByteArray body; + + static void formatMessage(QTextStream& stream, const LogMessage& msg, bool color, bool timestamp); }; -class ThreadLogging: public QObject { - Q_OBJECT; +size_t qHash(const LogMessage& message); -public: - explicit ThreadLogging(QObject* parent): QObject(parent) {} - - void init(); - void initFs(); - void setupFileLogging(); - -private slots: - void onMessage(const LogMessage& msg); - -private: - QFile* file = nullptr; - QTextStream fileStream; -}; +class ThreadLogging; class LoggingThreadProxy: public QObject { Q_OBJECT; @@ -67,7 +62,7 @@ public: static void initFs(); static LogManager* instance(); - static void formatMessage(QTextStream& stream, const LogMessage& msg, bool color, bool timestamp); + bool colorLogs; signals: void logMessage(LogMessage msg); @@ -76,7 +71,12 @@ private: explicit LogManager(); static void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg); - bool colorLogs; QTextStream stdoutStream; LoggingThreadProxy threadProxy; }; + +bool readEncodedLogs(QIODevice* device); + +} // namespace qs::log + +using LogManager = qs::log::LogManager; diff --git a/src/core/logging_p.hpp b/src/core/logging_p.hpp new file mode 100644 index 00000000..0ac59dc1 --- /dev/null +++ b/src/core/logging_p.hpp @@ -0,0 +1,123 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#include "logging.hpp" +#include "ringbuf.hpp" + +namespace qs::log { + +enum EncodedLogOpcode : quint8 { + RegisterCategory = 0, + RecentMessageShort, + RecentMessageLong, + BeginCategories, +}; + +enum CompressedLogType : quint8 { + Debug = 0, + Info = 1, + Warn = 2, + Critical = 3, +}; + +CompressedLogType compressedTypeOf(QtMsgType type); +QtMsgType typeOfCompressed(CompressedLogType type); + +class WriteBuffer { +public: + void setDevice(QIODevice* device); + [[nodiscard]] bool hasDevice() const; + [[nodiscard]] bool flush(); + void writeBytes(const char* data, qsizetype length); + void writeU8(quint8 data); + void writeU16(quint16 data); + void writeU32(quint32 data); + void writeU64(quint64 data); + +private: + QIODevice* device = nullptr; + QByteArray buffer; +}; + +class DeviceReader { +public: + void setDevice(QIODevice* device); + [[nodiscard]] bool hasDevice() const; + [[nodiscard]] bool readBytes(char* data, qsizetype length); + // peek UP TO length + [[nodiscard]] qsizetype peekBytes(char* data, qsizetype length); + [[nodiscard]] bool skip(qsizetype length); + [[nodiscard]] bool readU8(quint8* data); + [[nodiscard]] bool readU16(quint16* data); + [[nodiscard]] bool readU32(quint32* data); + [[nodiscard]] bool readU64(quint64* data); + +private: + QIODevice* device = nullptr; +}; + +class EncodedLogWriter { +public: + void setDevice(QIODevice* target); + [[nodiscard]] bool writeHeader(); + [[nodiscard]] bool write(const LogMessage& message); + +private: + void writeOp(EncodedLogOpcode opcode); + void writeVarInt(quint32 n); + void writeString(QByteArrayView bytes); + quint16 getOrCreateCategory(QLatin1StringView category); + + WriteBuffer buffer; + + QHash categories; + quint16 nextCategory = EncodedLogOpcode::BeginCategories; + + QDateTime lastMessageTime = QDateTime::fromSecsSinceEpoch(0); + HashBuffer recentMessages {256}; +}; + +class EncodedLogReader { +public: + void setDevice(QIODevice* source); + [[nodiscard]] bool readHeader(bool* success, quint8* logVersion, quint8* readerVersion); + // WARNING: log messages written to the given slot are invalidated when the log reader is destroyed. + [[nodiscard]] bool read(LogMessage* slot); + +private: + [[nodiscard]] bool readVarInt(quint32* slot); + [[nodiscard]] bool readString(QByteArray* slot); + [[nodiscard]] bool registerCategory(); + + DeviceReader reader; + QVector categories; + QDateTime lastMessageTime = QDateTime::fromSecsSinceEpoch(0); + RingBuffer recentMessages {256}; +}; + +class ThreadLogging: public QObject { + Q_OBJECT; + +public: + explicit ThreadLogging(QObject* parent): QObject(parent) {} + + void init(); + void initFs(); + void setupFileLogging(); + +private slots: + void onMessage(const LogMessage& msg); + +private: + QFile* file = nullptr; + QTextStream fileStream; + QFile* detailedFile = nullptr; + EncodedLogWriter detailedWriter; +}; + +} // namespace qs::log diff --git a/src/core/main.cpp b/src/core/main.cpp index cc71f9fb..ad2b1a8d 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -61,6 +61,7 @@ int qs_main(int argc, char** argv) { auto workdirOption = QCommandLineOption({"d", "workdir"}, "Initial working directory.", "path"); auto debugPortOption = QCommandLineOption("debugport", "Enable the QML debugger.", "port"); auto debugWaitOption = QCommandLineOption("waitfordebug", "Wait for debugger connection before launching."); + auto readLogOption = QCommandLineOption("read-log", "Read a quickshell log file to stdout.", "path"); // clang-format on parser.addOption(currentOption); @@ -70,8 +71,22 @@ int qs_main(int argc, char** argv) { parser.addOption(workdirOption); parser.addOption(debugPortOption); parser.addOption(debugWaitOption); + parser.addOption(readLogOption); parser.process(app); + auto logOption = parser.value(readLogOption); + if (!logOption.isEmpty()) { + auto file = QFile(logOption); + if (!file.open(QFile::ReadOnly)) { + qCritical() << "Failed to open log for reading:" << logOption; + return -1; + } else { + qInfo() << "Reading log" << logOption; + } + + return qs::log::readEncodedLogs(&file) ? 0 : -1; + } + auto debugPortStr = parser.value(debugPortOption); if (!debugPortStr.isEmpty()) { auto ok = false; diff --git a/src/core/ringbuf.hpp b/src/core/ringbuf.hpp new file mode 100644 index 00000000..a50a56da --- /dev/null +++ b/src/core/ringbuf.hpp @@ -0,0 +1,169 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +// NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) + +// capacity 0 buffer cannot be inserted into, only replaced with = +// this is NOT exception safe for constructors +template +class RingBuffer { +public: + explicit RingBuffer() = default; + explicit RingBuffer(qsizetype capacity): mCapacity(capacity) { + if (capacity > 0) this->createData(); + } + + ~RingBuffer() { this->deleteData(); } + + Q_DISABLE_COPY(RingBuffer); + + explicit RingBuffer(RingBuffer&& other) noexcept { *this = std::move(other); } + + RingBuffer& operator=(RingBuffer&& other) noexcept { + this->deleteData(); + this->data = other.data; + this->head = other.head; + this->mSize = other.mSize; + this->mCapacity = other.mCapacity; + other.data = nullptr; + other.head = -1; + return *this; + } + + // undefined if capacity is 0 + template + T& emplace(Args&&... args) { + auto i = (this->head + 1) % this->mCapacity; + + if (this->indexIsAllocated(i)) { + this->data[i].~T(); + } + + auto* slot = &this->data[i]; + new (&this->data[i]) T(std::forward(args)...); + + this->head = i; + if (this->mSize != this->mCapacity) this->mSize = i + 1; + + return *slot; + } + + void clear() { + if (this->head == -1) return; + + auto i = this->head; + + do { + i = (i + 1) % this->mSize; + this->data[i].~T(); + } while (i != this->head); + + this->mSize = 0; + this->head = -1; + } + + // negative indexes and >size indexes are undefined + [[nodiscard]] T& at(qsizetype i) { + auto bufferI = (this->head - i) % this->mCapacity; + if (bufferI < 0) bufferI += this->mCapacity; + return this->data[bufferI]; + } + + [[nodiscard]] const T& at(qsizetype i) const { + return const_cast*>(this)->at(i); // NOLINT + } + + [[nodiscard]] qsizetype size() const { return this->mSize; } + [[nodiscard]] qsizetype capacity() const { return this->mCapacity; } + +private: + void createData() { + if (this->data != nullptr) return; + this->data = + static_cast(::operator new(this->mCapacity * sizeof(T), std::align_val_t {alignof(T)})); + } + + void deleteData() { + this->clear(); + ::operator delete(this->data, std::align_val_t {alignof(T)}); + this->data = nullptr; + } + + bool indexIsAllocated(qsizetype index) { + return this->mSize == this->mCapacity || index <= this->head; + } + + T* data = nullptr; + qsizetype mCapacity = 0; + qsizetype head = -1; + qsizetype mSize = 0; +}; + +// ring buffer with the ability to look up elements by hash (single bucket) +template +class HashBuffer { +public: + explicit HashBuffer() = default; + explicit HashBuffer(qsizetype capacity): ring(capacity) {} + ~HashBuffer() = default; + + Q_DISABLE_COPY(HashBuffer); + explicit HashBuffer(HashBuffer&& other) noexcept: ring(other.ring) {} + + HashBuffer& operator=(HashBuffer&& other) noexcept { + this->ring = other.ring; + return *this; + } + + // returns the index of the given value or -1 if missing + [[nodiscard]] qsizetype indexOf(const T& value, T** slot = nullptr) { + auto hash = qHash(value); + + for (auto i = 0; i < this->size(); i++) { + auto& v = this->ring.at(i); + if (hash == v.first && value == v.second) { + if (slot != nullptr) *slot = &v.second; + return i; + } + } + + return -1; + } + + [[nodiscard]] qsizetype indexOf(const T& value, T const** slot = nullptr) const { + return const_cast*>(this)->indexOf(value, slot); // NOLINT + } + + template + T& emplace(Args&&... args) { + auto& entry = this->ring.emplace( + std::piecewise_construct, + std::forward_as_tuple(0), + std::forward_as_tuple(std::forward(args)...) + ); + + entry.first = qHash(entry.second); + return entry.second; + } + + void clear() { this->ring.clear(); } + + // negative indexes and >size indexes are undefined + [[nodiscard]] T& at(qsizetype i) { return this->ring.at(i).second; } + [[nodiscard]] const T& at(qsizetype i) const { return this->ring.at(i).second; } + [[nodiscard]] qsizetype size() const { return this->ring.size(); } + [[nodiscard]] qsizetype capacity() const { return this->ring.capacity(); } + +private: + RingBuffer> ring; +}; + +// NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) diff --git a/src/core/test/CMakeLists.txt b/src/core/test/CMakeLists.txt index d0191ee9..3c057d3b 100644 --- a/src/core/test/CMakeLists.txt +++ b/src/core/test/CMakeLists.txt @@ -6,3 +6,4 @@ endfunction() qs_test(popupwindow popupwindow.cpp) qs_test(transformwatcher transformwatcher.cpp) +qs_test(ringbuffer ringbuf.cpp) diff --git a/src/core/test/ringbuf.cpp b/src/core/test/ringbuf.cpp new file mode 100644 index 00000000..4f114796 --- /dev/null +++ b/src/core/test/ringbuf.cpp @@ -0,0 +1,125 @@ +#include "ringbuf.hpp" +#include + +#include +#include +#include +#include + +#include "../ringbuf.hpp" + +TestObject::TestObject(quint32* count): count(count) { + (*this->count)++; + qDebug() << "Created TestObject" << this << "- count is now" << *this->count; +} + +TestObject::~TestObject() { + (*this->count)--; + qDebug() << "Destroyed TestObject" << this << "- count is now" << *this->count; +} + +void TestRingBuffer::fill() { + quint32 counter = 0; + auto rb = RingBuffer(3); + QCOMPARE(rb.capacity(), 3); + + qInfo() << "adding test objects"; + auto* n1 = &rb.emplace(&counter); + auto* n2 = &rb.emplace(&counter); + auto* n3 = &rb.emplace(&counter); + QCOMPARE(counter, 3); + QCOMPARE(rb.size(), 3); + QCOMPARE(&rb.at(0), n3); + QCOMPARE(&rb.at(1), n2); + QCOMPARE(&rb.at(2), n1); + + qInfo() << "replacing last object with new one"; + auto* n4 = &rb.emplace(&counter); + QCOMPARE(counter, 3); + QCOMPARE(rb.size(), 3); + QCOMPARE(&rb.at(0), n4); + QCOMPARE(&rb.at(1), n3); + QCOMPARE(&rb.at(2), n2); + + qInfo() << "replacing the rest"; + auto* n5 = &rb.emplace(&counter); + auto* n6 = &rb.emplace(&counter); + QCOMPARE(counter, 3); + QCOMPARE(rb.size(), 3); + QCOMPARE(&rb.at(0), n6); + QCOMPARE(&rb.at(1), n5); + QCOMPARE(&rb.at(2), n4); + + qInfo() << "clearing buffer"; + rb.clear(); + QCOMPARE(counter, 0); + QCOMPARE(rb.size(), 0); +} + +void TestRingBuffer::clearPartial() { + quint32 counter = 0; + auto rb = RingBuffer(2); + + qInfo() << "adding object to buffer"; + auto* n1 = &rb.emplace(&counter); + QCOMPARE(counter, 1); + QCOMPARE(rb.size(), 1); + QCOMPARE(&rb.at(0), n1); + + qInfo() << "clearing buffer"; + rb.clear(); + QCOMPARE(counter, 0); + QCOMPARE(rb.size(), 0); +} + +void TestRingBuffer::move() { + quint32 counter = 0; + + { + auto rb1 = RingBuffer(1); + + qInfo() << "adding object to first buffer"; + auto* n1 = &rb1.emplace(&counter); + QCOMPARE(counter, 1); + QCOMPARE(rb1.size(), 1); + QCOMPARE(&rb1.at(0), n1); + + qInfo() << "move constructing new buffer"; + auto rb2 = RingBuffer(std::move(rb1)); + QCOMPARE(counter, 1); + QCOMPARE(rb2.size(), 1); + QCOMPARE(&rb2.at(0), n1); + + qInfo() << "move assigning new buffer"; + auto rb3 = RingBuffer(); + rb3 = std::move(rb2); + QCOMPARE(counter, 1); + QCOMPARE(rb3.size(), 1); + QCOMPARE(&rb3.at(0), n1); + } + + QCOMPARE(counter, 0); +} + +void TestRingBuffer::hashLookup() { + auto hb = HashBuffer(3); + + qInfo() << "inserting 1,2,3 into HashBuffer"; + hb.emplace(1); + hb.emplace(2); + hb.emplace(3); + + qInfo() << "checking lookups"; + QCOMPARE(hb.indexOf(3), 0); + QCOMPARE(hb.indexOf(2), 1); + QCOMPARE(hb.indexOf(1), 2); + + qInfo() << "adding 4"; + hb.emplace(4); + QCOMPARE(hb.indexOf(4), 0); + QCOMPARE(hb.indexOf(3), 1); + QCOMPARE(hb.indexOf(2), 2); + QCOMPARE(hb.indexOf(1), -1); +} + +QTEST_MAIN(TestRingBuffer); diff --git a/src/core/test/ringbuf.hpp b/src/core/test/ringbuf.hpp new file mode 100644 index 00000000..1413031c --- /dev/null +++ b/src/core/test/ringbuf.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +class TestObject { +public: + explicit TestObject(quint32* count); + ~TestObject(); + Q_DISABLE_COPY_MOVE(TestObject); + +private: + quint32* count; +}; + +class TestRingBuffer: public QObject { + Q_OBJECT; + +private slots: + static void fill(); + static void clearPartial(); + static void move(); + + static void hashLookup(); +}; From 291179ede2c6b3af490f1cc81326fafc398e8ac8 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 9 Aug 2024 19:22:18 -0700 Subject: [PATCH 117/305] core/command: rewrite command parser with CLI11 --- .clang-tidy | 1 + BUILD.md | 1 + default.nix | 2 + src/core/CMakeLists.txt | 4 +- src/core/logging.cpp | 11 +- src/core/logging.hpp | 4 +- src/core/main.cpp | 291 +++++++++++++++++++++++----------------- 7 files changed, 185 insertions(+), 129 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 14e9b9ae..5741bf61 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -15,6 +15,7 @@ Checks: > -cppcoreguidelines-non-private-member-variables-in-classes, -cppcoreguidelines-avoid-goto, -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-avoid-do-while, google-build-using-namespace. google-explicit-constructor, google-global-names-in-headers, diff --git a/BUILD.md b/BUILD.md index ea69cbd6..5fe6ebc6 100644 --- a/BUILD.md +++ b/BUILD.md @@ -9,6 +9,7 @@ Quickshell has a set of base dependencies you will always need, names vary by di - `qt6base` - `qt6declarative` - `pkg-config` +- `cli11` We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and svg icons will not work, including system ones. diff --git a/default.nix b/default.nix index 2393d8ab..34cc0f4b 100644 --- a/default.nix +++ b/default.nix @@ -8,6 +8,7 @@ cmake, ninja, qt6, + cli11, jemalloc, wayland, wayland-protocols, @@ -52,6 +53,7 @@ buildInputs = [ qt6.qtbase qt6.qtdeclarative + cli11 ] ++ (lib.optional withJemalloc jemalloc) ++ (lib.optional withQtSvg qt6.qtsvg) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 5ced5410..fb39287c 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,3 +1,5 @@ +find_package(CLI11 CONFIG REQUIRED) + qt_add_library(quickshell-core STATIC main.cpp plugin.cpp @@ -44,7 +46,7 @@ qt_add_library(quickshell-core STATIC set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1) -target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} Qt6::QuickPrivate) +target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} Qt6::QuickPrivate CLI11::CLI11) qs_pch(quickshell-core) target_link_libraries(quickshell PRIVATE quickshell-coreplugin) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 9271c07f..86b8bb37 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -81,9 +81,7 @@ void LogMessage::formatMessage( if (color && msg.type == QtFatalMsg) stream << "\033[0m"; } -LogManager::LogManager() - : colorLogs(qEnvironmentVariableIsEmpty("NO_COLOR")) - , stdoutStream(stdout) {} +LogManager::LogManager(): stdoutStream(stdout) {} void LogManager::messageHandler( QtMsgType type, @@ -105,8 +103,9 @@ LogManager* LogManager::instance() { return instance; } -void LogManager::init() { +void LogManager::init(bool color) { auto* instance = LogManager::instance(); + instance->colorLogs = color; qInstallMessageHandler(&LogManager::messageHandler); @@ -203,6 +202,8 @@ void ThreadLogging::initFs() { << path; delete file; file = nullptr; + } else { + qInfo() << "Saving logs to" << path; } // buffered by WriteBuffer @@ -212,6 +213,8 @@ void ThreadLogging::initFs() { << detailedPath; delete detailedFile; detailedFile = nullptr; + } else { + qCInfo(logLogging) << "Saving detailed logs to" << path; } qCDebug(logLogging) << "Copying memfd logs to log file..."; diff --git a/src/core/logging.hpp b/src/core/logging.hpp index 6f4c970f..9909b1a8 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -58,11 +58,11 @@ class LogManager: public QObject { Q_OBJECT; public: - static void init(); + static void init(bool color); static void initFs(); static LogManager* instance(); - bool colorLogs; + bool colorLogs = true; signals: void logMessage(LogMessage msg); diff --git a/src/core/main.cpp b/src/core/main.cpp index ad2b1a8d..b363692e 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -1,9 +1,11 @@ #include "main.hpp" #include +#include +#include // NOLINT: Need to include this for impls of some CLI11 classes +#include +#include #include -#include -#include #include #include #include @@ -28,87 +30,132 @@ #include "rootwrapper.hpp" int qs_main(int argc, char** argv) { - QString configFilePath; + + auto qArgC = 1; + auto* qArgV = argv; + + auto noColor = !qEnvironmentVariableIsEmpty("NO_COLOR"); + QString workingDirectory; + QString configFilePath; + QString shellId; + auto printInfo = false; + + auto debugPort = -1; + auto waitForDebug = false; auto useQApplication = false; auto nativeTextRendering = false; auto desktopSettingsAware = true; - auto shellId = QString(); QHash envOverrides; - int debugPort = -1; - bool waitForDebug = false; - bool printCurrent = false; - { - const auto app = QCoreApplication(argc, argv); - QCoreApplication::setApplicationName("quickshell"); - QCoreApplication::setApplicationVersion("0.1.0 (" GIT_REVISION ")"); + auto app = CLI::App(""); + + class QStringOption { + public: + QStringOption() = default; + QStringOption& operator=(const std::string& str) { + this->str = QString::fromStdString(str); + return *this; + } + + QString& operator*() { return this->str; } + + private: + QString str; + }; + + class QStringRefOption { + public: + QStringRefOption(QString* str): str(str) {} + QStringRefOption& operator=(const std::string& str) { + *this->str = QString::fromStdString(str); + return *this; + } + + private: + QString* str; + }; + + /// --- + QStringOption path; + QStringOption manifest; + QStringOption config; + QStringRefOption workdirRef(&workingDirectory); + + auto* selection = app.add_option_group( + "Config Selection", + "Select a configuration to run (defaults to $XDG_CONFIG_HOME/quickshell/shell.qml)" + ); + + auto* pathArg = + selection->add_option("-p,--path", path, "Path to a QML file to run. (Env:QS_CONFIG_PATH)"); + + auto* mfArg = selection->add_option( + "-m,--manifest", + manifest, + "Path to a manifest containing configurations. (Env:QS_MANIFEST)\n" + "(Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf)" + ); + + auto* cfgArg = selection->add_option( + "-c,--config", + config, + "Name of a configuration within a manifest. (Env:QS_CONFIG_NAME)" + ); + + selection->add_option("-d,--workdir", workdirRef, "Initial working directory."); + + pathArg->excludes(mfArg, cfgArg); + + /// --- + auto* debug = app.add_option_group("Debugging"); + + auto* debugPortArg = debug + ->add_option( + "--debugport", + debugPort, + "Open the given port for a QML debugger to connect to." + ) + ->check(CLI::Range(0, 65535)); + + debug + ->add_flag( + "--waitfordebug", + waitForDebug, + "Wait for a debugger to attach to the given port before launching." + ) + ->needs(debugPortArg); + + /// --- + app.add_flag("--info", printInfo, "Print information about the shell")->excludes(debugPortArg); + app.add_flag("--no-color", noColor, "Do not color the log output. (Env:NO_COLOR)"); + + /// --- + QStringOption logpath; + auto* readLog = app.add_subcommand("read-log", "Read a quickshell log file."); + readLog->add_option("path", logpath, "Path to the log file to read")->required(); + readLog->add_flag("--no-color", noColor, "Do not color the log output. (Env:NO_COLOR)"); + + CLI11_PARSE(app, argc, argv); + + const auto qApplication = QCoreApplication(qArgC, qArgV); // Start log manager - has to happen with an active event loop or offthread can't be started. - LogManager::init(); + LogManager::init(!noColor); - QCommandLineParser parser; - parser.addHelpOption(); - parser.addVersionOption(); - - // clang-format off - auto currentOption = QCommandLineOption("current", "Print information about the manifest and defaults."); - auto manifestOption = QCommandLineOption({"m", "manifest"}, "Path to a configuration manifest.", "path"); - auto configOption = QCommandLineOption({"c", "config"}, "Name of a configuration in the manifest.", "name"); - auto pathOption = QCommandLineOption({"p", "path"}, "Path to a configuration file.", "path"); - auto workdirOption = QCommandLineOption({"d", "workdir"}, "Initial working directory.", "path"); - auto debugPortOption = QCommandLineOption("debugport", "Enable the QML debugger.", "port"); - auto debugWaitOption = QCommandLineOption("waitfordebug", "Wait for debugger connection before launching."); - auto readLogOption = QCommandLineOption("read-log", "Read a quickshell log file to stdout.", "path"); - // clang-format on - - parser.addOption(currentOption); - parser.addOption(manifestOption); - parser.addOption(configOption); - parser.addOption(pathOption); - parser.addOption(workdirOption); - parser.addOption(debugPortOption); - parser.addOption(debugWaitOption); - parser.addOption(readLogOption); - parser.process(app); - - auto logOption = parser.value(readLogOption); - if (!logOption.isEmpty()) { - auto file = QFile(logOption); + if (*readLog) { + auto file = QFile(*logpath); if (!file.open(QFile::ReadOnly)) { - qCritical() << "Failed to open log for reading:" << logOption; + qCritical() << "Failed to open log for reading:" << *logpath; return -1; } else { - qInfo() << "Reading log" << logOption; + qInfo() << "Reading log" << *logpath; } return qs::log::readEncodedLogs(&file) ? 0 : -1; - } - - auto debugPortStr = parser.value(debugPortOption); - if (!debugPortStr.isEmpty()) { - auto ok = false; - debugPort = debugPortStr.toInt(&ok); - - if (!ok) { - qCritical() << "Debug port must be a valid port number."; - return -1; - } - } - - if (parser.isSet(debugWaitOption)) { - if (debugPort == -1) { - qCritical() << "Cannot wait for debugger without a debug port set."; - return -1; - } - - waitForDebug = true; - } - - { - printCurrent = parser.isSet(currentOption); + } else { // NOLINTBEGIN #define CHECK(rname, name, level, label, expr) \ @@ -116,7 +163,7 @@ int qs_main(int argc, char** argv) { if (rname.isEmpty() && !name.isEmpty()) { \ rname = name; \ rname##Level = level; \ - if (!printCurrent) goto label; \ + if (!printInfo) goto label; \ } #define OPTSTR(name) (name.isEmpty() ? "(unset)" : name.toStdString()) @@ -133,7 +180,7 @@ int qs_main(int argc, char** argv) { // clang-format on // NOLINTEND - if (printCurrent) { + if (printInfo) { // clang-format off std::cout << "Base path: " << OPTSTR(basePath) << "\n"; std::cout << " - Environment (QS_BASE_PATH): " << OPTSTR(envBasePath) << "\n"; @@ -147,11 +194,11 @@ int qs_main(int argc, char** argv) { int configPathLevel = 10; { // NOLINTBEGIN - CHECK(configPath, optionConfigPath, 0, foundpath, parser.value(pathOption)); + CHECK(configPath, optionConfigPath, 0, foundpath, *path); CHECK(configPath, envConfigPath, 1, foundpath, qEnvironmentVariable("QS_CONFIG_PATH")); // NOLINTEND - if (printCurrent) { + if (printInfo) { // clang-format off std::cout << "\nConfig path: " << OPTSTR(configPath) << "\n"; std::cout << " - Option: " << OPTSTR(optionConfigPath) << "\n"; @@ -166,13 +213,13 @@ int qs_main(int argc, char** argv) { { // NOLINTBEGIN // clang-format off - CHECK(manifestPath, optionManifestPath, 0, foundmf, parser.value(manifestOption)); + CHECK(manifestPath, optionManifestPath, 0, foundmf, *manifest); CHECK(manifestPath, envManifestPath, 1, foundmf, qEnvironmentVariable("QS_MANIFEST")); CHECK(manifestPath, defaultManifestPath, 2, foundmf, QDir(basePath).filePath("manifest.conf")); // clang-format on // NOLINTEND - if (printCurrent) { + if (printInfo) { // clang-format off std::cout << "\nManifest path: " << OPTSTR(manifestPath) << "\n"; std::cout << " - Option: " << OPTSTR(optionManifestPath) << "\n"; @@ -187,11 +234,11 @@ int qs_main(int argc, char** argv) { int configNameLevel = 10; { // NOLINTBEGIN - CHECK(configName, optionConfigName, 0, foundname, parser.value(configOption)); + CHECK(configName, optionConfigName, 0, foundname, *config); CHECK(configName, envConfigName, 1, foundname, qEnvironmentVariable("QS_CONFIG_NAME")); // NOLINTEND - if (printCurrent) { + if (printInfo) { // clang-format off std::cout << "\nConfig name: " << OPTSTR(configName) << "\n"; std::cout << " - Option: " << OPTSTR(optionConfigName) << "\n"; @@ -201,11 +248,6 @@ int qs_main(int argc, char** argv) { } foundname:; - if (configPathLevel == 0 && configNameLevel == 0) { - qCritical() << "Pass only one of --path or --config"; - return -1; - } - if (!configPath.isEmpty() && configPathLevel <= configNameLevel) { configFilePath = configPath; } else if (!configName.isEmpty()) { @@ -299,60 +341,56 @@ int qs_main(int argc, char** argv) { shellId = QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); - qInfo() << "config file path:" << configFilePath; - } + qInfo() << "Config file path:" << configFilePath; - if (!QFile(configFilePath).exists()) { - qCritical() << "config file does not exist"; - return -1; - } + if (!QFile(configFilePath).exists()) { + qCritical() << "config file does not exist"; + return -1; + } - if (parser.isSet(workdirOption)) { - workingDirectory = parser.value(workdirOption); - } + auto file = QFile(configFilePath); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + qCritical() << "could not open config file"; + return -1; + } - auto file = QFile(configFilePath); - if (!file.open(QFile::ReadOnly | QFile::Text)) { - qCritical() << "could not open config file"; - return -1; - } + auto stream = QTextStream(&file); + while (!stream.atEnd()) { + auto line = stream.readLine().trimmed(); + if (line.startsWith("//@ pragma ")) { + auto pragma = line.sliced(11).trimmed(); - auto stream = QTextStream(&file); - while (!stream.atEnd()) { - auto line = stream.readLine().trimmed(); - if (line.startsWith("//@ pragma ")) { - auto pragma = line.sliced(11).trimmed(); + if (pragma == "UseQApplication") useQApplication = true; + else if (pragma == "NativeTextRendering") nativeTextRendering = true; + else if (pragma == "IgnoreSystemSettings") desktopSettingsAware = false; + else if (pragma.startsWith("Env ")) { + auto envPragma = pragma.sliced(4); + auto splitIdx = envPragma.indexOf('='); - if (pragma == "UseQApplication") useQApplication = true; - else if (pragma == "NativeTextRendering") nativeTextRendering = true; - else if (pragma == "IgnoreSystemSettings") desktopSettingsAware = false; - else if (pragma.startsWith("Env ")) { - auto envPragma = pragma.sliced(4); - auto splitIdx = envPragma.indexOf('='); + if (splitIdx == -1) { + qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'"; + return -1; + } - if (splitIdx == -1) { - qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'"; + auto var = envPragma.sliced(0, splitIdx).trimmed(); + auto val = envPragma.sliced(splitIdx + 1).trimmed(); + envOverrides.insert(var, val); + } else if (pragma.startsWith("ShellId ")) { + shellId = pragma.sliced(8).trimmed(); + } else { + qCritical() << "Unrecognized pragma" << pragma; return -1; } + } else if (line.startsWith("import")) break; + } - auto var = envPragma.sliced(0, splitIdx).trimmed(); - auto val = envPragma.sliced(splitIdx + 1).trimmed(); - envOverrides.insert(var, val); - } else if (pragma.startsWith("ShellId ")) { - shellId = pragma.sliced(8).trimmed(); - } else { - qCritical() << "Unrecognized pragma" << pragma; - return -1; - } - } else if (line.startsWith("import")) break; + file.close(); } - - file.close(); } - qInfo() << "shell id:" << shellId; + qInfo() << "Shell ID:" << shellId; - if (printCurrent) return 0; + if (printInfo) return 0; for (auto [var, val]: envOverrides.asKeyValueRange()) { qputenv(var.toUtf8(), val.toUtf8()); @@ -360,6 +398,15 @@ int qs_main(int argc, char** argv) { QsPaths::init(shellId); + if (auto* cacheDir = QsPaths::instance()->cacheDir()) { + auto qmlCacheDir = cacheDir->filePath("qml-engine-cache"); + qputenv("QML_DISK_CACHE_PATH", qmlCacheDir.toLocal8Bit()); + + if (!qEnvironmentVariableIsSet("QML_DISK_CACHE")) { + qputenv("QML_DISK_CACHE", "aot,qmlc"); + } + } + // While the simple animation driver can lead to better animations in some cases, // it also can cause excessive repainting at excessively high framerates which can // lead to noticeable amounts of gpu usage, including overheating on some systems. @@ -401,9 +448,9 @@ int qs_main(int argc, char** argv) { QGuiApplication* app = nullptr; if (useQApplication) { - app = new QApplication(argc, argv); + app = new QApplication(qArgC, qArgV); } else { - app = new QGuiApplication(argc, argv); + app = new QGuiApplication(qArgC, qArgV); } LogManager::initFs(); From 0fc98652a85303cef54766c096286b90f401048a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 9 Aug 2024 20:24:00 -0700 Subject: [PATCH 118/305] core/log: create fully detailed logs by default The .qslog logs now log messages for quickshell* by default. --- src/core/logging.cpp | 61 ++++++++++++++++++++++++++++++++++++------ src/core/logging.hpp | 24 +++++++++++++++-- src/core/logging_p.hpp | 2 +- src/core/main.cpp | 11 ++++++-- 4 files changed, 85 insertions(+), 13 deletions(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 86b8bb37..a5ed80f5 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -92,10 +93,48 @@ void LogManager::messageHandler( auto* self = LogManager::instance(); - LogMessage::formatMessage(self->stdoutStream, message, self->colorLogs, false); - self->stdoutStream << Qt::endl; + auto display = true; - emit self->logMessage(message); + const auto* key = static_cast(context.category); + + if (self->sparseFilters.contains(key)) { + auto filter = self->sparseFilters.value(key); + switch (type) { + case QtDebugMsg: display = filter.debug; break; + case QtInfoMsg: display = filter.info; break; + case QtWarningMsg: display = filter.warn; break; + case QtCriticalMsg: display = filter.critical; break; + default: break; + } + } + + if (display) { + LogMessage::formatMessage(self->stdoutStream, message, self->colorLogs, false); + self->stdoutStream << Qt::endl; + } + + emit self->logMessage(message, display); +} + +void LogManager::filterCategory(QLoggingCategory* category) { + auto* instance = LogManager::instance(); + + if (instance->lastCategoryFilter) { + instance->lastCategoryFilter(category); + } + + if (QLatin1StringView(category->categoryName()).startsWith(QLatin1StringView("quickshell"))) { + // We assume the category name pointer will always be the same and be comparable in the message handler. + LogManager::instance()->sparseFilters.insert( + static_cast(category->categoryName()), + CategoryFilter(category) + ); + + category->setEnabled(QtDebugMsg, true); + category->setEnabled(QtInfoMsg, true); + category->setEnabled(QtWarningMsg, true); + category->setEnabled(QtCriticalMsg, true); + } } LogManager* LogManager::instance() { @@ -103,12 +142,16 @@ LogManager* LogManager::instance() { return instance; } -void LogManager::init(bool color) { +void LogManager::init(bool color, bool sparseOnly) { auto* instance = LogManager::instance(); instance->colorLogs = color; qInstallMessageHandler(&LogManager::messageHandler); + if (!sparseOnly) { + instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory); + } + qCDebug(logLogging) << "Creating offthread logger..."; auto* thread = new QThread(); instance->threadProxy.moveToThread(thread); @@ -269,10 +312,12 @@ void ThreadLogging::initFs() { qCDebug(logLogging) << "Switched threaded logger to queued eventloop connection."; } -void ThreadLogging::onMessage(const LogMessage& msg) { - if (this->fileStream.device() == nullptr) return; - LogMessage::formatMessage(this->fileStream, msg, false, true); - this->fileStream << Qt::endl; +void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) { + if (showInSparse) { + if (this->fileStream.device() == nullptr) return; + LogMessage::formatMessage(this->fileStream, msg, false, true); + this->fileStream << Qt::endl; + } if (this->detailedWriter.write(msg)) { this->detailedFile->flush(); diff --git a/src/core/logging.hpp b/src/core/logging.hpp index 9909b1a8..69188391 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -54,23 +55,42 @@ private: ThreadLogging* logging = nullptr; }; +struct CategoryFilter { + explicit CategoryFilter() = default; + explicit CategoryFilter(QLoggingCategory* category) + : debug(category->isDebugEnabled()) + , info(category->isInfoEnabled()) + , warn(category->isWarningEnabled()) + , critical(category->isCriticalEnabled()) {} + + bool debug = true; + bool info = true; + bool warn = true; + bool critical = true; +}; + class LogManager: public QObject { Q_OBJECT; public: - static void init(bool color); + static void init(bool color, bool sparseOnly); static void initFs(); static LogManager* instance(); bool colorLogs = true; signals: - void logMessage(LogMessage msg); + void logMessage(LogMessage msg, bool showInSparse); private: explicit LogManager(); static void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg); + static void filterCategory(QLoggingCategory* category); + + QLoggingCategory::CategoryFilter lastCategoryFilter = nullptr; + QHash sparseFilters; + QTextStream stdoutStream; LoggingThreadProxy threadProxy; }; diff --git a/src/core/logging_p.hpp b/src/core/logging_p.hpp index 0ac59dc1..cb1c3713 100644 --- a/src/core/logging_p.hpp +++ b/src/core/logging_p.hpp @@ -111,7 +111,7 @@ public: void setupFileLogging(); private slots: - void onMessage(const LogMessage& msg); + void onMessage(const LogMessage& msg, bool showInSparse); private: QFile* file = nullptr; diff --git a/src/core/main.cpp b/src/core/main.cpp index b363692e..86f4ec8c 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -2,8 +2,8 @@ #include #include -#include // NOLINT: Need to include this for impls of some CLI11 classes #include +#include // NOLINT: Need to include this for impls of some CLI11 classes #include #include #include @@ -129,9 +129,16 @@ int qs_main(int argc, char** argv) { ->needs(debugPortArg); /// --- + bool sparseLogsOnly = false; app.add_flag("--info", printInfo, "Print information about the shell")->excludes(debugPortArg); app.add_flag("--no-color", noColor, "Do not color the log output. (Env:NO_COLOR)"); + app.add_flag( + "--no-detailed-logs", + sparseLogsOnly, + "Do not enable this unless you know what you are doing." + ); + /// --- QStringOption logpath; auto* readLog = app.add_subcommand("read-log", "Read a quickshell log file."); @@ -143,7 +150,7 @@ int qs_main(int argc, char** argv) { const auto qApplication = QCoreApplication(qArgC, qArgV); // Start log manager - has to happen with an active event loop or offthread can't be started. - LogManager::init(!noColor); + LogManager::init(!noColor, sparseLogsOnly); if (*readLog) { auto file = QFile(*logpath); From c2b4610acb5fb55bb51c14bf075a7ffce2307e52 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 9 Aug 2024 23:45:46 -0700 Subject: [PATCH 119/305] core/log: add read-log --filter --- src/core/logging.cpp | 67 +++++++++++--- src/core/logging.hpp | 5 +- src/core/logging_qtprivate.cpp | 159 +++++++++++++++++++++++++++++++++ src/core/main.cpp | 20 +++-- 4 files changed, 232 insertions(+), 19 deletions(-) create mode 100644 src/core/logging_qtprivate.cpp diff --git a/src/core/logging.cpp b/src/core/logging.cpp index a5ed80f5..7a8426b8 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -7,12 +7,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -23,6 +25,7 @@ #include #include "logging_p.hpp" +#include "logging_qtprivate.cpp" // NOLINT #include "paths.hpp" namespace qs::log { @@ -82,6 +85,16 @@ void LogMessage::formatMessage( if (color && msg.type == QtFatalMsg) stream << "\033[0m"; } +bool CategoryFilter::shouldDisplay(QtMsgType type) const { + switch (type) { + case QtDebugMsg: return this->debug; + case QtInfoMsg: return this->info; + case QtWarningMsg: return this->warn; + case QtCriticalMsg: return this->critical; + default: return true; + } +} + LogManager::LogManager(): stdoutStream(stdout) {} void LogManager::messageHandler( @@ -98,14 +111,7 @@ void LogManager::messageHandler( const auto* key = static_cast(context.category); if (self->sparseFilters.contains(key)) { - auto filter = self->sparseFilters.value(key); - switch (type) { - case QtDebugMsg: display = filter.debug; break; - case QtInfoMsg: display = filter.info; break; - case QtWarningMsg: display = filter.warn; break; - case QtCriticalMsg: display = filter.critical; break; - default: break; - } + display = self->sparseFilters.value(key).shouldDisplay(type); } if (display) { @@ -520,7 +526,8 @@ start: slot->time = this->lastMessageTime; } } else { - auto category = this->categories.value(next - EncodedLogOpcode::BeginCategories); + auto categoryId = next - EncodedLogOpcode::BeginCategories; + auto category = this->categories.value(categoryId); quint8 field = 0; if (!this->reader.readU8(&field)) return false; @@ -555,6 +562,7 @@ start: if (!this->readString(&body)) return false; *slot = LogMessage(msgType, QLatin1StringView(category), body, this->lastMessageTime); + slot->readCategoryId = categoryId; } this->recentMessages.emplace(*slot); @@ -633,7 +641,17 @@ bool EncodedLogReader::registerCategory() { return true; } -bool readEncodedLogs(QIODevice* device) { +bool readEncodedLogs(QIODevice* device, const QString& rulespec) { + using namespace qt_logging_registry; + + QList rules; + + { + QLoggingSettingsParser parser; + parser.setContent(rulespec); + rules = parser.rules(); + } + auto reader = EncodedLogReader(); reader.setDevice(device); @@ -655,11 +673,36 @@ bool readEncodedLogs(QIODevice* device) { auto color = LogManager::instance()->colorLogs; + auto filters = QHash(); + LogMessage message; auto stream = QTextStream(stdout); while (reader.read(&message)) { - LogMessage::formatMessage(stream, message, color, true); - stream << '\n'; + CategoryFilter filter; + if (filters.contains(message.readCategoryId)) { + filter = filters.value(message.readCategoryId); + } else { + for (const auto& rule: rules) { + auto filterpass = rule.pass(message.category, QtDebugMsg); + if (filterpass != 0) filter.debug = filterpass > 0; + + filterpass = rule.pass(message.category, QtInfoMsg); + if (filterpass != 0) filter.info = filterpass > 0; + + filterpass = rule.pass(message.category, QtWarningMsg); + if (filterpass != 0) filter.warn = filterpass > 0; + + filterpass = rule.pass(message.category, QtCriticalMsg); + if (filterpass != 0) filter.critical = filterpass > 0; + } + + filters.insert(message.readCategoryId, filter); + } + + if (filter.shouldDisplay(message.type)) { + LogMessage::formatMessage(stream, message, color, true); + stream << '\n'; + } } stream << Qt::flush; diff --git a/src/core/logging.hpp b/src/core/logging.hpp index 69188391..5462144f 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -33,6 +33,7 @@ struct LogMessage { QDateTime time; QLatin1StringView category; QByteArray body; + quint16 readCategoryId = 0; static void formatMessage(QTextStream& stream, const LogMessage& msg, bool color, bool timestamp); }; @@ -63,6 +64,8 @@ struct CategoryFilter { , warn(category->isWarningEnabled()) , critical(category->isCriticalEnabled()) {} + [[nodiscard]] bool shouldDisplay(QtMsgType type) const; + bool debug = true; bool info = true; bool warn = true; @@ -95,7 +98,7 @@ private: LoggingThreadProxy threadProxy; }; -bool readEncodedLogs(QIODevice* device); +bool readEncodedLogs(QIODevice* device, const QString& rulespec); } // namespace qs::log diff --git a/src/core/logging_qtprivate.cpp b/src/core/logging_qtprivate.cpp new file mode 100644 index 00000000..05393f02 --- /dev/null +++ b/src/core/logging_qtprivate.cpp @@ -0,0 +1,159 @@ +// The logging rule parser from qloggingregistry_p.h and qloggingregistry.cpp. + +// Was unable to properly link the functions when directly using the headers (which we depend +// on anyway), so below is a slightly stripped down copy. Making the originals link would +// be preferable. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qs::log { +Q_DECLARE_LOGGING_CATEGORY(logLogging); + +namespace qt_logging_registry { + +class QLoggingRule { +public: + QLoggingRule(); + QLoggingRule(QStringView pattern, bool enabled); + [[nodiscard]] int pass(QLatin1StringView categoryName, QtMsgType type) const; + + enum PatternFlag { + FullText = 0x1, + LeftFilter = 0x2, + RightFilter = 0x4, + MidFilter = LeftFilter | RightFilter + }; + Q_DECLARE_FLAGS(PatternFlags, PatternFlag) + + QString category; + int messageType; + PatternFlags flags; + bool enabled; + +private: + void parse(QStringView pattern); +}; + +class QLoggingSettingsParser { +public: + void setContent(QStringView content); + + [[nodiscard]] QList rules() const { return this->mRules; } + +private: + void parseNextLine(QStringView line); + +private: + QList mRules; +}; + +void QLoggingSettingsParser::setContent(QStringView content) { + this->mRules.clear(); + for (auto line: qTokenize(content, u';')) this->parseNextLine(line); +} + +void QLoggingSettingsParser::parseNextLine(QStringView line) { + // Remove whitespace at start and end of line: + line = line.trimmed(); + + const qsizetype equalPos = line.indexOf(u'='); + if (equalPos != -1) { + if (line.lastIndexOf(u'=') == equalPos) { + const auto key = line.left(equalPos).trimmed(); + const QStringView pattern = key; + const auto valueStr = line.mid(equalPos + 1).trimmed(); + int value = -1; + if (valueStr == QString("true")) value = 1; + else if (valueStr == QString("false")) value = 0; + QLoggingRule rule(pattern, (value == 1)); + if (rule.flags != 0 && (value != -1)) this->mRules.append(std::move(rule)); + else + qCWarning(logLogging, "Ignoring malformed logging rule: '%s'", line.toUtf8().constData()); + } else { + qCWarning(logLogging, "Ignoring malformed logging rule: '%s'", line.toUtf8().constData()); + } + } +} + +QLoggingRule::QLoggingRule(QStringView pattern, bool enabled): messageType(-1), enabled(enabled) { + this->parse(pattern); +} + +void QLoggingRule::parse(QStringView pattern) { + QStringView p; + + // strip trailing ".messagetype" + if (pattern.endsWith(QString(".debug"))) { + p = pattern.chopped(6); // strlen(".debug") + this->messageType = QtDebugMsg; + } else if (pattern.endsWith(QString(".info"))) { + p = pattern.chopped(5); // strlen(".info") + this->messageType = QtInfoMsg; + } else if (pattern.endsWith(QString(".warning"))) { + p = pattern.chopped(8); // strlen(".warning") + this->messageType = QtWarningMsg; + } else if (pattern.endsWith(QString(".critical"))) { + p = pattern.chopped(9); // strlen(".critical") + this->messageType = QtCriticalMsg; + } else { + p = pattern; + } + + const QChar asterisk = u'*'; + if (!p.contains(asterisk)) { + this->flags = FullText; + } else { + if (p.endsWith(asterisk)) { + this->flags |= LeftFilter; + p = p.chopped(1); + } + if (p.startsWith(asterisk)) { + this->flags |= RightFilter; + p = p.mid(1); + } + if (p.contains(asterisk)) // '*' only supported at start/end + this->flags = PatternFlags(); + } + + this->category = p.toString(); +} + +int QLoggingRule::pass(QLatin1StringView cat, QtMsgType msgType) const { + // check message type + if (this->messageType > -1 && this->messageType != msgType) return 0; + + if (this->flags == FullText) { + // full match + if (this->category == cat) return (this->enabled ? 1 : -1); + else return 0; + } + + const qsizetype idx = cat.indexOf(this->category); + if (idx >= 0) { + if (this->flags == MidFilter) { + // matches somewhere + return (this->enabled ? 1 : -1); + } else if (this->flags == LeftFilter) { + // matches left + if (idx == 0) return (this->enabled ? 1 : -1); + } else if (this->flags == RightFilter) { + // matches right + if (idx == (cat.size() - this->category.size())) return (this->enabled ? 1 : -1); + } + } + return 0; +} + +} // namespace qt_logging_registry + +} // namespace qs::log diff --git a/src/core/main.cpp b/src/core/main.cpp index 86f4ec8c..23eb2124 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -140,9 +140,17 @@ int qs_main(int argc, char** argv) { ); /// --- - QStringOption logpath; + QStringOption logPath; + QStringOption logFilter; auto* readLog = app.add_subcommand("read-log", "Read a quickshell log file."); - readLog->add_option("path", logpath, "Path to the log file to read")->required(); + readLog->add_option("path", logPath, "Path to the log file to read")->required(); + + readLog->add_option( + "-f,--filter", + logFilter, + "Logging categories to display. (same syntax as QT_LOGGING_RULES)" + ); + readLog->add_flag("--no-color", noColor, "Do not color the log output. (Env:NO_COLOR)"); CLI11_PARSE(app, argc, argv); @@ -153,15 +161,15 @@ int qs_main(int argc, char** argv) { LogManager::init(!noColor, sparseLogsOnly); if (*readLog) { - auto file = QFile(*logpath); + auto file = QFile(*logPath); if (!file.open(QFile::ReadOnly)) { - qCritical() << "Failed to open log for reading:" << *logpath; + qCritical() << "Failed to open log for reading:" << *logPath; return -1; } else { - qInfo() << "Reading log" << *logpath; + qInfo() << "Reading log" << *logPath; } - return qs::log::readEncodedLogs(&file) ? 0 : -1; + return qs::log::readEncodedLogs(&file, *logFilter) ? 0 : -1; } else { // NOLINTBEGIN From 53b8f1ee0b675f50e2cf3b2b9482721a54b4e588 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 9 Aug 2024 23:58:30 -0700 Subject: [PATCH 120/305] core/log: add read-log --no-time --- src/core/logging.cpp | 4 ++-- src/core/logging.hpp | 2 +- src/core/main.cpp | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 7a8426b8..649eb947 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -641,7 +641,7 @@ bool EncodedLogReader::registerCategory() { return true; } -bool readEncodedLogs(QIODevice* device, const QString& rulespec) { +bool readEncodedLogs(QIODevice* device, bool timestamps, const QString& rulespec) { using namespace qt_logging_registry; QList rules; @@ -700,7 +700,7 @@ bool readEncodedLogs(QIODevice* device, const QString& rulespec) { } if (filter.shouldDisplay(message.type)) { - LogMessage::formatMessage(stream, message, color, true); + LogMessage::formatMessage(stream, message, color, timestamps); stream << '\n'; } } diff --git a/src/core/logging.hpp b/src/core/logging.hpp index 5462144f..88fd6716 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -98,7 +98,7 @@ private: LoggingThreadProxy threadProxy; }; -bool readEncodedLogs(QIODevice* device, const QString& rulespec); +bool readEncodedLogs(QIODevice* device, bool timestamps, const QString& rulespec); } // namespace qs::log diff --git a/src/core/main.cpp b/src/core/main.cpp index 23eb2124..c785f2f0 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -142,6 +142,8 @@ int qs_main(int argc, char** argv) { /// --- QStringOption logPath; QStringOption logFilter; + auto logNoTime = false; + auto* readLog = app.add_subcommand("read-log", "Read a quickshell log file."); readLog->add_option("path", logPath, "Path to the log file to read")->required(); @@ -151,6 +153,7 @@ int qs_main(int argc, char** argv) { "Logging categories to display. (same syntax as QT_LOGGING_RULES)" ); + readLog->add_flag("--no-time", logNoTime, "Do not print timestamps of log messages."); readLog->add_flag("--no-color", noColor, "Do not color the log output. (Env:NO_COLOR)"); CLI11_PARSE(app, argc, argv); @@ -169,7 +172,7 @@ int qs_main(int argc, char** argv) { qInfo() << "Reading log" << *logPath; } - return qs::log::readEncodedLogs(&file, *logFilter) ? 0 : -1; + return qs::log::readEncodedLogs(&file, !logNoTime, *logFilter) ? 0 : -1; } else { // NOLINTBEGIN From 5f4d7f89db1ab95bdb000689a756068e918cb5b5 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 10 Aug 2024 01:35:52 -0700 Subject: [PATCH 121/305] core/log: fix log corruption with messages at 29 second deltas 29, or 0x1d is used as a marker to mean the log level and time delta cannot fit in a single byte, and the time delta will be a varint following the current byte. Prior to this commit, 29 second deltas would be written as 0x1d instead of 0x1d1d, which the parser interpreted as a hint to read the next byte, causing the parser to become offset by one byte and all following logs to be potentially corrupt. --- src/core/logging.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 649eb947..01f9a90b 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -469,7 +469,7 @@ bool EncodedLogWriter::write(const LogMessage& message) { quint8 field = compressedTypeOf(message.type); auto secondDelta = this->lastMessageTime.secsTo(message.time); - if (secondDelta > 29) { + if (secondDelta >= 29) { // 0x1d = followed by delta int // 0x1e = followed by epoch delta int field |= (secondDelta < 0xffff ? 0x1d : 0x1e) << 3; From 14852700cbb3a5c15c3918ec91f14aad2456a3e5 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 10 Aug 2024 01:40:51 -0700 Subject: [PATCH 122/305] core/log: ensure malformed logs cannot overflow ring buffer --- src/core/logging.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 01f9a90b..99899aa7 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -469,7 +469,7 @@ bool EncodedLogWriter::write(const LogMessage& message) { quint8 field = compressedTypeOf(message.type); auto secondDelta = this->lastMessageTime.secsTo(message.time); - if (secondDelta >= 29) { + if (secondDelta >= 0x1d) { // 0x1d = followed by delta int // 0x1e = followed by epoch delta int field |= (secondDelta < 0xffff ? 0x1d : 0x1e) << 3; @@ -521,6 +521,7 @@ start: if (!this->readVarInt(&secondDelta)) return false; } + if (index < 0 || index >= this->recentMessages.size()) return false; *slot = this->recentMessages.at(index); this->lastMessageTime = this->lastMessageTime.addSecs(static_cast(secondDelta)); slot->time = this->lastMessageTime; From 683d92a05f600c347433108f713c349507dccc18 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 10 Aug 2024 01:59:40 -0700 Subject: [PATCH 123/305] core/command: add --version --- src/core/main.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core/main.cpp b/src/core/main.cpp index c785f2f0..e48213ac 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -129,9 +129,10 @@ int qs_main(int argc, char** argv) { ->needs(debugPortArg); /// --- - bool sparseLogsOnly = false; + auto sparseLogsOnly = false; app.add_flag("--info", printInfo, "Print information about the shell")->excludes(debugPortArg); app.add_flag("--no-color", noColor, "Do not color the log output. (Env:NO_COLOR)"); + auto* printVersion = app.add_flag("-V,--version", "Print quickshell's version, then exit."); app.add_flag( "--no-detailed-logs", @@ -163,7 +164,10 @@ int qs_main(int argc, char** argv) { // Start log manager - has to happen with an active event loop or offthread can't be started. LogManager::init(!noColor, sparseLogsOnly); - if (*readLog) { + if (*printVersion) { + std::cout << "quickshell pre-release, revision: " << GIT_REVISION << std::endl; + return 0; + } if (*readLog) { auto file = QFile(*logPath); if (!file.open(QFile::ReadOnly)) { qCritical() << "Failed to open log for reading:" << *logPath; From 23cd6cd9e1f85e03eac6a3a9ea6801b6d83805f2 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 15 Aug 2024 17:14:00 -0700 Subject: [PATCH 124/305] x11/panelwindow: set _NET_WM_DESKTOP to stay on all desktops --- src/x11/panel_window.cpp | 20 +++++++++++++++++++- src/x11/util.cpp | 2 ++ src/x11/util.hpp | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index 3a65ec92..fc043f1d 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -185,7 +185,25 @@ void XPanelWindow::setFocusable(bool focusable) { emit this->focusableChanged(); } -void XPanelWindow::xInit() { this->updateDimensions(); } +void XPanelWindow::xInit() { + if (this->window == nullptr || this->window->handle() == nullptr) return; + this->updateDimensions(); + + auto* conn = x11Connection(); + + // Stick to every workspace + auto desktop = 0xffffffff; + xcb_change_property( + conn, + XCB_PROP_MODE_REPLACE, + this->window->winId(), + XAtom::_NET_WM_DESKTOP.atom(), + XCB_ATOM_CARDINAL, + 32, + 1, + &desktop + ); +} void XPanelWindow::connectScreen() { if (this->mTrackedScreen != nullptr) { diff --git a/src/x11/util.cpp b/src/x11/util.cpp index 8760ea30..cdb86e01 100644 --- a/src/x11/util.cpp +++ b/src/x11/util.cpp @@ -23,11 +23,13 @@ xcb_connection_t* x11Connection() { // NOLINTBEGIN XAtom XAtom::_NET_WM_STRUT {}; XAtom XAtom::_NET_WM_STRUT_PARTIAL {}; +XAtom XAtom::_NET_WM_DESKTOP {}; // NOLINTEND void XAtom::initAtoms() { _NET_WM_STRUT.init("_NET_WM_STRUT"); _NET_WM_STRUT_PARTIAL.init("_NET_WM_STRUT_PARTIAL"); + _NET_WM_DESKTOP.init("_NET_WM_DESKTOP"); } void XAtom::init(const QByteArray& name) { diff --git a/src/x11/util.hpp b/src/x11/util.hpp index da3f718a..579cbc78 100644 --- a/src/x11/util.hpp +++ b/src/x11/util.hpp @@ -15,6 +15,7 @@ public: // NOLINTBEGIN static XAtom _NET_WM_STRUT; static XAtom _NET_WM_STRUT_PARTIAL; + static XAtom _NET_WM_DESKTOP; // NOLINTEND static void initAtoms(); From 22c397bbb0f2b287a2105b9a0cdd086088227945 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 15 Aug 2024 17:15:30 -0700 Subject: [PATCH 125/305] x11/panelwindow: respect exclusive zones per layer --- src/x11/panel_window.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index fc043f1d..fab50abe 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -232,6 +232,9 @@ void XPanelWindow::updateDimensions() { // we only care about windows below us if (panel == this) break; + // we only care about windows in the same layer + if (panel->mAboveWindows != this->mAboveWindows) continue; + int side = -1; quint32 exclusiveZone = 0; panel->getExclusion(side, exclusiveZone); From 815867c178688f9efd669422e4c6bcd5f796f774 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 15 Aug 2024 18:46:06 -0700 Subject: [PATCH 126/305] x11/panelwindow: fix multi monitor Previously attached panels to the virtual desktop geometry instead of the screen geometry. --- src/core/proxywindow.cpp | 6 +++++- src/x11/panel_window.cpp | 43 ++++++++++++++++++++++++++-------------- src/x11/panel_window.hpp | 4 +++- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index 05fbff0a..c4b72a04 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -140,6 +140,8 @@ void ProxyWindowBase::completeWindow() { if (this->mScreen != nullptr && this->window->screen() != this->mScreen) { if (this->window->isVisible()) this->window->setVisible(false); this->window->setScreen(this->mScreen); + } else if (this->mScreen == nullptr) { + this->mScreen = this->window->screen(); } this->setWidth(this->mWidth); @@ -259,7 +261,6 @@ void ProxyWindowBase::setScreen(QuickshellScreenInfo* screen) { } if (this->window == nullptr) { - this->mScreen = qscreen; emit this->screenChanged(); } else { auto reshow = this->isVisibleDirect(); @@ -267,6 +268,9 @@ void ProxyWindowBase::setScreen(QuickshellScreenInfo* screen) { if (this->window != nullptr) this->window->setScreen(qscreen); if (reshow) this->setVisibleDirect(true); } + + if (qscreen) this->mScreen = qscreen; + else this->mScreen = this->window->screen(); } void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; } diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index fab50abe..b092b7e5 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -88,10 +88,13 @@ void XPanelWindow::connectWindow() { this->window->installEventFilter(&this->eventFilter); this->connectScreen(); - // clang-format off - QObject::connect(this->window, &QQuickWindow::screenChanged, this, &XPanelWindow::connectScreen); - QObject::connect(this->window, &QQuickWindow::visibleChanged, this, &XPanelWindow::updatePanelStack); - // clang-format on + + QObject::connect( + this->window, + &QQuickWindow::visibleChanged, + this, + &XPanelWindow::updatePanelStack + ); // qt overwrites _NET_WM_STATE, so we have to use the qt api // QXcbWindow::WindowType::Dock in qplatformwindow_p.h @@ -129,6 +132,11 @@ void XPanelWindow::setHeight(qint32 height) { } } +void XPanelWindow::setScreen(QuickshellScreenInfo* screen) { + this->ProxyWindowBase::setScreen(screen); + this->connectScreen(); +} + Anchors XPanelWindow::anchors() const { return this->mAnchors; } void XPanelWindow::setAnchors(Anchors anchors) { @@ -194,14 +202,14 @@ void XPanelWindow::xInit() { // Stick to every workspace auto desktop = 0xffffffff; xcb_change_property( - conn, - XCB_PROP_MODE_REPLACE, - this->window->winId(), - XAtom::_NET_WM_DESKTOP.atom(), - XCB_ATOM_CARDINAL, - 32, - 1, - &desktop + conn, + XCB_PROP_MODE_REPLACE, + this->window->winId(), + XAtom::_NET_WM_DESKTOP.atom(), + XCB_ATOM_CARDINAL, + 32, + 1, + &desktop ); } @@ -210,7 +218,7 @@ void XPanelWindow::connectScreen() { QObject::disconnect(this->mTrackedScreen, nullptr, this, nullptr); } - this->mTrackedScreen = this->window->screen(); + this->mTrackedScreen = this->mScreen; if (this->mTrackedScreen != nullptr) { QObject::connect( @@ -220,12 +228,15 @@ void XPanelWindow::connectScreen() { &XPanelWindow::updateDimensions ); } + + this->updateDimensions(); } void XPanelWindow::updateDimensions() { - if (this->window == nullptr || this->window->handle() == nullptr) return; + if (this->window == nullptr || this->window->handle() == nullptr || this->mScreen == nullptr) + return; - auto screenGeometry = this->window->screen()->virtualGeometry(); + auto screenGeometry = this->mScreen->geometry(); if (this->mExclusionMode != ExclusionMode::Ignore) { for (auto* panel: XPanelStack::instance()->panels(this)) { @@ -235,6 +246,8 @@ void XPanelWindow::updateDimensions() { // we only care about windows in the same layer if (panel->mAboveWindows != this->mAboveWindows) continue; + if (panel->mScreen != this->mScreen) continue; + int side = -1; quint32 exclusiveZone = 0; panel->getExclusion(side, exclusiveZone); diff --git a/src/x11/panel_window.hpp b/src/x11/panel_window.hpp index db8de7d2..4cdfaaa3 100644 --- a/src/x11/panel_window.hpp +++ b/src/x11/panel_window.hpp @@ -49,6 +49,8 @@ public: void setWidth(qint32 width) override; void setHeight(qint32 height) override; + void setScreen(QuickshellScreenInfo* screen) override; + [[nodiscard]] Anchors anchors() const; void setAnchors(Anchors anchors); @@ -77,11 +79,11 @@ signals: private slots: void xInit(); - void connectScreen(); void updateDimensions(); void updatePanelStack(); private: + void connectScreen(); void getExclusion(int& side, quint32& exclusiveZone); void updateStrut(); void updateAboveWindows(); From 1d2bf5d7b405e068627aa7ec6f20de1dd4045f85 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 16 Aug 2024 02:32:46 -0700 Subject: [PATCH 127/305] core/clock: fix behavior with odd time changes --- src/core/clock.cpp | 48 +++++++++++++++++++++++++--------------------- src/core/clock.hpp | 6 +++--- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/core/clock.cpp b/src/core/clock.cpp index ee396ac8..75785223 100644 --- a/src/core/clock.cpp +++ b/src/core/clock.cpp @@ -31,20 +31,25 @@ void SystemClock::setPrecision(SystemClock::Enum precision) { } void SystemClock::onTimeout() { - this->setTime(this->nextTime); - this->schedule(this->nextTime); + this->setTime(this->targetTime); + this->schedule(this->targetTime); } void SystemClock::update() { if (this->mEnabled) { - this->setTime(QTime::currentTime()); - this->schedule(QTime::currentTime()); + this->setTime(QDateTime::fromMSecsSinceEpoch(0)); + this->schedule(QDateTime::fromMSecsSinceEpoch(0)); } else { this->timer.stop(); } } -void SystemClock::setTime(QTime time) { +void SystemClock::setTime(const QDateTime& targetTime) { + auto currentTime = QDateTime::currentDateTime(); + auto offset = currentTime.msecsTo(targetTime); + auto dtime = offset > -500 && offset < 500 ? targetTime : currentTime; + auto time = dtime.time(); + auto secondPrecision = this->mPrecision >= SystemClock::Seconds; auto secondChanged = this->setSeconds(secondPrecision ? time.second() : 0); @@ -57,34 +62,33 @@ void SystemClock::setTime(QTime time) { DropEmitter::call(secondChanged, minuteChanged, hourChanged); } -void SystemClock::schedule(QTime floor) { +void SystemClock::schedule(const QDateTime& targetTime) { auto secondPrecision = this->mPrecision >= SystemClock::Seconds; auto minutePrecision = this->mPrecision >= SystemClock::Minutes; auto hourPrecision = this->mPrecision >= SystemClock::Hours; -setnext: - auto nextTime = QTime( - hourPrecision ? floor.hour() : 0, - minutePrecision ? floor.minute() : 0, - secondPrecision ? floor.second() : 0 + auto currentTime = QDateTime::currentDateTime(); + + auto offset = currentTime.msecsTo(targetTime); + + // timer skew + auto nextTime = offset > 0 && offset < 500 ? targetTime : currentTime; + + auto baseTimeT = nextTime.time(); + nextTime.setTime( + {hourPrecision ? baseTimeT.hour() : 0, + minutePrecision ? baseTimeT.minute() : 0, + secondPrecision ? baseTimeT.second() : 0} ); if (secondPrecision) nextTime = nextTime.addSecs(1); else if (minutePrecision) nextTime = nextTime.addSecs(60); else if (hourPrecision) nextTime = nextTime.addSecs(3600); - auto delay = QTime::currentTime().msecsTo(nextTime); + auto delay = currentTime.msecsTo(nextTime); - // If off by more than 2 hours we likely wrapped around midnight. - if (delay < -7200000) delay += 86400000; - else if (delay < 0) { - // Otherwise its just the timer being unstable. - floor = QTime::currentTime(); - goto setnext; - } - - this->timer.start(delay); - this->nextTime = nextTime; + this->timer.start(static_cast(delay)); + this->targetTime = nextTime; } DEFINE_MEMBER_GETSET(SystemClock, hours, setHours); diff --git a/src/core/clock.hpp b/src/core/clock.hpp index 7f0dbf74..575f031e 100644 --- a/src/core/clock.hpp +++ b/src/core/clock.hpp @@ -59,11 +59,11 @@ private: quint32 mMinutes = 0; quint32 mSeconds = 0; QTimer timer; - QTime nextTime; + QDateTime targetTime; void update(); - void setTime(QTime time); - void schedule(QTime floor); + void setTime(const QDateTime& targetTime); + void schedule(const QDateTime& targetTime); DECLARE_PRIVATE_MEMBER(SystemClock, hours, setHours, mHours, hoursChanged); DECLARE_PRIVATE_MEMBER(SystemClock, minutes, setMinutes, mMinutes, minutesChanged); From f89c504b558d3280101bac6be4347f83df9d241c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 16 Aug 2024 16:47:50 -0700 Subject: [PATCH 128/305] core/menu: opening platform menus w/o QApplication no longer crashes An error is displayed instead. --- src/core/platformmenu.cpp | 22 ++++++++++++++++++++-- src/core/qsmenuanchor.cpp | 10 ++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/core/platformmenu.cpp b/src/core/platformmenu.cpp index 0f416a20..37eb4654 100644 --- a/src/core/platformmenu.cpp +++ b/src/core/platformmenu.cpp @@ -4,7 +4,9 @@ #include #include +#include #include +#include #include #include #include @@ -74,7 +76,13 @@ void PlatformMenuEntry::registerCreationHook(std::functionqmenu == nullptr) { + if (qobject_cast(QCoreApplication::instance()) == nullptr) { + qCritical() << "Cannot display PlatformMenuEntry as quickshell was not started in " + "QApplication mode."; + qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your " + "root QML file and restart quickshell."; + return false; + } else if (this->qmenu == nullptr) { qCritical() << "Cannot display PlatformMenuEntry as it is not a menu."; return false; } else if (parentWindow == nullptr) { @@ -113,7 +121,13 @@ bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relati } bool PlatformMenuEntry::display(PopupAnchor* anchor) { - if (!anchor->backingWindow() || !anchor->backingWindow()->isVisible()) { + if (qobject_cast(QCoreApplication::instance()) == nullptr) { + qCritical() << "Cannot display PlatformMenuEntry as quickshell was not started in " + "QApplication mode."; + qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your " + "root QML file and restart quickshell."; + return false; + } else if (!anchor->backingWindow() || !anchor->backingWindow()->isVisible()) { qCritical() << "Cannot display PlatformMenuEntry on anchor without visible window."; return false; } @@ -140,6 +154,10 @@ bool PlatformMenuEntry::display(PopupAnchor* anchor) { } void PlatformMenuEntry::relayout() { + if (qobject_cast(QCoreApplication::instance()) == nullptr) { + return; + } + if (this->menu->hasChildren()) { delete this->qaction; this->qaction = nullptr; diff --git a/src/core/qsmenuanchor.cpp b/src/core/qsmenuanchor.cpp index e6af7865..8c9b5250 100644 --- a/src/core/qsmenuanchor.cpp +++ b/src/core/qsmenuanchor.cpp @@ -1,5 +1,7 @@ #include "qsmenuanchor.hpp" +#include +#include #include #include #include @@ -15,6 +17,14 @@ namespace qs::menu { QsMenuAnchor::~QsMenuAnchor() { this->onClosed(); } void QsMenuAnchor::open() { + if (qobject_cast(QCoreApplication::instance()) == nullptr) { + qCritical() << "Cannot call QsMenuAnchor.open() as quickshell was not started in " + "QApplication mode."; + qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your " + "root QML file and restart quickshell."; + return; + } + if (this->mOpen) { qCritical() << "Cannot call QsMenuAnchor.open() as it is already open."; return; From e223408143a80db138d69d7af888770bc17ea155 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 18 Aug 2024 13:07:52 -0700 Subject: [PATCH 129/305] service/mpris: fix display position when paused --- src/services/mpris/player.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index d17975b0..48bd4768 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -217,6 +217,7 @@ void MprisPlayer::setPosition(qreal position) { void MprisPlayer::onPositionChanged() { const bool firstChange = !this->lastPositionTimestamp.isValid(); this->lastPositionTimestamp = QDateTime::currentDateTimeUtc(); + this->pausedTime = this->lastPositionTimestamp; emit this->positionChanged(); if (firstChange) emit this->positionSupportedChanged(); } From 5a038f085dce538406b8912135a3215662f01090 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 18 Aug 2024 13:41:16 -0700 Subject: [PATCH 130/305] service/mpris: support trackids in object path form Chromium reports trackids as object paths, which caused us to fall back to Seek, which is also entirely broken on chromium. --- src/services/mpris/player.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 48bd4768..b2d4af60 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -271,16 +271,21 @@ void MprisPlayer::onMetadataChanged() { auto trackChanged = false; + QString trackId; auto trackidVariant = this->pMetadata.get().value("mpris:trackid"); - if (trackidVariant.isValid() && trackidVariant.canConvert()) { - auto trackId = trackidVariant.toString(); - - if (trackId != this->mTrackId) { - this->mTrackId = trackId; - trackChanged = true; + if (trackidVariant.isValid()) { + if (trackidVariant.canConvert()) { + trackId = trackidVariant.toString(); + } else if (trackidVariant.canConvert()) { + trackId = trackidVariant.value().path(); } } + if (trackId != this->mTrackId) { + this->mTrackId = trackId; + trackChanged = true; + } + // Helps to catch players without trackid. auto urlVariant = this->pMetadata.get().value("xesam:url"); if (urlVariant.isValid() && urlVariant.canConvert()) { From 5040f3796cc633ed366a965715376537376e3df9 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 18 Aug 2024 19:54:36 -0700 Subject: [PATCH 131/305] core/reloader: delay post-reload reload hooks Ensures onReload runs after Component.onCompleted. --- src/core/reload.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/reload.cpp b/src/core/reload.cpp index c62706ab..10627c0a 100644 --- a/src/core/reload.cpp +++ b/src/core/reload.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "generation.hpp" @@ -12,8 +13,10 @@ void Reloadable::componentComplete() { if (this->engineGeneration != nullptr) { // When called this way there is no chance a reload will have old data, // but this will at least help prevent weird behaviors due to never getting a reload. - if (this->engineGeneration->reloadComplete) this->reload(); - else { + if (this->engineGeneration->reloadComplete) { + // Delayed due to Component.onCompleted running after QQmlParserStatus::componentComplete. + QTimer::singleShot(0, this, &Reloadable::onReloadFinished); + } else { QObject::connect( this->engineGeneration, &EngineGeneration::reloadFinished, From fe1d15e8f68e66e5f164dea930f15f35402ea9c0 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 20 Aug 2024 00:41:20 -0700 Subject: [PATCH 132/305] crash: add crash reporter --- .github/ISSUE_TEMPLATE/config.yml | 1 + .github/ISSUE_TEMPLATE/crash.yml | 72 +++ CMakeLists.txt | 2 + default.nix | 4 + src/CMakeLists.txt | 4 + src/core/CMakeLists.txt | 12 +- src/core/build.hpp.in | 6 + src/core/common.cpp | 9 + src/core/common.hpp | 11 + src/core/crashinfo.cpp | 19 + src/core/crashinfo.hpp | 26 + src/core/logging.cpp | 13 +- src/core/main.cpp | 730 +++++++++++++++----------- src/core/paths.cpp | 13 +- src/core/paths.hpp | 2 + src/crash/CMakeLists.txt | 16 + src/crash/handler.cpp | 180 +++++++ src/crash/handler.hpp | 23 + src/crash/interface.cpp | 97 ++++ src/crash/interface.hpp | 17 + src/crash/main.cpp | 165 ++++++ src/crash/main.hpp | 3 + src/services/status_notifier/host.cpp | 8 +- 23 files changed, 1118 insertions(+), 315 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/crash.yml create mode 100644 src/core/build.hpp.in create mode 100644 src/core/common.cpp create mode 100644 src/core/common.hpp create mode 100644 src/core/crashinfo.cpp create mode 100644 src/core/crashinfo.hpp create mode 100644 src/crash/CMakeLists.txt create mode 100644 src/crash/handler.cpp create mode 100644 src/crash/handler.hpp create mode 100644 src/crash/interface.cpp create mode 100644 src/crash/interface.hpp create mode 100644 src/crash/main.cpp create mode 100644 src/crash/main.hpp diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..0086358d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml new file mode 100644 index 00000000..13dcd33d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -0,0 +1,72 @@ +name: Crash Report +description: Quickshell has crashed +labels: ["bug", "crash"] +body: + - type: textarea + id: crashinfo + attributes: + label: General crash information + description: | + Paste the contents of the `info.txt` file in your crash folder here. + value: "
General information + + + ``` + + + + ``` + + +
" + validations: + required: true + - type: textarea + id: userinfo + attributes: + label: What caused the crash + description: | + Any information likely to help debug the crash. What were you doing when the crash occurred, + what changes did you make, can you get it to happen again? + - type: textarea + id: dump + attributes: + label: Minidump + description: | + Attach `minidump.dmp` here. If it is too big to upload, compress it. + + You may skip this step if quickshell crashed while processing a password + or other sensitive information. If you skipped it write why instead. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Log file + description: | + Attach `log.qslog` here. If it is too big to upload, compress it. + + You can preview the log if you'd like using `quickshell read-log `. + validations: + required: true + - type: textarea + id: config + attributes: + label: Configuration + description: | + Attach your configuration here, preferrably in full (not just one file). + Compress it into a zip, tar, etc. + + This will help us reproduce the crash ourselves. + - type: textarea + id: bt + attributes: + label: Backtrace + description: | + If you have gdb installed and use systemd, or otherwise know how to get a backtrace, + we would appreciate one. (You may have gdb installed without knowing it) + + 1. Run `coredumpctl debug ` where `pid` is the number shown after "Crashed process ID" + in the crash reporter. + 2. Once it loads, type `bt -full` (then enter) + 3. Copy the output and attach it as a file or in a spoiler. diff --git a/CMakeLists.txt b/CMakeLists.txt index b55c751f..e3c01592 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ option(BUILD_TESTING "Build tests" OFF) option(ASAN "Enable ASAN" OFF) # note: better output with gcc than clang option(FRAME_POINTERS "Always keep frame pointers" ${ASAN}) +option(CRASH_REPORTER "Enable the crash reporter" ON) option(USE_JEMALLOC "Use jemalloc over the system malloc implementation" ON) option(SOCKETS "Enable unix socket support" ON) option(WAYLAND "Enable wayland support" ON) @@ -29,6 +30,7 @@ option(SERVICE_UPOWER "UPower service" ON) option(SERVICE_NOTIFICATIONS "Notification server" ON) message(STATUS "Quickshell configuration") +message(STATUS " Crash reporter: ${CRASH_REPORTER}") message(STATUS " Jemalloc: ${USE_JEMALLOC}") message(STATUS " Build tests: ${BUILD_TESTING}") message(STATUS " Sockets: ${SOCKETS}") diff --git a/default.nix b/default.nix index 34cc0f4b..1ddb99b5 100644 --- a/default.nix +++ b/default.nix @@ -9,6 +9,7 @@ ninja, qt6, cli11, + breakpad, jemalloc, wayland, wayland-protocols, @@ -28,6 +29,7 @@ else "unknown"), debug ? false, + withCrashReporter ? true, withJemalloc ? true, # masks heap fragmentation withQtSvg ? true, withWayland ? true, @@ -55,6 +57,7 @@ qt6.qtdeclarative cli11 ] + ++ (lib.optional withCrashReporter breakpad) ++ (lib.optional withJemalloc jemalloc) ++ (lib.optional withQtSvg qt6.qtsvg) ++ (lib.optionals withWayland [ qt6.qtwayland wayland ]) @@ -67,6 +70,7 @@ cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; cmakeFlags = [ "-DGIT_REVISION=${gitRev}" ] + ++ lib.optional (!withCrashReporter) "-DCRASH_REPORTER=OFF" ++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF" ++ lib.optional (!withWayland) "-DWAYLAND=OFF" ++ lib.optional (!withPipewire) "-DSERVICE_PIPEWIRE=OFF" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index be3adaf8..42954775 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,10 @@ install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) add_subdirectory(core) add_subdirectory(io) +if (CRASH_REPORTER) + add_subdirectory(crash) +endif() + if (DBUS) add_subdirectory(dbus) endif() diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index fb39287c..0d6f7211 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -41,9 +41,19 @@ qt_add_library(quickshell-core STATIC clock.cpp logging.cpp paths.cpp + crashinfo.cpp + common.cpp ) -set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") +if (CRASH_REPORTER) + set(CRASH_REPORTER_DEF 1) +endif() + +add_library(quickshell-build INTERFACE) +configure_file(build.hpp.in build.hpp) +target_include_directories(quickshell-build INTERFACE ${CMAKE_CURRENT_BINARY_DIR}) +target_link_libraries(quickshell-core PRIVATE quickshell-build) + qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1) target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} Qt6::QuickPrivate CLI11::CLI11) diff --git a/src/core/build.hpp.in b/src/core/build.hpp.in new file mode 100644 index 00000000..ecf5dfc4 --- /dev/null +++ b/src/core/build.hpp.in @@ -0,0 +1,6 @@ +#pragma once + +// NOLINTBEGIN +#define GIT_REVISION "@GIT_REVISION@" +#define CRASH_REPORTER @CRASH_REPORTER_DEF@ +// NOLINTEND diff --git a/src/core/common.cpp b/src/core/common.cpp new file mode 100644 index 00000000..d09661f1 --- /dev/null +++ b/src/core/common.cpp @@ -0,0 +1,9 @@ +#include "common.hpp" + +#include + +namespace qs { + +const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime(); + +} diff --git a/src/core/common.hpp b/src/core/common.hpp new file mode 100644 index 00000000..36094f89 --- /dev/null +++ b/src/core/common.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace qs { + +struct Common { + static const QDateTime LAUNCH_TIME; +}; + +} // namespace qs diff --git a/src/core/crashinfo.cpp b/src/core/crashinfo.cpp new file mode 100644 index 00000000..f441530f --- /dev/null +++ b/src/core/crashinfo.cpp @@ -0,0 +1,19 @@ +#include "crashinfo.hpp" + +#include + +QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) { + stream << info.configPath << info.shellId << info.launchTime << info.noColor; + return stream; +} + +QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) { + stream >> info.configPath >> info.shellId >> info.launchTime >> info.noColor; + return stream; +} + +namespace qs::crash { + +CrashInfo CrashInfo::INSTANCE = {}; // NOLINT + +} diff --git a/src/core/crashinfo.hpp b/src/core/crashinfo.hpp new file mode 100644 index 00000000..a867563f --- /dev/null +++ b/src/core/crashinfo.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +struct InstanceInfo { + QString configPath; + QString shellId; + QString initialWorkdir; + QDateTime launchTime; + bool noColor = false; + bool sparseLogsOnly = false; +}; + +QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info); +QDataStream& operator>>(QDataStream& stream, InstanceInfo& info); + +namespace qs::crash { + +struct CrashInfo { + int logFd = -1; + + static CrashInfo INSTANCE; // NOLINT +}; + +} // namespace qs::crash diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 99899aa7..887e145f 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -24,6 +24,7 @@ #include #include +#include "crashinfo.hpp" #include "logging_p.hpp" #include "logging_qtprivate.cpp" // NOLINT #include "paths.hpp" @@ -198,14 +199,16 @@ void ThreadLogging::init() { if (logMfd != -1) { this->file = new QFile(); - this->file->open(logMfd, QFile::WriteOnly, QFile::AutoCloseHandle); + this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle); this->fileStream.setDevice(this->file); } if (dlogMfd != -1) { + crash::CrashInfo::INSTANCE.logFd = dlogMfd; + this->detailedFile = new QFile(); // buffered by WriteBuffer - this->detailedFile->open(dlogMfd, QFile::WriteOnly | QFile::Unbuffered, QFile::AutoCloseHandle); + this->detailedFile->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle); this->detailedWriter.setDevice(this->detailedFile); if (!this->detailedWriter.writeHeader()) { @@ -245,7 +248,7 @@ void ThreadLogging::initFs() { auto* file = new QFile(path); auto* detailedFile = new QFile(detailedPath); - if (!file->open(QFile::WriteOnly | QFile::Truncate)) { + if (!file->open(QFile::ReadWrite | QFile::Truncate)) { qCCritical(logLogging ) << "Could not start filesystem logger as the log file could not be created:" << path; @@ -256,7 +259,7 @@ void ThreadLogging::initFs() { } // buffered by WriteBuffer - if (!detailedFile->open(QFile::WriteOnly | QFile::Truncate | QFile::Unbuffered)) { + if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) { qCCritical(logLogging ) << "Could not start detailed filesystem logger as the log file could not be created:" << detailedPath; @@ -287,6 +290,8 @@ void ThreadLogging::initFs() { sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size()); } + crash::CrashInfo::INSTANCE.logFd = detailedFile->handle(); + this->detailedFile = detailedFile; this->detailedWriter.setDevice(detailedFile); diff --git a/src/core/main.cpp b/src/core/main.cpp index e48213ac..25257b5f 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -1,13 +1,17 @@ #include "main.hpp" +#include #include #include #include #include // NOLINT: Need to include this for impls of some CLI11 classes +#include #include #include #include #include +#include +#include #include #include #include @@ -24,162 +28,164 @@ #include #include +#include "build.hpp" +#include "common.hpp" +#include "crashinfo.hpp" #include "logging.hpp" #include "paths.hpp" #include "plugin.hpp" #include "rootwrapper.hpp" +#if CRASH_REPORTER +#include "../crash/handler.hpp" +#include "../crash/main.hpp" +#endif -int qs_main(int argc, char** argv) { +struct CommandInfo { + QString configPath; + QString manifestPath; + QString configName; + QString& initialWorkdir; + int& debugPort; + bool& waitForDebug; + bool& printInfo; + bool& noColor; + bool& sparseLogsOnly; +}; - auto qArgC = 1; - auto* qArgV = argv; +void processCommand(int argc, char** argv, CommandInfo& info) { + auto app = CLI::App(""); - auto noColor = !qEnvironmentVariableIsEmpty("NO_COLOR"); + class QStringOption { + public: + QStringOption() = default; + QStringOption& operator=(const std::string& str) { + this->str = QString::fromStdString(str); + return *this; + } - QString workingDirectory; - QString configFilePath; - QString shellId; - auto printInfo = false; + QString& operator*() { return this->str; } - auto debugPort = -1; - auto waitForDebug = false; + private: + QString str; + }; - auto useQApplication = false; - auto nativeTextRendering = false; - auto desktopSettingsAware = true; - QHash envOverrides; + class QStringRefOption { + public: + QStringRefOption(QString* str): str(str) {} + QStringRefOption& operator=(const std::string& str) { + *this->str = QString::fromStdString(str); + return *this; + } - { - auto app = CLI::App(""); + private: + QString* str; + }; - class QStringOption { - public: - QStringOption() = default; - QStringOption& operator=(const std::string& str) { - this->str = QString::fromStdString(str); - return *this; - } + /// --- + QStringRefOption path(&info.configPath); + QStringRefOption manifest(&info.manifestPath); + QStringRefOption config(&info.configName); + QStringRefOption workdirRef(&info.initialWorkdir); - QString& operator*() { return this->str; } + auto* selection = app.add_option_group( + "Config Selection", + "Select a configuration to run (defaults to $XDG_CONFIG_HOME/quickshell/shell.qml)" + ); - private: - QString str; - }; + auto* pathArg = + selection->add_option("-p,--path", path, "Path to a QML file to run. (Env:QS_CONFIG_PATH)"); - class QStringRefOption { - public: - QStringRefOption(QString* str): str(str) {} - QStringRefOption& operator=(const std::string& str) { - *this->str = QString::fromStdString(str); - return *this; - } + auto* mfArg = selection->add_option( + "-m,--manifest", + manifest, + "Path to a manifest containing configurations. (Env:QS_MANIFEST)\n" + "(Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf)" + ); - private: - QString* str; - }; + auto* cfgArg = selection->add_option( + "-c,--config", + config, + "Name of a configuration within a manifest. (Env:QS_CONFIG_NAME)" + ); - /// --- - QStringOption path; - QStringOption manifest; - QStringOption config; - QStringRefOption workdirRef(&workingDirectory); + selection->add_option("-d,--workdir", workdirRef, "Initial working directory."); - auto* selection = app.add_option_group( - "Config Selection", - "Select a configuration to run (defaults to $XDG_CONFIG_HOME/quickshell/shell.qml)" - ); + pathArg->excludes(mfArg, cfgArg); - auto* pathArg = - selection->add_option("-p,--path", path, "Path to a QML file to run. (Env:QS_CONFIG_PATH)"); + /// --- + auto* debug = app.add_option_group("Debugging"); - auto* mfArg = selection->add_option( - "-m,--manifest", - manifest, - "Path to a manifest containing configurations. (Env:QS_MANIFEST)\n" - "(Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf)" - ); + auto* debugPortArg = debug + ->add_option( + "--debugport", + info.debugPort, + "Open the given port for a QML debugger to connect to." + ) + ->check(CLI::Range(0, 65535)); - auto* cfgArg = selection->add_option( - "-c,--config", - config, - "Name of a configuration within a manifest. (Env:QS_CONFIG_NAME)" - ); + debug + ->add_flag( + "--waitfordebug", + info.waitForDebug, + "Wait for a debugger to attach to the given port before launching." + ) + ->needs(debugPortArg); - selection->add_option("-d,--workdir", workdirRef, "Initial working directory."); + /// --- + app.add_flag("--info", info.printInfo, "Print information about the shell") + ->excludes(debugPortArg); + app.add_flag("--no-color", info.noColor, "Do not color the log output. (Env:NO_COLOR)"); + auto* printVersion = app.add_flag("-V,--version", "Print quickshell's version, then exit."); - pathArg->excludes(mfArg, cfgArg); + app.add_flag( + "--no-detailed-logs", + info.sparseLogsOnly, + "Do not enable this unless you know what you are doing." + ); - /// --- - auto* debug = app.add_option_group("Debugging"); + /// --- + QStringOption logPath; + QStringOption logFilter; + auto logNoTime = false; - auto* debugPortArg = debug - ->add_option( - "--debugport", - debugPort, - "Open the given port for a QML debugger to connect to." - ) - ->check(CLI::Range(0, 65535)); + auto* readLog = app.add_subcommand("read-log", "Read a quickshell log file."); + readLog->add_option("path", logPath, "Path to the log file to read")->required(); - debug - ->add_flag( - "--waitfordebug", - waitForDebug, - "Wait for a debugger to attach to the given port before launching." - ) - ->needs(debugPortArg); + readLog->add_option( + "-f,--filter", + logFilter, + "Logging categories to display. (same syntax as QT_LOGGING_RULES)" + ); - /// --- - auto sparseLogsOnly = false; - app.add_flag("--info", printInfo, "Print information about the shell")->excludes(debugPortArg); - app.add_flag("--no-color", noColor, "Do not color the log output. (Env:NO_COLOR)"); - auto* printVersion = app.add_flag("-V,--version", "Print quickshell's version, then exit."); + readLog->add_flag("--no-time", logNoTime, "Do not print timestamps of log messages."); + readLog->add_flag("--no-color", info.noColor, "Do not color the log output. (Env:NO_COLOR)"); - app.add_flag( - "--no-detailed-logs", - sparseLogsOnly, - "Do not enable this unless you know what you are doing." - ); + try { + app.parse(argc, argv); + } catch (const CLI::ParseError& e) { + exit(app.exit(e)); // NOLINT + }; - /// --- - QStringOption logPath; - QStringOption logFilter; - auto logNoTime = false; - - auto* readLog = app.add_subcommand("read-log", "Read a quickshell log file."); - readLog->add_option("path", logPath, "Path to the log file to read")->required(); - - readLog->add_option( - "-f,--filter", - logFilter, - "Logging categories to display. (same syntax as QT_LOGGING_RULES)" - ); - - readLog->add_flag("--no-time", logNoTime, "Do not print timestamps of log messages."); - readLog->add_flag("--no-color", noColor, "Do not color the log output. (Env:NO_COLOR)"); - - CLI11_PARSE(app, argc, argv); - - const auto qApplication = QCoreApplication(qArgC, qArgV); - - // Start log manager - has to happen with an active event loop or offthread can't be started. - LogManager::init(!noColor, sparseLogsOnly); - - if (*printVersion) { - std::cout << "quickshell pre-release, revision: " << GIT_REVISION << std::endl; - return 0; - } if (*readLog) { - auto file = QFile(*logPath); - if (!file.open(QFile::ReadOnly)) { - qCritical() << "Failed to open log for reading:" << *logPath; - return -1; - } else { - qInfo() << "Reading log" << *logPath; - } - - return qs::log::readEncodedLogs(&file, !logNoTime, *logFilter) ? 0 : -1; + if (*printVersion) { + std::cout << "quickshell pre-release, revision: " << GIT_REVISION << std::endl; + exit(0); // NOLINT + } else if (*readLog) { + auto file = QFile(*logPath); + if (!file.open(QFile::ReadOnly)) { + qCritical() << "Failed to open log for reading:" << *logPath; + exit(-1); // NOLINT } else { + qInfo() << "Reading log" << *logPath; + } - // NOLINTBEGIN + exit( // NOLINT + qs::log::readEncodedLogs(&file, !logNoTime, *logFilter) ? 0 : -1 + ); + } +} + +QString commandConfigPath(QString path, QString manifest, QString config, bool printInfo) { + // NOLINTBEGIN #define CHECK(rname, name, level, label, expr) \ QString name = expr; \ if (rname.isEmpty() && !name.isEmpty()) { \ @@ -189,231 +195,341 @@ int qs_main(int argc, char** argv) { } #define OPTSTR(name) (name.isEmpty() ? "(unset)" : name.toStdString()) - // NOLINTEND + // NOLINTEND - QString basePath; - int basePathLevel = 0; - Q_UNUSED(basePathLevel); - { - // NOLINTBEGIN - // clang-format off - CHECK(basePath, envBasePath, 0, foundbase, qEnvironmentVariable("QS_BASE_PATH")); - CHECK(basePath, defaultBasePath, 0, foundbase, QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)).filePath("quickshell")); - // clang-format on - // NOLINTEND + QString basePath; + int basePathLevel = 0; + Q_UNUSED(basePathLevel); + { + // NOLINTBEGIN + // clang-format off + CHECK(basePath, envBasePath, 0, foundbase, qEnvironmentVariable("QS_BASE_PATH")); + CHECK(basePath, defaultBasePath, 0, foundbase, QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)).filePath("quickshell")); + // clang-format on + // NOLINTEND - if (printInfo) { - // clang-format off - std::cout << "Base path: " << OPTSTR(basePath) << "\n"; - std::cout << " - Environment (QS_BASE_PATH): " << OPTSTR(envBasePath) << "\n"; - std::cout << " - Default: " << OPTSTR(defaultBasePath) << "\n"; - // clang-format on - } - } - foundbase:; + if (printInfo) { + // clang-format off + std::cout << "Base path: " << OPTSTR(basePath) << "\n"; + std::cout << " - Environment (QS_BASE_PATH): " << OPTSTR(envBasePath) << "\n"; + std::cout << " - Default: " << OPTSTR(defaultBasePath) << "\n"; + // clang-format on + } + } +foundbase:; - QString configPath; - int configPathLevel = 10; - { - // NOLINTBEGIN - CHECK(configPath, optionConfigPath, 0, foundpath, *path); - CHECK(configPath, envConfigPath, 1, foundpath, qEnvironmentVariable("QS_CONFIG_PATH")); - // NOLINTEND + QString configPath; + int configPathLevel = 10; + { + // NOLINTBEGIN + CHECK(configPath, optionConfigPath, 0, foundpath, path); + CHECK(configPath, envConfigPath, 1, foundpath, qEnvironmentVariable("QS_CONFIG_PATH")); + // NOLINTEND - if (printInfo) { - // clang-format off - std::cout << "\nConfig path: " << OPTSTR(configPath) << "\n"; - std::cout << " - Option: " << OPTSTR(optionConfigPath) << "\n"; - std::cout << " - Environment (QS_CONFIG_PATH): " << OPTSTR(envConfigPath) << "\n"; - // clang-format on - } - } - foundpath:; + if (printInfo) { + // clang-format off + std::cout << "\nConfig path: " << OPTSTR(configPath) << "\n"; + std::cout << " - Option: " << OPTSTR(optionConfigPath) << "\n"; + std::cout << " - Environment (QS_CONFIG_PATH): " << OPTSTR(envConfigPath) << "\n"; + // clang-format on + } + } +foundpath:; - QString manifestPath; - int manifestPathLevel = 10; - { - // NOLINTBEGIN - // clang-format off - CHECK(manifestPath, optionManifestPath, 0, foundmf, *manifest); - CHECK(manifestPath, envManifestPath, 1, foundmf, qEnvironmentVariable("QS_MANIFEST")); - CHECK(manifestPath, defaultManifestPath, 2, foundmf, QDir(basePath).filePath("manifest.conf")); - // clang-format on - // NOLINTEND + QString manifestPath; + int manifestPathLevel = 10; + { + // NOLINTBEGIN + // clang-format off + CHECK(manifestPath, optionManifestPath, 0, foundmf, manifest); + CHECK(manifestPath, envManifestPath, 1, foundmf, qEnvironmentVariable("QS_MANIFEST")); + CHECK(manifestPath, defaultManifestPath, 2, foundmf, QDir(basePath).filePath("manifest.conf")); + // clang-format on + // NOLINTEND - if (printInfo) { - // clang-format off - std::cout << "\nManifest path: " << OPTSTR(manifestPath) << "\n"; - std::cout << " - Option: " << OPTSTR(optionManifestPath) << "\n"; - std::cout << " - Environment (QS_MANIFEST): " << OPTSTR(envManifestPath) << "\n"; - std::cout << " - Default: " << OPTSTR(defaultManifestPath) << "\n"; - // clang-format on - } - } - foundmf:; + if (printInfo) { + // clang-format off + std::cout << "\nManifest path: " << OPTSTR(manifestPath) << "\n"; + std::cout << " - Option: " << OPTSTR(optionManifestPath) << "\n"; + std::cout << " - Environment (QS_MANIFEST): " << OPTSTR(envManifestPath) << "\n"; + std::cout << " - Default: " << OPTSTR(defaultManifestPath) << "\n"; + // clang-format on + } + } +foundmf:; - QString configName; - int configNameLevel = 10; - { - // NOLINTBEGIN - CHECK(configName, optionConfigName, 0, foundname, *config); - CHECK(configName, envConfigName, 1, foundname, qEnvironmentVariable("QS_CONFIG_NAME")); - // NOLINTEND + QString configName; + int configNameLevel = 10; + { + // NOLINTBEGIN + CHECK(configName, optionConfigName, 0, foundname, config); + CHECK(configName, envConfigName, 1, foundname, qEnvironmentVariable("QS_CONFIG_NAME")); + // NOLINTEND - if (printInfo) { - // clang-format off - std::cout << "\nConfig name: " << OPTSTR(configName) << "\n"; - std::cout << " - Option: " << OPTSTR(optionConfigName) << "\n"; - std::cout << " - Environment (QS_CONFIG_NAME): " << OPTSTR(envConfigName) << "\n\n"; - // clang-format on - } - } - foundname:; + if (printInfo) { + // clang-format off + std::cout << "\nConfig name: " << OPTSTR(configName) << "\n"; + std::cout << " - Option: " << OPTSTR(optionConfigName) << "\n"; + std::cout << " - Environment (QS_CONFIG_NAME): " << OPTSTR(envConfigName) << "\n\n"; + // clang-format on + } + } +foundname:; - if (!configPath.isEmpty() && configPathLevel <= configNameLevel) { - configFilePath = configPath; - } else if (!configName.isEmpty()) { - if (!manifestPath.isEmpty()) { - auto file = QFile(manifestPath); - if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { - auto stream = QTextStream(&file); - while (!stream.atEnd()) { - auto line = stream.readLine(); - if (line.trimmed().startsWith("#")) continue; - if (line.trimmed().isEmpty()) continue; + QString configFilePath; - auto split = line.split('='); - if (split.length() != 2) { - qCritical() << "manifest line not in expected format 'name = relativepath':" - << line; - return -1; - } + if (!configPath.isEmpty() && configPathLevel <= configNameLevel) { + configFilePath = configPath; + } else if (!configName.isEmpty()) { + if (!manifestPath.isEmpty()) { + auto file = QFile(manifestPath); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + auto stream = QTextStream(&file); + while (!stream.atEnd()) { + auto line = stream.readLine(); + if (line.trimmed().startsWith("#")) continue; + if (line.trimmed().isEmpty()) continue; - if (split[0].trimmed() == configName) { - configFilePath = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed()); - goto haspath; // NOLINT - } - } + auto split = line.split('='); + if (split.length() != 2) { + qCritical() << "manifest line not in expected format 'name = relativepath':" << line; + exit(-1); // NOLINT + } - qCritical() << "configuration" << configName << "not found in manifest" << manifestPath; - return -1; - } else if (manifestPathLevel < 2) { - qCritical() << "cannot open config manifest at" << manifestPath; - return -1; + if (split[0].trimmed() == configName) { + configFilePath = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed()); + goto foundp; } } - { - auto basePathInfo = QFileInfo(basePath); - if (!basePathInfo.exists()) { - qCritical() << "base path does not exist:" << basePath; - return -1; - } else if (!QFileInfo(basePathInfo.canonicalFilePath()).isDir()) { - qCritical() << "base path is not a directory" << basePath; - return -1; - } + qCritical() << "configuration" << configName << "not found in manifest" << manifestPath; + exit(-1); // NOLINT + } else if (manifestPathLevel < 2) { + qCritical() << "cannot open config manifest at" << manifestPath; + exit(-1); // NOLINT + } + } - auto dir = QDir(basePath); - for (auto& entry: dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) { - if (entry == configName) { - configFilePath = dir.filePath(entry); - goto haspath; // NOLINT - } - } + { + auto basePathInfo = QFileInfo(basePath); + if (!basePathInfo.exists()) { + qCritical() << "base path does not exist:" << basePath; + exit(-1); // NOLINT + } else if (!QFileInfo(basePathInfo.canonicalFilePath()).isDir()) { + qCritical() << "base path is not a directory" << basePath; + exit(-1); // NOLINT + } - qCritical() << "no directory named " << configName << "found in base path" << basePath; - return -1; + auto dir = QDir(basePath); + for (auto& entry: dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) { + if (entry == configName) { + configFilePath = dir.filePath(entry); + goto foundp; } - haspath:; - } else { - configFilePath = basePath; } - auto configFile = QFileInfo(configFilePath); - if (!configFile.exists()) { - qCritical() << "config path does not exist:" << configFilePath; - return -1; - } + qCritical() << "no directory named " << configName << "found in base path" << basePath; + exit(-1); // NOLINT + } + } else { + configFilePath = basePath; + } - if (configFile.isDir()) { - configFilePath = QDir(configFilePath).filePath("shell.qml"); - } +foundp:; + auto configFile = QFileInfo(configFilePath); + if (!configFile.exists()) { + qCritical() << "config path does not exist:" << configFilePath; + exit(-1); // NOLINT + } - configFile = QFileInfo(configFilePath); - if (!configFile.exists()) { - qCritical() << "no shell.qml found in config path:" << configFilePath; - return -1; - } else if (configFile.isDir()) { - qCritical() << "shell.qml is a directory:" << configFilePath; - return -1; - } + if (configFile.isDir()) { + configFilePath = QDir(configFilePath).filePath("shell.qml"); + } - configFilePath = QFileInfo(configFilePath).canonicalFilePath(); - configFile = QFileInfo(configFilePath); - if (!configFile.exists()) { - qCritical() << "config file does not exist:" << configFilePath; - return -1; - } else if (configFile.isDir()) { - qCritical() << "config file is a directory:" << configFilePath; - return -1; - } + configFile = QFileInfo(configFilePath); + if (!configFile.exists()) { + qCritical() << "no shell.qml found in config path:" << configFilePath; + exit(-1); // NOLINT + } else if (configFile.isDir()) { + qCritical() << "shell.qml is a directory:" << configFilePath; + exit(-1); // NOLINT + } + + configFilePath = QFileInfo(configFilePath).canonicalFilePath(); + configFile = QFileInfo(configFilePath); + if (!configFile.exists()) { + qCritical() << "config file does not exist:" << configFilePath; + exit(-1); // NOLINT + } else if (configFile.isDir()) { + qCritical() << "config file is a directory:" << configFilePath; + exit(-1); // NOLINT + } #undef CHECK #undef OPTSTR - shellId = QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + return configFilePath; +} - qInfo() << "Config file path:" << configFilePath; +int qs_main(int argc, char** argv) { +#if CRASH_REPORTER + qsCheckCrash(argc, argv); + auto crashHandler = qs::crash::CrashHandler(); +#endif - if (!QFile(configFilePath).exists()) { - qCritical() << "config file does not exist"; - return -1; + auto qArgC = 1; + auto* qArgV = argv; + + QString configFilePath; + QString initialWorkdir; + QString shellId; + + int debugPort = -1; + bool waitForDebug = false; + bool printInfo = false; + bool noColor = !qEnvironmentVariableIsEmpty("NO_COLOR"); + bool sparseLogsOnly = false; + + auto useQApplication = false; + auto nativeTextRendering = false; + auto desktopSettingsAware = true; + QHash envOverrides; + + { + const auto qApplication = QCoreApplication(qArgC, qArgV); + +#if CRASH_REPORTER + auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD"); + + if (!lastInfoFdStr.isEmpty()) { + auto lastInfoFd = lastInfoFdStr.toInt(); + + QFile file; + file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle); + file.seek(0); + + auto ds = QDataStream(&file); + InstanceInfo info; + ds >> info; + + configFilePath = info.configPath; + initialWorkdir = info.initialWorkdir; + noColor = info.noColor; + sparseLogsOnly = info.sparseLogsOnly; + + LogManager::init(!noColor, sparseLogsOnly); + + qCritical().nospace() << "Quickshell has crashed under pid " + << qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt() + << " (Coredumps will be available under that pid.)"; + + qCritical() << "Further crash information is stored under" + << QsPaths::crashDir(info.shellId, info.launchTime).path(); + + if (info.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) { + qCritical() << "Quickshell crashed within 10 seconds of launching. Not restarting to avoid " + "a crash loop."; + return 0; + } else { + qCritical() << "Quickshell has been restarted."; } - auto file = QFile(configFilePath); - if (!file.open(QFile::ReadOnly | QFile::Text)) { - qCritical() << "could not open config file"; - return -1; - } + crashHandler.init(); + } else +#endif + { - auto stream = QTextStream(&file); - while (!stream.atEnd()) { - auto line = stream.readLine().trimmed(); - if (line.startsWith("//@ pragma ")) { - auto pragma = line.sliced(11).trimmed(); + auto command = CommandInfo { + .initialWorkdir = initialWorkdir, + .debugPort = debugPort, + .waitForDebug = waitForDebug, + .printInfo = printInfo, + .noColor = noColor, + .sparseLogsOnly = sparseLogsOnly, + }; - if (pragma == "UseQApplication") useQApplication = true; - else if (pragma == "NativeTextRendering") nativeTextRendering = true; - else if (pragma == "IgnoreSystemSettings") desktopSettingsAware = false; - else if (pragma.startsWith("Env ")) { - auto envPragma = pragma.sliced(4); - auto splitIdx = envPragma.indexOf('='); + processCommand(argc, argv, command); - if (splitIdx == -1) { - qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'"; - return -1; - } + // Start log manager - has to happen with an active event loop or offthread can't be started. + LogManager::init(!noColor, sparseLogsOnly); - auto var = envPragma.sliced(0, splitIdx).trimmed(); - auto val = envPragma.sliced(splitIdx + 1).trimmed(); - envOverrides.insert(var, val); - } else if (pragma.startsWith("ShellId ")) { - shellId = pragma.sliced(8).trimmed(); - } else { - qCritical() << "Unrecognized pragma" << pragma; +#if CRASH_REPORTER + // Started after log manager for pretty debug logs. Unlikely anything will crash before this point, but + // this can be moved if it happens. + crashHandler.init(); +#endif + + configFilePath = commandConfigPath( + command.configPath, + command.manifestPath, + command.configName, + command.printInfo + ); + } + + shellId = QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + + qInfo() << "Config file path:" << configFilePath; + + if (!QFile(configFilePath).exists()) { + qCritical() << "config file does not exist"; + return -1; + } + + auto file = QFile(configFilePath); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + qCritical() << "could not open config file"; + return -1; + } + + auto stream = QTextStream(&file); + while (!stream.atEnd()) { + auto line = stream.readLine().trimmed(); + if (line.startsWith("//@ pragma ")) { + auto pragma = line.sliced(11).trimmed(); + + if (pragma == "UseQApplication") useQApplication = true; + else if (pragma == "NativeTextRendering") nativeTextRendering = true; + else if (pragma == "IgnoreSystemSettings") desktopSettingsAware = false; + else if (pragma.startsWith("Env ")) { + auto envPragma = pragma.sliced(4); + auto splitIdx = envPragma.indexOf('='); + + if (splitIdx == -1) { + qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'"; return -1; } - } else if (line.startsWith("import")) break; - } - file.close(); + auto var = envPragma.sliced(0, splitIdx).trimmed(); + auto val = envPragma.sliced(splitIdx + 1).trimmed(); + envOverrides.insert(var, val); + } else if (pragma.startsWith("ShellId ")) { + shellId = pragma.sliced(8).trimmed(); + } else { + qCritical() << "Unrecognized pragma" << pragma; + return -1; + } + } else if (line.startsWith("import")) break; } + + file.close(); } qInfo() << "Shell ID:" << shellId; if (printInfo) return 0; +#if CRASH_REPORTER + crashHandler.setInstanceInfo(InstanceInfo { + .configPath = configFilePath, + .shellId = shellId, + .initialWorkdir = initialWorkdir, + .launchTime = qs::Common::LAUNCH_TIME, + .noColor = noColor, + .sparseLogsOnly = sparseLogsOnly, + }); +#endif + for (auto [var, val]: envOverrides.asKeyValueRange()) { qputenv(var.toUtf8(), val.toUtf8()); } @@ -484,8 +600,8 @@ int qs_main(int argc, char** argv) { QQmlDebuggingEnabler::startTcpDebugServer(debugPort, wait); } - if (!workingDirectory.isEmpty()) { - QDir::setCurrent(workingDirectory); + if (!initialWorkdir.isEmpty()) { + QDir::setCurrent(initialWorkdir); } QuickshellPlugin::initPlugins(); diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 7e05530d..8f63b3aa 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -9,6 +9,8 @@ #include #include +#include "common.hpp" + Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg); QsPaths* QsPaths::instance() { @@ -18,6 +20,15 @@ QsPaths* QsPaths::instance() { void QsPaths::init(QString shellId) { QsPaths::instance()->shellId = std::move(shellId); } +QDir QsPaths::crashDir(const QString& shellId, const QDateTime& launchTime) { + auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + dir = QDir(dir.filePath("crashes")); + dir = QDir(dir.filePath(shellId)); + dir = QDir(dir.filePath(QString("run-%1").arg(launchTime.toMSecsSinceEpoch()))); + + return dir; +} + QDir* QsPaths::cacheDir() { if (this->cacheState == DirState::Unknown) { auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); @@ -73,7 +84,7 @@ QDir* QsPaths::instanceRunDir() { this->instanceRunState = DirState::Failed; } else { this->mInstanceRunDir = - runtimeDir->filePath(QString("run-%1").arg(QDateTime::currentMSecsSinceEpoch())); + runtimeDir->filePath(QString("run-%1").arg(qs::Common::LAUNCH_TIME.toMSecsSinceEpoch())); qCDebug(logPaths) << "Initialized instance runtime path:" << this->mInstanceRunDir.path(); diff --git a/src/core/paths.hpp b/src/core/paths.hpp index b2a1c193..9716e299 100644 --- a/src/core/paths.hpp +++ b/src/core/paths.hpp @@ -1,10 +1,12 @@ #pragma once +#include #include class QsPaths { public: static QsPaths* instance(); static void init(QString shellId); + static QDir crashDir(const QString& shellId, const QDateTime& launchTime); QDir* cacheDir(); QDir* runDir(); diff --git a/src/crash/CMakeLists.txt b/src/crash/CMakeLists.txt new file mode 100644 index 00000000..522b5b02 --- /dev/null +++ b/src/crash/CMakeLists.txt @@ -0,0 +1,16 @@ +qt_add_library(quickshell-crash STATIC + main.cpp + interface.cpp + handler.cpp +) + +qs_pch(quickshell-crash) + +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) + +target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt6::Widgets) + +target_link_libraries(quickshell-core PRIVATE quickshell-crash) diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp new file mode 100644 index 00000000..dea6192c --- /dev/null +++ b/src/crash/handler.cpp @@ -0,0 +1,180 @@ +#include "handler.hpp" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/crashinfo.hpp" + +extern char** environ; // NOLINT + +using namespace google_breakpad; + +namespace qs::crash { + +Q_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg); + +struct CrashHandlerPrivate { + ExceptionHandler* exceptionHandler = nullptr; + int minidumpFd = -1; + int infoFd = -1; + + 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)); + } + + qCInfo(logCrashHandler) << "Crash handler initialized."; +} + +void CrashHandler::setInstanceInfo(const InstanceInfo& 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."; + return; + } + + QFile file; + file.open(this->d->infoFd, QFile::ReadWrite); + + QDataStream ds(&file); + ds << info; + file.flush(); + + qCDebug(logCrashHandler) << "Stored instance info in memfd" << this->d->infoFd; +} + +CrashHandler::~CrashHandler() { + delete this->d->exceptionHandler; + delete this->d; +} + +bool CrashHandlerPrivate::minidumpCallback( + const MinidumpDescriptor& /*descriptor*/, + void* context, + bool /*success*/ +) { + // A fork that just dies to ensure the coredump is caught by the system. + auto coredumpPid = fork(); + + if (coredumpPid == 0) { + return false; + } + + auto* self = static_cast(context); + + auto exe = std::array(); + if (readlink("/proc/self/exe", exe.data(), exe.size() - 1) == -1) { + perror("Failed to find crash reporter executable.\n"); + _exit(-1); + } + + auto arg = std::array {exe.data(), nullptr}; + + 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); + 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); + env[envi++] = corePidStr.data(); + + auto populateEnv = [&]() { + auto senvi = 0; + while (envi < 4095) { + env[envi++] = environ[senvi++]; // NOLINT + } + + 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; + } 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); + + env[envi++] = dumpFdStr.data(); + env[envi++] = logFdStr.data(); + + populateEnv(); + execve(exe.data(), arg.data(), env.data()); + + perror("Failed to launch crash reporter.\n"); + _exit(-1); + } else { + populateEnv(); + execve(exe.data(), arg.data(), env.data()); + + perror("Failed to relaunch quickshell.\n"); + _exit(-1); + } + + return false; // should make sure it hits the system coredump handler +} + +} // namespace qs::crash diff --git a/src/crash/handler.hpp b/src/crash/handler.hpp new file mode 100644 index 00000000..de7b46bc --- /dev/null +++ b/src/crash/handler.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "../core/crashinfo.hpp" +namespace qs::crash { + +struct CrashHandlerPrivate; + +class CrashHandler { +public: + explicit CrashHandler(); + ~CrashHandler(); + Q_DISABLE_COPY_MOVE(CrashHandler); + + void init(); + void setInstanceInfo(const InstanceInfo& info); + +private: + CrashHandlerPrivate* d; +}; + +} // namespace qs::crash diff --git a/src/crash/interface.cpp b/src/crash/interface.cpp new file mode 100644 index 00000000..3d296580 --- /dev/null +++ b/src/crash/interface.cpp @@ -0,0 +1,97 @@ +#include "interface.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "build.hpp" + +class ReportLabel: public QWidget { +public: + ReportLabel(const QString& label, const QString& content, QWidget* parent): QWidget(parent) { + auto* layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(new QLabel(label, this)); + + auto* cl = new QLabel(content, this); + cl->setTextInteractionFlags(Qt::TextSelectableByMouse); + layout->addWidget(cl); + + layout->addStretch(); + } +}; + +CrashReporterGui::CrashReporterGui(QString reportFolder, int pid) + : reportFolder(std::move(reportFolder)) { + this->setWindowFlags(Qt::Window); + + auto textHeight = QFontInfo(QFont()).pixelSize(); + + auto* mainLayout = new QVBoxLayout(this); + + mainLayout->addWidget(new QLabel( + "Quickshell has crashed. Please submit a bug report to help us fix it.", + this + )); + + mainLayout->addSpacing(textHeight); + + mainLayout->addWidget(new QLabel("General information", this)); + mainLayout->addWidget(new ReportLabel("Git Revision:", GIT_REVISION, this)); + mainLayout->addWidget(new ReportLabel("Crashed process ID:", QString::number(pid), this)); + mainLayout->addWidget(new ReportLabel("Crash report folder:", this->reportFolder, this)); + mainLayout->addSpacing(textHeight); + + mainLayout->addWidget(new QLabel("Please open a bug report for this issue via github or email.")); + + mainLayout->addWidget(new ReportLabel( + "Github:", + "https://github.com/outfoxxed/quickshell/issues/new?template=crash.yml", + this + )); + + mainLayout->addWidget(new ReportLabel("Email:", "quickshell-bugs@outfoxxed.me", this)); + + auto* buttons = new QWidget(this); + buttons->setMinimumWidth(900); + auto* buttonLayout = new QHBoxLayout(buttons); + buttonLayout->setContentsMargins(0, 0, 0, 0); + + auto* reportButton = new QPushButton("Open report page", buttons); + reportButton->setDefault(true); + QObject::connect(reportButton, &QPushButton::clicked, this, &CrashReporterGui::openReportUrl); + buttonLayout->addWidget(reportButton); + + auto* openFolderButton = new QPushButton("Open crash folder", buttons); + QObject::connect(openFolderButton, &QPushButton::clicked, this, &CrashReporterGui::openFolder); + buttonLayout->addWidget(openFolderButton); + + auto* cancelButton = new QPushButton("Exit", buttons); + QObject::connect(cancelButton, &QPushButton::clicked, this, &CrashReporterGui::cancel); + buttonLayout->addWidget(cancelButton); + + mainLayout->addWidget(buttons); + + mainLayout->addStretch(); + this->setFixedSize(this->sizeHint()); +} + +void CrashReporterGui::openFolder() { + QDesktopServices::openUrl(QUrl::fromLocalFile(this->reportFolder)); +} + +void CrashReporterGui::openReportUrl() { + QDesktopServices::openUrl( + QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash.yml") + ); +} + +void CrashReporterGui::cancel() { QApplication::quit(); } diff --git a/src/crash/interface.hpp b/src/crash/interface.hpp new file mode 100644 index 00000000..d7800435 --- /dev/null +++ b/src/crash/interface.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +class CrashReporterGui: public QWidget { +public: + CrashReporterGui(QString reportFolder, int pid); + +private slots: + void openFolder(); + + static void openReportUrl(); + static void cancel(); + +private: + QString reportFolder; +}; diff --git a/src/crash/main.cpp b/src/crash/main.cpp new file mode 100644 index 00000000..52776190 --- /dev/null +++ b/src/crash/main.cpp @@ -0,0 +1,165 @@ +#include "main.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/crashinfo.hpp" +#include "../core/logging.hpp" +#include "../core/paths.hpp" +#include "build.hpp" +#include "interface.hpp" + +Q_LOGGING_CATEGORY(logCrashReporter, "quickshell.crashreporter", QtWarningMsg); + +void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instanceInfo); + +void qsCheckCrash(int argc, char** argv) { + auto fd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD"); + if (fd.isEmpty()) return; + auto app = QApplication(argc, argv); + + InstanceInfo instance; + + auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt(); + + { + auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt(); + + QFile file; + file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle); + file.seek(0); + + auto ds = QDataStream(&file); + ds >> instance; + } + + LogManager::init(!instance.noColor, false); + auto crashDir = QsPaths::crashDir(instance.shellId, instance.launchTime); + + qCInfo(logCrashReporter) << "Starting crash reporter..."; + + recordCrashInfo(crashDir, instance); + + auto gui = CrashReporterGui(crashDir.path(), crashProc); + gui.show(); + exit(QApplication::exec()); // NOLINT +} + +int tryDup(int fd, const QString& path) { + QFile sourceFile; + if (!sourceFile.open(fd, QFile::ReadOnly, QFile::AutoCloseHandle)) { + qCCritical(logCrashReporter) << "Failed to open source fd for duplication."; + return 1; + } + + auto destFile = QFile(path); + if (!destFile.open(QFile::WriteOnly)) { + qCCritical(logCrashReporter) << "Failed to open dest file for duplication."; + return 2; + } + + auto size = sourceFile.size(); + off_t offset = 0; + ssize_t count = 0; + + sourceFile.seek(0); + + while (count != size) { + auto r = sendfile(destFile.handle(), sourceFile.handle(), &offset, sourceFile.size()); + if (r == -1) { + qCCritical(logCrashReporter).nospace() + << "Failed to duplicate fd " << fd << " with error code " << errno + << ". Error: " << qt_error_string(); + return 3; + } else { + count += r; + } + } + + return 0; +} + +void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { + qCDebug(logCrashReporter) << "Recording crash information at" << crashDir.path(); + + if (!crashDir.mkpath(".")) { + qCCritical(logCrashReporter) << "Failed to create folder" << crashDir + << "to save crash information."; + return; + } + + auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").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")); + if (dumpDupStatus != 0) { + qCCritical(logCrashReporter) << "Failed to write minidump:" << dumpDupStatus; + } + + qCDebug(logCrashReporter) << "Saving log from fd" << logFd; + auto logDupStatus = tryDup(logFd, crashDir.filePath("log.qslog")); + if (logDupStatus != 0) { + qCCritical(logCrashReporter) << "Failed to save log:" << logDupStatus; + } + + { + auto extraInfoFile = QFile(crashDir.filePath("info.txt")); + if (!extraInfoFile.open(QFile::WriteOnly)) { + qCCritical(logCrashReporter) << "Failed to open crash info file for writing."; + } else { + auto stream = QTextStream(&extraInfoFile); + stream << "===== Quickshell Crash =====\n"; + stream << "Git Revision: " << GIT_REVISION << '\n'; + stream << "Crashed process ID: " << crashProc << '\n'; + stream << "Run ID: " << QString("run-%1").arg(instance.launchTime.toMSecsSinceEpoch()) + << '\n'; + + stream << "\n===== Shell Information =====\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 << "\n===== System Information =====\n"; + stream << "Qt Version: " << QT_VERSION_STR << "\n\n"; + + stream << "/etc/os-release:"; + auto osReleaseFile = QFile("/etc/os-release"); + if (osReleaseFile.open(QFile::ReadOnly)) { + stream << '\n' << osReleaseFile.readAll() << '\n'; + osReleaseFile.close(); + } else { + stream << "FAILED TO OPEN\n"; + } + + stream << "/etc/lsb-release:"; + auto lsbReleaseFile = QFile("/etc/lsb-release"); + if (lsbReleaseFile.open(QFile::ReadOnly)) { + stream << '\n' << lsbReleaseFile.readAll() << '\n'; + lsbReleaseFile.close(); + } else { + stream << "FAILED TO OPEN\n"; + } + + extraInfoFile.close(); + } + } + + qCDebug(logCrashReporter) << "Recorded crash information."; +} diff --git a/src/crash/main.hpp b/src/crash/main.hpp new file mode 100644 index 00000000..b6a282cf --- /dev/null +++ b/src/crash/main.hpp @@ -0,0 +1,3 @@ +#pragma once + +void qsCheckCrash(int argc, char** argv); diff --git a/src/services/status_notifier/host.cpp b/src/services/status_notifier/host.cpp index 470b86a7..5fa9af0e 100644 --- a/src/services/status_notifier/host.cpp +++ b/src/services/status_notifier/host.cpp @@ -11,6 +11,7 @@ #include #include +#include "../../core/common.hpp" #include "../../dbus/properties.hpp" #include "dbus_watcher_interface.h" #include "item.hpp" @@ -31,7 +32,10 @@ StatusNotifierHost::StatusNotifierHost(QObject* parent): QObject(parent) { return; } - this->hostId = QString("org.kde.StatusNotifierHost-") + QString::number(getpid()); + this->hostId = QString("org.kde.StatusNotifierHost-%1-%2") + .arg(QString::number(getpid())) + .arg(QString::number(qs::Common::LAUNCH_TIME.toMSecsSinceEpoch())); + auto success = bus.registerService(this->hostId); if (!success) { @@ -98,7 +102,7 @@ void StatusNotifierHost::connectToWatcher() { [this](QStringList value, QDBusError error) { // NOLINT if (error.isValid()) { qCWarning(logStatusNotifierHost).noquote() - << "Error reading \"RegisteredStatusNotifierITems\" property of watcher" + << "Error reading \"RegisteredStatusNotifierItems\" property of watcher" << this->watcher->service(); qCWarning(logStatusNotifierHost) << error; From f95e7dbaf61b9868acc912896e65126bb1ee048c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 20 Aug 2024 16:41:04 -0700 Subject: [PATCH 133/305] hyprland/focus_grab: wait for surface creation if null Fixes an occasional crash with QWaylandWindow::surface() returning null. --- src/wayland/hyprland/focus_grab/grab.cpp | 33 +++++++++++++++++++++--- src/wayland/hyprland/focus_grab/grab.hpp | 3 +++ src/wayland/hyprland/focus_grab/qml.cpp | 8 +++--- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/wayland/hyprland/focus_grab/grab.cpp b/src/wayland/hyprland/focus_grab/grab.cpp index a45cf4ec..62298699 100644 --- a/src/wayland/hyprland/focus_grab/grab.cpp +++ b/src/wayland/hyprland/focus_grab/grab.cpp @@ -19,18 +19,34 @@ FocusGrab::~FocusGrab() { bool FocusGrab::isActive() const { return this->active; } void FocusGrab::addWindow(QWindow* window) { + auto tryAddWayland = [this](QWaylandWindow* waylandWindow) { + if (waylandWindow->surface()) { + this->addWaylandWindow(waylandWindow); + this->sync(); + } else { + QObject::connect( + waylandWindow, + &QWaylandWindow::surfaceCreated, + this, + [this, waylandWindow]() { + this->addWaylandWindow(waylandWindow); + this->sync(); + } + ); + } + }; + if (auto* waylandWindow = dynamic_cast(window->handle())) { - this->addWaylandWindow(waylandWindow); + tryAddWayland(waylandWindow); } else { - QObject::connect(window, &QWindow::visibleChanged, this, [this, window]() { + QObject::connect(window, &QWindow::visibleChanged, this, [this, window, tryAddWayland]() { if (window->isVisible()) { if (window->handle() == nullptr) { window->create(); } auto* waylandWindow = dynamic_cast(window->handle()); - this->addWaylandWindow(waylandWindow); - this->sync(); + tryAddWayland(waylandWindow); } }); } @@ -53,6 +69,8 @@ void FocusGrab::addWaylandWindow(QWaylandWindow* window) { } void FocusGrab::sync() { + if (this->transactionActive) return; + if (this->commitRequired) { this->commit(); this->commitRequired = false; @@ -70,6 +88,13 @@ void FocusGrab::sync() { } } +void FocusGrab::startTransaction() { this->transactionActive = true; } + +void FocusGrab::completeTransaction() { + this->transactionActive = false; + this->sync(); +} + void FocusGrab::hyprland_focus_grab_v1_cleared() { this->active = false; emit this->cleared(); diff --git a/src/wayland/hyprland/focus_grab/grab.hpp b/src/wayland/hyprland/focus_grab/grab.hpp index 2a9384d9..99d5125c 100644 --- a/src/wayland/hyprland/focus_grab/grab.hpp +++ b/src/wayland/hyprland/focus_grab/grab.hpp @@ -28,6 +28,8 @@ public: void addWindow(QWindow* window); void removeWindow(QWindow* window); void sync(); + void startTransaction(); + void completeTransaction(); signals: void activated(); @@ -40,6 +42,7 @@ private: QList pendingAdditions; bool commitRequired = false; + bool transactionActive = false; bool active = false; }; diff --git a/src/wayland/hyprland/focus_grab/qml.cpp b/src/wayland/hyprland/focus_grab/qml.cpp index ca644b9d..9ae309ff 100644 --- a/src/wayland/hyprland/focus_grab/qml.cpp +++ b/src/wayland/hyprland/focus_grab/qml.cpp @@ -74,13 +74,13 @@ void HyprlandFocusGrab::tryActivate() { QObject::connect(this->grab, &FocusGrab::activated, this, &HyprlandFocusGrab::onGrabActivated); QObject::connect(this->grab, &FocusGrab::cleared, this, &HyprlandFocusGrab::onGrabCleared); + this->grab->startTransaction(); for (auto* proxy: this->trackedProxies) { if (proxy->backingWindow() != nullptr) { this->grab->addWindow(proxy->backingWindow()); } } - - this->grab->sync(); + this->grab->completeTransaction(); } void HyprlandFocusGrab::syncWindows() { @@ -99,6 +99,8 @@ void HyprlandFocusGrab::syncWindows() { } } + if (this->grab) this->grab->startTransaction(); + for (auto* oldWindow: this->trackedProxies) { if (!newProxy.contains(oldWindow)) { QObject::disconnect(oldWindow, nullptr, this, nullptr); @@ -125,7 +127,7 @@ void HyprlandFocusGrab::syncWindows() { } this->trackedProxies = newProxy; - if (this->grab != nullptr) this->grab->sync(); + if (this->grab) this->grab->completeTransaction(); } } // namespace qs::hyprland From b40d4147e09548fb050749faf8e1e5b7509d7646 Mon Sep 17 00:00:00 2001 From: Nydragon Date: Sun, 25 Aug 2024 17:01:26 +0200 Subject: [PATCH 134/305] build: add opt-in installation of QML lib Override the package with `withQMLLib = true;` to enable lib installation, alternatively add `-DINSTALL_QML_LIB=ON` to your cmake build command. Co-authored-by: a-usr <81042605+a-usr@users.noreply.github.com> --- BUILD.md | 6 ++++++ CMakeLists.txt | 10 ++++++++++ default.nix | 4 +++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index 5fe6ebc6..4dcf9ac0 100644 --- a/BUILD.md +++ b/BUILD.md @@ -23,6 +23,12 @@ If your package manager supports enabling some features but not others, we recommend not exposing the subfeatures and just the main ones that introduce new dependencies: `wayland`, `x11`, `pipewire`, `hyprland` +### QML Library +If you wish to use a linter or similar tools, you will need the QML Modules for it to pick up on the types. + +To disable: `-DINSTALL_QML_LIB=OFF` + + ### Jemalloc We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused by the QML engine, which results in much lower memory usage. Without this you diff --git a/CMakeLists.txt b/CMakeLists.txt index e3c01592..1a7126ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ option(BUILD_TESTING "Build tests" OFF) option(ASAN "Enable ASAN" OFF) # note: better output with gcc than clang option(FRAME_POINTERS "Always keep frame pointers" ${ASAN}) +option(INSTALL_QML_LIB "Installing the QML lib" ON) option(CRASH_REPORTER "Enable the crash reporter" ON) option(USE_JEMALLOC "Use jemalloc over the system malloc implementation" ON) option(SOCKETS "Enable unix socket support" ON) @@ -30,6 +31,7 @@ option(SERVICE_UPOWER "UPower service" ON) option(SERVICE_NOTIFICATIONS "Notification server" ON) message(STATUS "Quickshell configuration") +message(STATUS " QML lib installation: ${INSTALL_QML_LIB}") message(STATUS " Crash reporter: ${CRASH_REPORTER}") message(STATUS " Jemalloc: ${USE_JEMALLOC}") message(STATUS " Build tests: ${BUILD_TESTING}") @@ -119,6 +121,14 @@ find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS}) qt_standard_project_setup(REQUIRES 6.6) set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules) +if (INSTALL_QML_LIB) + install( + DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules/ + DESTINATION ${CMAKE_INSTALL_LIBDIR}/qt-6/qml + FILES_MATCHING PATTERN "*" + ) +endif() + # pch breaks clang-tidy..... somehow if (NOT NO_PCH) file(GENERATE diff --git a/default.nix b/default.nix index 1ddb99b5..a00bf012 100644 --- a/default.nix +++ b/default.nix @@ -37,6 +37,7 @@ withPipewire ? true, withPam ? true, withHyprland ? true, + withQMLLib ? true, }: buildStdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; version = "0.1.0"; @@ -75,7 +76,8 @@ ++ lib.optional (!withWayland) "-DWAYLAND=OFF" ++ lib.optional (!withPipewire) "-DSERVICE_PIPEWIRE=OFF" ++ lib.optional (!withPam) "-DSERVICE_PAM=OFF" - ++ lib.optional (!withHyprland) "-DHYPRLAND=OFF"; + ++ lib.optional (!withHyprland) "-DHYPRLAND=OFF" + ++ lib.optional (!withQMLLib) "-DINSTALL_QML_LIB=OFF"; buildPhase = "ninjaBuildPhase"; enableParallelBuilding = true; From c60871a7fb56f66c9b4057bddc5326d4fa405164 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 27 Aug 2024 01:28:28 -0700 Subject: [PATCH 135/305] service/pipewire: set device node volumes with device object Fixes discrepancies between pulse and qs volumes, and volumes not persisting across reboot or vt switches. --- src/services/pipewire/CMakeLists.txt | 1 + src/services/pipewire/core.cpp | 4 +- src/services/pipewire/core.hpp | 5 +- src/services/pipewire/device.cpp | 192 +++++++++++++++++++++++++++ src/services/pipewire/device.hpp | 50 +++++++ src/services/pipewire/node.cpp | 184 +++++++++++++++---------- src/services/pipewire/node.hpp | 5 + src/services/pipewire/registry.cpp | 9 ++ src/services/pipewire/registry.hpp | 4 + 9 files changed, 380 insertions(+), 74 deletions(-) create mode 100644 src/services/pipewire/device.cpp create mode 100644 src/services/pipewire/device.hpp diff --git a/src/services/pipewire/CMakeLists.txt b/src/services/pipewire/CMakeLists.txt index 4fccdc0e..51c9fec8 100644 --- a/src/services/pipewire/CMakeLists.txt +++ b/src/services/pipewire/CMakeLists.txt @@ -9,6 +9,7 @@ qt_add_library(quickshell-service-pipewire STATIC node.cpp metadata.cpp link.cpp + device.cpp ) qt_add_qml_module(quickshell-service-pipewire diff --git a/src/services/pipewire/core.cpp b/src/services/pipewire/core.cpp index 4f997155..c4b31ab5 100644 --- a/src/services/pipewire/core.cpp +++ b/src/services/pipewire/core.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -68,11 +69,12 @@ bool PwCore::isValid() const { return this->core != nullptr; } -void PwCore::poll() const { +void PwCore::poll() { qCDebug(logLoop) << "Pipewire event loop received new events, iterating."; // Spin pw event loop. pw_loop_iterate(this->loop, 0); qCDebug(logLoop) << "Done iterating pipewire event loop."; + emit this->polled(); } SpaHook::SpaHook() { // NOLINT diff --git a/src/services/pipewire/core.hpp b/src/services/pipewire/core.hpp index ebf5c63e..bf7bd785 100644 --- a/src/services/pipewire/core.hpp +++ b/src/services/pipewire/core.hpp @@ -28,8 +28,11 @@ public: pw_context* context = nullptr; pw_core* core = nullptr; +signals: + void polled(); + private slots: - void poll() const; + void poll(); private: QSocketNotifier notifier; diff --git a/src/services/pipewire/device.cpp b/src/services/pipewire/device.cpp new file mode 100644 index 00000000..6adab506 --- /dev/null +++ b/src/services/pipewire/device.cpp @@ -0,0 +1,192 @@ +#include "device.hpp" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core.hpp" + +namespace qs::service::pipewire { + +Q_LOGGING_CATEGORY(logDevice, "quickshell.service.pipewire.device", QtWarningMsg); + +// https://github.com/PipeWire/wireplumber/blob/895c1c7286e8809fad869059179e53ab39c807e9/modules/module-mixer-api.c#L397 +// https://github.com/PipeWire/pipewire/blob/48c2e9516585ccc791335bc7baf4af6952ec54a0/src/modules/module-protocol-pulse/pulse-server.c#L2743-L2743 + +void PwDevice::bindHooks() { + pw_device_add_listener(this->proxy(), &this->listener.hook, &PwDevice::EVENTS, this); + QObject::connect(this->registry->core, &PwCore::polled, this, &PwDevice::polled); +} + +void PwDevice::unbindHooks() { + QObject::disconnect(this->registry->core, &PwCore::polled, this, &PwDevice::polled); + this->listener.remove(); + this->stagingIndexes.clear(); + this->routeDeviceIndexes.clear(); +} + +const pw_device_events PwDevice::EVENTS = { + .version = PW_VERSION_DEVICE_EVENTS, + .info = &PwDevice::onInfo, + .param = &PwDevice::onParam, +}; + +void PwDevice::onInfo(void* data, const pw_device_info* info) { + auto* self = static_cast(data); + + if ((info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS) == PW_DEVICE_CHANGE_MASK_PARAMS) { + for (quint32 i = 0; i != info->n_params; i++) { + auto& param = info->params[i]; // NOLINT + + if (param.id == SPA_PARAM_Route) { + if ((param.flags & SPA_PARAM_INFO_READWRITE) == SPA_PARAM_INFO_READWRITE) { + qCDebug(logDevice) << "Enumerating routes param for" << self; + self->stagingIndexes.clear(); + pw_device_enum_params(self->proxy(), 0, param.id, 0, UINT32_MAX, nullptr); + } else { + qCWarning(logDevice) << "Unable to enumerate route param for" << self + << "as the param does not have read+write permissions."; + } + + break; + } + } + } +} + +void PwDevice::onParam( + void* data, + qint32 /*seq*/, + quint32 id, + quint32 /*index*/, + quint32 next, + const spa_pod* param +) { + auto* self = static_cast(data); + + if (id == SPA_PARAM_Route) { + self->addDeviceIndexPairs(param); + } +} + +void PwDevice::addDeviceIndexPairs(const spa_pod* param) { + auto parser = spa_pod_parser(); + spa_pod_parser_pod(&parser, param); + + qint32 device = 0; + qint32 index = 0; + + // clang-format off + quint32 id = SPA_PARAM_Route; + spa_pod_parser_get_object( + &parser, SPA_TYPE_OBJECT_ParamRoute, &id, + SPA_PARAM_ROUTE_device, SPA_POD_Int(&device), + SPA_PARAM_ROUTE_index, SPA_POD_Int(&index) + ); + // clang-format on + + this->stagingIndexes.insert(device, index); + // Insert into the main map as well, staging's purpose is to remove old entries. + this->routeDeviceIndexes.insert(device, index); + + qCDebug(logDevice).nospace() << "Registered device/index pair for " << this + << ": [device: " << device << ", index: " << index << ']'; +} + +void PwDevice::polled() { + // It is far more likely that the list content has not come in yet than it having no entries, + // and there isn't a way to check in the case that there *aren't* actually any entries. + if (!this->stagingIndexes.isEmpty() && this->stagingIndexes != this->routeDeviceIndexes) { + this->routeDeviceIndexes = this->stagingIndexes; + qCDebug(logDevice) << "Updated device/index pair list for" << this << "to" + << this->routeDeviceIndexes; + } +} + +bool PwDevice::setVolumes(qint32 routeDevice, const QVector& volumes) { + return this->setRouteProps(routeDevice, [this, routeDevice, &volumes](spa_pod_builder* builder) { + auto cubedVolumes = QVector(); + for (auto volume: volumes) { + cubedVolumes.push_back(volume * volume * volume); + } + + // clang-format off + auto* props = spa_pod_builder_add_object( + builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, + SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float), SPA_TYPE_Float, cubedVolumes.length(), cubedVolumes.data()) + ); + // clang-format on + + qCInfo(logDevice) << "Changed volumes of" << this << "on route device" << routeDevice << "to" + << volumes; + return props; + }); +} + +bool PwDevice::setMuted(qint32 routeDevice, bool muted) { + return this->setRouteProps(routeDevice, [this, routeDevice, muted](spa_pod_builder* builder) { + // clang-format off + auto* props = spa_pod_builder_add_object( + builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, + SPA_PROP_mute, SPA_POD_Bool(muted) + ); + // clang-format on + + qCInfo(logDevice) << "Changed muted state of" << this << "on route device" << routeDevice + << "to" << muted; + return props; + }); +} + +bool PwDevice::setRouteProps( + qint32 routeDevice, + const std::function& propsCallback +) { + if (this->proxy() == nullptr) { + qCCritical(logDevice) << "Tried to change device route props for" << this + << "which is not bound."; + return false; + } + + if (!this->routeDeviceIndexes.contains(routeDevice)) { + qCCritical(logDevice) << "Tried to change device route props for" << this + << "with untracked route device" << routeDevice; + return false; + } + + auto routeIndex = this->routeDeviceIndexes.value(routeDevice); + + auto buffer = std::array(); + auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size()); + + auto* props = propsCallback(&builder); + + // clang-format off + auto* route = spa_pod_builder_add_object( + &builder, SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_Route, + SPA_PARAM_ROUTE_device, SPA_POD_Int(routeDevice), + SPA_PARAM_ROUTE_index, SPA_POD_Int(routeIndex), + SPA_PARAM_ROUTE_props, SPA_POD_PodObject(props), + SPA_PARAM_ROUTE_save, SPA_POD_Bool(true) + ); + // clang-format on + + pw_device_set_param(this->proxy(), SPA_PARAM_Route, 0, static_cast(route)); + return true; +} + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/device.hpp b/src/services/pipewire/device.hpp new file mode 100644 index 00000000..31f32f0f --- /dev/null +++ b/src/services/pipewire/device.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "core.hpp" +#include "registry.hpp" + +namespace qs::service::pipewire { + +class PwDevice; + +constexpr const char TYPE_INTERFACE_Device[] = PW_TYPE_INTERFACE_Device; // NOLINT +class PwDevice: public PwBindable { + Q_OBJECT; + +public: + void bindHooks() override; + void unbindHooks() override; + + bool setVolumes(qint32 routeDevice, const QVector& volumes); + bool setMuted(qint32 routeDevice, bool muted); + +private slots: + void polled(); + +private: + static const pw_device_events EVENTS; + static void onInfo(void* data, const pw_device_info* info); + static void + onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param); + + QHash routeDeviceIndexes; + QHash stagingIndexes; + void addDeviceIndexPairs(const spa_pod* param); + + bool + setRouteProps(qint32 routeDevice, const std::function& propsCallback); + + SpaHook listener; +}; + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index 969a8b71..280c450a 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -17,12 +18,15 @@ #include #include #include +#include #include #include #include #include #include +#include "device.hpp" + namespace qs::service::pipewire { Q_LOGGING_CATEGORY(logNode, "quickshell.service.pipewire.node", QtWarningMsg); @@ -79,17 +83,25 @@ QString PwAudioChannel::toString(Enum value) { } void PwNode::bindHooks() { + // Bind the device first as pw is in order, meaning the device should be bound before + // we want to do anything with it. + if (this->device) this->device->ref(); + pw_node_add_listener(this->proxy(), &this->listener.hook, &PwNode::EVENTS, this); } void PwNode::unbindHooks() { this->listener.remove(); + this->routeDevice = -1; this->properties.clear(); emit this->propertiesChanged(); if (this->boundData != nullptr) { this->boundData->onUnbind(); } + + // unbind after the node is unbound + if (this->device) this->device->unref(); } void PwNode::initProps(const spa_dict* props) { @@ -121,10 +133,28 @@ void PwNode::initProps(const spa_dict* props) { this->description = nodeDesc; } - if (const auto* nodeNick = spa_dict_lookup(props, "node.nick")) { + if (const auto* nodeNick = spa_dict_lookup(props, PW_KEY_NODE_NICK)) { this->nick = nodeNick; } + if (const auto* deviceId = spa_dict_lookup(props, PW_KEY_DEVICE_ID)) { + auto ok = false; + auto id = QString::fromUtf8(deviceId).toInt(&ok); + + if (!ok) { + qCCritical(logNode) << this << "has a device.id property but the value is not an integer. Id:" + << deviceId; + } else { + this->device = this->registry->devices.value(id); + + if (this->device == nullptr) { + qCCritical(logNode + ) << this + << "has a device.id property that does not corrospond to a device object. Id:" << id; + } + } + } + if (this->type == PwNodeType::Audio) { this->boundData = new PwNodeBoundAudio(this); } @@ -142,6 +172,24 @@ void PwNode::onInfo(void* data, const pw_node_info* info) { if ((info->change_mask & PW_NODE_CHANGE_MASK_PROPS) != 0) { auto properties = QMap(); + if (self->device) { + if (const auto* routeDevice = spa_dict_lookup(info->props, "card.profile.device")) { + auto ok = false; + auto id = QString::fromUtf8(routeDevice).toInt(&ok); + + if (!ok) { + qCCritical(logNode + ) << self + << "has a card.profile.device property but the value is not an integer. Value:" << id; + } + + self->routeDevice = id; + } else { + qCCritical(logNode) << self << "has attached device" << self->device + << "but no card.profile.device property."; + } + } + const spa_dict_item* item = nullptr; spa_dict_for_each(item, info->props) { properties.insert(item->key, item->value); } @@ -191,29 +239,6 @@ void PwNodeBoundAudio::updateVolumeFromParam(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); - if (volumesProp == nullptr) { - qCWarning(logNode) << "Cannot update volume props of" << this->node - << "- channelVolumes was null."; - return; - } - - if (channelsProp == nullptr) { - qCWarning(logNode) << "Cannot update volume props of" << this->node << "- channelMap was null."; - return; - } - - if (spa_pod_is_array(&volumesProp->value) == 0) { - qCWarning(logNode) << "Cannot update volume props of" << this->node - << "- channelVolumes was not an array."; - return; - } - - if (spa_pod_is_array(&channelsProp->value) == 0) { - qCWarning(logNode) << "Cannot update volume props of" << this->node - << "- channelMap was not an array."; - return; - } - const auto* volumes = reinterpret_cast(&volumesProp->value); // NOLINT const auto* channels = reinterpret_cast(&channelsProp->value); // NOLINT @@ -246,13 +271,13 @@ void PwNodeBoundAudio::updateVolumeFromParam(const spa_pod* param) { if (this->mChannels != channelsVec) { this->mChannels = channelsVec; channelsChanged = true; - qCDebug(logNode) << "Got updated channels of" << this->node << '-' << this->mChannels; + qCInfo(logNode) << "Got updated channels of" << this->node << '-' << this->mChannels; } if (this->mVolumes != volumesVec) { this->mVolumes = volumesVec; volumesChanged = true; - qCDebug(logNode) << "Got updated volumes of" << this->node << '-' << this->mVolumes; + qCInfo(logNode) << "Got updated volumes of" << this->node << '-' << this->mVolumes; } if (channelsChanged) emit this->channelsChanged(); @@ -260,25 +285,21 @@ void PwNodeBoundAudio::updateVolumeFromParam(const spa_pod* param) { } void PwNodeBoundAudio::updateMutedFromParam(const spa_pod* param) { - const auto* mutedProp = spa_pod_find_prop(param, nullptr, SPA_PROP_mute); + auto parser = spa_pod_parser(); + spa_pod_parser_pod(&parser, param); - if (mutedProp == nullptr) { - qCWarning(logNode) << "Cannot update muted state of" << this->node - << "- mute property was null."; - return; - } + auto muted = false; - if (spa_pod_is_bool(&mutedProp->value) == 0) { - qCWarning(logNode) << "Cannot update muted state of" << this->node - << "- mute property was not a boolean."; - return; - } - - bool muted = false; - spa_pod_get_bool(&mutedProp->value, &muted); + // clang-format off + quint32 id = SPA_PARAM_Props; + spa_pod_parser_get_object( + &parser, SPA_TYPE_OBJECT_Props, &id, + SPA_PROP_mute, SPA_POD_Bool(&muted) + ); + // clang-format on if (muted != this->mMuted) { - qCDebug(logNode) << "Got updated mute status of" << this->node << '-' << muted; + qCInfo(logNode) << "Got updated mute status of" << this->node << '-' << muted; this->mMuted = muted; emit this->mutedChanged(); } @@ -295,26 +316,35 @@ bool PwNodeBoundAudio::isMuted() const { return this->mMuted; } void PwNodeBoundAudio::setMuted(bool muted) { if (this->node->proxy() == nullptr) { - qCWarning(logNode) << "Tried to change mute state for" << this->node << "which is not bound."; + qCCritical(logNode) << "Tried to change mute state for" << this->node << "which is not bound."; return; } if (muted == this->mMuted) return; - auto buffer = std::array(); - auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size()); + if (this->node->device) { + if (!this->node->device->setMuted(this->node->routeDevice, muted)) { + return; + } - // is this a leak? seems like probably not? docs don't say, as usual. - // clang-format off - auto* pod = spa_pod_builder_add_object( - &builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, - SPA_PROP_mute, SPA_POD_Bool(muted) - ); - // clang-format on + qCInfo(logNode) << "Changed muted state of" << this->node << "to" << muted << "via device"; + } else { + auto buffer = std::array(); + auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size()); + + // is this a leak? seems like probably not? docs don't say, as usual. + // clang-format off + auto* pod = spa_pod_builder_add_object( + &builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, + SPA_PROP_mute, SPA_POD_Bool(muted) + ); + // clang-format on + + qCInfo(logNode) << "Changed muted state of" << this->node << "to" << muted; + pw_node_set_param(this->node->proxy(), SPA_PARAM_Props, 0, static_cast(pod)); + } - qCDebug(logNode) << "Changed muted state of" << this->node << "to" << muted; this->mMuted = muted; - pw_node_set_param(this->node->proxy(), SPA_PARAM_Props, 0, static_cast(pod)); emit this->mutedChanged(); } @@ -346,38 +376,48 @@ QVector PwNodeBoundAudio::volumes() const { return this->mVolumes; } void PwNodeBoundAudio::setVolumes(const QVector& volumes) { if (this->node->proxy() == nullptr) { - qCWarning(logNode) << "Tried to change node volumes for" << this->node << "which is not bound."; + qCCritical(logNode) << "Tried to change node volumes for" << this->node + << "which is not bound."; return; } if (volumes == this->mVolumes) return; if (volumes.length() != this->mVolumes.length()) { - qCWarning(logNode) << "Tried to change node volumes for" << this->node << "from" - << this->mVolumes << "to" << volumes - << "which has a different length than the list of channels" - << this->mChannels; + qCCritical(logNode) << "Tried to change node volumes for" << this->node << "from" + << this->mVolumes << "to" << volumes + << "which has a different length than the list of channels" + << this->mChannels; return; } - auto buffer = std::array(); - auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size()); + if (this->node->device) { + if (!this->node->device->setVolumes(this->node->routeDevice, volumes)) { + return; + } - auto cubedVolumes = QVector(); - for (auto volume: volumes) { - cubedVolumes.push_back(volume * volume * volume); + qCInfo(logNode) << "Changed volumes of" << this->node << "to" << volumes << "via device"; + } else { + auto buffer = std::array(); + auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size()); + + auto cubedVolumes = QVector(); + for (auto volume: volumes) { + cubedVolumes.push_back(volume * volume * volume); + } + + // clang-format off + auto* pod = spa_pod_builder_add_object( + &builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, + SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float), SPA_TYPE_Float, cubedVolumes.length(), cubedVolumes.data()) + ); + // clang-format on + + qCInfo(logNode) << "Changed volumes of" << this->node << "to" << volumes; + pw_node_set_param(this->node->proxy(), SPA_PARAM_Props, 0, static_cast(pod)); } - // clang-format off - auto* pod = spa_pod_builder_add_object( - &builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, - SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float), SPA_TYPE_Float, cubedVolumes.length(), cubedVolumes.data()) - ); - // clang-format on - - qCDebug(logNode) << "Changed volumes of" << this->node << "to" << volumes; this->mVolumes = volumes; - pw_node_set_param(this->node->proxy(), SPA_PARAM_Props, 0, static_cast(pod)); emit this->volumesChanged(); } diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index 75c93d0a..b8165137 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -18,6 +18,8 @@ namespace qs::service::pipewire { +class PwDevice; + ///! Audio channel of a pipewire node. /// See @@PwNodeAudio.channels. class PwAudioChannel: public QObject { @@ -161,6 +163,9 @@ public: PwNodeBoundData* boundData = nullptr; + PwDevice* device = nullptr; + qint32 routeDevice = -1; + signals: void propertiesChanged(); diff --git a/src/services/pipewire/registry.cpp b/src/services/pipewire/registry.cpp index 28142765..55cfb276 100644 --- a/src/services/pipewire/registry.cpp +++ b/src/services/pipewire/registry.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include "core.hpp" +#include "device.hpp" #include "link.hpp" #include "metadata.hpp" #include "node.hpp" @@ -114,6 +116,7 @@ void PwBindableObjectRef::onObjectDestroyed() { } void PwRegistry::init(PwCore& core) { + this->core = &core; this->object = pw_core_get_registry(core.core, PW_VERSION_REGISTRY, 0); pw_registry_add_listener(this->object, &this->listener.hook, &PwRegistry::EVENTS, this); } @@ -156,6 +159,12 @@ void PwRegistry::onGlobal( self->nodes.emplace(id, node); emit self->nodeAdded(node); + } else if (strcmp(type, PW_TYPE_INTERFACE_Device) == 0) { + auto* device = new PwDevice(); + device->init(self, id, permissions); + device->initProps(props); + + self->devices.emplace(id, device); } } diff --git a/src/services/pipewire/registry.hpp b/src/services/pipewire/registry.hpp index dab01af7..6ccd7148 100644 --- a/src/services/pipewire/registry.hpp +++ b/src/services/pipewire/registry.hpp @@ -21,6 +21,7 @@ class PwRegistry; class PwMetadata; class PwNode; class PwLink; +class PwDevice; class PwLinkGroup; class PwBindableObject: public QObject { @@ -120,9 +121,12 @@ public: //QHash clients; QHash metadata; QHash nodes; + QHash devices; QHash links; QVector linkGroups; + PwCore* core = nullptr; + signals: void nodeAdded(PwNode* node); void linkAdded(PwLink* link); From 79b22af093652bc59e1902b88f03f574aae665d7 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 27 Aug 2024 19:56:17 -0700 Subject: [PATCH 136/305] service/pipewire: avoid overloading devices with volume changes Wait until in-flight changes have been responded to before sending more. --- src/services/pipewire/device.cpp | 21 +++- src/services/pipewire/device.hpp | 8 ++ src/services/pipewire/node.cpp | 164 +++++++++++++++++++------------ src/services/pipewire/node.hpp | 18 +++- 4 files changed, 140 insertions(+), 71 deletions(-) diff --git a/src/services/pipewire/device.cpp b/src/services/pipewire/device.cpp index 6adab506..06ed102a 100644 --- a/src/services/pipewire/device.cpp +++ b/src/services/pipewire/device.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ void PwDevice::unbindHooks() { this->listener.remove(); this->stagingIndexes.clear(); this->routeDeviceIndexes.clear(); + this->mWaitingForDevice = false; } const pw_device_events PwDevice::EVENTS = { @@ -56,6 +58,7 @@ void PwDevice::onInfo(void* data, const pw_device_info* info) { if ((param.flags & SPA_PARAM_INFO_READWRITE) == SPA_PARAM_INFO_READWRITE) { qCDebug(logDevice) << "Enumerating routes param for" << self; self->stagingIndexes.clear(); + self->deviceResponded = false; pw_device_enum_params(self->proxy(), 0, param.id, 0, UINT32_MAX, nullptr); } else { qCWarning(logDevice) << "Unable to enumerate route param for" << self @@ -73,12 +76,21 @@ void PwDevice::onParam( qint32 /*seq*/, quint32 id, quint32 /*index*/, - quint32 next, + quint32 /*next*/, const spa_pod* param ) { auto* self = static_cast(data); if (id == SPA_PARAM_Route) { + if (!self->deviceResponded) { + self->deviceResponded = true; + + if (self->mWaitingForDevice) { + self->mWaitingForDevice = false; + emit self->deviceReady(); + } + } + self->addDeviceIndexPairs(param); } } @@ -131,7 +143,7 @@ bool PwDevice::setVolumes(qint32 routeDevice, const QVector& volumes) { ); // clang-format on - qCInfo(logDevice) << "Changed volumes of" << this << "on route device" << routeDevice << "to" + qCInfo(logDevice) << "Changing volumes of" << this << "on route device" << routeDevice << "to" << volumes; return props; }); @@ -146,12 +158,15 @@ bool PwDevice::setMuted(qint32 routeDevice, bool muted) { ); // clang-format on - qCInfo(logDevice) << "Changed muted state of" << this << "on route device" << routeDevice + qCInfo(logDevice) << "Changing muted state of" << this << "on route device" << routeDevice << "to" << muted; return props; }); } +void PwDevice::waitForDevice() { this->mWaitingForDevice = true; } +bool PwDevice::waitingForDevice() const { return this->mWaitingForDevice; } + bool PwDevice::setRouteProps( qint32 routeDevice, const std::function& propsCallback diff --git a/src/services/pipewire/device.hpp b/src/services/pipewire/device.hpp index 31f32f0f..ed6b6c53 100644 --- a/src/services/pipewire/device.hpp +++ b/src/services/pipewire/device.hpp @@ -28,6 +28,12 @@ public: bool setVolumes(qint32 routeDevice, const QVector& volumes); bool setMuted(qint32 routeDevice, bool muted); + void waitForDevice(); + [[nodiscard]] bool waitingForDevice() const; + +signals: + void deviceReady(); + private slots: void polled(); @@ -44,6 +50,8 @@ private: bool setRouteProps(qint32 routeDevice, const std::function& propsCallback); + bool mWaitingForDevice = false; + bool deviceResponded = false; SpaHook listener; }; diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index 280c450a..2c14ef04 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -216,13 +215,25 @@ void PwNode::onParam( } } +PwNodeBoundAudio::PwNodeBoundAudio(PwNode* node): node(node) { + if (node->device) { + QObject::connect(node->device, &PwDevice::deviceReady, this, &PwNodeBoundAudio::onDeviceReady); + } +} + 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++) { auto& param = info->params[i]; // NOLINT - if (param.id == SPA_PARAM_Props && (param.flags & SPA_PARAM_INFO_READ) != 0) { - pw_node_enum_params(this->node->proxy(), 0, param.id, 0, UINT32_MAX, nullptr); + if (param.id == SPA_PARAM_Props) { + if ((param.flags & SPA_PARAM_INFO_READWRITE) == SPA_PARAM_INFO_READWRITE) { + qCDebug(logNode) << "Enumerating props param for" << this; + pw_node_enum_params(this->node->proxy(), 0, param.id, 0, UINT32_MAX, nullptr); + } else { + qCWarning(logNode) << "Unable to enumerate props param for" << this + << "as the param does not have read+write permissions."; + } } } } @@ -230,84 +241,53 @@ void PwNodeBoundAudio::onInfo(const pw_node_info* info) { void PwNodeBoundAudio::onSpaParam(quint32 id, quint32 index, const spa_pod* param) { if (id == SPA_PARAM_Props && index == 0) { - this->updateVolumeFromParam(param); - this->updateMutedFromParam(param); + this->updateVolumeProps(param); } } -void PwNodeBoundAudio::updateVolumeFromParam(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); +void PwNodeBoundAudio::updateVolumeProps(const spa_pod* param) { + auto volumeProps = PwVolumeProps::parseSpaPod(param); - const auto* volumes = reinterpret_cast(&volumesProp->value); // NOLINT - const auto* channels = reinterpret_cast(&channelsProp->value); // NOLINT - - auto volumesVec = QVector(); - auto channelsVec = QVector(); - - 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); // NOLINT - auto visual = std::cbrt(linear); - volumesVec.push_back(visual); - } - - SPA_POD_ARRAY_FOREACH(channels, iter) { - channelsVec.push_back(*reinterpret_cast(iter)); // NOLINT - } - - if (volumesVec.size() != channelsVec.size()) { + if (volumeProps.volumes.size() != volumeProps.channels.size()) { qCWarning(logNode) << "Cannot update volume props of" << this->node << "- channelVolumes and channelMap are not the same size. Sizes:" - << volumesVec.size() << channelsVec.size(); + << volumeProps.volumes.size() << volumeProps.channels.size(); return; } // It is important that the lengths of channels and volumes stay in sync whenever you read them. auto channelsChanged = false; auto volumesChanged = false; + auto mutedChanged = false; - if (this->mChannels != channelsVec) { - this->mChannels = channelsVec; + if (this->mChannels != volumeProps.channels) { + this->mChannels = volumeProps.channels; channelsChanged = true; qCInfo(logNode) << "Got updated channels of" << this->node << '-' << this->mChannels; } - if (this->mVolumes != volumesVec) { - this->mVolumes = volumesVec; + if (this->mVolumes != volumeProps.volumes) { + this->mVolumes = volumeProps.volumes; volumesChanged = true; qCInfo(logNode) << "Got updated volumes of" << this->node << '-' << this->mVolumes; } + if (volumeProps.mute != this->mMuted) { + this->mMuted = volumeProps.mute; + mutedChanged = true; + qCInfo(logNode) << "Got updated mute status of" << this->node << '-' << volumeProps.mute; + } + if (channelsChanged) emit this->channelsChanged(); if (volumesChanged) emit this->volumesChanged(); -} - -void PwNodeBoundAudio::updateMutedFromParam(const spa_pod* param) { - auto parser = spa_pod_parser(); - spa_pod_parser_pod(&parser, param); - - auto muted = false; - - // clang-format off - quint32 id = SPA_PARAM_Props; - spa_pod_parser_get_object( - &parser, SPA_TYPE_OBJECT_Props, &id, - SPA_PROP_mute, SPA_POD_Bool(&muted) - ); - // clang-format on - - if (muted != this->mMuted) { - qCInfo(logNode) << "Got updated mute status of" << this->node << '-' << muted; - this->mMuted = muted; - emit this->mutedChanged(); - } + if (mutedChanged) emit this->mutedChanged(); } void PwNodeBoundAudio::onUnbind() { this->mChannels.clear(); this->mVolumes.clear(); + this->mDeviceVolumes.clear(); + this->waitingVolumes.clear(); emit this->channelsChanged(); emit this->volumesChanged(); } @@ -323,11 +303,10 @@ void PwNodeBoundAudio::setMuted(bool muted) { if (muted == this->mMuted) return; if (this->node->device) { + qCInfo(logNode) << "Changing muted state of" << this->node << "to" << muted << "via device"; if (!this->node->device->setMuted(this->node->routeDevice, muted)) { return; } - - qCInfo(logNode) << "Changed muted state of" << this->node << "to" << muted << "via device"; } else { auto buffer = std::array(); auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size()); @@ -340,7 +319,7 @@ void PwNodeBoundAudio::setMuted(bool muted) { ); // clang-format on - qCInfo(logNode) << "Changed muted state of" << this->node << "to" << muted; + qCInfo(logNode) << "Changed muted state of" << this->node << "to" << muted << "via node"; pw_node_set_param(this->node->proxy(), SPA_PARAM_Props, 0, static_cast(pod)); } @@ -381,9 +360,14 @@ void PwNodeBoundAudio::setVolumes(const QVector& volumes) { return; } - if (volumes == this->mVolumes) return; + auto realVolumes = QVector(); + for (auto volume: volumes) { + realVolumes.push_back(volume < 0 ? 0 : volume); + } - if (volumes.length() != this->mVolumes.length()) { + if (realVolumes == this->mVolumes) return; + + if (realVolumes.length() != this->mVolumes.length()) { qCCritical(logNode) << "Tried to change node volumes for" << this->node << "from" << this->mVolumes << "to" << volumes << "which has a different length than the list of channels" @@ -392,17 +376,25 @@ void PwNodeBoundAudio::setVolumes(const QVector& volumes) { } if (this->node->device) { - if (!this->node->device->setVolumes(this->node->routeDevice, volumes)) { - return; - } + if (this->node->device->waitingForDevice()) { + qCInfo(logNode) << "Waiting to change volumes of" << this->node << "to" << realVolumes + << "via device"; + this->waitingVolumes = realVolumes; + } else { + qCInfo(logNode) << "Changing volumes of" << this->node << "to" << realVolumes << "via device"; + if (!this->node->device->setVolumes(this->node->routeDevice, realVolumes)) { + return; + } - qCInfo(logNode) << "Changed volumes of" << this->node << "to" << volumes << "via device"; + this->mDeviceVolumes = realVolumes; + this->node->device->waitForDevice(); + } } else { auto buffer = std::array(); auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size()); auto cubedVolumes = QVector(); - for (auto volume: volumes) { + for (auto volume: realVolumes) { cubedVolumes.push_back(volume * volume * volume); } @@ -413,12 +405,54 @@ void PwNodeBoundAudio::setVolumes(const QVector& volumes) { ); // clang-format on - qCInfo(logNode) << "Changed volumes of" << this->node << "to" << volumes; + qCInfo(logNode) << "Changing volumes of" << this->node << "to" << volumes << "via node"; pw_node_set_param(this->node->proxy(), SPA_PARAM_Props, 0, static_cast(pod)); } - this->mVolumes = volumes; + this->mVolumes = realVolumes; emit this->volumesChanged(); } +void PwNodeBoundAudio::onDeviceReady() { + if (!this->waitingVolumes.isEmpty()) { + if (this->waitingVolumes != this->mDeviceVolumes) { + qCInfo(logNode) << "Changing volumes of" << this->node << "to" << this->waitingVolumes + << "via device (delayed)"; + + this->node->device->setVolumes(this->node->routeDevice, this->waitingVolumes); + this->mDeviceVolumes = this->waitingVolumes; + this->mVolumes = this->waitingVolumes; + } + + this->waitingVolumes.clear(); + } +} + +PwVolumeProps PwVolumeProps::parseSpaPod(const spa_pod* param) { + auto props = PwVolumeProps(); + + 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* volumes = reinterpret_cast(&volumesProp->value); // NOLINT + const auto* channels = reinterpret_cast(&channelsProp->value); // NOLINT + + 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); // NOLINT + auto visual = std::cbrt(linear); + props.volumes.push_back(visual); + } + + SPA_POD_ARRAY_FOREACH(channels, iter) { + props.channels.push_back(*reinterpret_cast(iter)); // NOLINT + } + + spa_pod_get_bool(&muteProp->value, &props.mute); + + return props; +} + } // namespace qs::service::pipewire diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index b8165137..3477fd92 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -94,6 +94,14 @@ enum class PwNodeType { class PwNode; +struct PwVolumeProps { + QVector channels; + QVector volumes; + bool mute = false; + + static PwVolumeProps parseSpaPod(const spa_pod* param); +}; + class PwNodeBoundData { public: PwNodeBoundData() = default; @@ -111,7 +119,7 @@ class PwNodeBoundAudio Q_OBJECT; public: - explicit PwNodeBoundAudio(PwNode* node): node(node) {} + explicit PwNodeBoundAudio(PwNode* node); void onInfo(const pw_node_info* info) override; void onSpaParam(quint32 id, quint32 index, const spa_pod* param) override; @@ -133,13 +141,17 @@ signals: void channelsChanged(); void mutedChanged(); +private slots: + void onDeviceReady(); + private: - void updateVolumeFromParam(const spa_pod* param); - void updateMutedFromParam(const spa_pod* param); + void updateVolumeProps(const spa_pod* param); bool mMuted = false; QVector mChannels; QVector mVolumes; + QVector mDeviceVolumes; + QVector waitingVolumes; PwNode* node; }; From e327d6750d0cb233bcfec757b216a7555945e4cf Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 28 Aug 2024 11:32:14 -0700 Subject: [PATCH 137/305] build: fix -DCRASH_REPORTER=OFF --- BUILD.md | 16 ++++++++++------ src/core/CMakeLists.txt | 2 ++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/BUILD.md b/BUILD.md index 4dcf9ac0..f32a2335 100644 --- a/BUILD.md +++ b/BUILD.md @@ -18,16 +18,20 @@ At least Qt 6.6 is required. All features are enabled by default and some have their own dependencies. -##### Additional note to packagers: -If your package manager supports enabling some features but not others, -we recommend not exposing the subfeatures and just the main ones that introduce -new dependencies: `wayland`, `x11`, `pipewire`, `hyprland` - ### QML Library -If you wish to use a linter or similar tools, you will need the QML Modules for it to pick up on the types. +If you wish to use a linter or similar tools, you will need the QML Modules for it +to pick up on the types. To disable: `-DINSTALL_QML_LIB=OFF` +### Crash Reporter +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` + +Dependencies: `google-breakpad` ### Jemalloc We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 0d6f7211..800da496 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -47,6 +47,8 @@ qt_add_library(quickshell-core STATIC if (CRASH_REPORTER) set(CRASH_REPORTER_DEF 1) +else() + set(CRASH_REPORTER_DEF 0) endif() add_library(quickshell-build INTERFACE) From 9967e2e03bd72797acb636b36a56e7d6ac9ce22a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 28 Aug 2024 15:30:02 -0700 Subject: [PATCH 138/305] core: fix UAF when calling Qt.quit or Qt.exit A pointer to the last generation had shutdown() called on it after deletion. --- src/core/rootwrapper.cpp | 4 ++++ src/core/rootwrapper.hpp | 1 + 2 files changed, 5 insertions(+) diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index c4c5e711..ec0d0a89 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -96,6 +96,7 @@ void RootWrapper::reloadGraph(bool hard) { generation->onReload(hard ? nullptr : this->generation); if (hard && this->generation != nullptr) { + QObject::disconnect(this->generation, nullptr, this, nullptr); this->generation->destroy(); } @@ -103,6 +104,7 @@ void RootWrapper::reloadGraph(bool hard) { qInfo() << "Configuration Loaded"; + QObject::connect(this->generation, &QObject::destroyed, this, &RootWrapper::generationDestroyed); QObject::connect( this->generation, &EngineGeneration::filesChanged, @@ -117,6 +119,8 @@ void RootWrapper::reloadGraph(bool hard) { } } +void RootWrapper::generationDestroyed() { this->generation = nullptr; } + void RootWrapper::onWatchFilesChanged() { auto watchFiles = QuickshellSettings::instance()->watchFiles(); if (this->generation != nullptr) { diff --git a/src/core/rootwrapper.hpp b/src/core/rootwrapper.hpp index 46603097..02d7a143 100644 --- a/src/core/rootwrapper.hpp +++ b/src/core/rootwrapper.hpp @@ -19,6 +19,7 @@ public: void reloadGraph(bool hard); private slots: + void generationDestroyed(); void onWatchFilesChanged(); void onWatchedFilesChanged(); From af29bc277e8b5c5502f6d38a6de966e75d9f8c96 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 28 Aug 2024 17:53:39 -0700 Subject: [PATCH 139/305] core: add by-pid symlinks to instance runtime paths --- src/core/main.cpp | 1 + src/core/paths.cpp | 80 +++++++++++++++++++++++++++++++++++++++------- src/core/paths.hpp | 4 +++ 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/src/core/main.cpp b/src/core/main.cpp index 25257b5f..c886cc1d 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -535,6 +535,7 @@ int qs_main(int argc, char** argv) { } QsPaths::init(shellId); + QsPaths::instance()->linkPidRunDir(); if (auto* cacheDir = QsPaths::instance()->cacheDir()) { auto qmlCacheDir = cacheDir->filePath("qml-engine-cache"); diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 8f63b3aa..7162da5c 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -1,4 +1,5 @@ #include "paths.hpp" +#include #include #include @@ -38,9 +39,11 @@ QDir* QsPaths::cacheDir() { qCDebug(logPaths) << "Initialized cache path:" << dir.path(); if (!dir.mkpath(".")) { - qCCritical(logPaths) << "Cannot create cache directory at" << dir.path(); + qCCritical(logPaths) << "Could not create cache directory at" << dir.path(); this->cacheState = DirState::Failed; + } else { + this->cacheState = DirState::Ready; } } @@ -48,23 +51,48 @@ QDir* QsPaths::cacheDir() { else return &this->mCacheDir; } -QDir* QsPaths::runDir() { - if (this->runState == DirState::Unknown) { +QDir* QsPaths::baseRunDir() { + if (this->baseRunState == DirState::Unknown) { auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR"); if (runtimeDir.isEmpty()) { runtimeDir = QString("/run/user/$1").arg(getuid()); qCInfo(logPaths) << "XDG_RUNTIME_DIR was not set, defaulting to" << runtimeDir; } - auto dir = QDir(runtimeDir); - dir = QDir(dir.filePath("quickshell")); - dir = QDir(dir.filePath(this->shellId)); - this->mRunDir = dir; + this->mBaseRunDir = QDir(runtimeDir); + this->mBaseRunDir = QDir(this->mBaseRunDir.filePath("quickshell")); + qCDebug(logPaths) << "Initialized base runtime path:" << this->mBaseRunDir.path(); - qCDebug(logPaths) << "Initialized runtime path:" << dir.path(); + if (!this->mBaseRunDir.mkpath(".")) { + qCCritical(logPaths) << "Could not create base runtime directory at" + << this->mBaseRunDir.path(); - if (!dir.mkpath(".")) { - qCCritical(logPaths) << "Cannot create runtime directory at" << dir.path(); + this->baseRunState = DirState::Failed; + } else { + this->baseRunState = DirState::Ready; + } + } + + if (this->baseRunState == DirState::Failed) return nullptr; + else return &this->mBaseRunDir; +} + +QDir* QsPaths::runDir() { + if (this->runState == DirState::Unknown) { + if (auto* baseRunDir = this->baseRunDir()) { + this->mRunDir = QDir(baseRunDir->filePath(this->shellId)); + + qCDebug(logPaths) << "Initialized runtime path:" << this->mRunDir.path(); + + if (!this->mRunDir.mkpath(".")) { + qCCritical(logPaths) << "Could not create runtime directory at" << this->mRunDir.path(); + this->runState = DirState::Failed; + } else { + this->runState = DirState::Ready; + } + } else { + qCCritical(logPaths) << "Could not create shell runtime path as it was not possible to " + "create the base runtime path."; this->runState = DirState::Failed; } @@ -89,9 +117,11 @@ QDir* QsPaths::instanceRunDir() { qCDebug(logPaths) << "Initialized instance runtime path:" << this->mInstanceRunDir.path(); if (!this->mInstanceRunDir.mkpath(".")) { - qCCritical(logPaths) << "Cannot create instance runtime directory at" + qCCritical(logPaths) << "Could not create instance runtime directory at" << this->mInstanceRunDir.path(); this->instanceRunState = DirState::Failed; + } else { + this->instanceRunState = DirState::Ready; } } } @@ -99,3 +129,31 @@ QDir* QsPaths::instanceRunDir() { if (this->runState == DirState::Failed) return nullptr; else return &this->mInstanceRunDir; } + +void QsPaths::linkPidRunDir() { + if (auto* runDir = this->instanceRunDir()) { + auto pidDir = QDir(this->baseRunDir()->filePath("by-pid")); + + if (!pidDir.mkpath(".")) { + qCCritical(logPaths) << "Could not create PID symlink directory."; + return; + } + + auto pidPath = pidDir.filePath(QString::number(getpid())); + + QFile::remove(pidPath); + auto r = symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, pidPath.toStdString().c_str()); + + if (r != 0) { + qCCritical(logPaths).nospace() + << "Could not create PID symlink to " << runDir->path() << " at " << pidPath + << " with error code " << errno << ": " << qt_error_string(); + } else { + qCDebug(logPaths) << "Created PID symlink" << pidPath << "to instance runtime path" + << runDir->path(); + } + } else { + qCCritical(logPaths) << "Could not create PID symlink to runtime directory, as the runtime " + "directory could not be created."; + } +} diff --git a/src/core/paths.hpp b/src/core/paths.hpp index 9716e299..866a33cf 100644 --- a/src/core/paths.hpp +++ b/src/core/paths.hpp @@ -9,8 +9,10 @@ public: static QDir crashDir(const QString& shellId, const QDateTime& launchTime); QDir* cacheDir(); + QDir* baseRunDir(); QDir* runDir(); QDir* instanceRunDir(); + void linkPidRunDir(); private: enum class DirState { @@ -21,9 +23,11 @@ private: QString shellId; QDir mCacheDir; + QDir mBaseRunDir; QDir mRunDir; QDir mInstanceRunDir; DirState cacheState = DirState::Unknown; + DirState baseRunState = DirState::Unknown; DirState runState = DirState::Unknown; DirState instanceRunState = DirState::Unknown; }; From a116f39c63236ff215fc5056e9464506105057b2 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 28 Aug 2024 22:05:21 -0700 Subject: [PATCH 140/305] core/desktopentry: prioritize fallback keys over mismatched keys The fallback key will now be selected when there isn't a more specific key to select, instead of the first key. --- src/core/desktopentry.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 0fa2d8f3..3714df01 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -142,7 +142,7 @@ void DesktopEntry::parseEntry(const QString& text) { auto splitIdx = line.indexOf(u'='); if (splitIdx == -1) { - qCDebug(logDesktopEntry) << "Encountered invalid line in desktop entry (no =)" << line; + qCWarning(logDesktopEntry) << "Encountered invalid line in desktop entry (no =)" << line; continue; } @@ -159,7 +159,10 @@ void DesktopEntry::parseEntry(const QString& text) { if (entries.contains(key)) { const auto& old = entries.value(key); - if (system.matchScore(locale) > system.matchScore(old.first)) { + auto oldScore = system.matchScore(old.first); + auto newScore = system.matchScore(locale); + + if (newScore > oldScore || (oldScore == 0 && !locale.isValid())) { entries.insert(key, qMakePair(locale, value)); } } else { From f6ad617b67ce735745496439af6f09d1f0279bfb Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 29 Aug 2024 13:17:24 -0700 Subject: [PATCH 141/305] service/pipewire: ignore insignificant device volume changes Fixes devices getting stuck in a "waiting for volume change acknowledgement" state forever. --- src/services/pipewire/node.cpp | 38 +++++++++++++++++++++++++++------- src/services/pipewire/node.hpp | 1 + 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index 2c14ef04..5eb38e7d 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -228,10 +228,10 @@ void PwNodeBoundAudio::onInfo(const pw_node_info* info) { if (param.id == SPA_PARAM_Props) { if ((param.flags & SPA_PARAM_INFO_READWRITE) == SPA_PARAM_INFO_READWRITE) { - qCDebug(logNode) << "Enumerating props param for" << this; + qCDebug(logNode) << "Enumerating props param for" << this->node; pw_node_enum_params(this->node->proxy(), 0, param.id, 0, UINT32_MAX, nullptr); } else { - qCWarning(logNode) << "Unable to enumerate props param for" << this + qCWarning(logNode) << "Unable to enumerate props param for" << this->node << "as the param does not have read+write permissions."; } } @@ -266,6 +266,10 @@ void PwNodeBoundAudio::updateVolumeProps(const spa_pod* param) { qCInfo(logNode) << "Got updated channels of" << this->node << '-' << this->mChannels; } + if (this->mServerVolumes != volumeProps.volumes) { + this->mServerVolumes = volumeProps.volumes; + } + if (this->mVolumes != volumeProps.volumes) { this->mVolumes = volumeProps.volumes; volumesChanged = true; @@ -286,6 +290,7 @@ void PwNodeBoundAudio::updateVolumeProps(const spa_pod* param) { void PwNodeBoundAudio::onUnbind() { this->mChannels.clear(); this->mVolumes.clear(); + this->mServerVolumes.clear(); this->mDeviceVolumes.clear(); this->waitingVolumes.clear(); emit this->channelsChanged(); @@ -381,13 +386,32 @@ void PwNodeBoundAudio::setVolumes(const QVector& volumes) { << "via device"; this->waitingVolumes = realVolumes; } else { - qCInfo(logNode) << "Changing volumes of" << this->node << "to" << realVolumes << "via device"; - if (!this->node->device->setVolumes(this->node->routeDevice, realVolumes)) { - return; + 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; + } } - this->mDeviceVolumes = realVolumes; - this->node->device->waitForDevice(); + 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) << "Ignoring volume change for" << this->node << "to" << realVolumes + << "from" << this->mServerVolumes + << "as it is a device node and the change is too small."; + } } } else { auto buffer = std::array(); diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index 3477fd92..29c02033 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -150,6 +150,7 @@ private: bool mMuted = false; QVector mChannels; QVector mVolumes; + QVector mServerVolumes; QVector mDeviceVolumes; QVector waitingVolumes; PwNode* node; From 77c5a2d569723b1a6207d443cac960f91856704c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 29 Aug 2024 14:11:40 -0700 Subject: [PATCH 142/305] build: add "qs" as a symlink to the "quickshell" binary --- CMakeLists.txt | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a7126ba..5fe5387e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,14 +121,6 @@ find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS}) qt_standard_project_setup(REQUIRES 6.6) set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules) -if (INSTALL_QML_LIB) - install( - DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules/ - DESTINATION ${CMAKE_INSTALL_LIBDIR}/qt-6/qml - FILES_MATCHING PATTERN "*" - ) -endif() - # pch breaks clang-tidy..... somehow if (NOT NO_PCH) file(GENERATE @@ -164,3 +156,18 @@ if (USE_JEMALLOC) pkg_check_modules(JEMALLOC REQUIRED jemalloc) target_link_libraries(quickshell PRIVATE ${JEMALLOC_LIBRARIES}) endif() + +if (INSTALL_QML_LIB) + install( + DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules/ + DESTINATION ${CMAKE_INSTALL_LIBDIR}/qt-6/qml + FILES_MATCHING PATTERN "*" + ) +endif() + +install(CODE " + execute_process( + COMMAND ${CMAKE_COMMAND} -E create_symlink \ + ${CMAKE_INSTALL_FULL_BINDIR}/quickshell ${CMAKE_INSTALL_FULL_BINDIR}/qs + ) +") From 60349f1894eed148c77b8a5be889fb65cd5b9355 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 29 Aug 2024 14:43:25 -0700 Subject: [PATCH 143/305] core: set application name to avoid bin name fallback --- src/core/main.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/main.cpp b/src/core/main.cpp index c886cc1d..84d778c4 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -53,6 +53,8 @@ struct CommandInfo { }; void processCommand(int argc, char** argv, CommandInfo& info) { + QCoreApplication::setApplicationName("quickshell"); + auto app = CLI::App(""); class QStringOption { From 3edb3f4efa531dc90339589d0f026f256dd563c5 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 30 Aug 2024 15:23:48 -0700 Subject: [PATCH 144/305] core/reloader: disconnect old generation before reloading Previously the old generation was not disconnected, causing the root wrapper's generation pointer to be nulled when pointing to the new generation, leaving multiple shell versions running simultaneously. --- src/core/rootwrapper.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index ec0d0a89..0365b523 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -17,10 +17,10 @@ #include "shell.hpp" RootWrapper::RootWrapper(QString rootPath, QString shellId) - : QObject(nullptr) - , rootPath(std::move(rootPath)) - , shellId(std::move(shellId)) - , originalWorkingDirectory(QDir::current().absolutePath()) { + : 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 @@ -92,11 +92,14 @@ void RootWrapper::reloadGraph(bool hard) { component.completeCreate(); + if (this->generation) { + QObject::disconnect(this->generation, nullptr, this, nullptr); + } + auto isReload = this->generation != nullptr; generation->onReload(hard ? nullptr : this->generation); - if (hard && this->generation != nullptr) { - QObject::disconnect(this->generation, nullptr, this, nullptr); + if (hard && this->generation) { this->generation->destroy(); } From 13b6eeaa22aa4b4b1469d04c57bb850884cf370e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 30 Aug 2024 16:23:32 -0700 Subject: [PATCH 145/305] core/reloader: null generation ref in reloadables on destruction On the post-reload reloadable initialzation path, a timer is used to delay reload(). This change fixes a UAF when switching generations while that timer is running. --- src/core/reload.cpp | 10 ++++++++++ src/core/reload.hpp | 1 + 2 files changed, 11 insertions(+) diff --git a/src/core/reload.cpp b/src/core/reload.cpp index 10627c0a..25ab33f3 100644 --- a/src/core/reload.cpp +++ b/src/core/reload.cpp @@ -16,6 +16,15 @@ void Reloadable::componentComplete() { if (this->engineGeneration->reloadComplete) { // Delayed due to Component.onCompleted running after QQmlParserStatus::componentComplete. QTimer::singleShot(0, this, &Reloadable::onReloadFinished); + + // This only matters for preventing the above timer from UAFing the generation, + // so it isn't connected anywhere else. + QObject::connect( + this->engineGeneration, + &QObject::destroyed, + this, + &Reloadable::onGenerationDestroyed + ); } else { QObject::connect( this->engineGeneration, @@ -43,6 +52,7 @@ void Reloadable::reload(QObject* oldInstance) { } void Reloadable::onReloadFinished() { this->reload(nullptr); } +void Reloadable::onGenerationDestroyed() { this->engineGeneration = nullptr; } void ReloadPropagator::onReload(QObject* oldInstance) { auto* old = qobject_cast(oldInstance); diff --git a/src/core/reload.hpp b/src/core/reload.hpp index 378a9520..560c8bd0 100644 --- a/src/core/reload.hpp +++ b/src/core/reload.hpp @@ -71,6 +71,7 @@ public: private slots: void onReloadFinished(); + void onGenerationDestroyed(); protected: // Called unconditionally in the reload phase, with nullptr if no source could be determined. From da043e092a871419fdc94698b1d71177b26b8a74 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 30 Aug 2024 21:45:20 -0700 Subject: [PATCH 146/305] core/ipc: add ipc server/client Currently can only kill a remote instance. --- src/core/CMakeLists.txt | 3 +- src/core/crashinfo.cpp | 19 -- src/core/instanceinfo.cpp | 31 +++ src/core/{crashinfo.hpp => instanceinfo.hpp} | 10 + src/core/ipc.cpp | 119 ++++++++++ src/core/ipc.hpp | 74 ++++++ src/core/logging.cpp | 64 ++--- src/core/logging.hpp | 2 + src/core/main.cpp | 236 +++++++++++++++++-- src/core/paths.cpp | 218 ++++++++++++++--- src/core/paths.hpp | 28 ++- src/crash/handler.cpp | 4 +- src/crash/handler.hpp | 4 +- src/crash/main.cpp | 18 +- 14 files changed, 710 insertions(+), 120 deletions(-) delete mode 100644 src/core/crashinfo.cpp create mode 100644 src/core/instanceinfo.cpp rename src/core/{crashinfo.hpp => instanceinfo.hpp} (66%) create mode 100644 src/core/ipc.cpp create mode 100644 src/core/ipc.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 800da496..75c16537 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -41,8 +41,9 @@ qt_add_library(quickshell-core STATIC clock.cpp logging.cpp paths.cpp - crashinfo.cpp + instanceinfo.cpp common.cpp + ipc.cpp ) if (CRASH_REPORTER) diff --git a/src/core/crashinfo.cpp b/src/core/crashinfo.cpp deleted file mode 100644 index f441530f..00000000 --- a/src/core/crashinfo.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "crashinfo.hpp" - -#include - -QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) { - stream << info.configPath << info.shellId << info.launchTime << info.noColor; - return stream; -} - -QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) { - stream >> info.configPath >> info.shellId >> info.launchTime >> info.noColor; - return stream; -} - -namespace qs::crash { - -CrashInfo CrashInfo::INSTANCE = {}; // NOLINT - -} diff --git a/src/core/instanceinfo.cpp b/src/core/instanceinfo.cpp new file mode 100644 index 00000000..794212b8 --- /dev/null +++ b/src/core/instanceinfo.cpp @@ -0,0 +1,31 @@ +#include "instanceinfo.hpp" + +#include + +QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) { + stream << info.instanceId << info.configPath << info.shellId << info.launchTime; + return stream; +} + +QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) { + stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime; + return stream; +} + +QDataStream& operator<<(QDataStream& stream, const RelaunchInfo& info) { + stream << info.instance << info.noColor << info.sparseLogsOnly; + return stream; +} + +QDataStream& operator>>(QDataStream& stream, RelaunchInfo& info) { + stream >> info.instance >> info.noColor >> info.sparseLogsOnly; + return stream; +} + +InstanceInfo InstanceInfo::CURRENT = {}; // NOLINT + +namespace qs::crash { + +CrashInfo CrashInfo::INSTANCE = {}; // NOLINT + +} diff --git a/src/core/crashinfo.hpp b/src/core/instanceinfo.hpp similarity index 66% rename from src/core/crashinfo.hpp rename to src/core/instanceinfo.hpp index a867563f..21bb62d3 100644 --- a/src/core/crashinfo.hpp +++ b/src/core/instanceinfo.hpp @@ -4,10 +4,17 @@ #include struct InstanceInfo { + QString instanceId; QString configPath; QString shellId; QString initialWorkdir; QDateTime launchTime; + + static InstanceInfo CURRENT; // NOLINT +}; + +struct RelaunchInfo { + InstanceInfo instance; bool noColor = false; bool sparseLogsOnly = false; }; @@ -15,6 +22,9 @@ struct InstanceInfo { QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info); QDataStream& operator>>(QDataStream& stream, InstanceInfo& info); +QDataStream& operator<<(QDataStream& stream, const RelaunchInfo& info); +QDataStream& operator>>(QDataStream& stream, RelaunchInfo& info); + namespace qs::crash { struct CrashInfo { diff --git a/src/core/ipc.cpp b/src/core/ipc.cpp new file mode 100644 index 00000000..406d0909 --- /dev/null +++ b/src/core/ipc.cpp @@ -0,0 +1,119 @@ +#include "ipc.hpp" +#include + +#include +#include +#include +#include +#include + +#include "generation.hpp" +#include "paths.hpp" + +namespace qs::ipc { + +Q_LOGGING_CATEGORY(logIpc, "quickshell.ipc", QtWarningMsg); + +IpcServer::IpcServer(const QString& path) { + QObject::connect(&this->server, &QLocalServer::newConnection, this, &IpcServer::onNewConnection); + + QLocalServer::removeServer(path); + + if (!this->server.listen(path)) { + qCCritical(logIpc) << "Failed to start IPC server on path" << path; + return; + } + + qCInfo(logIpc) << "Started IPC server on path" << path; +} + +void IpcServer::start() { + if (auto* run = QsPaths::instance()->instanceRunDir()) { + auto path = run->filePath("ipc.sock"); + new IpcServer(path); + } else { + qCCritical(logIpc + ) << "Could not start IPC server as the instance runtime path could not be created."; + } +} + +void IpcServer::onNewConnection() { + while (auto* connection = this->server.nextPendingConnection()) { + new IpcServerConnection(connection, this); + } +} + +IpcServerConnection::IpcServerConnection(QLocalSocket* socket, IpcServer* server) + : QObject(server) + , socket(socket) { + socket->setParent(this); + this->stream.setDevice(socket); + QObject::connect(socket, &QLocalSocket::disconnected, this, &IpcServerConnection::onDisconnected); + QObject::connect(socket, &QLocalSocket::readyRead, this, &IpcServerConnection::onReadyRead); + + qCInfo(logIpc) << "New IPC connection" << this; +} + +void IpcServerConnection::onDisconnected() { + qCInfo(logIpc) << "IPC connection disconnected" << this; +} + +void IpcServerConnection::onReadyRead() { + this->stream.startTransaction(); + + this->stream.startTransaction(); + auto command = IpcCommand::Unknown; + this->stream >> command; + if (!this->stream.commitTransaction()) return; + + switch (command) { + case IpcCommand::Kill: + qInfo() << "Exiting due to IPC request."; + EngineGeneration::currentGeneration()->quit(); + break; + default: + qCCritical(logIpc) << "Received invalid IPC command from" << this; + this->socket->disconnectFromServer(); + break; + } + + if (!this->stream.commitTransaction()) return; +} + +IpcClient::IpcClient(const QString& path) { + QObject::connect(&this->socket, &QLocalSocket::connected, this, &IpcClient::connected); + QObject::connect(&this->socket, &QLocalSocket::disconnected, this, &IpcClient::disconnected); + QObject::connect(&this->socket, &QLocalSocket::errorOccurred, this, &IpcClient::onError); + + this->socket.connectToServer(path); + this->stream.setDevice(&this->socket); +} + +bool IpcClient::isConnected() const { return this->socket.isValid(); } + +void IpcClient::waitForConnected() { this->socket.waitForConnected(); } +void IpcClient::waitForDisconnected() { this->socket.waitForDisconnected(); } + +void IpcClient::kill() { + qCDebug(logIpc) << "Sending kill command..."; + this->stream << IpcCommand::Kill; + this->socket.flush(); +} + +void IpcClient::onError(QLocalSocket::LocalSocketError error) { + qCCritical(logIpc) << "Socket Error" << error; +} + +bool IpcClient::connect(const QString& id, const std::function& callback) { + auto path = QsPaths::ipcPath(id); + auto client = IpcClient(path); + qCDebug(logIpc) << "Connecting to instance" << id << "at" << path; + + client.waitForConnected(); + if (!client.isConnected()) return false; + qCDebug(logIpc) << "Connected."; + + callback(client); + return true; +} +} // namespace qs::ipc diff --git a/src/core/ipc.hpp b/src/core/ipc.hpp new file mode 100644 index 00000000..a62f7b77 --- /dev/null +++ b/src/core/ipc.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace qs::ipc { + +enum class IpcCommand : quint8 { + Unknown = 0, + Kill, +}; + +class IpcServer: public QObject { + Q_OBJECT; + +public: + explicit IpcServer(const QString& path); + + static void start(); + +private slots: + void onNewConnection(); + +private: + QLocalServer server; +}; + +class IpcServerConnection: public QObject { + Q_OBJECT; + +public: + explicit IpcServerConnection(QLocalSocket* socket, IpcServer* server); + +private slots: + void onDisconnected(); + void onReadyRead(); + +private: + QLocalSocket* socket; + QDataStream stream; +}; + +class IpcClient: public QObject { + Q_OBJECT; + +public: + explicit IpcClient(const QString& path); + + [[nodiscard]] bool isConnected() const; + void waitForConnected(); + void waitForDisconnected(); + + void kill(); + + [[nodiscard]] static bool + connect(const QString& id, const std::function& callback); + +signals: + void connected(); + void disconnected(); + +private slots: + static void onError(QLocalSocket::LocalSocketError error); + +private: + QLocalSocket socket; + QDataStream stream; +}; + +} // namespace qs::ipc diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 887e145f..fe319f54 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -24,11 +24,13 @@ #include #include -#include "crashinfo.hpp" +#include "instanceinfo.hpp" #include "logging_p.hpp" #include "logging_qtprivate.cpp" // NOLINT #include "paths.hpp" +Q_LOGGING_CATEGORY(logBare, "quickshell.bare"); + namespace qs::log { Q_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg); @@ -53,37 +55,41 @@ void LogMessage::formatMessage( stream << msg.time.toString("yyyy-MM-dd hh:mm:ss.zzz"); } - if (color) { - switch (msg.type) { - case QtDebugMsg: stream << "\033[34m DEBUG"; break; - case QtInfoMsg: stream << "\033[32m INFO"; break; - case QtWarningMsg: stream << "\033[33m WARN"; break; - case QtCriticalMsg: stream << "\033[31m ERROR"; break; - case QtFatalMsg: stream << "\033[31m FATAL"; break; - } + if (msg.category == "quickshell.bare") { + stream << msg.body; } else { - switch (msg.type) { - case QtDebugMsg: stream << " DEBUG"; break; - case QtInfoMsg: stream << " INFO"; break; - case QtWarningMsg: stream << " WARN"; break; - case QtCriticalMsg: stream << " ERROR"; break; - case QtFatalMsg: stream << " FATAL"; break; + if (color) { + switch (msg.type) { + case QtDebugMsg: stream << "\033[34m DEBUG"; break; + case QtInfoMsg: stream << "\033[32m INFO"; break; + case QtWarningMsg: stream << "\033[33m WARN"; break; + case QtCriticalMsg: stream << "\033[31m ERROR"; break; + case QtFatalMsg: stream << "\033[31m FATAL"; break; + } + } else { + switch (msg.type) { + case QtDebugMsg: stream << " DEBUG"; break; + case QtInfoMsg: stream << " INFO"; break; + case QtWarningMsg: stream << " WARN"; break; + case QtCriticalMsg: stream << " ERROR"; break; + case QtFatalMsg: stream << " FATAL"; break; + } } + + const auto isDefault = msg.category == "default"; + + if (color && !isDefault && msg.type != QtFatalMsg) stream << "\033[97m"; + + if (!isDefault) { + stream << ' ' << msg.category; + } + + if (color && msg.type != QtFatalMsg) stream << "\033[0m"; + + stream << ": " << msg.body; + + if (color && msg.type == QtFatalMsg) stream << "\033[0m"; } - - const auto isDefault = msg.category == "default"; - - if (color && !isDefault && msg.type != QtFatalMsg) stream << "\033[97m"; - - if (!isDefault) { - stream << ' ' << msg.category; - } - - if (color && msg.type != QtFatalMsg) stream << "\033[0m"; - - stream << ": " << msg.body; - - if (color && msg.type == QtFatalMsg) stream << "\033[0m"; } bool CategoryFilter::shouldDisplay(QtMsgType type) const { diff --git a/src/core/logging.hpp b/src/core/logging.hpp index 88fd6716..618a1744 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -11,6 +11,8 @@ #include #include +Q_DECLARE_LOGGING_CATEGORY(logBare); + namespace qs::log { struct LogMessage { diff --git a/src/core/main.cpp b/src/core/main.cpp index 84d778c4..68d0a40d 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -1,4 +1,6 @@ #include "main.hpp" +#include +#include #include #include #include @@ -8,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -17,8 +20,12 @@ #include #include #include +#include +#include +#include #include #include +#include #include #include #include @@ -27,10 +34,13 @@ #include #include #include +#include +#include #include "build.hpp" #include "common.hpp" -#include "crashinfo.hpp" +#include "instanceinfo.hpp" +#include "ipc.hpp" #include "logging.hpp" #include "paths.hpp" #include "plugin.hpp" @@ -40,6 +50,8 @@ #include "../crash/main.hpp" #endif +using qs::ipc::IpcClient; + struct CommandInfo { QString configPath; QString manifestPath; @@ -52,6 +64,8 @@ struct CommandInfo { bool& sparseLogsOnly; }; +QString commandConfigPath(QString path, QString manifest, QString config, bool printInfo); + void processCommand(int argc, char** argv, CommandInfo& info) { QCoreApplication::setApplicationName("quickshell"); @@ -162,12 +176,97 @@ void processCommand(int argc, char** argv, CommandInfo& info) { readLog->add_flag("--no-time", logNoTime, "Do not print timestamps of log messages."); readLog->add_flag("--no-color", info.noColor, "Do not color the log output. (Env:NO_COLOR)"); + /// --- + QStringOption instanceId; + pid_t instancePid = -1; + + auto sortInstances = [](QVector& list) { + std::sort(list.begin(), list.end(), [](const InstanceLockInfo& a, const InstanceLockInfo& b) { + return a.instance.launchTime < b.instance.launchTime; + }); + }; + + auto selectInstance = [&]() { + auto* basePath = QsPaths::instance()->baseRunDir(); + if (!basePath) exit(-1); // NOLINT + + QString path; + InstanceLockInfo instance; + + if (instancePid != -1) { + path = QDir(basePath->filePath("by-pid")).filePath(QString::number(instancePid)); + if (!QsPaths::checkLock(path, &instance)) { + qCInfo(logBare) << "No instance found for pid" << instancePid; + exit(-1); // NOLINT + } + } else if (!(*instanceId).isEmpty()) { + path = basePath->filePath("by-pid"); + auto instances = QsPaths::collectInstances(path); + + auto itr = + std::remove_if(instances.begin(), instances.end(), [&](const InstanceLockInfo& info) { + return !info.instance.instanceId.startsWith(*instanceId); + }); + + instances.erase(itr, instances.end()); + + if (instances.isEmpty()) { + qCInfo(logBare) << "No running instances start with" << *instanceId; + } else if (instances.length() != 1) { + qCInfo(logBare) << "More than one instance starts with" << *instanceId; + + for (auto& instance: instances) { + qCInfo(logBare).noquote() << " -" << instance.instance.instanceId; + } + + exit(-1); // NOLINT + } else { + instance = instances.value(0); + } + } else { + auto configFilePath = + commandConfigPath(info.configPath, info.manifestPath, info.configName, info.printInfo); + + auto pathId = + QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + + path = QDir(basePath->filePath("by-path")).filePath(pathId); + + auto instances = QsPaths::collectInstances(path); + sortInstances(instances); + + if (instances.isEmpty()) { + qCInfo(logBare) << "No running instances for" << configFilePath; + exit(-1); // NOLINT + } + + instance = instances.value(0); + } + + return instance; + }; + + auto* instances = + app.add_subcommand("instances", "List running quickshell instances.")->fallthrough(); + + auto* allInstances = + instances->add_flag("-a,--all", "List all instances instead of just the current config."); + + auto* instancesJson = instances->add_flag("-j,--json", "Output the list as a json."); + + auto* kill = app.add_subcommand("kill", "Kill an instance.")->fallthrough(); + auto* kInstance = app.add_option("-i,--instance", instanceId, "The instance id to kill."); + app.add_option("-p,--pid", instancePid, "The process id to kill.")->excludes(kInstance); + try { app.parse(argc, argv); } catch (const CLI::ParseError& e) { exit(app.exit(e)); // NOLINT }; + // Start log manager - has to happen with an active event loop or offthread can't be started. + LogManager::init(!info.noColor, info.sparseLogsOnly); + if (*printVersion) { std::cout << "quickshell pre-release, revision: " << GIT_REVISION << std::endl; exit(0); // NOLINT @@ -183,6 +282,86 @@ void processCommand(int argc, char** argv, CommandInfo& info) { exit( // NOLINT qs::log::readEncodedLogs(&file, !logNoTime, *logFilter) ? 0 : -1 ); + } else if (*instances) { + auto* basePath = QsPaths::instance()->baseRunDir(); + if (!basePath) exit(-1); // NOLINT + + QString path; + QString configFilePath; + if (*allInstances) { + path = basePath->filePath("by-pid"); + } else { + configFilePath = + commandConfigPath(info.configPath, info.manifestPath, info.configName, info.printInfo); + + auto pathId = + QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + + path = QDir(basePath->filePath("by-path")).filePath(pathId); + } + + auto instances = QsPaths::collectInstances(path); + + if (instances.isEmpty()) { + if (*allInstances) { + qCInfo(logBare) << "No running instances."; + } else { + qCInfo(logBare) << "No running instances for" << configFilePath; + qCInfo(logBare) << "Use --all to list all instances."; + } + } else { + sortInstances(instances); + + if (*instancesJson) { + auto array = QJsonArray(); + + for (auto& instance: instances) { + auto json = QJsonObject(); + + json["id"] = instance.instance.instanceId; + json["pid"] = instance.pid; + json["shell_id"] = instance.instance.shellId; + json["config_path"] = instance.instance.configPath; + json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate); + + array.push_back(json); + } + + auto document = QJsonDocument(array); + QTextStream(stdout) << document.toJson(QJsonDocument::Indented); + } else { + for (auto& instance: instances) { + auto launchTimeStr = instance.instance.launchTime.toString("yyyy-MM-dd hh:mm:ss"); + + auto runSeconds = instance.instance.launchTime.secsTo(QDateTime::currentDateTime()); + auto remSeconds = runSeconds % 60; + auto runMinutes = (runSeconds - remSeconds) / 60; + auto remMinutes = runMinutes % 60; + auto runHours = (runMinutes - remMinutes) / 60; + auto runtimeStr = QString("%1 hours, %2 minutes, %3 seconds") + .arg(runHours) + .arg(remMinutes) + .arg(remSeconds); + + qCInfo(logBare).noquote().nospace() + << "Instance " << instance.instance.instanceId << ":\n" + << " Process ID: " << instance.pid << '\n' + << " Shell ID: " << instance.instance.shellId << '\n' + << " Config path: " << instance.instance.configPath << '\n' + << " Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n"; + } + } + } + exit(0); // NOLINT + } else if (*kill) { + auto instance = selectInstance(); + + auto r = IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { + client.kill(); + qCInfo(logBare).noquote() << "Killed" << instance.instance.instanceId; + }); + + exit(r ? 0 : -1); // NOLINT } } @@ -373,6 +552,26 @@ foundp:; return configFilePath; } +template +QString base36Encode(T number) { + const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz"; + QString result; + + do { + result.prepend(digits[number % 36]); + number /= 36; + } while (number > 0); + + for (auto i = 0; i < result.length() / 2; i++) { + auto opposite = result.length() - i - 1; + auto c = result.at(i); + result[i] = result.at(opposite); + result[opposite] = c; + } + + return result; +} + int qs_main(int argc, char** argv) { #if CRASH_REPORTER qsCheckCrash(argc, argv); @@ -385,6 +584,7 @@ int qs_main(int argc, char** argv) { QString configFilePath; QString initialWorkdir; QString shellId; + QString pathId; int debugPort = -1; bool waitForDebug = false; @@ -411,11 +611,11 @@ int qs_main(int argc, char** argv) { file.seek(0); auto ds = QDataStream(&file); - InstanceInfo info; + RelaunchInfo info; ds >> info; - configFilePath = info.configPath; - initialWorkdir = info.initialWorkdir; + configFilePath = info.instance.configPath; + initialWorkdir = info.instance.initialWorkdir; noColor = info.noColor; sparseLogsOnly = info.sparseLogsOnly; @@ -426,9 +626,9 @@ int qs_main(int argc, char** argv) { << " (Coredumps will be available under that pid.)"; qCritical() << "Further crash information is stored under" - << QsPaths::crashDir(info.shellId, info.launchTime).path(); + << QsPaths::crashDir(info.instance.instanceId).path(); - if (info.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) { + if (info.instance.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) { qCritical() << "Quickshell crashed within 10 seconds of launching. Not restarting to avoid " "a crash loop."; return 0; @@ -452,9 +652,6 @@ int qs_main(int argc, char** argv) { processCommand(argc, argv, command); - // Start log manager - has to happen with an active event loop or offthread can't be started. - LogManager::init(!noColor, sparseLogsOnly); - #if CRASH_REPORTER // Started after log manager for pretty debug logs. Unlikely anything will crash before this point, but // this can be moved if it happens. @@ -469,7 +666,8 @@ int qs_main(int argc, char** argv) { ); } - shellId = QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + pathId = QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + shellId = pathId; qInfo() << "Config file path:" << configFilePath; @@ -521,12 +719,18 @@ int qs_main(int argc, char** argv) { if (printInfo) return 0; -#if CRASH_REPORTER - crashHandler.setInstanceInfo(InstanceInfo { + auto launchTime = qs::Common::LAUNCH_TIME.toSecsSinceEpoch(); + InstanceInfo::CURRENT = InstanceInfo { + .instanceId = base36Encode(getpid()) + base36Encode(launchTime), .configPath = configFilePath, .shellId = shellId, .initialWorkdir = initialWorkdir, .launchTime = qs::Common::LAUNCH_TIME, + }; + +#if CRASH_REPORTER + crashHandler.setInstanceInfo(RelaunchInfo { + .instance = InstanceInfo::CURRENT, .noColor = noColor, .sparseLogsOnly = sparseLogsOnly, }); @@ -536,8 +740,9 @@ int qs_main(int argc, char** argv) { qputenv(var.toUtf8(), val.toUtf8()); } - QsPaths::init(shellId); - QsPaths::instance()->linkPidRunDir(); + QsPaths::init(shellId, pathId); + QsPaths::instance()->linkRunDir(); + QsPaths::instance()->linkPathDir(); if (auto* cacheDir = QsPaths::instance()->cacheDir()) { auto qmlCacheDir = cacheDir->filePath("qml-engine-cache"); @@ -617,6 +822,9 @@ int qs_main(int argc, char** argv) { QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); } + qs::ipc::IpcServer::start(); + QsPaths::instance()->createLock(); + auto root = RootWrapper(configFilePath, shellId); QGuiApplication::setQuitOnLastWindowClosed(false); diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 7162da5c..7b7f91f1 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -1,8 +1,11 @@ #include "paths.hpp" #include +#include #include -#include +#include +#include +#include #include #include #include @@ -10,7 +13,7 @@ #include #include -#include "common.hpp" +#include "instanceinfo.hpp" Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg); @@ -19,17 +22,27 @@ QsPaths* QsPaths::instance() { return instance; } -void QsPaths::init(QString shellId) { QsPaths::instance()->shellId = std::move(shellId); } +void QsPaths::init(QString shellId, QString pathId) { + auto* instance = QsPaths::instance(); + instance->shellId = std::move(shellId); + instance->pathId = std::move(pathId); +} -QDir QsPaths::crashDir(const QString& shellId, const QDateTime& launchTime) { +QDir QsPaths::crashDir(const QString& id) { auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); dir = QDir(dir.filePath("crashes")); - dir = QDir(dir.filePath(shellId)); - dir = QDir(dir.filePath(QString("run-%1").arg(launchTime.toMSecsSinceEpoch()))); + dir = QDir(dir.filePath(id)); return dir; } +QString QsPaths::ipcPath(const QString& id) { + auto ipcPath = QsPaths::instance()->baseRunDir()->filePath("by-id"); + ipcPath = QDir(ipcPath).filePath(id); + ipcPath = QDir(ipcPath).filePath("ipc.sock"); + return ipcPath; +} + QDir* QsPaths::cacheDir() { if (this->cacheState == DirState::Unknown) { auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); @@ -77,42 +90,45 @@ QDir* QsPaths::baseRunDir() { else return &this->mBaseRunDir; } -QDir* QsPaths::runDir() { - if (this->runState == DirState::Unknown) { +QDir* QsPaths::shellRunDir() { + if (this->shellRunState == DirState::Unknown) { if (auto* baseRunDir = this->baseRunDir()) { - this->mRunDir = QDir(baseRunDir->filePath(this->shellId)); + this->mShellRunDir = QDir(baseRunDir->filePath("by-shell")); + this->mShellRunDir = QDir(this->mShellRunDir.filePath(this->shellId)); - qCDebug(logPaths) << "Initialized runtime path:" << this->mRunDir.path(); + qCDebug(logPaths) << "Initialized runtime path:" << this->mShellRunDir.path(); - if (!this->mRunDir.mkpath(".")) { - qCCritical(logPaths) << "Could not create runtime directory at" << this->mRunDir.path(); - this->runState = DirState::Failed; + if (!this->mShellRunDir.mkpath(".")) { + qCCritical(logPaths) << "Could not create runtime directory at" + << this->mShellRunDir.path(); + this->shellRunState = DirState::Failed; } else { - this->runState = DirState::Ready; + this->shellRunState = DirState::Ready; } } else { qCCritical(logPaths) << "Could not create shell runtime path as it was not possible to " "create the base runtime path."; - this->runState = DirState::Failed; + this->shellRunState = DirState::Failed; } } - if (this->runState == DirState::Failed) return nullptr; - else return &this->mRunDir; + if (this->shellRunState == DirState::Failed) return nullptr; + else return &this->mShellRunDir; } QDir* QsPaths::instanceRunDir() { if (this->instanceRunState == DirState::Unknown) { - auto* runtimeDir = this->runDir(); + auto* runDir = this->baseRunDir(); - if (!runtimeDir) { + if (!runDir) { qCCritical(logPaths) << "Cannot create instance runtime directory as main runtim directory " "could not be created."; this->instanceRunState = DirState::Failed; } else { - this->mInstanceRunDir = - runtimeDir->filePath(QString("run-%1").arg(qs::Common::LAUNCH_TIME.toMSecsSinceEpoch())); + auto byIdDir = QDir(runDir->filePath("by-id")); + + this->mInstanceRunDir = byIdDir.filePath(InstanceInfo::CURRENT.instanceId); qCDebug(logPaths) << "Initialized instance runtime path:" << this->mInstanceRunDir.path(); @@ -126,34 +142,162 @@ QDir* QsPaths::instanceRunDir() { } } - if (this->runState == DirState::Failed) return nullptr; + if (this->shellRunState == DirState::Failed) return nullptr; else return &this->mInstanceRunDir; } -void QsPaths::linkPidRunDir() { +void QsPaths::linkRunDir() { if (auto* runDir = this->instanceRunDir()) { auto pidDir = QDir(this->baseRunDir()->filePath("by-pid")); + auto* shellDir = this->shellRunDir(); + + if (!shellDir) { + qCCritical(logPaths + ) << "Could not create by-id symlink as the shell runtime path could not be created."; + } else { + auto shellPath = shellDir->filePath(runDir->dirName()); + + QFile::remove(shellPath); + auto r = + symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, shellPath.toStdString().c_str()); + + if (r != 0) { + qCCritical(logPaths).nospace() + << "Could not create id symlink to " << runDir->path() << " at " << shellPath + << " with error code " << errno << ": " << qt_error_string(); + } else { + qCDebug(logPaths) << "Created shellid symlink" << shellPath << "to instance runtime path" + << runDir->path(); + } + } if (!pidDir.mkpath(".")) { qCCritical(logPaths) << "Could not create PID symlink directory."; - return; - } - - auto pidPath = pidDir.filePath(QString::number(getpid())); - - QFile::remove(pidPath); - auto r = symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, pidPath.toStdString().c_str()); - - if (r != 0) { - qCCritical(logPaths).nospace() - << "Could not create PID symlink to " << runDir->path() << " at " << pidPath - << " with error code " << errno << ": " << qt_error_string(); } else { - qCDebug(logPaths) << "Created PID symlink" << pidPath << "to instance runtime path" - << runDir->path(); + auto pidPath = pidDir.filePath(QString::number(getpid())); + + QFile::remove(pidPath); + auto r = + symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, pidPath.toStdString().c_str()); + + if (r != 0) { + qCCritical(logPaths).nospace() + << "Could not create PID symlink to " << runDir->path() << " at " << pidPath + << " with error code " << errno << ": " << qt_error_string(); + } else { + qCDebug(logPaths) << "Created PID symlink" << pidPath << "to instance runtime path" + << runDir->path(); + } } } else { qCCritical(logPaths) << "Could not create PID symlink to runtime directory, as the runtime " "directory could not be created."; } } + +void QsPaths::linkPathDir() { + if (auto* runDir = this->shellRunDir()) { + auto pathDir = QDir(this->baseRunDir()->filePath("by-path")); + + if (!pathDir.mkpath(".")) { + qCCritical(logPaths) << "Could not create path symlink directory."; + return; + } + + auto linkPath = pathDir.filePath(this->pathId); + + QFile::remove(linkPath); + auto r = + symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, linkPath.toStdString().c_str()); + + if (r != 0) { + qCCritical(logPaths).nospace() + << "Could not create path symlink to " << runDir->path() << " at " << linkPath + << " with error code " << errno << ": " << qt_error_string(); + } else { + qCDebug(logPaths) << "Created path symlink" << linkPath << "to shell runtime path" + << runDir->path(); + } + } else { + qCCritical(logPaths) << "Could not create path symlink to shell runtime directory, as the " + "shell runtime directory could not be created."; + } +} + +void QsPaths::createLock() { + if (auto* runDir = this->instanceRunDir()) { + auto path = runDir->filePath("instance.lock"); + auto* file = new QFile(path); // leaked + + if (!file->open(QFile::ReadWrite | QFile::Truncate)) { + qCCritical(logPaths) << "Could not create instance lock at" << path; + return; + } + + auto lock = flock { + .l_type = F_WRLCK, + .l_whence = SEEK_SET, + .l_start = 0, + .l_len = 0, + }; + + if (fcntl(file->handle(), F_SETLK, &lock) != 0) { // NOLINT + qCCritical(logPaths).nospace() << "Could not lock instance lock at " << path + << " with error code " << errno << ": " << qt_error_string(); + } else { + auto stream = QDataStream(file); + stream << InstanceInfo::CURRENT; + file->flush(); + qCDebug(logPaths) << "Created instance lock at" << path; + } + } else { + qCCritical(logPaths + ) << "Could not create instance lock, as the instance runtime directory could not be created."; + } +} + +bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info) { + auto file = QFile(QDir(path).filePath("instance.lock")); + if (!file.open(QFile::ReadOnly)) return false; + + auto lock = flock { + .l_type = F_WRLCK, + .l_whence = SEEK_SET, + .l_start = 0, + .l_len = 0, + }; + + fcntl(file.handle(), F_GETLK, &lock); // NOLINT + if (lock.l_type == F_UNLCK) return false; + + if (info) { + info->pid = lock.l_pid; + + auto stream = QDataStream(&file); + stream >> info->instance; + } + + return true; +} + +QVector QsPaths::collectInstances(const QString& path) { + qCDebug(logPaths) << "Collecting instances from" << path; + auto instances = QVector(); + auto dir = QDir(path); + + InstanceLockInfo info; + for (auto& entry: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { + auto path = dir.filePath(entry); + + if (QsPaths::checkLock(path, &info)) { + qCDebug(logPaths).nospace() << "Found live instance " << info.instance.instanceId << " (pid " + << info.pid << ") at " << path; + + instances.push_back(info); + } else { + qCDebug(logPaths) << "Skipped dead instance at" << path; + } + } + + return instances; +} diff --git a/src/core/paths.hpp b/src/core/paths.hpp index 866a33cf..62858bdb 100644 --- a/src/core/paths.hpp +++ b/src/core/paths.hpp @@ -2,17 +2,32 @@ #include #include +#include "instanceinfo.hpp" + +struct InstanceLockInfo { + pid_t pid = -1; + InstanceInfo instance; +}; + +QDataStream& operator<<(QDataStream& stream, const InstanceLockInfo& info); +QDataStream& operator>>(QDataStream& stream, InstanceLockInfo& info); + class QsPaths { public: static QsPaths* instance(); - static void init(QString shellId); - static QDir crashDir(const QString& shellId, const QDateTime& launchTime); + static void init(QString shellId, QString pathId); + static QDir crashDir(const QString& id); + static QString ipcPath(const QString& id); + static bool checkLock(const QString& path, InstanceLockInfo* info = nullptr); + static QVector collectInstances(const QString& path); QDir* cacheDir(); QDir* baseRunDir(); - QDir* runDir(); + QDir* shellRunDir(); QDir* instanceRunDir(); - void linkPidRunDir(); + void linkRunDir(); + void linkPathDir(); + void createLock(); private: enum class DirState { @@ -22,12 +37,13 @@ private: }; QString shellId; + QString pathId; QDir mCacheDir; QDir mBaseRunDir; - QDir mRunDir; + QDir mShellRunDir; QDir mInstanceRunDir; DirState cacheState = DirState::Unknown; DirState baseRunState = DirState::Unknown; - DirState runState = DirState::Unknown; + DirState shellRunState = DirState::Unknown; DirState instanceRunState = DirState::Unknown; }; diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index dea6192c..496aaba6 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -14,7 +14,7 @@ #include #include -#include "../core/crashinfo.hpp" +#include "../core/instanceinfo.hpp" extern char** environ; // NOLINT @@ -64,7 +64,7 @@ void CrashHandler::init() { qCInfo(logCrashHandler) << "Crash handler initialized."; } -void CrashHandler::setInstanceInfo(const InstanceInfo& info) { +void CrashHandler::setInstanceInfo(const RelaunchInfo& info) { this->d->infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC); if (this->d->infoFd == -1) { diff --git a/src/crash/handler.hpp b/src/crash/handler.hpp index de7b46bc..dd618f7f 100644 --- a/src/crash/handler.hpp +++ b/src/crash/handler.hpp @@ -2,7 +2,7 @@ #include -#include "../core/crashinfo.hpp" +#include "../core/instanceinfo.hpp" namespace qs::crash { struct CrashHandlerPrivate; @@ -14,7 +14,7 @@ public: Q_DISABLE_COPY_MOVE(CrashHandler); void init(); - void setInstanceInfo(const InstanceInfo& info); + void setInstanceInfo(const RelaunchInfo& info); private: CrashHandlerPrivate* d; diff --git a/src/crash/main.cpp b/src/crash/main.cpp index 52776190..8583ff91 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -15,7 +14,7 @@ #include #include -#include "../core/crashinfo.hpp" +#include "../core/instanceinfo.hpp" #include "../core/logging.hpp" #include "../core/paths.hpp" #include "build.hpp" @@ -23,14 +22,14 @@ Q_LOGGING_CATEGORY(logCrashReporter, "quickshell.crashreporter", QtWarningMsg); -void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instanceInfo); +void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance); void qsCheckCrash(int argc, char** argv) { auto fd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD"); if (fd.isEmpty()) return; auto app = QApplication(argc, argv); - InstanceInfo instance; + RelaunchInfo info; auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt(); @@ -42,15 +41,15 @@ void qsCheckCrash(int argc, char** argv) { file.seek(0); auto ds = QDataStream(&file); - ds >> instance; + ds >> info; } - LogManager::init(!instance.noColor, false); - auto crashDir = QsPaths::crashDir(instance.shellId, instance.launchTime); + LogManager::init(!info.noColor, false); + auto crashDir = QsPaths::crashDir(info.instance.instanceId); qCInfo(logCrashReporter) << "Starting crash reporter..."; - recordCrashInfo(crashDir, instance); + recordCrashInfo(crashDir, info.instance); auto gui = CrashReporterGui(crashDir.path(), crashProc); gui.show(); @@ -125,8 +124,7 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { stream << "===== Quickshell Crash =====\n"; stream << "Git Revision: " << GIT_REVISION << '\n'; stream << "Crashed process ID: " << crashProc << '\n'; - stream << "Run ID: " << QString("run-%1").arg(instance.launchTime.toMSecsSinceEpoch()) - << '\n'; + stream << "Run ID: " << instance.instanceId << '\n'; stream << "\n===== Shell Information =====\n"; stream << "Shell ID: " << instance.shellId << '\n'; From 94e881e6b0f72c651fe29364b498a9d12a652a2e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 1 Sep 2024 14:17:39 -0700 Subject: [PATCH 147/305] core!: refactor launch sequence Also includes slight changes to the command syntax. See --help for details. --- src/core/instanceinfo.cpp | 8 +- src/core/instanceinfo.hpp | 5 +- src/core/ipc.cpp | 6 +- src/core/ipc.hpp | 2 +- src/core/logging.cpp | 114 +++- src/core/logging.hpp | 34 +- src/core/main.cpp | 1190 ++++++++++++++++++------------------- src/core/main.hpp | 6 +- src/crash/handler.cpp | 2 +- src/crash/handler.hpp | 2 +- src/crash/main.cpp | 9 +- src/main.cpp | 2 +- 12 files changed, 729 insertions(+), 651 deletions(-) diff --git a/src/core/instanceinfo.cpp b/src/core/instanceinfo.cpp index 794212b8..96097c76 100644 --- a/src/core/instanceinfo.cpp +++ b/src/core/instanceinfo.cpp @@ -13,12 +13,16 @@ QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) { } QDataStream& operator<<(QDataStream& stream, const RelaunchInfo& info) { - stream << info.instance << info.noColor << info.sparseLogsOnly; + stream << info.instance << info.noColor << info.timestamp << info.sparseLogsOnly + << info.defaultLogLevel << info.logRules; + return stream; } QDataStream& operator>>(QDataStream& stream, RelaunchInfo& info) { - stream >> info.instance >> info.noColor >> info.sparseLogsOnly; + stream >> info.instance >> info.noColor >> info.timestamp >> info.sparseLogsOnly + >> info.defaultLogLevel >> info.logRules; + return stream; } diff --git a/src/core/instanceinfo.hpp b/src/core/instanceinfo.hpp index 21bb62d3..f0fc02a0 100644 --- a/src/core/instanceinfo.hpp +++ b/src/core/instanceinfo.hpp @@ -1,13 +1,13 @@ #pragma once #include +#include #include struct InstanceInfo { QString instanceId; QString configPath; QString shellId; - QString initialWorkdir; QDateTime launchTime; static InstanceInfo CURRENT; // NOLINT @@ -16,7 +16,10 @@ struct InstanceInfo { struct RelaunchInfo { InstanceInfo instance; bool noColor = false; + bool timestamp = false; bool sparseLogsOnly = false; + QtMsgType defaultLogLevel = QtWarningMsg; + QString logRules; }; QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info); diff --git a/src/core/ipc.cpp b/src/core/ipc.cpp index 406d0909..ca14834a 100644 --- a/src/core/ipc.cpp +++ b/src/core/ipc.cpp @@ -104,16 +104,16 @@ void IpcClient::onError(QLocalSocket::LocalSocketError error) { qCCritical(logIpc) << "Socket Error" << error; } -bool IpcClient::connect(const QString& id, const std::function& callback) { +int IpcClient::connect(const QString& id, const std::function& callback) { auto path = QsPaths::ipcPath(id); auto client = IpcClient(path); qCDebug(logIpc) << "Connecting to instance" << id << "at" << path; client.waitForConnected(); - if (!client.isConnected()) return false; + if (!client.isConnected()) return -1; qCDebug(logIpc) << "Connected."; callback(client); - return true; + return 0; } } // namespace qs::ipc diff --git a/src/core/ipc.hpp b/src/core/ipc.hpp index a62f7b77..9738f4b9 100644 --- a/src/core/ipc.hpp +++ b/src/core/ipc.hpp @@ -56,7 +56,7 @@ public: void kill(); - [[nodiscard]] static bool + [[nodiscard]] static int connect(const QString& id, const std::function& callback); signals: diff --git a/src/core/logging.cpp b/src/core/logging.cpp index fe319f54..72ac81b1 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -32,6 +32,7 @@ Q_LOGGING_CATEGORY(logBare, "quickshell.bare"); namespace qs::log { +using namespace qt_logging_registry; Q_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg); @@ -48,8 +49,16 @@ void LogMessage::formatMessage( QTextStream& stream, const LogMessage& msg, bool color, - bool timestamp + bool timestamp, + const QString& prefix ) { + if (!prefix.isEmpty()) { + if (color) stream << "\033[90m"; + stream << '[' << prefix << ']'; + if (timestamp) stream << ' '; + if (color) stream << "\033[0m"; + } + if (timestamp) { if (color) stream << "\033[90m"; stream << msg.time.toString("yyyy-MM-dd hh:mm:ss.zzz"); @@ -102,6 +111,30 @@ bool CategoryFilter::shouldDisplay(QtMsgType type) const { } } +void CategoryFilter::apply(QLoggingCategory* category) const { + category->setEnabled(QtDebugMsg, this->debug); + category->setEnabled(QtInfoMsg, this->info); + category->setEnabled(QtWarningMsg, this->warn); + category->setEnabled(QtCriticalMsg, this->critical); +} + +void CategoryFilter::applyRule( + QLatin1StringView category, + const qt_logging_registry::QLoggingRule& rule +) { + auto filterpass = rule.pass(category, QtDebugMsg); + if (filterpass != 0) this->debug = filterpass > 0; + + filterpass = rule.pass(category, QtInfoMsg); + if (filterpass != 0) this->info = filterpass > 0; + + filterpass = rule.pass(category, QtWarningMsg); + if (filterpass != 0) this->warn = filterpass > 0; + + filterpass = rule.pass(category, QtCriticalMsg); + if (filterpass != 0) this->critical = filterpass > 0; +} + LogManager::LogManager(): stdoutStream(stdout) {} void LogManager::messageHandler( @@ -122,7 +155,14 @@ void LogManager::messageHandler( } if (display) { - LogMessage::formatMessage(self->stdoutStream, message, self->colorLogs, false); + LogMessage::formatMessage( + self->stdoutStream, + message, + self->colorLogs, + self->timestampLogs, + self->prefix + ); + self->stdoutStream << Qt::endl; } @@ -132,21 +172,37 @@ void LogManager::messageHandler( void LogManager::filterCategory(QLoggingCategory* category) { auto* instance = LogManager::instance(); + auto categoryName = QLatin1StringView(category->categoryName()); + auto isQs = categoryName.startsWith(QLatin1StringView("quickshell.")); + if (instance->lastCategoryFilter) { instance->lastCategoryFilter(category); } - if (QLatin1StringView(category->categoryName()).startsWith(QLatin1StringView("quickshell"))) { + auto filter = CategoryFilter(category); + + if (isQs) { + filter.debug = filter.debug || instance->mDefaultLevel == QtDebugMsg; + filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg; + filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg; + filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg; + } + + for (const auto& rule: *instance->rules) { + filter.applyRule(categoryName, rule); + } + + if (isQs && instance->sparse) { // We assume the category name pointer will always be the same and be comparable in the message handler. LogManager::instance()->sparseFilters.insert( static_cast(category->categoryName()), - CategoryFilter(category) + filter ); - category->setEnabled(QtDebugMsg, true); - category->setEnabled(QtInfoMsg, true); - category->setEnabled(QtWarningMsg, true); - category->setEnabled(QtCriticalMsg, true); + // all enabled by default + CategoryFilter().apply(category); + } else { + filter.apply(category); } } @@ -155,15 +211,31 @@ LogManager* LogManager::instance() { return instance; } -void LogManager::init(bool color, bool sparseOnly) { +void LogManager::init( + bool color, + bool timestamp, + bool sparseOnly, + QtMsgType defaultLevel, + const QString& rules, + const QString& prefix +) { auto* instance = LogManager::instance(); instance->colorLogs = color; + instance->timestampLogs = timestamp; + instance->sparse = sparseOnly; + instance->prefix = prefix; + instance->mDefaultLevel = defaultLevel; + instance->mRulesString = rules; + + { + QLoggingSettingsParser parser; + parser.setContent(rules); + instance->rules = new QList(parser.rules()); + } qInstallMessageHandler(&LogManager::messageHandler); - if (!sparseOnly) { - instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory); - } + instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory); qCDebug(logLogging) << "Creating offthread logger..."; auto* thread = new QThread(); @@ -181,6 +253,10 @@ void LogManager::initFs() { ); } +QString LogManager::rulesString() const { return this->mRulesString; } +QtMsgType LogManager::defaultLevel() const { return this->mDefaultLevel; } +bool LogManager::isSparse() const { return this->sparse; } + void LoggingThreadProxy::initInThread() { this->logging = new ThreadLogging(this); this->logging->init(); @@ -654,8 +730,6 @@ bool EncodedLogReader::registerCategory() { } bool readEncodedLogs(QIODevice* device, bool timestamps, const QString& rulespec) { - using namespace qt_logging_registry; - QList rules; { @@ -695,17 +769,7 @@ bool readEncodedLogs(QIODevice* device, bool timestamps, const QString& rulespec filter = filters.value(message.readCategoryId); } else { for (const auto& rule: rules) { - auto filterpass = rule.pass(message.category, QtDebugMsg); - if (filterpass != 0) filter.debug = filterpass > 0; - - filterpass = rule.pass(message.category, QtInfoMsg); - if (filterpass != 0) filter.info = filterpass > 0; - - filterpass = rule.pass(message.category, QtWarningMsg); - if (filterpass != 0) filter.warn = filterpass > 0; - - filterpass = rule.pass(message.category, QtCriticalMsg); - if (filterpass != 0) filter.critical = filterpass > 0; + filter.applyRule(message.category, rule); } filters.insert(message.readCategoryId, filter); diff --git a/src/core/logging.hpp b/src/core/logging.hpp index 618a1744..bd3be771 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -37,7 +37,13 @@ struct LogMessage { QByteArray body; quint16 readCategoryId = 0; - static void formatMessage(QTextStream& stream, const LogMessage& msg, bool color, bool timestamp); + static void formatMessage( + QTextStream& stream, + const LogMessage& msg, + bool color, + bool timestamp, + const QString& prefix = "" + ); }; size_t qHash(const LogMessage& message); @@ -58,6 +64,10 @@ private: ThreadLogging* logging = nullptr; }; +namespace qt_logging_registry { +class QLoggingRule; +} + struct CategoryFilter { explicit CategoryFilter() = default; explicit CategoryFilter(QLoggingCategory* category) @@ -67,6 +77,8 @@ struct CategoryFilter { , critical(category->isCriticalEnabled()) {} [[nodiscard]] bool shouldDisplay(QtMsgType type) const; + void apply(QLoggingCategory* category) const; + void applyRule(QLatin1StringView category, const qt_logging_registry::QLoggingRule& rule); bool debug = true; bool info = true; @@ -78,11 +90,24 @@ class LogManager: public QObject { Q_OBJECT; public: - static void init(bool color, bool sparseOnly); + static void init( + bool color, + bool timestamp, + bool sparseOnly, + QtMsgType defaultLevel, + const QString& rules, + const QString& prefix = "" + ); + static void initFs(); static LogManager* instance(); bool colorLogs = true; + bool timestampLogs = false; + + [[nodiscard]] QString rulesString() const; + [[nodiscard]] QtMsgType defaultLevel() const; + [[nodiscard]] bool isSparse() const; signals: void logMessage(LogMessage msg, bool showInSparse); @@ -94,6 +119,11 @@ private: static void filterCategory(QLoggingCategory* category); QLoggingCategory::CategoryFilter lastCategoryFilter = nullptr; + bool sparse = false; + QString prefix; + QString mRulesString; + QList* rules = nullptr; + QtMsgType mDefaultLevel = QtWarningMsg; QHash sparseFilters; QTextStream stdoutStream; diff --git a/src/core/main.cpp b/src/core/main.cpp index 68d0a40d..59942742 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -2,12 +2,10 @@ #include #include #include -#include #include #include #include // NOLINT: Need to include this for impls of some CLI11 classes -#include #include #include #include @@ -33,8 +31,6 @@ #include #include #include -#include -#include #include #include "build.hpp" @@ -45,423 +41,286 @@ #include "paths.hpp" #include "plugin.hpp" #include "rootwrapper.hpp" + #if CRASH_REPORTER #include "../crash/handler.hpp" #include "../crash/main.hpp" #endif +namespace qs::launch { + using qs::ipc::IpcClient; -struct CommandInfo { - QString configPath; - QString manifestPath; - QString configName; - QString& initialWorkdir; - int& debugPort; - bool& waitForDebug; - bool& printInfo; - bool& noColor; - bool& sparseLogsOnly; -}; +void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication); +int runCommand(int argc, char** argv, QCoreApplication* coreApplication); -QString commandConfigPath(QString path, QString manifest, QString config, bool printInfo); - -void processCommand(int argc, char** argv, CommandInfo& info) { +int main(int argc, char** argv) { QCoreApplication::setApplicationName("quickshell"); - auto app = CLI::App(""); +#if CRASH_REPORTER + qsCheckCrash(argc, argv); +#endif - class QStringOption { - public: - QStringOption() = default; - QStringOption& operator=(const std::string& str) { - this->str = QString::fromStdString(str); - return *this; - } + auto qArgC = 1; + auto* coreApplication = new QCoreApplication(qArgC, argv); - QString& operator*() { return this->str; } - - private: - QString str; - }; - - class QStringRefOption { - public: - QStringRefOption(QString* str): str(str) {} - QStringRefOption& operator=(const std::string& str) { - *this->str = QString::fromStdString(str); - return *this; - } - - private: - QString* str; - }; - - /// --- - QStringRefOption path(&info.configPath); - QStringRefOption manifest(&info.manifestPath); - QStringRefOption config(&info.configName); - QStringRefOption workdirRef(&info.initialWorkdir); - - auto* selection = app.add_option_group( - "Config Selection", - "Select a configuration to run (defaults to $XDG_CONFIG_HOME/quickshell/shell.qml)" - ); - - auto* pathArg = - selection->add_option("-p,--path", path, "Path to a QML file to run. (Env:QS_CONFIG_PATH)"); - - auto* mfArg = selection->add_option( - "-m,--manifest", - manifest, - "Path to a manifest containing configurations. (Env:QS_MANIFEST)\n" - "(Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf)" - ); - - auto* cfgArg = selection->add_option( - "-c,--config", - config, - "Name of a configuration within a manifest. (Env:QS_CONFIG_NAME)" - ); - - selection->add_option("-d,--workdir", workdirRef, "Initial working directory."); - - pathArg->excludes(mfArg, cfgArg); - - /// --- - auto* debug = app.add_option_group("Debugging"); - - auto* debugPortArg = debug - ->add_option( - "--debugport", - info.debugPort, - "Open the given port for a QML debugger to connect to." - ) - ->check(CLI::Range(0, 65535)); - - debug - ->add_flag( - "--waitfordebug", - info.waitForDebug, - "Wait for a debugger to attach to the given port before launching." - ) - ->needs(debugPortArg); - - /// --- - app.add_flag("--info", info.printInfo, "Print information about the shell") - ->excludes(debugPortArg); - app.add_flag("--no-color", info.noColor, "Do not color the log output. (Env:NO_COLOR)"); - auto* printVersion = app.add_flag("-V,--version", "Print quickshell's version, then exit."); - - app.add_flag( - "--no-detailed-logs", - info.sparseLogsOnly, - "Do not enable this unless you know what you are doing." - ); - - /// --- - QStringOption logPath; - QStringOption logFilter; - auto logNoTime = false; - - auto* readLog = app.add_subcommand("read-log", "Read a quickshell log file."); - readLog->add_option("path", logPath, "Path to the log file to read")->required(); - - readLog->add_option( - "-f,--filter", - logFilter, - "Logging categories to display. (same syntax as QT_LOGGING_RULES)" - ); - - readLog->add_flag("--no-time", logNoTime, "Do not print timestamps of log messages."); - readLog->add_flag("--no-color", info.noColor, "Do not color the log output. (Env:NO_COLOR)"); - - /// --- - QStringOption instanceId; - pid_t instancePid = -1; - - auto sortInstances = [](QVector& list) { - std::sort(list.begin(), list.end(), [](const InstanceLockInfo& a, const InstanceLockInfo& b) { - return a.instance.launchTime < b.instance.launchTime; - }); - }; - - auto selectInstance = [&]() { - auto* basePath = QsPaths::instance()->baseRunDir(); - if (!basePath) exit(-1); // NOLINT - - QString path; - InstanceLockInfo instance; - - if (instancePid != -1) { - path = QDir(basePath->filePath("by-pid")).filePath(QString::number(instancePid)); - if (!QsPaths::checkLock(path, &instance)) { - qCInfo(logBare) << "No instance found for pid" << instancePid; - exit(-1); // NOLINT - } - } else if (!(*instanceId).isEmpty()) { - path = basePath->filePath("by-pid"); - auto instances = QsPaths::collectInstances(path); - - auto itr = - std::remove_if(instances.begin(), instances.end(), [&](const InstanceLockInfo& info) { - return !info.instance.instanceId.startsWith(*instanceId); - }); - - instances.erase(itr, instances.end()); - - if (instances.isEmpty()) { - qCInfo(logBare) << "No running instances start with" << *instanceId; - } else if (instances.length() != 1) { - qCInfo(logBare) << "More than one instance starts with" << *instanceId; - - for (auto& instance: instances) { - qCInfo(logBare).noquote() << " -" << instance.instance.instanceId; - } - - exit(-1); // NOLINT - } else { - instance = instances.value(0); - } - } else { - auto configFilePath = - commandConfigPath(info.configPath, info.manifestPath, info.configName, info.printInfo); - - auto pathId = - QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); - - path = QDir(basePath->filePath("by-path")).filePath(pathId); - - auto instances = QsPaths::collectInstances(path); - sortInstances(instances); - - if (instances.isEmpty()) { - qCInfo(logBare) << "No running instances for" << configFilePath; - exit(-1); // NOLINT - } - - instance = instances.value(0); - } - - return instance; - }; - - auto* instances = - app.add_subcommand("instances", "List running quickshell instances.")->fallthrough(); - - auto* allInstances = - instances->add_flag("-a,--all", "List all instances instead of just the current config."); - - auto* instancesJson = instances->add_flag("-j,--json", "Output the list as a json."); - - auto* kill = app.add_subcommand("kill", "Kill an instance.")->fallthrough(); - auto* kInstance = app.add_option("-i,--instance", instanceId, "The instance id to kill."); - app.add_option("-p,--pid", instancePid, "The process id to kill.")->excludes(kInstance); - - try { - app.parse(argc, argv); - } catch (const CLI::ParseError& e) { - exit(app.exit(e)); // NOLINT - }; - - // Start log manager - has to happen with an active event loop or offthread can't be started. - LogManager::init(!info.noColor, info.sparseLogsOnly); - - if (*printVersion) { - std::cout << "quickshell pre-release, revision: " << GIT_REVISION << std::endl; - exit(0); // NOLINT - } else if (*readLog) { - auto file = QFile(*logPath); - if (!file.open(QFile::ReadOnly)) { - qCritical() << "Failed to open log for reading:" << *logPath; - exit(-1); // NOLINT - } else { - qInfo() << "Reading log" << *logPath; - } - - exit( // NOLINT - qs::log::readEncodedLogs(&file, !logNoTime, *logFilter) ? 0 : -1 - ); - } else if (*instances) { - auto* basePath = QsPaths::instance()->baseRunDir(); - if (!basePath) exit(-1); // NOLINT - - QString path; - QString configFilePath; - if (*allInstances) { - path = basePath->filePath("by-pid"); - } else { - configFilePath = - commandConfigPath(info.configPath, info.manifestPath, info.configName, info.printInfo); - - auto pathId = - QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); - - path = QDir(basePath->filePath("by-path")).filePath(pathId); - } - - auto instances = QsPaths::collectInstances(path); - - if (instances.isEmpty()) { - if (*allInstances) { - qCInfo(logBare) << "No running instances."; - } else { - qCInfo(logBare) << "No running instances for" << configFilePath; - qCInfo(logBare) << "Use --all to list all instances."; - } - } else { - sortInstances(instances); - - if (*instancesJson) { - auto array = QJsonArray(); - - for (auto& instance: instances) { - auto json = QJsonObject(); - - json["id"] = instance.instance.instanceId; - json["pid"] = instance.pid; - json["shell_id"] = instance.instance.shellId; - json["config_path"] = instance.instance.configPath; - json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate); - - array.push_back(json); - } - - auto document = QJsonDocument(array); - QTextStream(stdout) << document.toJson(QJsonDocument::Indented); - } else { - for (auto& instance: instances) { - auto launchTimeStr = instance.instance.launchTime.toString("yyyy-MM-dd hh:mm:ss"); - - auto runSeconds = instance.instance.launchTime.secsTo(QDateTime::currentDateTime()); - auto remSeconds = runSeconds % 60; - auto runMinutes = (runSeconds - remSeconds) / 60; - auto remMinutes = runMinutes % 60; - auto runHours = (runMinutes - remMinutes) / 60; - auto runtimeStr = QString("%1 hours, %2 minutes, %3 seconds") - .arg(runHours) - .arg(remMinutes) - .arg(remSeconds); - - qCInfo(logBare).noquote().nospace() - << "Instance " << instance.instance.instanceId << ":\n" - << " Process ID: " << instance.pid << '\n' - << " Shell ID: " << instance.instance.shellId << '\n' - << " Config path: " << instance.instance.configPath << '\n' - << " Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n"; - } - } - } - exit(0); // NOLINT - } else if (*kill) { - auto instance = selectInstance(); - - auto r = IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { - client.kill(); - qCInfo(logBare).noquote() << "Killed" << instance.instance.instanceId; - }); - - exit(r ? 0 : -1); // NOLINT - } + checkCrashRelaunch(argv, coreApplication); + return runCommand(argc, argv, coreApplication); } -QString commandConfigPath(QString path, QString manifest, QString config, bool printInfo) { - // NOLINTBEGIN -#define CHECK(rname, name, level, label, expr) \ - QString name = expr; \ - if (rname.isEmpty() && !name.isEmpty()) { \ - rname = name; \ - rname##Level = level; \ - if (!printInfo) goto label; \ +class QStringOption { +public: + QStringOption() = default; + QStringOption& operator=(const std::string& str) { + this->str = QString::fromStdString(str); + return *this; } -#define OPTSTR(name) (name.isEmpty() ? "(unset)" : name.toStdString()) - // NOLINTEND + QString& operator*() { return this->str; } + QString* operator->() { return &this->str; } - QString basePath; - int basePathLevel = 0; - Q_UNUSED(basePathLevel); - { - // NOLINTBEGIN - // clang-format off - CHECK(basePath, envBasePath, 0, foundbase, qEnvironmentVariable("QS_BASE_PATH")); - CHECK(basePath, defaultBasePath, 0, foundbase, QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)).filePath("quickshell")); - // clang-format on - // NOLINTEND +private: + QString str; +}; - if (printInfo) { - // clang-format off - std::cout << "Base path: " << OPTSTR(basePath) << "\n"; - std::cout << " - Environment (QS_BASE_PATH): " << OPTSTR(envBasePath) << "\n"; - std::cout << " - Default: " << OPTSTR(defaultBasePath) << "\n"; - // clang-format on - } - } -foundbase:; +struct CommandState { + struct { + int argc = 0; + char** argv = nullptr; + } exec; + struct { + bool timestamp = false; + bool noColor = !qEnvironmentVariableIsEmpty("NO_COLOR"); + bool sparse = false; + size_t verbosity = 0; + QStringOption rules; + QStringOption readoutRules; + QStringOption file; + } log; + + struct { + QStringOption path; + QStringOption manifest; + QStringOption name; + } config; + + struct { + int port = -1; + bool wait = false; + } debug; + + struct { + QStringOption id; + pid_t pid = -1; // NOLINT (include) + bool all = false; + } instance; + + struct { + bool json = false; + } output; + + struct { + CLI::App* log = nullptr; + CLI::App* list = nullptr; + CLI::App* kill = nullptr; + } subcommand; + + struct { + bool printVersion = false; + bool killAll = false; + } misc; +}; + +int readLogFile(CommandState& cmd); +int listInstances(CommandState& cmd); +int killInstances(CommandState& cmd); +int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication); + +struct LaunchArgs { QString configPath; - int configPathLevel = 10; + int debugPort = -1; + bool waitForDebug = false; +}; + +int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication); + +int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { + auto state = CommandState(); + + state.exec = { + .argc = argc, + .argv = argv, + }; + + auto addConfigSelection = [&](CLI::App* cmd) { + auto* group = cmd->add_option_group("Config Selection") + ->description("If no options in this group are specified,\n" + "$XDG_CONFIG_HOME/quickshell/shell.qml will be used."); + + auto* path = group->add_option("-p,--path", state.config.path) + ->description("Path to a QML file.") + ->envname("QS_CONFIG_PATH"); + + group->add_option("-m,--manifest", state.config.manifest) + ->description("Path to a quickshell manifest.\n" + "Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf") + ->envname("QS_MANIFEST") + ->excludes(path); + + group->add_option("-c,--config", state.config.name) + ->description("Name of a quickshell configuration to run.\n" + "If -m is specified, this is a configuration in the manifest,\n" + "otherwise it is the name of a folder in $XDG_CONFIG_HOME/quickshell.") + ->envname("QS_CONFIG_NAME"); + + return group; + }; + + auto addDebugOptions = [&](CLI::App* cmd) { + auto* group = cmd->add_option_group("Debugging", "Options for QML debugging."); + + auto* debug = group->add_option("--debug", state.debug.port) + ->description("Open the given port for a QML debugger connection.") + ->check(CLI::Range(0, 65535)); + + group->add_flag("--waitfordebug", state.debug.wait) + ->description("Wait for a QML debugger to connect before executing the configuration.") + ->needs(debug); + + return group; + }; + + auto addLoggingOptions = [&](CLI::App* cmd, bool noGroup, bool noDisplay = false) { + 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\n" + "for the NO_COLOR environment variable."); + + group->add_flag("--log-times", state.log.timestamp) + ->description("Log timestamps with each message."); + + group->add_option("--log-rules", state.log.rules) + ->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."); + + auto* hgroup = cmd->add_option_group(""); + hgroup->add_flag("--no-detailed-logs", state.log.sparse); + }; + + auto addInstanceSelection = [&](CLI::App* cmd) { + auto* group = cmd->add_option_group("Instance Selection"); + + group->add_option("-i,--instance", 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,\n" + "for example \"abc\" will select \"abcdefg\"."); + + group->add_option("--pid", state.instance.pid) + ->description("The process id of the instance to operate on."); + + return group; + }; + + auto cli = CLI::App(); + addConfigSelection(&cli); + addLoggingOptions(&cli, false); + addDebugOptions(&cli); + { - // NOLINTBEGIN - CHECK(configPath, optionConfigPath, 0, foundpath, path); - CHECK(configPath, envConfigPath, 1, foundpath, qEnvironmentVariable("QS_CONFIG_PATH")); - // NOLINTEND - - if (printInfo) { - // clang-format off - std::cout << "\nConfig path: " << OPTSTR(configPath) << "\n"; - std::cout << " - Option: " << OPTSTR(optionConfigPath) << "\n"; - std::cout << " - Environment (QS_CONFIG_PATH): " << OPTSTR(envConfigPath) << "\n"; - // clang-format on - } + cli.add_flag("-V,--version", state.misc.printVersion) + ->description("Print quickshell's version and exit."); } -foundpath:; - QString manifestPath; - int manifestPathLevel = 10; { - // NOLINTBEGIN - // clang-format off - CHECK(manifestPath, optionManifestPath, 0, foundmf, manifest); - CHECK(manifestPath, envManifestPath, 1, foundmf, qEnvironmentVariable("QS_MANIFEST")); - CHECK(manifestPath, defaultManifestPath, 2, foundmf, QDir(basePath).filePath("manifest.conf")); - // clang-format on - // NOLINTEND + auto* sub = cli.add_subcommand("log", "Read quickshell logs."); + sub->add_option("--file", state.log.file, "Log file to read.")->required(); - if (printInfo) { - // clang-format off - std::cout << "\nManifest path: " << OPTSTR(manifestPath) << "\n"; - std::cout << " - Option: " << OPTSTR(optionManifestPath) << "\n"; - std::cout << " - Environment (QS_MANIFEST): " << OPTSTR(envManifestPath) << "\n"; - std::cout << " - Default: " << OPTSTR(defaultManifestPath) << "\n"; - // clang-format on - } + sub->add_option("-r,--rules", state.log.readoutRules, "Log file to read.") + ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES."); + + addLoggingOptions(sub, false); + + // todo + // addConfigSelection(sub)->excludes(file); + + state.subcommand.log = sub; } -foundmf:; - QString configName; - int configNameLevel = 10; { - // NOLINTBEGIN - CHECK(configName, optionConfigName, 0, foundname, config); - CHECK(configName, envConfigName, 1, foundname, qEnvironmentVariable("QS_CONFIG_NAME")); - // NOLINTEND + 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."); - if (printInfo) { - // clang-format off - std::cout << "\nConfig name: " << OPTSTR(configName) << "\n"; - std::cout << " - Option: " << OPTSTR(optionConfigName) << "\n"; - std::cout << " - Environment (QS_CONFIG_NAME): " << OPTSTR(envConfigName) << "\n\n"; - // clang-format on - } + sub->add_flag("-j,--json", state.output.json, "Output the list as a json."); + + addConfigSelection(sub)->excludes(all); + addLoggingOptions(sub, false, true); + + state.subcommand.list = sub; } -foundname:; - QString configFilePath; + { + auto* sub = cli.add_subcommand("kill", "Kill quickshell instances."); + //sub->add_flag("-a,--all", "Kill all matching instances instead of just one."); + auto* instance = addInstanceSelection(sub); + addConfigSelection(sub)->excludes(instance); + addLoggingOptions(sub, false, true); + + state.subcommand.kill = sub; + } + + CLI11_PARSE(cli, argc, argv); + + { + auto level = state.log.verbosity == 0 ? QtWarningMsg + : state.log.verbosity == 1 ? QtInfoMsg + : QtDebugMsg; + + LogManager::init( + !state.log.noColor, + state.log.timestamp, + state.log.sparse, + level, + *state.log.rules, + *state.subcommand.log ? "READER" : "" + ); + } + + if (state.misc.printVersion) { + qCInfo(logBare).noquote() << "quickshell pre-release, revision" << GIT_REVISION; + } else if (*state.subcommand.log) { + return readLogFile(state); + } else if (*state.subcommand.list) { + return listInstances(state); + } else if (*state.subcommand.kill) { + return killInstances(state); + } else { + return launchFromCommand(state, coreApplication); + } + + return 0; +} + +int locateConfigFile(CommandState& cmd, QString& path) { + if (!cmd.config.path->isEmpty()) { + path = *cmd.config.path; + } else { + auto manifestPath = *cmd.config.manifest; + if (manifestPath.isEmpty()) { + auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); + auto path = configDir.filePath("manifest.conf"); + if (QFileInfo(path).isFile()) manifestPath = path; + } - if (!configPath.isEmpty() && configPathLevel <= configNameLevel) { - configFilePath = configPath; - } else if (!configName.isEmpty()) { if (!manifestPath.isEmpty()) { auto file = QFile(manifestPath); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { @@ -473,83 +332,214 @@ foundname:; auto split = line.split('='); if (split.length() != 2) { - qCritical() << "manifest line not in expected format 'name = relativepath':" << line; - exit(-1); // NOLINT + qCritical() << "Manifest line not in expected format 'name = relativepath':" << line; + return -1; } - if (split[0].trimmed() == configName) { - configFilePath = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed()); - goto foundp; + if (split[0].trimmed() == *cmd.config.name) { + path = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed()); + break; } } - qCritical() << "configuration" << configName << "not found in manifest" << manifestPath; - exit(-1); // NOLINT - } else if (manifestPathLevel < 2) { - qCritical() << "cannot open config manifest at" << manifestPath; - exit(-1); // NOLINT + if (path.isEmpty()) { + qCCritical(logBare) << "Configuration" << *cmd.config.name + << "not found when searching manifest" << manifestPath; + return -1; + } + } else { + qCCritical(logBare) << "Could not open maifest at path" << *cmd.config.manifest; + return -1; + } + } else { + auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); + + if (cmd.config.name->isEmpty()) { + path = configDir.path(); + } else { + path = configDir.filePath(*cmd.config.name); } } + } - { - auto basePathInfo = QFileInfo(basePath); - if (!basePathInfo.exists()) { - qCritical() << "base path does not exist:" << basePath; - exit(-1); // NOLINT - } else if (!QFileInfo(basePathInfo.canonicalFilePath()).isDir()) { - qCritical() << "base path is not a directory" << basePath; - exit(-1); // NOLINT + if (QFileInfo(path).isDir()) { + path = QDir(path).filePath("shell.qml"); + } + + if (!QFileInfo(path).isFile()) { + qCCritical(logBare) << "Could not open config file at" << path; + return -1; + } + + path = QFileInfo(path).canonicalFilePath(); + + return 0; +} + +void sortInstances(QVector& list) { + std::sort(list.begin(), list.end(), [](const InstanceLockInfo& a, const InstanceLockInfo& b) { + return a.instance.launchTime < b.instance.launchTime; + }); +}; + +int selectInstance(CommandState& cmd, InstanceLockInfo* instance) { + auto* basePath = QsPaths::instance()->baseRunDir(); + if (!basePath) return -1; + + QString path; + + if (cmd.instance.pid != -1) { + path = QDir(basePath->filePath("by-pid")).filePath(QString::number(cmd.instance.pid)); + if (!QsPaths::checkLock(path, instance)) { + qCInfo(logBare) << "No instance found for pid" << cmd.instance.pid; + return -1; + } + } else if (!cmd.instance.id->isEmpty()) { + path = basePath->filePath("by-pid"); + auto instances = QsPaths::collectInstances(path); + + auto itr = + std::remove_if(instances.begin(), instances.end(), [&](const InstanceLockInfo& info) { + return !info.instance.instanceId.startsWith(*cmd.instance.id); + }); + + instances.erase(itr, instances.end()); + + if (instances.isEmpty()) { + qCInfo(logBare) << "No running instances start with" << *cmd.instance.id; + return -1; + } else if (instances.length() != 1) { + qCInfo(logBare) << "More than one instance starts with" << *cmd.instance.id; + + for (auto& instance: instances) { + qCInfo(logBare).noquote() << " -" << instance.instance.instanceId; } - auto dir = QDir(basePath); - for (auto& entry: dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) { - if (entry == configName) { - configFilePath = dir.filePath(entry); - goto foundp; - } - } - - qCritical() << "no directory named " << configName << "found in base path" << basePath; - exit(-1); // NOLINT + return -1; + } else { + *instance = instances.value(0); } } else { - configFilePath = basePath; + QString configFilePath; + auto r = locateConfigFile(cmd, configFilePath); + if (r != 0) return r; + + auto pathId = + QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + + path = QDir(basePath->filePath("by-path")).filePath(pathId); + + auto instances = QsPaths::collectInstances(path); + sortInstances(instances); + + if (instances.isEmpty()) { + qCInfo(logBare) << "No running instances for" << configFilePath; + return -1; + } + + *instance = instances.value(0); } -foundp:; - auto configFile = QFileInfo(configFilePath); - if (!configFile.exists()) { - qCritical() << "config path does not exist:" << configFilePath; - exit(-1); // NOLINT + return 0; +} + +int readLogFile(CommandState& cmd) { + auto file = QFile(*cmd.log.file); + if (!file.open(QFile::ReadOnly)) { + qCCritical(logBare) << "Failed to open log file" << *cmd.log.file; + return -1; } - if (configFile.isDir()) { - configFilePath = QDir(configFilePath).filePath("shell.qml"); + return qs::log::readEncodedLogs(&file, cmd.log.timestamp, *cmd.log.readoutRules) ? 0 : -1; +} + +int listInstances(CommandState& cmd) { + auto* basePath = QsPaths::instance()->baseRunDir(); + if (!basePath) exit(-1); // NOLINT + + QString path; + QString configFilePath; + if (cmd.instance.all) { + path = basePath->filePath("by-pid"); + } else { + auto r = locateConfigFile(cmd, configFilePath); + + if (r != 0) { + qCInfo(logBare) << "Use --all to list all instances."; + return r; + } + + auto pathId = + QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + + path = QDir(basePath->filePath("by-path")).filePath(pathId); } - configFile = QFileInfo(configFilePath); - if (!configFile.exists()) { - qCritical() << "no shell.qml found in config path:" << configFilePath; - exit(-1); // NOLINT - } else if (configFile.isDir()) { - qCritical() << "shell.qml is a directory:" << configFilePath; - exit(-1); // NOLINT + auto instances = QsPaths::collectInstances(path); + + if (instances.isEmpty()) { + if (cmd.instance.all) { + qCInfo(logBare) << "No running instances."; + } else { + qCInfo(logBare) << "No running instances for" << configFilePath; + qCInfo(logBare) << "Use --all to list all instances."; + } + } else { + sortInstances(instances); + + if (cmd.output.json) { + auto array = QJsonArray(); + + for (auto& instance: instances) { + auto json = QJsonObject(); + + json["id"] = instance.instance.instanceId; + json["pid"] = instance.pid; + json["shell_id"] = instance.instance.shellId; + json["config_path"] = instance.instance.configPath; + json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate); + + array.push_back(json); + } + + auto document = QJsonDocument(array); + QTextStream(stdout) << document.toJson(QJsonDocument::Indented); + } else { + for (auto& instance: instances) { + auto launchTimeStr = instance.instance.launchTime.toString("yyyy-MM-dd hh:mm:ss"); + + auto runSeconds = instance.instance.launchTime.secsTo(QDateTime::currentDateTime()); + auto remSeconds = runSeconds % 60; + auto runMinutes = (runSeconds - remSeconds) / 60; + auto remMinutes = runMinutes % 60; + auto runHours = (runMinutes - remMinutes) / 60; + auto runtimeStr = QString("%1 hours, %2 minutes, %3 seconds") + .arg(runHours) + .arg(remMinutes) + .arg(remSeconds); + + qCInfo(logBare).noquote().nospace() + << "Instance " << instance.instance.instanceId << ":\n" + << " Process ID: " << instance.pid << '\n' + << " Shell ID: " << instance.instance.shellId << '\n' + << " Config path: " << instance.instance.configPath << '\n' + << " Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n"; + } + } } - configFilePath = QFileInfo(configFilePath).canonicalFilePath(); - configFile = QFileInfo(configFilePath); - if (!configFile.exists()) { - qCritical() << "config file does not exist:" << configFilePath; - exit(-1); // NOLINT - } else if (configFile.isDir()) { - qCritical() << "config file is a directory:" << configFilePath; - exit(-1); // NOLINT - } + return 0; +} -#undef CHECK -#undef OPTSTR +int killInstances(CommandState& cmd) { + InstanceLockInfo instance; + auto r = selectInstance(cmd, &instance); + if (r != 0) return r; - return configFilePath; + return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { + client.kill(); + qCInfo(logBare).noquote() << "Killed" << instance.instance.instanceId; + }); } template @@ -572,187 +562,164 @@ QString base36Encode(T number) { return result; } -int qs_main(int argc, char** argv) { +void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) { #if CRASH_REPORTER - qsCheckCrash(argc, argv); - auto crashHandler = qs::crash::CrashHandler(); + auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD"); + + if (!lastInfoFdStr.isEmpty()) { + auto lastInfoFd = lastInfoFdStr.toInt(); + + QFile file; + file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle); + file.seek(0); + + auto ds = QDataStream(&file); + RelaunchInfo info; + ds >> info; + + LogManager::init( + !info.noColor, + info.timestamp, + info.sparseLogsOnly, + info.defaultLogLevel, + info.logRules + ); + + qCritical().nospace() << "Quickshell has crashed under pid " + << qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt() + << " (Coredumps will be available under that pid.)"; + + qCritical() << "Further crash information is stored under" + << QsPaths::crashDir(info.instance.instanceId).path(); + + if (info.instance.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) { + qCritical() << "Quickshell crashed within 10 seconds of launching. Not restarting to avoid " + "a crash loop."; + exit(-1); // NOLINT + } else { + qCritical() << "Quickshell has been restarted."; + + launch({.configPath = info.instance.configPath}, argv, coreApplication); + } + } #endif +} - auto qArgC = 1; - auto* qArgV = argv; +int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication) { + QString configPath; - QString configFilePath; - QString initialWorkdir; - QString shellId; - QString pathId; + auto r = locateConfigFile(cmd, configPath); + if (r != 0) return r; - int debugPort = -1; - bool waitForDebug = false; - bool printInfo = false; - bool noColor = !qEnvironmentVariableIsEmpty("NO_COLOR"); - bool sparseLogsOnly = false; + return launch( + { + .configPath = configPath, + .debugPort = cmd.debug.port, + .waitForDebug = cmd.debug.wait, + }, + cmd.exec.argv, + coreApplication + ); +} - auto useQApplication = false; - auto nativeTextRendering = false; - auto desktopSettingsAware = true; - QHash envOverrides; +int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication) { + auto pathId = QCryptographicHash::hash(args.configPath.toUtf8(), QCryptographicHash::Md5).toHex(); + auto shellId = QString(pathId); - { - const auto qApplication = QCoreApplication(qArgC, qArgV); + qInfo() << "Launching config:" << args.configPath; -#if CRASH_REPORTER - auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD"); - - if (!lastInfoFdStr.isEmpty()) { - auto lastInfoFd = lastInfoFdStr.toInt(); - - QFile file; - file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle); - file.seek(0); - - auto ds = QDataStream(&file); - RelaunchInfo info; - ds >> info; - - configFilePath = info.instance.configPath; - initialWorkdir = info.instance.initialWorkdir; - noColor = info.noColor; - sparseLogsOnly = info.sparseLogsOnly; - - LogManager::init(!noColor, sparseLogsOnly); - - qCritical().nospace() << "Quickshell has crashed under pid " - << qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt() - << " (Coredumps will be available under that pid.)"; - - qCritical() << "Further crash information is stored under" - << QsPaths::crashDir(info.instance.instanceId).path(); - - if (info.instance.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) { - qCritical() << "Quickshell crashed within 10 seconds of launching. Not restarting to avoid " - "a crash loop."; - return 0; - } else { - qCritical() << "Quickshell has been restarted."; - } - - crashHandler.init(); - } else -#endif - { - - auto command = CommandInfo { - .initialWorkdir = initialWorkdir, - .debugPort = debugPort, - .waitForDebug = waitForDebug, - .printInfo = printInfo, - .noColor = noColor, - .sparseLogsOnly = sparseLogsOnly, - }; - - processCommand(argc, argv, command); - -#if CRASH_REPORTER - // Started after log manager for pretty debug logs. Unlikely anything will crash before this point, but - // this can be moved if it happens. - crashHandler.init(); -#endif - - configFilePath = commandConfigPath( - command.configPath, - command.manifestPath, - command.configName, - command.printInfo - ); - } - - pathId = QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); - shellId = pathId; - - qInfo() << "Config file path:" << configFilePath; - - if (!QFile(configFilePath).exists()) { - qCritical() << "config file does not exist"; - return -1; - } - - auto file = QFile(configFilePath); - if (!file.open(QFile::ReadOnly | QFile::Text)) { - qCritical() << "could not open config file"; - return -1; - } - - auto stream = QTextStream(&file); - while (!stream.atEnd()) { - auto line = stream.readLine().trimmed(); - if (line.startsWith("//@ pragma ")) { - auto pragma = line.sliced(11).trimmed(); - - if (pragma == "UseQApplication") useQApplication = true; - else if (pragma == "NativeTextRendering") nativeTextRendering = true; - else if (pragma == "IgnoreSystemSettings") desktopSettingsAware = false; - else if (pragma.startsWith("Env ")) { - auto envPragma = pragma.sliced(4); - auto splitIdx = envPragma.indexOf('='); - - if (splitIdx == -1) { - qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'"; - return -1; - } - - auto var = envPragma.sliced(0, splitIdx).trimmed(); - auto val = envPragma.sliced(splitIdx + 1).trimmed(); - envOverrides.insert(var, val); - } else if (pragma.startsWith("ShellId ")) { - shellId = pragma.sliced(8).trimmed(); - } else { - qCritical() << "Unrecognized pragma" << pragma; - return -1; - } - } else if (line.startsWith("import")) break; - } - - file.close(); + auto file = QFile(args.configPath); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + qCritical() << "Could not open config file" << args.configPath; + return -1; } - qInfo() << "Shell ID:" << shellId; + struct { + bool useQApplication = false; + bool nativeTextRendering = false; + bool desktopSettingsAware = true; + QHash envOverrides; + } pragmas; - if (printInfo) return 0; + auto stream = QTextStream(&file); + while (!stream.atEnd()) { + auto line = stream.readLine().trimmed(); + if (line.startsWith("//@ pragma ")) { + auto pragma = line.sliced(11).trimmed(); + + if (pragma == "UseQApplication") pragmas.useQApplication = true; + else if (pragma == "NativeTextRendering") pragmas.nativeTextRendering = true; + else if (pragma == "IgnoreSystemSettings") pragmas.desktopSettingsAware = false; + else if (pragma.startsWith("Env ")) { + auto envPragma = pragma.sliced(4); + auto splitIdx = envPragma.indexOf('='); + + if (splitIdx == -1) { + qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'"; + return -1; + } + + auto var = envPragma.sliced(0, splitIdx).trimmed(); + auto val = envPragma.sliced(splitIdx + 1).trimmed(); + pragmas.envOverrides.insert(var, val); + } else if (pragma.startsWith("ShellId ")) { + shellId = pragma.sliced(8).trimmed(); + } else { + qCritical() << "Unrecognized pragma" << pragma; + return -1; + } + } else if (line.startsWith("import")) break; + } + + file.close(); + + qInfo() << "Shell ID:" << shellId << "Path ID" << pathId; auto launchTime = qs::Common::LAUNCH_TIME.toSecsSinceEpoch(); InstanceInfo::CURRENT = InstanceInfo { .instanceId = base36Encode(getpid()) + base36Encode(launchTime), - .configPath = configFilePath, + .configPath = args.configPath, .shellId = shellId, - .initialWorkdir = initialWorkdir, .launchTime = qs::Common::LAUNCH_TIME, }; #if CRASH_REPORTER - crashHandler.setInstanceInfo(RelaunchInfo { - .instance = InstanceInfo::CURRENT, - .noColor = noColor, - .sparseLogsOnly = sparseLogsOnly, - }); -#endif + auto crashHandler = crash::CrashHandler(); + crashHandler.init(); - for (auto [var, val]: envOverrides.asKeyValueRange()) { - qputenv(var.toUtf8(), val.toUtf8()); + { + auto* log = LogManager::instance(); + crashHandler.setRelaunchInfo({ + .instance = InstanceInfo::CURRENT, + .noColor = !log->colorLogs, + .timestamp = log->timestampLogs, + .sparseLogsOnly = log->isSparse(), + .defaultLogLevel = log->defaultLevel(), + .logRules = log->rulesString(), + }); } +#endif QsPaths::init(shellId, pathId); QsPaths::instance()->linkRunDir(); QsPaths::instance()->linkPathDir(); + LogManager::initFs(); - if (auto* cacheDir = QsPaths::instance()->cacheDir()) { - auto qmlCacheDir = cacheDir->filePath("qml-engine-cache"); - qputenv("QML_DISK_CACHE_PATH", qmlCacheDir.toLocal8Bit()); - - if (!qEnvironmentVariableIsSet("QML_DISK_CACHE")) { - qputenv("QML_DISK_CACHE", "aot,qmlc"); - } + for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) { + qputenv(var.toUtf8(), val.toUtf8()); } + // The qml engine currently refuses to cache non file (qsintercept) paths. + + // if (auto* cacheDir = QsPaths::instance()->cacheDir()) { + // auto qmlCacheDir = cacheDir->filePath("qml-engine-cache"); + // qputenv("QML_DISK_CACHE_PATH", qmlCacheDir.toLocal8Bit()); + // + // if (!qEnvironmentVariableIsSet("QML_DISK_CACHE")) { + // qputenv("QML_DISK_CACHE", "aot,qmlc"); + // } + // } + // While the simple animation driver can lead to better animations in some cases, // it also can cause excessive repainting at excessively high framerates which can // lead to noticeable amounts of gpu usage, including overheating on some systems. @@ -789,27 +756,24 @@ int qs_main(int argc, char** argv) { QIcon::setFallbackSearchPaths(fallbackPaths); } - QGuiApplication::setDesktopSettingsAware(desktopSettingsAware); + QGuiApplication::setDesktopSettingsAware(pragmas.desktopSettingsAware); + + delete coreApplication; QGuiApplication* app = nullptr; + auto qArgC = 0; - if (useQApplication) { - app = new QApplication(qArgC, qArgV); + if (pragmas.useQApplication) { + app = new QApplication(qArgC, argv); } else { - app = new QGuiApplication(qArgC, qArgV); + app = new QGuiApplication(qArgC, argv); } - LogManager::initFs(); - - if (debugPort != -1) { + if (args.debugPort != -1) { QQmlDebuggingEnabler::enableDebugging(true); - auto wait = waitForDebug ? QQmlDebuggingEnabler::WaitForClient - : QQmlDebuggingEnabler::DoNotWaitForClient; - QQmlDebuggingEnabler::startTcpDebugServer(debugPort, wait); - } - - if (!initialWorkdir.isEmpty()) { - QDir::setCurrent(initialWorkdir); + auto wait = args.waitForDebug ? QQmlDebuggingEnabler::WaitForClient + : QQmlDebuggingEnabler::DoNotWaitForClient; + QQmlDebuggingEnabler::startTcpDebugServer(args.debugPort, wait); } QuickshellPlugin::initPlugins(); @@ -818,17 +782,19 @@ int qs_main(int argc, char** argv) { // Use a fully transparent window with a colored rect. QQuickWindow::setDefaultAlphaBuffer(true); - if (nativeTextRendering) { + if (pragmas.nativeTextRendering) { QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); } qs::ipc::IpcServer::start(); QsPaths::instance()->createLock(); - auto root = RootWrapper(configFilePath, shellId); + auto root = RootWrapper(args.configPath, shellId); QGuiApplication::setQuitOnLastWindowClosed(false); auto code = QGuiApplication::exec(); delete app; return code; } + +} // namespace qs::launch diff --git a/src/core/main.hpp b/src/core/main.hpp index 33921b40..795bf7ad 100644 --- a/src/core/main.hpp +++ b/src/core/main.hpp @@ -1,3 +1,7 @@ #pragma once -int qs_main(int argc, char** argv); // NOLINT +namespace qs::launch { + +int main(int argc, char** argv); // NOLINT + +} diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index 496aaba6..31f51826 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -64,7 +64,7 @@ void CrashHandler::init() { qCInfo(logCrashHandler) << "Crash handler initialized."; } -void CrashHandler::setInstanceInfo(const RelaunchInfo& info) { +void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) { this->d->infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC); if (this->d->infoFd == -1) { diff --git a/src/crash/handler.hpp b/src/crash/handler.hpp index dd618f7f..2a1d86fa 100644 --- a/src/crash/handler.hpp +++ b/src/crash/handler.hpp @@ -14,7 +14,7 @@ public: Q_DISABLE_COPY_MOVE(CrashHandler); void init(); - void setInstanceInfo(const RelaunchInfo& info); + void setRelaunchInfo(const RelaunchInfo& info); private: CrashHandlerPrivate* d; diff --git a/src/crash/main.cpp b/src/crash/main.cpp index 8583ff91..9f56d894 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -44,7 +44,14 @@ void qsCheckCrash(int argc, char** argv) { ds >> info; } - LogManager::init(!info.noColor, false); + LogManager::init( + !info.noColor, + info.timestamp, + info.sparseLogsOnly, + info.defaultLogLevel, + info.logRules + ); + auto crashDir = QsPaths::crashDir(info.instance.instanceId); qCInfo(logCrashReporter) << "Starting crash reporter..."; diff --git a/src/main.cpp b/src/main.cpp index 935853ac..7e4811da 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,3 @@ #include "core/main.hpp" -int main(int argc, char** argv) { return qs_main(argc, argv); } +int main(int argc, char** argv) { return qs::launch::main(argc, argv); } From 95245cb6a5c8f3e3bfa6810a26ad7a0ad71b735d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 1 Sep 2024 17:30:58 -0700 Subject: [PATCH 148/305] x11/panelwindow: fix strut start/end, patch around awesome, resize all panels --- src/x11/panel_window.cpp | 55 ++++++++++++++++++++++++++++++++-------- src/x11/panel_window.hpp | 5 ++-- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index b092b7e5..d0781937 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,7 @@ #include "../core/generation.hpp" #include "../core/panelinterface.hpp" #include "../core/proxywindow.hpp" +#include "../core/qmlscreen.hpp" #include "util.hpp" class XPanelStack { @@ -56,6 +58,17 @@ public: } } + void updateLowerDimensions(XPanelWindow* exclude) { + auto& panels = this->mPanels[EngineGeneration::findObjectGeneration(exclude)]; + + // update all panels lower than the one we start from + auto found = false; + for (auto* panel: panels) { + if (panel == exclude) found = true; + else if (found) panel->updateDimensions(false); + } + } + private: std::map> mPanels; }; @@ -151,9 +164,8 @@ qint32 XPanelWindow::exclusiveZone() const { return this->mExclusiveZone; } void XPanelWindow::setExclusiveZone(qint32 exclusiveZone) { if (this->mExclusiveZone == exclusiveZone) return; this->mExclusiveZone = exclusiveZone; - const bool wasNormal = this->mExclusionMode == ExclusionMode::Normal; this->setExclusionMode(ExclusionMode::Normal); - if (wasNormal) this->updateStrut(); + this->updateStrut(); emit this->exclusiveZoneChanged(); } @@ -225,14 +237,16 @@ void XPanelWindow::connectScreen() { this->mTrackedScreen, &QScreen::geometryChanged, this, - &XPanelWindow::updateDimensions + &XPanelWindow::updateDimensionsSlot ); } this->updateDimensions(); } -void XPanelWindow::updateDimensions() { +void XPanelWindow::updateDimensionsSlot() { this->updateDimensions(); } + +void XPanelWindow::updateDimensions(bool propagate) { if (this->window == nullptr || this->window->handle() == nullptr || this->mScreen == nullptr) return; @@ -302,7 +316,15 @@ void XPanelWindow::updateDimensions() { } this->window->setGeometry(geometry); - this->updateStrut(); + this->updateStrut(propagate); + + // AwesomeWM incorrectly repositions the window without this. + // See https://github.com/polybar/polybar/blob/f0f9563ecf39e78ba04cc433cb7b38a83efde473/src/components/bar.cpp#L666 + QTimer::singleShot(0, this, [this, geometry]() { + // forces second call not to be discarded as duplicate + this->window->setGeometry({0, 0, 0, 0}); + this->window->setGeometry(geometry); + }); } void XPanelWindow::updatePanelStack() { @@ -314,7 +336,10 @@ void XPanelWindow::updatePanelStack() { } void XPanelWindow::getExclusion(int& side, quint32& exclusiveZone) { - if (this->mExclusionMode == ExclusionMode::Ignore) return; + if (this->mExclusionMode == ExclusionMode::Ignore) { + exclusiveZone = 0; + return; + } auto& anchors = this->mAnchors; if (anchors.mLeft || anchors.mRight || anchors.mTop || anchors.mBottom) { @@ -344,7 +369,7 @@ void XPanelWindow::getExclusion(int& side, quint32& exclusiveZone) { } } -void XPanelWindow::updateStrut() { +void XPanelWindow::updateStrut(bool propagate) { if (this->window == nullptr || this->window->handle() == nullptr) return; auto* conn = x11Connection(); @@ -359,13 +384,19 @@ void XPanelWindow::updateStrut() { return; } + // Due to missing headers it isn't even possible to do this right. + // We assume a single xinerama monitor with a matching size root. + auto screenGeometry = this->window->screen()->geometry(); + auto horizontal = side == 0 || side == 1; + auto data = std::array(); data[side] = exclusiveZone; - // https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm45573693101552 - // assuming "specified in root window coordinates" means relative to the window geometry - // in which case only the end position should be set, to the opposite extent. - data[side * 2 + 5] = side == 0 || side == 1 ? this->window->height() : this->window->width(); + auto start = horizontal ? screenGeometry.top() + this->window->y() + : screenGeometry.left() + this->window->x(); + + data[4 + side * 2] = start; + data[5 + side * 2] = start + (horizontal ? this->window->height() : this->window->width()); xcb_change_property( conn, @@ -388,6 +419,8 @@ void XPanelWindow::updateStrut() { 12, data.data() ); + + if (propagate) XPanelStack::instance()->updateLowerDimensions(this); } void XPanelWindow::updateAboveWindows() { diff --git a/src/x11/panel_window.hpp b/src/x11/panel_window.hpp index 4cdfaaa3..40d9a9bf 100644 --- a/src/x11/panel_window.hpp +++ b/src/x11/panel_window.hpp @@ -79,15 +79,16 @@ signals: private slots: void xInit(); - void updateDimensions(); void updatePanelStack(); + void updateDimensionsSlot(); private: void connectScreen(); void getExclusion(int& side, quint32& exclusiveZone); - void updateStrut(); + void updateStrut(bool propagate = true); void updateAboveWindows(); void updateFocusable(); + void updateDimensions(bool propagate = true); QPointer mTrackedScreen = nullptr; bool mAboveWindows = true; From 6cb7d894ab2d4a3ce9cab22bd71f775472a3fd6c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 1 Sep 2024 18:08:53 -0700 Subject: [PATCH 149/305] x11/panelwindow: fix multi monitor struts --- src/x11/panel_window.cpp | 31 +++++++++++++++++++++++++++---- src/x11/panel_window.hpp | 3 +++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index d0781937..b5afab44 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -239,11 +239,28 @@ void XPanelWindow::connectScreen() { this, &XPanelWindow::updateDimensionsSlot ); + + QObject::connect( + this->mTrackedScreen, + &QScreen::virtualGeometryChanged, + this, + &XPanelWindow::onScreenVirtualGeometryChanged + + ); } this->updateDimensions(); } +// For some reason this gets sent multiple times with the same value. +void XPanelWindow::onScreenVirtualGeometryChanged() { + auto geometry = this->mTrackedScreen->virtualGeometry(); + if (geometry != this->lastScreenVirtualGeometry) { + this->lastScreenVirtualGeometry = geometry; + this->updateStrut(false); + } +} + void XPanelWindow::updateDimensionsSlot() { this->updateDimensions(); } void XPanelWindow::updateDimensions(bool propagate) { @@ -384,16 +401,22 @@ void XPanelWindow::updateStrut(bool propagate) { return; } - // Due to missing headers it isn't even possible to do this right. - // We assume a single xinerama monitor with a matching size root. + auto rootGeometry = this->window->screen()->virtualGeometry(); auto screenGeometry = this->window->screen()->geometry(); auto horizontal = side == 0 || side == 1; + switch (side) { + case 0: exclusiveZone += screenGeometry.left(); break; + case 1: exclusiveZone += rootGeometry.right() - screenGeometry.right(); break; + case 2: exclusiveZone += screenGeometry.top(); break; + case 3: exclusiveZone += rootGeometry.bottom() - screenGeometry.bottom(); break; + default: break; + } + auto data = std::array(); data[side] = exclusiveZone; - auto start = horizontal ? screenGeometry.top() + this->window->y() - : screenGeometry.left() + this->window->x(); + auto start = horizontal ? this->window->y() : this->window->x(); data[4 + side * 2] = start; data[5 + side * 2] = start + (horizontal ? this->window->height() : this->window->width()); diff --git a/src/x11/panel_window.hpp b/src/x11/panel_window.hpp index 40d9a9bf..9bcaf64d 100644 --- a/src/x11/panel_window.hpp +++ b/src/x11/panel_window.hpp @@ -81,6 +81,7 @@ private slots: void xInit(); void updatePanelStack(); void updateDimensionsSlot(); + void onScreenVirtualGeometryChanged(); private: void connectScreen(); @@ -97,6 +98,8 @@ private: Margins mMargins; qint32 mExclusiveZone = 0; ExclusionMode::Enum mExclusionMode = ExclusionMode::Auto; + + QRect lastScreenVirtualGeometry; XPanelEventFilter eventFilter; friend class XPanelStack; From 397476244c4d4e7b50b7a9f391e325f6f4158e2d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 1 Sep 2024 19:00:13 -0700 Subject: [PATCH 150/305] x11/panelwindow: add option to disable Xinerama aware struts Breaks bad WMs less. --- src/x11/panel_window.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index b5afab44..3f0aa279 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -386,6 +387,11 @@ void XPanelWindow::getExclusion(int& side, quint32& exclusiveZone) { } } +// Disable xinerama structs to break multi monitor configurations with bad WMs less. +// Usually this results in one monitor at the top left corner of the root window working +// perfectly and all others being broken semi randomly. +static bool XINERAMA_STRUTS = qEnvironmentVariableIsEmpty("QS_NO_XINERAMA_STRUTS"); // NOLINT + void XPanelWindow::updateStrut(bool propagate) { if (this->window == nullptr || this->window->handle() == nullptr) return; auto* conn = x11Connection(); @@ -405,12 +411,14 @@ void XPanelWindow::updateStrut(bool propagate) { auto screenGeometry = this->window->screen()->geometry(); auto horizontal = side == 0 || side == 1; - switch (side) { - case 0: exclusiveZone += screenGeometry.left(); break; - case 1: exclusiveZone += rootGeometry.right() - screenGeometry.right(); break; - case 2: exclusiveZone += screenGeometry.top(); break; - case 3: exclusiveZone += rootGeometry.bottom() - screenGeometry.bottom(); break; - default: break; + if (XINERAMA_STRUTS) { + switch (side) { + case 0: exclusiveZone += screenGeometry.left(); break; + case 1: exclusiveZone += rootGeometry.right() - screenGeometry.right(); break; + case 2: exclusiveZone += screenGeometry.top(); break; + case 3: exclusiveZone += rootGeometry.bottom() - screenGeometry.bottom(); break; + default: break; + } } auto data = std::array(); From 465d5402f29e0927a5ed2f06aa1b640bb792f96b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 2 Sep 2024 22:19:36 -0700 Subject: [PATCH 151/305] crash: fix off-end read when copying environ array --- src/crash/handler.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index 31f51826..1f300cc9 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -126,8 +126,10 @@ bool CrashHandlerPrivate::minidumpCallback( auto populateEnv = [&]() { auto senvi = 0; - while (envi < 4095) { - env[envi++] = environ[senvi++]; // NOLINT + while (envi != 4095) { + auto var = environ[senvi++]; // NOLINT + if (var == nullptr) break; + env[envi++] = var; } env[envi] = nullptr; From 3a1eec0ed59ce46ff878fbbbdf260a2268566709 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 5 Sep 2024 21:44:05 -0700 Subject: [PATCH 152/305] core/log: fix sparse logs being on by default --- src/core/logging.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 72ac81b1..60d2c3c2 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -192,7 +192,7 @@ void LogManager::filterCategory(QLoggingCategory* category) { filter.applyRule(categoryName, rule); } - if (isQs && instance->sparse) { + if (isQs && !instance->sparse) { // We assume the category name pointer will always be the same and be comparable in the message handler. LogManager::instance()->sparseFilters.insert( static_cast(category->categoryName()), @@ -241,7 +241,13 @@ void LogManager::init( auto* thread = new QThread(); instance->threadProxy.moveToThread(thread); thread->start(); - QMetaObject::invokeMethod(&instance->threadProxy, "initInThread", Qt::BlockingQueuedConnection); + + QMetaObject::invokeMethod( + &instance->threadProxy, + &LoggingThreadProxy::initInThread, + Qt::BlockingQueuedConnection + ); + qCDebug(logLogging) << "Logger initialized."; } From 85be3861ceadba6e3d877a64cade15015ea0c96f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 9 Sep 2024 03:15:16 -0700 Subject: [PATCH 153/305] io/fileview: add FileView --- src/core/util.hpp | 41 ++++++ src/io/CMakeLists.txt | 8 +- src/io/FileView.qml | 48 +++++++ src/io/fileview.cpp | 302 ++++++++++++++++++++++++++++++++++++++++++ src/io/fileview.hpp | 236 +++++++++++++++++++++++++++++++++ src/io/module.md | 1 + 6 files changed, 635 insertions(+), 1 deletion(-) create mode 100644 src/io/FileView.qml create mode 100644 src/io/fileview.cpp create mode 100644 src/io/fileview.hpp diff --git a/src/core/util.hpp b/src/core/util.hpp index b2599234..3c1a5ac6 100644 --- a/src/core/util.hpp +++ b/src/core/util.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include + // NOLINTBEGIN #define DROP_EMIT(object, func) \ DropEmitter(object, static_cast([](typeof(object) o) { o->func(); })) @@ -72,6 +74,8 @@ private: DECLARE_MEMBER_GET(name); \ DECLARE_MEMBER_SET(name, setter) +#define DECLARE_MEMBER_SETONLY(class, name, setter, member, signal) DECLARE_MEMBER(cl + #define DECLARE_MEMBER_FULL(class, name, setter, member, signal) \ DECLARE_MEMBER(class, name, member, signal); \ DECLARE_MEMBER_GETSET(name, setter) @@ -123,6 +127,8 @@ private: #define DEFINE_MEMBER_GETSET(Class, name, setter) \ DEFINE_MEMBER_GET(Class, name) \ DEFINE_MEMBER_SET(Class, name, setter) + +#define MEMBER_EMIT(name) std::remove_reference_t::M_##name::emitter(this) // NOLINTEND template @@ -154,6 +160,12 @@ public: } else { if (MemberMetadata::get(obj) == value) return DropEmitter(); obj->*member = value; + return MemberMetadata::emitter(obj); + } + } + + static Ret emitter(Class* obj) { + if constexpr (signal != nullptr) { return DropEmitter(obj, &MemberMetadata::emitForObject); } } @@ -170,3 +182,32 @@ public: using Ref = const Type&; using Ret = std::conditional_t; }; + +class GuardedEmitBlocker { +public: + explicit GuardedEmitBlocker(bool* var): var(var) { *this->var = true; } + ~GuardedEmitBlocker() { *this->var = false; } + Q_DISABLE_COPY_MOVE(GuardedEmitBlocker); + +private: + bool* var; +}; + +template +class GuardedEmitter { + using Traits = MemberPointerTraits; + using Class = Traits::Class; + + bool blocked = false; + +public: + GuardedEmitter() = default; + ~GuardedEmitter() = default; + Q_DISABLE_COPY_MOVE(GuardedEmitter); + + void call(Class* obj) { + if (!this->blocked) (obj->*signal)(); + } + + GuardedEmitBlocker block() { return GuardedEmitBlocker(&this->blocked); } +}; diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index 23758064..7113cd7d 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -1,6 +1,7 @@ qt_add_library(quickshell-io STATIC datastream.cpp process.cpp + fileview.cpp ) add_library(quickshell-io-init OBJECT init.cpp) @@ -9,7 +10,12 @@ if (SOCKETS) target_sources(quickshell-io PRIVATE socket.cpp) endif() -qt_add_qml_module(quickshell-io URI Quickshell.Io VERSION 0.1) +qt_add_qml_module(quickshell-io + URI Quickshell.Io + VERSION 0.1 + QML_FILES + FileView.qml +) target_link_libraries(quickshell-io PRIVATE ${QT_DEPS}) target_link_libraries(quickshell-io-init PRIVATE ${QT_DEPS}) diff --git a/src/io/FileView.qml b/src/io/FileView.qml new file mode 100644 index 00000000..97e99c40 --- /dev/null +++ b/src/io/FileView.qml @@ -0,0 +1,48 @@ +import Quickshell.Io + +FileViewInternal { + property bool preload: this.__preload; + property bool blockLoading: this.__blockLoading; + property bool blockAllReads: this.__blockAllReads; + property string path: this.__path; + + onPreloadChanged: this.__preload = preload; + onBlockLoadingChanged: this.__blockLoading = this.blockLoading; + onBlockAllReadsChanged: this.__blockAllReads = this.blockAllReads; + + // Unfortunately path can't be kept as an empty string until the file loads + // without using QQmlPropertyValueInterceptor which is private. If we lean fully + // into using private code in the future, there will be no reason not to do it here. + + onPathChanged: { + if (!this.preload) this.__preload = false; + this.__path = this.path; + if (this.preload) this.__preload = true; + } + + // The C++ side can't force bindings to be resolved in a specific order so + // its done here. Functions are used to avoid the eager loading aspect of properties. + + // Preload is set as it is below to avoid starting an async read from a preload + // if the user wants an initial blocking read. + + function text(): string { + if (!this.preload) this.__preload = false; + this.__blockLoading = this.blockLoading; + this.__blockAllReads = this.blockAllReads; + this.__path = this.path; + const text = this.__text; + if (this.preload) this.__preload = true; + return text; + } + + function data(): string { + if (!this.preload) this.__preload = false; + this.__blockLoading = this.blockLoading; + this.__blockAllReads = this.blockAllReads; + this.__path = this.path; + const data = this.__data; + if (this.preload) this.__preload = true; + return data; + } +} diff --git a/src/io/fileview.cpp b/src/io/fileview.cpp new file mode 100644 index 00000000..40dde6d7 --- /dev/null +++ b/src/io/fileview.cpp @@ -0,0 +1,302 @@ +#include "fileview.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/util.hpp" + +namespace qs::io { + +Q_LOGGING_CATEGORY(logFileView, "quickshell.io.fileview", QtWarningMsg); + +FileViewReader::FileViewReader(QString path, bool doStringConversion) + : doStringConversion(doStringConversion) { + this->state.path = std::move(path); + this->setAutoDelete(false); + this->blockMutex.lock(); +} + +void FileViewReader::run() { + FileViewReader::read(this->state, this->doStringConversion); + + this->blockMutex.unlock(); + QMetaObject::invokeMethod(this, &FileViewReader::finished, Qt::QueuedConnection); +} + +void FileViewReader::block() { + // block until a lock can be acauired, then immediately drop it + auto unused = QMutexLocker(&this->blockMutex); +} + +void FileViewReader::finished() { + emit this->done(); + delete this; +} + +void FileViewReader::read(FileViewState& state, bool doStringConversion) { + { + qCDebug(logFileView) << "Reader started for" << state.path; + + auto info = QFileInfo(state.path); + state.exists = info.exists(); + + if (!state.exists) return; + + if (!info.isFile()) { + qCCritical(logFileView) << state.path << "is not a file."; + goto error; + } else if (!info.isReadable()) { + qCCritical(logFileView) << "No permission to read" << state.path; + state.error = true; + goto error; + } + + auto file = QFile(state.path); + + if (!file.open(QFile::ReadOnly)) { + qCCritical(logFileView) << "Failed to open" << state.path; + goto error; + } + + auto& data = state.data; + data = QByteArray(file.size(), Qt::Uninitialized); + + qint64 i = 0; + + while (true) { + auto r = file.read(data.data() + i, data.length() - i); // NOLINT + + if (r == -1) { + qCCritical(logFileView) << "Failed to read" << state.path; + goto error; + } else if (r == 0) { + data.resize(i); + break; + } + + i += r; + } + + if (doStringConversion) { + state.text = QString::fromUtf8(state.data); + state.textDirty = false; + } else { + state.textDirty = true; + } + + return; + } + +error: + state.error = true; +} + +void FileView::loadAsync(bool doStringConversion) { + if (!this->reader || this->pathInFlight != this->targetPath) { + this->cancelAsync(); + this->pathInFlight = this->targetPath; + + if (this->targetPath.isEmpty()) { + auto state = FileViewState(); + this->updateState(state); + } else { + qCDebug(logFileView) << "Starting async load for" << this << "of" << this->targetPath; + this->reader = new FileViewReader(this->targetPath, doStringConversion); + QObject::connect(this->reader, &FileViewReader::done, this, &FileView::readerFinished); + QThreadPool::globalInstance()->start(this->reader); // takes ownership + } + } +} + +void FileView::cancelAsync() { + if (this->reader) { + qCDebug(logFileView) << "Disowning async read for" << this; + QObject::disconnect(this->reader, nullptr, this, nullptr); + this->reader = nullptr; + } +} + +void FileView::readerFinished() { + if (this->sender() != this->reader) { + qCWarning(logFileView) << "got read finished from dropped FileViewReader" << this->sender(); + return; + } + + qCDebug(logFileView) << "Async load finished for" << this; + this->updateState(this->reader->state); + this->reader = nullptr; +} + +void FileView::reload() { this->updatePath(); } + +bool FileView::blockUntilLoaded() { + if (this->reader != nullptr) { + QObject::disconnect(this->reader, nullptr, this, nullptr); + this->reader->block(); + this->updateState(this->reader->state); + this->reader = nullptr; + return true; + } else return false; +} + +void FileView::loadSync() { + if (this->targetPath.isEmpty()) { + auto state = FileViewState(); + this->updateState(state); + } else if (!this->blockUntilLoaded()) { + auto state = FileViewState {.path = this->targetPath}; + FileViewReader::read(state, false); + this->updateState(state); + } +} + +void FileView::updateState(FileViewState& newState) { + DEFINE_DROP_EMIT_IF(newState.path != this->state.path, this, pathChanged); + // assume if the path was changed the data also changed + auto dataChanged = pathChanged || newState.data != this->state.data; + // DEFINE_DROP_EMIT_IF(newState.exists != this->state.exists, this, existsChanged); + + this->mPrepared = true; + auto loadedChanged = this->setLoadedOrAsync(!newState.path.isEmpty() && newState.exists); + + this->state.path = std::move(newState.path); + + if (dataChanged) { + this->state.data = newState.data; + this->state.text = newState.text; + this->state.textDirty = newState.textDirty; + } + + this->state.exists = newState.exists; + this->state.error = newState.error; + + DropEmitter::call( + pathChanged, + // existsChanged, + loadedChanged + ); + + if (dataChanged) this->emitDataChanged(); + + if (this->state.error) emit this->loadFailed(); +} + +void FileView::textConversion() { + if (this->state.textDirty) { + this->state.text = QString::fromUtf8(this->state.data); + this->state.textDirty = false; + } +} + +QString FileView::path() const { return this->state.path; } + +void FileView::setPath(const QString& path) { + auto p = path.startsWith("file://") ? path.sliced(7) : path; + if (p == this->targetPath) return; + this->targetPath = p; + this->updatePath(); +} + +void FileView::updatePath() { + this->mPrepared = false; + + if (this->targetPath.isEmpty()) { + auto state = FileViewState(); + this->updateState(state); + } else if (this->mPreload) { + this->loadAsync(true); + } else { + // loadAsync will do this already + this->cancelAsync(); + this->emitDataChanged(); + } +} + +bool FileView::shouldBlock() const { + return this->mBlockAllReads || (this->mBlockLoading && !this->mLoadedOrAsync); +} + +QByteArray FileView::data() { + auto guard = this->dataChangedEmitter.block(); + + if (!this->mPrepared) { + if (this->shouldBlock()) this->loadSync(); + else this->loadAsync(false); + } + + return this->state.data; +} + +QString FileView::text() { + auto guard = this->textChangedEmitter.block(); + + if (!this->mPrepared) { + if (this->shouldBlock()) this->loadSync(); + else this->loadAsync(true); + } + + this->textConversion(); + return this->state.text; +} + +void FileView::emitDataChanged() { + this->dataChangedEmitter.call(this); + this->textChangedEmitter.call(this); + emit this->dataChanged(); + emit this->textChanged(); +} + +DEFINE_MEMBER_GETSET(FileView, isLoadedOrAsync, setLoadedOrAsync); +DEFINE_MEMBER_GET(FileView, shouldPreload); +DEFINE_MEMBER_GET(FileView, blockLoading); +DEFINE_MEMBER_GET(FileView, blockAllReads); + +void FileView::setPreload(bool preload) { + if (preload != this->mPreload) { + this->mPreload = preload; + emit this->preloadChanged(); + + if (preload) this->emitDataChanged(); + + if (!this->mPrepared && this->mPreload) { + this->loadAsync(false); + } + } +} + +void FileView::setBlockLoading(bool blockLoading) { + if (blockLoading != this->mBlockLoading) { + auto wasBlocking = this->shouldBlock(); + + this->mBlockLoading = blockLoading; + emit this->blockLoadingChanged(); + + if (!wasBlocking && this->shouldBlock()) { + this->emitDataChanged(); + } + } +} + +void FileView::setBlockAllReads(bool blockAllReads) { + if (blockAllReads != this->mBlockAllReads) { + auto wasBlocking = this->shouldBlock(); + + this->mBlockAllReads = blockAllReads; + emit this->blockAllReadsChanged(); + + if (!wasBlocking && this->shouldBlock()) { + this->emitDataChanged(); + } + } +} + +} // namespace qs::io diff --git a/src/io/fileview.hpp b/src/io/fileview.hpp new file mode 100644 index 00000000..04ed421a --- /dev/null +++ b/src/io/fileview.hpp @@ -0,0 +1,236 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/doc.hpp" +#include "../core/util.hpp" + +namespace qs::io { + +struct FileViewState { + QString path; + QString text; + QByteArray data; + bool textDirty = false; + bool exists = false; + bool error = false; +}; + +class FileView; + +class FileViewReader + : public QObject + , public QRunnable { + Q_OBJECT; + +public: + explicit FileViewReader(QString path, bool doStringConversion); + + void run() override; + void block(); + + FileViewState state; + + static void read(FileViewState& state, bool doStringConversion); + +signals: + void done(); + +private slots: + void finished(); + +private: + bool doStringConversion; + QMutex blockMutex; +}; + +///! Simplified reader for small files. +/// A reader for small to medium files that don't need seeking/cursor access, +/// suitable for most text files. +/// +/// #### Example: Reading a JSON +/// ```qml +/// FileView { +/// id: jsonFile +/// path: Qt.resolvedUrl("./your.json") +/// // Forces the file to be loaded by the time we call JSON.parse(). +/// // see blockLoading's property documentation for details. +/// blockLoading: true +/// } +/// +/// readonly property var jsonData: JSON.parse(jsonFile.text()) +/// ``` +class FileView: public QObject { + Q_OBJECT; + // clang-format off + /// The path to the file that should be read, or an empty string to unload the file. + QSDOC_PROPERTY_OVERRIDE(QString path READ path WRITE setPath NOTIFY pathChanged); + /// If the file should be loaded in the background immediately when set. Defaults to true. + /// + /// This may either increase or decrease the amount of time it takes to load the file + /// depending on how large the file is, how fast its storage is, and how you access its data. + QSDOC_PROPERTY_OVERRIDE(bool preload READ shouldPreload WRITE setPreload NOTIFY preloadChanged); + /// If @@text() and @@data() should block all operations until the file is loaded. Defaults to false. + /// + /// If the file is already loaded, no blocking will occur. + /// If a file was loaded, and @@path was changed to a new file, no blocking will occur. + /// + /// > [!WARNING] Blocking operations should be used carefully to avoid stutters and other performance + /// > degradations. Blocking means that your interface **WILL NOT FUNCTION** during the call. + /// > + /// > **We recommend you use a blocking load ONLY for files loaded before the windows of your shell + /// > are loaded, which happens after `Component.onCompleted` runs for the root component of your shell.** + /// > + /// > The most reasonable use case would be to load things like configuration files that the program + /// > must have available. + QSDOC_PROPERTY_OVERRIDE(bool blockLoading READ blockLoading WRITE setBlockLoading NOTIFY blockLoadingChanged); + /// If @@text() and @@data() should block all operations while a file loads. Defaults to false. + /// + /// This is nearly identical to @@blockLoading, but will additionally block when + /// a file is loaded and @@path changes. + /// + /// > [!WARNING] We cannot think of a valid use case for this. + /// > You almost definitely want @@blockLoading. + QSDOC_PROPERTY_OVERRIDE(bool blockAllReads READ blockAllReads WRITE setBlockAllReads NOTIFY blockAllReadsChanged); + + QSDOC_HIDE Q_PROPERTY(QString __path READ path WRITE setPath NOTIFY pathChanged); + QSDOC_HIDE Q_PROPERTY(QString __text READ text NOTIFY internalTextChanged); + QSDOC_HIDE Q_PROPERTY(QByteArray __data READ data NOTIFY internalDataChanged); + QSDOC_HIDE Q_PROPERTY(bool __preload READ shouldPreload WRITE setPreload NOTIFY preloadChanged); + /// If a file is currently loaded, which may or may not be the one currently specified by @@path. + /// + /// > [!INFO] If a file is loaded, @@path is changed, and a new file is loaded, + /// > this property will stay true the whole time. + /// > If @@path is set to an empty string to unload the file it will become false. + Q_PROPERTY(bool loaded READ isLoadedOrAsync NOTIFY loadedOrAsyncChanged); + QSDOC_HIDE Q_PROPERTY(bool __blockLoading READ blockLoading WRITE setBlockLoading NOTIFY blockLoadingChanged); + QSDOC_HIDE Q_PROPERTY(bool __blockAllReads READ blockAllReads WRITE setBlockAllReads NOTIFY blockAllReadsChanged); + // clang-format on + QML_NAMED_ELEMENT(FileViewInternal); + QSDOC_NAMED_ELEMENT(FileView); + +public: + explicit FileView(QObject* parent = nullptr): QObject(parent) {} + + /// Returns the data of the file specified by @@path as text. + /// + /// If @@blockAllReads is true, all changes to @@path will cause the program to block + /// when this function is called. + /// + /// If @@blockLoading is true, reading this property before the file has been loaded + /// will block, but changing @@path or calling @@reload() will return the old data + /// until the load completes. + /// + /// If neither is true, an empty string will be returned if no file is loaded, + /// otherwise it will behave as in the case above. + /// + /// > [!INFO] Due to technical limitations, @@text() could not be a property, + /// > however you can treat it like a property, it will trigger property updates + /// > as a property would, and the signal `textChanged()` is present. + //@ Q_INVOKABLE QString text(); + /// Returns the data of the file specified by @@path as an [ArrayBuffer]. + /// + /// If @@blockAllReads is true, all changes to @@path will cause the program to block + /// when this function is called. + /// + /// If @@blockLoading is true, reading this property before the file has been loaded + /// will block, but changing @@path or calling @@reload() will return the old data + /// until the load completes. + /// + /// If neither is true, an empty buffer will be returned if no file is loaded, + /// otherwise it will behave as in the case above. + /// + /// > [!INFO] Due to technical limitations, @@data() could not be a property, + /// > however you can treat it like a property, it will trigger property updates + /// > as a property would, and the signal `dataChanged()` is present. + /// + /// [ArrayBuffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer + //@ Q_INVOKABLE QByteArray data(); + + /// Block all operations until the currently running load completes. + /// + /// > [!WARNING] See @@blockLoading for an explanation and warning about blocking. + Q_INVOKABLE bool blockUntilLoaded(); + /// Unload the loaded file and reload it, usually in response to changes. + /// + /// This will not block if @@blockLoading is set, only if @@blockAllReads is true. + /// It acts the same as changing @@path to a new file, except loading the same file. + Q_INVOKABLE void reload(); + + [[nodiscard]] QString path() const; + void setPath(const QString& path); + + [[nodiscard]] QByteArray data(); + [[nodiscard]] QString text(); + +signals: + ///! Fires if the file failed to load. A warning will be printed in the log. + void loadFailed(); + + void pathChanged(); + QSDOC_HIDE void internalTextChanged(); + QSDOC_HIDE void internalDataChanged(); + QSDOC_HIDE void textChanged(); + QSDOC_HIDE void dataChanged(); + void preloadChanged(); + void loadedOrAsyncChanged(); + void blockLoadingChanged(); + void blockAllReadsChanged(); + +private slots: + void readerFinished(); + +private: + void loadAsync(bool doStringConversion); + void cancelAsync(); + void loadSync(); + void updateState(FileViewState& newState); + void textConversion(); + void updatePath(); + + [[nodiscard]] bool shouldBlock() const; + + FileViewState state; + FileViewReader* reader = nullptr; + QString pathInFlight; + + QString targetPath; + bool mAsyncUpdate = true; + bool mWritable = false; + bool mCreate = false; + bool mPreload = true; + bool mPrepared = false; + bool mLoadedOrAsync = false; + bool mBlockLoading = false; + bool mBlockAllReads = false; + + GuardedEmitter<&FileView::internalTextChanged> textChangedEmitter; + GuardedEmitter<&FileView::internalDataChanged> dataChangedEmitter; + void emitDataChanged(); + + DECLARE_PRIVATE_MEMBER( + FileView, + isLoadedOrAsync, + setLoadedOrAsync, + mLoadedOrAsync, + loadedOrAsyncChanged + ); + +public: + DECLARE_MEMBER_WITH_GET(FileView, shouldPreload, mPreload, preloadChanged); + DECLARE_MEMBER_WITH_GET(FileView, blockLoading, mBlockLoading, blockLoadingChanged); + DECLARE_MEMBER_WITH_GET(FileView, blockAllReads, mBlockAllReads, blockAllReadsChanged); + + void setPreload(bool preload); + void setBlockLoading(bool blockLoading); + void setBlockAllReads(bool blockAllReads); +}; + +} // namespace qs::io diff --git a/src/io/module.md b/src/io/module.md index 676cff73..8af3799e 100644 --- a/src/io/module.md +++ b/src/io/module.md @@ -4,5 +4,6 @@ headers = [ "datastream.hpp", "socket.hpp", "process.hpp", + "fileview.hpp", ] ----- From 8cdb41317f371e78cff3dd7544d446c021a7c65f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 9 Sep 2024 03:23:27 -0700 Subject: [PATCH 154/305] nix: modernize cmake options --- default.nix | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/default.nix b/default.nix index a00bf012..3016a313 100644 --- a/default.nix +++ b/default.nix @@ -58,29 +58,28 @@ qt6.qtdeclarative cli11 ] - ++ (lib.optional withCrashReporter breakpad) - ++ (lib.optional withJemalloc jemalloc) - ++ (lib.optional withQtSvg qt6.qtsvg) - ++ (lib.optionals withWayland [ qt6.qtwayland wayland ]) - ++ (lib.optional withX11 xorg.libxcb) - ++ (lib.optional withPam pam) - ++ (lib.optional withPipewire pipewire); + ++ lib.optional withCrashReporter breakpad + ++ lib.optional withJemalloc jemalloc + ++ lib.optional withQtSvg qt6.qtsvg + ++ lib.optionals withWayland [ qt6.qtwayland wayland ] + ++ lib.optional withX11 xorg.libxcb + ++ lib.optional withPam pam + ++ lib.optional withPipewire pipewire; QTWAYLANDSCANNER = lib.optionalString withWayland "${qt6.qtwayland}/libexec/qtwaylandscanner"; cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; - cmakeFlags = [ "-DGIT_REVISION=${gitRev}" ] - ++ lib.optional (!withCrashReporter) "-DCRASH_REPORTER=OFF" - ++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF" - ++ lib.optional (!withWayland) "-DWAYLAND=OFF" - ++ lib.optional (!withPipewire) "-DSERVICE_PIPEWIRE=OFF" - ++ lib.optional (!withPam) "-DSERVICE_PAM=OFF" - ++ lib.optional (!withHyprland) "-DHYPRLAND=OFF" - ++ lib.optional (!withQMLLib) "-DINSTALL_QML_LIB=OFF"; - - buildPhase = "ninjaBuildPhase"; - enableParallelBuilding = true; + cmakeFlags = [ + (lib.cmakeFeature "GIT_REVISION" gitRev) + (lib.cmakeBool "CRASH_REPORTER" withCrashReporter) + (lib.cmakeBool "USE_JEMALLOC" withJemalloc) + (lib.cmakeBool "WAYLAND" withWayland) + (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) + (lib.cmakeBool "SERVICE_PAM" withPam) + (lib.cmakeBool "HYPRLAND" withHyprland) + (lib.cmakeBool "INSTALL_QML_LIB" withQMLLib) + ]; # How to get debuginfo in gdb from a release build: # 1. build `quickshell.debug` @@ -91,7 +90,7 @@ meta = with lib; { homepage = "https://git.outfoxxed.me/outfoxxed/quickshell"; - description = "Simple and flexbile QtQuick based desktop shell toolkit"; + description = "Flexbile QtQuick based desktop shell toolkit"; license = licenses.lgpl3Only; platforms = platforms.linux; }; From 2c485e415d29b74ce134e8736fb7fd63764d871c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 9 Sep 2024 03:27:58 -0700 Subject: [PATCH 155/305] nix: update lockfile to avoid mesa mismatches --- README.md | 3 +++ flake.lock | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0cc3d1b1..726bf22a 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ This repo has a nix flake you can use to install the package directly: quickshell = { url = "git+https://git.outfoxxed.me/outfoxxed/quickshell"; + + # THIS IS IMPORTANT + # Mismatched system dependencies will lead to crashes and other issues. inputs.nixpkgs.follows = "nixpkgs"; }; }; diff --git a/flake.lock b/flake.lock index 1527f635..8325718b 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1709237383, - "narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=", + "lastModified": 1725634671, + "narHash": "sha256-v3rIhsJBOMLR8e/RNWxr828tB+WywYIoajrZKFM+0Gg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8", + "rev": "574d1eac1c200690e27b8eb4e24887f8df7ac27c", "type": "github" }, "original": { From 19d74595d6b27b7b8922c7502c8ed05b474c2d7f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 10 Sep 2024 00:01:17 -0700 Subject: [PATCH 156/305] core/window: premultiply background colors Apparently these are supposed to be premultiplied. Some docs would be nice. --- src/core/proxywindow.cpp | 21 ++++++++++++++------- src/core/windowinterface.hpp | 14 -------------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index c4b72a04..5d4659dd 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -287,16 +287,23 @@ QuickshellScreenInfo* ProxyWindowBase::screen() const { return QuickshellTracked::instance()->screenInfo(qscreen); } -QColor ProxyWindowBase::color() const { - if (this->window == nullptr) return this->mColor; - else return this->window->color(); -} +QColor ProxyWindowBase::color() const { return this->mColor; } void ProxyWindowBase::setColor(QColor color) { + this->mColor = color; + if (this->window == nullptr) { - this->mColor = color; - emit this->colorChanged(); - } else this->window->setColor(color); + if (color != this->mColor) emit this->colorChanged(); + } else { + auto premultiplied = QColor::fromRgbF( + color.redF() * color.alphaF(), + color.greenF() * color.alphaF(), + color.blueF() * color.alphaF(), + color.alphaF() + ); + + this->window->setColor(premultiplied); + } } PendingRegion* ProxyWindowBase::mask() const { return this->mMask; } diff --git a/src/core/windowinterface.hpp b/src/core/windowinterface.hpp index 3970d9da..f90df24c 100644 --- a/src/core/windowinterface.hpp +++ b/src/core/windowinterface.hpp @@ -46,20 +46,6 @@ class WindowInterface: public Reloadable { /// along with map[To|From]Item (which is not reactive). Q_PROPERTY(QObject* windowTransform READ windowTransform NOTIFY windowTransformChanged); /// The background color of the window. Defaults to white. - /// - /// > [!WARNING] This seems to behave weirdly when using transparent colors on some systems. - /// > Using a colored content item over a transparent window is the recommended way to work around this: - /// > ```qml - /// > ProxyWindow { - /// > color: "transparent" - /// > Rectangle { - /// > anchors.fill: parent - /// > color: "#20ffffff" - /// > - /// > // your content here - /// > } - /// > } - /// > ``` Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged); /// The clickthrough mask. Defaults to null. /// From f810c63ffc8b42842e95511f3bff440917cd4840 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 10 Sep 2024 00:32:39 -0700 Subject: [PATCH 157/305] core/command: allow log files to be specified w/ instance selectors --- src/core/main.cpp | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/core/main.cpp b/src/core/main.cpp index 59942742..749f8a05 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -239,17 +239,23 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { } { - auto* sub = cli.add_subcommand("log", "Read quickshell logs."); - sub->add_option("--file", state.log.file, "Log file to read.")->required(); + auto* sub = cli.add_subcommand( + "log", + "Read quickshell logs.\n" + "If --file is specified, the given file will be read.\n" + "If not, the log of the first launched instance matching" + "the instance selection flags will be read." + ); + + auto* file = sub->add_option("--file", state.log.file, "Log file to read."); sub->add_option("-r,--rules", state.log.readoutRules, "Log file to read.") ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES."); + auto* instance = addInstanceSelection(sub)->excludes(file); + addConfigSelection(sub)->excludes(instance)->excludes(file); addLoggingOptions(sub, false); - // todo - // addConfigSelection(sub)->excludes(file); - state.subcommand.log = sub; } @@ -444,9 +450,19 @@ int selectInstance(CommandState& cmd, InstanceLockInfo* instance) { } int readLogFile(CommandState& cmd) { - auto file = QFile(*cmd.log.file); + auto path = *cmd.log.file; + + if (path.isEmpty()) { + InstanceLockInfo instance; + auto r = selectInstance(cmd, &instance); + if (r != 0) return r; + + path = QDir(QsPaths::basePath(instance.instance.instanceId)).filePath("log.qslog"); + } + + auto file = QFile(path); if (!file.open(QFile::ReadOnly)) { - qCCritical(logBare) << "Failed to open log file" << *cmd.log.file; + qCCritical(logBare) << "Failed to open log file" << path; return -1; } From c78381f6d00dabcf864e6c5343edbbbd0828902d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 10 Sep 2024 01:02:43 -0700 Subject: [PATCH 158/305] core/command: add --tail to log subcommand --- src/core/logging.cpp | 24 ++++++++++++++++++++---- src/core/logging.hpp | 2 +- src/core/main.cpp | 10 +++++++++- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 60d2c3c2..a6360fe5 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -28,6 +28,7 @@ #include "logging_p.hpp" #include "logging_qtprivate.cpp" // NOLINT #include "paths.hpp" +#include "ringbuf.hpp" Q_LOGGING_CATEGORY(logBare, "quickshell.bare"); @@ -65,6 +66,7 @@ void LogMessage::formatMessage( } if (msg.category == "quickshell.bare") { + if (!prefix.isEmpty()) stream << ' '; stream << msg.body; } else { if (color) { @@ -243,9 +245,9 @@ void LogManager::init( thread->start(); QMetaObject::invokeMethod( - &instance->threadProxy, - &LoggingThreadProxy::initInThread, - Qt::BlockingQueuedConnection + &instance->threadProxy, + &LoggingThreadProxy::initInThread, + Qt::BlockingQueuedConnection ); qCDebug(logLogging) << "Logger initialized."; @@ -735,7 +737,7 @@ bool EncodedLogReader::registerCategory() { return true; } -bool readEncodedLogs(QIODevice* device, bool timestamps, const QString& rulespec) { +bool readEncodedLogs(QIODevice* device, bool timestamps, int tail, const QString& rulespec) { QList rules; { @@ -767,6 +769,8 @@ bool readEncodedLogs(QIODevice* device, bool timestamps, const QString& rulespec auto filters = QHash(); + auto tailRing = RingBuffer(tail); + LogMessage message; auto stream = QTextStream(stdout); while (reader.read(&message)) { @@ -782,6 +786,18 @@ bool readEncodedLogs(QIODevice* device, bool timestamps, const QString& rulespec } if (filter.shouldDisplay(message.type)) { + if (tail == 0) { + LogMessage::formatMessage(stream, message, color, timestamps); + stream << '\n'; + } else { + tailRing.emplace(message); + } + } + } + + if (tail != 0) { + for (auto i = tailRing.size() - 1; i != -1; i--) { + auto& message = tailRing.at(i); LogMessage::formatMessage(stream, message, color, timestamps); stream << '\n'; } diff --git a/src/core/logging.hpp b/src/core/logging.hpp index bd3be771..131e840f 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -130,7 +130,7 @@ private: LoggingThreadProxy threadProxy; }; -bool readEncodedLogs(QIODevice* device, bool timestamps, const QString& rulespec); +bool readEncodedLogs(QIODevice* device, bool timestamps, int tail, const QString& rulespec); } // namespace qs::log diff --git a/src/core/main.cpp b/src/core/main.cpp index 749f8a05..2e3a43fe 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -94,6 +95,7 @@ struct CommandState { bool noColor = !qEnvironmentVariableIsEmpty("NO_COLOR"); bool sparse = false; size_t verbosity = 0; + int tail = 0; QStringOption rules; QStringOption readoutRules; QStringOption file; @@ -249,6 +251,10 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { auto* file = sub->add_option("--file", state.log.file, "Log file to read."); + sub->add_option("-t,--tail", state.log.tail) + ->description("Maximum number of lines to print, starting from the bottom.") + ->check(CLI::Range(1, std::numeric_limits::max(), "INT > 0")); + sub->add_option("-r,--rules", state.log.readoutRules, "Log file to read.") ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES."); @@ -466,7 +472,9 @@ int readLogFile(CommandState& cmd) { return -1; } - return qs::log::readEncodedLogs(&file, cmd.log.timestamp, *cmd.log.readoutRules) ? 0 : -1; + return qs::log::readEncodedLogs(&file, cmd.log.timestamp, cmd.log.tail, *cmd.log.readoutRules) + ? 0 + : -1; } int listInstances(CommandState& cmd) { From a82fbf40c2c00432952d865e8c3d29f0c8b983fc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 10 Sep 2024 03:31:49 -0700 Subject: [PATCH 159/305] core/command: add log --follow --- src/core/logging.cpp | 143 ++++++++++++++++++++++++++------- src/core/logging.hpp | 10 ++- src/core/logging_p.hpp | 66 +++++++++++++++ src/core/logging_qtprivate.cpp | 25 +----- src/core/logging_qtprivate.hpp | 44 ++++++++++ src/core/main.cpp | 25 ++++-- src/core/paths.cpp | 11 ++- src/core/paths.hpp | 1 + 8 files changed, 262 insertions(+), 63 deletions(-) create mode 100644 src/core/logging_qtprivate.hpp diff --git a/src/core/logging.cpp b/src/core/logging.cpp index a6360fe5..265b83b5 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -1,10 +1,14 @@ #include "logging.hpp" #include +#include #include +#include #include +#include #include #include +#include #include #include #include @@ -356,6 +360,18 @@ void ThreadLogging::initFs() { delete detailedFile; detailedFile = nullptr; } else { + auto lock = flock { + .l_type = F_WRLCK, + .l_whence = SEEK_SET, + .l_start = 0, + .l_len = 0, + }; + + if (fcntl(detailedFile->handle(), F_SETLK, &lock) != 0) { // NOLINT + qCWarning(logLogging) << "Unable to set lock marker on detailed log file. --follow from " + "other instances will not work."; + } + qCInfo(logLogging) << "Saving detailed logs to" << path; } @@ -737,22 +753,13 @@ bool EncodedLogReader::registerCategory() { return true; } -bool readEncodedLogs(QIODevice* device, bool timestamps, int tail, const QString& rulespec) { - QList rules; - - { - QLoggingSettingsParser parser; - parser.setContent(rulespec); - rules = parser.rules(); - } - - auto reader = EncodedLogReader(); - reader.setDevice(device); +bool LogReader::initialize() { + this->reader.setDevice(this->file); bool readable = false; quint8 logVersion = 0; quint8 readerVersion = 0; - if (!reader.readHeader(&readable, &logVersion, &readerVersion)) { + if (!this->reader.readHeader(&readable, &logVersion, &readerVersion)) { qCritical() << "Failed to read log header."; return false; } @@ -765,29 +772,33 @@ bool readEncodedLogs(QIODevice* device, bool timestamps, int tail, const QString return false; } + return true; +} + +bool LogReader::continueReading() { auto color = LogManager::instance()->colorLogs; - - auto filters = QHash(); - - auto tailRing = RingBuffer(tail); + auto tailRing = RingBuffer(this->remainingTail); LogMessage message; auto stream = QTextStream(stdout); - while (reader.read(&message)) { + auto readCursor = this->file->pos(); + while (this->reader.read(&message)) { + readCursor = this->file->pos(); + CategoryFilter filter; - if (filters.contains(message.readCategoryId)) { - filter = filters.value(message.readCategoryId); + if (this->filters.contains(message.readCategoryId)) { + filter = this->filters.value(message.readCategoryId); } else { - for (const auto& rule: rules) { + for (const auto& rule: this->rules) { filter.applyRule(message.category, rule); } - filters.insert(message.readCategoryId, filter); + this->filters.insert(message.readCategoryId, filter); } if (filter.shouldDisplay(message.type)) { - if (tail == 0) { - LogMessage::formatMessage(stream, message, color, timestamps); + if (this->remainingTail == 0) { + LogMessage::formatMessage(stream, message, color, this->timestamps); stream << '\n'; } else { tailRing.emplace(message); @@ -795,19 +806,97 @@ bool readEncodedLogs(QIODevice* device, bool timestamps, int tail, const QString } } - if (tail != 0) { + if (this->remainingTail != 0) { for (auto i = tailRing.size() - 1; i != -1; i--) { auto& message = tailRing.at(i); - LogMessage::formatMessage(stream, message, color, timestamps); + LogMessage::formatMessage(stream, message, color, this->timestamps); stream << '\n'; } } stream << Qt::flush; - if (!device->atEnd()) { + if (this->file->pos() != readCursor) { qCritical() << "An error occurred parsing the end of this log file."; - qCritical() << "Remaining data:" << device->readAll(); + qCritical() << "Remaining data:" << this->file->readAll(); + return false; + } + + return true; +} + +void LogFollower::FcntlWaitThread::run() { + auto lock = flock { + .l_type = F_RDLCK, // won't block other read locks when we take it + .l_whence = SEEK_SET, + .l_start = 0, + .l_len = 0, + }; + + auto r = fcntl(this->follower->reader->file->handle(), F_SETLKW, &lock); // NOLINT + + if (r != 0) { + qCWarning(logLogging).nospace() + << "Failed to wait for write locks to be removed from log file with error code " << errno + << ": " << qt_error_string(); + } +} + +bool LogFollower::follow() { + QObject::connect(&this->waitThread, &QThread::finished, this, &LogFollower::onFileLocked); + + QObject::connect( + &this->fileWatcher, + &QFileSystemWatcher::fileChanged, + this, + &LogFollower::onFileChanged + ); + + this->fileWatcher.addPath(this->path); + this->waitThread.start(); + + auto r = QCoreApplication::exec(); + return r == 0; +} + +void LogFollower::onFileChanged() { + if (!this->reader->continueReading()) { + QCoreApplication::exit(1); + } +} + +void LogFollower::onFileLocked() { + if (!this->reader->continueReading()) { + QCoreApplication::exit(1); + } else { + QCoreApplication::exit(0); + } +} + +bool readEncodedLogs( + QFile* file, + const QString& path, + bool timestamps, + int tail, + bool follow, + const QString& rulespec +) { + QList rules; + + { + QLoggingSettingsParser parser; + parser.setContent(rulespec); + rules = parser.rules(); + } + + auto reader = LogReader(file, timestamps, tail, rules); + + if (!reader.initialize()) return false; + if (!reader.continueReading()) return false; + + if (follow) { + auto follower = LogFollower(&reader, path); + return follower.follow(); } return true; diff --git a/src/core/logging.hpp b/src/core/logging.hpp index 131e840f..8b6ea618 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -130,7 +131,14 @@ private: LoggingThreadProxy threadProxy; }; -bool readEncodedLogs(QIODevice* device, bool timestamps, int tail, const QString& rulespec); +bool readEncodedLogs( + QFile* file, + const QString& path, + bool timestamps, + int tail, + bool follow, + const QString& rulespec +); } // namespace qs::log diff --git a/src/core/logging_p.hpp b/src/core/logging_p.hpp index cb1c3713..e2489922 100644 --- a/src/core/logging_p.hpp +++ b/src/core/logging_p.hpp @@ -1,12 +1,18 @@ #pragma once +#include + #include #include #include +#include #include +#include +#include #include #include #include "logging.hpp" +#include "logging_qtprivate.hpp" #include "ringbuf.hpp" namespace qs::log { @@ -120,4 +126,64 @@ private: EncodedLogWriter detailedWriter; }; +class LogFollower; + +class LogReader { +public: + explicit LogReader( + QFile* file, + bool timestamps, + int tail, + QList rules + ) + : file(file) + , timestamps(timestamps) + , remainingTail(tail) + , rules(std::move(rules)) {} + + bool initialize(); + bool continueReading(); + +private: + QFile* file; + EncodedLogReader reader; + bool timestamps; + int remainingTail; + QHash filters; + QList rules; + + friend class LogFollower; +}; + +class LogFollower: public QObject { + Q_OBJECT; + +public: + explicit LogFollower(LogReader* reader, QString path): reader(reader), path(std::move(path)) {} + + bool follow(); + +private slots: + void onFileChanged(); + void onFileLocked(); + +private: + LogReader* reader; + QString path; + QFileSystemWatcher fileWatcher; + + class FcntlWaitThread: public QThread { + public: + explicit FcntlWaitThread(LogFollower* follower): follower(follower) {} + + protected: + void run() override; + + private: + LogFollower* follower; + }; + + FcntlWaitThread waitThread {this}; +}; + } // namespace qs::log diff --git a/src/core/logging_qtprivate.cpp b/src/core/logging_qtprivate.cpp index 05393f02..5078eeb4 100644 --- a/src/core/logging_qtprivate.cpp +++ b/src/core/logging_qtprivate.cpp @@ -16,34 +16,13 @@ #include #include +#include "logging_qtprivate.hpp" + namespace qs::log { Q_DECLARE_LOGGING_CATEGORY(logLogging); namespace qt_logging_registry { -class QLoggingRule { -public: - QLoggingRule(); - QLoggingRule(QStringView pattern, bool enabled); - [[nodiscard]] int pass(QLatin1StringView categoryName, QtMsgType type) const; - - enum PatternFlag { - FullText = 0x1, - LeftFilter = 0x2, - RightFilter = 0x4, - MidFilter = LeftFilter | RightFilter - }; - Q_DECLARE_FLAGS(PatternFlags, PatternFlag) - - QString category; - int messageType; - PatternFlags flags; - bool enabled; - -private: - void parse(QStringView pattern); -}; - class QLoggingSettingsParser { public: void setContent(QStringView content); diff --git a/src/core/logging_qtprivate.hpp b/src/core/logging_qtprivate.hpp new file mode 100644 index 00000000..17563400 --- /dev/null +++ b/src/core/logging_qtprivate.hpp @@ -0,0 +1,44 @@ +#pragma once + +// The logging rule parser from qloggingregistry_p.h and qloggingregistry.cpp. + +// Was unable to properly link the functions when directly using the headers (which we depend +// on anyway), so below is a slightly stripped down copy. Making the originals link would +// be preferable. + +#include +#include +#include +#include + +namespace qs::log { +Q_DECLARE_LOGGING_CATEGORY(logLogging); + +namespace qt_logging_registry { + +class QLoggingRule { +public: + QLoggingRule(); + QLoggingRule(QStringView pattern, bool enabled); + [[nodiscard]] int pass(QLatin1StringView categoryName, QtMsgType type) const; + + enum PatternFlag { + FullText = 0x1, + LeftFilter = 0x2, + RightFilter = 0x4, + MidFilter = LeftFilter | RightFilter + }; + Q_DECLARE_FLAGS(PatternFlags, PatternFlag) + + QString category; + int messageType; + PatternFlags flags; + bool enabled; + +private: + void parse(QStringView pattern); +}; + +} // namespace qt_logging_registry + +} // namespace qs::log diff --git a/src/core/main.cpp b/src/core/main.cpp index 2e3a43fe..e1a6885b 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -96,6 +96,7 @@ struct CommandState { bool sparse = false; size_t verbosity = 0; int tail = 0; + bool follow = false; QStringOption rules; QStringOption readoutRules; QStringOption file; @@ -241,13 +242,10 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { } { - auto* sub = cli.add_subcommand( - "log", - "Read quickshell logs.\n" - "If --file is specified, the given file will be read.\n" - "If not, the log of the first launched instance matching" - "the instance selection flags will be read." - ); + auto* sub = cli.add_subcommand("log", "Read quickshell logs.\n") + ->description("If --file is specified, the given file will be read.\n" + "If not, the log of the first launched instance matching" + "the instance selection flags will be read."); auto* file = sub->add_option("--file", state.log.file, "Log file to read."); @@ -255,6 +253,9 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { ->description("Maximum number of lines to print, starting from the bottom.") ->check(CLI::Range(1, std::numeric_limits::max(), "INT > 0")); + sub->add_flag("--follow", state.log.follow) + ->description("Keep reading the log until the logging process terminates."); + sub->add_option("-r,--rules", state.log.readoutRules, "Log file to read.") ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES."); @@ -267,6 +268,7 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { { 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" @@ -472,7 +474,14 @@ int readLogFile(CommandState& cmd) { return -1; } - return qs::log::readEncodedLogs(&file, cmd.log.timestamp, cmd.log.tail, *cmd.log.readoutRules) + return qs::log::readEncodedLogs( + &file, + path, + cmd.log.timestamp, + cmd.log.tail, + cmd.log.follow, + *cmd.log.readoutRules + ) ? 0 : -1; } diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 7b7f91f1..e2b15307 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -36,11 +36,14 @@ QDir QsPaths::crashDir(const QString& id) { return dir; } +QString QsPaths::basePath(const QString& id) { + auto path = QsPaths::instance()->baseRunDir()->filePath("by-id"); + path = QDir(path).filePath(id); + return path; +} + QString QsPaths::ipcPath(const QString& id) { - auto ipcPath = QsPaths::instance()->baseRunDir()->filePath("by-id"); - ipcPath = QDir(ipcPath).filePath(id); - ipcPath = QDir(ipcPath).filePath("ipc.sock"); - return ipcPath; + return QDir(QsPaths::basePath(id)).filePath("ipc.sock"); } QDir* QsPaths::cacheDir() { diff --git a/src/core/paths.hpp b/src/core/paths.hpp index 62858bdb..b3042bac 100644 --- a/src/core/paths.hpp +++ b/src/core/paths.hpp @@ -17,6 +17,7 @@ public: static QsPaths* instance(); static void init(QString shellId, QString pathId); static QDir crashDir(const QString& id); + static QString basePath(const QString& id); static QString ipcPath(const QString& id); static bool checkLock(const QString& path, InstanceLockInfo* info = nullptr); static QVector collectInstances(const QString& path); From 01deefe241aac1090b69c180e1ca40d5876e8961 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 10 Sep 2024 04:48:54 -0700 Subject: [PATCH 160/305] core/log: encode category log levels --- src/core/logging.cpp | 41 ++++++++++++++++++++++++++++++++++------- src/core/logging.hpp | 3 +++ src/core/logging_p.hpp | 3 ++- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 265b83b5..91408264 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -200,16 +201,15 @@ void LogManager::filterCategory(QLoggingCategory* category) { if (isQs && !instance->sparse) { // We assume the category name pointer will always be the same and be comparable in the message handler. - LogManager::instance()->sparseFilters.insert( - static_cast(category->categoryName()), - filter - ); + instance->sparseFilters.insert(static_cast(category->categoryName()), filter); // all enabled by default CategoryFilter().apply(category); } else { filter.apply(category); } + + instance->allFilters.insert(categoryName, filter); } LogManager* LogManager::instance() { @@ -269,6 +269,10 @@ QString LogManager::rulesString() const { return this->mRulesString; } QtMsgType LogManager::defaultLevel() const { return this->mDefaultLevel; } bool LogManager::isSparse() const { return this->sparse; } +CategoryFilter LogManager::getFilter(QLatin1StringView category) { + return this->allFilters.value(category); +} + void LoggingThreadProxy::initInThread() { this->logging = new ThreadLogging(this); this->logging->init(); @@ -527,7 +531,7 @@ bool DeviceReader::readU64(quint64* data) { void EncodedLogWriter::setDevice(QIODevice* target) { this->buffer.setDevice(target); } void EncodedLogReader::setDevice(QIODevice* source) { this->reader.setDevice(source); } -constexpr quint8 LOG_VERSION = 1; +constexpr quint8 LOG_VERSION = 2; bool EncodedLogWriter::writeHeader() { this->buffer.writeU8(LOG_VERSION); @@ -673,7 +677,7 @@ start: QByteArray body; if (!this->readString(&body)) return false; - *slot = LogMessage(msgType, QLatin1StringView(category), body, this->lastMessageTime); + *slot = LogMessage(msgType, QLatin1StringView(category.first), body, this->lastMessageTime); slot->readCategoryId = categoryId; } @@ -681,6 +685,10 @@ start: return true; } +CategoryFilter EncodedLogReader::categoryFilterById(quint16 id) { + return this->categories.value(id).second; +} + void EncodedLogWriter::writeOp(EncodedLogOpcode opcode) { this->buffer.writeU8(opcode); } void EncodedLogWriter::writeVarInt(quint32 n) { @@ -742,14 +750,31 @@ quint16 EncodedLogWriter::getOrCreateCategory(QLatin1StringView category) { auto id = this->nextCategory++; this->categories.insert(category, id); + auto filter = LogManager::instance()->getFilter(category); + quint8 flags = 0; + flags |= filter.debug << 0; + flags |= filter.info << 1; + flags |= filter.warn << 2; + flags |= filter.critical << 3; + + this->buffer.writeU8(flags); return id; } } bool EncodedLogReader::registerCategory() { QByteArray name; + quint8 flags = 0; if (!this->readString(&name)) return false; - this->categories.append(name); + if (!this->reader.readU8(&flags)) return false; + + CategoryFilter filter; + filter.debug = (flags >> 0) & 1; + filter.info = (flags >> 1) & 1; + filter.warn = (flags >> 2) & 1; + filter.critical = (flags >> 3) & 1; + + this->categories.append(qMakePair(name, filter)); return true; } @@ -789,6 +814,8 @@ bool LogReader::continueReading() { if (this->filters.contains(message.readCategoryId)) { filter = this->filters.value(message.readCategoryId); } else { + filter = this->reader.categoryFilterById(message.readCategoryId); + for (const auto& rule: this->rules) { filter.applyRule(message.category, rule); } diff --git a/src/core/logging.hpp b/src/core/logging.hpp index 8b6ea618..7ff1b5e0 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -110,6 +110,8 @@ public: [[nodiscard]] QtMsgType defaultLevel() const; [[nodiscard]] bool isSparse() const; + [[nodiscard]] CategoryFilter getFilter(QLatin1StringView category); + signals: void logMessage(LogMessage msg, bool showInSparse); @@ -126,6 +128,7 @@ private: QList* rules = nullptr; QtMsgType mDefaultLevel = QtWarningMsg; QHash sparseFilters; + QHash allFilters; QTextStream stdoutStream; LoggingThreadProxy threadProxy; diff --git a/src/core/logging_p.hpp b/src/core/logging_p.hpp index e2489922..3297ea1b 100644 --- a/src/core/logging_p.hpp +++ b/src/core/logging_p.hpp @@ -94,6 +94,7 @@ public: [[nodiscard]] bool readHeader(bool* success, quint8* logVersion, quint8* readerVersion); // WARNING: log messages written to the given slot are invalidated when the log reader is destroyed. [[nodiscard]] bool read(LogMessage* slot); + [[nodiscard]] CategoryFilter categoryFilterById(quint16 id); private: [[nodiscard]] bool readVarInt(quint32* slot); @@ -101,7 +102,7 @@ private: [[nodiscard]] bool registerCategory(); DeviceReader reader; - QVector categories; + QVector> categories; QDateTime lastMessageTime = QDateTime::fromSecsSinceEpoch(0); RingBuffer recentMessages {256}; }; From 47ec85ffef1d31a4cb1974b0a4952d0872ce59d2 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 10 Sep 2024 04:55:44 -0700 Subject: [PATCH 161/305] core/command: make log --file positional Also frees up -f for --follow. --- src/core/main.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/main.cpp b/src/core/main.cpp index e1a6885b..eb21fb03 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -243,17 +243,17 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { { auto* sub = cli.add_subcommand("log", "Read quickshell logs.\n") - ->description("If --file is specified, the given file will be read.\n" + ->description("If file is specified, the given file will be read.\n" "If not, the log of the first launched instance matching" "the instance selection flags will be read."); - auto* file = sub->add_option("--file", state.log.file, "Log file to read."); + auto* file = sub->add_option("file", state.log.file, "Log file to read."); sub->add_option("-t,--tail", state.log.tail) ->description("Maximum number of lines to print, starting from the bottom.") ->check(CLI::Range(1, std::numeric_limits::max(), "INT > 0")); - sub->add_flag("--follow", state.log.follow) + sub->add_flag("-f,--follow", state.log.follow) ->description("Keep reading the log until the logging process terminates."); sub->add_option("-r,--rules", state.log.readoutRules, "Log file to read.") From 9d21a01153bf714fbc01921c6ff021fab6982939 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 10 Sep 2024 14:35:30 -0700 Subject: [PATCH 162/305] core/command: add --no-duplicate --- src/core/main.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/core/main.cpp b/src/core/main.cpp index eb21fb03..7675a03d 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -132,6 +132,7 @@ struct CommandState { struct { bool printVersion = false; bool killAll = false; + bool noDuplicate = false; } misc; }; @@ -239,13 +240,13 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { { cli.add_flag("-V,--version", state.misc.printVersion) ->description("Print quickshell's version and exit."); + + cli.add_flag("--no-duplicate", state.misc.noDuplicate) + ->description("Exit immediately if another instance of the given config is running."); } { - auto* sub = cli.add_subcommand("log", "Read quickshell logs.\n") - ->description("If file is specified, the given file will be read.\n" - "If not, the log of the first launched instance matching" - "the instance selection flags will be read."); + auto* sub = cli.add_subcommand("log", "Print quickshell logs."); auto* file = sub->add_option("file", state.log.file, "Log file to read."); @@ -644,6 +645,14 @@ int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication) { auto r = locateConfigFile(cmd, configPath); if (r != 0) return r; + { + InstanceLockInfo info; + if (cmd.misc.noDuplicate && selectInstance(cmd, &info) == 0) { + qCDebug(logBare) << "An instance of this configuration is already running."; + return 0; + } + } + return launch( { .configPath = configPath, From 01f6331cb71b597db3d78e9fdbaa0f92052e13b1 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 10 Sep 2024 15:42:19 -0700 Subject: [PATCH 163/305] core/command: add --daemonize --- src/core/main.cpp | 81 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/src/core/main.cpp b/src/core/main.cpp index 7675a03d..5353a480 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -1,5 +1,7 @@ #include "main.hpp" #include +#include +#include #include #include #include @@ -8,6 +10,7 @@ #include #include // NOLINT: Need to include this for impls of some CLI11 classes #include +#include #include #include #include @@ -55,6 +58,34 @@ using qs::ipc::IpcClient; void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication); int runCommand(int argc, char** argv, QCoreApplication* coreApplication); +int DAEMON_PIPE = -1; // NOLINT +void exitDaemon(int code) { + if (DAEMON_PIPE == -1) return; + + if (write(DAEMON_PIPE, &code, sizeof(int)) == -1) { + qCritical().nospace() << "Failed to write daemon exit command with error code " << errno << ": " + << qt_error_string(); + } + + close(DAEMON_PIPE); + + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + if (open("/dev/null", O_RDONLY) != STDIN_FILENO) { // NOLINT + qFatal() << "Failed to open /dev/null on stdin"; + } + + if (open("/dev/null", O_WRONLY) != STDOUT_FILENO) { // NOLINT + qFatal() << "Failed to open /dev/null on stdout"; + } + + if (open("/dev/null", O_WRONLY) != STDERR_FILENO) { // NOLINT + qFatal() << "Failed to open /dev/null on stderr"; + } +} + int main(int argc, char** argv) { QCoreApplication::setApplicationName("quickshell"); @@ -66,7 +97,10 @@ int main(int argc, char** argv) { auto* coreApplication = new QCoreApplication(qArgC, argv); checkCrashRelaunch(argv, coreApplication); - return runCommand(argc, argv, coreApplication); + auto code = runCommand(argc, argv, coreApplication); + + exitDaemon(code); + return code; } class QStringOption { @@ -133,6 +167,7 @@ struct CommandState { bool printVersion = false; bool killAll = false; bool noDuplicate = false; + bool daemonize = false; } misc; }; @@ -241,8 +276,11 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { cli.add_flag("-V,--version", state.misc.printVersion) ->description("Print quickshell's version and exit."); - cli.add_flag("--no-duplicate", state.misc.noDuplicate) + cli.add_flag("-n,--no-duplicate", state.misc.noDuplicate) ->description("Exit immediately if another instance of the given config is running."); + + cli.add_flag("-d,--daemonize", state.misc.daemonize) + ->description("Detach from the controlling terminal."); } { @@ -295,6 +333,39 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { CLI11_PARSE(cli, argc, argv); + // Has to happen before extra threads are spawned. + if (state.misc.daemonize) { + auto closepipes = std::array(); + if (pipe(closepipes.data()) == -1) { + qFatal().nospace() << "Failed to create messaging pipes for daemon with error " << errno + << ": " << qt_error_string(); + } + + DAEMON_PIPE = closepipes[1]; + + pid_t pid = fork(); // NOLINT (include) + + if (pid == -1) { + qFatal().nospace() << "Failed to fork daemon with error " << errno << ": " + << qt_error_string(); + } else if (pid == 0) { + close(closepipes[0]); + + if (setsid() == -1) { + qFatal().nospace() << "Failed to setsid with error " << errno << ": " << qt_error_string(); + } + } else { + close(closepipes[1]); + + int ret = 0; + if (read(closepipes[0], &ret, sizeof(int)) == -1) { + qFatal() << "Failed to wait for daemon launch (it may have crashed)"; + } + + return ret; + } + } + { auto level = state.log.verbosity == 0 ? QtWarningMsg : state.log.verbosity == 1 ? QtInfoMsg @@ -489,7 +560,7 @@ int readLogFile(CommandState& cmd) { int listInstances(CommandState& cmd) { auto* basePath = QsPaths::instance()->baseRunDir(); - if (!basePath) exit(-1); // NOLINT + if (!basePath) return -1; // NOLINT QString path; QString configFilePath; @@ -648,7 +719,7 @@ int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication) { { InstanceLockInfo info; if (cmd.misc.noDuplicate && selectInstance(cmd, &info) == 0) { - qCDebug(logBare) << "An instance of this configuration is already running."; + qCInfo(logBare) << "An instance of this configuration is already running."; return 0; } } @@ -834,6 +905,8 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio auto root = RootWrapper(args.configPath, shellId); QGuiApplication::setQuitOnLastWindowClosed(false); + exitDaemon(0); + auto code = QGuiApplication::exec(); delete app; return code; From 36908129196d0a40eb6c4add997a9985f5473343 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 10 Sep 2024 16:30:50 -0700 Subject: [PATCH 164/305] core/log: fix encoding 29 second deltas (again) Forgot the second if statement and didn't actually fix the bug last time. --- src/core/logging.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 91408264..1564e895 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -594,7 +594,7 @@ bool EncodedLogWriter::write(const LogMessage& message) { this->buffer.writeU8(field); - if (secondDelta > 29) { + if (secondDelta >= 0x1d) { if (secondDelta > 0xffff) { writeFullTimestamp(); } else { From 5e2fb1455146792e42539d9147ce51d8100c2a4b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 13 Sep 2024 02:44:33 -0700 Subject: [PATCH 165/305] io/ipchandler: add IpcHandler and qs msg Also reworks the whole ipc system to use serialized variants. --- src/core/generation.cpp | 16 ++ src/core/generation.hpp | 13 ++ src/core/ipc.cpp | 38 +++-- src/core/ipc.hpp | 168 +++++++++++++++++++-- src/core/ipccommand.hpp | 20 +++ src/core/main.cpp | 71 +++++++++ src/io/CMakeLists.txt | 3 + src/io/ipc.cpp | 180 ++++++++++++++++++++++ src/io/ipc.hpp | 139 +++++++++++++++++ src/io/ipccomm.cpp | 236 +++++++++++++++++++++++++++++ src/io/ipccomm.hpp | 39 +++++ src/io/ipchandler.cpp | 324 ++++++++++++++++++++++++++++++++++++++++ src/io/ipchandler.hpp | 207 +++++++++++++++++++++++++ src/io/module.md | 1 + 14 files changed, 1428 insertions(+), 27 deletions(-) create mode 100644 src/core/ipccommand.hpp create mode 100644 src/io/ipc.cpp create mode 100644 src/io/ipc.hpp create mode 100644 src/io/ipccomm.cpp create mode 100644 src/io/ipccomm.hpp create mode 100644 src/io/ipchandler.cpp create mode 100644 src/io/ipchandler.hpp diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 5f21a19a..32018d67 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -67,6 +67,10 @@ void EngineGeneration::destroy() { this->watcher = nullptr; } + for (auto* extension: this->extensions.values()) { + delete extension; + } + if (this->root != nullptr) { QObject::connect(this->root, &QObject::destroyed, this, [this]() { // prevent further js execution between garbage collection and engine destruction. @@ -285,6 +289,18 @@ void EngineGeneration::incubationControllerDestroyed() { } } +void EngineGeneration::registerExtension(const void* key, EngineGenerationExt* extension) { + if (this->extensions.contains(key)) { + delete this->extensions.value(key); + } + + this->extensions.insert(key, extension); +} + +EngineGenerationExt* EngineGeneration::findExtension(const void* key) { + return this->extensions.value(key); +} + void EngineGeneration::quit() { this->shouldTerminate = true; this->destroy(); diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 54863752..823ca82a 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,13 @@ class RootWrapper; class QuickshellGlobal; +class EngineGenerationExt { +public: + EngineGenerationExt() = default; + virtual ~EngineGenerationExt() = default; + Q_DISABLE_COPY_MOVE(EngineGenerationExt); +}; + class EngineGeneration: public QObject { Q_OBJECT; @@ -35,6 +43,10 @@ public: void registerIncubationController(QQmlIncubationController* controller); void deregisterIncubationController(QQmlIncubationController* controller); + // takes ownership + void registerExtension(const void* key, EngineGenerationExt* extension); + EngineGenerationExt* findExtension(const void* key); + static EngineGeneration* findEngineGeneration(QQmlEngine* engine); static EngineGeneration* findObjectGeneration(QObject* object); @@ -78,6 +90,7 @@ private: void postReload(); void assignIncubationController(); QVector> incubationControllers; + QHash extensions; bool destroying = false; bool shouldTerminate = false; diff --git a/src/core/ipc.cpp b/src/core/ipc.cpp index ca14834a..dd2cd1e8 100644 --- a/src/core/ipc.cpp +++ b/src/core/ipc.cpp @@ -1,6 +1,8 @@ #include "ipc.hpp" #include +#include +#include #include #include #include @@ -8,6 +10,7 @@ #include #include "generation.hpp" +#include "ipccommand.hpp" #include "paths.hpp" namespace qs::ipc { @@ -62,20 +65,21 @@ void IpcServerConnection::onReadyRead() { this->stream.startTransaction(); this->stream.startTransaction(); - auto command = IpcCommand::Unknown; + IpcCommand command; this->stream >> command; if (!this->stream.commitTransaction()) return; - switch (command) { - case IpcCommand::Kill: - qInfo() << "Exiting due to IPC request."; - EngineGeneration::currentGeneration()->quit(); - break; - default: - qCCritical(logIpc) << "Received invalid IPC command from" << this; - this->socket->disconnectFromServer(); - break; - } + std::visit( + [this](Command& command) { + if constexpr (std::is_same_v) { + qCCritical(logIpc) << "Received invalid IPC command from" << this; + this->socket->disconnectFromServer(); + } else { + command.exec(this); + } + }, + command + ); if (!this->stream.commitTransaction()) return; } @@ -94,11 +98,7 @@ bool IpcClient::isConnected() const { return this->socket.isValid(); } void IpcClient::waitForConnected() { this->socket.waitForConnected(); } void IpcClient::waitForDisconnected() { this->socket.waitForDisconnected(); } -void IpcClient::kill() { - qCDebug(logIpc) << "Sending kill command..."; - this->stream << IpcCommand::Kill; - this->socket.flush(); -} +void IpcClient::kill() { this->sendMessage(IpcCommand(IpcKillCommand())); } void IpcClient::onError(QLocalSocket::LocalSocketError error) { qCCritical(logIpc) << "Socket Error" << error; @@ -116,4 +116,10 @@ int IpcClient::connect(const QString& id, const std::functionquit(); +} + } // namespace qs::ipc diff --git a/src/core/ipc.hpp b/src/core/ipc.hpp index 9738f4b9..77bff913 100644 --- a/src/core/ipc.hpp +++ b/src/core/ipc.hpp @@ -1,17 +1,133 @@ #pragma once +#include #include +#include +#include +#include +#include #include #include +#include +#include #include #include +#include + +template +constexpr void assertSerializable() { + // monostate being zero ensures transactional reads wont break + static_assert( + std::is_same_v>, std::monostate>, + "Serialization of variants without std::monostate at index 0 is disallowed." + ); + + static_assert( + sizeof...(Types) <= std::numeric_limits::max(), + "Serialization of variants that can't fit the tag in a uint8 is disallowed." + ); +} + +template +QDataStream& operator<<(QDataStream& stream, const std::variant& variant) { + assertSerializable(); + + if (variant.valueless_by_exception()) { + stream << static_cast(0); // must be monostate + } else { + stream << static_cast(variant.index()); + std::visit([&](const T& value) { stream << value; }, variant); + } + + return stream; +} + +template +constexpr bool forEachTypeIndex(const auto& f) { + return [&](std::index_sequence) { + return (f(std::in_place_index_t()) || ...); + }(std::index_sequence_for()); +} + +template +std::variant createIndexedOrMonostate(size_t index, std::variant& variant) { + assertSerializable(); + + const auto initialized = + forEachTypeIndex([index, &variant](std::in_place_index_t) { + if (index == Index) { + variant.template emplace(); + return true; + } else { + return false; + } + }); + + if (!initialized) { + variant = std::monostate(); + } + + return variant; +} + +template +QDataStream& operator>>(QDataStream& stream, std::variant& variant) { + assertSerializable(); + + quint8 index = 0; + stream >> index; + + createIndexedOrMonostate(index, variant); + std::visit([&](T& value) { stream >> value; }, variant); + + return stream; +} + +template +QDataStream& streamInValues(QDataStream& stream, const Types&... types) { + return (stream << ... << types); +} + +template +QDataStream& streamOutValues(QDataStream& stream, Types&... types) { + return (stream >> ... >> types); +} + +// NOLINTBEGIN +#define DEFINE_SIMPLE_DATASTREAM_OPS(Type, ...) \ + inline QDataStream& operator<<(QDataStream& stream, const Type& __VA_OPT__(data)) { \ + return streamInValues(stream __VA_OPT__(, __VA_ARGS__)); \ + } \ + \ + inline QDataStream& operator>>(QDataStream& stream, Type& __VA_OPT__(data)) { \ + return streamOutValues(stream __VA_OPT__(, __VA_ARGS__)); \ + } +// NOLINTEND + +DEFINE_SIMPLE_DATASTREAM_OPS(std::monostate); namespace qs::ipc { -enum class IpcCommand : quint8 { - Unknown = 0, - Kill, +Q_DECLARE_LOGGING_CATEGORY(logIpc); + +template +class MessageStream { +public: + explicit MessageStream(QDataStream* stream, QLocalSocket* socket) + : stream(stream) + , socket(socket) {} + + template + MessageStream& operator<<(V value) { + *this->stream << T(value); + this->socket->flush(); + return *this; + } + +private: + QDataStream* stream; + QLocalSocket* socket; }; class IpcServer: public QObject { @@ -35,13 +151,24 @@ class IpcServerConnection: public QObject { public: explicit IpcServerConnection(QLocalSocket* socket, IpcServer* server); + template + void respond(const T& message) { + this->stream << message; + this->socket->flush(); + } + + template + MessageStream responseStream() { + return MessageStream(&this->stream, this->socket); + } + + // public for access by nonlocal handlers + QLocalSocket* socket; + QDataStream stream; + private slots: void onDisconnected(); void onReadyRead(); - -private: - QLocalSocket* socket; - QDataStream stream; }; class IpcClient: public QObject { @@ -56,19 +183,38 @@ public: void kill(); + template + void sendMessage(const T& message) { + this->stream << message; + this->socket.flush(); + } + + template + bool waitForResponse(T& slot) { + while (this->socket.waitForReadyRead(-1)) { + this->stream.startTransaction(); + this->stream >> slot; + if (!this->stream.commitTransaction()) continue; + return true; + } + + qCCritical(logIpc) << "Error occurred while waiting for response."; + return false; + } + [[nodiscard]] static int connect(const QString& id, const std::function& callback); + // public for access by nonlocal handlers + QLocalSocket socket; + QDataStream stream; + signals: void connected(); void disconnected(); private slots: static void onError(QLocalSocket::LocalSocketError error); - -private: - QLocalSocket socket; - QDataStream stream; }; } // namespace qs::ipc diff --git a/src/core/ipccommand.hpp b/src/core/ipccommand.hpp new file mode 100644 index 00000000..c2e5059f --- /dev/null +++ b/src/core/ipccommand.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include "../io/ipccomm.hpp" +#include "ipc.hpp" + +namespace qs::ipc { + +struct IpcKillCommand: std::monostate { + static void exec(IpcServerConnection* /*unused*/); +}; + +using IpcCommand = std::variant< + std::monostate, + IpcKillCommand, + qs::io::ipc::comm::QueryMetadataCommand, + qs::io::ipc::comm::StringCallCommand>; + +} // namespace qs::ipc diff --git a/src/core/main.cpp b/src/core/main.cpp index 5353a480..8549deb4 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include // NOLINT: Need to include this for impls of some CLI11 classes @@ -37,6 +38,7 @@ #include #include +#include "../io/ipccomm.hpp" #include "build.hpp" #include "common.hpp" #include "instanceinfo.hpp" @@ -157,10 +159,18 @@ struct CommandState { bool json = false; } output; + struct { + bool info = false; + QStringOption target; + QStringOption function; + std::vector arguments; + } ipc; + struct { CLI::App* log = nullptr; CLI::App* list = nullptr; CLI::App* kill = nullptr; + CLI::App* msg = nullptr; } subcommand; struct { @@ -174,6 +184,7 @@ struct CommandState { int readLogFile(CommandState& cmd); int listInstances(CommandState& cmd); int killInstances(CommandState& cmd); +int msgInstance(CommandState& cmd); int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication); struct LaunchArgs { @@ -268,6 +279,10 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { }; auto cli = CLI::App(); + + // Require 0-1 subcommands. Without this, positionals can be parsed as more subcommands. + cli.require_subcommand(0, 1); + addConfigSelection(&cli); addLoggingOptions(&cli, false); addDebugOptions(&cli); @@ -331,6 +346,34 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { state.subcommand.kill = sub; } + { + auto* sub = cli.add_subcommand("msg", "Send messages to IpcHandlers.")->require_option(); + + auto* target = sub->add_option("target", state.ipc.target, "The target to message."); + + auto* function = sub->add_option("function", state.ipc.function) + ->description("The function to call in the target.") + ->needs(target); + + auto* arguments = sub->add_option("arguments", state.ipc.arguments) + ->description("Arguments to the called function.") + ->needs(function) + ->allow_extra_args(); + + sub->add_flag("-i,--info", state.ipc.info) + ->description("Print information about a function or target if given, or all available " + "targets if not.") + ->excludes(arguments); + + auto* instance = addInstanceSelection(sub); + addConfigSelection(sub)->excludes(instance); + addLoggingOptions(sub, false, true); + + sub->require_option(); + + state.subcommand.msg = sub; + } + CLI11_PARSE(cli, argc, argv); // Has to happen before extra threads are spawned. @@ -389,6 +432,8 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { return listInstances(state); } else if (*state.subcommand.kill) { return killInstances(state); + } else if (*state.subcommand.msg) { + return msgInstance(state); } else { return launchFromCommand(state, coreApplication); } @@ -647,6 +692,32 @@ int killInstances(CommandState& cmd) { }); } +int msgInstance(CommandState& cmd) { + InstanceLockInfo instance; + auto r = selectInstance(cmd, &instance); + if (r != 0) return r; + + return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { + if (cmd.ipc.info) { + return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.function); + } else { + QVector arguments; + for (auto& arg: cmd.ipc.arguments) { + arguments += *arg; + } + + return qs::io::ipc::comm::callFunction( + &client, + *cmd.ipc.target, + *cmd.ipc.function, + arguments + ); + } + + return -1; + }); +} + template QString base36Encode(T number) { const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz"; diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index 7113cd7d..389b8a6f 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -2,6 +2,9 @@ qt_add_library(quickshell-io STATIC datastream.cpp process.cpp fileview.cpp + ipccomm.cpp + ipc.cpp + ipchandler.cpp ) add_library(quickshell-io-init OBJECT init.cpp) diff --git a/src/io/ipc.cpp b/src/io/ipc.cpp new file mode 100644 index 00000000..37a37eb3 --- /dev/null +++ b/src/io/ipc.cpp @@ -0,0 +1,180 @@ +#include "ipc.hpp" +#include + +#include +#include +#include + +namespace qs::io::ipc { + +const VoidIpcType VoidIpcType::INSTANCE {}; +const StringIpcType StringIpcType::INSTANCE {}; +const IntIpcType IntIpcType::INSTANCE {}; +const BoolIpcType BoolIpcType::INSTANCE {}; +const DoubleIpcType DoubleIpcType::INSTANCE {}; +const ColorIpcType ColorIpcType::INSTANCE {}; + +const IpcType* IpcType::ipcType(const QMetaType& metaType) { + if (metaType.id() == QMetaType::Void) return &VoidIpcType::INSTANCE; + if (metaType.id() == QMetaType::QString) return &StringIpcType::INSTANCE; + if (metaType.id() == QMetaType::Int) return &IntIpcType::INSTANCE; + if (metaType.id() == QMetaType::Bool) return &BoolIpcType::INSTANCE; + if (metaType.id() == QMetaType::Double) return &DoubleIpcType::INSTANCE; + if (metaType.id() == QMetaType::QColor) return &ColorIpcType::INSTANCE; + return nullptr; +} + +IpcTypeSlot::IpcTypeSlot(IpcTypeSlot&& other) noexcept { *this = std::move(other); } + +IpcTypeSlot& IpcTypeSlot::operator=(IpcTypeSlot&& other) noexcept { + this->mType = other.mType; + this->storage = other.storage; + other.mType = nullptr; + other.storage = nullptr; + return *this; +} + +IpcTypeSlot::~IpcTypeSlot() { this->replace(nullptr); } + +const IpcType* IpcTypeSlot::type() const { return this->mType; } + +void* IpcTypeSlot::get() { + if (this->storage == nullptr) { + this->storage = this->mType->createStorage(); + } + + return this->storage; +} + +QGenericArgument IpcTypeSlot::asGenericArgument() { + if (this->mType) { + return QGenericArgument(this->mType->genericArgumentName(), this->get()); + } else { + return QGenericArgument(); + } +} + +QGenericReturnArgument IpcTypeSlot::asGenericReturnArgument() { + if (this->mType) { + return QGenericReturnArgument(this->mType->genericArgumentName(), this->get()); + } else { + return QGenericReturnArgument(); + } +} + +void IpcTypeSlot::replace(void* value) { + if (this->storage != nullptr) { + this->mType->destroyStorage(this->storage); + } + + this->storage = value; +} + +const char* VoidIpcType::name() const { return "void"; } +const char* VoidIpcType::genericArgumentName() const { return "void"; } + +// string +const char* StringIpcType::name() const { return "string"; } +const char* StringIpcType::genericArgumentName() const { return "QString"; } +void* StringIpcType::fromString(const QString& string) const { return new QString(string); } +QString StringIpcType::toString(void* slot) const { return *static_cast(slot); } +void* StringIpcType::createStorage() const { return new QString(); } +void StringIpcType::destroyStorage(void* slot) const { delete static_cast(slot); } + +// int +const char* IntIpcType::name() const { return "int"; } +const char* IntIpcType::genericArgumentName() const { return "int"; } + +void* IntIpcType::fromString(const QString& string) const { + auto ok = false; + auto v = string.toInt(&ok); + + return ok ? new int(v) : nullptr; +} + +QString IntIpcType::toString(void* slot) const { return QString::number(*static_cast(slot)); } + +void* IntIpcType::createStorage() const { return new int(); } +void IntIpcType::destroyStorage(void* slot) const { delete static_cast(slot); } + +// bool +const char* BoolIpcType::name() const { return "bool"; } +const char* BoolIpcType::genericArgumentName() const { return "bool"; } + +void* BoolIpcType::fromString(const QString& string) const { + if (string == "true") return new bool(true); + if (string == "false") return new bool(false); + + auto isInt = false; + auto iv = string.toInt(&isInt); + + return isInt ? new bool(iv != 0) : nullptr; +} + +QString BoolIpcType::toString(void* slot) const { + return *static_cast(slot) ? "true" : "false"; +} + +void* BoolIpcType::createStorage() const { return new bool(); } +void BoolIpcType::destroyStorage(void* slot) const { delete static_cast(slot); } + +// double +const char* DoubleIpcType::name() const { return "real"; } +const char* DoubleIpcType::genericArgumentName() const { return "double"; } + +void* DoubleIpcType::fromString(const QString& string) const { + auto ok = false; + auto v = string.toDouble(&ok); + + return ok ? new double(v) : nullptr; +} + +QString DoubleIpcType::toString(void* slot) const { + return QString::number(*static_cast(slot)); +} + +void* DoubleIpcType::createStorage() const { return new double(); } +void DoubleIpcType::destroyStorage(void* slot) const { delete static_cast(slot); } + +// color +const char* ColorIpcType::name() const { return "color"; } +const char* ColorIpcType::genericArgumentName() const { return "QColor"; } + +void* ColorIpcType::fromString(const QString& string) const { + auto color = QColor::fromString(string); + + if (!color.isValid() && !string.startsWith('#')) { + color = QColor::fromString('#' % string); + } + + return color.isValid() ? new QColor(color) : nullptr; +} + +QString ColorIpcType::toString(void* slot) const { + return static_cast(slot)->name(QColor::HexArgb); +} + +void* ColorIpcType::createStorage() const { return new bool(); } +void ColorIpcType::destroyStorage(void* slot) const { delete static_cast(slot); } + +QString WireFunctionDefinition::toString() const { + QString paramString; + for (const auto& [name, type]: this->arguments) { + if (!paramString.isEmpty()) paramString += ", "; + paramString += name % ": " % type; + } + + return "function " % this->name % '(' % paramString % "): " % this->returnType; +} + +QString WireTargetDefinition::toString() const { + QString accum = "target " % this->name; + + for (const auto& func: this->functions) { + accum += "\n " % func.toString(); + } + + return accum; +} + +} // namespace qs::io::ipc diff --git a/src/io/ipc.hpp b/src/io/ipc.hpp new file mode 100644 index 00000000..50a94759 --- /dev/null +++ b/src/io/ipc.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include + +#include "../core/ipc.hpp" + +namespace qs::io::ipc { + +class IpcTypeSlot; + +class IpcType { +public: + IpcType() = default; + virtual ~IpcType() = default; + IpcType(const IpcType&) = default; + IpcType(IpcType&&) = default; + IpcType& operator=(const IpcType&) = default; + IpcType& operator=(IpcType&&) = default; + + [[nodiscard]] virtual const char* name() const = 0; + [[nodiscard]] virtual const char* genericArgumentName() const = 0; + [[nodiscard]] virtual void* fromString(const QString& /*string*/) const { return nullptr; } + [[nodiscard]] virtual QString toString(void* /*slot*/) const { return ""; } + [[nodiscard]] virtual void* createStorage() const { return nullptr; } + virtual void destroyStorage(void* /*slot*/) const {} + + static const IpcType* ipcType(const QMetaType& metaType); +}; + +class IpcTypeSlot { +public: + explicit IpcTypeSlot(const IpcType* type = nullptr): mType(type) {} + ~IpcTypeSlot(); + Q_DISABLE_COPY(IpcTypeSlot); + IpcTypeSlot(IpcTypeSlot&& other) noexcept; + IpcTypeSlot& operator=(IpcTypeSlot&& other) noexcept; + + [[nodiscard]] const IpcType* type() const; + [[nodiscard]] void* get(); + [[nodiscard]] QGenericArgument asGenericArgument(); + [[nodiscard]] QGenericReturnArgument asGenericReturnArgument(); + + void replace(void* value); + +private: + const IpcType* mType = nullptr; + void* storage = nullptr; +}; + +class VoidIpcType: public IpcType { +public: + [[nodiscard]] const char* name() const override; + [[nodiscard]] const char* genericArgumentName() const override; + + static const VoidIpcType INSTANCE; +}; + +class StringIpcType: public IpcType { +public: + [[nodiscard]] const char* name() const override; + [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] void* fromString(const QString& string) const override; + [[nodiscard]] QString toString(void* slot) const override; + [[nodiscard]] void* createStorage() const override; + void destroyStorage(void* slot) const override; + + static const StringIpcType INSTANCE; +}; + +class IntIpcType: public IpcType { +public: + [[nodiscard]] const char* name() const override; + [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] void* fromString(const QString& string) const override; + [[nodiscard]] QString toString(void* slot) const override; + [[nodiscard]] void* createStorage() const override; + void destroyStorage(void* slot) const override; + + static const IntIpcType INSTANCE; +}; + +class BoolIpcType: public IpcType { +public: + [[nodiscard]] const char* name() const override; + [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] void* fromString(const QString& string) const override; + [[nodiscard]] QString toString(void* slot) const override; + [[nodiscard]] void* createStorage() const override; + void destroyStorage(void* slot) const override; + + static const BoolIpcType INSTANCE; +}; + +class DoubleIpcType: public IpcType { +public: + [[nodiscard]] const char* name() const override; + [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] void* fromString(const QString& string) const override; + [[nodiscard]] QString toString(void* slot) const override; + [[nodiscard]] void* createStorage() const override; + void destroyStorage(void* slot) const override; + + static const DoubleIpcType INSTANCE; +}; + +class ColorIpcType: public IpcType { +public: + [[nodiscard]] const char* name() const override; + [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] void* fromString(const QString& string) const override; + [[nodiscard]] QString toString(void* slot) const override; + [[nodiscard]] void* createStorage() const override; + void destroyStorage(void* slot) const override; + + static const ColorIpcType INSTANCE; +}; + +struct WireFunctionDefinition { + QString name; + QString returnType; + QVector> arguments; + + [[nodiscard]] QString toString() const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(WireFunctionDefinition, data.name, data.returnType, data.arguments); + +struct WireTargetDefinition { + QString name; + QVector functions; + + [[nodiscard]] QString toString() const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions); + +} // namespace qs::io::ipc diff --git a/src/io/ipccomm.cpp b/src/io/ipccomm.cpp new file mode 100644 index 00000000..56381260 --- /dev/null +++ b/src/io/ipccomm.cpp @@ -0,0 +1,236 @@ +#include "ipccomm.hpp" +#include +#include + +#include +#include +#include +#include +#include + +#include "../core/generation.hpp" +#include "../core/ipc.hpp" +#include "../core/ipccommand.hpp" +#include "../core/logging.hpp" +#include "ipc.hpp" +#include "ipchandler.hpp" + +using namespace qs::ipc; + +namespace qs::io::ipc::comm { + +struct NoCurrentGeneration: std::monostate {}; +struct TargetNotFound: std::monostate {}; +struct FunctionNotFound: std::monostate {}; + +using QueryResponse = std::variant< + std::monostate, + NoCurrentGeneration, + TargetNotFound, + FunctionNotFound, + QVector, + WireTargetDefinition, + WireFunctionDefinition>; + +void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const { + auto resp = conn->responseStream(); + + if (auto* generation = EngineGeneration::currentGeneration()) { + auto* registry = IpcHandlerRegistry::forGeneration(generation); + + if (this->target.isEmpty()) { + resp << registry->wireTargets(); + } else { + auto* handler = registry->findHandler(this->target); + + if (handler) { + if (this->function.isEmpty()) { + resp << handler->wireDef(); + } else { + auto* func = handler->findFunction(this->function); + + if (func) { + resp << func->wireDef(); + } else { + resp << FunctionNotFound(); + } + } + } else { + resp << TargetNotFound(); + } + } + } else { + resp << NoCurrentGeneration(); + } +} + +int queryMetadata(IpcClient* client, const QString& target, const QString& function) { + client->sendMessage(IpcCommand(QueryMetadataCommand {.target = target, .function = function})); + + QueryResponse slot; + if (!client->waitForResponse(slot)) return -1; + + if (std::holds_alternative>(slot)) { + const auto& targets = std::get>(slot); + + for (const auto& target: targets) { + qCInfo(logBare).noquote() << target.toString(); + } + + return 0; + } else if (std::holds_alternative(slot)) { + qCInfo(logBare).noquote() << std::get(slot).toString(); + } else if (std::holds_alternative(slot)) { + qCInfo(logBare).noquote() << std::get(slot).toString(); + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Target not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Function 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; + } + + return -1; +} + +struct ArgParseFailed { + WireFunctionDefinition definition; + bool isCountMismatch = false; + quint8 paramIndex = 0; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS( + ArgParseFailed, + data.definition, + data.isCountMismatch, + data.paramIndex +); + +struct Completed { + bool isVoid = false; + QString returnValue; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(Completed, data.isVoid, data.returnValue); + +using StringCallResponse = std::variant< + std::monostate, + NoCurrentGeneration, + TargetNotFound, + FunctionNotFound, + ArgParseFailed, + Completed>; + +void StringCallCommand::exec(qs::ipc::IpcServerConnection* conn) const { + 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* func = handler->findFunction(this->function); + if (!func) { + resp << FunctionNotFound(); + return; + } + + if (func->argumentTypes.length() != this->arguments.length()) { + resp << ArgParseFailed { + .definition = func->wireDef(), + .isCountMismatch = true, + }; + + return; + } + + auto storage = IpcCallStorage(*func); + for (auto i = 0; i < this->arguments.length(); i++) { + if (!storage.setArgumentStr(i, this->arguments.value(i))) { + resp << ArgParseFailed { + .definition = func->wireDef(), + .paramIndex = static_cast(i), + }; + + return; + } + } + + func->invoke(handler, storage); + + resp << Completed { + .isVoid = func->returnType == &VoidIpcType::INSTANCE, + .returnValue = storage.getReturnStr(), + }; + } else { + conn->respond(StringCallResponse(NoCurrentGeneration())); + } +} + +int callFunction( + IpcClient* client, + const QString& target, + const QString& function, + const QVector& arguments +) { + if (target.isEmpty()) { + qCCritical(logBare) << "Target required to send message."; + return -1; + } else if (function.isEmpty()) { + qCCritical(logBare) << "Function required to send message."; + return -1; + } + + client->sendMessage( + IpcCommand(StringCallCommand {.target = target, .function = function, .arguments = arguments}) + ); + + StringCallResponse slot; + if (!client->waitForResponse(slot)) return -1; + + if (std::holds_alternative(slot)) { + auto& result = std::get(slot); + if (!result.isVoid) { + QTextStream(stdout) << result.returnValue << Qt::endl; + } + + return 0; + } else if (std::holds_alternative(slot)) { + auto& error = std::get(slot); + + if (error.isCountMismatch) { + auto correctCount = error.definition.arguments.length(); + + qCCritical(logBare).nospace() + << "Too " << (correctCount < arguments.length() ? "many" : "few") + << " arguments provided (" << correctCount << " required but " << arguments.length() + << " were provided.)"; + } else { + const auto& provided = arguments.at(error.paramIndex); + const auto& definition = error.definition.arguments.at(error.paramIndex); + + qCCritical(logBare).nospace() + << "Unable to parse argument " << (error.paramIndex + 1) << " as " << definition.second + << ". Provided argument: " << provided; + } + + qCCritical(logBare).noquote() << "Function definition:" << error.definition.toString(); + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Target not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Function 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; + } + + return -1; +} +} // namespace qs::io::ipc::comm diff --git a/src/io/ipccomm.hpp b/src/io/ipccomm.hpp new file mode 100644 index 00000000..7b1ec02a --- /dev/null +++ b/src/io/ipccomm.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include "../core/ipc.hpp" + +namespace qs::io::ipc::comm { + +struct QueryMetadataCommand { + QString target; + QString function; + + void exec(qs::ipc::IpcServerConnection* conn) const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(QueryMetadataCommand, data.target, data.function); + +struct StringCallCommand { + QString target; + QString function; + QVector arguments; + + void exec(qs::ipc::IpcServerConnection* conn) const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(StringCallCommand, data.target, data.function, data.arguments); + +void handleMsg(qs::ipc::IpcServerConnection* conn); +int queryMetadata(qs::ipc::IpcClient* client, const QString& target, const QString& function); + +int callFunction( + qs::ipc::IpcClient* client, + const QString& target, + const QString& function, + const QVector& arguments +); + +} // namespace qs::io::ipc::comm diff --git a/src/io/ipchandler.cpp b/src/io/ipchandler.cpp new file mode 100644 index 00000000..d2a549b2 --- /dev/null +++ b/src/io/ipchandler.cpp @@ -0,0 +1,324 @@ +#include "ipchandler.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/generation.hpp" +#include "ipc.hpp" + +namespace qs::io::ipc { + +bool IpcFunction::resolve(QString& error) { + if (this->method.parameterCount() > 10) { + error = "Due to technical limitations, IPC functions can only have 10 arguments."; + return false; + } + + for (auto i = 0; i < this->method.parameterCount(); i++) { + const auto& metaType = this->method.parameterMetaType(i); + const auto* type = IpcType::ipcType(metaType); + + if (type == nullptr) { + error = QString("Type of argument %1 (%2: %3) cannot be used across IPC.") + .arg(i + 1) + .arg(this->method.parameterNames().value(i)) + .arg(metaType.name()); + + return false; + } + + this->argumentTypes.append(type); + } + + const auto& metaType = this->method.returnMetaType(); + const auto* type = IpcType::ipcType(metaType); + + if (type == nullptr) { + // void and var get mixed by qml engine in return types + if (metaType.id() == QMetaType::QVariant) type = &VoidIpcType::INSTANCE; + + if (type == nullptr) { + error = QString("Return type (%1) cannot be used across IPC.").arg(metaType.name()); + return false; + } + } + + this->returnType = type; + + return true; +} + +void IpcFunction::invoke(QObject* target, IpcCallStorage& storage) const { + auto getArg = [&](size_t i) { + return i < storage.argumentSlots.size() ? storage.argumentSlots.at(i).asGenericArgument() + : QGenericArgument(); + }; + + this->method.invoke( + target, + storage.returnSlot.asGenericReturnArgument(), + getArg(0), + getArg(1), + getArg(2), + getArg(3), + getArg(4), + getArg(5), + getArg(6), + getArg(7), + getArg(8), + getArg(9) + ); +} + +QString IpcFunction::toString() const { + QString paramString; + auto paramNames = this->method.parameterNames(); + for (auto i = 0; i < this->argumentTypes.length(); i++) { + paramString += paramNames.value(i) % ": " % this->argumentTypes.value(i)->name(); + + if (i + 1 != this->argumentTypes.length()) { + paramString += ", "; + } + } + + return "function " % this->method.name() % '(' % paramString % "): " % this->returnType->name(); +} + +WireFunctionDefinition IpcFunction::wireDef() const { + WireFunctionDefinition wire; + wire.name = this->method.name(); + wire.returnType = this->returnType->name(); + + auto paramNames = this->method.parameterNames(); + for (auto i = 0; i < this->argumentTypes.length(); i++) { + wire.arguments += qMakePair(paramNames.value(i), this->argumentTypes.value(i)->name()); + } + + return wire; +} + +IpcCallStorage::IpcCallStorage(const IpcFunction& function): returnSlot(function.returnType) { + for (const auto& arg: function.argumentTypes) { + this->argumentSlots.emplace_back(arg); + } +} + +bool IpcCallStorage::setArgumentStr(size_t i, const QString& value) { + auto& slot = this->argumentSlots.at(i); + + auto* data = slot.type()->fromString(value); + slot.replace(data); + return data != nullptr; +} + +QString IpcCallStorage::getReturnStr() { + return this->returnSlot.type()->toString(this->returnSlot.get()); +} + +IpcHandler::~IpcHandler() { + if (this->registeredState.enabled) { + this->targetState.enabled = false; + this->updateRegistration(true); + } +} + +void IpcHandler::onPostReload() { + const auto& smeta = IpcHandler::staticMetaObject; + const auto* meta = this->metaObject(); + + // Start at the first function following IpcHandler's slots, + // 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; + + 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); + } + } + + this->complete = true; + this->updateRegistration(); + + if (this->targetState.enabled && this->targetState.target.isEmpty()) { + qmlWarning(this) << "This IPC handler is enabled but no target is set. This means it is " + "effectively inoperable."; + } +} + +IpcHandlerRegistry* IpcHandlerRegistry::forGeneration(EngineGeneration* generation) { + static const int key = 0; + auto* ext = generation->findExtension(&key); + + if (!ext) { + ext = new IpcHandlerRegistry(); + generation->registerExtension(&key, ext); + } + + return dynamic_cast(ext); +} + +void IpcHandler::updateRegistration(bool destroying) { + if (!this->complete) return; + + auto* generation = EngineGeneration::findObjectGeneration(this); + + if (!generation) { + if (!destroying) { + qmlWarning(this) << "Unable to identify engine generation, cannot register."; + } + + return; + } + + auto* registry = IpcHandlerRegistry::forGeneration(generation); + + if (this->registeredState.enabled) { + registry->deregisterHandler(this); + } + + if (this->targetState.enabled && !this->targetState.target.isEmpty()) { + registry->registerHandler(this); + } +} + +bool IpcHandler::enabled() const { return this->targetState.enabled; } + +void IpcHandler::setEnabled(bool enabled) { + if (enabled != this->targetState.enabled) { + this->targetState.enabled = enabled; + emit this->enabledChanged(); + this->updateRegistration(); + } +} + +QString IpcHandler::target() const { return this->targetState.target; } + +void IpcHandler::setTarget(const QString& target) { + if (target != this->targetState.target) { + this->targetState.target = target; + emit this->targetChanged(); + this->updateRegistration(); + } +} + +void IpcHandlerRegistry::registerHandler(IpcHandler* handler) { + // inserting a new vec if not present is the desired behavior + auto& targetVec = this->knownHandlers[handler->targetState.target]; + targetVec.append(handler); + + if (this->handlers.contains(handler->targetState.target)) { + qmlWarning(handler) << "Handler was registered but will not be used because another handler " + "is registered for target " + << handler->targetState.target; + } else { + this->handlers.insert(handler->targetState.target, handler); + } + + handler->registeredState = handler->targetState; + handler->registeredState.enabled = true; +} + +void IpcHandlerRegistry::deregisterHandler(IpcHandler* handler) { + auto& targetVec = this->knownHandlers[handler->registeredState.target]; + targetVec.removeOne(handler); + + if (this->handlers.value(handler->registeredState.target) == handler) { + if (targetVec.isEmpty()) { + this->handlers.remove(handler->registeredState.target); + } else { + this->handlers.insert(handler->registeredState.target, targetVec.first()); + } + } + + handler->registeredState = {.enabled = false, .target = ""}; +} + +QString IpcHandler::listMembers(qsizetype indent) { + auto indentStr = QString(indent, ' '); + QString accum; + + for (const auto& func: this->functionMap.values()) { + if (!accum.isEmpty()) accum += '\n'; + accum += indentStr % func.toString(); + } + + return accum; +} + +WireTargetDefinition IpcHandler::wireDef() const { + WireTargetDefinition wire; + wire.name = this->registeredState.target; + + for (const auto& func: this->functionMap.values()) { + wire.functions += func.wireDef(); + } + + return wire; +} + +QString IpcHandlerRegistry::listMembers(const QString& target, qsizetype indent) { + if (auto* handler = this->handlers.value(target)) { + return handler->listMembers(indent); + } else { + QString accum; + + for (auto* handler: this->knownHandlers.value(target)) { + if (!accum.isEmpty()) accum += '\n'; + accum += handler->listMembers(indent); + } + + return accum; + } +} + +QString IpcHandlerRegistry::listTargets(qsizetype indent) { + auto indentStr = QString(indent, ' '); + QString accum; + + for (const auto& target: this->knownHandlers.keys()) { + if (!accum.isEmpty()) accum += '\n'; + accum += indentStr % "Target " % target % '\n' % this->listMembers(target, indent + 2); + } + + return accum; +} + +IpcFunction* IpcHandler::findFunction(const QString& name) { + auto itr = this->functionMap.find(name); + + if (itr == this->functionMap.end()) return nullptr; + else return &*itr; +} + +IpcHandler* IpcHandlerRegistry::findHandler(const QString& target) { + return this->handlers.value(target); +} + +QVector IpcHandlerRegistry::wireTargets() const { + QVector wire; + + for (const auto* handler: this->handlers.values()) { + wire += handler->wireDef(); + } + + return wire; +} + +} // namespace qs::io::ipc diff --git a/src/io/ipchandler.hpp b/src/io/ipchandler.hpp new file mode 100644 index 00000000..df920334 --- /dev/null +++ b/src/io/ipchandler.hpp @@ -0,0 +1,207 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/generation.hpp" +#include "../core/reload.hpp" +#include "ipc.hpp" + +namespace qs::io::ipc { + +class IpcCallStorage; + +class IpcFunction { +public: + explicit IpcFunction(QMetaMethod method): method(method) {} + + bool resolve(QString& error); + void invoke(QObject* target, IpcCallStorage& storage) const; + + [[nodiscard]] QString toString() const; + [[nodiscard]] WireFunctionDefinition wireDef() const; + + QMetaMethod method; + QVector argumentTypes; + const IpcType* returnType = nullptr; +}; + +class IpcCallStorage { +public: + explicit IpcCallStorage(const IpcFunction& function); + + bool setArgumentStr(size_t i, const QString& value); + [[nodiscard]] QString getReturnStr(); + +private: + std::vector argumentSlots; + IpcTypeSlot returnSlot; + + friend class IpcFunction; +}; + +class IpcHandlerRegistry; + +///! Handler for IPC message calls. +/// Each IpcHandler is registered into a per-instance map by its unique @@target. +/// Functions defined on the IpcHandler can be called by `qs msg`. +/// +/// #### Handler Functions +/// IPC handler functions can be called by `qs msg` as long as they have at most 10 +/// arguments, and all argument types along with the return type are listed below. +/// +/// **Argument and return types must be explicitly specified or they will not +/// be registered.** +/// +/// ##### Arguments +/// - `string` will be passed to the parameter as is. +/// - `int` will only accept parameters that can be parsed as an integer. +/// - `bool` will only accept parameters that are "true", "false", or an integer, +/// where 0 will be converted to false, and anything else to true. +/// - `real` will only accept parameters that can be parsed as a number with +/// or without a decimal. +/// - `color` will accept [named colors] or hex strings (RGB, RRGGBB, AARRGGBB) with +/// an optional `#` prefix. +/// +/// [named colors]: https://doc.qt.io/qt-6/qml-color.html#svg-color-reference +/// +/// ##### Return Type +/// - `void` will return nothing. +/// - `string` will be returned as is. +/// - `int` will be converted to a string and returned. +/// - `bool` will be converted to "true" or "false" and returned. +/// - `real` will be converted to a string and returned. +/// - `color` will be converted to a hex string in the form `#AARRGGBB` and returned. +/// +/// #### Example +/// The following example creates ipc functions to control and retrieve the appearance +/// of a Rectangle. +/// +/// ```qml +/// FloatingWindow { +/// Rectangle { +/// id: rect +/// anchors.centerIn: parent +/// width: 100 +/// height: 100 +/// color: "red" +/// } +/// +/// IpcHandler { +/// target: "rect" +/// +/// 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 getRadius(): int { return rect.radius; } +/// } +/// } +/// ``` +/// The list of registered targets can be inspected using `qs msg -i`. +/// ```sh +/// $ qs msg -i +/// target rect +/// function setColor(color: color): void +/// function getColor(): color +/// function setAngle(angle: real): void +/// function getAngle(): real +/// function setRadius(radius: int): void +/// function getRadius(): int +/// ``` +/// +/// and then invoked using `qs msg`. +/// ```sh +/// $ qs msg rect setColor orange +/// $ qs msg rect setAngle 40.5 +/// $ qs msg rect setRadius 30 +/// $ qs msg rect getColor +/// #ffffa500 +/// $ qs msg rect getAngle +/// 40.5 +/// $ qs msg rect getRadius +/// 30 +/// ``` +class IpcHandler + : public QObject + , public PostReloadHook { + Q_OBJECT; + /// If the handler should be able to receive calls. Defaults to true. + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged); + /// The target this handler should be accessible from. + /// Required and must be unique. May be changed at runtime. + Q_PROPERTY(QString target READ target WRITE setTarget NOTIFY targetChanged); + QML_ELEMENT; + +public: + explicit IpcHandler(QObject* parent = nullptr): QObject(parent) {}; + ~IpcHandler() override; + Q_DISABLE_COPY_MOVE(IpcHandler); + + void onPostReload() override; + + [[nodiscard]] bool enabled() const; + void setEnabled(bool enabled); + + [[nodiscard]] QString target() const; + void setTarget(const QString& target); + + QString listMembers(qsizetype indent); + [[nodiscard]] IpcFunction* findFunction(const QString& name); + [[nodiscard]] WireTargetDefinition wireDef() const; + +signals: + void enabledChanged(); + void targetChanged(); + +private: + void updateRegistration(bool destroying = false); + + struct RegistrationState { + bool enabled = false; + QString target; + }; + + RegistrationState registeredState; + RegistrationState targetState {.enabled = true}; + bool complete = false; + + QHash functionMap; + + friend class IpcHandlerRegistry; +}; + +class IpcHandlerRegistry: public EngineGenerationExt { +public: + static IpcHandlerRegistry* forGeneration(EngineGeneration* generation); + + void registerHandler(IpcHandler* handler); + void deregisterHandler(IpcHandler* handler); + + QString listMembers(const QString& target, qsizetype indent); + QString listTargets(qsizetype indent); + + IpcHandler* findHandler(const QString& target); + + [[nodiscard]] QVector wireTargets() const; + +private: + QHash handlers; + QHash> knownHandlers; +}; + +} // namespace qs::io::ipc diff --git a/src/io/module.md b/src/io/module.md index 8af3799e..8c9e510c 100644 --- a/src/io/module.md +++ b/src/io/module.md @@ -5,5 +5,6 @@ headers = [ "socket.hpp", "process.hpp", "fileview.hpp", + "ipchandler.hpp", ] ----- From 293341c9e1a855567207dc35a1af27c153fe54fb Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 13 Sep 2024 04:18:52 -0700 Subject: [PATCH 166/305] core/reloader: ensure generation ptrs are removed on destroy Broke things that used currentGeneration, and we shouldn't have a list of dangling pointers anyway. --- src/core/generation.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 32018d67..395f255b 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -94,6 +94,8 @@ void EngineGeneration::destroy() { this->root->deleteLater(); this->root = nullptr; } else { + g_generations.remove(this->engine); + // the engine has never been used, no need to clean up delete this->engine; this->engine = nullptr; From accdc59a1c4cfeeb169c22940cb4b5ca322dff27 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 14 Sep 2024 01:31:39 -0700 Subject: [PATCH 167/305] wayland/all: scale layers and popup anchors correctly Layers now scale window size and exclusive zone to native pixels. Popup anchors do the same. --- src/wayland/popupanchor.cpp | 20 +++++++++++++++++++- src/wayland/wlr_layershell/surface.cpp | 6 ++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/wayland/popupanchor.cpp b/src/wayland/popupanchor.cpp index ec6e5dbe..e38eeff0 100644 --- a/src/wayland/popupanchor.cpp +++ b/src/wayland/popupanchor.cpp @@ -1,5 +1,6 @@ #include "popupanchor.hpp" +#include #include #include #include @@ -41,6 +42,14 @@ void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bo positioner.set_constraint_adjustment(anchor->adjustment().toInt()); auto anchorRect = anchor->rect(); + + if (auto* p = window->transientParent()) { + anchorRect.x = QHighDpi::toNativePixels(anchorRect.x, p); + anchorRect.y = QHighDpi::toNativePixels(anchorRect.y, p); + anchorRect.w = QHighDpi::toNativePixels(anchorRect.w, p); + anchorRect.h = QHighDpi::toNativePixels(anchorRect.h, p); + } + positioner.set_anchor_rect(anchorRect.x, anchorRect.y, anchorRect.w, anchorRect.h); XdgPositioner::anchor anchorFlag = XdgPositioner::anchor_none; @@ -92,9 +101,18 @@ void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bo bool WaylandPopupPositioner::shouldRepositionOnMove() const { return true; } void WaylandPopupPositioner::setFlags(PopupAnchor* anchor, QWindow* window) { + auto anchorRect = anchor->rect(); + + if (auto* p = window->transientParent()) { + anchorRect.x = QHighDpi::toNativePixels(anchorRect.x, p); + anchorRect.y = QHighDpi::toNativePixels(anchorRect.y, p); + anchorRect.w = QHighDpi::toNativePixels(anchorRect.w, p); + anchorRect.h = QHighDpi::toNativePixels(anchorRect.h, p); + } + // clang-format off window->setProperty("_q_waylandPopupConstraintAdjustment", anchor->adjustment().toInt()); - window->setProperty("_q_waylandPopupAnchorRect", anchor->rect().qrect()); + window->setProperty("_q_waylandPopupAnchorRect", anchorRect.qrect()); window->setProperty("_q_waylandPopupAnchor", QVariant::fromValue(Edges::toQt(anchor->edges()))); window->setProperty("_q_waylandPopupGravity", QVariant::fromValue(Edges::toQt(anchor->gravity()))); // clang-format on diff --git a/src/wayland/wlr_layershell/surface.cpp b/src/wayland/wlr_layershell/surface.cpp index 695ecc48..ca5e7d10 100644 --- a/src/wayland/wlr_layershell/surface.cpp +++ b/src/wayland/wlr_layershell/surface.cpp @@ -1,6 +1,7 @@ #include "surface.hpp" #include +#include #include #include #include @@ -70,7 +71,7 @@ QSWaylandLayerSurface::QSWaylandLayerSurface( // new updates will be sent from the extension this->ext->surface = this; - auto size = constrainedSize(this->ext->mAnchors, qwindow->size()); + auto size = constrainedSize(this->ext->mAnchors, window->surfaceSize()); this->set_size(size.width(), size.height()); } @@ -137,7 +138,8 @@ void QSWaylandLayerSurface::updateMargins() { } void QSWaylandLayerSurface::updateExclusiveZone() { - this->set_exclusive_zone(this->ext->mExclusiveZone); + auto nativeZone = QHighDpi::toNativePixels(this->ext->mExclusiveZone, this->window()->window()); + this->set_exclusive_zone(nativeZone); this->window()->waylandSurface()->commit(); } From abe0327e676cdae42443ec4cee5fbb3bbd902e56 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 14 Sep 2024 03:10:44 -0700 Subject: [PATCH 168/305] widgets: add IconImage widget Docs currently cannot be generated due to lack of qml parsing support in typegen. --- src/CMakeLists.txt | 1 + src/widgets/CMakeLists.txt | 13 ++++++++ src/widgets/IconImage.qml | 67 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 src/widgets/CMakeLists.txt create mode 100644 src/widgets/IconImage.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 42954775..33554832 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,6 +4,7 @@ install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) add_subdirectory(core) add_subdirectory(io) +add_subdirectory(widgets) if (CRASH_REPORTER) add_subdirectory(crash) diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt new file mode 100644 index 00000000..ac3682fa --- /dev/null +++ b/src/widgets/CMakeLists.txt @@ -0,0 +1,13 @@ +qt_add_library(quickshell-widgets STATIC) + +qt_add_qml_module(quickshell-widgets + URI Quickshell.Widgets + VERSION 0.1 + QML_FILES + IconImage.qml +) + +qs_pch(quickshell-widgets) +qs_pch(quickshell-widgetsplugin) + +target_link_libraries(quickshell PRIVATE quickshell-widgetsplugin) diff --git a/src/widgets/IconImage.qml b/src/widgets/IconImage.qml new file mode 100644 index 00000000..1cfd7969 --- /dev/null +++ b/src/widgets/IconImage.qml @@ -0,0 +1,67 @@ +import QtQuick + +///! Image component for displaying widget/icon style images. +/// This is a specialization of @@QtQuick.Image configured for icon-style images, +/// designed to make it easier to use correctly. If you need more control, use +/// @@QtQuick.Image directly. +/// +/// The image's aspect raito is assumed to be 1:1. If it is not 1:1, padding +/// will be added to make it 1:1. This is currently applied before the actual +/// aspect ratio of the image is taken into account, and may change in a future +/// release. +/// +/// You should use it for: +/// - Icons for custom buttons +/// - Status indicator icons +/// - System tray icons +/// - Things similar to the above. +/// +/// Do not use it for: +/// - Big images +/// - Images that change size frequently +/// - Anything that doesn't feel like an icon. +/// +/// > [!INFO] More information about many of these properties can be found in +/// > the documentation for @@QtQuick.Image. +Item { + id: root + + /// URL of the image. Defaults to an empty string. + /// See @@QtQuick.Image.source. + property alias source: image.source + /// If the image should be loaded asynchronously. Defaults to false. + /// See @@QtQuick.Image.asynchronous. + property alias asynchronous: image.asynchronous + /// The load status of the image. See @@QtQuick.Image.status. + property alias status: image.status + /// If the image should be mipmap filtered. Defaults to false. + /// See @@QtQuick.Image.mipmap. + /// + /// Try enabling this if your image is significantly scaled down + /// and looks bad because of it. + property alias mipmap: image.mipmap + /// The @@QtQuick.Image backing this object. + /// + /// This is useful if you need to access more functionality than + /// exposed by IconImage. + property alias backer: image + + /// The suggested size of the image. This is used as a defualt + /// for @@QtQuick.Item.implicitWidth and @@QtQuick.Item.implicitHeight. + property real implicitSize: 0 + + /// The actual size the image will be displayed at. + readonly property real actualSize: Math.min(root.width, root.height) + + implicitWidth: root.implicitSize + implicitHeight: root.implicitSize + + Image { + id: image + anchors.fill: parent + fillMode: Image.PreserveAspectFit + + sourceSize.width: root.actualSize + sourceSize.height: root.actualSize + } +} From 01f2be057e3425064bcc5fa4cf657a534adfd992 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 15 Sep 2024 02:20:58 -0700 Subject: [PATCH 169/305] widgets/iconimage: add typegen hints to alias properties --- src/widgets/IconImage.qml | 8 ++++---- src/widgets/module.md | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 src/widgets/module.md diff --git a/src/widgets/IconImage.qml b/src/widgets/IconImage.qml index 1cfd7969..dd8a5f11 100644 --- a/src/widgets/IconImage.qml +++ b/src/widgets/IconImage.qml @@ -28,10 +28,10 @@ Item { /// URL of the image. Defaults to an empty string. /// See @@QtQuick.Image.source. - property alias source: image.source + property /*string*/alias source: image.source /// If the image should be loaded asynchronously. Defaults to false. /// See @@QtQuick.Image.asynchronous. - property alias asynchronous: image.asynchronous + property /*bool*/alias asynchronous: image.asynchronous /// The load status of the image. See @@QtQuick.Image.status. property alias status: image.status /// If the image should be mipmap filtered. Defaults to false. @@ -39,12 +39,12 @@ Item { /// /// Try enabling this if your image is significantly scaled down /// and looks bad because of it. - property alias mipmap: image.mipmap + property /*bool*/alias mipmap: image.mipmap /// The @@QtQuick.Image backing this object. /// /// This is useful if you need to access more functionality than /// exposed by IconImage. - property alias backer: image + property /*Image*/alias backer: image /// The suggested size of the image. This is used as a defualt /// for @@QtQuick.Item.implicitWidth and @@QtQuick.Item.implicitHeight. diff --git a/src/widgets/module.md b/src/widgets/module.md new file mode 100644 index 00000000..9a51894c --- /dev/null +++ b/src/widgets/module.md @@ -0,0 +1,6 @@ +name = "Quickshell.Widgets" +description = "Bundled widgets" +qml_files = [ + "IconImage.qml", +] +----- From bdc9fe958bc261dd887b4de2466732e63c96ed21 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 15 Sep 2024 13:50:00 -0700 Subject: [PATCH 170/305] service/tray: delete image pixmaps created with new[] using delete[] --- src/services/status_notifier/dbus_item_types.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/status_notifier/dbus_item_types.cpp b/src/services/status_notifier/dbus_item_types.cpp index 567f4644..afb623ef 100644 --- a/src/services/status_notifier/dbus_item_types.cpp +++ b/src/services/status_notifier/dbus_item_types.cpp @@ -14,7 +14,7 @@ QImage DBusSniIconPixmap::createImage() const { // fix byte order if on a little endian machine if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) { auto* newbuf = new quint32[this->data.size()]; - const auto* oldbuf = reinterpret_cast(this->data.data()); // NOLINT + const auto* oldbuf = reinterpret_cast(this->data.constData()); // NOLINT for (uint i = 0; i < this->data.size() / sizeof(quint32); ++i) { newbuf[i] = qFromBigEndian(oldbuf[i]); // NOLINT @@ -25,12 +25,12 @@ QImage DBusSniIconPixmap::createImage() const { this->width, this->height, QImage::Format_ARGB32, - [](void* ptr) { delete reinterpret_cast(ptr); }, // NOLINT + [](void* ptr) { delete[] reinterpret_cast(ptr); }, // NOLINT newbuf ); } else { return QImage( - reinterpret_cast(this->data.data()), // NOLINT + reinterpret_cast(this->data.constData()), // NOLINT this->width, this->height, QImage::Format_ARGB32 From 84e3f04f3c828899f9204830c2e4d7468057f582 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 15 Sep 2024 15:32:01 -0700 Subject: [PATCH 171/305] service/tray: disconnect menu from handle on deletion Fixes loaded being set to true after deleting the menu. --- src/dbus/dbusmenu/dbusmenu.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dbus/dbusmenu/dbusmenu.cpp b/src/dbus/dbusmenu/dbusmenu.cpp index 0d966610..13919faf 100644 --- a/src/dbus/dbusmenu/dbusmenu.cpp +++ b/src/dbus/dbusmenu/dbusmenu.cpp @@ -545,6 +545,9 @@ void DBusMenuHandle::onMenuPathChanged() { qCDebug(logDbusMenu) << "Updating" << this << "with refcount" << this->refcount; if (this->mMenu) { + // Without this, layout updated can be sent after mMenu is set to null, + // leaving loaded = true while mMenu = nullptr. + QObject::disconnect(&this->mMenu->rootItem, nullptr, this, nullptr); this->mMenu->deleteLater(); this->mMenu = nullptr; this->loaded = false; From 08966f91c587710371fa028ac51fa9dc9ea09f5a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 15 Sep 2024 15:57:29 -0700 Subject: [PATCH 172/305] service/tray: always mark the root menu item as having children Blueman doesn't for some reason. This causes PlatformMenuEntry::display to crash after ::relayout created a QAction instead of a QMenu. Fixes #5 --- src/dbus/dbusmenu/dbusmenu.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dbus/dbusmenu/dbusmenu.cpp b/src/dbus/dbusmenu/dbusmenu.cpp index 13919faf..6e4feeb7 100644 --- a/src/dbus/dbusmenu/dbusmenu.cpp +++ b/src/dbus/dbusmenu/dbusmenu.cpp @@ -98,7 +98,7 @@ void DBusMenuItem::updateLayout() const { this->menu->updateLayout(this->id, -1); } -bool DBusMenuItem::hasChildren() const { return this->displayChildren; } +bool DBusMenuItem::hasChildren() const { return this->displayChildren || this->id == 0; } QQmlListProperty DBusMenuItem::children() { return QQmlListProperty( From c57ac4b1f283896bab7fb91e8fdc9249c18d3afe Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 15 Sep 2024 16:06:20 -0700 Subject: [PATCH 173/305] core/menu: disconnect menu before unref when changed --- src/core/qsmenu.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/qsmenu.cpp b/src/core/qsmenu.cpp index 1587912d..de9ed6f6 100644 --- a/src/core/qsmenu.cpp +++ b/src/core/qsmenu.cpp @@ -71,8 +71,8 @@ void QsMenuOpener::setMenu(QsMenuHandle* menu) { QObject::disconnect(this->mMenu, nullptr, this, nullptr); if (this->mMenu->menu()) { - this->mMenu->menu()->unref(); QObject::disconnect(this->mMenu->menu(), nullptr, this, nullptr); + this->mMenu->menu()->unref(); } this->mMenu->unrefHandle(); From 7a283089b1dc98d2ea2611d7492df9877e3db8ec Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 17 Sep 2024 14:04:54 -0700 Subject: [PATCH 174/305] core/command: rename --instance to --id and --info to --show Fixes conflicting short flags. --- src/core/main.cpp | 4 ++-- src/io/ipchandler.hpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/main.cpp b/src/core/main.cpp index 8549deb4..b6a2f683 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -267,7 +267,7 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { auto addInstanceSelection = [&](CLI::App* cmd) { auto* group = cmd->add_option_group("Instance Selection"); - group->add_option("-i,--instance", state.instance.id) + 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,\n" "for example \"abc\" will select \"abcdefg\"."); @@ -360,7 +360,7 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { ->needs(function) ->allow_extra_args(); - sub->add_flag("-i,--info", state.ipc.info) + sub->add_flag("-s,--show", state.ipc.info) ->description("Print information about a function or target if given, or all available " "targets if not.") ->excludes(arguments); diff --git a/src/io/ipchandler.hpp b/src/io/ipchandler.hpp index df920334..97519807 100644 --- a/src/io/ipchandler.hpp +++ b/src/io/ipchandler.hpp @@ -112,9 +112,9 @@ class IpcHandlerRegistry; /// } /// } /// ``` -/// The list of registered targets can be inspected using `qs msg -i`. +/// The list of registered targets can be inspected using `qs msg -s`. /// ```sh -/// $ qs msg -i +/// $ qs msg -s /// target rect /// function setColor(color: color): void /// function getColor(): color From bd8978375beb37ab543476c7dd0d3c06cfe9a295 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 17 Sep 2024 14:21:34 -0700 Subject: [PATCH 175/305] core/icon: allow changing the icon theme --- src/core/main.cpp | 6 ++++++ src/core/qmlglobal.hpp | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/src/core/main.cpp b/src/core/main.cpp index b6a2f683..57bdfb85 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -822,6 +822,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio bool useQApplication = false; bool nativeTextRendering = false; bool desktopSettingsAware = true; + QString iconTheme = qEnvironmentVariable("QS_ICON_THEME"); QHash envOverrides; } pragmas; @@ -834,6 +835,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.startsWith("IconTheme ")) pragmas.iconTheme = pragma.sliced(10); else if (pragma.startsWith("Env ")) { auto envPragma = pragma.sliced(4); auto splitIdx = envPragma.indexOf('='); @@ -857,6 +859,10 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio file.close(); + if (!pragmas.iconTheme.isEmpty()) { + QIcon::setThemeName(pragmas.iconTheme); + } + qInfo() << "Shell ID:" << shellId << "Path ID" << pathId; auto launchTime = qs::Common::LAUNCH_TIME.toSecsSinceEpoch(); diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index ae797d62..fb1853fb 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -124,6 +124,13 @@ public: Q_INVOKABLE QVariant env(const QString& variable); /// Returns a string usable for a @@QtQuick.Image.source for a given system icon. + /// + /// > [!INFO] By default, icons are loaded from the theme selected by the qt platform theme, + /// > which means they should match with all other qt applications on your system. + /// > + /// > If you want to use a different icon theme, you can put `//@ pragma IconTheme ` + /// > at the top of your root config file or set the `QS_ICON_THEME` variable to the name + /// > of your icon theme. Q_INVOKABLE static QString iconPath(const QString& icon); [[nodiscard]] QString workingDirectory() const; From 931aca53925f1e89d9e720a1c0b0c3152deaa91d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 17 Sep 2024 23:04:06 -0700 Subject: [PATCH 176/305] service/pipewire: don't use configured default devices These don't appear to be intended for use by applications, only the non configured ones. This fixes the default device being unset on many computers and the device being lost on actions like headphone unplug which replace it. --- src/services/pipewire/metadata.cpp | 27 ++++----------------------- src/services/pipewire/metadata.hpp | 2 -- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/src/services/pipewire/metadata.cpp b/src/services/pipewire/metadata.cpp index 3a64a38d..db0e4dd2 100644 --- a/src/services/pipewire/metadata.cpp +++ b/src/services/pipewire/metadata.cpp @@ -70,22 +70,11 @@ void PwDefaultsMetadata::onMetadataUpdate( ) { if (subject != 0) return; - // non "configured" sinks and sources have lower priority as wireplumber seems to only change - // the "configured" ones. - - bool sink = false; - if (strcmp(key, "default.configured.audio.sink") == 0) { - sink = true; - this->sinkConfigured = true; - } else if ((!this->sinkConfigured && strcmp(key, "default.audio.sink") == 0)) { - sink = true; - } - - if (sink) { + if (strcmp(key, "default.audio.sink") == 0) { this->defaultSinkHolder.setObject(metadata); auto newSink = PwDefaultsMetadata::parseNameSpaJson(value); - qCInfo(logMeta) << "Got default sink" << newSink << "configured:" << this->sinkConfigured; + qCInfo(logMeta) << "Got default sink" << newSink; if (newSink == this->mDefaultSink) return; this->mDefaultSink = newSink; @@ -93,19 +82,11 @@ void PwDefaultsMetadata::onMetadataUpdate( return; } - bool source = false; - if (strcmp(key, "default.configured.audio.source") == 0) { - source = true; - this->sourceConfigured = true; - } else if ((!this->sourceConfigured && strcmp(key, "default.audio.source") == 0)) { - source = true; - } - - if (source) { + if (strcmp(key, "default.audio.source") == 0) { this->defaultSourceHolder.setObject(metadata); auto newSource = PwDefaultsMetadata::parseNameSpaJson(value); - qCInfo(logMeta) << "Got default source" << newSource << "configured:" << this->sourceConfigured; + qCInfo(logMeta) << "Got default source" << newSource; if (newSource == this->mDefaultSource) return; this->mDefaultSource = newSource; diff --git a/src/services/pipewire/metadata.hpp b/src/services/pipewire/metadata.hpp index 4937a747..f57c9c58 100644 --- a/src/services/pipewire/metadata.hpp +++ b/src/services/pipewire/metadata.hpp @@ -55,9 +55,7 @@ private: PwBindableRef defaultSinkHolder; PwBindableRef defaultSourceHolder; - bool sinkConfigured = false; QString mDefaultSink; - bool sourceConfigured = false; QString mDefaultSource; }; From 7f9762be5368ca33b84e7b2b3e23a626d432436d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 17 Sep 2024 23:44:41 -0700 Subject: [PATCH 177/305] service/pipewire: disconnect link tracker from registry on node destroy Caused duplicate entries to be created due to double connection, which then caused a crash. --- src/services/pipewire/qml.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp index b40de687..7092eb4a 100644 --- a/src/services/pipewire/qml.cpp +++ b/src/services/pipewire/qml.cpp @@ -220,6 +220,8 @@ PwNodeLinkTracker::linkGroupAt(QQmlListProperty* property, qsi void PwNodeLinkTracker::onNodeDestroyed() { this->mNode = nullptr; + QObject::disconnect(&PwConnection::instance()->registry, nullptr, this, nullptr); + this->updateLinks(); emit this->nodeChanged(); } @@ -350,7 +352,6 @@ PwLinkIface* PwLinkIface::instance(PwLink* link) { PwLinkGroupIface::PwLinkGroupIface(PwLinkGroup* group): QObject(group), mGroup(group) { QObject::connect(group, &PwLinkGroup::stateChanged, this, &PwLinkGroupIface::stateChanged); - QObject::connect(group, &QObject::destroyed, this, [this]() { delete this; }); } void PwLinkGroupIface::ref() { this->mGroup->ref(); } From f889f089017dc787022a4ec3cd271a3f0a19b3ad Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 23 Sep 2024 18:41:38 -0700 Subject: [PATCH 178/305] service/pipewire: refactor defaults and metadata handling --- src/core/util.hpp | 33 ++++ src/services/pipewire/CMakeLists.txt | 1 + src/services/pipewire/connection.hpp | 4 +- src/services/pipewire/defaults.cpp | 224 +++++++++++++++++++++++++++ src/services/pipewire/defaults.hpp | 73 +++++++++ src/services/pipewire/metadata.cpp | 96 +++--------- src/services/pipewire/metadata.hpp | 45 ++---- src/services/pipewire/qml.cpp | 65 +++++--- src/services/pipewire/qml.hpp | 31 +++- src/services/pipewire/registry.cpp | 13 +- src/services/pipewire/registry.hpp | 14 +- 11 files changed, 455 insertions(+), 144 deletions(-) create mode 100644 src/services/pipewire/defaults.cpp create mode 100644 src/services/pipewire/defaults.hpp diff --git a/src/core/util.hpp b/src/core/util.hpp index 3c1a5ac6..3ca095e4 100644 --- a/src/core/util.hpp +++ b/src/core/util.hpp @@ -1,7 +1,9 @@ #pragma once #include +#include #include +#include // NOLINTBEGIN #define DROP_EMIT(object, func) \ @@ -211,3 +213,34 @@ 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); +} diff --git a/src/services/pipewire/CMakeLists.txt b/src/services/pipewire/CMakeLists.txt index 51c9fec8..6996eff7 100644 --- a/src/services/pipewire/CMakeLists.txt +++ b/src/services/pipewire/CMakeLists.txt @@ -10,6 +10,7 @@ qt_add_library(quickshell-service-pipewire STATIC metadata.cpp link.cpp device.cpp + defaults.cpp ) qt_add_qml_module(quickshell-service-pipewire diff --git a/src/services/pipewire/connection.hpp b/src/services/pipewire/connection.hpp index fa270356..2b3e860e 100644 --- a/src/services/pipewire/connection.hpp +++ b/src/services/pipewire/connection.hpp @@ -1,7 +1,7 @@ #pragma once #include "core.hpp" -#include "metadata.hpp" +#include "defaults.hpp" #include "registry.hpp" namespace qs::service::pipewire { @@ -13,7 +13,7 @@ public: explicit PwConnection(QObject* parent = nullptr); PwRegistry registry; - PwDefaultsMetadata defaults {&this->registry}; + PwDefaultTracker defaults {&this->registry}; static PwConnection* instance(); diff --git a/src/services/pipewire/defaults.cpp b/src/services/pipewire/defaults.cpp new file mode 100644 index 00000000..86e50f50 --- /dev/null +++ b/src/services/pipewire/defaults.cpp @@ -0,0 +1,224 @@ +#include "defaults.hpp" +#include +#include + +#include +#include +#include +#include +#include + +#include "../../core/util.hpp" +#include "metadata.hpp" +#include "node.hpp" +#include "registry.hpp" + +namespace qs::service::pipewire { + +Q_LOGGING_CATEGORY(logDefaults, "quickshell.service.pipewire.defaults", QtWarningMsg); + +PwDefaultTracker::PwDefaultTracker(PwRegistry* registry): registry(registry) { + QObject::connect(registry, &PwRegistry::metadataAdded, this, &PwDefaultTracker::onMetadataAdded); + QObject::connect(registry, &PwRegistry::nodeAdded, this, &PwDefaultTracker::onNodeAdded); +} + +void PwDefaultTracker::onMetadataAdded(PwMetadata* metadata) { + if (metadata->name() == "default") { + qCDebug(logDefaults) << "Got new defaults metadata object" << metadata; + + if (this->defaultsMetadata.object()) { + QObject::disconnect(this->defaultsMetadata.object(), nullptr, this, nullptr); + } + + QObject::connect( + metadata, + &PwMetadata::propertyChanged, + this, + &PwDefaultTracker::onMetadataProperty + ); + + this->defaultsMetadata.setObject(metadata); + } +} + +void PwDefaultTracker::onMetadataProperty(const char* key, const char* type, const char* value) { + void (PwDefaultTracker::*nodeSetter)(PwNode*) = nullptr; + void (PwDefaultTracker::*nameSetter)(const QString&) = nullptr; + + qCDebug(logDefaults).nospace() << "Got default metadata update for " << key << ": " + << QString(value); + + if (strcmp(key, "default.audio.sink") == 0) { + nodeSetter = &PwDefaultTracker::setDefaultSink; + nameSetter = &PwDefaultTracker::setDefaultSinkName; + } else if (strcmp(key, "default.audio.source") == 0) { + nodeSetter = &PwDefaultTracker::setDefaultSource; + nameSetter = &PwDefaultTracker::setDefaultSourceName; + } else if (strcmp(key, "default.configured.audio.sink") == 0) { + nodeSetter = &PwDefaultTracker::setDefaultConfiguredSink; + nameSetter = &PwDefaultTracker::setDefaultConfiguredSinkName; + } else if (strcmp(key, "default.configured.audio.source") == 0) { + nodeSetter = &PwDefaultTracker::setDefaultConfiguredSource; + nameSetter = &PwDefaultTracker::setDefaultConfiguredSourceName; + } else return; + + QString name; + if (strcmp(type, "Spa:String:JSON") == 0) { + auto failed = true; + auto iter = std::array(); + spa_json_init(&iter[0], value, strlen(value)); + + if (spa_json_enter_object(&iter[0], &iter[1]) > 0) { + auto buf = std::array(); + + if (spa_json_get_string(&iter[1], buf.data(), buf.size()) > 0) { + if (strcmp(buf.data(), "name") == 0) { + if (spa_json_get_string(&iter[1], buf.data(), buf.size()) > 0) { + name = buf.data(); + failed = false; + } + } + } + } + + if (failed) { + qCWarning(logDefaults) << "Failed to parse SPA default json:" + << QString::fromLocal8Bit(value); + } + } + + (this->*nameSetter)(name); + (this->*nodeSetter)(this->registry->findNodeByName(name)); +} + +void PwDefaultTracker::onNodeAdded(PwNode* node) { + if (node->name.isEmpty()) return; + + if (this->mDefaultSink == nullptr && node->name == this->mDefaultSinkName) { + this->setDefaultSink(node); + } + + if (this->mDefaultSource == nullptr && node->name == this->mDefaultSourceName) { + this->setDefaultSource(node); + } + + if (this->mDefaultConfiguredSink == nullptr && node->name == this->mDefaultConfiguredSinkName) { + this->setDefaultConfiguredSink(node); + } + + if (this->mDefaultConfiguredSource == nullptr && node->name == this->mDefaultConfiguredSourceName) + { + this->setDefaultConfiguredSource(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::setDefaultSink(PwNode* node) { + if (node == this->mDefaultSink) return; + qCInfo(logDefaults) << "Default sink changed to" << node; + + setSimpleObjectHandle< + &PwDefaultTracker::mDefaultSink, + &PwDefaultTracker::onNodeDestroyed, + &PwDefaultTracker::defaultSinkChanged>(this, node); +} + +void PwDefaultTracker::setDefaultSinkName(const QString& name) { + if (name == this->mDefaultSinkName) return; + qCInfo(logDefaults) << "Default sink name changed to" << name; + this->mDefaultSinkName = name; + emit this->defaultSinkNameChanged(); +} + +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); +} + +void PwDefaultTracker::setDefaultSourceName(const QString& name) { + if (name == this->mDefaultSourceName) return; + qCInfo(logDefaults) << "Default source name changed to" << name; + this->mDefaultSourceName = name; + emit this->defaultSourceNameChanged(); +} + +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); +} + +void PwDefaultTracker::setDefaultConfiguredSinkName(const QString& name) { + if (name == this->mDefaultConfiguredSinkName) return; + qCInfo(logDefaults) << "Default configured sink name changed to" << name; + this->mDefaultConfiguredSinkName = name; + emit this->defaultConfiguredSinkNameChanged(); +} + +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); +} + +void PwDefaultTracker::setDefaultConfiguredSourceName(const QString& name) { + if (name == this->mDefaultConfiguredSourceName) return; + qCInfo(logDefaults) << "Default configured source name changed to" << name; + this->mDefaultConfiguredSourceName = name; + emit this->defaultConfiguredSourceNameChanged(); +} + +PwNode* PwDefaultTracker::defaultSink() const { return this->mDefaultSink; } +PwNode* PwDefaultTracker::defaultSource() const { return this->mDefaultSource; } + +PwNode* PwDefaultTracker::defaultConfiguredSink() const { return this->mDefaultConfiguredSink; } + +const QString& PwDefaultTracker::defaultConfiguredSinkName() const { + return this->mDefaultConfiguredSinkName; +} + +PwNode* PwDefaultTracker::defaultConfiguredSource() const { return this->mDefaultConfiguredSource; } + +const QString& PwDefaultTracker::defaultConfiguredSourceName() const { + return this->mDefaultConfiguredSourceName; +} + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/defaults.hpp b/src/services/pipewire/defaults.hpp new file mode 100644 index 00000000..9544514e --- /dev/null +++ b/src/services/pipewire/defaults.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include +#include + +#include "registry.hpp" + +namespace qs::service::pipewire { + +class PwDefaultTracker: public QObject { + Q_OBJECT; + +public: + explicit PwDefaultTracker(PwRegistry* registry); + + [[nodiscard]] PwNode* defaultSink() const; + [[nodiscard]] PwNode* defaultSource() const; + + [[nodiscard]] PwNode* defaultConfiguredSink() const; + [[nodiscard]] const QString& defaultConfiguredSinkName() const; + + [[nodiscard]] PwNode* defaultConfiguredSource() const; + [[nodiscard]] const QString& defaultConfiguredSourceName() const; + +signals: + void defaultSinkChanged(); + void defaultSinkNameChanged(); + + void defaultSourceChanged(); + void defaultSourceNameChanged(); + + void defaultConfiguredSinkChanged(); + void defaultConfiguredSinkNameChanged(); + + void defaultConfiguredSourceChanged(); + void defaultConfiguredSourceNameChanged(); + +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); + +private: + void setDefaultSink(PwNode* node); + void setDefaultSinkName(const QString& name); + + void setDefaultSource(PwNode* node); + void setDefaultSourceName(const QString& name); + + void setDefaultConfiguredSink(PwNode* node); + void setDefaultConfiguredSinkName(const QString& name); + + void setDefaultConfiguredSource(PwNode* node); + void setDefaultConfiguredSourceName(const QString& name); + + PwRegistry* registry; + PwBindableRef defaultsMetadata; + + PwNode* mDefaultSink = nullptr; + QString mDefaultSinkName; + + PwNode* mDefaultSource = nullptr; + QString mDefaultSourceName; + + PwNode* mDefaultConfiguredSink = nullptr; + QString mDefaultConfiguredSinkName; + + PwNode* mDefaultConfiguredSource = nullptr; + QString mDefaultConfiguredSourceName; +}; + +} // namespace qs::service::pipewire diff --git a/src/services/pipewire/metadata.cpp b/src/services/pipewire/metadata.cpp index db0e4dd2..582e9ac6 100644 --- a/src/services/pipewire/metadata.cpp +++ b/src/services/pipewire/metadata.cpp @@ -1,14 +1,15 @@ #include "metadata.hpp" -#include -#include +#include #include #include #include #include +#include #include #include -#include +#include +#include #include "registry.hpp" @@ -22,6 +23,14 @@ void PwMetadata::bindHooks() { void PwMetadata::unbindHooks() { this->listener.remove(); } +void PwMetadata::initProps(const spa_dict* props) { + if (const auto* name = spa_dict_lookup(props, PW_KEY_METADATA_NAME)) { + this->mName = name; + } +} + +const QString& PwMetadata::name() const { return this->mName; } + const pw_metadata_events PwMetadata::EVENTS = { .version = PW_VERSION_METADATA_EVENTS, .property = &PwMetadata::onProperty, @@ -39,89 +48,26 @@ int PwMetadata::onProperty( << "key:" << QString(key) << "type:" << QString(type) << "value:" << QString(value); - emit self->registry->metadataUpdate(self, subject, key, type, value); - - // ideally we'd dealloc metadata that wasn't picked up but there's no information - // available about if updates can come in later, so I assume they can. + emit self->propertyChanged(key, type, value); return 0; // ??? - no docs and no reason for a callback to return an int } -PwDefaultsMetadata::PwDefaultsMetadata(PwRegistry* registry) { - QObject::connect( - registry, - &PwRegistry::metadataUpdate, - this, - &PwDefaultsMetadata::onMetadataUpdate - ); +bool PwMetadata::hasSetPermission() const { + return (this->perms & SPA_PARAM_INFO_WRITE) == SPA_PARAM_INFO_WRITE; } -QString PwDefaultsMetadata::defaultSink() const { return this->mDefaultSink; } - -QString PwDefaultsMetadata::defaultSource() const { return this->mDefaultSource; } - -// we don't really care if the metadata objects are destroyed, but try to ref them so we get property updates -void PwDefaultsMetadata::onMetadataUpdate( - PwMetadata* metadata, - quint32 subject, - const char* key, - const char* /*type*/, - const char* value -) { - if (subject != 0) return; - - if (strcmp(key, "default.audio.sink") == 0) { - this->defaultSinkHolder.setObject(metadata); - - auto newSink = PwDefaultsMetadata::parseNameSpaJson(value); - qCInfo(logMeta) << "Got default sink" << newSink; - if (newSink == this->mDefaultSink) return; - - this->mDefaultSink = newSink; - emit this->defaultSinkChanged(); +void PwMetadata::setProperty(const char* key, const char* type, const char* value) { + if (this->proxy() == nullptr) { + qCCritical(logMeta) << "Tried to change property of" << this << "which is not bound."; return; } - if (strcmp(key, "default.audio.source") == 0) { - this->defaultSourceHolder.setObject(metadata); - - auto newSource = PwDefaultsMetadata::parseNameSpaJson(value); - qCInfo(logMeta) << "Got default source" << newSource; - if (newSource == this->mDefaultSource) return; - - this->mDefaultSource = newSource; - emit this->defaultSourceChanged(); - return; - } -} - -QString PwDefaultsMetadata::parseNameSpaJson(const char* spaJson) { - auto iter = std::array(); - spa_json_init(&iter[0], spaJson, strlen(spaJson)); - - if (spa_json_enter_object(&iter[0], &iter[1]) < 0) { - qCWarning(logMeta) << "Failed to parse source/sink SPA json - failed to enter object of" - << QString(spaJson); - return ""; + if (!this->hasSetPermission()) { + qCCritical(logMeta) << "Tried to change property of" << this << "which is read-only."; } - auto buf = std::array(); - while (spa_json_get_string(&iter[1], buf.data(), buf.size()) > 0) { - if (strcmp(buf.data(), "name") != 0) continue; - - if (spa_json_get_string(&iter[1], buf.data(), buf.size()) < 0) { - qCWarning(logMeta - ) << "Failed to parse source/sink SPA json - failed to read value of name property" - << QString(spaJson); - return ""; - } - - return QString(buf.data()); - } - - qCWarning(logMeta) << "Failed to parse source/sink SPA json - failed to find name property of" - << QString(spaJson); - return ""; + pw_metadata_set_property(this->proxy(), PW_ID_CORE, key, type, value); } } // namespace qs::service::pipewire diff --git a/src/services/pipewire/metadata.hpp b/src/services/pipewire/metadata.hpp index f57c9c58..812a8534 100644 --- a/src/services/pipewire/metadata.hpp +++ b/src/services/pipewire/metadata.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "core.hpp" @@ -18,45 +19,25 @@ class PwMetadata public: void bindHooks() override; void unbindHooks() override; + void initProps(const spa_dict* props) override; + + [[nodiscard]] const QString& name() const; + [[nodiscard]] bool hasSetPermission() const; + + // null value clears + void setProperty(const char* key, const char* type, const char* value); + +signals: + void propertyChanged(const char* key, const char* type, const char* value); private: static const pw_metadata_events EVENTS; static int onProperty(void* data, quint32 subject, const char* key, const char* type, const char* value); + QString mName; + SpaHook listener; }; -class PwDefaultsMetadata: public QObject { - Q_OBJECT; - -public: - explicit PwDefaultsMetadata(PwRegistry* registry); - - [[nodiscard]] QString defaultSource() const; - [[nodiscard]] QString defaultSink() const; - -signals: - void defaultSourceChanged(); - void defaultSinkChanged(); - -private slots: - void onMetadataUpdate( - PwMetadata* metadata, - quint32 subject, - const char* key, - const char* type, - const char* value - ); - -private: - static QString parseNameSpaJson(const char* spaJson); - - PwBindableRef defaultSinkHolder; - PwBindableRef defaultSourceHolder; - - QString mDefaultSink; - QString mDefaultSource; -}; - } // namespace qs::service::pipewire diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp index 7092eb4a..aad58a57 100644 --- a/src/services/pipewire/qml.cpp +++ b/src/services/pipewire/qml.cpp @@ -10,8 +10,8 @@ #include "../../core/model.hpp" #include "connection.hpp" +#include "defaults.hpp" #include "link.hpp" -#include "metadata.hpp" #include "node.hpp" #include "registry.hpp" @@ -60,10 +60,33 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) { &Pipewire::onLinkGroupAdded ); - // clang-format off - QObject::connect(&connection->defaults, &PwDefaultsMetadata::defaultSinkChanged, this, &Pipewire::defaultAudioSinkChanged); - QObject::connect(&connection->defaults, &PwDefaultsMetadata::defaultSourceChanged, this, &Pipewire::defaultAudioSourceChanged); - // clang-format on + QObject::connect( + &connection->defaults, + &PwDefaultTracker::defaultSinkChanged, + this, + &Pipewire::defaultAudioSinkChanged + ); + + QObject::connect( + &connection->defaults, + &PwDefaultTracker::defaultSourceChanged, + this, + &Pipewire::defaultAudioSourceChanged + ); + + QObject::connect( + &connection->defaults, + &PwDefaultTracker::defaultConfiguredSinkChanged, + this, + &Pipewire::defaultConfiguredAudioSinkChanged + ); + + QObject::connect( + &connection->defaults, + &PwDefaultTracker::defaultConfiguredSourceChanged, + this, + &Pipewire::defaultConfiguredAudioSourceChanged + ); } ObjectModel* Pipewire::nodes() { return &this->mNodes; } @@ -106,29 +129,23 @@ void Pipewire::onLinkGroupRemoved(QObject* object) { } PwNodeIface* Pipewire::defaultAudioSink() const { // NOLINT - auto* connection = PwConnection::instance(); - auto name = connection->defaults.defaultSink(); - - for (auto* node: connection->registry.nodes.values()) { - if (name == node->name) { - return PwNodeIface::instance(node); - } - } - - return nullptr; + auto* node = PwConnection::instance()->defaults.defaultSink(); + return PwNodeIface::instance(node); } PwNodeIface* Pipewire::defaultAudioSource() const { // NOLINT - auto* connection = PwConnection::instance(); - auto name = connection->defaults.defaultSource(); + auto* node = PwConnection::instance()->defaults.defaultSource(); + return PwNodeIface::instance(node); +} - for (auto* node: connection->registry.nodes.values()) { - if (name == node->name) { - return PwNodeIface::instance(node); - } - } +PwNodeIface* Pipewire::defaultConfiguredAudioSink() const { // NOLINT + auto* node = PwConnection::instance()->defaults.defaultConfiguredSink(); + return PwNodeIface::instance(node); +} - return nullptr; +PwNodeIface* Pipewire::defaultConfiguredAudioSource() const { // NOLINT + auto* node = PwConnection::instance()->defaults.defaultConfiguredSource(); + return PwNodeIface::instance(node); } PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; } @@ -305,6 +322,8 @@ QVariantMap PwNodeIface::properties() const { PwNodeAudioIface* PwNodeIface::audio() const { return this->audioIface; } PwNodeIface* PwNodeIface::instance(PwNode* node) { + if (node == nullptr) return nullptr; + auto v = node->property("iface"); if (v.canConvert()) { return v.value(); diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 2c2c1d60..261770c9 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -58,10 +58,30 @@ class Pipewire: public QObject { Q_PROPERTY(ObjectModel* links READ links CONSTANT); /// All pipewire link groups. Q_PROPERTY(ObjectModel* linkGroups READ linkGroups CONSTANT); - /// The default audio sink or `null`. + /// The default audio sink (output) or `null`. + /// + /// > [!INFO] When the default sink changes, this property may breifly become null. + /// > This depends on your hardware. Q_PROPERTY(PwNodeIface* defaultAudioSink READ defaultAudioSink NOTIFY defaultAudioSinkChanged); - /// The default audio source or `null`. + /// The default audio source (input) or `null`. + /// + /// > [!INFO] When the default source changes, this property may breifly become null. + /// > This depends on your hardware. Q_PROPERTY(PwNodeIface* defaultAudioSource READ defaultAudioSource NOTIFY defaultAudioSourceChanged); + /// The configured default audio sink (output) or `null`. + /// + /// This is not the same as @@defaultAudioSink. While @@defaultAudioSink is the + /// sink that will be used by applications, @@defaultConfiguredAudioSink is the + /// sink requested to be the default by quickshell or another configuration tool, + /// which might not exist or be valid. + Q_PROPERTY(PwNodeIface* defaultConfiguredAudioSink READ defaultConfiguredAudioSink NOTIFY defaultConfiguredAudioSinkChanged); + /// The configured default audio source (input) or `null`. + /// + /// This is not the same as @@defaultAudioSource. While @@defaultAudioSource is the + /// source that will be used by applications, @@defaultConfiguredAudioSource is the + /// source requested to be the default by quickshell or another configuration tool, + /// which might not exist or be valid. + Q_PROPERTY(PwNodeIface* defaultConfiguredAudioSource READ defaultConfiguredAudioSource NOTIFY defaultConfiguredAudioSourceChanged); // clang-format on QML_ELEMENT; QML_SINGLETON; @@ -72,13 +92,20 @@ public: [[nodiscard]] ObjectModel* nodes(); [[nodiscard]] ObjectModel* links(); [[nodiscard]] ObjectModel* linkGroups(); + [[nodiscard]] PwNodeIface* defaultAudioSink() const; [[nodiscard]] PwNodeIface* defaultAudioSource() const; + [[nodiscard]] PwNodeIface* defaultConfiguredAudioSink() const; + [[nodiscard]] PwNodeIface* defaultConfiguredAudioSource() const; + signals: void defaultAudioSinkChanged(); void defaultAudioSourceChanged(); + void defaultConfiguredAudioSinkChanged(); + void defaultConfiguredAudioSourceChanged(); + private slots: void onNodeAdded(PwNode* node); void onNodeRemoved(QObject* object); diff --git a/src/services/pipewire/registry.cpp b/src/services/pipewire/registry.cpp index 55cfb276..1370fa10 100644 --- a/src/services/pipewire/registry.cpp +++ b/src/services/pipewire/registry.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -143,7 +144,7 @@ void PwRegistry::onGlobal( meta->initProps(props); self->metadata.emplace(id, meta); - meta->bind(); + emit self->metadataAdded(meta); } else if (strcmp(type, PW_TYPE_INTERFACE_Link) == 0) { auto* link = new PwLink(); link->init(self, id, permissions); @@ -199,4 +200,14 @@ void PwRegistry::onLinkGroupDestroyed(QObject* object) { this->linkGroups.removeOne(group); } +PwNode* PwRegistry::findNodeByName(QStringView name) const { + if (name.isEmpty()) return nullptr; + + for (auto* node: this->nodes.values()) { + if (node->name == name) return node; + } + + return nullptr; +} + } // namespace qs::service::pipewire diff --git a/src/services/pipewire/registry.hpp b/src/services/pipewire/registry.hpp index 6ccd7148..c61773b2 100644 --- a/src/services/pipewire/registry.hpp +++ b/src/services/pipewire/registry.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -105,9 +106,8 @@ class PwBindableRef: public PwBindableObjectRef { public: explicit PwBindableRef(T* object = nullptr): PwBindableObjectRef(object) {} + T* object() { return static_cast(this->mObject); } void setObject(T* object) { this->PwBindableObjectRef::setObject(object); } - - T* object() { return this->mObject; } }; class PwRegistry @@ -127,17 +127,13 @@ public: PwCore* core = nullptr; + [[nodiscard]] PwNode* findNodeByName(QStringView name) const; + signals: void nodeAdded(PwNode* node); void linkAdded(PwLink* link); void linkGroupAdded(PwLinkGroup* group); - void metadataUpdate( - PwMetadata* owner, - quint32 subject, - const char* key, - const char* type, - const char* value - ); + void metadataAdded(PwMetadata* metadata); private slots: void onLinkGroupDestroyed(QObject* object); From fdc78ae16f88dacbf42119dd4d8599679909da46 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 24 Sep 2024 01:59:01 -0700 Subject: [PATCH 179/305] service/pipewire: add a way to set preferred default nodes --- src/services/pipewire/defaults.cpp | 69 +++++++++++++++++++++++++++++- src/services/pipewire/defaults.hpp | 6 +++ src/services/pipewire/qml.cpp | 8 ++++ src/services/pipewire/qml.hpp | 37 ++++++++++------ 4 files changed, 107 insertions(+), 13 deletions(-) diff --git a/src/services/pipewire/defaults.cpp b/src/services/pipewire/defaults.cpp index 86e50f50..7cc6d171 100644 --- a/src/services/pipewire/defaults.cpp +++ b/src/services/pipewire/defaults.cpp @@ -2,9 +2,12 @@ #include #include +#include +#include #include #include #include +#include #include #include @@ -63,7 +66,7 @@ void PwDefaultTracker::onMetadataProperty(const char* key, const char* type, con } else return; QString name; - if (strcmp(type, "Spa:String:JSON") == 0) { + if (type != nullptr && value != nullptr && strcmp(type, "Spa:String:JSON") == 0) { auto failed = true; auto iter = std::array(); spa_json_init(&iter[0], value, strlen(value)); @@ -138,6 +141,70 @@ void PwDefaultTracker::onNodeDestroyed(QObject* node) { } } +void PwDefaultTracker::changeConfiguredSink(PwNode* node) { + if (node != nullptr) { + if (!node->isSink) { + qCCritical(logDefaults) << "Cannot change default sink to a node that is not a sink."; + return; + } + + this->changeConfiguredSinkName(node->name); + } else { + this->changeConfiguredSinkName(""); + } +} + +void PwDefaultTracker::changeConfiguredSinkName(const QString& sink) { + if (sink == this->mDefaultConfiguredSinkName) return; + + if (this->setConfiguredDefault("default.configured.audio.sink", sink)) { + this->mDefaultConfiguredSinkName = sink; + qCInfo(logDefaults) << "Set default configured sink to" << sink; + } +} + +void PwDefaultTracker::changeConfiguredSource(PwNode* node) { + if (node != nullptr) { + if (node->isSink) { + qCCritical(logDefaults) << "Cannot change default source to a node that is not a source."; + return; + } + + this->changeConfiguredSourceName(node->name); + } else { + this->changeConfiguredSourceName(""); + } +} + +void PwDefaultTracker::changeConfiguredSourceName(const QString& source) { + if (source == this->mDefaultConfiguredSourceName) return; + + if (this->setConfiguredDefault("default.configured.audio.source", source)) { + this->mDefaultConfiguredSourceName = source; + qCInfo(logDefaults) << "Set default configured source to" << source; + } +} + +bool PwDefaultTracker::setConfiguredDefault(const char* key, const QString& value) { + auto* meta = this->defaultsMetadata.object(); + + if (!meta || !meta->proxy()) { + qCCritical(logDefaults) << "Cannot set default node as metadata is not ready."; + return false; + } + + if (value.isEmpty()) { + meta->setProperty(key, "Spa:String:JSON", nullptr); + } else { + // Spa json is a superset of json so we can avoid the awful spa json api when serializing. + auto json = QJsonDocument({{"name", value}}).toJson(QJsonDocument::Compact); + + meta->setProperty(key, "Spa:String:JSON", json.toStdString().c_str()); + } + + return true; +} + void PwDefaultTracker::setDefaultSink(PwNode* node) { if (node == this->mDefaultSink) return; qCInfo(logDefaults) << "Default sink changed to" << node; diff --git a/src/services/pipewire/defaults.hpp b/src/services/pipewire/defaults.hpp index 9544514e..f3a8e3f9 100644 --- a/src/services/pipewire/defaults.hpp +++ b/src/services/pipewire/defaults.hpp @@ -18,9 +18,13 @@ public: [[nodiscard]] PwNode* defaultConfiguredSink() const; [[nodiscard]] const QString& defaultConfiguredSinkName() const; + void changeConfiguredSink(PwNode* node); + void changeConfiguredSinkName(const QString& sink); [[nodiscard]] PwNode* defaultConfiguredSource() const; [[nodiscard]] const QString& defaultConfiguredSourceName() const; + void changeConfiguredSource(PwNode* node); + void changeConfiguredSourceName(const QString& source); signals: void defaultSinkChanged(); @@ -54,6 +58,8 @@ private: void setDefaultConfiguredSource(PwNode* node); void setDefaultConfiguredSourceName(const QString& name); + bool setConfiguredDefault(const char* key, const QString& value); + PwRegistry* registry; PwBindableRef defaultsMetadata; diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp index aad58a57..a8186ea3 100644 --- a/src/services/pipewire/qml.cpp +++ b/src/services/pipewire/qml.cpp @@ -143,11 +143,19 @@ PwNodeIface* Pipewire::defaultConfiguredAudioSink() const { // NOLINT return PwNodeIface::instance(node); } +void Pipewire::setDefaultConfiguredAudioSink(PwNodeIface* node) { + PwConnection::instance()->defaults.changeConfiguredSink(node ? node->node() : nullptr); +} + PwNodeIface* Pipewire::defaultConfiguredAudioSource() const { // NOLINT auto* node = PwConnection::instance()->defaults.defaultConfiguredSource(); return PwNodeIface::instance(node); } +void Pipewire::setDefaultConfiguredAudioSource(PwNodeIface* node) { + PwConnection::instance()->defaults.changeConfiguredSource(node ? node->node() : nullptr); +} + PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; } void PwNodeLinkTracker::setNode(PwNodeIface* node) { diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 261770c9..80049ddc 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -60,28 +60,38 @@ class Pipewire: public QObject { Q_PROPERTY(ObjectModel* linkGroups READ linkGroups CONSTANT); /// The default audio sink (output) or `null`. /// + /// This is the default sink currently in use by pipewire, and the one applications + /// are currently using. + /// + /// To set the default sink, use @@preferredDefaultAudioSink. + /// /// > [!INFO] When the default sink changes, this property may breifly become null. /// > This depends on your hardware. Q_PROPERTY(PwNodeIface* defaultAudioSink READ defaultAudioSink NOTIFY defaultAudioSinkChanged); /// The default audio source (input) or `null`. /// + /// This is the default source currently in use by pipewire, and the one applications + /// are currently using. + /// + /// To set the default source, use @@preferredDefaultAudioSource. + /// /// > [!INFO] When the default source changes, this property may breifly become null. /// > This depends on your hardware. Q_PROPERTY(PwNodeIface* defaultAudioSource READ defaultAudioSource NOTIFY defaultAudioSourceChanged); - /// The configured default audio sink (output) or `null`. + /// The preferred default audio sink (output) or `null`. /// - /// This is not the same as @@defaultAudioSink. While @@defaultAudioSink is the - /// sink that will be used by applications, @@defaultConfiguredAudioSink is the - /// sink requested to be the default by quickshell or another configuration tool, - /// which might not exist or be valid. - Q_PROPERTY(PwNodeIface* defaultConfiguredAudioSink READ defaultConfiguredAudioSink NOTIFY defaultConfiguredAudioSinkChanged); - /// The configured default audio source (input) or `null`. + /// This is a hint to pipewire telling it which sink should be the default when possible. + /// @@defaultAudioSink may differ when it is not possible for pipewire to pick this node. /// - /// This is not the same as @@defaultAudioSource. While @@defaultAudioSource is the - /// source that will be used by applications, @@defaultConfiguredAudioSource is the - /// source requested to be the default by quickshell or another configuration tool, - /// which might not exist or be valid. - Q_PROPERTY(PwNodeIface* defaultConfiguredAudioSource READ defaultConfiguredAudioSource NOTIFY defaultConfiguredAudioSourceChanged); + /// See @@defaultAudioSink for the current default sink, regardless of preference. + Q_PROPERTY(PwNodeIface* preferredDefaultAudioSink READ defaultConfiguredAudioSink WRITE setDefaultConfiguredAudioSink NOTIFY defaultConfiguredAudioSinkChanged); + /// The preferred default audio source (input) or `null`. + /// + /// This is a hint to pipewire telling it which source should be the default when possible. + /// @@defaultAudioSource may differ when it is not possible for pipewire to pick this node. + /// + /// See @@defaultAudioSource for the current default source, regardless of preference. + Q_PROPERTY(PwNodeIface* preferredDefaultAudioSource READ defaultConfiguredAudioSource WRITE setDefaultConfiguredAudioSource NOTIFY defaultConfiguredAudioSourceChanged); // clang-format on QML_ELEMENT; QML_SINGLETON; @@ -97,7 +107,10 @@ public: [[nodiscard]] PwNodeIface* defaultAudioSource() const; [[nodiscard]] PwNodeIface* defaultConfiguredAudioSink() const; + static void setDefaultConfiguredAudioSink(PwNodeIface* node); + [[nodiscard]] PwNodeIface* defaultConfiguredAudioSource() const; + static void setDefaultConfiguredAudioSource(PwNodeIface* node); signals: void defaultAudioSinkChanged(); From fbaec141c01e8b3376850bd98aabeadf9fad0fcf Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 24 Sep 2024 01:59:38 -0700 Subject: [PATCH 180/305] service/pipewire: improve documentation --- src/services/pipewire/qml.hpp | 48 +++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 80049ddc..2366a373 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -52,11 +52,31 @@ private: class Pipewire: public QObject { Q_OBJECT; // clang-format off - /// All pipewire nodes. + /// All nodes present in pipewire. + /// + /// This list contains every node on the system. + /// To find a useful subset, filtering with the following properties may be helpful: + /// - @@PwNode.isStream - if the node is an application or hardware device. + /// - @@PwNode.isSink - if the node is a sink or source. + /// - @@PwNode.audio - if non null the node is an audio node. Q_PROPERTY(ObjectModel* nodes READ nodes CONSTANT); - /// All pipewire links. + /// All links present in pipewire. + /// + /// Links connect pipewire nodes to each other, and can be used to determine + /// their relationship. + /// + /// If you already have a node you want to check for connections to, + /// use @@PwNodeLinkTracker instead of filtering this list. + /// + /// > [!INFO] Multiple links may exist between the same nodes. See @@linkGroups + /// > for a deduplicated list containing only one entry per link between nodes. Q_PROPERTY(ObjectModel* links READ links CONSTANT); - /// All pipewire link groups. + /// All link groups present in pipewire. + /// + /// The same as @@links but deduplicated. + /// + /// If you already have a node you want to check for connections to, + /// use @@PwNodeLinkTracker instead of filtering this list. Q_PROPERTY(ObjectModel* linkGroups READ linkGroups CONSTANT); /// The default audio sink (output) or `null`. /// @@ -231,7 +251,7 @@ class PwNodeIface: public PwObjectIface { Q_OBJECT; /// The pipewire object id of the node. /// - /// Mainly useful for debugging. you can inspect the node directly + /// Mainly useful for debugging. You can inspect the node directly /// with `pw-cli i `. Q_PROPERTY(quint32 id READ id CONSTANT); /// The node's name, corrosponding to the object's `node.name` property. @@ -247,7 +267,8 @@ class PwNodeIface: public PwObjectIface { /// If `true`, then the node accepts audio input from other nodes, /// if `false` the node outputs audio to other nodes. Q_PROPERTY(bool isSink READ isSink CONSTANT); - /// If `true` then the node is likely to be a program, if false it is liekly to be hardware. + /// If `true` then the node is likely to be a program, if `false` it is likely to be + /// a hardware device. Q_PROPERTY(bool isStream READ isStream CONSTANT); /// The property set present on the node, as an object containing key-value pairs. /// You can inspect this directly with `pw-cli i `. @@ -263,6 +284,9 @@ class PwNodeIface: public PwObjectIface { /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). Q_PROPERTY(QVariantMap properties READ properties NOTIFY propertiesChanged); /// Extra information present only if the node sends or receives audio. + /// + /// The presence or absence of this property can be used to determine if a node + /// manages audio, regardless of if it is bound. If non null, the node is an audio node. Q_PROPERTY(PwNodeAudioIface* audio READ audio CONSTANT); QML_NAMED_ELEMENT(PwNode); QML_UNCREATABLE("PwNodes cannot be created directly"); @@ -370,11 +394,19 @@ private: }; ///! Binds pipewire objects. -/// If the object list of at least one PwObjectTracker contains a given pipewire object, -/// it will become *bound* and you will be able to interact with bound-only properties. +/// PwObjectTracker binds every node given in its @@objects list. +/// +/// #### Object Binding +/// By default, pipewire objects are unbound. Unbound objects only have a subset of +/// information available for use or modification. **Binding an object makes all of its +/// properties available for use or modification if applicable.** +/// +/// Properties that require their object be bound to use are clearly marked. You do not +/// need to bind the object unless mentioned in the description of the property you +/// want to use. class PwObjectTracker: public QObject { Q_OBJECT; - /// The list of objects to bind. + /// The list of objects to bind. May contain nulls. Q_PROPERTY(QList objects READ objects WRITE setObjects NOTIFY objectsChanged); QML_ELEMENT; From 3ed39b2a798419a168e5c79a2db9f7ee20de70fa Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 26 Sep 2024 15:52:31 -0700 Subject: [PATCH 181/305] service/pipewire: fix metadata permission checks --- src/services/pipewire/defaults.cpp | 7 +++++++ src/services/pipewire/metadata.cpp | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/services/pipewire/defaults.cpp b/src/services/pipewire/defaults.cpp index 7cc6d171..4851f27b 100644 --- a/src/services/pipewire/defaults.cpp +++ b/src/services/pipewire/defaults.cpp @@ -193,6 +193,13 @@ bool PwDefaultTracker::setConfiguredDefault(const char* key, const QString& valu return false; } + if (!meta->hasSetPermission()) { + qCCritical(logDefaults + ) << "Cannot set default node as write+execute permissions are missing for" + << meta; + return false; + } + if (value.isEmpty()) { meta->setProperty(key, "Spa:String:JSON", nullptr); } else { diff --git a/src/services/pipewire/metadata.cpp b/src/services/pipewire/metadata.cpp index 582e9ac6..930725c3 100644 --- a/src/services/pipewire/metadata.cpp +++ b/src/services/pipewire/metadata.cpp @@ -2,13 +2,13 @@ #include #include +#include #include #include #include #include #include #include -#include #include #include "registry.hpp" @@ -54,7 +54,7 @@ int PwMetadata::onProperty( } bool PwMetadata::hasSetPermission() const { - return (this->perms & SPA_PARAM_INFO_WRITE) == SPA_PARAM_INFO_WRITE; + return (this->perms & (PW_PERM_W | PW_PERM_X)) == (PW_PERM_W | PW_PERM_X); } void PwMetadata::setProperty(const char* key, const char* type, const char* value) { @@ -64,7 +64,8 @@ void PwMetadata::setProperty(const char* key, const char* type, const char* valu } if (!this->hasSetPermission()) { - qCCritical(logMeta) << "Tried to change property of" << this << "which is read-only."; + qCCritical(logMeta) << "Tried to change property of" << this + << "which is missing write+execute permissions."; } pw_metadata_set_property(this->proxy(), PW_ID_CORE, key, type, value); From 8e40112d143f805a3fdcc967c50500e2c035ff12 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 6 Oct 2024 00:57:19 -0700 Subject: [PATCH 182/305] service/pipewire: ignore metadata updates with null keys Fixes #6 --- src/services/pipewire/defaults.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/pipewire/defaults.cpp b/src/services/pipewire/defaults.cpp index 4851f27b..cd018f9f 100644 --- a/src/services/pipewire/defaults.cpp +++ b/src/services/pipewire/defaults.cpp @@ -51,7 +51,8 @@ void PwDefaultTracker::onMetadataProperty(const char* key, const char* type, con qCDebug(logDefaults).nospace() << "Got default metadata update for " << key << ": " << QString(value); - if (strcmp(key, "default.audio.sink") == 0) { + if (key == nullptr) return; // NOLINT(bugprone-branch-clone) + else if (strcmp(key, "default.audio.sink") == 0) { nodeSetter = &PwDefaultTracker::setDefaultSink; nameSetter = &PwDefaultTracker::setDefaultSinkName; } else if (strcmp(key, "default.audio.source") == 0) { From 23f59ec4c395b68608307384b6b239ea7d79c58e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 15 Oct 2024 23:01:14 -0700 Subject: [PATCH 183/305] crash: add build configuration and distributor information Also adds distributor to --version and build configuration to --version --verbose --- .github/ISSUE_TEMPLATE/crash.yml | 14 +++- BUILD.md | 32 ++++++--- CMakeLists.txt | 116 ++++++++++++++++--------------- default.nix | 4 +- src/core/CMakeLists.txt | 10 ++- src/core/build.hpp.in | 6 ++ src/core/main.cpp | 18 ++++- src/crash/interface.cpp | 2 +- src/crash/main.cpp | 34 ++++++--- 9 files changed, 155 insertions(+), 81 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml index 13dcd33d..c8b4804e 100644 --- a/.github/ISSUE_TEMPLATE/crash.yml +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -33,7 +33,7 @@ body: attributes: label: Minidump description: | - Attach `minidump.dmp` here. If it is too big to upload, compress it. + Attach `minidump.dmp.log` here. If it is too big to upload, compress it. You may skip this step if quickshell crashed while processing a password or other sensitive information. If you skipped it write why instead. @@ -44,7 +44,7 @@ body: attributes: label: Log file description: | - Attach `log.qslog` here. If it is too big to upload, compress it. + 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: @@ -70,3 +70,13 @@ body: in the crash reporter. 2. Once it loads, type `bt -full` (then enter) 3. Copy the output and attach it as a file or in a spoiler. + - type: textarea + id: exe + attributes: + label: Executable + description: | + If the crash folder contains a executable.txt file, upload it here. If not you can ignore this field. + If it is too big to upload, compress it. + + Note: executable.txt is the quickshell binary. It has a .txt extension due to github's limitations on + filetypes. diff --git a/BUILD.md b/BUILD.md index f32a2335..4650d1ff 100644 --- a/BUILD.md +++ b/BUILD.md @@ -2,6 +2,29 @@ Instructions for building from source and distro packagers. We highly recommend distro packagers read through this page fully. +## Packaging +If you are packaging quickshell for official or unofficial distribution channels, +such as a distro package repository, user repository, or other shared build location, +please set the following CMake flags. + +`-DDISTRIBUTOR="your distribution platform"` + +Please make this descriptive enough to identify your specific package, for example: +- `Official Nix Flake` +- `AUR (quickshell-git)` +- `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). + ## Dependencies Quickshell has a set of base dependencies you will always need, names vary by distro: @@ -18,12 +41,6 @@ At least Qt 6.6 is required. All features are enabled by default and some have their own dependencies. -### QML Library -If you wish to use a linter or similar tools, you will need the QML Modules for it -to pick up on the types. - -To disable: `-DINSTALL_QML_LIB=OFF` - ### Crash Reporter The crash reporter catches crashes, restarts quickshell when it crashes, and collects useful crash information in one place. Leaving this enabled will @@ -169,9 +186,6 @@ or quickshell will fail to build. Additionally, note that clang builds much faster than gcc if you care. -You may disable debug information but it's only a couple megabytes and is extremely helpful -for helping us fix problems when they do arise. - #### Building ```sh $ cmake --build build diff --git a/CMakeLists.txt b/CMakeLists.txt index 5fe5387e..c0b7e574 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,58 +5,64 @@ set(QT_MIN_VERSION "6.6.0") set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -option(BUILD_TESTING "Build tests" OFF) -option(ASAN "Enable ASAN" OFF) # note: better output with gcc than clang -option(FRAME_POINTERS "Always keep frame pointers" ${ASAN}) +set(QS_BUILD_OPTIONS "") -option(INSTALL_QML_LIB "Installing the QML lib" ON) -option(CRASH_REPORTER "Enable the crash reporter" ON) -option(USE_JEMALLOC "Use jemalloc over the system malloc implementation" ON) -option(SOCKETS "Enable unix socket support" ON) -option(WAYLAND "Enable wayland support" ON) -option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON) -option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON) -option(WAYLAND_TOPLEVEL_MANAGEMENT "Support the zwlr_foreign_toplevel_management_v1 wayland protocol" ON) -option(X11 "Enable X11 support" ON) -option(HYPRLAND "Support hyprland specific features" ON) -option(HYPRLAND_IPC "Hyprland IPC" ON) -option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON) -option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON) -option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) -option(SERVICE_PIPEWIRE "PipeWire service" ON) -option(SERVICE_MPRIS "Mpris service" ON) -option(SERVICE_PAM "Pam service" ON) -option(SERVICE_GREETD "Greet service" ON) -option(SERVICE_UPOWER "UPower service" ON) -option(SERVICE_NOTIFICATIONS "Notification server" ON) +function(boption VAR NAME DEFAULT) + cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "") + + option(${VAR} ${NAME} ${DEFAULT}) + + set(STATUS "${VAR}_status") + set(EFFECTIVE "${VAR}_effective") + set(${STATUS} ${${VAR}}) + set(${EFFECTIVE} ${${VAR}}) + + if (${${VAR}} AND DEFINED arg_REQUIRES) + set(REQUIRED_EFFECTIVE "${arg_REQUIRES}_effective") + if (NOT ${${REQUIRED_EFFECTIVE}}) + set(${STATUS} "OFF (Requires ${arg_REQUIRES})") + set(${EFFECTIVE} OFF) + endif() + endif() + + set(${EFFECTIVE} "${${EFFECTIVE}}" PARENT_SCOPE) + + message(STATUS " ${NAME}: ${${STATUS}}") + + string(APPEND QS_BUILD_OPTIONS "\\n ${NAME}: ${${STATUS}}") + set(QS_BUILD_OPTIONS "${QS_BUILD_OPTIONS}" PARENT_SCOPE) +endfunction() + +set(DISTRIBUTOR "Unset" CACHE STRING "Distributor") +string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}") message(STATUS "Quickshell configuration") -message(STATUS " QML lib installation: ${INSTALL_QML_LIB}") -message(STATUS " Crash reporter: ${CRASH_REPORTER}") -message(STATUS " Jemalloc: ${USE_JEMALLOC}") -message(STATUS " Build tests: ${BUILD_TESTING}") -message(STATUS " Sockets: ${SOCKETS}") -message(STATUS " Wayland: ${WAYLAND}") -if (WAYLAND) - message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}") - message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}") - message(STATUS " Toplevel Management: ${WAYLAND_TOPLEVEL_MANAGEMENT}") -endif () -message(STATUS " X11: ${X11}") -message(STATUS " Services") -message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") -message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") -message(STATUS " Mpris: ${SERVICE_MPRIS}") -message(STATUS " Pam: ${SERVICE_PAM}") -message(STATUS " Greetd: ${SERVICE_GREETD}") -message(STATUS " UPower: ${SERVICE_UPOWER}") -message(STATUS " Notifications: ${SERVICE_NOTIFICATIONS}") -message(STATUS " Hyprland: ${HYPRLAND}") -if (HYPRLAND) - message(STATUS " IPC: ${HYPRLAND_IPC}") - message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}") - message(STATUS " Global Shortcuts: ${HYPRLAND_GLOBAL_SHORTCUTS}") -endif() +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 +boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN}) + +boption(CRASH_REPORTER "Crash Handling" ON) +boption(USE_JEMALLOC "Use jemalloc" ON) +boption(SOCKETS "Unix Sockets" ON) +boption(WAYLAND "Wayland" ON) +boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND) +boption(WAYLAND_SESSION_LOCK " Session Lock" ON REQUIRES WAYLAND) +boption(WAYLAND_TOPLEVEL_MANAGEMENT " Foreign Toplevel Management" ON REQUIRES WAYLAND) +boption(HYPRLAND " Hyprland" ON REQUIRES WAYLAND) +boption(HYPRLAND_IPC " Hyprland IPC" ON REQUIRES HYPRLAND) +boption(HYPRLAND_GLOBAL_SHORTCUTS " Hyprland Global Shortcuts" ON REQUIRES HYPRLAND) +boption(HYPRLAND_FOCUS_GRAB " Hyprland Focus Grabbing" ON REQUIRES HYPRLAND) +boption(X11 "X11" ON) +boption(SERVICE_STATUS_NOTIFIER "System Tray" ON) +boption(SERVICE_PIPEWIRE "PipeWire" ON) +boption(SERVICE_MPRIS "Mpris" ON) +boption(SERVICE_PAM "Pam" ON) +boption(SERVICE_GREETD "Greetd" ON) +boption(SERVICE_UPOWER "UPower" ON) +boption(SERVICE_NOTIFICATIONS "Notifications" ON) if (NOT DEFINED GIT_REVISION) execute_process( @@ -157,13 +163,11 @@ if (USE_JEMALLOC) target_link_libraries(quickshell PRIVATE ${JEMALLOC_LIBRARIES}) endif() -if (INSTALL_QML_LIB) - install( - DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules/ - DESTINATION ${CMAKE_INSTALL_LIBDIR}/qt-6/qml - FILES_MATCHING PATTERN "*" - ) -endif() +install( + DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules/ + DESTINATION ${CMAKE_INSTALL_LIBDIR}/qt-6/qml + FILES_MATCHING PATTERN "*" +) install(CODE " execute_process( diff --git a/default.nix b/default.nix index 3016a313..88c0e3b5 100644 --- a/default.nix +++ b/default.nix @@ -37,7 +37,6 @@ withPipewire ? true, withPam ? true, withHyprland ? true, - withQMLLib ? true, }: buildStdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; version = "0.1.0"; @@ -71,6 +70,8 @@ cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; cmakeFlags = [ + (lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake") + (lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true) (lib.cmakeFeature "GIT_REVISION" gitRev) (lib.cmakeBool "CRASH_REPORTER" withCrashReporter) (lib.cmakeBool "USE_JEMALLOC" withJemalloc) @@ -78,7 +79,6 @@ (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) (lib.cmakeBool "SERVICE_PAM" withPam) (lib.cmakeBool "HYPRLAND" withHyprland) - (lib.cmakeBool "INSTALL_QML_LIB" withQMLLib) ]; # How to get debuginfo in gdb from a release build: diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 75c16537..811965e7 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -52,8 +52,16 @@ else() set(CRASH_REPORTER_DEF 0) endif() +if (DISTRIBUTOR_DEBUGINFO_AVAILABLE) + set(DEBUGINFO_AVAILABLE 1) +else() + set(DEBUGINFO_AVAILABLE 0) +endif() + add_library(quickshell-build INTERFACE) -configure_file(build.hpp.in build.hpp) + +configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES) + target_include_directories(quickshell-build INTERFACE ${CMAKE_CURRENT_BINARY_DIR}) target_link_libraries(quickshell-core PRIVATE quickshell-build) diff --git a/src/core/build.hpp.in b/src/core/build.hpp.in index ecf5dfc4..075abd17 100644 --- a/src/core/build.hpp.in +++ b/src/core/build.hpp.in @@ -2,5 +2,11 @@ // NOLINTBEGIN #define GIT_REVISION "@GIT_REVISION@" +#define DISTRIBUTOR "@DISTRIBUTOR@" +#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@ #define CRASH_REPORTER @CRASH_REPORTER_DEF@ +#define BUILD_TYPE "@CMAKE_BUILD_TYPE@" +#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)" +#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@" +#define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@" // NOLINTEND diff --git a/src/core/main.cpp b/src/core/main.cpp index 57bdfb85..287af3b1 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include #include #include +#include #include #include "../io/ipccomm.hpp" @@ -425,7 +427,21 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { } if (state.misc.printVersion) { - qCInfo(logBare).noquote() << "quickshell pre-release, revision" << GIT_REVISION; + qCInfo(logBare).noquote().nospace() << "quickshell pre-release, revision " << GIT_REVISION + << ", distributed by: " << DISTRIBUTOR; + + if (state.log.verbosity > 1) { + qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; + qCInfo(logBare).noquote() << "Runtime Qt Version:" << qVersion(); + qCInfo(logBare).noquote() << "Compiler:" << COMPILER; + qCInfo(logBare).noquote() << "Compile Flags:" << COMPILE_FLAGS; + } + + if (state.log.verbosity > 0) { + qCInfo(logBare).noquote() << "\nBuild Type:" << BUILD_TYPE; + qCInfo(logBare).noquote() << "Build configuration:"; + qCInfo(logBare).noquote().nospace() << BUILD_CONFIGURATION; + } } else if (*state.subcommand.log) { return readLogFile(state); } else if (*state.subcommand.list) { diff --git a/src/crash/interface.cpp b/src/crash/interface.cpp index 3d296580..7691b260 100644 --- a/src/crash/interface.cpp +++ b/src/crash/interface.cpp @@ -54,7 +54,7 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid) mainLayout->addWidget(new ReportLabel( "Github:", - "https://github.com/outfoxxed/quickshell/issues/new?template=crash.yml", + "https://github.com/quickshell-mirror/quickshell/issues/new?template=crash.yml", this )); diff --git a/src/crash/main.cpp b/src/crash/main.cpp index 9f56d894..08c38892 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include #include @@ -111,39 +113,53 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { auto logFd = qEnvironmentVariable("__QUICKSHELL_CRASH_LOG_FD").toInt(); qCDebug(logCrashReporter) << "Saving minidump from fd" << dumpFd; - auto dumpDupStatus = tryDup(dumpFd, crashDir.filePath("minidump.dmp")); + auto dumpDupStatus = tryDup(dumpFd, crashDir.filePath("minidump.dmp.log")); if (dumpDupStatus != 0) { qCCritical(logCrashReporter) << "Failed to write minidump:" << dumpDupStatus; } qCDebug(logCrashReporter) << "Saving log from fd" << logFd; - auto logDupStatus = tryDup(logFd, crashDir.filePath("log.qslog")); + auto logDupStatus = tryDup(logFd, 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")); if (!extraInfoFile.open(QFile::WriteOnly)) { qCCritical(logCrashReporter) << "Failed to open crash info file for writing."; } else { auto stream = QTextStream(&extraInfoFile); - stream << "===== Quickshell Crash =====\n"; + stream << "===== Build Information =====\n"; stream << "Git Revision: " << GIT_REVISION << '\n'; + stream << "Buildtime Qt Version: " << QT_VERSION_STR << "\n"; + stream << "Build Type: " << BUILD_TYPE << '\n'; + stream << "Compiler: " << COMPILER << '\n'; + stream << "Complie Flags: " << COMPILE_FLAGS << "\n\n"; + stream << "Build configuration:\n" << BUILD_CONFIGURATION << "\n"; + + stream << "\n===== Runtime Information =====\n"; + stream << "Runtime Qt Version: " << qVersion() << '\n'; stream << "Crashed process ID: " << crashProc << '\n'; stream << "Run ID: " << instance.instanceId << '\n'; - - stream << "\n===== Shell Information =====\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"; - stream << "Qt Version: " << QT_VERSION_STR << "\n\n"; - + stream << "\n===== System Information =====\n\n"; stream << "/etc/os-release:"; auto osReleaseFile = QFile("/etc/os-release"); if (osReleaseFile.open(QFile::ReadOnly)) { @@ -156,7 +172,7 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { stream << "/etc/lsb-release:"; auto lsbReleaseFile = QFile("/etc/lsb-release"); if (lsbReleaseFile.open(QFile::ReadOnly)) { - stream << '\n' << lsbReleaseFile.readAll() << '\n'; + stream << '\n' << lsbReleaseFile.readAll(); lsbReleaseFile.close(); } else { stream << "FAILED TO OPEN\n"; From 89d04f34a54092db65ba7128bc3e259e99494d7a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 16 Oct 2024 00:00:13 -0700 Subject: [PATCH 184/305] build: find waylandscanner and qtwaylandscanner from imported target Removes the QTWAYLANDSCANNER env hack. --- default.nix | 2 -- shell.nix | 1 - src/wayland/CMakeLists.txt | 54 ++++++++++++++++++++++++-------------- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/default.nix b/default.nix index 88c0e3b5..f7352267 100644 --- a/default.nix +++ b/default.nix @@ -65,8 +65,6 @@ ++ lib.optional withPam pam ++ lib.optional withPipewire pipewire; - QTWAYLANDSCANNER = lib.optionalString withWayland "${qt6.qtwayland}/libexec/qtwaylandscanner"; - cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; cmakeFlags = [ diff --git a/shell.nix b/shell.nix index 07b5b57d..0182a0d3 100644 --- a/shell.nix +++ b/shell.nix @@ -21,7 +21,6 @@ in pkgs.mkShell.override { stdenv = quickshell.stdenv; } { ]; TIDYFOX = "${tidyfox}/lib/libtidyfox.so"; - QTWAYLANDSCANNER = quickshell.QTWAYLANDSCANNER; shellHook = '' export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index d8702b7a..568edc42 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -1,25 +1,17 @@ find_package(PkgConfig REQUIRED) +find_package(WaylandScanner REQUIRED) pkg_check_modules(wayland REQUIRED IMPORTED_TARGET wayland-client wayland-protocols) -find_package(Qt6 REQUIRED COMPONENTS WaylandClient) - # wayland protocols -if (DEFINED ENV{QTWAYLANDSCANNER}) - set(qtwaylandscanner $ENV{QTWAYLANDSCANNER}) -else() - find_program(qtwaylandscanner NAMES qtwaylandscanner) +if(NOT TARGET Wayland::Scanner) + message(FATAL_ERROR "Wayland::Scanner target not found. You might be missing the WaylandScanner CMake package.") endif() -if (qtwaylandscanner STREQUAL "qtwaylandscanner-NOTFOUND") - message(FATAL_ERROR "qtwaylandscanner not found. Set the QTWAYLANDSCANNER environment variable to specify its path explicity.") +if(NOT TARGET Qt6::qtwaylandscanner) + message(FATAL_ERROR "qtwaylandscanner executable not found. Most likely there is an issue with your Qt installation.") endif() -message(STATUS "Found qtwaylandscanner at ${qtwaylandscanner}") - -find_program(waylandscanner NAMES wayland-scanner) -message(STATUS "Found wayland-scanner at ${waylandscanner}") - execute_process( COMMAND pkg-config --variable=pkgdatadir wayland-protocols OUTPUT_VARIABLE WAYLAND_PROTOCOLS @@ -32,14 +24,38 @@ function (wl_proto target name path) set(PROTO_BUILD_PATH ${CMAKE_CURRENT_BINARY_DIR}/wl-proto/${name}) make_directory(${PROTO_BUILD_PATH}) - execute_process(COMMAND ${waylandscanner} client-header ${path} ${PROTO_BUILD_PATH}/wayland-${name}-client-protocol.h) - execute_process(COMMAND ${waylandscanner} private-code ${path} ${PROTO_BUILD_PATH}/wayland-${name}.c) - execute_process(COMMAND ${qtwaylandscanner} client-header ${path} OUTPUT_FILE ${PROTO_BUILD_PATH}/qwayland-${name}.h) - execute_process(COMMAND ${qtwaylandscanner} client-code ${path} OUTPUT_FILE ${PROTO_BUILD_PATH}/qwayland-${name}.cpp) + set(WS_CLIENT_HEADER "${PROTO_BUILD_PATH}/wayland-${name}-client-protocol.h") + set(WS_CLIENT_CODE "${PROTO_BUILD_PATH}/wayland-${name}.c") + set(QWS_CLIENT_HEADER "${PROTO_BUILD_PATH}/qwayland-${name}.h") + set(QWS_CLIENT_CODE "${PROTO_BUILD_PATH}/qwayland-${name}.cpp") + + add_custom_command( + OUTPUT "${WS_CLIENT_HEADER}" + COMMAND Wayland::Scanner client-header "${path}" "${WS_CLIENT_HEADER}" + DEPENDS Wayland::Scanner "${path}" + ) + + add_custom_command( + OUTPUT "${WS_CLIENT_CODE}" + COMMAND Wayland::Scanner private-code "${path}" "${WS_CLIENT_CODE}" + DEPENDS Wayland::Scanner "${path}" + ) + + add_custom_command( + OUTPUT "${QWS_CLIENT_HEADER}" + COMMAND Qt6::qtwaylandscanner client-header "${path}" > "${QWS_CLIENT_HEADER}" + DEPENDS Qt6::qtwaylandscanner "${path}" + ) + + add_custom_command( + OUTPUT "${QWS_CLIENT_CODE}" + COMMAND Qt6::qtwaylandscanner client-code "${path}" > "${QWS_CLIENT_CODE}" + DEPENDS Qt6::qtwaylandscanner "${path}" + ) add_library(wl-proto-${name} - ${PROTO_BUILD_PATH}/wayland-${name}.c - ${PROTO_BUILD_PATH}/qwayland-${name}.cpp + ${WS_CLIENT_HEADER} ${WS_CLIENT_CODE} + ${QWS_CLIENT_HEADER} ${QWS_CLIENT_CODE} ) target_include_directories(wl-proto-${name} INTERFACE ${PROTO_BUILD_PATH}) From 4c2d7a7e41fabefa1c4519ad6adebcad09808fbe Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 17 Oct 2024 13:15:09 -0700 Subject: [PATCH 185/305] crash: print warning messages for run/buildtime Qt version mismatch --- README.md | 5 +++++ src/core/main.cpp | 23 +++++++++++++++++++++++ src/crash/interface.cpp | 33 ++++++++++++++++++++++++++++----- src/crash/main.cpp | 2 +- 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 726bf22a..82f912fd 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,11 @@ It is not managed by us and should be looked over before use. [AUR package]: https://aur.archlinux.org/packages/quickshell +> [!CAUTION] +> The AUR provides no way to force the quickshell package to rebuild when the Qt version changes. +> If you experience crashes after updating Qt, please try rebuilding Quickshell against the +> current Qt version before opening an issue. + ## Fedora (COPR) Quickshell has a third party [Fedora COPR package] available under the same name. It is not managed by us and should be looked over before use. diff --git a/src/core/main.cpp b/src/core/main.cpp index 287af3b1..cc57cc61 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -176,6 +177,7 @@ struct CommandState { } subcommand; struct { + bool checkCompat = false; bool printVersion = false; bool killAll = false; bool noDuplicate = false; @@ -290,6 +292,8 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { addDebugOptions(&cli); { + cli.add_option_group("")->add_flag("--private-check-compat", state.misc.checkCompat); + cli.add_flag("-V,--version", state.misc.printVersion) ->description("Print quickshell's version and exit."); @@ -378,6 +382,18 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { CLI11_PARSE(cli, argc, argv); + if (state.misc.checkCompat) { + if (strcmp(qVersion(), QT_VERSION_STR) != 0) { + 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"; + return 1; + } + + return 0; + } + // Has to happen before extra threads are spawned. if (state.misc.daemonize) { auto closepipes = std::array(); @@ -451,6 +467,13 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { } else if (*state.subcommand.msg) { return msgInstance(state); } else { + if (strcmp(qVersion(), QT_VERSION_STR) != 0) { + qWarning() << "\033[31mQuickshell 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 " + "the quickshell package must be rebuilt.\n"; + } + return launchFromCommand(state, coreApplication); } diff --git a/src/crash/interface.cpp b/src/crash/interface.cpp index 7691b260..c6334401 100644 --- a/src/crash/interface.cpp +++ b/src/crash/interface.cpp @@ -1,8 +1,10 @@ #include "interface.hpp" +#include #include #include #include +#include #include #include #include @@ -10,6 +12,7 @@ #include #include #include +#include #include #include "build.hpp" @@ -37,20 +40,40 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid) auto* mainLayout = new QVBoxLayout(this); - mainLayout->addWidget(new QLabel( - "Quickshell has crashed. Please submit a bug report to help us fix it.", - this - )); + auto qtVersionMatches = strcmp(qVersion(), QT_VERSION_STR) == 0; + if (qtVersionMatches) { + mainLayout->addWidget(new QLabel( + "Quickshell has crashed. Please submit a bug report to help us fix it.", + this + )); + } else { + mainLayout->addWidget( + new QLabel("Quickshell has crashed, likely due to a Qt version mismatch.", this) + ); + } mainLayout->addSpacing(textHeight); mainLayout->addWidget(new QLabel("General information", this)); mainLayout->addWidget(new ReportLabel("Git Revision:", GIT_REVISION, this)); + mainLayout->addWidget(new QLabel( + QString::fromLatin1("Runtime Qt version: ") % qVersion() % ", Buildtime Qt version: " + % QT_VERSION_STR, + this + )); mainLayout->addWidget(new ReportLabel("Crashed process ID:", QString::number(pid), this)); mainLayout->addWidget(new ReportLabel("Crash report folder:", this->reportFolder, this)); mainLayout->addSpacing(textHeight); - mainLayout->addWidget(new QLabel("Please open a bug report for this issue via github or email.")); + if (qtVersionMatches) { + mainLayout->addWidget(new QLabel("Please open a bug report for this issue via github or email.") + ); + } else { + mainLayout->addWidget(new QLabel( + "Please rebuild Quickshell against the current Qt version.\n" + "If this does not solve the problem, please open a bug report via github or email." + )); + } mainLayout->addWidget(new ReportLabel( "Github:", diff --git a/src/crash/main.cpp b/src/crash/main.cpp index 08c38892..1beb6749 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -12,7 +13,6 @@ #include #include #include -#include #include #include From 1adad9e822a9ac66a962823e7d931639782d3558 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 17 Oct 2024 13:51:21 -0700 Subject: [PATCH 186/305] build: avoid creating qs symlink in privileged directory --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c0b7e574..9ef18479 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,6 +172,6 @@ install( install(CODE " execute_process( COMMAND ${CMAKE_COMMAND} -E create_symlink \ - ${CMAKE_INSTALL_FULL_BINDIR}/quickshell ${CMAKE_INSTALL_FULL_BINDIR}/qs + ${CMAKE_INSTALL_FULL_BINDIR}/quickshell \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/qs ) ") From 4e48c6eefb399abfcdefbe046a6a642bdcfcc1a6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 28 Oct 2024 16:18:41 -0700 Subject: [PATCH 187/305] all: refactor windows code out of core There are still some links from core to window but its now separate enough to fix PanelWindow in qml tooling. --- CMakeLists.txt | 9 - src/CMakeLists.txt | 5 + src/build/CMakeLists.txt | 26 + src/{core => build}/build.hpp.in | 0 src/core/CMakeLists.txt | 34 +- src/core/main.cpp | 1031 --------------------- src/core/main.hpp | 7 - src/core/platformmenu.cpp | 4 +- src/core/platformmenu.hpp | 2 +- src/core/popupanchor.cpp | 4 +- src/core/popupanchor.hpp | 2 +- src/core/test/CMakeLists.txt | 3 +- src/core/test/popupwindow.hpp | 18 - src/io/ipc.hpp | 2 +- src/io/ipccomm.cpp | 4 +- src/io/ipccomm.hpp | 2 +- src/ipc/CMakeLists.txt | 7 + src/{core => ipc}/ipc.cpp | 4 +- src/{core => ipc}/ipc.hpp | 0 src/{core => ipc}/ipccommand.hpp | 0 src/main.cpp | 1031 ++++++++++++++++++++- src/wayland/hyprland/focus_grab/qml.cpp | 4 +- src/wayland/toplevel_management/qml.cpp | 6 +- src/wayland/toplevel_management/qml.hpp | 2 +- src/wayland/wlr_layershell.cpp | 4 +- src/wayland/wlr_layershell.hpp | 4 +- src/wayland/wlr_layershell/surface.cpp | 2 +- src/wayland/wlr_layershell/window.cpp | 2 +- src/wayland/wlr_layershell/window.hpp | 2 +- src/window/CMakeLists.txt | 23 + src/{core => window}/floatingwindow.cpp | 0 src/{core => window}/floatingwindow.hpp | 0 src/{core => window}/panelinterface.cpp | 0 src/{core => window}/panelinterface.hpp | 2 +- src/{core => window}/popupwindow.cpp | 5 +- src/{core => window}/popupwindow.hpp | 6 +- src/{core => window}/proxywindow.cpp | 10 +- src/{core => window}/proxywindow.hpp | 7 +- src/window/test/CMakeLists.txt | 7 + src/{core => window}/test/popupwindow.cpp | 0 src/window/test/popupwindow.hpp | 18 + src/{core => window}/windowinterface.cpp | 0 src/{core => window}/windowinterface.hpp | 6 +- src/x11/panel_window.cpp | 4 +- src/x11/panel_window.hpp | 4 +- 45 files changed, 1171 insertions(+), 1142 deletions(-) create mode 100644 src/build/CMakeLists.txt rename src/{core => build}/build.hpp.in (100%) delete mode 100644 src/core/main.cpp delete mode 100644 src/core/main.hpp create mode 100644 src/ipc/CMakeLists.txt rename src/{core => ipc}/ipc.cpp (98%) rename src/{core => ipc}/ipc.hpp (100%) rename src/{core => ipc}/ipccommand.hpp (100%) create mode 100644 src/window/CMakeLists.txt rename src/{core => window}/floatingwindow.cpp (100%) rename src/{core => window}/floatingwindow.hpp (100%) rename src/{core => window}/panelinterface.cpp (100%) rename src/{core => window}/panelinterface.hpp (99%) rename src/{core => window}/popupwindow.cpp (98%) rename src/{core => window}/popupwindow.hpp (97%) rename src/{core => window}/proxywindow.cpp (98%) rename src/{core => window}/proxywindow.hpp (97%) create mode 100644 src/window/test/CMakeLists.txt rename src/{core => window}/test/popupwindow.cpp (100%) create mode 100644 src/window/test/popupwindow.hpp rename src/{core => window}/windowinterface.cpp (100%) rename src/{core => window}/windowinterface.hpp (98%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ef18479..367b39e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,15 +64,6 @@ boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON) -if (NOT DEFINED GIT_REVISION) - execute_process( - COMMAND git rev-parse HEAD - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_REVISION - OUTPUT_STRIP_TRAILING_WHITESPACE - ) -endif() - add_compile_options(-Wall -Wextra) if (FRAME_POINTERS) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 33554832..5b843543 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,8 +1,13 @@ qt_add_executable(quickshell main.cpp) +target_link_libraries(quickshell PRIVATE ${QT_DEPS} quickshell-build) + install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +add_subdirectory(build) add_subdirectory(core) +add_subdirectory(ipc) +add_subdirectory(window) add_subdirectory(io) add_subdirectory(widgets) diff --git a/src/build/CMakeLists.txt b/src/build/CMakeLists.txt new file mode 100644 index 00000000..bb35da99 --- /dev/null +++ b/src/build/CMakeLists.txt @@ -0,0 +1,26 @@ +add_library(quickshell-build INTERFACE) + +if (NOT DEFINED GIT_REVISION) + execute_process( + COMMAND git rev-parse HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_REVISION + OUTPUT_STRIP_TRAILING_WHITESPACE + ) +endif() + +if (CRASH_REPORTER) + set(CRASH_REPORTER_DEF 1) +else() + set(CRASH_REPORTER_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/core/build.hpp.in b/src/build/build.hpp.in similarity index 100% rename from src/core/build.hpp.in rename to src/build/build.hpp.in diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 811965e7..b454f3a7 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,22 +1,16 @@ find_package(CLI11 CONFIG REQUIRED) qt_add_library(quickshell-core STATIC - main.cpp plugin.cpp shell.cpp variants.cpp rootwrapper.cpp - proxywindow.cpp reload.cpp rootwrapper.cpp qmlglobal.cpp qmlscreen.cpp region.cpp persistentprops.cpp - windowinterface.cpp - floatingwindow.cpp - panelinterface.cpp - popupwindow.cpp singleton.cpp generation.cpp scan.cpp @@ -43,32 +37,20 @@ qt_add_library(quickshell-core STATIC paths.cpp instanceinfo.cpp common.cpp - ipc.cpp ) -if (CRASH_REPORTER) - set(CRASH_REPORTER_DEF 1) -else() - set(CRASH_REPORTER_DEF 0) -endif() - -if (DISTRIBUTOR_DEBUGINFO_AVAILABLE) - set(DEBUGINFO_AVAILABLE 1) -else() - set(DEBUGINFO_AVAILABLE 0) -endif() - -add_library(quickshell-build INTERFACE) - -configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES) - -target_include_directories(quickshell-build INTERFACE ${CMAKE_CURRENT_BINARY_DIR}) target_link_libraries(quickshell-core PRIVATE quickshell-build) -qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1) +qt_add_qml_module(quickshell-core + URI Quickshell + VERSION 0.1 + IMPORTS Quickshell._Window +) + +target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} CLI11::CLI11) -target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} Qt6::QuickPrivate CLI11::CLI11) qs_pch(quickshell-core) +qs_pch(quickshell-coreplugin) target_link_libraries(quickshell PRIVATE quickshell-coreplugin) diff --git a/src/core/main.cpp b/src/core/main.cpp deleted file mode 100644 index cc57cc61..00000000 --- a/src/core/main.cpp +++ /dev/null @@ -1,1031 +0,0 @@ -#include "main.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include // NOLINT: Need to include this for impls of some CLI11 classes -#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 -#include -#include -#include -#include - -#include "../io/ipccomm.hpp" -#include "build.hpp" -#include "common.hpp" -#include "instanceinfo.hpp" -#include "ipc.hpp" -#include "logging.hpp" -#include "paths.hpp" -#include "plugin.hpp" -#include "rootwrapper.hpp" - -#if CRASH_REPORTER -#include "../crash/handler.hpp" -#include "../crash/main.hpp" -#endif - -namespace qs::launch { - -using qs::ipc::IpcClient; - -void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication); -int runCommand(int argc, char** argv, QCoreApplication* coreApplication); - -int DAEMON_PIPE = -1; // NOLINT -void exitDaemon(int code) { - if (DAEMON_PIPE == -1) return; - - if (write(DAEMON_PIPE, &code, sizeof(int)) == -1) { - qCritical().nospace() << "Failed to write daemon exit command with error code " << errno << ": " - << qt_error_string(); - } - - close(DAEMON_PIPE); - - close(STDIN_FILENO); - close(STDOUT_FILENO); - close(STDERR_FILENO); - - if (open("/dev/null", O_RDONLY) != STDIN_FILENO) { // NOLINT - qFatal() << "Failed to open /dev/null on stdin"; - } - - if (open("/dev/null", O_WRONLY) != STDOUT_FILENO) { // NOLINT - qFatal() << "Failed to open /dev/null on stdout"; - } - - if (open("/dev/null", O_WRONLY) != STDERR_FILENO) { // NOLINT - qFatal() << "Failed to open /dev/null on stderr"; - } -} - -int main(int argc, char** argv) { - QCoreApplication::setApplicationName("quickshell"); - -#if CRASH_REPORTER - qsCheckCrash(argc, argv); -#endif - - auto qArgC = 1; - auto* coreApplication = new QCoreApplication(qArgC, argv); - - checkCrashRelaunch(argv, coreApplication); - auto code = runCommand(argc, argv, coreApplication); - - exitDaemon(code); - return code; -} - -class QStringOption { -public: - QStringOption() = default; - QStringOption& operator=(const std::string& str) { - this->str = QString::fromStdString(str); - return *this; - } - - QString& operator*() { return this->str; } - QString* operator->() { return &this->str; } - -private: - QString str; -}; - -struct CommandState { - struct { - int argc = 0; - char** argv = nullptr; - } exec; - - struct { - bool timestamp = false; - bool noColor = !qEnvironmentVariableIsEmpty("NO_COLOR"); - bool sparse = false; - size_t verbosity = 0; - int tail = 0; - bool follow = false; - QStringOption rules; - QStringOption readoutRules; - QStringOption file; - } log; - - struct { - QStringOption path; - QStringOption manifest; - QStringOption name; - } config; - - struct { - int port = -1; - bool wait = false; - } debug; - - struct { - QStringOption id; - pid_t pid = -1; // NOLINT (include) - bool all = false; - } instance; - - struct { - bool json = false; - } output; - - struct { - bool info = false; - QStringOption target; - QStringOption function; - std::vector arguments; - } ipc; - - struct { - CLI::App* log = nullptr; - CLI::App* list = nullptr; - CLI::App* kill = nullptr; - CLI::App* msg = nullptr; - } subcommand; - - struct { - bool checkCompat = false; - bool printVersion = false; - bool killAll = false; - bool noDuplicate = false; - bool daemonize = false; - } misc; -}; - -int readLogFile(CommandState& cmd); -int listInstances(CommandState& cmd); -int killInstances(CommandState& cmd); -int msgInstance(CommandState& cmd); -int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication); - -struct LaunchArgs { - QString configPath; - int debugPort = -1; - bool waitForDebug = false; -}; - -int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication); - -int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { - auto state = CommandState(); - - state.exec = { - .argc = argc, - .argv = argv, - }; - - auto addConfigSelection = [&](CLI::App* cmd) { - auto* group = cmd->add_option_group("Config Selection") - ->description("If no options in this group are specified,\n" - "$XDG_CONFIG_HOME/quickshell/shell.qml will be used."); - - auto* path = group->add_option("-p,--path", state.config.path) - ->description("Path to a QML file.") - ->envname("QS_CONFIG_PATH"); - - group->add_option("-m,--manifest", state.config.manifest) - ->description("Path to a quickshell manifest.\n" - "Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf") - ->envname("QS_MANIFEST") - ->excludes(path); - - group->add_option("-c,--config", state.config.name) - ->description("Name of a quickshell configuration to run.\n" - "If -m is specified, this is a configuration in the manifest,\n" - "otherwise it is the name of a folder in $XDG_CONFIG_HOME/quickshell.") - ->envname("QS_CONFIG_NAME"); - - return group; - }; - - auto addDebugOptions = [&](CLI::App* cmd) { - auto* group = cmd->add_option_group("Debugging", "Options for QML debugging."); - - auto* debug = group->add_option("--debug", state.debug.port) - ->description("Open the given port for a QML debugger connection.") - ->check(CLI::Range(0, 65535)); - - group->add_flag("--waitfordebug", state.debug.wait) - ->description("Wait for a QML debugger to connect before executing the configuration.") - ->needs(debug); - - return group; - }; - - auto addLoggingOptions = [&](CLI::App* cmd, bool noGroup, bool noDisplay = false) { - 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\n" - "for the NO_COLOR environment variable."); - - group->add_flag("--log-times", state.log.timestamp) - ->description("Log timestamps with each message."); - - group->add_option("--log-rules", state.log.rules) - ->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."); - - auto* hgroup = cmd->add_option_group(""); - hgroup->add_flag("--no-detailed-logs", state.log.sparse); - }; - - auto addInstanceSelection = [&](CLI::App* cmd) { - 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,\n" - "for example \"abc\" will select \"abcdefg\"."); - - group->add_option("--pid", state.instance.pid) - ->description("The process id of the instance to operate on."); - - return group; - }; - - auto cli = CLI::App(); - - // Require 0-1 subcommands. Without this, positionals can be parsed as more subcommands. - cli.require_subcommand(0, 1); - - addConfigSelection(&cli); - addLoggingOptions(&cli, false); - addDebugOptions(&cli); - - { - cli.add_option_group("")->add_flag("--private-check-compat", state.misc.checkCompat); - - cli.add_flag("-V,--version", state.misc.printVersion) - ->description("Print quickshell's version and exit."); - - cli.add_flag("-n,--no-duplicate", state.misc.noDuplicate) - ->description("Exit immediately if another instance of the given config is running."); - - cli.add_flag("-d,--daemonize", state.misc.daemonize) - ->description("Detach from the controlling terminal."); - } - - { - auto* sub = cli.add_subcommand("log", "Print quickshell logs."); - - auto* file = sub->add_option("file", state.log.file, "Log file to read."); - - sub->add_option("-t,--tail", state.log.tail) - ->description("Maximum number of lines to print, starting from the bottom.") - ->check(CLI::Range(1, std::numeric_limits::max(), "INT > 0")); - - sub->add_flag("-f,--follow", state.log.follow) - ->description("Keep reading the log until the logging process terminates."); - - sub->add_option("-r,--rules", state.log.readoutRules, "Log file to read.") - ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES."); - - auto* instance = addInstanceSelection(sub)->excludes(file); - addConfigSelection(sub)->excludes(instance)->excludes(file); - addLoggingOptions(sub, false); - - state.subcommand.log = sub; - } - - { - 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."); - - sub->add_flag("-j,--json", state.output.json, "Output the list as a json."); - - addConfigSelection(sub)->excludes(all); - addLoggingOptions(sub, false, true); - - state.subcommand.list = sub; - } - - { - auto* sub = cli.add_subcommand("kill", "Kill quickshell instances."); - //sub->add_flag("-a,--all", "Kill all matching instances instead of just one."); - auto* instance = addInstanceSelection(sub); - addConfigSelection(sub)->excludes(instance); - addLoggingOptions(sub, false, true); - - state.subcommand.kill = sub; - } - - { - auto* sub = cli.add_subcommand("msg", "Send messages to IpcHandlers.")->require_option(); - - auto* target = sub->add_option("target", state.ipc.target, "The target to message."); - - auto* function = sub->add_option("function", state.ipc.function) - ->description("The function to call in the target.") - ->needs(target); - - auto* arguments = sub->add_option("arguments", state.ipc.arguments) - ->description("Arguments to the called function.") - ->needs(function) - ->allow_extra_args(); - - sub->add_flag("-s,--show", state.ipc.info) - ->description("Print information about a function or target if given, or all available " - "targets if not.") - ->excludes(arguments); - - auto* instance = addInstanceSelection(sub); - addConfigSelection(sub)->excludes(instance); - addLoggingOptions(sub, false, true); - - sub->require_option(); - - state.subcommand.msg = sub; - } - - CLI11_PARSE(cli, argc, argv); - - if (state.misc.checkCompat) { - if (strcmp(qVersion(), QT_VERSION_STR) != 0) { - 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"; - return 1; - } - - return 0; - } - - // Has to happen before extra threads are spawned. - if (state.misc.daemonize) { - auto closepipes = std::array(); - if (pipe(closepipes.data()) == -1) { - qFatal().nospace() << "Failed to create messaging pipes for daemon with error " << errno - << ": " << qt_error_string(); - } - - DAEMON_PIPE = closepipes[1]; - - pid_t pid = fork(); // NOLINT (include) - - if (pid == -1) { - qFatal().nospace() << "Failed to fork daemon with error " << errno << ": " - << qt_error_string(); - } else if (pid == 0) { - close(closepipes[0]); - - if (setsid() == -1) { - qFatal().nospace() << "Failed to setsid with error " << errno << ": " << qt_error_string(); - } - } else { - close(closepipes[1]); - - int ret = 0; - if (read(closepipes[0], &ret, sizeof(int)) == -1) { - qFatal() << "Failed to wait for daemon launch (it may have crashed)"; - } - - return ret; - } - } - - { - auto level = state.log.verbosity == 0 ? QtWarningMsg - : state.log.verbosity == 1 ? QtInfoMsg - : QtDebugMsg; - - LogManager::init( - !state.log.noColor, - state.log.timestamp, - state.log.sparse, - level, - *state.log.rules, - *state.subcommand.log ? "READER" : "" - ); - } - - if (state.misc.printVersion) { - qCInfo(logBare).noquote().nospace() << "quickshell pre-release, revision " << GIT_REVISION - << ", distributed by: " << DISTRIBUTOR; - - if (state.log.verbosity > 1) { - qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; - qCInfo(logBare).noquote() << "Runtime Qt Version:" << qVersion(); - qCInfo(logBare).noquote() << "Compiler:" << COMPILER; - qCInfo(logBare).noquote() << "Compile Flags:" << COMPILE_FLAGS; - } - - if (state.log.verbosity > 0) { - qCInfo(logBare).noquote() << "\nBuild Type:" << BUILD_TYPE; - qCInfo(logBare).noquote() << "Build configuration:"; - qCInfo(logBare).noquote().nospace() << BUILD_CONFIGURATION; - } - } else if (*state.subcommand.log) { - return readLogFile(state); - } else if (*state.subcommand.list) { - return listInstances(state); - } else if (*state.subcommand.kill) { - return killInstances(state); - } else if (*state.subcommand.msg) { - return msgInstance(state); - } else { - if (strcmp(qVersion(), QT_VERSION_STR) != 0) { - qWarning() << "\033[31mQuickshell 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 " - "the quickshell package must be rebuilt.\n"; - } - - return launchFromCommand(state, coreApplication); - } - - return 0; -} - -int locateConfigFile(CommandState& cmd, QString& path) { - if (!cmd.config.path->isEmpty()) { - path = *cmd.config.path; - } else { - auto manifestPath = *cmd.config.manifest; - if (manifestPath.isEmpty()) { - auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); - auto path = configDir.filePath("manifest.conf"); - if (QFileInfo(path).isFile()) manifestPath = path; - } - - if (!manifestPath.isEmpty()) { - auto file = QFile(manifestPath); - if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { - auto stream = QTextStream(&file); - while (!stream.atEnd()) { - auto line = stream.readLine(); - if (line.trimmed().startsWith("#")) continue; - if (line.trimmed().isEmpty()) continue; - - auto split = line.split('='); - if (split.length() != 2) { - qCritical() << "Manifest line not in expected format 'name = relativepath':" << line; - return -1; - } - - if (split[0].trimmed() == *cmd.config.name) { - path = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed()); - break; - } - } - - if (path.isEmpty()) { - qCCritical(logBare) << "Configuration" << *cmd.config.name - << "not found when searching manifest" << manifestPath; - return -1; - } - } else { - qCCritical(logBare) << "Could not open maifest at path" << *cmd.config.manifest; - return -1; - } - } else { - auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); - - if (cmd.config.name->isEmpty()) { - path = configDir.path(); - } else { - path = configDir.filePath(*cmd.config.name); - } - } - } - - if (QFileInfo(path).isDir()) { - path = QDir(path).filePath("shell.qml"); - } - - if (!QFileInfo(path).isFile()) { - qCCritical(logBare) << "Could not open config file at" << path; - return -1; - } - - path = QFileInfo(path).canonicalFilePath(); - - return 0; -} - -void sortInstances(QVector& list) { - std::sort(list.begin(), list.end(), [](const InstanceLockInfo& a, const InstanceLockInfo& b) { - return a.instance.launchTime < b.instance.launchTime; - }); -}; - -int selectInstance(CommandState& cmd, InstanceLockInfo* instance) { - auto* basePath = QsPaths::instance()->baseRunDir(); - if (!basePath) return -1; - - QString path; - - if (cmd.instance.pid != -1) { - path = QDir(basePath->filePath("by-pid")).filePath(QString::number(cmd.instance.pid)); - if (!QsPaths::checkLock(path, instance)) { - qCInfo(logBare) << "No instance found for pid" << cmd.instance.pid; - return -1; - } - } else if (!cmd.instance.id->isEmpty()) { - path = basePath->filePath("by-pid"); - auto instances = QsPaths::collectInstances(path); - - auto itr = - std::remove_if(instances.begin(), instances.end(), [&](const InstanceLockInfo& info) { - return !info.instance.instanceId.startsWith(*cmd.instance.id); - }); - - instances.erase(itr, instances.end()); - - if (instances.isEmpty()) { - qCInfo(logBare) << "No running instances start with" << *cmd.instance.id; - return -1; - } else if (instances.length() != 1) { - qCInfo(logBare) << "More than one instance starts with" << *cmd.instance.id; - - for (auto& instance: instances) { - qCInfo(logBare).noquote() << " -" << instance.instance.instanceId; - } - - return -1; - } else { - *instance = instances.value(0); - } - } else { - QString configFilePath; - auto r = locateConfigFile(cmd, configFilePath); - if (r != 0) return r; - - auto pathId = - QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); - - path = QDir(basePath->filePath("by-path")).filePath(pathId); - - auto instances = QsPaths::collectInstances(path); - sortInstances(instances); - - if (instances.isEmpty()) { - qCInfo(logBare) << "No running instances for" << configFilePath; - return -1; - } - - *instance = instances.value(0); - } - - return 0; -} - -int readLogFile(CommandState& cmd) { - auto path = *cmd.log.file; - - if (path.isEmpty()) { - InstanceLockInfo instance; - auto r = selectInstance(cmd, &instance); - if (r != 0) return r; - - path = QDir(QsPaths::basePath(instance.instance.instanceId)).filePath("log.qslog"); - } - - auto file = QFile(path); - if (!file.open(QFile::ReadOnly)) { - qCCritical(logBare) << "Failed to open log file" << path; - return -1; - } - - return qs::log::readEncodedLogs( - &file, - path, - cmd.log.timestamp, - cmd.log.tail, - cmd.log.follow, - *cmd.log.readoutRules - ) - ? 0 - : -1; -} - -int listInstances(CommandState& cmd) { - auto* basePath = QsPaths::instance()->baseRunDir(); - if (!basePath) return -1; // NOLINT - - QString path; - QString configFilePath; - if (cmd.instance.all) { - path = basePath->filePath("by-pid"); - } else { - auto r = locateConfigFile(cmd, configFilePath); - - if (r != 0) { - qCInfo(logBare) << "Use --all to list all instances."; - return r; - } - - auto pathId = - QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); - - path = QDir(basePath->filePath("by-path")).filePath(pathId); - } - - auto instances = QsPaths::collectInstances(path); - - if (instances.isEmpty()) { - if (cmd.instance.all) { - qCInfo(logBare) << "No running instances."; - } else { - qCInfo(logBare) << "No running instances for" << configFilePath; - qCInfo(logBare) << "Use --all to list all instances."; - } - } else { - sortInstances(instances); - - if (cmd.output.json) { - auto array = QJsonArray(); - - for (auto& instance: instances) { - auto json = QJsonObject(); - - json["id"] = instance.instance.instanceId; - json["pid"] = instance.pid; - json["shell_id"] = instance.instance.shellId; - json["config_path"] = instance.instance.configPath; - json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate); - - array.push_back(json); - } - - auto document = QJsonDocument(array); - QTextStream(stdout) << document.toJson(QJsonDocument::Indented); - } else { - for (auto& instance: instances) { - auto launchTimeStr = instance.instance.launchTime.toString("yyyy-MM-dd hh:mm:ss"); - - auto runSeconds = instance.instance.launchTime.secsTo(QDateTime::currentDateTime()); - auto remSeconds = runSeconds % 60; - auto runMinutes = (runSeconds - remSeconds) / 60; - auto remMinutes = runMinutes % 60; - auto runHours = (runMinutes - remMinutes) / 60; - auto runtimeStr = QString("%1 hours, %2 minutes, %3 seconds") - .arg(runHours) - .arg(remMinutes) - .arg(remSeconds); - - qCInfo(logBare).noquote().nospace() - << "Instance " << instance.instance.instanceId << ":\n" - << " Process ID: " << instance.pid << '\n' - << " Shell ID: " << instance.instance.shellId << '\n' - << " Config path: " << instance.instance.configPath << '\n' - << " Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n"; - } - } - } - - return 0; -} - -int killInstances(CommandState& cmd) { - InstanceLockInfo instance; - auto r = selectInstance(cmd, &instance); - if (r != 0) return r; - - return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { - client.kill(); - qCInfo(logBare).noquote() << "Killed" << instance.instance.instanceId; - }); -} - -int msgInstance(CommandState& cmd) { - InstanceLockInfo instance; - auto r = selectInstance(cmd, &instance); - if (r != 0) return r; - - return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { - if (cmd.ipc.info) { - return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.function); - } else { - QVector arguments; - for (auto& arg: cmd.ipc.arguments) { - arguments += *arg; - } - - return qs::io::ipc::comm::callFunction( - &client, - *cmd.ipc.target, - *cmd.ipc.function, - arguments - ); - } - - return -1; - }); -} - -template -QString base36Encode(T number) { - const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz"; - QString result; - - do { - result.prepend(digits[number % 36]); - number /= 36; - } while (number > 0); - - for (auto i = 0; i < result.length() / 2; i++) { - auto opposite = result.length() - i - 1; - auto c = result.at(i); - result[i] = result.at(opposite); - result[opposite] = c; - } - - return result; -} - -void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) { -#if CRASH_REPORTER - auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD"); - - if (!lastInfoFdStr.isEmpty()) { - auto lastInfoFd = lastInfoFdStr.toInt(); - - QFile file; - file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle); - file.seek(0); - - auto ds = QDataStream(&file); - RelaunchInfo info; - ds >> info; - - LogManager::init( - !info.noColor, - info.timestamp, - info.sparseLogsOnly, - info.defaultLogLevel, - info.logRules - ); - - qCritical().nospace() << "Quickshell has crashed under pid " - << qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt() - << " (Coredumps will be available under that pid.)"; - - qCritical() << "Further crash information is stored under" - << QsPaths::crashDir(info.instance.instanceId).path(); - - if (info.instance.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) { - qCritical() << "Quickshell crashed within 10 seconds of launching. Not restarting to avoid " - "a crash loop."; - exit(-1); // NOLINT - } else { - qCritical() << "Quickshell has been restarted."; - - launch({.configPath = info.instance.configPath}, argv, coreApplication); - } - } -#endif -} - -int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication) { - QString configPath; - - auto r = locateConfigFile(cmd, configPath); - if (r != 0) return r; - - { - InstanceLockInfo info; - if (cmd.misc.noDuplicate && selectInstance(cmd, &info) == 0) { - qCInfo(logBare) << "An instance of this configuration is already running."; - return 0; - } - } - - return launch( - { - .configPath = configPath, - .debugPort = cmd.debug.port, - .waitForDebug = cmd.debug.wait, - }, - cmd.exec.argv, - coreApplication - ); -} - -int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication) { - auto pathId = QCryptographicHash::hash(args.configPath.toUtf8(), QCryptographicHash::Md5).toHex(); - auto shellId = QString(pathId); - - qInfo() << "Launching config:" << args.configPath; - - auto file = QFile(args.configPath); - if (!file.open(QFile::ReadOnly | QFile::Text)) { - qCritical() << "Could not open config file" << args.configPath; - return -1; - } - - struct { - bool useQApplication = false; - bool nativeTextRendering = false; - bool desktopSettingsAware = true; - QString iconTheme = qEnvironmentVariable("QS_ICON_THEME"); - QHash envOverrides; - } pragmas; - - auto stream = QTextStream(&file); - while (!stream.atEnd()) { - auto line = stream.readLine().trimmed(); - if (line.startsWith("//@ pragma ")) { - auto pragma = line.sliced(11).trimmed(); - - if (pragma == "UseQApplication") pragmas.useQApplication = true; - else if (pragma == "NativeTextRendering") pragmas.nativeTextRendering = true; - else if (pragma == "IgnoreSystemSettings") pragmas.desktopSettingsAware = false; - else if (pragma.startsWith("IconTheme ")) pragmas.iconTheme = pragma.sliced(10); - else if (pragma.startsWith("Env ")) { - auto envPragma = pragma.sliced(4); - auto splitIdx = envPragma.indexOf('='); - - if (splitIdx == -1) { - qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'"; - return -1; - } - - auto var = envPragma.sliced(0, splitIdx).trimmed(); - auto val = envPragma.sliced(splitIdx + 1).trimmed(); - pragmas.envOverrides.insert(var, val); - } else if (pragma.startsWith("ShellId ")) { - shellId = pragma.sliced(8).trimmed(); - } else { - qCritical() << "Unrecognized pragma" << pragma; - return -1; - } - } else if (line.startsWith("import")) break; - } - - file.close(); - - if (!pragmas.iconTheme.isEmpty()) { - QIcon::setThemeName(pragmas.iconTheme); - } - - qInfo() << "Shell ID:" << shellId << "Path ID" << pathId; - - auto launchTime = qs::Common::LAUNCH_TIME.toSecsSinceEpoch(); - InstanceInfo::CURRENT = InstanceInfo { - .instanceId = base36Encode(getpid()) + base36Encode(launchTime), - .configPath = args.configPath, - .shellId = shellId, - .launchTime = qs::Common::LAUNCH_TIME, - }; - -#if CRASH_REPORTER - auto crashHandler = crash::CrashHandler(); - crashHandler.init(); - - { - auto* log = LogManager::instance(); - crashHandler.setRelaunchInfo({ - .instance = InstanceInfo::CURRENT, - .noColor = !log->colorLogs, - .timestamp = log->timestampLogs, - .sparseLogsOnly = log->isSparse(), - .defaultLogLevel = log->defaultLevel(), - .logRules = log->rulesString(), - }); - } -#endif - - QsPaths::init(shellId, pathId); - QsPaths::instance()->linkRunDir(); - QsPaths::instance()->linkPathDir(); - LogManager::initFs(); - - for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) { - qputenv(var.toUtf8(), val.toUtf8()); - } - - // The qml engine currently refuses to cache non file (qsintercept) paths. - - // if (auto* cacheDir = QsPaths::instance()->cacheDir()) { - // auto qmlCacheDir = cacheDir->filePath("qml-engine-cache"); - // qputenv("QML_DISK_CACHE_PATH", qmlCacheDir.toLocal8Bit()); - // - // if (!qEnvironmentVariableIsSet("QML_DISK_CACHE")) { - // qputenv("QML_DISK_CACHE", "aot,qmlc"); - // } - // } - - // While the simple animation driver can lead to better animations in some cases, - // it also can cause excessive repainting at excessively high framerates which can - // lead to noticeable amounts of gpu usage, including overheating on some systems. - // This gets worse the more windows are open, as repaints trigger on all of them for - // some reason. See QTBUG-126099 for details. - - // if (!qEnvironmentVariableIsSet("QSG_USE_SIMPLE_ANIMATION_DRIVER")) { - // qputenv("QSG_USE_SIMPLE_ANIMATION_DRIVER", "1"); - // } - - // Some programs place icons in the pixmaps folder instead of the icons folder. - // This seems to be controlled by the QPA and qt6ct does not provide it. - { - QList dataPaths; - - 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"); - } - - auto fallbackPaths = QIcon::fallbackSearchPaths(); - - for (auto& path: dataPaths) { - auto newPath = QDir(path).filePath("pixmaps"); - - if (!fallbackPaths.contains(newPath)) { - fallbackPaths.push_back(newPath); - } - } - - QIcon::setFallbackSearchPaths(fallbackPaths); - } - - QGuiApplication::setDesktopSettingsAware(pragmas.desktopSettingsAware); - - delete coreApplication; - - QGuiApplication* app = nullptr; - auto qArgC = 0; - - if (pragmas.useQApplication) { - app = new QApplication(qArgC, argv); - } else { - app = new QGuiApplication(qArgC, argv); - } - - if (args.debugPort != -1) { - QQmlDebuggingEnabler::enableDebugging(true); - auto wait = args.waitForDebug ? QQmlDebuggingEnabler::WaitForClient - : QQmlDebuggingEnabler::DoNotWaitForClient; - QQmlDebuggingEnabler::startTcpDebugServer(args.debugPort, wait); - } - - QuickshellPlugin::initPlugins(); - - // Base window transparency appears to be additive. - // Use a fully transparent window with a colored rect. - QQuickWindow::setDefaultAlphaBuffer(true); - - if (pragmas.nativeTextRendering) { - QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); - } - - qs::ipc::IpcServer::start(); - QsPaths::instance()->createLock(); - - auto root = RootWrapper(args.configPath, shellId); - QGuiApplication::setQuitOnLastWindowClosed(false); - - exitDaemon(0); - - auto code = QGuiApplication::exec(); - delete app; - return code; -} - -} // namespace qs::launch diff --git a/src/core/main.hpp b/src/core/main.hpp deleted file mode 100644 index 795bf7ad..00000000 --- a/src/core/main.hpp +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -namespace qs::launch { - -int main(int argc, char** argv); // NOLINT - -} diff --git a/src/core/platformmenu.cpp b/src/core/platformmenu.cpp index 37eb4654..09837ec9 100644 --- a/src/core/platformmenu.cpp +++ b/src/core/platformmenu.cpp @@ -17,11 +17,11 @@ #include #include +#include "../window/proxywindow.hpp" +#include "../window/windowinterface.hpp" #include "generation.hpp" #include "popupanchor.hpp" -#include "proxywindow.hpp" #include "qsmenu.hpp" -#include "windowinterface.hpp" namespace qs::menu::platform { diff --git a/src/core/platformmenu.hpp b/src/core/platformmenu.hpp index 85aaffac..5e8a0afe 100644 --- a/src/core/platformmenu.hpp +++ b/src/core/platformmenu.hpp @@ -13,7 +13,7 @@ #include #include -#include "popupanchor.hpp" +#include "../core/popupanchor.hpp" #include "qsmenu.hpp" namespace qs::menu::platform { diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index 1f4c5a76..0dc9c4a4 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -7,9 +7,9 @@ #include #include -#include "proxywindow.hpp" +#include "../window/proxywindow.hpp" +#include "../window/windowinterface.hpp" #include "types.hpp" -#include "windowinterface.hpp" bool PopupAnchorState::operator==(const PopupAnchorState& other) const { return this->rect == other.rect && this->edges == other.edges && this->gravity == other.gravity diff --git a/src/core/popupanchor.hpp b/src/core/popupanchor.hpp index 11b2ba20..0897928a 100644 --- a/src/core/popupanchor.hpp +++ b/src/core/popupanchor.hpp @@ -13,8 +13,8 @@ #include #include +#include "../window/proxywindow.hpp" #include "doc.hpp" -#include "proxywindow.hpp" #include "types.hpp" ///! Adjustment strategy for popups that do not fit on screen. diff --git a/src/core/test/CMakeLists.txt b/src/core/test/CMakeLists.txt index 3c057d3b..448881a6 100644 --- a/src/core/test/CMakeLists.txt +++ b/src/core/test/CMakeLists.txt @@ -1,9 +1,8 @@ function (qs_test name) add_executable(${name} ${ARGN}) - target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test quickshell-core) + target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test quickshell-core quickshell-window) add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) endfunction() -qs_test(popupwindow popupwindow.cpp) qs_test(transformwatcher transformwatcher.cpp) qs_test(ringbuffer ringbuf.cpp) diff --git a/src/core/test/popupwindow.hpp b/src/core/test/popupwindow.hpp index bebc5154..e69de29b 100644 --- a/src/core/test/popupwindow.hpp +++ b/src/core/test/popupwindow.hpp @@ -1,18 +0,0 @@ -#pragma once - -#include -#include - -class TestPopupWindow: public QObject { - Q_OBJECT; - -private slots: - void initiallyVisible(); - void reloadReparent(); - void reloadUnparent(); - void invisibleWithoutParent(); - void moveWithParent(); - void attachParentLate(); - void reparentLate(); - void xMigrationFix(); -}; diff --git a/src/io/ipc.hpp b/src/io/ipc.hpp index 50a94759..924f045e 100644 --- a/src/io/ipc.hpp +++ b/src/io/ipc.hpp @@ -4,7 +4,7 @@ #include #include -#include "../core/ipc.hpp" +#include "../ipc/ipc.hpp" namespace qs::io::ipc { diff --git a/src/io/ipccomm.cpp b/src/io/ipccomm.cpp index 56381260..f2a9118a 100644 --- a/src/io/ipccomm.cpp +++ b/src/io/ipccomm.cpp @@ -9,9 +9,9 @@ #include #include "../core/generation.hpp" -#include "../core/ipc.hpp" -#include "../core/ipccommand.hpp" #include "../core/logging.hpp" +#include "../ipc/ipc.hpp" +#include "../ipc/ipccommand.hpp" #include "ipc.hpp" #include "ipchandler.hpp" diff --git a/src/io/ipccomm.hpp b/src/io/ipccomm.hpp index 7b1ec02a..69463983 100644 --- a/src/io/ipccomm.hpp +++ b/src/io/ipccomm.hpp @@ -3,7 +3,7 @@ #include #include -#include "../core/ipc.hpp" +#include "../ipc/ipc.hpp" namespace qs::io::ipc::comm { diff --git a/src/ipc/CMakeLists.txt b/src/ipc/CMakeLists.txt new file mode 100644 index 00000000..ff6093c6 --- /dev/null +++ b/src/ipc/CMakeLists.txt @@ -0,0 +1,7 @@ +qt_add_library(quickshell-ipc STATIC + ipc.cpp +) + +target_link_libraries(quickshell-ipc PRIVATE ${QT_DEPS}) + +target_link_libraries(quickshell PRIVATE quickshell-ipc) diff --git a/src/core/ipc.cpp b/src/ipc/ipc.cpp similarity index 98% rename from src/core/ipc.cpp rename to src/ipc/ipc.cpp index dd2cd1e8..3580e2be 100644 --- a/src/core/ipc.cpp +++ b/src/ipc/ipc.cpp @@ -9,9 +9,9 @@ #include #include -#include "generation.hpp" +#include "../core/generation.hpp" +#include "../core/paths.hpp" #include "ipccommand.hpp" -#include "paths.hpp" namespace qs::ipc { diff --git a/src/core/ipc.hpp b/src/ipc/ipc.hpp similarity index 100% rename from src/core/ipc.hpp rename to src/ipc/ipc.hpp diff --git a/src/core/ipccommand.hpp b/src/ipc/ipccommand.hpp similarity index 100% rename from src/core/ipccommand.hpp rename to src/ipc/ipccommand.hpp diff --git a/src/main.cpp b/src/main.cpp index 7e4811da..f18c2341 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,1032 @@ -#include "core/main.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include // NOLINT: Need to include this for impls of some CLI11 classes +#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 +#include +#include +#include +#include + +#include "build.hpp" +#include "core/common.hpp" +#include "core/instanceinfo.hpp" +#include "core/logging.hpp" +#include "core/paths.hpp" +#include "core/plugin.hpp" +#include "core/rootwrapper.hpp" +#include "io/ipccomm.hpp" +#include "ipc/ipc.hpp" + +#if CRASH_REPORTER +#include "crash/handler.hpp" +#include "crash/main.hpp" +#endif + +namespace qs::launch { + +using qs::ipc::IpcClient; + +void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication); +int runCommand(int argc, char** argv, QCoreApplication* coreApplication); + +int DAEMON_PIPE = -1; // NOLINT +void exitDaemon(int code) { + if (DAEMON_PIPE == -1) return; + + if (write(DAEMON_PIPE, &code, sizeof(int)) == -1) { + qCritical().nospace() << "Failed to write daemon exit command with error code " << errno << ": " + << qt_error_string(); + } + + close(DAEMON_PIPE); + + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + if (open("/dev/null", O_RDONLY) != STDIN_FILENO) { // NOLINT + qFatal() << "Failed to open /dev/null on stdin"; + } + + if (open("/dev/null", O_WRONLY) != STDOUT_FILENO) { // NOLINT + qFatal() << "Failed to open /dev/null on stdout"; + } + + if (open("/dev/null", O_WRONLY) != STDERR_FILENO) { // NOLINT + qFatal() << "Failed to open /dev/null on stderr"; + } +} + +int main(int argc, char** argv) { + QCoreApplication::setApplicationName("quickshell"); + +#if CRASH_REPORTER + qsCheckCrash(argc, argv); +#endif + + auto qArgC = 1; + auto* coreApplication = new QCoreApplication(qArgC, argv); + + checkCrashRelaunch(argv, coreApplication); + auto code = runCommand(argc, argv, coreApplication); + + exitDaemon(code); + return code; +} + +class QStringOption { +public: + QStringOption() = default; + QStringOption& operator=(const std::string& str) { + this->str = QString::fromStdString(str); + return *this; + } + + QString& operator*() { return this->str; } + QString* operator->() { return &this->str; } + +private: + QString str; +}; + +struct CommandState { + struct { + int argc = 0; + char** argv = nullptr; + } exec; + + struct { + bool timestamp = false; + bool noColor = !qEnvironmentVariableIsEmpty("NO_COLOR"); + bool sparse = false; + size_t verbosity = 0; + int tail = 0; + bool follow = false; + QStringOption rules; + QStringOption readoutRules; + QStringOption file; + } log; + + struct { + QStringOption path; + QStringOption manifest; + QStringOption name; + } config; + + struct { + int port = -1; + bool wait = false; + } debug; + + struct { + QStringOption id; + pid_t pid = -1; // NOLINT (include) + bool all = false; + } instance; + + struct { + bool json = false; + } output; + + struct { + bool info = false; + QStringOption target; + QStringOption function; + std::vector arguments; + } ipc; + + struct { + CLI::App* log = nullptr; + CLI::App* list = nullptr; + CLI::App* kill = nullptr; + CLI::App* msg = nullptr; + } subcommand; + + struct { + bool checkCompat = false; + bool printVersion = false; + bool killAll = false; + bool noDuplicate = false; + bool daemonize = false; + } misc; +}; + +int readLogFile(CommandState& cmd); +int listInstances(CommandState& cmd); +int killInstances(CommandState& cmd); +int msgInstance(CommandState& cmd); +int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication); + +struct LaunchArgs { + QString configPath; + int debugPort = -1; + bool waitForDebug = false; +}; + +int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication); + +int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { + auto state = CommandState(); + + state.exec = { + .argc = argc, + .argv = argv, + }; + + auto addConfigSelection = [&](CLI::App* cmd) { + auto* group = cmd->add_option_group("Config Selection") + ->description("If no options in this group are specified,\n" + "$XDG_CONFIG_HOME/quickshell/shell.qml will be used."); + + auto* path = group->add_option("-p,--path", state.config.path) + ->description("Path to a QML file.") + ->envname("QS_CONFIG_PATH"); + + group->add_option("-m,--manifest", state.config.manifest) + ->description("Path to a quickshell manifest.\n" + "Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf") + ->envname("QS_MANIFEST") + ->excludes(path); + + group->add_option("-c,--config", state.config.name) + ->description("Name of a quickshell configuration to run.\n" + "If -m is specified, this is a configuration in the manifest,\n" + "otherwise it is the name of a folder in $XDG_CONFIG_HOME/quickshell.") + ->envname("QS_CONFIG_NAME"); + + return group; + }; + + auto addDebugOptions = [&](CLI::App* cmd) { + auto* group = cmd->add_option_group("Debugging", "Options for QML debugging."); + + auto* debug = group->add_option("--debug", state.debug.port) + ->description("Open the given port for a QML debugger connection.") + ->check(CLI::Range(0, 65535)); + + group->add_flag("--waitfordebug", state.debug.wait) + ->description("Wait for a QML debugger to connect before executing the configuration.") + ->needs(debug); + + return group; + }; + + auto addLoggingOptions = [&](CLI::App* cmd, bool noGroup, bool noDisplay = false) { + 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\n" + "for the NO_COLOR environment variable."); + + group->add_flag("--log-times", state.log.timestamp) + ->description("Log timestamps with each message."); + + group->add_option("--log-rules", state.log.rules) + ->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."); + + auto* hgroup = cmd->add_option_group(""); + hgroup->add_flag("--no-detailed-logs", state.log.sparse); + }; + + auto addInstanceSelection = [&](CLI::App* cmd) { + 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,\n" + "for example \"abc\" will select \"abcdefg\"."); + + group->add_option("--pid", state.instance.pid) + ->description("The process id of the instance to operate on."); + + return group; + }; + + auto cli = CLI::App(); + + // Require 0-1 subcommands. Without this, positionals can be parsed as more subcommands. + cli.require_subcommand(0, 1); + + addConfigSelection(&cli); + addLoggingOptions(&cli, false); + addDebugOptions(&cli); + + { + cli.add_option_group("")->add_flag("--private-check-compat", state.misc.checkCompat); + + cli.add_flag("-V,--version", state.misc.printVersion) + ->description("Print quickshell's version and exit."); + + cli.add_flag("-n,--no-duplicate", state.misc.noDuplicate) + ->description("Exit immediately if another instance of the given config is running."); + + cli.add_flag("-d,--daemonize", state.misc.daemonize) + ->description("Detach from the controlling terminal."); + } + + { + auto* sub = cli.add_subcommand("log", "Print quickshell logs."); + + auto* file = sub->add_option("file", state.log.file, "Log file to read."); + + sub->add_option("-t,--tail", state.log.tail) + ->description("Maximum number of lines to print, starting from the bottom.") + ->check(CLI::Range(1, std::numeric_limits::max(), "INT > 0")); + + sub->add_flag("-f,--follow", state.log.follow) + ->description("Keep reading the log until the logging process terminates."); + + sub->add_option("-r,--rules", state.log.readoutRules, "Log file to read.") + ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES."); + + auto* instance = addInstanceSelection(sub)->excludes(file); + addConfigSelection(sub)->excludes(instance)->excludes(file); + addLoggingOptions(sub, false); + + state.subcommand.log = sub; + } + + { + 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."); + + sub->add_flag("-j,--json", state.output.json, "Output the list as a json."); + + addConfigSelection(sub)->excludes(all); + addLoggingOptions(sub, false, true); + + state.subcommand.list = sub; + } + + { + auto* sub = cli.add_subcommand("kill", "Kill quickshell instances."); + //sub->add_flag("-a,--all", "Kill all matching instances instead of just one."); + auto* instance = addInstanceSelection(sub); + addConfigSelection(sub)->excludes(instance); + addLoggingOptions(sub, false, true); + + state.subcommand.kill = sub; + } + + { + auto* sub = cli.add_subcommand("msg", "Send messages to IpcHandlers.")->require_option(); + + auto* target = sub->add_option("target", state.ipc.target, "The target to message."); + + auto* function = sub->add_option("function", state.ipc.function) + ->description("The function to call in the target.") + ->needs(target); + + auto* arguments = sub->add_option("arguments", state.ipc.arguments) + ->description("Arguments to the called function.") + ->needs(function) + ->allow_extra_args(); + + sub->add_flag("-s,--show", state.ipc.info) + ->description("Print information about a function or target if given, or all available " + "targets if not.") + ->excludes(arguments); + + auto* instance = addInstanceSelection(sub); + addConfigSelection(sub)->excludes(instance); + addLoggingOptions(sub, false, true); + + sub->require_option(); + + state.subcommand.msg = sub; + } + + CLI11_PARSE(cli, argc, argv); + + if (state.misc.checkCompat) { + if (strcmp(qVersion(), QT_VERSION_STR) != 0) { + 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"; + return 1; + } + + return 0; + } + + // Has to happen before extra threads are spawned. + if (state.misc.daemonize) { + auto closepipes = std::array(); + if (pipe(closepipes.data()) == -1) { + qFatal().nospace() << "Failed to create messaging pipes for daemon with error " << errno + << ": " << qt_error_string(); + } + + DAEMON_PIPE = closepipes[1]; + + pid_t pid = fork(); // NOLINT (include) + + if (pid == -1) { + qFatal().nospace() << "Failed to fork daemon with error " << errno << ": " + << qt_error_string(); + } else if (pid == 0) { + close(closepipes[0]); + + if (setsid() == -1) { + qFatal().nospace() << "Failed to setsid with error " << errno << ": " << qt_error_string(); + } + } else { + close(closepipes[1]); + + int ret = 0; + if (read(closepipes[0], &ret, sizeof(int)) == -1) { + qFatal() << "Failed to wait for daemon launch (it may have crashed)"; + } + + return ret; + } + } + + { + auto level = state.log.verbosity == 0 ? QtWarningMsg + : state.log.verbosity == 1 ? QtInfoMsg + : QtDebugMsg; + + LogManager::init( + !state.log.noColor, + state.log.timestamp, + state.log.sparse, + level, + *state.log.rules, + *state.subcommand.log ? "READER" : "" + ); + } + + if (state.misc.printVersion) { + qCInfo(logBare).noquote().nospace() << "quickshell pre-release, revision " << GIT_REVISION + << ", distributed by: " << DISTRIBUTOR; + + if (state.log.verbosity > 1) { + qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; + qCInfo(logBare).noquote() << "Runtime Qt Version:" << qVersion(); + qCInfo(logBare).noquote() << "Compiler:" << COMPILER; + qCInfo(logBare).noquote() << "Compile Flags:" << COMPILE_FLAGS; + } + + if (state.log.verbosity > 0) { + qCInfo(logBare).noquote() << "\nBuild Type:" << BUILD_TYPE; + qCInfo(logBare).noquote() << "Build configuration:"; + qCInfo(logBare).noquote().nospace() << BUILD_CONFIGURATION; + } + } else if (*state.subcommand.log) { + return readLogFile(state); + } else if (*state.subcommand.list) { + return listInstances(state); + } else if (*state.subcommand.kill) { + return killInstances(state); + } else if (*state.subcommand.msg) { + return msgInstance(state); + } else { + if (strcmp(qVersion(), QT_VERSION_STR) != 0) { + qWarning() << "\033[31mQuickshell 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 " + "the quickshell package must be rebuilt.\n"; + } + + return launchFromCommand(state, coreApplication); + } + + return 0; +} + +int locateConfigFile(CommandState& cmd, QString& path) { + if (!cmd.config.path->isEmpty()) { + path = *cmd.config.path; + } else { + auto manifestPath = *cmd.config.manifest; + if (manifestPath.isEmpty()) { + auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); + auto path = configDir.filePath("manifest.conf"); + if (QFileInfo(path).isFile()) manifestPath = path; + } + + if (!manifestPath.isEmpty()) { + auto file = QFile(manifestPath); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + auto stream = QTextStream(&file); + while (!stream.atEnd()) { + auto line = stream.readLine(); + if (line.trimmed().startsWith("#")) continue; + if (line.trimmed().isEmpty()) continue; + + auto split = line.split('='); + if (split.length() != 2) { + qCritical() << "Manifest line not in expected format 'name = relativepath':" << line; + return -1; + } + + if (split[0].trimmed() == *cmd.config.name) { + path = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed()); + break; + } + } + + if (path.isEmpty()) { + qCCritical(logBare) << "Configuration" << *cmd.config.name + << "not found when searching manifest" << manifestPath; + return -1; + } + } else { + qCCritical(logBare) << "Could not open maifest at path" << *cmd.config.manifest; + return -1; + } + } else { + auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); + + if (cmd.config.name->isEmpty()) { + path = configDir.path(); + } else { + path = configDir.filePath(*cmd.config.name); + } + } + } + + if (QFileInfo(path).isDir()) { + path = QDir(path).filePath("shell.qml"); + } + + if (!QFileInfo(path).isFile()) { + qCCritical(logBare) << "Could not open config file at" << path; + return -1; + } + + path = QFileInfo(path).canonicalFilePath(); + + return 0; +} + +void sortInstances(QVector& list) { + std::sort(list.begin(), list.end(), [](const InstanceLockInfo& a, const InstanceLockInfo& b) { + return a.instance.launchTime < b.instance.launchTime; + }); +}; + +int selectInstance(CommandState& cmd, InstanceLockInfo* instance) { + auto* basePath = QsPaths::instance()->baseRunDir(); + if (!basePath) return -1; + + QString path; + + if (cmd.instance.pid != -1) { + path = QDir(basePath->filePath("by-pid")).filePath(QString::number(cmd.instance.pid)); + if (!QsPaths::checkLock(path, instance)) { + qCInfo(logBare) << "No instance found for pid" << cmd.instance.pid; + return -1; + } + } else if (!cmd.instance.id->isEmpty()) { + path = basePath->filePath("by-pid"); + auto instances = QsPaths::collectInstances(path); + + auto itr = + std::remove_if(instances.begin(), instances.end(), [&](const InstanceLockInfo& info) { + return !info.instance.instanceId.startsWith(*cmd.instance.id); + }); + + instances.erase(itr, instances.end()); + + if (instances.isEmpty()) { + qCInfo(logBare) << "No running instances start with" << *cmd.instance.id; + return -1; + } else if (instances.length() != 1) { + qCInfo(logBare) << "More than one instance starts with" << *cmd.instance.id; + + for (auto& instance: instances) { + qCInfo(logBare).noquote() << " -" << instance.instance.instanceId; + } + + return -1; + } else { + *instance = instances.value(0); + } + } else { + QString configFilePath; + auto r = locateConfigFile(cmd, configFilePath); + if (r != 0) return r; + + auto pathId = + QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + + path = QDir(basePath->filePath("by-path")).filePath(pathId); + + auto instances = QsPaths::collectInstances(path); + sortInstances(instances); + + if (instances.isEmpty()) { + qCInfo(logBare) << "No running instances for" << configFilePath; + return -1; + } + + *instance = instances.value(0); + } + + return 0; +} + +int readLogFile(CommandState& cmd) { + auto path = *cmd.log.file; + + if (path.isEmpty()) { + InstanceLockInfo instance; + auto r = selectInstance(cmd, &instance); + if (r != 0) return r; + + path = QDir(QsPaths::basePath(instance.instance.instanceId)).filePath("log.qslog"); + } + + auto file = QFile(path); + if (!file.open(QFile::ReadOnly)) { + qCCritical(logBare) << "Failed to open log file" << path; + return -1; + } + + return qs::log::readEncodedLogs( + &file, + path, + cmd.log.timestamp, + cmd.log.tail, + cmd.log.follow, + *cmd.log.readoutRules + ) + ? 0 + : -1; +} + +int listInstances(CommandState& cmd) { + auto* basePath = QsPaths::instance()->baseRunDir(); + if (!basePath) return -1; // NOLINT + + QString path; + QString configFilePath; + if (cmd.instance.all) { + path = basePath->filePath("by-pid"); + } else { + auto r = locateConfigFile(cmd, configFilePath); + + if (r != 0) { + qCInfo(logBare) << "Use --all to list all instances."; + return r; + } + + auto pathId = + QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + + path = QDir(basePath->filePath("by-path")).filePath(pathId); + } + + auto instances = QsPaths::collectInstances(path); + + if (instances.isEmpty()) { + if (cmd.instance.all) { + qCInfo(logBare) << "No running instances."; + } else { + qCInfo(logBare) << "No running instances for" << configFilePath; + qCInfo(logBare) << "Use --all to list all instances."; + } + } else { + sortInstances(instances); + + if (cmd.output.json) { + auto array = QJsonArray(); + + for (auto& instance: instances) { + auto json = QJsonObject(); + + json["id"] = instance.instance.instanceId; + json["pid"] = instance.pid; + json["shell_id"] = instance.instance.shellId; + json["config_path"] = instance.instance.configPath; + json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate); + + array.push_back(json); + } + + auto document = QJsonDocument(array); + QTextStream(stdout) << document.toJson(QJsonDocument::Indented); + } else { + for (auto& instance: instances) { + auto launchTimeStr = instance.instance.launchTime.toString("yyyy-MM-dd hh:mm:ss"); + + auto runSeconds = instance.instance.launchTime.secsTo(QDateTime::currentDateTime()); + auto remSeconds = runSeconds % 60; + auto runMinutes = (runSeconds - remSeconds) / 60; + auto remMinutes = runMinutes % 60; + auto runHours = (runMinutes - remMinutes) / 60; + auto runtimeStr = QString("%1 hours, %2 minutes, %3 seconds") + .arg(runHours) + .arg(remMinutes) + .arg(remSeconds); + + qCInfo(logBare).noquote().nospace() + << "Instance " << instance.instance.instanceId << ":\n" + << " Process ID: " << instance.pid << '\n' + << " Shell ID: " << instance.instance.shellId << '\n' + << " Config path: " << instance.instance.configPath << '\n' + << " Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n"; + } + } + } + + return 0; +} + +int killInstances(CommandState& cmd) { + InstanceLockInfo instance; + auto r = selectInstance(cmd, &instance); + if (r != 0) return r; + + return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { + client.kill(); + qCInfo(logBare).noquote() << "Killed" << instance.instance.instanceId; + }); +} + +int msgInstance(CommandState& cmd) { + InstanceLockInfo instance; + auto r = selectInstance(cmd, &instance); + if (r != 0) return r; + + return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { + if (cmd.ipc.info) { + return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.function); + } else { + QVector arguments; + for (auto& arg: cmd.ipc.arguments) { + arguments += *arg; + } + + return qs::io::ipc::comm::callFunction( + &client, + *cmd.ipc.target, + *cmd.ipc.function, + arguments + ); + } + + return -1; + }); +} + +template +QString base36Encode(T number) { + const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz"; + QString result; + + do { + result.prepend(digits[number % 36]); + number /= 36; + } while (number > 0); + + for (auto i = 0; i < result.length() / 2; i++) { + auto opposite = result.length() - i - 1; + auto c = result.at(i); + result[i] = result.at(opposite); + result[opposite] = c; + } + + return result; +} + +void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) { +#if CRASH_REPORTER + auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD"); + + if (!lastInfoFdStr.isEmpty()) { + auto lastInfoFd = lastInfoFdStr.toInt(); + + QFile file; + file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle); + file.seek(0); + + auto ds = QDataStream(&file); + RelaunchInfo info; + ds >> info; + + LogManager::init( + !info.noColor, + info.timestamp, + info.sparseLogsOnly, + info.defaultLogLevel, + info.logRules + ); + + qCritical().nospace() << "Quickshell has crashed under pid " + << qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt() + << " (Coredumps will be available under that pid.)"; + + qCritical() << "Further crash information is stored under" + << QsPaths::crashDir(info.instance.instanceId).path(); + + if (info.instance.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) { + qCritical() << "Quickshell crashed within 10 seconds of launching. Not restarting to avoid " + "a crash loop."; + exit(-1); // NOLINT + } else { + qCritical() << "Quickshell has been restarted."; + + launch({.configPath = info.instance.configPath}, argv, coreApplication); + } + } +#endif +} + +int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication) { + QString configPath; + + auto r = locateConfigFile(cmd, configPath); + if (r != 0) return r; + + { + InstanceLockInfo info; + if (cmd.misc.noDuplicate && selectInstance(cmd, &info) == 0) { + qCInfo(logBare) << "An instance of this configuration is already running."; + return 0; + } + } + + return launch( + { + .configPath = configPath, + .debugPort = cmd.debug.port, + .waitForDebug = cmd.debug.wait, + }, + cmd.exec.argv, + coreApplication + ); +} + +int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication) { + auto pathId = QCryptographicHash::hash(args.configPath.toUtf8(), QCryptographicHash::Md5).toHex(); + auto shellId = QString(pathId); + + qInfo() << "Launching config:" << args.configPath; + + auto file = QFile(args.configPath); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + qCritical() << "Could not open config file" << args.configPath; + return -1; + } + + struct { + bool useQApplication = false; + bool nativeTextRendering = false; + bool desktopSettingsAware = true; + QString iconTheme = qEnvironmentVariable("QS_ICON_THEME"); + QHash envOverrides; + } pragmas; + + auto stream = QTextStream(&file); + while (!stream.atEnd()) { + auto line = stream.readLine().trimmed(); + if (line.startsWith("//@ pragma ")) { + auto pragma = line.sliced(11).trimmed(); + + if (pragma == "UseQApplication") pragmas.useQApplication = true; + else if (pragma == "NativeTextRendering") pragmas.nativeTextRendering = true; + else if (pragma == "IgnoreSystemSettings") pragmas.desktopSettingsAware = false; + else if (pragma.startsWith("IconTheme ")) pragmas.iconTheme = pragma.sliced(10); + else if (pragma.startsWith("Env ")) { + auto envPragma = pragma.sliced(4); + auto splitIdx = envPragma.indexOf('='); + + if (splitIdx == -1) { + qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'"; + return -1; + } + + auto var = envPragma.sliced(0, splitIdx).trimmed(); + auto val = envPragma.sliced(splitIdx + 1).trimmed(); + pragmas.envOverrides.insert(var, val); + } else if (pragma.startsWith("ShellId ")) { + shellId = pragma.sliced(8).trimmed(); + } else { + qCritical() << "Unrecognized pragma" << pragma; + return -1; + } + } else if (line.startsWith("import")) break; + } + + file.close(); + + if (!pragmas.iconTheme.isEmpty()) { + QIcon::setThemeName(pragmas.iconTheme); + } + + qInfo() << "Shell ID:" << shellId << "Path ID" << pathId; + + auto launchTime = qs::Common::LAUNCH_TIME.toSecsSinceEpoch(); + InstanceInfo::CURRENT = InstanceInfo { + .instanceId = base36Encode(getpid()) + base36Encode(launchTime), + .configPath = args.configPath, + .shellId = shellId, + .launchTime = qs::Common::LAUNCH_TIME, + }; + +#if CRASH_REPORTER + auto crashHandler = crash::CrashHandler(); + crashHandler.init(); + + { + auto* log = LogManager::instance(); + crashHandler.setRelaunchInfo({ + .instance = InstanceInfo::CURRENT, + .noColor = !log->colorLogs, + .timestamp = log->timestampLogs, + .sparseLogsOnly = log->isSparse(), + .defaultLogLevel = log->defaultLevel(), + .logRules = log->rulesString(), + }); + } +#endif + + QsPaths::init(shellId, pathId); + QsPaths::instance()->linkRunDir(); + QsPaths::instance()->linkPathDir(); + LogManager::initFs(); + + for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) { + qputenv(var.toUtf8(), val.toUtf8()); + } + + // The qml engine currently refuses to cache non file (qsintercept) paths. + + // if (auto* cacheDir = QsPaths::instance()->cacheDir()) { + // auto qmlCacheDir = cacheDir->filePath("qml-engine-cache"); + // qputenv("QML_DISK_CACHE_PATH", qmlCacheDir.toLocal8Bit()); + // + // if (!qEnvironmentVariableIsSet("QML_DISK_CACHE")) { + // qputenv("QML_DISK_CACHE", "aot,qmlc"); + // } + // } + + // While the simple animation driver can lead to better animations in some cases, + // it also can cause excessive repainting at excessively high framerates which can + // lead to noticeable amounts of gpu usage, including overheating on some systems. + // This gets worse the more windows are open, as repaints trigger on all of them for + // some reason. See QTBUG-126099 for details. + + // if (!qEnvironmentVariableIsSet("QSG_USE_SIMPLE_ANIMATION_DRIVER")) { + // qputenv("QSG_USE_SIMPLE_ANIMATION_DRIVER", "1"); + // } + + // Some programs place icons in the pixmaps folder instead of the icons folder. + // This seems to be controlled by the QPA and qt6ct does not provide it. + { + QList dataPaths; + + 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"); + } + + auto fallbackPaths = QIcon::fallbackSearchPaths(); + + for (auto& path: dataPaths) { + auto newPath = QDir(path).filePath("pixmaps"); + + if (!fallbackPaths.contains(newPath)) { + fallbackPaths.push_back(newPath); + } + } + + QIcon::setFallbackSearchPaths(fallbackPaths); + } + + QGuiApplication::setDesktopSettingsAware(pragmas.desktopSettingsAware); + + delete coreApplication; + + QGuiApplication* app = nullptr; + auto qArgC = 0; + + if (pragmas.useQApplication) { + app = new QApplication(qArgC, argv); + } else { + app = new QGuiApplication(qArgC, argv); + } + + if (args.debugPort != -1) { + QQmlDebuggingEnabler::enableDebugging(true); + auto wait = args.waitForDebug ? QQmlDebuggingEnabler::WaitForClient + : QQmlDebuggingEnabler::DoNotWaitForClient; + QQmlDebuggingEnabler::startTcpDebugServer(args.debugPort, wait); + } + + QuickshellPlugin::initPlugins(); + + // Base window transparency appears to be additive. + // Use a fully transparent window with a colored rect. + QQuickWindow::setDefaultAlphaBuffer(true); + + if (pragmas.nativeTextRendering) { + QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); + } + + qs::ipc::IpcServer::start(); + QsPaths::instance()->createLock(); + + auto root = RootWrapper(args.configPath, shellId); + QGuiApplication::setQuitOnLastWindowClosed(false); + + exitDaemon(0); + + auto code = QGuiApplication::exec(); + delete app; + return code; +} + +} // namespace qs::launch int main(int argc, char** argv) { return qs::launch::main(argc, argv); } diff --git a/src/wayland/hyprland/focus_grab/qml.cpp b/src/wayland/hyprland/focus_grab/qml.cpp index 9ae309ff..9d09fc44 100644 --- a/src/wayland/hyprland/focus_grab/qml.cpp +++ b/src/wayland/hyprland/focus_grab/qml.cpp @@ -8,8 +8,8 @@ #include #include -#include "../../../core/proxywindow.hpp" -#include "../../../core/windowinterface.hpp" +#include "../../../window/proxywindow.hpp" +#include "../../../window/windowinterface.hpp" #include "grab.hpp" #include "manager.hpp" diff --git a/src/wayland/toplevel_management/qml.cpp b/src/wayland/toplevel_management/qml.cpp index 1f0d1fe8..153ed1fc 100644 --- a/src/wayland/toplevel_management/qml.cpp +++ b/src/wayland/toplevel_management/qml.cpp @@ -3,11 +3,11 @@ #include #include -#include "../../core/util.hpp" #include "../../core/model.hpp" -#include "../../core/proxywindow.hpp" #include "../../core/qmlscreen.hpp" -#include "../../core/windowinterface.hpp" +#include "../../core/util.hpp" +#include "../../window/proxywindow.hpp" +#include "../../window/windowinterface.hpp" #include "handle.hpp" #include "manager.hpp" diff --git a/src/wayland/toplevel_management/qml.hpp b/src/wayland/toplevel_management/qml.hpp index d50713c5..7fa7b65a 100644 --- a/src/wayland/toplevel_management/qml.hpp +++ b/src/wayland/toplevel_management/qml.hpp @@ -5,9 +5,9 @@ #include #include "../../core/model.hpp" -#include "../../core/proxywindow.hpp" #include "../../core/qmlscreen.hpp" #include "../../core/util.hpp" +#include "../../window/proxywindow.hpp" namespace qs::wayland::toplevel_management { diff --git a/src/wayland/wlr_layershell.cpp b/src/wayland/wlr_layershell.cpp index 6a381c01..1ce7b7fc 100644 --- a/src/wayland/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell.cpp @@ -9,9 +9,9 @@ #include #include -#include "../core/panelinterface.hpp" -#include "../core/proxywindow.hpp" #include "../core/qmlscreen.hpp" +#include "../window/panelinterface.hpp" +#include "../window/proxywindow.hpp" #include "wlr_layershell/window.hpp" WlrLayershell::WlrLayershell(QObject* parent) diff --git a/src/wayland/wlr_layershell.hpp b/src/wayland/wlr_layershell.hpp index 7687c4f3..e7a1a077 100644 --- a/src/wayland/wlr_layershell.hpp +++ b/src/wayland/wlr_layershell.hpp @@ -8,8 +8,8 @@ #include #include "../core/doc.hpp" -#include "../core/panelinterface.hpp" -#include "../core/proxywindow.hpp" +#include "../window/panelinterface.hpp" +#include "../window/proxywindow.hpp" #include "wlr_layershell/window.hpp" ///! Wlroots layershell window diff --git a/src/wayland/wlr_layershell/surface.cpp b/src/wayland/wlr_layershell/surface.cpp index ca5e7d10..25b58ff8 100644 --- a/src/wayland/wlr_layershell/surface.cpp +++ b/src/wayland/wlr_layershell/surface.cpp @@ -14,7 +14,7 @@ #include #include -#include "../../core/panelinterface.hpp" +#include "../../window/panelinterface.hpp" #include "shell_integration.hpp" #include "window.hpp" diff --git a/src/wayland/wlr_layershell/window.cpp b/src/wayland/wlr_layershell/window.cpp index a671d59e..8139e841 100644 --- a/src/wayland/wlr_layershell/window.cpp +++ b/src/wayland/wlr_layershell/window.cpp @@ -9,7 +9,7 @@ #include #include -#include "../../core/panelinterface.hpp" +#include "../../window/panelinterface.hpp" #include "shell_integration.hpp" #include "surface.hpp" diff --git a/src/wayland/wlr_layershell/window.hpp b/src/wayland/wlr_layershell/window.hpp index ea38e6e9..59711b5f 100644 --- a/src/wayland/wlr_layershell/window.hpp +++ b/src/wayland/wlr_layershell/window.hpp @@ -7,7 +7,7 @@ #include #include -#include "../../core/panelinterface.hpp" +#include "../../window/panelinterface.hpp" ///! WlrLayershell layer. /// See @@WlrLayershell.layer. diff --git a/src/window/CMakeLists.txt b/src/window/CMakeLists.txt new file mode 100644 index 00000000..6b14c58a --- /dev/null +++ b/src/window/CMakeLists.txt @@ -0,0 +1,23 @@ +qt_add_library(quickshell-window STATIC + proxywindow.cpp + windowinterface.cpp + panelinterface.cpp + floatingwindow.cpp + popupwindow.cpp +) + +qt_add_qml_module(quickshell-window + URI Quickshell._Window + VERSION 0.1 +) + +target_link_libraries(quickshell-window PRIVATE ${QT_DEPS} Qt6::QuickPrivate) + +qs_pch(quickshell-window) +qs_pch(quickshell-windowplugin) + +target_link_libraries(quickshell PRIVATE quickshell-windowplugin) + +if (BUILD_TESTING) + add_subdirectory(test) +endif() diff --git a/src/core/floatingwindow.cpp b/src/window/floatingwindow.cpp similarity index 100% rename from src/core/floatingwindow.cpp rename to src/window/floatingwindow.cpp diff --git a/src/core/floatingwindow.hpp b/src/window/floatingwindow.hpp similarity index 100% rename from src/core/floatingwindow.hpp rename to src/window/floatingwindow.hpp diff --git a/src/core/panelinterface.cpp b/src/window/panelinterface.cpp similarity index 100% rename from src/core/panelinterface.cpp rename to src/window/panelinterface.cpp diff --git a/src/core/panelinterface.hpp b/src/window/panelinterface.hpp similarity index 99% rename from src/core/panelinterface.hpp rename to src/window/panelinterface.hpp index 78665df3..5ccb5186 100644 --- a/src/core/panelinterface.hpp +++ b/src/window/panelinterface.hpp @@ -3,7 +3,7 @@ #include #include -#include "doc.hpp" +#include "../core/doc.hpp" #include "windowinterface.hpp" class Anchors { diff --git a/src/core/popupwindow.cpp b/src/window/popupwindow.cpp similarity index 98% rename from src/core/popupwindow.cpp rename to src/window/popupwindow.cpp index 7a3d9316..b355238e 100644 --- a/src/core/popupwindow.cpp +++ b/src/window/popupwindow.cpp @@ -7,9 +7,8 @@ #include #include -#include "popupanchor.hpp" -#include "proxywindow.hpp" -#include "qmlscreen.hpp" +#include "../core/popupanchor.hpp" +#include "../core/qmlscreen.hpp" #include "windowinterface.hpp" ProxyPopupWindow::ProxyPopupWindow(QObject* parent): ProxyWindowBase(parent) { diff --git a/src/core/popupwindow.hpp b/src/window/popupwindow.hpp similarity index 97% rename from src/core/popupwindow.hpp rename to src/window/popupwindow.hpp index 47db4038..bb245eb8 100644 --- a/src/core/popupwindow.hpp +++ b/src/window/popupwindow.hpp @@ -6,10 +6,10 @@ #include #include -#include "doc.hpp" -#include "popupanchor.hpp" +#include "../core/doc.hpp" +#include "../core/popupanchor.hpp" +#include "../core/qmlscreen.hpp" #include "proxywindow.hpp" -#include "qmlscreen.hpp" #include "windowinterface.hpp" ///! Popup window. diff --git a/src/core/proxywindow.cpp b/src/window/proxywindow.cpp similarity index 98% rename from src/core/proxywindow.cpp rename to src/window/proxywindow.cpp index 5d4659dd..07f8a233 100644 --- a/src/core/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -14,11 +14,11 @@ #include #include -#include "generation.hpp" -#include "qmlglobal.hpp" -#include "qmlscreen.hpp" -#include "region.hpp" -#include "reload.hpp" +#include "../core/generation.hpp" +#include "../core/qmlglobal.hpp" +#include "../core/qmlscreen.hpp" +#include "../core/region.hpp" +#include "../core/reload.hpp" #include "windowinterface.hpp" ProxyWindowBase::ProxyWindowBase(QObject* parent) diff --git a/src/core/proxywindow.hpp b/src/window/proxywindow.hpp similarity index 97% rename from src/core/proxywindow.hpp rename to src/window/proxywindow.hpp index ce8228fe..dbbf1910 100644 --- a/src/core/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -12,10 +12,9 @@ #include #include -#include "qmlglobal.hpp" -#include "qmlscreen.hpp" -#include "region.hpp" -#include "reload.hpp" +#include "../core/qmlscreen.hpp" +#include "../core/region.hpp" +#include "../core/reload.hpp" #include "windowinterface.hpp" // Proxy to an actual window exposing a limited property set with the ability to diff --git a/src/window/test/CMakeLists.txt b/src/window/test/CMakeLists.txt new file mode 100644 index 00000000..ad9e5a0a --- /dev/null +++ b/src/window/test/CMakeLists.txt @@ -0,0 +1,7 @@ +function (qs_test name) + add_executable(${name} ${ARGN}) + target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test quickshell-window quickshell-core) + add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) +endfunction() + +qs_test(popupwindow popupwindow.cpp) diff --git a/src/core/test/popupwindow.cpp b/src/window/test/popupwindow.cpp similarity index 100% rename from src/core/test/popupwindow.cpp rename to src/window/test/popupwindow.cpp diff --git a/src/window/test/popupwindow.hpp b/src/window/test/popupwindow.hpp new file mode 100644 index 00000000..bebc5154 --- /dev/null +++ b/src/window/test/popupwindow.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +class TestPopupWindow: public QObject { + Q_OBJECT; + +private slots: + void initiallyVisible(); + void reloadReparent(); + void reloadUnparent(); + void invisibleWithoutParent(); + void moveWithParent(); + void attachParentLate(); + void reparentLate(); + void xMigrationFix(); +}; diff --git a/src/core/windowinterface.cpp b/src/window/windowinterface.cpp similarity index 100% rename from src/core/windowinterface.cpp rename to src/window/windowinterface.cpp diff --git a/src/core/windowinterface.hpp b/src/window/windowinterface.hpp similarity index 98% rename from src/core/windowinterface.hpp rename to src/window/windowinterface.hpp index f90df24c..c351cf34 100644 --- a/src/core/windowinterface.hpp +++ b/src/window/windowinterface.hpp @@ -8,9 +8,9 @@ #include #include -#include "qmlscreen.hpp" -#include "region.hpp" -#include "reload.hpp" +#include "../core/qmlscreen.hpp" +#include "../core/region.hpp" +#include "../core/reload.hpp" class ProxyWindowBase; class QsWindowAttached; diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index 3f0aa279..d0441adf 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -17,9 +17,9 @@ #include #include "../core/generation.hpp" -#include "../core/panelinterface.hpp" -#include "../core/proxywindow.hpp" #include "../core/qmlscreen.hpp" +#include "../window/panelinterface.hpp" +#include "../window/proxywindow.hpp" #include "util.hpp" class XPanelStack { diff --git a/src/x11/panel_window.hpp b/src/x11/panel_window.hpp index 9bcaf64d..b37c9c50 100644 --- a/src/x11/panel_window.hpp +++ b/src/x11/panel_window.hpp @@ -8,8 +8,8 @@ #include #include "../core/doc.hpp" -#include "../core/panelinterface.hpp" -#include "../core/proxywindow.hpp" +#include "../window/panelinterface.hpp" +#include "../window/proxywindow.hpp" class XPanelStack; From 9980f8587e90971af1e512cc120b1e50d8320191 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 31 Oct 2024 01:28:06 -0700 Subject: [PATCH 188/305] window: generate qmltypes --- src/core/CMakeLists.txt | 3 ++- src/core/doc.hpp | 3 +++ src/core/module.md | 10 +++++----- src/core/plugin.cpp | 4 ++++ src/core/plugin.hpp | 3 +++ src/wayland/init.cpp | 3 +++ src/window/CMakeLists.txt | 7 ++++++- src/window/init.cpp | 22 ++++++++++++++++++++++ src/window/panelinterface.hpp | 4 +++- 9 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 src/window/init.cpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b454f3a7..8c8d077d 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -44,7 +44,8 @@ target_link_libraries(quickshell-core PRIVATE quickshell-build) qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1 - IMPORTS Quickshell._Window + OPTIONAL_IMPORTS Quickshell._Window + DEFAULT_IMPORTS Quickshell._Window ) target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} CLI11::CLI11) diff --git a/src/core/doc.hpp b/src/core/doc.hpp index e1f2ee4c..f7db7ac4 100644 --- a/src/core/doc.hpp +++ b/src/core/doc.hpp @@ -10,6 +10,9 @@ #define QSDOC_ELEMENT #define QSDOC_NAMED_ELEMENT(name) +// unmark uncreatable (will be overlayed by other types) +#define QSDOC_CREATABLE + // change the cname used for this type #define QSDOC_CNAME(name) diff --git a/src/core/module.md b/src/core/module.md index 060aca9f..831f561b 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -7,12 +7,12 @@ headers = [ "shell.hpp", "variants.hpp", "region.hpp", - "proxywindow.hpp", + "../window/proxywindow.hpp", "persistentprops.hpp", - "windowinterface.hpp", - "panelinterface.hpp", - "floatingwindow.hpp", - "popupwindow.hpp", + "../window/windowinterface.hpp", + "../window/panelinterface.hpp", + "../window/floatingwindow.hpp", + "../window/popupwindow.hpp", "singleton.hpp", "lazyloader.hpp", "easingcurve.hpp", diff --git a/src/core/plugin.cpp b/src/core/plugin.cpp index 8f1d0e96..697406a9 100644 --- a/src/core/plugin.cpp +++ b/src/core/plugin.cpp @@ -19,6 +19,10 @@ void QuickshellPlugin::initPlugins() { plugins.end() ); + std::sort(plugins.begin(), plugins.end(), [](QuickshellPlugin* a, QuickshellPlugin* b) { + return b->dependencies().contains(a->name()); + }); + for (QuickshellPlugin* plugin: plugins) { plugin->init(); } diff --git a/src/core/plugin.hpp b/src/core/plugin.hpp index 38c9ddc2..082eb035 100644 --- a/src/core/plugin.hpp +++ b/src/core/plugin.hpp @@ -2,6 +2,7 @@ #include #include +#include class EngineGeneration; @@ -14,6 +15,8 @@ public: void operator=(QuickshellPlugin&&) = delete; void operator=(const QuickshellPlugin&) = delete; + virtual QString name() { return QString(); } + virtual QList dependencies() { return {}; } virtual bool applies() { return true; } virtual void init() {} virtual void registerTypes() {} diff --git a/src/wayland/init.cpp b/src/wayland/init.cpp index 1ad51cea..a9ddcadb 100644 --- a/src/wayland/init.cpp +++ b/src/wayland/init.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -15,6 +16,8 @@ void installPopupPositioner(); namespace { class WaylandPlugin: public QuickshellPlugin { + QList dependencies() override { return {"window"}; } + bool applies() override { auto isWayland = QGuiApplication::platformName() == "wayland"; diff --git a/src/window/CMakeLists.txt b/src/window/CMakeLists.txt index 6b14c58a..d415533e 100644 --- a/src/window/CMakeLists.txt +++ b/src/window/CMakeLists.txt @@ -11,12 +11,17 @@ qt_add_qml_module(quickshell-window VERSION 0.1 ) +add_library(quickshell-window-init OBJECT init.cpp) + target_link_libraries(quickshell-window PRIVATE ${QT_DEPS} Qt6::QuickPrivate) +target_link_libraries(quickshell-windowplugin PRIVATE ${QT_DEPS}) +target_link_libraries(quickshell-window-init PRIVATE ${QT_DEPS}) qs_pch(quickshell-window) qs_pch(quickshell-windowplugin) +qs_pch(quickshell-window-init) -target_link_libraries(quickshell PRIVATE quickshell-windowplugin) +target_link_libraries(quickshell PRIVATE quickshell-windowplugin quickshell-window-init) if (BUILD_TESTING) add_subdirectory(test) diff --git a/src/window/init.cpp b/src/window/init.cpp new file mode 100644 index 00000000..ef2b8c1d --- /dev/null +++ b/src/window/init.cpp @@ -0,0 +1,22 @@ +#include "../core/plugin.hpp" + +namespace { + +class WindowPlugin: public QuickshellPlugin { + // _Window has to be registered before wayland or x11 modules, otherwise module overlays + // will apply in the wrong order. + QString name() override { return "window"; } + + void registerTypes() override { + qmlRegisterModuleImport( + "Quickshell", + QQmlModuleImportModuleAny, + "Quickshell._Window", + QQmlModuleImportLatest + ); + } +}; + +QS_REGISTER_PLUGIN(WindowPlugin); + +} // namespace diff --git a/src/window/panelinterface.hpp b/src/window/panelinterface.hpp index 5ccb5186..ada01a7f 100644 --- a/src/window/panelinterface.hpp +++ b/src/window/panelinterface.hpp @@ -128,7 +128,9 @@ class PanelWindowInterface: public WindowInterface { /// Note: On Wayland this property corrosponds to @@Quickshell.Wayland.WlrLayershell.keyboardFocus. Q_PROPERTY(bool focusable READ focusable WRITE setFocusable NOTIFY focusableChanged); // clang-format on - QSDOC_NAMED_ELEMENT(PanelWindow); + QML_NAMED_ELEMENT(PanelWindow); + QML_UNCREATABLE("No PanelWindow backend loaded."); + QSDOC_CREATABLE; public: explicit PanelWindowInterface(QObject* parent = nullptr): WindowInterface(parent) {} From a931adf033d93e9e63302f26008983959e2fbf35 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 31 Oct 2024 13:45:26 -0700 Subject: [PATCH 189/305] all: add DEPENDENCIES entries to qml modules Fixes some qmlls/qmllint issues. --- src/core/CMakeLists.txt | 1 + src/dbus/dbusmenu/CMakeLists.txt | 6 +++++- src/io/CMakeLists.txt | 1 + src/services/greetd/CMakeLists.txt | 1 + src/services/mpris/CMakeLists.txt | 1 + src/services/notifications/CMakeLists.txt | 1 + src/services/pam/CMakeLists.txt | 1 + src/services/pipewire/CMakeLists.txt | 1 + src/services/status_notifier/CMakeLists.txt | 1 + src/services/upower/CMakeLists.txt | 1 + src/wayland/CMakeLists.txt | 1 + src/wayland/hyprland/focus_grab/CMakeLists.txt | 1 + src/wayland/hyprland/global_shortcuts/CMakeLists.txt | 1 + src/wayland/hyprland/ipc/CMakeLists.txt | 1 + src/wayland/toplevel_management/CMakeLists.txt | 1 + src/wayland/wlr_layershell/CMakeLists.txt | 8 +++++++- src/window/CMakeLists.txt | 1 + 17 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 8c8d077d..8d50f1f5 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -44,6 +44,7 @@ target_link_libraries(quickshell-core PRIVATE quickshell-build) qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1 + DEPENDENCIES QtQuick OPTIONAL_IMPORTS Quickshell._Window DEFAULT_IMPORTS Quickshell._Window ) diff --git a/src/dbus/dbusmenu/CMakeLists.txt b/src/dbus/dbusmenu/CMakeLists.txt index ab222e5a..9e4885a8 100644 --- a/src/dbus/dbusmenu/CMakeLists.txt +++ b/src/dbus/dbusmenu/CMakeLists.txt @@ -14,7 +14,11 @@ qt_add_library(quickshell-dbusmenu STATIC ${DBUS_INTERFACES} ) -qt_add_qml_module(quickshell-dbusmenu URI Quickshell.DBusMenu VERSION 0.1) +qt_add_qml_module(quickshell-dbusmenu + URI Quickshell.DBusMenu + VERSION 0.1 + DEPENDENCIES QtQml Quickshell +) # dbus headers target_include_directories(quickshell-dbusmenu PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index 389b8a6f..54cfac00 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -16,6 +16,7 @@ endif() qt_add_qml_module(quickshell-io URI Quickshell.Io VERSION 0.1 + DEPENDENCIES QtQml QML_FILES FileView.qml ) diff --git a/src/services/greetd/CMakeLists.txt b/src/services/greetd/CMakeLists.txt index 3c8fcf3b..349d28ce 100644 --- a/src/services/greetd/CMakeLists.txt +++ b/src/services/greetd/CMakeLists.txt @@ -6,6 +6,7 @@ qt_add_library(quickshell-service-greetd STATIC qt_add_qml_module(quickshell-service-greetd URI Quickshell.Services.Greetd VERSION 0.1 + DEPENDENCIES QtQml ) target_link_libraries(quickshell-service-greetd PRIVATE ${QT_DEPS}) diff --git a/src/services/mpris/CMakeLists.txt b/src/services/mpris/CMakeLists.txt index 3ee96061..f87edecc 100644 --- a/src/services/mpris/CMakeLists.txt +++ b/src/services/mpris/CMakeLists.txt @@ -30,6 +30,7 @@ target_include_directories(quickshell-service-mpris PRIVATE ${CMAKE_CURRENT_BINA qt_add_qml_module(quickshell-service-mpris URI Quickshell.Services.Mpris VERSION 0.1 + DEPENDENCIES QtQml Quickshell ) target_link_libraries(quickshell-service-mpris PRIVATE ${QT_DEPS} quickshell-dbus) diff --git a/src/services/notifications/CMakeLists.txt b/src/services/notifications/CMakeLists.txt index cc986a84..23f4d692 100644 --- a/src/services/notifications/CMakeLists.txt +++ b/src/services/notifications/CMakeLists.txt @@ -20,6 +20,7 @@ target_include_directories(quickshell-service-notifications PRIVATE ${CMAKE_CURR qt_add_qml_module(quickshell-service-notifications URI Quickshell.Services.Notifications VERSION 0.1 + DEPENDENCIES QtQml Quickshell ) target_link_libraries(quickshell-service-notifications PRIVATE ${QT_DEPS} quickshell-dbus) diff --git a/src/services/pam/CMakeLists.txt b/src/services/pam/CMakeLists.txt index fef23578..a55da739 100644 --- a/src/services/pam/CMakeLists.txt +++ b/src/services/pam/CMakeLists.txt @@ -8,6 +8,7 @@ qt_add_library(quickshell-service-pam STATIC qt_add_qml_module(quickshell-service-pam URI Quickshell.Services.Pam VERSION 0.1 + DEPENDENCIES QtQml ) target_link_libraries(quickshell-service-pam PRIVATE ${QT_DEPS} pam ${PAM_LIBRARIES}) diff --git a/src/services/pipewire/CMakeLists.txt b/src/services/pipewire/CMakeLists.txt index 6996eff7..8c33e644 100644 --- a/src/services/pipewire/CMakeLists.txt +++ b/src/services/pipewire/CMakeLists.txt @@ -16,6 +16,7 @@ qt_add_library(quickshell-service-pipewire STATIC qt_add_qml_module(quickshell-service-pipewire URI Quickshell.Services.Pipewire VERSION 0.1 + DEPENDENCIES QtQml Quickshell ) target_link_libraries(quickshell-service-pipewire PRIVATE ${QT_DEPS} PkgConfig::pipewire) diff --git a/src/services/status_notifier/CMakeLists.txt b/src/services/status_notifier/CMakeLists.txt index 79026836..bc8918d0 100644 --- a/src/services/status_notifier/CMakeLists.txt +++ b/src/services/status_notifier/CMakeLists.txt @@ -41,6 +41,7 @@ target_include_directories(quickshell-service-statusnotifier PRIVATE ${CMAKE_CUR qt_add_qml_module(quickshell-service-statusnotifier URI Quickshell.Services.SystemTray VERSION 0.1 + DEPENDENCIES QtQml Quickshell Quickshell.DBusMenu ) target_link_libraries(quickshell-service-statusnotifier PRIVATE ${QT_DEPS} quickshell-dbus quickshell-dbusmenuplugin) diff --git a/src/services/upower/CMakeLists.txt b/src/services/upower/CMakeLists.txt index 59987c40..e600f8c7 100644 --- a/src/services/upower/CMakeLists.txt +++ b/src/services/upower/CMakeLists.txt @@ -30,6 +30,7 @@ target_include_directories(quickshell-service-upower PRIVATE ${CMAKE_CURRENT_BIN qt_add_qml_module(quickshell-service-upower URI Quickshell.Services.UPower VERSION 0.1 + DEPENDENCIES QtQml Quickshell ) target_link_libraries(quickshell-service-upower PRIVATE ${QT_DEPS} quickshell-dbus) diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 568edc42..ccb31b07 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -106,6 +106,7 @@ target_link_libraries(quickshell-wayland-init PRIVATE ${QT_DEPS}) qt_add_qml_module(quickshell-wayland URI Quickshell.Wayland VERSION 0.1 + DEPENDENCIES QtQuick Quickshell IMPORTS ${WAYLAND_MODULES} ) diff --git a/src/wayland/hyprland/focus_grab/CMakeLists.txt b/src/wayland/hyprland/focus_grab/CMakeLists.txt index 1e37c9fe..a17436ef 100644 --- a/src/wayland/hyprland/focus_grab/CMakeLists.txt +++ b/src/wayland/hyprland/focus_grab/CMakeLists.txt @@ -7,6 +7,7 @@ qt_add_library(quickshell-hyprland-focus-grab STATIC qt_add_qml_module(quickshell-hyprland-focus-grab URI Quickshell.Hyprland._FocusGrab VERSION 0.1 + DEPENDENCIES QtQml Quickshell ) wl_proto(quickshell-hyprland-focus-grab diff --git a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt index 2ccfb74d..cebaa652 100644 --- a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt +++ b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt @@ -7,6 +7,7 @@ qt_add_library(quickshell-hyprland-global-shortcuts STATIC qt_add_qml_module(quickshell-hyprland-global-shortcuts URI Quickshell.Hyprland._GlobalShortcuts VERSION 0.1 + DEPENDENCIES QtQml ) wl_proto(quickshell-hyprland-global-shortcuts diff --git a/src/wayland/hyprland/ipc/CMakeLists.txt b/src/wayland/hyprland/ipc/CMakeLists.txt index 59200462..c2e32888 100644 --- a/src/wayland/hyprland/ipc/CMakeLists.txt +++ b/src/wayland/hyprland/ipc/CMakeLists.txt @@ -8,6 +8,7 @@ qt_add_library(quickshell-hyprland-ipc STATIC qt_add_qml_module(quickshell-hyprland-ipc URI Quickshell.Hyprland._Ipc VERSION 0.1 + DEPENDENCIES QtQml Quickshell ) target_link_libraries(quickshell-hyprland-ipc PRIVATE ${QT_DEPS}) diff --git a/src/wayland/toplevel_management/CMakeLists.txt b/src/wayland/toplevel_management/CMakeLists.txt index 4537c201..6ea0c254 100644 --- a/src/wayland/toplevel_management/CMakeLists.txt +++ b/src/wayland/toplevel_management/CMakeLists.txt @@ -7,6 +7,7 @@ qt_add_library(quickshell-wayland-toplevel-management STATIC qt_add_qml_module(quickshell-wayland-toplevel-management URI Quickshell.Wayland._ToplevelManagement VERSION 0.1 + DEPENDENCIES QtQml Quickshell Quickshell.Wayland ) wl_proto(quickshell-wayland-toplevel-management diff --git a/src/wayland/wlr_layershell/CMakeLists.txt b/src/wayland/wlr_layershell/CMakeLists.txt index 9f82826b..d5439f60 100644 --- a/src/wayland/wlr_layershell/CMakeLists.txt +++ b/src/wayland/wlr_layershell/CMakeLists.txt @@ -4,7 +4,13 @@ qt_add_library(quickshell-wayland-layershell STATIC window.cpp ) -qt_add_qml_module(quickshell-wayland-layershell URI Quickshell.Wayland._WlrLayerShell VERSION 0.1) +qt_add_qml_module(quickshell-wayland-layershell + URI Quickshell.Wayland._WlrLayerShell + VERSION 0.1 + # Quickshell.Wayland currently creates a dependency cycle, add it here once the main + # ls class is moved to this module. + DEPENDENCIES QtQuick Quickshell +) wl_proto(quickshell-wayland-layershell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/wlr-layer-shell-unstable-v1.xml") target_link_libraries(quickshell-wayland-layershell PRIVATE ${QT_DEPS} wayland-client) diff --git a/src/window/CMakeLists.txt b/src/window/CMakeLists.txt index d415533e..7b140946 100644 --- a/src/window/CMakeLists.txt +++ b/src/window/CMakeLists.txt @@ -9,6 +9,7 @@ qt_add_library(quickshell-window STATIC qt_add_qml_module(quickshell-window URI Quickshell._Window VERSION 0.1 + DEPENDENCIES QtQuick Quickshell ) add_library(quickshell-window-init OBJECT init.cpp) From 746b0e70d70539b84624ba6dbeb936f18e68eb34 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 1 Nov 2024 01:43:22 -0700 Subject: [PATCH 190/305] all: use fully qualified type names in Q_PROPERTY Fixes type deduction issues with qmllint/qmlls. --- src/core/qsmenu.hpp | 6 ++-- src/core/qsmenuanchor.hpp | 2 +- src/dbus/dbusmenu/dbusmenu.hpp | 4 +-- src/services/mpris/player.hpp | 4 +-- src/services/mpris/watcher.hpp | 2 +- src/services/notifications/notification.hpp | 6 ++-- src/services/notifications/qml.hpp | 2 +- src/services/pipewire/qml.hpp | 34 +++++++++++---------- src/services/status_notifier/qml.hpp | 2 +- src/services/upower/core.hpp | 6 ++-- src/services/upower/device.hpp | 4 +-- src/wayland/hyprland/ipc/monitor.hpp | 4 ++- src/wayland/hyprland/ipc/qml.hpp | 8 +++-- src/wayland/toplevel_management/qml.hpp | 8 +++-- 14 files changed, 52 insertions(+), 40 deletions(-) diff --git a/src/core/qsmenu.hpp b/src/core/qsmenu.hpp index f0e81edd..b5a2cded 100644 --- a/src/core/qsmenu.hpp +++ b/src/core/qsmenu.hpp @@ -30,7 +30,7 @@ public: }; Q_ENUM(Enum); - Q_INVOKABLE static QString toString(QsMenuButtonType::Enum value); + Q_INVOKABLE static QString toString(qs::menu::QsMenuButtonType::Enum value); }; class QsMenuEntry; @@ -79,7 +79,7 @@ class QsMenuEntry: public QsMenuHandle { /// ``` Q_PROPERTY(QString icon READ icon NOTIFY iconChanged); /// If this menu item has an associated checkbox or radiobutton. - Q_PROPERTY(QsMenuButtonType::Enum buttonType READ buttonType NOTIFY buttonTypeChanged); + Q_PROPERTY(qs::menu::QsMenuButtonType::Enum buttonType READ buttonType NOTIFY buttonTypeChanged); /// The check state of the checkbox or radiobutton if applicable, as a /// [Qt.CheckState](https://doc.qt.io/qt-6/qt.html#CheckState-enum). Q_PROPERTY(Qt::CheckState checkState READ checkState NOTIFY checkStateChanged); @@ -138,7 +138,7 @@ private: class QsMenuOpener: public QObject { Q_OBJECT; /// The menu to retrieve children from. - Q_PROPERTY(QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged); + Q_PROPERTY(qs::menu::QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged); /// The children of the given menu. Q_PROPERTY(QQmlListProperty children READ children NOTIFY childrenChanged); QML_ELEMENT; diff --git a/src/core/qsmenuanchor.hpp b/src/core/qsmenuanchor.hpp index 683895ab..14e06c63 100644 --- a/src/core/qsmenuanchor.hpp +++ b/src/core/qsmenuanchor.hpp @@ -36,7 +36,7 @@ class QsMenuAnchor: public QObject { /// The menu that should be displayed on this anchor. /// /// See also: @@Quickshell.Services.SystemTray.SystemTrayItem.menu. - Q_PROPERTY(QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged); + Q_PROPERTY(qs::menu::QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged); /// If the menu is currently open and visible. /// /// See also: @@open(), @@close(). diff --git a/src/dbus/dbusmenu/dbusmenu.hpp b/src/dbus/dbusmenu/dbusmenu.hpp index 0687761f..c01edf95 100644 --- a/src/dbus/dbusmenu/dbusmenu.hpp +++ b/src/dbus/dbusmenu/dbusmenu.hpp @@ -36,7 +36,7 @@ class DBusMenuPngImage; class DBusMenuItem: public QsMenuEntry { Q_OBJECT; /// Handle to the root of this menu. - Q_PROPERTY(DBusMenu* menuHandle READ menuHandle CONSTANT); + Q_PROPERTY(qs::dbus::dbusmenu::DBusMenu* menuHandle READ menuHandle CONSTANT); QML_ELEMENT; QML_UNCREATABLE("DBusMenus can only be acquired from a DBusMenuHandle"); @@ -108,7 +108,7 @@ QDebug operator<<(QDebug debug, DBusMenuItem* item); /// Handle to a menu tree provided by a remote process. class DBusMenu: public QObject { Q_OBJECT; - Q_PROPERTY(DBusMenuItem* menu READ menu CONSTANT); + Q_PROPERTY(qs::dbus::dbusmenu::DBusMenuItem* menu READ menu CONSTANT); QML_NAMED_ELEMENT(DBusMenuHandle); QML_UNCREATABLE("Menu handles cannot be directly created"); diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 04e78208..3498a746 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -150,11 +150,11 @@ class MprisPlayer: public QObject { /// - If @@canPause is false, you cannot assign the `Paused` state. /// - If @@canControl is false, you cannot assign the `Stopped` state. /// (or any of the others, though their repsective properties will also be false) - Q_PROPERTY(MprisPlaybackState::Enum playbackState READ playbackState WRITE setPlaybackState NOTIFY playbackStateChanged); + Q_PROPERTY(qs::service::mpris::MprisPlaybackState::Enum playbackState READ playbackState WRITE setPlaybackState NOTIFY playbackStateChanged); /// The loop state of the media player, or `None` if @@loopSupported is false. /// /// May only be written to if @@canControl and @@loopSupported are true. - Q_PROPERTY(MprisLoopState::Enum loopState READ loopState WRITE setLoopState NOTIFY loopStateChanged); + Q_PROPERTY(qs::service::mpris::MprisLoopState::Enum loopState READ loopState WRITE setLoopState NOTIFY loopStateChanged); Q_PROPERTY(bool loopSupported READ loopSupported NOTIFY loopSupportedChanged); /// The speed the song is playing at, as a multiplier. /// diff --git a/src/services/mpris/watcher.hpp b/src/services/mpris/watcher.hpp index d60471cc..f3a5b9b7 100644 --- a/src/services/mpris/watcher.hpp +++ b/src/services/mpris/watcher.hpp @@ -46,7 +46,7 @@ class MprisQml: public QObject { QML_NAMED_ELEMENT(Mpris); QML_SINGLETON; /// All connected MPRIS players. - Q_PROPERTY(ObjectModel* players READ players CONSTANT); + Q_PROPERTY(ObjectModel* players READ players CONSTANT); public: explicit MprisQml(QObject* parent = nullptr): QObject(parent) {}; diff --git a/src/services/notifications/notification.hpp b/src/services/notifications/notification.hpp index d5280bb7..e647b3dd 100644 --- a/src/services/notifications/notification.hpp +++ b/src/services/notifications/notification.hpp @@ -65,6 +65,7 @@ class Notification : public QObject , public Retainable { Q_OBJECT; + // clang-format off /// Id of the notification as given to the client. Q_PROPERTY(quint32 id READ id CONSTANT); /// If the notification is tracked by the notification server. @@ -87,9 +88,9 @@ class Notification /// The image associated with this notification, or "" if none. Q_PROPERTY(QString summary READ summary NOTIFY summaryChanged); Q_PROPERTY(QString body READ body NOTIFY bodyChanged); - Q_PROPERTY(NotificationUrgency::Enum urgency READ urgency NOTIFY urgencyChanged); + Q_PROPERTY(qs::service::notifications::NotificationUrgency::Enum urgency READ urgency NOTIFY urgencyChanged); /// Actions that can be taken for this notification. - Q_PROPERTY(QVector actions READ actions NOTIFY actionsChanged); + Q_PROPERTY(QVector actions READ actions NOTIFY actionsChanged); /// If actions associated with this notification have icons available. /// /// See @@NotificationAction.identifier for details. @@ -107,6 +108,7 @@ class Notification /// All hints sent by the client application as a javascript object. /// Many common hints are exposed via other properties. Q_PROPERTY(QVariantMap hints READ hints NOTIFY hintsChanged); + // clang-format on QML_ELEMENT; QML_UNCREATABLE("Notifications must be acquired from a NotificationServer"); diff --git a/src/services/notifications/qml.hpp b/src/services/notifications/qml.hpp index 86851030..bedbcbef 100644 --- a/src/services/notifications/qml.hpp +++ b/src/services/notifications/qml.hpp @@ -67,7 +67,7 @@ class NotificationServerQml /// If the notification server should advertise that it supports images. Defaults to false. Q_PROPERTY(bool imageSupported READ imageSupported WRITE setImageSupported NOTIFY imageSupportedChanged); /// All notifications currently tracked by the server. - Q_PROPERTY(ObjectModel* trackedNotifications READ trackedNotifications NOTIFY trackedNotificationsChanged); + Q_PROPERTY(ObjectModel* trackedNotifications READ trackedNotifications NOTIFY trackedNotificationsChanged); /// Extra hints to expose to notification clients. Q_PROPERTY(QVector extraHints READ extraHints WRITE setExtraHints NOTIFY extraHintsChanged); // clang-format on diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 2366a373..44b3fbad 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -59,7 +59,7 @@ class Pipewire: public QObject { /// - @@PwNode.isStream - if the node is an application or hardware device. /// - @@PwNode.isSink - if the node is a sink or source. /// - @@PwNode.audio - if non null the node is an audio node. - Q_PROPERTY(ObjectModel* nodes READ nodes CONSTANT); + Q_PROPERTY(ObjectModel* nodes READ nodes CONSTANT); /// All links present in pipewire. /// /// Links connect pipewire nodes to each other, and can be used to determine @@ -70,14 +70,14 @@ class Pipewire: public QObject { /// /// > [!INFO] Multiple links may exist between the same nodes. See @@linkGroups /// > for a deduplicated list containing only one entry per link between nodes. - Q_PROPERTY(ObjectModel* links READ links CONSTANT); + Q_PROPERTY(ObjectModel* links READ links CONSTANT); /// All link groups present in pipewire. /// /// The same as @@links but deduplicated. /// /// If you already have a node you want to check for connections to, /// use @@PwNodeLinkTracker instead of filtering this list. - Q_PROPERTY(ObjectModel* linkGroups READ linkGroups CONSTANT); + Q_PROPERTY(ObjectModel* linkGroups READ linkGroups CONSTANT); /// The default audio sink (output) or `null`. /// /// This is the default sink currently in use by pipewire, and the one applications @@ -87,7 +87,7 @@ class Pipewire: public QObject { /// /// > [!INFO] When the default sink changes, this property may breifly become null. /// > This depends on your hardware. - Q_PROPERTY(PwNodeIface* defaultAudioSink READ defaultAudioSink NOTIFY defaultAudioSinkChanged); + Q_PROPERTY(qs::service::pipewire::PwNodeIface* defaultAudioSink READ defaultAudioSink NOTIFY defaultAudioSinkChanged); /// The default audio source (input) or `null`. /// /// This is the default source currently in use by pipewire, and the one applications @@ -97,21 +97,21 @@ class Pipewire: public QObject { /// /// > [!INFO] When the default source changes, this property may breifly become null. /// > This depends on your hardware. - Q_PROPERTY(PwNodeIface* defaultAudioSource READ defaultAudioSource NOTIFY defaultAudioSourceChanged); + Q_PROPERTY(qs::service::pipewire::PwNodeIface* defaultAudioSource READ defaultAudioSource NOTIFY defaultAudioSourceChanged); /// The preferred default audio sink (output) or `null`. /// /// This is a hint to pipewire telling it which sink should be the default when possible. /// @@defaultAudioSink may differ when it is not possible for pipewire to pick this node. /// /// See @@defaultAudioSink for the current default sink, regardless of preference. - Q_PROPERTY(PwNodeIface* preferredDefaultAudioSink READ defaultConfiguredAudioSink WRITE setDefaultConfiguredAudioSink NOTIFY defaultConfiguredAudioSinkChanged); + Q_PROPERTY(qs::service::pipewire::PwNodeIface* preferredDefaultAudioSink READ defaultConfiguredAudioSink WRITE setDefaultConfiguredAudioSink NOTIFY defaultConfiguredAudioSinkChanged); /// The preferred default audio source (input) or `null`. /// /// This is a hint to pipewire telling it which source should be the default when possible. /// @@defaultAudioSource may differ when it is not possible for pipewire to pick this node. /// /// See @@defaultAudioSource for the current default source, regardless of preference. - Q_PROPERTY(PwNodeIface* preferredDefaultAudioSource READ defaultConfiguredAudioSource WRITE setDefaultConfiguredAudioSource NOTIFY defaultConfiguredAudioSourceChanged); + Q_PROPERTY(qs::service::pipewire::PwNodeIface* preferredDefaultAudioSource READ defaultConfiguredAudioSource WRITE setDefaultConfiguredAudioSource NOTIFY defaultConfiguredAudioSourceChanged); // clang-format on QML_ELEMENT; QML_SINGLETON; @@ -158,12 +158,12 @@ class PwNodeLinkTracker: public QObject { Q_OBJECT; // clang-format off /// The node to track connections to. - Q_PROPERTY(PwNodeIface* node READ node WRITE setNode NOTIFY nodeChanged); + Q_PROPERTY(qs::service::pipewire::PwNodeIface* node READ node WRITE setNode NOTIFY nodeChanged); /// Link groups connected to the given node. /// /// 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. - Q_PROPERTY(QQmlListProperty linkGroups READ linkGroups NOTIFY linkGroupsChanged); + Q_PROPERTY(QQmlListProperty linkGroups READ linkGroups NOTIFY linkGroupsChanged); // clang-format on QML_ELEMENT; @@ -201,6 +201,7 @@ private: /// See @@PwNode.audio. class PwNodeAudioIface: public QObject { Q_OBJECT; + // clang-format off /// If the node is currently muted. Setting this property changes the mute state. /// /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). @@ -213,13 +214,14 @@ class PwNodeAudioIface: public QObject { /// The audio channels present on the node. /// /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). - Q_PROPERTY(QVector channels READ channels NOTIFY channelsChanged); + Q_PROPERTY(QVector channels READ channels NOTIFY channelsChanged); /// The volumes of each audio channel individually. Each entry corrosponds to /// the volume of the channel at the same index in @@channels. @@volumes and @@channels /// will always be the same length. /// /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). Q_PROPERTY(QVector volumes READ volumes WRITE setVolumes NOTIFY volumesChanged); + // clang-format on QML_NAMED_ELEMENT(PwNodeAudio); QML_UNCREATABLE("PwNodeAudio cannot be created directly"); @@ -287,7 +289,7 @@ class PwNodeIface: public PwObjectIface { /// /// The presence or absence of this property can be used to determine if a node /// manages audio, regardless of if it is bound. If non null, the node is an audio node. - Q_PROPERTY(PwNodeAudioIface* audio READ audio CONSTANT); + Q_PROPERTY(qs::service::pipewire::PwNodeAudioIface* audio READ audio CONSTANT); QML_NAMED_ELEMENT(PwNode); QML_UNCREATABLE("PwNodes cannot be created directly"); @@ -325,9 +327,9 @@ class PwLinkIface: public PwObjectIface { /// with `pw-cli i `. Q_PROPERTY(quint32 id READ id CONSTANT); /// The node that is *receiving* information. (the sink) - Q_PROPERTY(PwNodeIface* target READ target CONSTANT); + Q_PROPERTY(qs::service::pipewire::PwNodeIface* target READ target CONSTANT); /// The node that is *sending* information. (the source) - Q_PROPERTY(PwNodeIface* source READ source CONSTANT); + Q_PROPERTY(qs::service::pipewire::PwNodeIface* source READ source CONSTANT); /// The current state of the link. /// /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). @@ -360,13 +362,13 @@ class PwLinkGroupIface , public PwObjectRefIface { Q_OBJECT; /// The node that is *receiving* information. (the sink) - Q_PROPERTY(PwNodeIface* target READ target CONSTANT); + Q_PROPERTY(qs::service::pipewire::PwNodeIface* target READ target CONSTANT); /// The node that is *sending* information. (the source) - Q_PROPERTY(PwNodeIface* source READ source CONSTANT); + Q_PROPERTY(qs::service::pipewire::PwNodeIface* source READ source CONSTANT); /// The current state of the link group. /// /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). - Q_PROPERTY(PwLinkState::Enum state READ state NOTIFY stateChanged); + Q_PROPERTY(qs::service::pipewire::PwLinkState::Enum state READ state NOTIFY stateChanged); QML_NAMED_ELEMENT(PwLinkGroup); QML_UNCREATABLE("PwLinkGroups cannot be created directly"); diff --git a/src/services/status_notifier/qml.hpp b/src/services/status_notifier/qml.hpp index 343fa5b6..797f364d 100644 --- a/src/services/status_notifier/qml.hpp +++ b/src/services/status_notifier/qml.hpp @@ -72,7 +72,7 @@ class SystemTrayItem: public QObject { /// A handle to the menu associated with this tray item, if any. /// /// Can be displayed with @@Quickshell.QsMenuAnchor or @@Quickshell.QsMenuOpener. - Q_PROPERTY(QsMenuHandle* menu READ menu NOTIFY hasMenuChanged); + Q_PROPERTY(qs::menu::QsMenuHandle* menu READ menu NOTIFY hasMenuChanged); /// If this tray item only offers a menu and activation will do nothing. Q_PROPERTY(bool onlyMenu READ onlyMenu NOTIFY onlyMenuChanged); QML_ELEMENT; diff --git a/src/services/upower/core.hpp b/src/services/upower/core.hpp index 0a2367c0..3446d66c 100644 --- a/src/services/upower/core.hpp +++ b/src/services/upower/core.hpp @@ -53,15 +53,17 @@ class UPowerQml: public QObject { Q_OBJECT; QML_NAMED_ELEMENT(UPower); QML_SINGLETON; + // clang-format off /// UPower's DisplayDevice for your system. Can be `null`. /// /// This is an aggregate device and not a physical one, meaning you will not find it in @@devices. /// It is typically the device that is used for displaying information in desktop environments. - Q_PROPERTY(UPowerDevice* displayDevice READ displayDevice NOTIFY displayDeviceChanged); + Q_PROPERTY(qs::service::upower::UPowerDevice* displayDevice READ displayDevice NOTIFY displayDeviceChanged); /// All connected UPower devices. - Q_PROPERTY(ObjectModel* devices READ devices CONSTANT); + Q_PROPERTY(ObjectModel* devices READ devices CONSTANT); /// If the system is currently running on battery power, or discharging. Q_PROPERTY(bool onBattery READ onBattery NOTIFY onBatteryChanged); + // clang-format on public: explicit UPowerQml(QObject* parent = nullptr); diff --git a/src/services/upower/device.hpp b/src/services/upower/device.hpp index aef9efd6..af0f0c37 100644 --- a/src/services/upower/device.hpp +++ b/src/services/upower/device.hpp @@ -85,7 +85,7 @@ class UPowerDevice: public QObject { Q_OBJECT; // clang-format off /// The type of device. - Q_PROPERTY(UPowerDeviceType::Enum type READ type NOTIFY typeChanged); + Q_PROPERTY(qs::service::upower::UPowerDeviceType::Enum type READ type NOTIFY typeChanged); /// If the device is a power supply for your computer and can provide charge. Q_PROPERTY(bool powerSupply READ powerSupply NOTIFY powerSupplyChanged); /// Current energy level of the device in watt-hours. @@ -111,7 +111,7 @@ class UPowerDevice: public QObject { /// If the device `type` is not `Battery`, then the property will be invalid. Q_PROPERTY(bool isPresent READ isPresent NOTIFY isPresentChanged); /// Current state of the device. - Q_PROPERTY(UPowerDeviceState::Enum state READ state NOTIFY stateChanged); + Q_PROPERTY(qs::service::upower::UPowerDeviceState::Enum state READ state NOTIFY stateChanged); /// Health of the device as a percentage of its original health. Q_PROPERTY(qreal healthPercentage READ healthPercentage NOTIFY healthPercentageChanged); Q_PROPERTY(bool healthSupported READ healthSupported NOTIFY healthSupportedChanged); diff --git a/src/wayland/hyprland/ipc/monitor.hpp b/src/wayland/hyprland/ipc/monitor.hpp index e5a5eddf..c16777c7 100644 --- a/src/wayland/hyprland/ipc/monitor.hpp +++ b/src/wayland/hyprland/ipc/monitor.hpp @@ -13,6 +13,7 @@ namespace qs::hyprland::ipc { class HyprlandMonitor: public QObject { Q_OBJECT; + // clang-format off Q_PROPERTY(qint32 id READ id NOTIFY idChanged); Q_PROPERTY(QString name READ name NOTIFY nameChanged); Q_PROPERTY(QString description READ description NOTIFY descriptionChanged); @@ -28,7 +29,8 @@ class HyprlandMonitor: public QObject { /// > property, run @@Hyprland.refreshMonitors() and wait for this property to update. Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); /// The currently active workspace on this monitor. May be null. - Q_PROPERTY(HyprlandWorkspace* activeWorkspace READ activeWorkspace NOTIFY activeWorkspaceChanged); + Q_PROPERTY(qs::hyprland::ipc::HyprlandWorkspace* activeWorkspace READ activeWorkspace NOTIFY activeWorkspaceChanged); + // clang-format on QML_ELEMENT; QML_UNCREATABLE("HyprlandMonitors must be retrieved from the HyprlandIpc object."); diff --git a/src/wayland/hyprland/ipc/qml.hpp b/src/wayland/hyprland/ipc/qml.hpp index 2d39623f..e39855a2 100644 --- a/src/wayland/hyprland/ipc/qml.hpp +++ b/src/wayland/hyprland/ipc/qml.hpp @@ -14,16 +14,18 @@ namespace qs::hyprland::ipc { class HyprlandIpcQml: public QObject { Q_OBJECT; + // clang-format off /// Path to the request socket (.socket.sock) Q_PROPERTY(QString requestSocketPath READ requestSocketPath CONSTANT); /// Path to the event socket (.socket2.sock) Q_PROPERTY(QString eventSocketPath READ eventSocketPath CONSTANT); /// The currently focused hyprland monitor. May be null. - Q_PROPERTY(HyprlandMonitor* focusedMonitor READ focusedMonitor NOTIFY focusedMonitorChanged); + Q_PROPERTY(qs::hyprland::ipc::HyprlandMonitor* focusedMonitor READ focusedMonitor NOTIFY focusedMonitorChanged); /// All hyprland monitors. - Q_PROPERTY(ObjectModel* monitors READ monitors CONSTANT); + Q_PROPERTY(ObjectModel* monitors READ monitors CONSTANT); /// All hyprland workspaces. - Q_PROPERTY(ObjectModel* workspaces READ workspaces CONSTANT); + Q_PROPERTY(ObjectModel* workspaces READ workspaces CONSTANT); + // clang-format on QML_NAMED_ELEMENT(Hyprland); QML_SINGLETON; diff --git a/src/wayland/toplevel_management/qml.hpp b/src/wayland/toplevel_management/qml.hpp index 7fa7b65a..e08fcfa6 100644 --- a/src/wayland/toplevel_management/qml.hpp +++ b/src/wayland/toplevel_management/qml.hpp @@ -24,7 +24,7 @@ class Toplevel: public QObject { Q_PROPERTY(QString appId READ appId NOTIFY appIdChanged); Q_PROPERTY(QString title READ title NOTIFY titleChanged); /// Parent toplevel if this toplevel is a modal/dialog, otherwise null. - Q_PROPERTY(Toplevel* parent READ parent NOTIFY parentChanged); + Q_PROPERTY(qs::wayland::toplevel_management::Toplevel* parent READ parent NOTIFY parentChanged); /// If the window is currently activated or focused. /// /// Activation can be requested with the @@activate() function. @@ -141,13 +141,15 @@ private: /// wayland protocol. class ToplevelManagerQml: public QObject { Q_OBJECT; + // clang-format off /// All toplevel windows exposed by the compositor. - Q_PROPERTY(ObjectModel* toplevels READ toplevels CONSTANT); + Q_PROPERTY(ObjectModel* toplevels READ toplevels CONSTANT); /// Active toplevel or null. /// /// > [!INFO] If multiple are active, this will be the most recently activated one. /// > Usually compositors will not report more than one toplevel as active at a time. - Q_PROPERTY(Toplevel* activeToplevel READ activeToplevel NOTIFY activeToplevelChanged); + Q_PROPERTY(qs::wayland::toplevel_management::Toplevel* activeToplevel READ activeToplevel NOTIFY activeToplevelChanged); + // clang-format on QML_NAMED_ELEMENT(ToplevelManager); QML_SINGLETON; From 98cdb8718101577068dedc95459df4c2adce57d3 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 1 Nov 2024 03:12:07 -0700 Subject: [PATCH 191/305] all: use UntypedObjectModel instead of ObjectModel in Q_PROPERTY Fixes qmllint/qmlls type deduction for ObjectModels --- src/core/desktopentry.hpp | 4 +++- src/core/doc.hpp | 3 +++ src/services/mpris/watcher.hpp | 4 +++- src/services/notifications/qml.hpp | 4 +++- src/services/pipewire/qml.hpp | 10 +++++++--- src/services/status_notifier/qml.hpp | 4 +++- src/services/upower/core.hpp | 4 +++- src/wayland/hyprland/ipc/qml.hpp | 7 +++++-- src/wayland/toplevel_management/qml.hpp | 4 +++- 9 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index 57bc3bc7..3871181b 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -9,6 +9,7 @@ #include #include +#include "doc.hpp" #include "model.hpp" class DesktopAction; @@ -139,7 +140,8 @@ private: class DesktopEntries: public QObject { Q_OBJECT; /// All desktop entries of type Application that are not Hidden or NoDisplay. - Q_PROPERTY(ObjectModel* applications READ applications CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* applications READ applications CONSTANT); QML_ELEMENT; QML_SINGLETON; diff --git a/src/core/doc.hpp b/src/core/doc.hpp index f7db7ac4..fbb21400 100644 --- a/src/core/doc.hpp +++ b/src/core/doc.hpp @@ -18,3 +18,6 @@ // overridden properties #define QSDOC_PROPERTY_OVERRIDE(...) + +// override types of properties for docs +#define QSDOC_TYPE_OVERRIDE(type) diff --git a/src/services/mpris/watcher.hpp b/src/services/mpris/watcher.hpp index f3a5b9b7..bd922cd3 100644 --- a/src/services/mpris/watcher.hpp +++ b/src/services/mpris/watcher.hpp @@ -10,6 +10,7 @@ #include #include +#include "../../core/doc.hpp" #include "../../core/model.hpp" #include "player.hpp" @@ -46,7 +47,8 @@ class MprisQml: public QObject { QML_NAMED_ELEMENT(Mpris); QML_SINGLETON; /// All connected MPRIS players. - Q_PROPERTY(ObjectModel* players READ players CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* players READ players CONSTANT); public: explicit MprisQml(QObject* parent = nullptr): QObject(parent) {}; diff --git a/src/services/notifications/qml.hpp b/src/services/notifications/qml.hpp index bedbcbef..3f5f34c0 100644 --- a/src/services/notifications/qml.hpp +++ b/src/services/notifications/qml.hpp @@ -6,6 +6,7 @@ #include #include +#include "../../core/doc.hpp" #include "../../core/model.hpp" #include "../../core/reload.hpp" #include "notification.hpp" @@ -67,7 +68,8 @@ class NotificationServerQml /// If the notification server should advertise that it supports images. Defaults to false. Q_PROPERTY(bool imageSupported READ imageSupported WRITE setImageSupported NOTIFY imageSupportedChanged); /// All notifications currently tracked by the server. - Q_PROPERTY(ObjectModel* trackedNotifications READ trackedNotifications NOTIFY trackedNotificationsChanged); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* trackedNotifications READ trackedNotifications NOTIFY trackedNotificationsChanged); /// Extra hints to expose to notification clients. Q_PROPERTY(QVector extraHints READ extraHints WRITE setExtraHints NOTIFY extraHintsChanged); // clang-format on diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 44b3fbad..675b923b 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -8,6 +8,7 @@ #include #include +#include "../../core/doc.hpp" #include "../../core/model.hpp" #include "link.hpp" #include "node.hpp" @@ -59,7 +60,8 @@ class Pipewire: public QObject { /// - @@PwNode.isStream - if the node is an application or hardware device. /// - @@PwNode.isSink - if the node is a sink or source. /// - @@PwNode.audio - if non null the node is an audio node. - Q_PROPERTY(ObjectModel* nodes READ nodes CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* nodes READ nodes CONSTANT); /// All links present in pipewire. /// /// Links connect pipewire nodes to each other, and can be used to determine @@ -70,14 +72,16 @@ class Pipewire: public QObject { /// /// > [!INFO] Multiple links may exist between the same nodes. See @@linkGroups /// > for a deduplicated list containing only one entry per link between nodes. - Q_PROPERTY(ObjectModel* links READ links CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* links READ links CONSTANT); /// All link groups present in pipewire. /// /// The same as @@links but deduplicated. /// /// If you already have a node you want to check for connections to, /// use @@PwNodeLinkTracker instead of filtering this list. - Q_PROPERTY(ObjectModel* linkGroups READ linkGroups CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* linkGroups READ linkGroups CONSTANT); /// The default audio sink (output) or `null`. /// /// This is the default sink currently in use by pipewire, and the one applications diff --git a/src/services/status_notifier/qml.hpp b/src/services/status_notifier/qml.hpp index 797f364d..98a647fa 100644 --- a/src/services/status_notifier/qml.hpp +++ b/src/services/status_notifier/qml.hpp @@ -5,6 +5,7 @@ #include #include +#include "../../core/doc.hpp" #include "../../core/model.hpp" #include "item.hpp" @@ -125,7 +126,8 @@ signals: class SystemTray: public QObject { Q_OBJECT; /// List of all system tray icons. - Q_PROPERTY(ObjectModel* items READ items CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* items READ items CONSTANT); QML_ELEMENT; QML_SINGLETON; diff --git a/src/services/upower/core.hpp b/src/services/upower/core.hpp index 3446d66c..302311ea 100644 --- a/src/services/upower/core.hpp +++ b/src/services/upower/core.hpp @@ -7,6 +7,7 @@ #include #include +#include "../../core/doc.hpp" #include "../../core/model.hpp" #include "../../dbus/properties.hpp" #include "dbus_service.h" @@ -60,7 +61,8 @@ class UPowerQml: public QObject { /// It is typically the device that is used for displaying information in desktop environments. Q_PROPERTY(qs::service::upower::UPowerDevice* displayDevice READ displayDevice NOTIFY displayDeviceChanged); /// All connected UPower devices. - Q_PROPERTY(ObjectModel* devices READ devices CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT); /// If the system is currently running on battery power, or discharging. Q_PROPERTY(bool onBattery READ onBattery NOTIFY onBatteryChanged); // clang-format on diff --git a/src/wayland/hyprland/ipc/qml.hpp b/src/wayland/hyprland/ipc/qml.hpp index e39855a2..72bb4e14 100644 --- a/src/wayland/hyprland/ipc/qml.hpp +++ b/src/wayland/hyprland/ipc/qml.hpp @@ -5,6 +5,7 @@ #include #include +#include "../../../core/doc.hpp" #include "../../../core/model.hpp" #include "../../../core/qmlscreen.hpp" #include "connection.hpp" @@ -22,9 +23,11 @@ class HyprlandIpcQml: public QObject { /// The currently focused hyprland monitor. May be null. Q_PROPERTY(qs::hyprland::ipc::HyprlandMonitor* focusedMonitor READ focusedMonitor NOTIFY focusedMonitorChanged); /// All hyprland monitors. - Q_PROPERTY(ObjectModel* monitors READ monitors CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* monitors READ monitors CONSTANT); /// All hyprland workspaces. - Q_PROPERTY(ObjectModel* workspaces READ workspaces CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* workspaces READ workspaces CONSTANT); // clang-format on QML_NAMED_ELEMENT(Hyprland); QML_SINGLETON; diff --git a/src/wayland/toplevel_management/qml.hpp b/src/wayland/toplevel_management/qml.hpp index e08fcfa6..127b4c8f 100644 --- a/src/wayland/toplevel_management/qml.hpp +++ b/src/wayland/toplevel_management/qml.hpp @@ -4,6 +4,7 @@ #include #include +#include "../../core/doc.hpp" #include "../../core/model.hpp" #include "../../core/qmlscreen.hpp" #include "../../core/util.hpp" @@ -143,7 +144,8 @@ class ToplevelManagerQml: public QObject { Q_OBJECT; // clang-format off /// All toplevel windows exposed by the compositor. - Q_PROPERTY(ObjectModel* toplevels READ toplevels CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* toplevels READ toplevels CONSTANT); /// Active toplevel or null. /// /// > [!INFO] If multiple are active, this will be the most recently activated one. From cdeec6ee83f6512b2736b4e87695f708f291f843 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 1 Nov 2024 21:10:21 -0700 Subject: [PATCH 192/305] all: use fully qualified type names in signals and invokables Further fixes qmllint/qmlls --- src/core/qsmenu.hpp | 2 +- src/services/mpris/player.hpp | 4 ++-- src/services/notifications/notification.hpp | 7 ++++--- src/services/notifications/qml.hpp | 2 +- src/services/pipewire/link.hpp | 2 +- src/services/pipewire/node.hpp | 2 +- src/services/upower/device.hpp | 4 ++-- src/wayland/hyprland/ipc/qml.hpp | 2 +- 8 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/core/qsmenu.hpp b/src/core/qsmenu.hpp index b5a2cded..a088eacd 100644 --- a/src/core/qsmenu.hpp +++ b/src/core/qsmenu.hpp @@ -140,7 +140,7 @@ class QsMenuOpener: public QObject { /// The menu to retrieve children from. Q_PROPERTY(qs::menu::QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged); /// The children of the given menu. - Q_PROPERTY(QQmlListProperty children READ children NOTIFY childrenChanged); + Q_PROPERTY(QQmlListProperty children READ children NOTIFY childrenChanged); QML_ELEMENT; public: diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 3498a746..47ea7922 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -29,7 +29,7 @@ public: }; Q_ENUM(Enum); - Q_INVOKABLE static QString toString(MprisPlaybackState::Enum status); + Q_INVOKABLE static QString toString(qs::service::mpris::MprisPlaybackState::Enum status); }; ///! Loop state of an MprisPlayer @@ -47,7 +47,7 @@ public: }; Q_ENUM(Enum); - Q_INVOKABLE static QString toString(MprisLoopState::Enum status); + Q_INVOKABLE static QString toString(qs::service::mpris::MprisLoopState::Enum status); }; ///! A media player exposed over MPRIS. diff --git a/src/services/notifications/notification.hpp b/src/services/notifications/notification.hpp index e647b3dd..21b5b95a 100644 --- a/src/services/notifications/notification.hpp +++ b/src/services/notifications/notification.hpp @@ -30,7 +30,7 @@ public: }; Q_ENUM(Enum); - Q_INVOKABLE static QString toString(NotificationUrgency::Enum value); + Q_INVOKABLE static QString toString(qs::service::notifications::NotificationUrgency::Enum value); }; ///! The reason a Notification was closed. @@ -51,7 +51,8 @@ public: }; Q_ENUM(Enum); - Q_INVOKABLE static QString toString(NotificationCloseReason::Enum value); + Q_INVOKABLE static QString + toString(qs::service::notifications::NotificationCloseReason::Enum value); }; class NotificationAction; @@ -149,7 +150,7 @@ signals: /// Sent when a notification has been closed. /// /// The notification object will be destroyed as soon as all signal handlers exit. - void closed(NotificationCloseReason::Enum reason); + void closed(qs::service::notifications::NotificationCloseReason::Enum reason); void trackedChanged(); void expireTimeoutChanged(); diff --git a/src/services/notifications/qml.hpp b/src/services/notifications/qml.hpp index 3f5f34c0..d7502101 100644 --- a/src/services/notifications/qml.hpp +++ b/src/services/notifications/qml.hpp @@ -114,7 +114,7 @@ signals: /// Sent when a notification is received by the server. /// /// If this notification should not be discarded, set its `tracked` property to true. - void notification(Notification* notification); + void notification(qs::service::notifications::Notification* notification); void keepOnReloadChanged(); void persistenceSupportedChanged(); diff --git a/src/services/pipewire/link.hpp b/src/services/pipewire/link.hpp index 01c9e60f..0c7bde29 100644 --- a/src/services/pipewire/link.hpp +++ b/src/services/pipewire/link.hpp @@ -32,7 +32,7 @@ public: }; Q_ENUM(Enum); - Q_INVOKABLE static QString toString(PwLinkState::Enum value); + Q_INVOKABLE static QString toString(qs::service::pipewire::PwLinkState::Enum value); }; constexpr const char TYPE_INTERFACE_Link[] = PW_TYPE_INTERFACE_Link; // NOLINT diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index 29c02033..783614ac 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -84,7 +84,7 @@ public: /// Print a human readable representation of the given channel, /// including aux and custom channel ranges. - Q_INVOKABLE static QString toString(PwAudioChannel::Enum value); + Q_INVOKABLE static QString toString(qs::service::pipewire::PwAudioChannel::Enum value); }; enum class PwNodeType { diff --git a/src/services/upower/device.hpp b/src/services/upower/device.hpp index af0f0c37..c971dd0e 100644 --- a/src/services/upower/device.hpp +++ b/src/services/upower/device.hpp @@ -33,7 +33,7 @@ public: }; Q_ENUM(Enum); - Q_INVOKABLE static QString toString(UPowerDeviceState::Enum status); + Q_INVOKABLE static QString toString(qs::service::upower::UPowerDeviceState::Enum status); }; ///! Type of a UPower device. @@ -77,7 +77,7 @@ public: }; Q_ENUM(Enum); - Q_INVOKABLE static QString toString(UPowerDeviceType::Enum type); + Q_INVOKABLE static QString toString(qs::service::upower::UPowerDeviceType::Enum type); }; ///! A device exposed through the UPower system service. diff --git a/src/wayland/hyprland/ipc/qml.hpp b/src/wayland/hyprland/ipc/qml.hpp index 72bb4e14..1fc93c38 100644 --- a/src/wayland/hyprland/ipc/qml.hpp +++ b/src/wayland/hyprland/ipc/qml.hpp @@ -63,7 +63,7 @@ signals: /// Emitted for every event that comes in through the hyprland event socket (socket2). /// /// See [Hyprland Wiki: IPC](https://wiki.hyprland.org/IPC/) for a list of events. - void rawEvent(HyprlandIpcEvent* event); + void rawEvent(qs::hyprland::ipc::HyprlandIpcEvent* event); void focusedMonitorChanged(); }; From 2e183409958159cbdc2769dbdfc02d888f18b7ab Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 4 Nov 2024 13:42:21 -0800 Subject: [PATCH 193/305] build: allow specifying QML install dir --- BUILD.md | 10 ++++++++++ CMakeLists.txt | 24 +++++++++++++++++++----- default.nix | 1 + 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/BUILD.md b/BUILD.md index 4650d1ff..5a0652fe 100644 --- a/BUILD.md +++ b/BUILD.md @@ -25,6 +25,16 @@ If we cannot retrieve debug information, please set this to `NO` and In both cases you should build with `-DCMAKE_BUILD_TYPE=RelWithDebInfo` (then split or keep the debuginfo). +### QML Module dir +Currently all QML modules are statically linked to quickshell, but this is where +tooling information will go. + +`-DINSTALL_QML_PREFIX="path/to/qml"` + +`-DINSTALL_QMLDIR="/full/path/to/qml"` + +`INSTALL_QML_PREFIX` works the same as `INSTALL_QMLDIR`, except it prepends `CMAKE_INSTALL_PREFIX`. You usually want this. + ## Dependencies Quickshell has a set of base dependencies you will always need, names vary by distro: diff --git a/CMakeLists.txt b/CMakeLists.txt index 367b39e0..e142ae57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,6 +64,9 @@ boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON) +set(INSTALL_QMLDIR "" CACHE STRING "QML install dir") +set(INSTALL_QML_PREFIX "" CACHE STRING "QML install prefix") + add_compile_options(-Wall -Wextra) if (FRAME_POINTERS) @@ -154,11 +157,22 @@ if (USE_JEMALLOC) target_link_libraries(quickshell PRIVATE ${JEMALLOC_LIBRARIES}) endif() -install( - DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules/ - DESTINATION ${CMAKE_INSTALL_LIBDIR}/qt-6/qml - FILES_MATCHING PATTERN "*" -) +if ("${INSTALL_QMLDIR}" STREQUAL "" AND "${INSTALL_QML_PREFIX}" STREQUAL "") + message(WARNING "Neither INSTALL_QMLDIR nor INSTALL_QML_PREFIX is set. QML modules will not be installed.") +else() + if ("${INSTALL_QMLDIR}" STREQUAL "") + set(INSTALLDIR "${CMAKE_INSTALL_PREFIX}/${INSTALL_QML_PREFIX}") + else() + set(INSTALLDIR "${INSTALL_QMLDIR}") + endif() + + message(STATUS "QML install dir: ${INSTALLDIR}") + install( + DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules/ + DESTINATION ${INSTALLDIR} + FILES_MATCHING PATTERN "*" + ) +endif() install(CODE " execute_process( diff --git a/default.nix b/default.nix index f7352267..298e561e 100644 --- a/default.nix +++ b/default.nix @@ -69,6 +69,7 @@ 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) From 1168879d6d5b79b15e18dce03a37912c00abf62d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 4 Nov 2024 14:13:37 -0800 Subject: [PATCH 194/305] build: only install necessary qml module files --- CMakeLists.txt | 20 +---- cmake/install-qml-module.cmake | 89 +++++++++++++++++++ src/core/CMakeLists.txt | 2 + src/dbus/dbusmenu/CMakeLists.txt | 2 + src/io/CMakeLists.txt | 2 + src/services/greetd/CMakeLists.txt | 2 + src/services/mpris/CMakeLists.txt | 2 + src/services/notifications/CMakeLists.txt | 2 + src/services/pam/CMakeLists.txt | 2 + src/services/pipewire/CMakeLists.txt | 2 + src/services/status_notifier/CMakeLists.txt | 2 + src/services/upower/CMakeLists.txt | 2 + src/wayland/CMakeLists.txt | 2 + src/wayland/hyprland/CMakeLists.txt | 2 + .../hyprland/focus_grab/CMakeLists.txt | 2 + .../hyprland/global_shortcuts/CMakeLists.txt | 2 + src/wayland/hyprland/ipc/CMakeLists.txt | 2 + .../toplevel_management/CMakeLists.txt | 2 + src/wayland/wlr_layershell/CMakeLists.txt | 2 + src/widgets/CMakeLists.txt | 2 + src/window/CMakeLists.txt | 2 + src/x11/CMakeLists.txt | 2 + 22 files changed, 130 insertions(+), 19 deletions(-) create mode 100644 cmake/install-qml-module.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index e142ae57..4014cce2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,8 +64,7 @@ boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON) -set(INSTALL_QMLDIR "" CACHE STRING "QML install dir") -set(INSTALL_QML_PREFIX "" CACHE STRING "QML install prefix") +include(cmake/install-qml-module.cmake) add_compile_options(-Wall -Wextra) @@ -157,23 +156,6 @@ if (USE_JEMALLOC) target_link_libraries(quickshell PRIVATE ${JEMALLOC_LIBRARIES}) endif() -if ("${INSTALL_QMLDIR}" STREQUAL "" AND "${INSTALL_QML_PREFIX}" STREQUAL "") - message(WARNING "Neither INSTALL_QMLDIR nor INSTALL_QML_PREFIX is set. QML modules will not be installed.") -else() - if ("${INSTALL_QMLDIR}" STREQUAL "") - set(INSTALLDIR "${CMAKE_INSTALL_PREFIX}/${INSTALL_QML_PREFIX}") - else() - set(INSTALLDIR "${INSTALL_QMLDIR}") - endif() - - message(STATUS "QML install dir: ${INSTALLDIR}") - install( - DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules/ - DESTINATION ${INSTALLDIR} - FILES_MATCHING PATTERN "*" - ) -endif() - install(CODE " execute_process( COMMAND ${CMAKE_COMMAND} -E create_symlink \ diff --git a/cmake/install-qml-module.cmake b/cmake/install-qml-module.cmake new file mode 100644 index 00000000..5c95531c --- /dev/null +++ b/cmake/install-qml-module.cmake @@ -0,0 +1,89 @@ +set(INSTALL_QMLDIR "" CACHE STRING "QML install dir") +set(INSTALL_QML_PREFIX "" CACHE STRING "QML install prefix") + +# There doesn't seem to be a standard cross-distro qml install path. +if ("${INSTALL_QMLDIR}" STREQUAL "" AND "${INSTALL_QML_PREFIX}" STREQUAL "") + message(WARNING "Neither INSTALL_QMLDIR nor INSTALL_QML_PREFIX is set. QML modules will not be installed.") +else() + if ("${INSTALL_QMLDIR}" STREQUAL "") + set(QML_FULL_INSTALLDIR "${CMAKE_INSTALL_PREFIX}/${INSTALL_QML_PREFIX}") + else() + set(QML_FULL_INSTALLDIR "${INSTALL_QMLDIR}") + endif() + + message(STATUS "QML install dir: ${QML_FULL_INSTALLDIR}") +endif() + +# Install a given target as a QML module. This is mostly pulled from ECM, as there does not seem +# to be an official way to do it. +# see https://github.com/KDE/extra-cmake-modules/blob/fe0f606bf7f222e36f7560fd7a2c33ef993e23bb/modules/ECMQmlModule6.cmake#L160 +function(install_qml_module arg_TARGET) + if (NOT DEFINED QML_FULL_INSTALLDIR) + return() + endif() + + qt_query_qml_module(${arg_TARGET} + URI module_uri + VERSION module_version + PLUGIN_TARGET module_plugin_target + TARGET_PATH module_target_path + QMLDIR module_qmldir + TYPEINFO module_typeinfo + QML_FILES module_qml_files + RESOURCES module_resources + ) + + set(module_dir "${QML_FULL_INSTALLDIR}/${module_target_path}") + + if (NOT TARGET "${module_plugin_target}") + message(FATAL_ERROR "install_qml_modules called for a target without a plugin") + endif() + + get_target_property(target_type "${arg_TARGET}" TYPE) + if (NOT "${target_type}" STREQUAL "STATIC_LIBRARY") + install( + TARGETS "${arg_TARGET}" + LIBRARY DESTINATION "${module_dir}" + RUNTIME DESTINATION "${module_dir}" + ) + + install( + TARGETS "${module_plugin_target}" + LIBRARY DESTINATION "${module_dir}" + RUNTIME DESTINATION "${module_dir}" + ) + endif() + + install(FILES "${module_qmldir}" DESTINATION "${module_dir}") + install(FILES "${module_typeinfo}" DESTINATION "${module_dir}") + + # Install QML files + list(LENGTH module_qml_files num_files) + if (NOT "${module_qml_files}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0) + qt_query_qml_module(${arg_TARGET} QML_FILES_DEPLOY_PATHS qml_files_deploy_paths) + + math(EXPR last_index "${num_files} - 1") + foreach(i RANGE 0 ${last_index}) + list(GET module_qml_files ${i} src_file) + list(GET qml_files_deploy_paths ${i} deploy_path) + get_filename_component(dst_name "${deploy_path}" NAME) + get_filename_component(dest_dir "${deploy_path}" DIRECTORY) + install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}") + endforeach() + endif() + + # Install resources + list(LENGTH module_resources num_files) + if (NOT "${module_resources}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0) + qt_query_qml_module(${arg_TARGET} RESOURCES_DEPLOY_PATHS resources_deploy_paths) + + math(EXPR last_index "${num_files} - 1") + foreach(i RANGE 0 ${last_index}) + list(GET module_resources ${i} src_file) + list(GET resources_deploy_paths ${i} deploy_path) + get_filename_component(dst_name "${deploy_path}" NAME) + get_filename_component(dest_dir "${deploy_path}" DIRECTORY) + install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}") + endforeach() + endif() +endfunction() diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 8d50f1f5..62f29425 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -49,6 +49,8 @@ qt_add_qml_module(quickshell-core DEFAULT_IMPORTS Quickshell._Window ) +install_qml_module(quickshell-core) + target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} CLI11::CLI11) qs_pch(quickshell-core) diff --git a/src/dbus/dbusmenu/CMakeLists.txt b/src/dbus/dbusmenu/CMakeLists.txt index 9e4885a8..f9e4446c 100644 --- a/src/dbus/dbusmenu/CMakeLists.txt +++ b/src/dbus/dbusmenu/CMakeLists.txt @@ -20,6 +20,8 @@ qt_add_qml_module(quickshell-dbusmenu DEPENDENCIES QtQml Quickshell ) +install_qml_module(quickshell-dbusmenu) + # dbus headers target_include_directories(quickshell-dbusmenu PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index 54cfac00..1d936d17 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -21,6 +21,8 @@ qt_add_qml_module(quickshell-io FileView.qml ) +install_qml_module(quickshell-io) + target_link_libraries(quickshell-io PRIVATE ${QT_DEPS}) target_link_libraries(quickshell-io-init PRIVATE ${QT_DEPS}) diff --git a/src/services/greetd/CMakeLists.txt b/src/services/greetd/CMakeLists.txt index 349d28ce..870f8085 100644 --- a/src/services/greetd/CMakeLists.txt +++ b/src/services/greetd/CMakeLists.txt @@ -9,6 +9,8 @@ qt_add_qml_module(quickshell-service-greetd DEPENDENCIES QtQml ) +install_qml_module(quickshell-service-greetd) + target_link_libraries(quickshell-service-greetd PRIVATE ${QT_DEPS}) qs_pch(quickshell-service-greetd) diff --git a/src/services/mpris/CMakeLists.txt b/src/services/mpris/CMakeLists.txt index f87edecc..505df7a6 100644 --- a/src/services/mpris/CMakeLists.txt +++ b/src/services/mpris/CMakeLists.txt @@ -33,6 +33,8 @@ qt_add_qml_module(quickshell-service-mpris DEPENDENCIES QtQml Quickshell ) +install_qml_module(quickshell-service-mpris) + target_link_libraries(quickshell-service-mpris PRIVATE ${QT_DEPS} quickshell-dbus) target_link_libraries(quickshell PRIVATE quickshell-service-mprisplugin) diff --git a/src/services/notifications/CMakeLists.txt b/src/services/notifications/CMakeLists.txt index 23f4d692..4ba8d3cc 100644 --- a/src/services/notifications/CMakeLists.txt +++ b/src/services/notifications/CMakeLists.txt @@ -23,6 +23,8 @@ qt_add_qml_module(quickshell-service-notifications DEPENDENCIES QtQml Quickshell ) +install_qml_module(quickshell-service-notifications) + target_link_libraries(quickshell-service-notifications PRIVATE ${QT_DEPS} quickshell-dbus) target_link_libraries(quickshell PRIVATE quickshell-service-notificationsplugin) diff --git a/src/services/pam/CMakeLists.txt b/src/services/pam/CMakeLists.txt index a55da739..f9d017e7 100644 --- a/src/services/pam/CMakeLists.txt +++ b/src/services/pam/CMakeLists.txt @@ -11,6 +11,8 @@ qt_add_qml_module(quickshell-service-pam DEPENDENCIES QtQml ) +install_qml_module(quickshell-service-pam) + target_link_libraries(quickshell-service-pam PRIVATE ${QT_DEPS} pam ${PAM_LIBRARIES}) qs_pch(quickshell-service-pam) diff --git a/src/services/pipewire/CMakeLists.txt b/src/services/pipewire/CMakeLists.txt index 8c33e644..bb74a078 100644 --- a/src/services/pipewire/CMakeLists.txt +++ b/src/services/pipewire/CMakeLists.txt @@ -19,6 +19,8 @@ qt_add_qml_module(quickshell-service-pipewire DEPENDENCIES QtQml Quickshell ) +install_qml_module(quickshell-service-pipewire) + target_link_libraries(quickshell-service-pipewire PRIVATE ${QT_DEPS} PkgConfig::pipewire) qs_pch(quickshell-service-pipewire) diff --git a/src/services/status_notifier/CMakeLists.txt b/src/services/status_notifier/CMakeLists.txt index bc8918d0..20de11a1 100644 --- a/src/services/status_notifier/CMakeLists.txt +++ b/src/services/status_notifier/CMakeLists.txt @@ -44,6 +44,8 @@ qt_add_qml_module(quickshell-service-statusnotifier DEPENDENCIES QtQml Quickshell Quickshell.DBusMenu ) +install_qml_module(quickshell-service-statusnotifier) + target_link_libraries(quickshell-service-statusnotifier PRIVATE ${QT_DEPS} quickshell-dbus quickshell-dbusmenuplugin) target_link_libraries(quickshell PRIVATE quickshell-service-statusnotifierplugin) diff --git a/src/services/upower/CMakeLists.txt b/src/services/upower/CMakeLists.txt index e600f8c7..e913a550 100644 --- a/src/services/upower/CMakeLists.txt +++ b/src/services/upower/CMakeLists.txt @@ -33,6 +33,8 @@ qt_add_qml_module(quickshell-service-upower DEPENDENCIES QtQml Quickshell ) +install_qml_module(quickshell-service-upower) + target_link_libraries(quickshell-service-upower PRIVATE ${QT_DEPS} quickshell-dbus) target_link_libraries(quickshell PRIVATE quickshell-service-upowerplugin) diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index ccb31b07..19e74b90 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -110,6 +110,8 @@ qt_add_qml_module(quickshell-wayland IMPORTS ${WAYLAND_MODULES} ) +install_qml_module(quickshell-wayland) + qs_pch(quickshell-wayland) qs_pch(quickshell-waylandplugin) qs_pch(quickshell-wayland-init) diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt index be2f0c59..59458fe6 100644 --- a/src/wayland/hyprland/CMakeLists.txt +++ b/src/wayland/hyprland/CMakeLists.txt @@ -25,6 +25,8 @@ qt_add_qml_module(quickshell-hyprland IMPORTS ${HYPRLAND_MODULES} ) +install_qml_module(quickshell-hyprland) + qs_pch(quickshell-hyprland) qs_pch(quickshell-hyprlandplugin) diff --git a/src/wayland/hyprland/focus_grab/CMakeLists.txt b/src/wayland/hyprland/focus_grab/CMakeLists.txt index a17436ef..0fd1f85e 100644 --- a/src/wayland/hyprland/focus_grab/CMakeLists.txt +++ b/src/wayland/hyprland/focus_grab/CMakeLists.txt @@ -10,6 +10,8 @@ qt_add_qml_module(quickshell-hyprland-focus-grab DEPENDENCIES QtQml Quickshell ) +install_qml_module(quickshell-hyprland-focus-grab) + wl_proto(quickshell-hyprland-focus-grab hyprland-focus-grab-v1 "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml" diff --git a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt index cebaa652..d2314177 100644 --- a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt +++ b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt @@ -10,6 +10,8 @@ qt_add_qml_module(quickshell-hyprland-global-shortcuts DEPENDENCIES QtQml ) +install_qml_module(quickshell-hyprland-global-shortcuts) + wl_proto(quickshell-hyprland-global-shortcuts hyprland-global-shortcuts-v1 "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-global-shortcuts-v1.xml" diff --git a/src/wayland/hyprland/ipc/CMakeLists.txt b/src/wayland/hyprland/ipc/CMakeLists.txt index c2e32888..367fa8f4 100644 --- a/src/wayland/hyprland/ipc/CMakeLists.txt +++ b/src/wayland/hyprland/ipc/CMakeLists.txt @@ -11,6 +11,8 @@ qt_add_qml_module(quickshell-hyprland-ipc DEPENDENCIES QtQml Quickshell ) +install_qml_module(quickshell-hyprland-ipc) + target_link_libraries(quickshell-hyprland-ipc PRIVATE ${QT_DEPS}) qs_pch(quickshell-hyprland-ipc) diff --git a/src/wayland/toplevel_management/CMakeLists.txt b/src/wayland/toplevel_management/CMakeLists.txt index 6ea0c254..01c9d756 100644 --- a/src/wayland/toplevel_management/CMakeLists.txt +++ b/src/wayland/toplevel_management/CMakeLists.txt @@ -10,6 +10,8 @@ qt_add_qml_module(quickshell-wayland-toplevel-management DEPENDENCIES QtQml Quickshell Quickshell.Wayland ) +install_qml_module(quickshell-wayland-toplevel-management) + wl_proto(quickshell-wayland-toplevel-management wlr-foreign-toplevel-management-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/wlr-foreign-toplevel-management-unstable-v1.xml" diff --git a/src/wayland/wlr_layershell/CMakeLists.txt b/src/wayland/wlr_layershell/CMakeLists.txt index d5439f60..640b7ec2 100644 --- a/src/wayland/wlr_layershell/CMakeLists.txt +++ b/src/wayland/wlr_layershell/CMakeLists.txt @@ -12,6 +12,8 @@ qt_add_qml_module(quickshell-wayland-layershell DEPENDENCIES QtQuick Quickshell ) +install_qml_module(quickshell-wayland-layershell) + wl_proto(quickshell-wayland-layershell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/wlr-layer-shell-unstable-v1.xml") target_link_libraries(quickshell-wayland-layershell PRIVATE ${QT_DEPS} wayland-client) diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index ac3682fa..06671b13 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -7,6 +7,8 @@ qt_add_qml_module(quickshell-widgets IconImage.qml ) +install_qml_module(quickshell-widgets) + qs_pch(quickshell-widgets) qs_pch(quickshell-widgetsplugin) diff --git a/src/window/CMakeLists.txt b/src/window/CMakeLists.txt index 7b140946..e7dd1977 100644 --- a/src/window/CMakeLists.txt +++ b/src/window/CMakeLists.txt @@ -12,6 +12,8 @@ qt_add_qml_module(quickshell-window DEPENDENCIES QtQuick Quickshell ) +install_qml_module(quickshell-window) + add_library(quickshell-window-init OBJECT init.cpp) target_link_libraries(quickshell-window PRIVATE ${QT_DEPS} Qt6::QuickPrivate) diff --git a/src/x11/CMakeLists.txt b/src/x11/CMakeLists.txt index 2da30238..d1079d29 100644 --- a/src/x11/CMakeLists.txt +++ b/src/x11/CMakeLists.txt @@ -10,6 +10,8 @@ qt_add_qml_module(quickshell-x11 VERSION 0.1 ) +install_qml_module(quickshell-x11) + add_library(quickshell-x11-init OBJECT init.cpp) target_link_libraries(quickshell-x11 PRIVATE ${QT_DEPS} ${XCB_LIBRARIES}) From 7ffce72b31a662127f6a6ae0a1e007b27d661504 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 5 Nov 2024 04:15:17 -0800 Subject: [PATCH 195/305] all: optimize build --- CMakeLists.txt | 37 +- cmake/pch.cmake | 85 ++ cmake/util.cmake | 20 + src/CMakeLists.txt | 3 +- src/core/CMakeLists.txt | 10 +- src/core/clock.cpp | 1 + src/core/generation.cpp | 89 -- src/core/generation.hpp | 4 - src/core/iconprovider.cpp | 105 ++ src/core/iconprovider.hpp | 8 + src/core/platformmenu.cpp | 12 +- src/core/platformmenu.hpp | 14 +- src/core/platformmenu_p.hpp | 19 + src/core/popupanchor.hpp | 1 - src/core/test/CMakeLists.txt | 2 +- src/dbus/CMakeLists.txt | 14 +- src/dbus/dbusmenu/CMakeLists.txt | 11 +- src/dbus/properties.cpp | 1 - src/io/CMakeLists.txt | 8 +- src/io/test/CMakeLists.txt | 2 +- src/ipc/CMakeLists.txt | 4 +- src/launch/CMakeLists.txt | 23 + src/launch/command.cpp | 448 +++++++ src/launch/launch.cpp | 238 ++++ src/launch/launch_p.hpp | 103 ++ src/launch/main.cpp | 116 ++ src/launch/main.hpp | 7 + src/launch/parsecommand.cpp | 196 ++++ src/main.cpp | 1031 +---------------- src/services/greetd/CMakeLists.txt | 6 +- src/services/mpris/CMakeLists.txt | 12 +- src/services/notifications/CMakeLists.txt | 8 +- src/services/pam/CMakeLists.txt | 5 +- src/services/pipewire/CMakeLists.txt | 11 +- src/services/status_notifier/CMakeLists.txt | 9 +- src/services/upower/CMakeLists.txt | 9 +- src/wayland/CMakeLists.txt | 33 +- src/wayland/hyprland/CMakeLists.txt | 3 +- .../hyprland/focus_grab/CMakeLists.txt | 11 +- .../hyprland/global_shortcuts/CMakeLists.txt | 7 +- src/wayland/hyprland/ipc/CMakeLists.txt | 9 +- src/wayland/platformmenu.cpp | 1 + src/wayland/session_lock/CMakeLists.txt | 3 +- .../toplevel_management/CMakeLists.txt | 13 +- src/wayland/wlr_layershell/CMakeLists.txt | 14 +- src/widgets/CMakeLists.txt | 3 +- src/window/CMakeLists.txt | 16 +- src/window/init.cpp | 3 + src/window/test/CMakeLists.txt | 2 +- src/x11/CMakeLists.txt | 9 +- src/x11/init.cpp | 4 + 51 files changed, 1526 insertions(+), 1277 deletions(-) create mode 100644 cmake/pch.cmake create mode 100644 cmake/util.cmake create mode 100644 src/core/iconprovider.cpp create mode 100644 src/core/iconprovider.hpp create mode 100644 src/core/platformmenu_p.hpp create mode 100644 src/launch/CMakeLists.txt create mode 100644 src/launch/command.cpp create mode 100644 src/launch/launch.cpp create mode 100644 src/launch/launch_p.hpp create mode 100644 src/launch/main.cpp create mode 100644 src/launch/main.hpp create mode 100644 src/launch/parsecommand.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4014cce2..f951d968 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,7 @@ boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON) include(cmake/install-qml-module.cmake) +include(cmake/util.cmake) add_compile_options(-Wall -Wextra) @@ -87,9 +88,10 @@ if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Debug) endif() -set(QT_DEPS Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2 Qt6::Widgets) set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets) +include(cmake/pch.cmake) + if (BUILD_TESTING) enable_testing() add_definitions(-DQS_TEST) @@ -97,56 +99,27 @@ if (BUILD_TESTING) endif() if (SOCKETS) - list(APPEND QT_DEPS Qt6::Network) list(APPEND QT_FPDEPS Network) endif() if (WAYLAND) - list(APPEND QT_DEPS Qt6::WaylandClient Qt6::WaylandClientPrivate) list(APPEND QT_FPDEPS WaylandClient) endif() -if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS) +if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS) set(DBUS ON) endif() if (DBUS) - list(APPEND QT_DEPS Qt6::DBus) list(APPEND QT_FPDEPS DBus) endif() find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS}) +set(CMAKE_AUTOUIC OFF) qt_standard_project_setup(REQUIRES 6.6) set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules) -# pch breaks clang-tidy..... somehow -if (NOT NO_PCH) - file(GENERATE - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/pchstub.cpp - CONTENT "// intentionally empty" - ) - - add_library(qt-pch ${CMAKE_CURRENT_BINARY_DIR}/pchstub.cpp) - target_link_libraries(qt-pch PRIVATE ${QT_DEPS}) - target_precompile_headers(qt-pch PUBLIC - - - - - - - - ) -endif() - -function (qs_pch target) - if (NOT NO_PCH) - target_precompile_headers(${target} REUSE_FROM qt-pch) - target_link_libraries(${target} PRIVATE ${QT_DEPS}) # required for gcc to accept the pch on plugin targets - endif() -endfunction() - add_subdirectory(src) if (USE_JEMALLOC) diff --git a/cmake/pch.cmake b/cmake/pch.cmake new file mode 100644 index 00000000..e136015e --- /dev/null +++ b/cmake/pch.cmake @@ -0,0 +1,85 @@ +# pch breaks clang-tidy..... somehow +if (NOT NO_PCH) + file(GENERATE + OUTPUT ${CMAKE_BINARY_DIR}/pchstub.cpp + CONTENT "// intentionally empty" + ) +endif() + +function (qs_pch target) + if (NO_PCH) + return() + endif() + + cmake_parse_arguments(PARSE_ARGV 1 arg "" "SET" "") + + if ("${arg_SET}" STREQUAL "") + set(arg_SET "common") + endif() + + target_precompile_headers(${target} REUSE_FROM "qs-pchset-${arg_SET}") +endfunction() + +function (qs_module_pch target) + qs_pch(${target} ${ARGN}) + qs_pch("${target}plugin" SET plugin) + qs_pch("${target}plugin_init" SET plugin) +endfunction() + +function (qs_add_pchset SETNAME) + if (NO_PCH) + return() + endif() + + cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "HEADERS;DEPENDENCIES") + + set(LIBNAME "qs-pchset-${SETNAME}") + + add_library(${LIBNAME} ${CMAKE_BINARY_DIR}/pchstub.cpp) + target_link_libraries(${LIBNAME} ${arg_DEPENDENCIES}) + target_precompile_headers(${LIBNAME} PUBLIC ${arg_HEADERS}) +endfunction() + +set(COMMON_PCH_SET + + + + + + + + + + +) + +qs_add_pchset(common + DEPENDENCIES Qt::Quick + HEADERS ${COMMON_PCH_SET} +) + +qs_add_pchset(large + DEPENDENCIES Qt::Quick + HEADERS + ${COMMON_PCH_SET} + + + + + + + + + + +) + + +# including qplugin.h directly will cause required symbols to disappear +qs_add_pchset(plugin + DEPENDENCIES Qt::Qml + HEADERS + + + +) diff --git a/cmake/util.cmake b/cmake/util.cmake new file mode 100644 index 00000000..5d261a40 --- /dev/null +++ b/cmake/util.cmake @@ -0,0 +1,20 @@ +function (qs_append_qmldir target text) + get_property(qmldir_content TARGET ${target} PROPERTY _qt_internal_qmldir_content) + + if ("${qmldir_content}" STREQUAL "") + message(WARNING "qs_append_qmldir depends on private Qt cmake code, which has broken.") + return() + endif() + + set_property(TARGET ${target} APPEND_STRING PROPERTY _qt_internal_qmldir_content ${text}) +endfunction() + +# DEPENDENCIES introduces a cmake dependency which we don't need with static modules. +# This greatly improves comp speed by not introducing those dependencies. +function (qs_add_module_deps_light target) + foreach (dep IN LISTS ARGN) + string(APPEND qmldir_extra "depends ${dep}\n") + endforeach() + + qs_append_qmldir(${target} "${qmldir_extra}") +endfunction() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5b843543..c518a1c9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,9 +1,8 @@ qt_add_executable(quickshell main.cpp) -target_link_libraries(quickshell PRIVATE ${QT_DEPS} quickshell-build) - install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +add_subdirectory(launch) add_subdirectory(build) add_subdirectory(core) add_subdirectory(ipc) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 62f29425..c75dd588 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,5 +1,3 @@ -find_package(CLI11 CONFIG REQUIRED) - qt_add_library(quickshell-core STATIC plugin.cpp shell.cpp @@ -37,10 +35,9 @@ qt_add_library(quickshell-core STATIC paths.cpp instanceinfo.cpp common.cpp + iconprovider.cpp ) -target_link_libraries(quickshell-core PRIVATE quickshell-build) - qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1 @@ -51,10 +48,9 @@ qt_add_qml_module(quickshell-core install_qml_module(quickshell-core) -target_link_libraries(quickshell-core PRIVATE ${QT_DEPS} CLI11::CLI11) +target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets) -qs_pch(quickshell-core) -qs_pch(quickshell-coreplugin) +qs_module_pch(quickshell-core SET large) target_link_libraries(quickshell PRIVATE quickshell-coreplugin) diff --git a/src/core/clock.cpp b/src/core/clock.cpp index 75785223..ebb7e92a 100644 --- a/src/core/clock.cpp +++ b/src/core/clock.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "util.hpp" diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 395f255b..147e2f93 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -8,17 +8,12 @@ #include #include #include -#include -#include #include #include #include -#include #include #include #include -#include -#include #include #include "iconimageprovider.hpp" @@ -331,90 +326,6 @@ EngineGeneration* EngineGeneration::currentGeneration() { } else return nullptr; } -// QMenu re-calls pixmap() every time the mouse moves so its important to cache it. -class PixmapCacheIconEngine: public QIconEngine { - void paint( - QPainter* /*unused*/, - const QRect& /*unused*/, - QIcon::Mode /*unused*/, - QIcon::State /*unused*/ - ) override { - 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 { - if (this->lastPixmap.isNull() || size != this->lastSize) { - this->lastPixmap = this->createPixmap(size); - this->lastSize = size; - } - - return this->lastPixmap; - } - - virtual QPixmap createPixmap(const QSize& size) = 0; - -private: - QSize lastSize; - QPixmap lastPixmap; -}; - -class ImageProviderIconEngine: public PixmapCacheIconEngine { -public: - explicit ImageProviderIconEngine(QQuickImageProvider* provider, QString id) - : provider(provider) - , id(std::move(id)) {} - - QPixmap createPixmap(const QSize& size) override { - if (this->provider->imageType() == QQmlImageProviderBase::Pixmap) { - return this->provider->requestPixmap(this->id, nullptr, size); - } else if (this->provider->imageType() == QQmlImageProviderBase::Image) { - auto image = this->provider->requestImage(this->id, nullptr, size); - return QPixmap::fromImage(image); - } else { - qFatal() << "Unexpected ImageProviderIconEngine image type" << this->provider->imageType(); - return QPixmap(); // never reached, satisfies lint - } - } - - [[nodiscard]] QIconEngine* clone() const override { - return new ImageProviderIconEngine(this->provider, this->id); - } - -private: - QQuickImageProvider* provider; - QString id; -}; - -QIcon EngineGeneration::iconByUrl(const QUrl& url) const { - if (url.isEmpty()) return QIcon(); - - auto scheme = url.scheme(); - if (scheme == "image") { - auto providerName = url.authority(); - auto path = url.path(); - if (!path.isEmpty()) path = path.sliced(1); - - auto* provider = qobject_cast(this->engine->imageProvider(providerName)); - - if (provider == nullptr) { - qWarning() << "iconByUrl failed: no provider found for" << url; - return QIcon(); - } - - if (provider->imageType() == QQmlImageProviderBase::Pixmap - || provider->imageType() == QQmlImageProviderBase::Image) - { - return QIcon(new ImageProviderIconEngine(provider, path)); - } - - } else { - qWarning() << "iconByUrl failed: unsupported scheme" << scheme << "in path" << url; - } - - return QIcon(); -} - EngineGeneration* EngineGeneration::findEngineGeneration(QQmlEngine* engine) { return g_generations.value(engine); } diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 823ca82a..043d2f70 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -4,13 +4,11 @@ #include #include #include -#include #include #include #include #include #include -#include #include "incubator.hpp" #include "qsintercept.hpp" @@ -54,8 +52,6 @@ public: // otherwise null. static EngineGeneration* currentGeneration(); - [[nodiscard]] QIcon iconByUrl(const QUrl& url) const; - RootWrapper* wrapper = nullptr; QDir rootPath; QmlScanner scanner; diff --git a/src/core/iconprovider.cpp b/src/core/iconprovider.cpp new file mode 100644 index 00000000..99b423ed --- /dev/null +++ b/src/core/iconprovider.cpp @@ -0,0 +1,105 @@ +#include "iconprovider.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "generation.hpp" + +// QMenu re-calls pixmap() every time the mouse moves so its important to cache it. +class PixmapCacheIconEngine: public QIconEngine { + void paint( + QPainter* /*unused*/, + const QRect& /*unused*/, + QIcon::Mode /*unused*/, + QIcon::State /*unused*/ + ) override { + 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 { + if (this->lastPixmap.isNull() || size != this->lastSize) { + this->lastPixmap = this->createPixmap(size); + this->lastSize = size; + } + + return this->lastPixmap; + } + + virtual QPixmap createPixmap(const QSize& size) = 0; + +private: + QSize lastSize; + QPixmap lastPixmap; +}; + +class ImageProviderIconEngine: public PixmapCacheIconEngine { +public: + explicit ImageProviderIconEngine(QQuickImageProvider* provider, QString id) + : provider(provider) + , id(std::move(id)) {} + + QPixmap createPixmap(const QSize& size) override { + if (this->provider->imageType() == QQmlImageProviderBase::Pixmap) { + return this->provider->requestPixmap(this->id, nullptr, size); + } else if (this->provider->imageType() == QQmlImageProviderBase::Image) { + auto image = this->provider->requestImage(this->id, nullptr, size); + return QPixmap::fromImage(image); + } else { + qFatal() << "Unexpected ImageProviderIconEngine image type" << this->provider->imageType(); + return QPixmap(); // never reached, satisfies lint + } + } + + [[nodiscard]] QIconEngine* clone() const override { + return new ImageProviderIconEngine(this->provider, this->id); + } + +private: + QQuickImageProvider* provider; + QString id; +}; + +QIcon getEngineImageAsIcon(QQmlEngine* engine, const QUrl& url) { + if (!engine || url.isEmpty()) return QIcon(); + + auto scheme = url.scheme(); + if (scheme == "image") { + auto providerName = url.authority(); + auto path = url.path(); + if (!path.isEmpty()) path = path.sliced(1); + + auto* provider = qobject_cast(engine->imageProvider(providerName)); + + if (provider == nullptr) { + qWarning() << "iconByUrl failed: no provider found for" << url; + return QIcon(); + } + + if (provider->imageType() == QQmlImageProviderBase::Pixmap + || provider->imageType() == QQmlImageProviderBase::Image) + { + return QIcon(new ImageProviderIconEngine(provider, path)); + } + + } else { + qWarning() << "iconByUrl failed: unsupported scheme" << scheme << "in path" << url; + } + + return QIcon(); +} + +QIcon getCurrentEngineImageAsIcon(const QUrl& url) { + auto* generation = EngineGeneration::currentGeneration(); + if (!generation) return QIcon(); + return getEngineImageAsIcon(generation->engine, url); +} diff --git a/src/core/iconprovider.hpp b/src/core/iconprovider.hpp new file mode 100644 index 00000000..173d20e6 --- /dev/null +++ b/src/core/iconprovider.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include +#include +#include + +QIcon getEngineImageAsIcon(QQmlEngine* engine, const QUrl& url); +QIcon getCurrentEngineImageAsIcon(const QUrl& url); diff --git a/src/core/platformmenu.cpp b/src/core/platformmenu.cpp index 09837ec9..a06575ea 100644 --- a/src/core/platformmenu.cpp +++ b/src/core/platformmenu.cpp @@ -19,7 +19,8 @@ #include "../window/proxywindow.hpp" #include "../window/windowinterface.hpp" -#include "generation.hpp" +#include "iconprovider.hpp" +#include "platformmenu_p.hpp" #include "popupanchor.hpp" #include "qsmenu.hpp" @@ -174,8 +175,7 @@ void PlatformMenuEntry::relayout() { auto icon = this->menu->icon(); if (!icon.isEmpty()) { - auto* generation = EngineGeneration::currentGeneration(); - this->qmenu->setIcon(generation->iconByUrl(this->menu->icon())); + this->qmenu->setIcon(getCurrentEngineImageAsIcon(icon)); } auto children = this->menu->children(); @@ -216,8 +216,7 @@ void PlatformMenuEntry::relayout() { auto icon = this->menu->icon(); if (!icon.isEmpty()) { - auto* generation = EngineGeneration::currentGeneration(); - this->qaction->setIcon(generation->iconByUrl(this->menu->icon())); + this->qaction->setIcon(getCurrentEngineImageAsIcon(icon)); } this->qaction->setEnabled(this->menu->enabled()); @@ -272,8 +271,7 @@ void PlatformMenuEntry::onIconChanged() { QIcon icon; if (!iconName.isEmpty()) { - auto* generation = EngineGeneration::currentGeneration(); - icon = generation->iconByUrl(iconName); + icon = getCurrentEngineImageAsIcon(iconName); } if (this->qmenu != nullptr) { diff --git a/src/core/platformmenu.hpp b/src/core/platformmenu.hpp index 5e8a0afe..5979f90e 100644 --- a/src/core/platformmenu.hpp +++ b/src/core/platformmenu.hpp @@ -5,9 +5,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -18,17 +16,7 @@ namespace qs::menu::platform { -class PlatformMenuQMenu: public QMenu { -public: - explicit PlatformMenuQMenu() = default; - ~PlatformMenuQMenu() override; - Q_DISABLE_COPY_MOVE(PlatformMenuQMenu); - - void setVisible(bool visible) override; - - PlatformMenuQMenu* containingMenu = nullptr; - QPoint targetPosition; -}; +class PlatformMenuQMenu; class PlatformMenuEntry: public QObject { Q_OBJECT; diff --git a/src/core/platformmenu_p.hpp b/src/core/platformmenu_p.hpp new file mode 100644 index 00000000..9109959d --- /dev/null +++ b/src/core/platformmenu_p.hpp @@ -0,0 +1,19 @@ +#pragma once +#include +#include + +namespace qs::menu::platform { + +class PlatformMenuQMenu: public QMenu { +public: + explicit PlatformMenuQMenu() = default; + ~PlatformMenuQMenu() override; + Q_DISABLE_COPY_MOVE(PlatformMenuQMenu); + + void setVisible(bool visible) override; + + PlatformMenuQMenu* containingMenu = nullptr; + QPoint targetPosition; +}; + +} // namespace qs::menu::platform diff --git a/src/core/popupanchor.hpp b/src/core/popupanchor.hpp index 0897928a..a0f6353c 100644 --- a/src/core/popupanchor.hpp +++ b/src/core/popupanchor.hpp @@ -2,7 +2,6 @@ #include -#include #include #include #include diff --git a/src/core/test/CMakeLists.txt b/src/core/test/CMakeLists.txt index 448881a6..c9a82005 100644 --- a/src/core/test/CMakeLists.txt +++ b/src/core/test/CMakeLists.txt @@ -1,6 +1,6 @@ function (qs_test name) add_executable(${name} ${ARGN}) - target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test quickshell-core quickshell-window) + target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window) add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) endfunction() diff --git a/src/dbus/CMakeLists.txt b/src/dbus/CMakeLists.txt index 49a4a06b..9948ea74 100644 --- a/src/dbus/CMakeLists.txt +++ b/src/dbus/CMakeLists.txt @@ -16,8 +16,18 @@ qt_add_library(quickshell-dbus STATIC # dbus headers target_include_directories(quickshell-dbus PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) -target_link_libraries(quickshell-dbus PRIVATE ${QT_DEPS}) +target_link_libraries(quickshell-dbus PRIVATE Qt::Core Qt::DBus) +# todo: link dbus to quickshell here instead of in modules that use it directly +# linker does not like this as is -qs_pch(quickshell-dbus) +qs_add_pchset(dbus + DEPENDENCIES Qt::DBus + HEADERS + + + +) + +qs_pch(quickshell-dbus SET dbus) add_subdirectory(dbusmenu) diff --git a/src/dbus/dbusmenu/CMakeLists.txt b/src/dbus/dbusmenu/CMakeLists.txt index f9e4446c..ac50b28a 100644 --- a/src/dbus/dbusmenu/CMakeLists.txt +++ b/src/dbus/dbusmenu/CMakeLists.txt @@ -17,15 +17,18 @@ qt_add_library(quickshell-dbusmenu STATIC qt_add_qml_module(quickshell-dbusmenu URI Quickshell.DBusMenu VERSION 0.1 - DEPENDENCIES QtQml Quickshell + DEPENDENCIES QtQml ) +qs_add_module_deps_light(quickshell-dbusmenu Quickshell) + install_qml_module(quickshell-dbusmenu) # dbus headers target_include_directories(quickshell-dbusmenu PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) -target_link_libraries(quickshell-dbusmenu PRIVATE ${QT_DEPS}) +target_link_libraries(quickshell-dbusmenu PRIVATE Qt::Quick Qt::DBus quickshell-dbus) -qs_pch(quickshell-dbusmenu) -qs_pch(quickshell-dbusmenuplugin) +qs_module_pch(quickshell-dbusmenu SET dbus) + +target_link_libraries(quickshell PRIVATE quickshell-dbusmenuplugin) diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index 7dac84ab..1a40ca23 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index 1d936d17..6299b397 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -23,14 +23,12 @@ qt_add_qml_module(quickshell-io install_qml_module(quickshell-io) -target_link_libraries(quickshell-io PRIVATE ${QT_DEPS}) -target_link_libraries(quickshell-io-init PRIVATE ${QT_DEPS}) +target_link_libraries(quickshell-io PRIVATE Qt::Quick) +target_link_libraries(quickshell-io-init PRIVATE Qt::Qml) target_link_libraries(quickshell PRIVATE quickshell-ioplugin quickshell-io-init) -qs_pch(quickshell-io) -qs_pch(quickshell-ioplugin) -qs_pch(quickshell-io-init) +qs_module_pch(quickshell-io) if (BUILD_TESTING) add_subdirectory(test) diff --git a/src/io/test/CMakeLists.txt b/src/io/test/CMakeLists.txt index 0c0cfc55..4ab51739 100644 --- a/src/io/test/CMakeLists.txt +++ b/src/io/test/CMakeLists.txt @@ -1,6 +1,6 @@ function (qs_test name) add_executable(${name} ${ARGN}) - target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test) + target_link_libraries(${name} PRIVATE Qt::Quick Qt::Network Qt::Test) add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) endfunction() diff --git a/src/ipc/CMakeLists.txt b/src/ipc/CMakeLists.txt index ff6093c6..e3f9e25f 100644 --- a/src/ipc/CMakeLists.txt +++ b/src/ipc/CMakeLists.txt @@ -2,6 +2,8 @@ qt_add_library(quickshell-ipc STATIC ipc.cpp ) -target_link_libraries(quickshell-ipc PRIVATE ${QT_DEPS}) +qs_pch(quickshell-ipc) + +target_link_libraries(quickshell-ipc PRIVATE Qt::Quick Qt::Network) target_link_libraries(quickshell PRIVATE quickshell-ipc) diff --git a/src/launch/CMakeLists.txt b/src/launch/CMakeLists.txt new file mode 100644 index 00000000..4db11bf0 --- /dev/null +++ b/src/launch/CMakeLists.txt @@ -0,0 +1,23 @@ +find_package(CLI11 CONFIG REQUIRED) + +qt_add_library(quickshell-launch STATIC + parsecommand.cpp + command.cpp + launch.cpp + main.cpp +) + +target_link_libraries(quickshell-launch PRIVATE + Qt::Quick Qt::Widgets CLI11::CLI11 quickshell-build +) + +qs_add_pchset(launch + DEPENDENCIES Qt::Core CLI11::CLI11 + HEADERS + + +) + +qs_pch(quickshell-launch SET launch) + +target_link_libraries(quickshell PRIVATE quickshell-launch) diff --git a/src/launch/command.cpp b/src/launch/command.cpp new file mode 100644 index 00000000..83001037 --- /dev/null +++ b/src/launch/command.cpp @@ -0,0 +1,448 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/instanceinfo.hpp" +#include "../core/logging.hpp" +#include "../core/paths.hpp" +#include "../io/ipccomm.hpp" +#include "../ipc/ipc.hpp" +#include "build.hpp" +#include "launch_p.hpp" + +namespace qs::launch { + +using qs::ipc::IpcClient; + +int readLogFile(CommandState& cmd); +int listInstances(CommandState& cmd); +int killInstances(CommandState& cmd); +int msgInstance(CommandState& cmd); +int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication); +int locateConfigFile(CommandState& cmd, QString& path); + +int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { + auto state = CommandState(); + if (auto ret = parseCommand(argc, argv, state); ret != 0) return ret; + + if (state.misc.checkCompat) { + if (strcmp(qVersion(), QT_VERSION_STR) != 0) { + 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"; + return 1; + } + + return 0; + } + + // Has to happen before extra threads are spawned. + if (state.misc.daemonize) { + auto closepipes = std::array(); + if (pipe(closepipes.data()) == -1) { + qFatal().nospace() << "Failed to create messaging pipes for daemon with error " << errno + << ": " << qt_error_string(); + } + + DAEMON_PIPE = closepipes[1]; + + pid_t pid = fork(); // NOLINT (include) + + if (pid == -1) { + qFatal().nospace() << "Failed to fork daemon with error " << errno << ": " + << qt_error_string(); + } else if (pid == 0) { + close(closepipes[0]); + + if (setsid() == -1) { + qFatal().nospace() << "Failed to setsid with error " << errno << ": " << qt_error_string(); + } + } else { + close(closepipes[1]); + + int ret = 0; + if (read(closepipes[0], &ret, sizeof(int)) == -1) { + qFatal() << "Failed to wait for daemon launch (it may have crashed)"; + } + + return ret; + } + } + + { + auto level = state.log.verbosity == 0 ? QtWarningMsg + : state.log.verbosity == 1 ? QtInfoMsg + : QtDebugMsg; + + LogManager::init( + !state.log.noColor, + state.log.timestamp, + state.log.sparse, + level, + *state.log.rules, + *state.subcommand.log ? "READER" : "" + ); + } + + if (state.misc.printVersion) { + qCInfo(logBare).noquote().nospace() << "quickshell pre-release, revision " << GIT_REVISION + << ", distributed by: " << DISTRIBUTOR; + + if (state.log.verbosity > 1) { + qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; + qCInfo(logBare).noquote() << "Runtime Qt Version:" << qVersion(); + qCInfo(logBare).noquote() << "Compiler:" << COMPILER; + qCInfo(logBare).noquote() << "Compile Flags:" << COMPILE_FLAGS; + } + + if (state.log.verbosity > 0) { + qCInfo(logBare).noquote() << "\nBuild Type:" << BUILD_TYPE; + qCInfo(logBare).noquote() << "Build configuration:"; + qCInfo(logBare).noquote().nospace() << BUILD_CONFIGURATION; + } + } else if (*state.subcommand.log) { + return readLogFile(state); + } else if (*state.subcommand.list) { + return listInstances(state); + } else if (*state.subcommand.kill) { + return killInstances(state); + } else if (*state.subcommand.msg) { + return msgInstance(state); + } else { + if (strcmp(qVersion(), QT_VERSION_STR) != 0) { + qWarning() << "\033[31mQuickshell 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 " + "the quickshell package must be rebuilt.\n"; + } + + return launchFromCommand(state, coreApplication); + } + + return 0; +} + +int locateConfigFile(CommandState& cmd, QString& path) { + if (!cmd.config.path->isEmpty()) { + path = *cmd.config.path; + } else { + auto manifestPath = *cmd.config.manifest; + if (manifestPath.isEmpty()) { + auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); + auto path = configDir.filePath("manifest.conf"); + if (QFileInfo(path).isFile()) manifestPath = path; + } + + if (!manifestPath.isEmpty()) { + auto file = QFile(manifestPath); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + auto stream = QTextStream(&file); + while (!stream.atEnd()) { + auto line = stream.readLine(); + if (line.trimmed().startsWith("#")) continue; + if (line.trimmed().isEmpty()) continue; + + auto split = line.split('='); + if (split.length() != 2) { + qCritical() << "Manifest line not in expected format 'name = relativepath':" << line; + return -1; + } + + if (split[0].trimmed() == *cmd.config.name) { + path = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed()); + break; + } + } + + if (path.isEmpty()) { + qCCritical(logBare) << "Configuration" << *cmd.config.name + << "not found when searching manifest" << manifestPath; + return -1; + } + } else { + qCCritical(logBare) << "Could not open maifest at path" << *cmd.config.manifest; + return -1; + } + } else { + auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); + + if (cmd.config.name->isEmpty()) { + path = configDir.path(); + } else { + path = configDir.filePath(*cmd.config.name); + } + } + } + + if (QFileInfo(path).isDir()) { + path = QDir(path).filePath("shell.qml"); + } + + if (!QFileInfo(path).isFile()) { + qCCritical(logBare) << "Could not open config file at" << path; + return -1; + } + + path = QFileInfo(path).canonicalFilePath(); + + return 0; +} + +void sortInstances(QVector& list) { + std::sort(list.begin(), list.end(), [](const InstanceLockInfo& a, const InstanceLockInfo& b) { + return a.instance.launchTime < b.instance.launchTime; + }); +}; + +int selectInstance(CommandState& cmd, InstanceLockInfo* instance) { + auto* basePath = QsPaths::instance()->baseRunDir(); + if (!basePath) return -1; + + QString path; + + if (cmd.instance.pid != -1) { + path = QDir(basePath->filePath("by-pid")).filePath(QString::number(cmd.instance.pid)); + if (!QsPaths::checkLock(path, instance)) { + qCInfo(logBare) << "No instance found for pid" << cmd.instance.pid; + return -1; + } + } else if (!cmd.instance.id->isEmpty()) { + path = basePath->filePath("by-pid"); + auto instances = QsPaths::collectInstances(path); + + auto itr = + std::remove_if(instances.begin(), instances.end(), [&](const InstanceLockInfo& info) { + return !info.instance.instanceId.startsWith(*cmd.instance.id); + }); + + instances.erase(itr, instances.end()); + + if (instances.isEmpty()) { + qCInfo(logBare) << "No running instances start with" << *cmd.instance.id; + return -1; + } else if (instances.length() != 1) { + qCInfo(logBare) << "More than one instance starts with" << *cmd.instance.id; + + for (auto& instance: instances) { + qCInfo(logBare).noquote() << " -" << instance.instance.instanceId; + } + + return -1; + } else { + *instance = instances.value(0); + } + } else { + QString configFilePath; + auto r = locateConfigFile(cmd, configFilePath); + if (r != 0) return r; + + auto pathId = + QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + + path = QDir(basePath->filePath("by-path")).filePath(pathId); + + auto instances = QsPaths::collectInstances(path); + sortInstances(instances); + + if (instances.isEmpty()) { + qCInfo(logBare) << "No running instances for" << configFilePath; + return -1; + } + + *instance = instances.value(0); + } + + return 0; +} + +int readLogFile(CommandState& cmd) { + auto path = *cmd.log.file; + + if (path.isEmpty()) { + InstanceLockInfo instance; + auto r = selectInstance(cmd, &instance); + if (r != 0) return r; + + path = QDir(QsPaths::basePath(instance.instance.instanceId)).filePath("log.qslog"); + } + + auto file = QFile(path); + if (!file.open(QFile::ReadOnly)) { + qCCritical(logBare) << "Failed to open log file" << path; + return -1; + } + + return qs::log::readEncodedLogs( + &file, + path, + cmd.log.timestamp, + cmd.log.tail, + cmd.log.follow, + *cmd.log.readoutRules + ) + ? 0 + : -1; +} + +int listInstances(CommandState& cmd) { + auto* basePath = QsPaths::instance()->baseRunDir(); + if (!basePath) return -1; // NOLINT + + QString path; + QString configFilePath; + if (cmd.instance.all) { + path = basePath->filePath("by-pid"); + } else { + auto r = locateConfigFile(cmd, configFilePath); + + if (r != 0) { + qCInfo(logBare) << "Use --all to list all instances."; + return r; + } + + auto pathId = + QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); + + path = QDir(basePath->filePath("by-path")).filePath(pathId); + } + + auto instances = QsPaths::collectInstances(path); + + if (instances.isEmpty()) { + if (cmd.instance.all) { + qCInfo(logBare) << "No running instances."; + } else { + qCInfo(logBare) << "No running instances for" << configFilePath; + qCInfo(logBare) << "Use --all to list all instances."; + } + } else { + sortInstances(instances); + + if (cmd.output.json) { + auto array = QJsonArray(); + + for (auto& instance: instances) { + auto json = QJsonObject(); + + json["id"] = instance.instance.instanceId; + json["pid"] = instance.pid; + json["shell_id"] = instance.instance.shellId; + json["config_path"] = instance.instance.configPath; + json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate); + + array.push_back(json); + } + + auto document = QJsonDocument(array); + QTextStream(stdout) << document.toJson(QJsonDocument::Indented); + } else { + for (auto& instance: instances) { + auto launchTimeStr = instance.instance.launchTime.toString("yyyy-MM-dd hh:mm:ss"); + + auto runSeconds = instance.instance.launchTime.secsTo(QDateTime::currentDateTime()); + auto remSeconds = runSeconds % 60; + auto runMinutes = (runSeconds - remSeconds) / 60; + auto remMinutes = runMinutes % 60; + auto runHours = (runMinutes - remMinutes) / 60; + auto runtimeStr = QString("%1 hours, %2 minutes, %3 seconds") + .arg(runHours) + .arg(remMinutes) + .arg(remSeconds); + + qCInfo(logBare).noquote().nospace() + << "Instance " << instance.instance.instanceId << ":\n" + << " Process ID: " << instance.pid << '\n' + << " Shell ID: " << instance.instance.shellId << '\n' + << " Config path: " << instance.instance.configPath << '\n' + << " Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n"; + } + } + } + + return 0; +} + +int killInstances(CommandState& cmd) { + InstanceLockInfo instance; + auto r = selectInstance(cmd, &instance); + if (r != 0) return r; + + return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { + client.kill(); + qCInfo(logBare).noquote() << "Killed" << instance.instance.instanceId; + }); +} + +int msgInstance(CommandState& cmd) { + InstanceLockInfo instance; + auto r = selectInstance(cmd, &instance); + if (r != 0) return r; + + return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { + if (cmd.ipc.info) { + return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.function); + } else { + QVector arguments; + for (auto& arg: cmd.ipc.arguments) { + arguments += *arg; + } + + return qs::io::ipc::comm::callFunction( + &client, + *cmd.ipc.target, + *cmd.ipc.function, + arguments + ); + } + + return -1; + }); +} + +int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication) { + QString configPath; + + auto r = locateConfigFile(cmd, configPath); + if (r != 0) return r; + + { + InstanceLockInfo info; + if (cmd.misc.noDuplicate && selectInstance(cmd, &info) == 0) { + qCInfo(logBare) << "An instance of this configuration is already running."; + return 0; + } + } + + return launch( + { + .configPath = configPath, + .debugPort = cmd.debug.port, + .waitForDebug = cmd.debug.wait, + }, + cmd.exec.argv, + coreApplication + ); +} + +} // namespace qs::launch diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp new file mode 100644 index 00000000..30c87a62 --- /dev/null +++ b/src/launch/launch.cpp @@ -0,0 +1,238 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/common.hpp" +#include "../core/instanceinfo.hpp" +#include "../core/logging.hpp" +#include "../core/paths.hpp" +#include "../core/plugin.hpp" +#include "../core/rootwrapper.hpp" +#include "../ipc/ipc.hpp" +#include "build.hpp" +#include "launch_p.hpp" + +#if CRASH_REPORTER +#include "../crash/handler.hpp" +#endif + +namespace qs::launch { + +template +QString base36Encode(T number) { + const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz"; + QString result; + + do { + result.prepend(digits[number % 36]); + number /= 36; + } while (number > 0); + + for (auto i = 0; i < result.length() / 2; i++) { + auto opposite = result.length() - i - 1; + auto c = result.at(i); + result[i] = result.at(opposite); + result[opposite] = c; + } + + return result; +} + +int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication) { + auto pathId = QCryptographicHash::hash(args.configPath.toUtf8(), QCryptographicHash::Md5).toHex(); + auto shellId = QString(pathId); + + qInfo() << "Launching config:" << args.configPath; + + auto file = QFile(args.configPath); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + qCritical() << "Could not open config file" << args.configPath; + return -1; + } + + struct { + bool useQApplication = false; + bool nativeTextRendering = false; + bool desktopSettingsAware = true; + QString iconTheme = qEnvironmentVariable("QS_ICON_THEME"); + QHash envOverrides; + } pragmas; + + auto stream = QTextStream(&file); + while (!stream.atEnd()) { + auto line = stream.readLine().trimmed(); + if (line.startsWith("//@ pragma ")) { + auto pragma = line.sliced(11).trimmed(); + + if (pragma == "UseQApplication") pragmas.useQApplication = true; + else if (pragma == "NativeTextRendering") pragmas.nativeTextRendering = true; + else if (pragma == "IgnoreSystemSettings") pragmas.desktopSettingsAware = false; + else if (pragma.startsWith("IconTheme ")) pragmas.iconTheme = pragma.sliced(10); + else if (pragma.startsWith("Env ")) { + auto envPragma = pragma.sliced(4); + auto splitIdx = envPragma.indexOf('='); + + if (splitIdx == -1) { + qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'"; + return -1; + } + + auto var = envPragma.sliced(0, splitIdx).trimmed(); + auto val = envPragma.sliced(splitIdx + 1).trimmed(); + pragmas.envOverrides.insert(var, val); + } else if (pragma.startsWith("ShellId ")) { + shellId = pragma.sliced(8).trimmed(); + } else { + qCritical() << "Unrecognized pragma" << pragma; + return -1; + } + } else if (line.startsWith("import")) break; + } + + file.close(); + + if (!pragmas.iconTheme.isEmpty()) { + QIcon::setThemeName(pragmas.iconTheme); + } + + qInfo() << "Shell ID:" << shellId << "Path ID" << pathId; + + auto launchTime = qs::Common::LAUNCH_TIME.toSecsSinceEpoch(); + InstanceInfo::CURRENT = InstanceInfo { + .instanceId = base36Encode(getpid()) + base36Encode(launchTime), + .configPath = args.configPath, + .shellId = shellId, + .launchTime = qs::Common::LAUNCH_TIME, + }; + +#if CRASH_REPORTER + auto crashHandler = crash::CrashHandler(); + crashHandler.init(); + + { + auto* log = LogManager::instance(); + crashHandler.setRelaunchInfo({ + .instance = InstanceInfo::CURRENT, + .noColor = !log->colorLogs, + .timestamp = log->timestampLogs, + .sparseLogsOnly = log->isSparse(), + .defaultLogLevel = log->defaultLevel(), + .logRules = log->rulesString(), + }); + } +#endif + + QsPaths::init(shellId, pathId); + QsPaths::instance()->linkRunDir(); + QsPaths::instance()->linkPathDir(); + LogManager::initFs(); + + for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) { + qputenv(var.toUtf8(), val.toUtf8()); + } + + // The qml engine currently refuses to cache non file (qsintercept) paths. + + // if (auto* cacheDir = QsPaths::instance()->cacheDir()) { + // auto qmlCacheDir = cacheDir->filePath("qml-engine-cache"); + // qputenv("QML_DISK_CACHE_PATH", qmlCacheDir.toLocal8Bit()); + // + // if (!qEnvironmentVariableIsSet("QML_DISK_CACHE")) { + // qputenv("QML_DISK_CACHE", "aot,qmlc"); + // } + // } + + // While the simple animation driver can lead to better animations in some cases, + // it also can cause excessive repainting at excessively high framerates which can + // lead to noticeable amounts of gpu usage, including overheating on some systems. + // This gets worse the more windows are open, as repaints trigger on all of them for + // some reason. See QTBUG-126099 for details. + + // if (!qEnvironmentVariableIsSet("QSG_USE_SIMPLE_ANIMATION_DRIVER")) { + // qputenv("QSG_USE_SIMPLE_ANIMATION_DRIVER", "1"); + // } + + // Some programs place icons in the pixmaps folder instead of the icons folder. + // This seems to be controlled by the QPA and qt6ct does not provide it. + { + QList dataPaths; + + 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"); + } + + auto fallbackPaths = QIcon::fallbackSearchPaths(); + + for (auto& path: dataPaths) { + auto newPath = QDir(path).filePath("pixmaps"); + + if (!fallbackPaths.contains(newPath)) { + fallbackPaths.push_back(newPath); + } + } + + QIcon::setFallbackSearchPaths(fallbackPaths); + } + + QGuiApplication::setDesktopSettingsAware(pragmas.desktopSettingsAware); + + delete coreApplication; + + QGuiApplication* app = nullptr; + auto qArgC = 0; + + if (pragmas.useQApplication) { + app = new QApplication(qArgC, argv); + } else { + app = new QGuiApplication(qArgC, argv); + } + + if (args.debugPort != -1) { + QQmlDebuggingEnabler::enableDebugging(true); + auto wait = args.waitForDebug ? QQmlDebuggingEnabler::WaitForClient + : QQmlDebuggingEnabler::DoNotWaitForClient; + QQmlDebuggingEnabler::startTcpDebugServer(args.debugPort, wait); + } + + QuickshellPlugin::initPlugins(); + + // Base window transparency appears to be additive. + // Use a fully transparent window with a colored rect. + QQuickWindow::setDefaultAlphaBuffer(true); + + if (pragmas.nativeTextRendering) { + QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); + } + + qs::ipc::IpcServer::start(); + QsPaths::instance()->createLock(); + + auto root = RootWrapper(args.configPath, shellId); + QGuiApplication::setQuitOnLastWindowClosed(false); + + exitDaemon(0); + + auto code = QGuiApplication::exec(); + delete app; + return code; +} + +} // namespace qs::launch diff --git a/src/launch/launch_p.hpp b/src/launch/launch_p.hpp new file mode 100644 index 00000000..d1916d50 --- /dev/null +++ b/src/launch/launch_p.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include + +#include +#include +#include + +namespace qs::launch { + +extern int DAEMON_PIPE; // NOLINT + +class QStringOption { +public: + QStringOption() = default; + QStringOption& operator=(const std::string& str) { + this->str = QString::fromStdString(str); + return *this; + } + + QString& operator*() { return this->str; } + QString* operator->() { return &this->str; } + +private: + QString str; +}; + +struct CommandState { + struct { + int argc = 0; + char** argv = nullptr; + } exec; + + struct { + bool timestamp = false; + bool noColor = !qEnvironmentVariableIsEmpty("NO_COLOR"); + bool sparse = false; + size_t verbosity = 0; + int tail = 0; + bool follow = false; + QStringOption rules; + QStringOption readoutRules; + QStringOption file; + } log; + + struct { + QStringOption path; + QStringOption manifest; + QStringOption name; + } config; + + struct { + int port = -1; + bool wait = false; + } debug; + + struct { + QStringOption id; + pid_t pid = -1; // NOLINT (include) + bool all = false; + } instance; + + struct { + bool json = false; + } output; + + struct { + bool info = false; + QStringOption target; + QStringOption function; + std::vector arguments; + } ipc; + + struct { + CLI::App* log = nullptr; + CLI::App* list = nullptr; + CLI::App* kill = nullptr; + CLI::App* msg = nullptr; + } subcommand; + + struct { + bool checkCompat = false; + bool printVersion = false; + bool killAll = false; + bool noDuplicate = false; + bool daemonize = false; + } misc; +}; + +struct LaunchArgs { + QString configPath; + int debugPort = -1; + bool waitForDebug = false; +}; + +void exitDaemon(int code); + +int parseCommand(int argc, char** argv, CommandState& state); +int runCommand(int argc, char** argv, QCoreApplication* coreApplication); + +int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication); + +} // namespace qs::launch diff --git a/src/launch/main.cpp b/src/launch/main.cpp new file mode 100644 index 00000000..3a2b5822 --- /dev/null +++ b/src/launch/main.cpp @@ -0,0 +1,116 @@ +#include "main.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/instanceinfo.hpp" +#include "../core/logging.hpp" +#include "../core/paths.hpp" +#include "build.hpp" +#include "launch_p.hpp" + +#if CRASH_REPORTER +#include "../crash/main.hpp" +#endif + +namespace qs::launch { + +void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication); + +int DAEMON_PIPE = -1; // NOLINT + +void exitDaemon(int code) { + if (DAEMON_PIPE == -1) return; + + if (write(DAEMON_PIPE, &code, sizeof(int)) == -1) { + qCritical().nospace() << "Failed to write daemon exit command with error code " << errno << ": " + << qt_error_string(); + } + + close(DAEMON_PIPE); + + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + if (open("/dev/null", O_RDONLY) != STDIN_FILENO) { // NOLINT + qFatal() << "Failed to open /dev/null on stdin"; + } + + if (open("/dev/null", O_WRONLY) != STDOUT_FILENO) { // NOLINT + qFatal() << "Failed to open /dev/null on stdout"; + } + + if (open("/dev/null", O_WRONLY) != STDERR_FILENO) { // NOLINT + qFatal() << "Failed to open /dev/null on stderr"; + } +} + +void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) { +#if CRASH_REPORTER + auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD"); + + if (!lastInfoFdStr.isEmpty()) { + auto lastInfoFd = lastInfoFdStr.toInt(); + + QFile file; + file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle); + file.seek(0); + + auto ds = QDataStream(&file); + RelaunchInfo info; + ds >> info; + + LogManager::init( + !info.noColor, + info.timestamp, + info.sparseLogsOnly, + info.defaultLogLevel, + info.logRules + ); + + qCritical().nospace() << "Quickshell has crashed under pid " + << qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt() + << " (Coredumps will be available under that pid.)"; + + qCritical() << "Further crash information is stored under" + << QsPaths::crashDir(info.instance.instanceId).path(); + + if (info.instance.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) { + qCritical() << "Quickshell crashed within 10 seconds of launching. Not restarting to avoid " + "a crash loop."; + exit(-1); // NOLINT + } else { + qCritical() << "Quickshell has been restarted."; + + launch({.configPath = info.instance.configPath}, argv, coreApplication); + } + } +#endif +} + +int main(int argc, char** argv) { + QCoreApplication::setApplicationName("quickshell"); + +#if CRASH_REPORTER + qsCheckCrash(argc, argv); +#endif + + auto qArgC = 1; + auto* coreApplication = new QCoreApplication(qArgC, argv); + + checkCrashRelaunch(argv, coreApplication); + auto code = runCommand(argc, argv, coreApplication); + + exitDaemon(code); + return code; +} + +} // namespace qs::launch diff --git a/src/launch/main.hpp b/src/launch/main.hpp new file mode 100644 index 00000000..e9d22902 --- /dev/null +++ b/src/launch/main.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace qs::launch { + +int main(int argc, char** argv); + +} diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp new file mode 100644 index 00000000..14fd9203 --- /dev/null +++ b/src/launch/parsecommand.cpp @@ -0,0 +1,196 @@ +#include +#include + +#include +#include // NOLINT: Need to include this for impls of some CLI11 classes +#include + +#include "launch_p.hpp" + +namespace qs::launch { + +int parseCommand(int argc, char** argv, CommandState& state) { + state.exec = { + .argc = argc, + .argv = argv, + }; + + auto addConfigSelection = [&](CLI::App* cmd) { + auto* group = cmd->add_option_group("Config Selection") + ->description("If no options in this group are specified,\n" + "$XDG_CONFIG_HOME/quickshell/shell.qml will be used."); + + auto* path = group->add_option("-p,--path", state.config.path) + ->description("Path to a QML file.") + ->envname("QS_CONFIG_PATH"); + + group->add_option("-m,--manifest", state.config.manifest) + ->description("Path to a quickshell manifest.\n" + "Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf") + ->envname("QS_MANIFEST") + ->excludes(path); + + group->add_option("-c,--config", state.config.name) + ->description("Name of a quickshell configuration to run.\n" + "If -m is specified, this is a configuration in the manifest,\n" + "otherwise it is the name of a folder in $XDG_CONFIG_HOME/quickshell.") + ->envname("QS_CONFIG_NAME"); + + return group; + }; + + auto addDebugOptions = [&](CLI::App* cmd) { + auto* group = cmd->add_option_group("Debugging", "Options for QML debugging."); + + auto* debug = group->add_option("--debug", state.debug.port) + ->description("Open the given port for a QML debugger connection.") + ->check(CLI::Range(0, 65535)); + + group->add_flag("--waitfordebug", state.debug.wait) + ->description("Wait for a QML debugger to connect before executing the configuration.") + ->needs(debug); + + return group; + }; + + auto addLoggingOptions = [&](CLI::App* cmd, bool noGroup, bool noDisplay = false) { + 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\n" + "for the NO_COLOR environment variable."); + + group->add_flag("--log-times", state.log.timestamp) + ->description("Log timestamps with each message."); + + group->add_option("--log-rules", state.log.rules) + ->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."); + + auto* hgroup = cmd->add_option_group(""); + hgroup->add_flag("--no-detailed-logs", state.log.sparse); + }; + + auto addInstanceSelection = [&](CLI::App* cmd) { + 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,\n" + "for example \"abc\" will select \"abcdefg\"."); + + group->add_option("--pid", state.instance.pid) + ->description("The process id of the instance to operate on."); + + return group; + }; + + auto cli = CLI::App(); + + // Require 0-1 subcommands. Without this, positionals can be parsed as more subcommands. + cli.require_subcommand(0, 1); + + addConfigSelection(&cli); + addLoggingOptions(&cli, false); + addDebugOptions(&cli); + + { + cli.add_option_group("")->add_flag("--private-check-compat", state.misc.checkCompat); + + cli.add_flag("-V,--version", state.misc.printVersion) + ->description("Print quickshell's version and exit."); + + cli.add_flag("-n,--no-duplicate", state.misc.noDuplicate) + ->description("Exit immediately if another instance of the given config is running."); + + cli.add_flag("-d,--daemonize", state.misc.daemonize) + ->description("Detach from the controlling terminal."); + } + + { + auto* sub = cli.add_subcommand("log", "Print quickshell logs."); + + auto* file = sub->add_option("file", state.log.file, "Log file to read."); + + sub->add_option("-t,--tail", state.log.tail) + ->description("Maximum number of lines to print, starting from the bottom.") + ->check(CLI::Range(1, std::numeric_limits::max(), "INT > 0")); + + sub->add_flag("-f,--follow", state.log.follow) + ->description("Keep reading the log until the logging process terminates."); + + sub->add_option("-r,--rules", state.log.readoutRules, "Log file to read.") + ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES."); + + auto* instance = addInstanceSelection(sub)->excludes(file); + addConfigSelection(sub)->excludes(instance)->excludes(file); + addLoggingOptions(sub, false); + + state.subcommand.log = sub; + } + + { + 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."); + + sub->add_flag("-j,--json", state.output.json, "Output the list as a json."); + + addConfigSelection(sub)->excludes(all); + addLoggingOptions(sub, false, true); + + state.subcommand.list = sub; + } + + { + auto* sub = cli.add_subcommand("kill", "Kill quickshell instances."); + //sub->add_flag("-a,--all", "Kill all matching instances instead of just one."); + auto* instance = addInstanceSelection(sub); + addConfigSelection(sub)->excludes(instance); + addLoggingOptions(sub, false, true); + + state.subcommand.kill = sub; + } + + { + auto* sub = cli.add_subcommand("msg", "Send messages to IpcHandlers.")->require_option(); + + auto* target = sub->add_option("target", state.ipc.target, "The target to message."); + + auto* function = sub->add_option("function", state.ipc.function) + ->description("The function to call in the target.") + ->needs(target); + + auto* arguments = sub->add_option("arguments", state.ipc.arguments) + ->description("Arguments to the called function.") + ->needs(function) + ->allow_extra_args(); + + sub->add_flag("-s,--show", state.ipc.info) + ->description("Print information about a function or target if given, or all available " + "targets if not.") + ->excludes(arguments); + + auto* instance = addInstanceSelection(sub); + addConfigSelection(sub)->excludes(instance); + addLoggingOptions(sub, false, true); + + sub->require_option(); + + state.subcommand.msg = sub; + } + + CLI11_PARSE(cli, argc, argv); + + return 0; +} + +} // namespace qs::launch diff --git a/src/main.cpp b/src/main.cpp index f18c2341..e0ce937f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,1032 +1,3 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include // NOLINT: Need to include this for impls of some CLI11 classes -#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 -#include -#include -#include -#include - -#include "build.hpp" -#include "core/common.hpp" -#include "core/instanceinfo.hpp" -#include "core/logging.hpp" -#include "core/paths.hpp" -#include "core/plugin.hpp" -#include "core/rootwrapper.hpp" -#include "io/ipccomm.hpp" -#include "ipc/ipc.hpp" - -#if CRASH_REPORTER -#include "crash/handler.hpp" -#include "crash/main.hpp" -#endif - -namespace qs::launch { - -using qs::ipc::IpcClient; - -void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication); -int runCommand(int argc, char** argv, QCoreApplication* coreApplication); - -int DAEMON_PIPE = -1; // NOLINT -void exitDaemon(int code) { - if (DAEMON_PIPE == -1) return; - - if (write(DAEMON_PIPE, &code, sizeof(int)) == -1) { - qCritical().nospace() << "Failed to write daemon exit command with error code " << errno << ": " - << qt_error_string(); - } - - close(DAEMON_PIPE); - - close(STDIN_FILENO); - close(STDOUT_FILENO); - close(STDERR_FILENO); - - if (open("/dev/null", O_RDONLY) != STDIN_FILENO) { // NOLINT - qFatal() << "Failed to open /dev/null on stdin"; - } - - if (open("/dev/null", O_WRONLY) != STDOUT_FILENO) { // NOLINT - qFatal() << "Failed to open /dev/null on stdout"; - } - - if (open("/dev/null", O_WRONLY) != STDERR_FILENO) { // NOLINT - qFatal() << "Failed to open /dev/null on stderr"; - } -} - -int main(int argc, char** argv) { - QCoreApplication::setApplicationName("quickshell"); - -#if CRASH_REPORTER - qsCheckCrash(argc, argv); -#endif - - auto qArgC = 1; - auto* coreApplication = new QCoreApplication(qArgC, argv); - - checkCrashRelaunch(argv, coreApplication); - auto code = runCommand(argc, argv, coreApplication); - - exitDaemon(code); - return code; -} - -class QStringOption { -public: - QStringOption() = default; - QStringOption& operator=(const std::string& str) { - this->str = QString::fromStdString(str); - return *this; - } - - QString& operator*() { return this->str; } - QString* operator->() { return &this->str; } - -private: - QString str; -}; - -struct CommandState { - struct { - int argc = 0; - char** argv = nullptr; - } exec; - - struct { - bool timestamp = false; - bool noColor = !qEnvironmentVariableIsEmpty("NO_COLOR"); - bool sparse = false; - size_t verbosity = 0; - int tail = 0; - bool follow = false; - QStringOption rules; - QStringOption readoutRules; - QStringOption file; - } log; - - struct { - QStringOption path; - QStringOption manifest; - QStringOption name; - } config; - - struct { - int port = -1; - bool wait = false; - } debug; - - struct { - QStringOption id; - pid_t pid = -1; // NOLINT (include) - bool all = false; - } instance; - - struct { - bool json = false; - } output; - - struct { - bool info = false; - QStringOption target; - QStringOption function; - std::vector arguments; - } ipc; - - struct { - CLI::App* log = nullptr; - CLI::App* list = nullptr; - CLI::App* kill = nullptr; - CLI::App* msg = nullptr; - } subcommand; - - struct { - bool checkCompat = false; - bool printVersion = false; - bool killAll = false; - bool noDuplicate = false; - bool daemonize = false; - } misc; -}; - -int readLogFile(CommandState& cmd); -int listInstances(CommandState& cmd); -int killInstances(CommandState& cmd); -int msgInstance(CommandState& cmd); -int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication); - -struct LaunchArgs { - QString configPath; - int debugPort = -1; - bool waitForDebug = false; -}; - -int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication); - -int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { - auto state = CommandState(); - - state.exec = { - .argc = argc, - .argv = argv, - }; - - auto addConfigSelection = [&](CLI::App* cmd) { - auto* group = cmd->add_option_group("Config Selection") - ->description("If no options in this group are specified,\n" - "$XDG_CONFIG_HOME/quickshell/shell.qml will be used."); - - auto* path = group->add_option("-p,--path", state.config.path) - ->description("Path to a QML file.") - ->envname("QS_CONFIG_PATH"); - - group->add_option("-m,--manifest", state.config.manifest) - ->description("Path to a quickshell manifest.\n" - "Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf") - ->envname("QS_MANIFEST") - ->excludes(path); - - group->add_option("-c,--config", state.config.name) - ->description("Name of a quickshell configuration to run.\n" - "If -m is specified, this is a configuration in the manifest,\n" - "otherwise it is the name of a folder in $XDG_CONFIG_HOME/quickshell.") - ->envname("QS_CONFIG_NAME"); - - return group; - }; - - auto addDebugOptions = [&](CLI::App* cmd) { - auto* group = cmd->add_option_group("Debugging", "Options for QML debugging."); - - auto* debug = group->add_option("--debug", state.debug.port) - ->description("Open the given port for a QML debugger connection.") - ->check(CLI::Range(0, 65535)); - - group->add_flag("--waitfordebug", state.debug.wait) - ->description("Wait for a QML debugger to connect before executing the configuration.") - ->needs(debug); - - return group; - }; - - auto addLoggingOptions = [&](CLI::App* cmd, bool noGroup, bool noDisplay = false) { - 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\n" - "for the NO_COLOR environment variable."); - - group->add_flag("--log-times", state.log.timestamp) - ->description("Log timestamps with each message."); - - group->add_option("--log-rules", state.log.rules) - ->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."); - - auto* hgroup = cmd->add_option_group(""); - hgroup->add_flag("--no-detailed-logs", state.log.sparse); - }; - - auto addInstanceSelection = [&](CLI::App* cmd) { - 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,\n" - "for example \"abc\" will select \"abcdefg\"."); - - group->add_option("--pid", state.instance.pid) - ->description("The process id of the instance to operate on."); - - return group; - }; - - auto cli = CLI::App(); - - // Require 0-1 subcommands. Without this, positionals can be parsed as more subcommands. - cli.require_subcommand(0, 1); - - addConfigSelection(&cli); - addLoggingOptions(&cli, false); - addDebugOptions(&cli); - - { - cli.add_option_group("")->add_flag("--private-check-compat", state.misc.checkCompat); - - cli.add_flag("-V,--version", state.misc.printVersion) - ->description("Print quickshell's version and exit."); - - cli.add_flag("-n,--no-duplicate", state.misc.noDuplicate) - ->description("Exit immediately if another instance of the given config is running."); - - cli.add_flag("-d,--daemonize", state.misc.daemonize) - ->description("Detach from the controlling terminal."); - } - - { - auto* sub = cli.add_subcommand("log", "Print quickshell logs."); - - auto* file = sub->add_option("file", state.log.file, "Log file to read."); - - sub->add_option("-t,--tail", state.log.tail) - ->description("Maximum number of lines to print, starting from the bottom.") - ->check(CLI::Range(1, std::numeric_limits::max(), "INT > 0")); - - sub->add_flag("-f,--follow", state.log.follow) - ->description("Keep reading the log until the logging process terminates."); - - sub->add_option("-r,--rules", state.log.readoutRules, "Log file to read.") - ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES."); - - auto* instance = addInstanceSelection(sub)->excludes(file); - addConfigSelection(sub)->excludes(instance)->excludes(file); - addLoggingOptions(sub, false); - - state.subcommand.log = sub; - } - - { - 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."); - - sub->add_flag("-j,--json", state.output.json, "Output the list as a json."); - - addConfigSelection(sub)->excludes(all); - addLoggingOptions(sub, false, true); - - state.subcommand.list = sub; - } - - { - auto* sub = cli.add_subcommand("kill", "Kill quickshell instances."); - //sub->add_flag("-a,--all", "Kill all matching instances instead of just one."); - auto* instance = addInstanceSelection(sub); - addConfigSelection(sub)->excludes(instance); - addLoggingOptions(sub, false, true); - - state.subcommand.kill = sub; - } - - { - auto* sub = cli.add_subcommand("msg", "Send messages to IpcHandlers.")->require_option(); - - auto* target = sub->add_option("target", state.ipc.target, "The target to message."); - - auto* function = sub->add_option("function", state.ipc.function) - ->description("The function to call in the target.") - ->needs(target); - - auto* arguments = sub->add_option("arguments", state.ipc.arguments) - ->description("Arguments to the called function.") - ->needs(function) - ->allow_extra_args(); - - sub->add_flag("-s,--show", state.ipc.info) - ->description("Print information about a function or target if given, or all available " - "targets if not.") - ->excludes(arguments); - - auto* instance = addInstanceSelection(sub); - addConfigSelection(sub)->excludes(instance); - addLoggingOptions(sub, false, true); - - sub->require_option(); - - state.subcommand.msg = sub; - } - - CLI11_PARSE(cli, argc, argv); - - if (state.misc.checkCompat) { - if (strcmp(qVersion(), QT_VERSION_STR) != 0) { - 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"; - return 1; - } - - return 0; - } - - // Has to happen before extra threads are spawned. - if (state.misc.daemonize) { - auto closepipes = std::array(); - if (pipe(closepipes.data()) == -1) { - qFatal().nospace() << "Failed to create messaging pipes for daemon with error " << errno - << ": " << qt_error_string(); - } - - DAEMON_PIPE = closepipes[1]; - - pid_t pid = fork(); // NOLINT (include) - - if (pid == -1) { - qFatal().nospace() << "Failed to fork daemon with error " << errno << ": " - << qt_error_string(); - } else if (pid == 0) { - close(closepipes[0]); - - if (setsid() == -1) { - qFatal().nospace() << "Failed to setsid with error " << errno << ": " << qt_error_string(); - } - } else { - close(closepipes[1]); - - int ret = 0; - if (read(closepipes[0], &ret, sizeof(int)) == -1) { - qFatal() << "Failed to wait for daemon launch (it may have crashed)"; - } - - return ret; - } - } - - { - auto level = state.log.verbosity == 0 ? QtWarningMsg - : state.log.verbosity == 1 ? QtInfoMsg - : QtDebugMsg; - - LogManager::init( - !state.log.noColor, - state.log.timestamp, - state.log.sparse, - level, - *state.log.rules, - *state.subcommand.log ? "READER" : "" - ); - } - - if (state.misc.printVersion) { - qCInfo(logBare).noquote().nospace() << "quickshell pre-release, revision " << GIT_REVISION - << ", distributed by: " << DISTRIBUTOR; - - if (state.log.verbosity > 1) { - qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; - qCInfo(logBare).noquote() << "Runtime Qt Version:" << qVersion(); - qCInfo(logBare).noquote() << "Compiler:" << COMPILER; - qCInfo(logBare).noquote() << "Compile Flags:" << COMPILE_FLAGS; - } - - if (state.log.verbosity > 0) { - qCInfo(logBare).noquote() << "\nBuild Type:" << BUILD_TYPE; - qCInfo(logBare).noquote() << "Build configuration:"; - qCInfo(logBare).noquote().nospace() << BUILD_CONFIGURATION; - } - } else if (*state.subcommand.log) { - return readLogFile(state); - } else if (*state.subcommand.list) { - return listInstances(state); - } else if (*state.subcommand.kill) { - return killInstances(state); - } else if (*state.subcommand.msg) { - return msgInstance(state); - } else { - if (strcmp(qVersion(), QT_VERSION_STR) != 0) { - qWarning() << "\033[31mQuickshell 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 " - "the quickshell package must be rebuilt.\n"; - } - - return launchFromCommand(state, coreApplication); - } - - return 0; -} - -int locateConfigFile(CommandState& cmd, QString& path) { - if (!cmd.config.path->isEmpty()) { - path = *cmd.config.path; - } else { - auto manifestPath = *cmd.config.manifest; - if (manifestPath.isEmpty()) { - auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); - auto path = configDir.filePath("manifest.conf"); - if (QFileInfo(path).isFile()) manifestPath = path; - } - - if (!manifestPath.isEmpty()) { - auto file = QFile(manifestPath); - if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { - auto stream = QTextStream(&file); - while (!stream.atEnd()) { - auto line = stream.readLine(); - if (line.trimmed().startsWith("#")) continue; - if (line.trimmed().isEmpty()) continue; - - auto split = line.split('='); - if (split.length() != 2) { - qCritical() << "Manifest line not in expected format 'name = relativepath':" << line; - return -1; - } - - if (split[0].trimmed() == *cmd.config.name) { - path = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed()); - break; - } - } - - if (path.isEmpty()) { - qCCritical(logBare) << "Configuration" << *cmd.config.name - << "not found when searching manifest" << manifestPath; - return -1; - } - } else { - qCCritical(logBare) << "Could not open maifest at path" << *cmd.config.manifest; - return -1; - } - } else { - auto configDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)); - - if (cmd.config.name->isEmpty()) { - path = configDir.path(); - } else { - path = configDir.filePath(*cmd.config.name); - } - } - } - - if (QFileInfo(path).isDir()) { - path = QDir(path).filePath("shell.qml"); - } - - if (!QFileInfo(path).isFile()) { - qCCritical(logBare) << "Could not open config file at" << path; - return -1; - } - - path = QFileInfo(path).canonicalFilePath(); - - return 0; -} - -void sortInstances(QVector& list) { - std::sort(list.begin(), list.end(), [](const InstanceLockInfo& a, const InstanceLockInfo& b) { - return a.instance.launchTime < b.instance.launchTime; - }); -}; - -int selectInstance(CommandState& cmd, InstanceLockInfo* instance) { - auto* basePath = QsPaths::instance()->baseRunDir(); - if (!basePath) return -1; - - QString path; - - if (cmd.instance.pid != -1) { - path = QDir(basePath->filePath("by-pid")).filePath(QString::number(cmd.instance.pid)); - if (!QsPaths::checkLock(path, instance)) { - qCInfo(logBare) << "No instance found for pid" << cmd.instance.pid; - return -1; - } - } else if (!cmd.instance.id->isEmpty()) { - path = basePath->filePath("by-pid"); - auto instances = QsPaths::collectInstances(path); - - auto itr = - std::remove_if(instances.begin(), instances.end(), [&](const InstanceLockInfo& info) { - return !info.instance.instanceId.startsWith(*cmd.instance.id); - }); - - instances.erase(itr, instances.end()); - - if (instances.isEmpty()) { - qCInfo(logBare) << "No running instances start with" << *cmd.instance.id; - return -1; - } else if (instances.length() != 1) { - qCInfo(logBare) << "More than one instance starts with" << *cmd.instance.id; - - for (auto& instance: instances) { - qCInfo(logBare).noquote() << " -" << instance.instance.instanceId; - } - - return -1; - } else { - *instance = instances.value(0); - } - } else { - QString configFilePath; - auto r = locateConfigFile(cmd, configFilePath); - if (r != 0) return r; - - auto pathId = - QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); - - path = QDir(basePath->filePath("by-path")).filePath(pathId); - - auto instances = QsPaths::collectInstances(path); - sortInstances(instances); - - if (instances.isEmpty()) { - qCInfo(logBare) << "No running instances for" << configFilePath; - return -1; - } - - *instance = instances.value(0); - } - - return 0; -} - -int readLogFile(CommandState& cmd) { - auto path = *cmd.log.file; - - if (path.isEmpty()) { - InstanceLockInfo instance; - auto r = selectInstance(cmd, &instance); - if (r != 0) return r; - - path = QDir(QsPaths::basePath(instance.instance.instanceId)).filePath("log.qslog"); - } - - auto file = QFile(path); - if (!file.open(QFile::ReadOnly)) { - qCCritical(logBare) << "Failed to open log file" << path; - return -1; - } - - return qs::log::readEncodedLogs( - &file, - path, - cmd.log.timestamp, - cmd.log.tail, - cmd.log.follow, - *cmd.log.readoutRules - ) - ? 0 - : -1; -} - -int listInstances(CommandState& cmd) { - auto* basePath = QsPaths::instance()->baseRunDir(); - if (!basePath) return -1; // NOLINT - - QString path; - QString configFilePath; - if (cmd.instance.all) { - path = basePath->filePath("by-pid"); - } else { - auto r = locateConfigFile(cmd, configFilePath); - - if (r != 0) { - qCInfo(logBare) << "Use --all to list all instances."; - return r; - } - - auto pathId = - QCryptographicHash::hash(configFilePath.toUtf8(), QCryptographicHash::Md5).toHex(); - - path = QDir(basePath->filePath("by-path")).filePath(pathId); - } - - auto instances = QsPaths::collectInstances(path); - - if (instances.isEmpty()) { - if (cmd.instance.all) { - qCInfo(logBare) << "No running instances."; - } else { - qCInfo(logBare) << "No running instances for" << configFilePath; - qCInfo(logBare) << "Use --all to list all instances."; - } - } else { - sortInstances(instances); - - if (cmd.output.json) { - auto array = QJsonArray(); - - for (auto& instance: instances) { - auto json = QJsonObject(); - - json["id"] = instance.instance.instanceId; - json["pid"] = instance.pid; - json["shell_id"] = instance.instance.shellId; - json["config_path"] = instance.instance.configPath; - json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate); - - array.push_back(json); - } - - auto document = QJsonDocument(array); - QTextStream(stdout) << document.toJson(QJsonDocument::Indented); - } else { - for (auto& instance: instances) { - auto launchTimeStr = instance.instance.launchTime.toString("yyyy-MM-dd hh:mm:ss"); - - auto runSeconds = instance.instance.launchTime.secsTo(QDateTime::currentDateTime()); - auto remSeconds = runSeconds % 60; - auto runMinutes = (runSeconds - remSeconds) / 60; - auto remMinutes = runMinutes % 60; - auto runHours = (runMinutes - remMinutes) / 60; - auto runtimeStr = QString("%1 hours, %2 minutes, %3 seconds") - .arg(runHours) - .arg(remMinutes) - .arg(remSeconds); - - qCInfo(logBare).noquote().nospace() - << "Instance " << instance.instance.instanceId << ":\n" - << " Process ID: " << instance.pid << '\n' - << " Shell ID: " << instance.instance.shellId << '\n' - << " Config path: " << instance.instance.configPath << '\n' - << " Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n"; - } - } - } - - return 0; -} - -int killInstances(CommandState& cmd) { - InstanceLockInfo instance; - auto r = selectInstance(cmd, &instance); - if (r != 0) return r; - - return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { - client.kill(); - qCInfo(logBare).noquote() << "Killed" << instance.instance.instanceId; - }); -} - -int msgInstance(CommandState& cmd) { - InstanceLockInfo instance; - auto r = selectInstance(cmd, &instance); - if (r != 0) return r; - - return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { - if (cmd.ipc.info) { - return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.function); - } else { - QVector arguments; - for (auto& arg: cmd.ipc.arguments) { - arguments += *arg; - } - - return qs::io::ipc::comm::callFunction( - &client, - *cmd.ipc.target, - *cmd.ipc.function, - arguments - ); - } - - return -1; - }); -} - -template -QString base36Encode(T number) { - const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz"; - QString result; - - do { - result.prepend(digits[number % 36]); - number /= 36; - } while (number > 0); - - for (auto i = 0; i < result.length() / 2; i++) { - auto opposite = result.length() - i - 1; - auto c = result.at(i); - result[i] = result.at(opposite); - result[opposite] = c; - } - - return result; -} - -void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) { -#if CRASH_REPORTER - auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD"); - - if (!lastInfoFdStr.isEmpty()) { - auto lastInfoFd = lastInfoFdStr.toInt(); - - QFile file; - file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle); - file.seek(0); - - auto ds = QDataStream(&file); - RelaunchInfo info; - ds >> info; - - LogManager::init( - !info.noColor, - info.timestamp, - info.sparseLogsOnly, - info.defaultLogLevel, - info.logRules - ); - - qCritical().nospace() << "Quickshell has crashed under pid " - << qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt() - << " (Coredumps will be available under that pid.)"; - - qCritical() << "Further crash information is stored under" - << QsPaths::crashDir(info.instance.instanceId).path(); - - if (info.instance.launchTime.msecsTo(QDateTime::currentDateTime()) < 10000) { - qCritical() << "Quickshell crashed within 10 seconds of launching. Not restarting to avoid " - "a crash loop."; - exit(-1); // NOLINT - } else { - qCritical() << "Quickshell has been restarted."; - - launch({.configPath = info.instance.configPath}, argv, coreApplication); - } - } -#endif -} - -int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication) { - QString configPath; - - auto r = locateConfigFile(cmd, configPath); - if (r != 0) return r; - - { - InstanceLockInfo info; - if (cmd.misc.noDuplicate && selectInstance(cmd, &info) == 0) { - qCInfo(logBare) << "An instance of this configuration is already running."; - return 0; - } - } - - return launch( - { - .configPath = configPath, - .debugPort = cmd.debug.port, - .waitForDebug = cmd.debug.wait, - }, - cmd.exec.argv, - coreApplication - ); -} - -int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication) { - auto pathId = QCryptographicHash::hash(args.configPath.toUtf8(), QCryptographicHash::Md5).toHex(); - auto shellId = QString(pathId); - - qInfo() << "Launching config:" << args.configPath; - - auto file = QFile(args.configPath); - if (!file.open(QFile::ReadOnly | QFile::Text)) { - qCritical() << "Could not open config file" << args.configPath; - return -1; - } - - struct { - bool useQApplication = false; - bool nativeTextRendering = false; - bool desktopSettingsAware = true; - QString iconTheme = qEnvironmentVariable("QS_ICON_THEME"); - QHash envOverrides; - } pragmas; - - auto stream = QTextStream(&file); - while (!stream.atEnd()) { - auto line = stream.readLine().trimmed(); - if (line.startsWith("//@ pragma ")) { - auto pragma = line.sliced(11).trimmed(); - - if (pragma == "UseQApplication") pragmas.useQApplication = true; - else if (pragma == "NativeTextRendering") pragmas.nativeTextRendering = true; - else if (pragma == "IgnoreSystemSettings") pragmas.desktopSettingsAware = false; - else if (pragma.startsWith("IconTheme ")) pragmas.iconTheme = pragma.sliced(10); - else if (pragma.startsWith("Env ")) { - auto envPragma = pragma.sliced(4); - auto splitIdx = envPragma.indexOf('='); - - if (splitIdx == -1) { - qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'"; - return -1; - } - - auto var = envPragma.sliced(0, splitIdx).trimmed(); - auto val = envPragma.sliced(splitIdx + 1).trimmed(); - pragmas.envOverrides.insert(var, val); - } else if (pragma.startsWith("ShellId ")) { - shellId = pragma.sliced(8).trimmed(); - } else { - qCritical() << "Unrecognized pragma" << pragma; - return -1; - } - } else if (line.startsWith("import")) break; - } - - file.close(); - - if (!pragmas.iconTheme.isEmpty()) { - QIcon::setThemeName(pragmas.iconTheme); - } - - qInfo() << "Shell ID:" << shellId << "Path ID" << pathId; - - auto launchTime = qs::Common::LAUNCH_TIME.toSecsSinceEpoch(); - InstanceInfo::CURRENT = InstanceInfo { - .instanceId = base36Encode(getpid()) + base36Encode(launchTime), - .configPath = args.configPath, - .shellId = shellId, - .launchTime = qs::Common::LAUNCH_TIME, - }; - -#if CRASH_REPORTER - auto crashHandler = crash::CrashHandler(); - crashHandler.init(); - - { - auto* log = LogManager::instance(); - crashHandler.setRelaunchInfo({ - .instance = InstanceInfo::CURRENT, - .noColor = !log->colorLogs, - .timestamp = log->timestampLogs, - .sparseLogsOnly = log->isSparse(), - .defaultLogLevel = log->defaultLevel(), - .logRules = log->rulesString(), - }); - } -#endif - - QsPaths::init(shellId, pathId); - QsPaths::instance()->linkRunDir(); - QsPaths::instance()->linkPathDir(); - LogManager::initFs(); - - for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) { - qputenv(var.toUtf8(), val.toUtf8()); - } - - // The qml engine currently refuses to cache non file (qsintercept) paths. - - // if (auto* cacheDir = QsPaths::instance()->cacheDir()) { - // auto qmlCacheDir = cacheDir->filePath("qml-engine-cache"); - // qputenv("QML_DISK_CACHE_PATH", qmlCacheDir.toLocal8Bit()); - // - // if (!qEnvironmentVariableIsSet("QML_DISK_CACHE")) { - // qputenv("QML_DISK_CACHE", "aot,qmlc"); - // } - // } - - // While the simple animation driver can lead to better animations in some cases, - // it also can cause excessive repainting at excessively high framerates which can - // lead to noticeable amounts of gpu usage, including overheating on some systems. - // This gets worse the more windows are open, as repaints trigger on all of them for - // some reason. See QTBUG-126099 for details. - - // if (!qEnvironmentVariableIsSet("QSG_USE_SIMPLE_ANIMATION_DRIVER")) { - // qputenv("QSG_USE_SIMPLE_ANIMATION_DRIVER", "1"); - // } - - // Some programs place icons in the pixmaps folder instead of the icons folder. - // This seems to be controlled by the QPA and qt6ct does not provide it. - { - QList dataPaths; - - 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"); - } - - auto fallbackPaths = QIcon::fallbackSearchPaths(); - - for (auto& path: dataPaths) { - auto newPath = QDir(path).filePath("pixmaps"); - - if (!fallbackPaths.contains(newPath)) { - fallbackPaths.push_back(newPath); - } - } - - QIcon::setFallbackSearchPaths(fallbackPaths); - } - - QGuiApplication::setDesktopSettingsAware(pragmas.desktopSettingsAware); - - delete coreApplication; - - QGuiApplication* app = nullptr; - auto qArgC = 0; - - if (pragmas.useQApplication) { - app = new QApplication(qArgC, argv); - } else { - app = new QGuiApplication(qArgC, argv); - } - - if (args.debugPort != -1) { - QQmlDebuggingEnabler::enableDebugging(true); - auto wait = args.waitForDebug ? QQmlDebuggingEnabler::WaitForClient - : QQmlDebuggingEnabler::DoNotWaitForClient; - QQmlDebuggingEnabler::startTcpDebugServer(args.debugPort, wait); - } - - QuickshellPlugin::initPlugins(); - - // Base window transparency appears to be additive. - // Use a fully transparent window with a colored rect. - QQuickWindow::setDefaultAlphaBuffer(true); - - if (pragmas.nativeTextRendering) { - QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); - } - - qs::ipc::IpcServer::start(); - QsPaths::instance()->createLock(); - - auto root = RootWrapper(args.configPath, shellId); - QGuiApplication::setQuitOnLastWindowClosed(false); - - exitDaemon(0); - - auto code = QGuiApplication::exec(); - delete app; - return code; -} - -} // namespace qs::launch +#include "launch/main.hpp" int main(int argc, char** argv) { return qs::launch::main(argc, argv); } diff --git a/src/services/greetd/CMakeLists.txt b/src/services/greetd/CMakeLists.txt index 870f8085..2252f8cf 100644 --- a/src/services/greetd/CMakeLists.txt +++ b/src/services/greetd/CMakeLists.txt @@ -11,9 +11,9 @@ qt_add_qml_module(quickshell-service-greetd install_qml_module(quickshell-service-greetd) -target_link_libraries(quickshell-service-greetd PRIVATE ${QT_DEPS}) +# can't be Qt::Qml because generation.hpp pulls in gui types +target_link_libraries(quickshell-service-greetd PRIVATE Qt::Quick) -qs_pch(quickshell-service-greetd) -qs_pch(quickshell-service-greetdplugin) +qs_module_pch(quickshell-service-greetd) target_link_libraries(quickshell PRIVATE quickshell-service-greetdplugin) diff --git a/src/services/mpris/CMakeLists.txt b/src/services/mpris/CMakeLists.txt index 505df7a6..122a0c5c 100644 --- a/src/services/mpris/CMakeLists.txt +++ b/src/services/mpris/CMakeLists.txt @@ -30,13 +30,15 @@ target_include_directories(quickshell-service-mpris PRIVATE ${CMAKE_CURRENT_BINA qt_add_qml_module(quickshell-service-mpris URI Quickshell.Services.Mpris VERSION 0.1 - DEPENDENCIES QtQml Quickshell + DEPENDENCIES QtQml ) +qs_add_module_deps_light(quickshell-service-mpris Quickshell) + install_qml_module(quickshell-service-mpris) -target_link_libraries(quickshell-service-mpris PRIVATE ${QT_DEPS} quickshell-dbus) -target_link_libraries(quickshell PRIVATE quickshell-service-mprisplugin) +target_link_libraries(quickshell-service-mpris PRIVATE Qt::Qml Qt::DBus) -qs_pch(quickshell-service-mpris) -qs_pch(quickshell-service-mprisplugin) +qs_module_pch(quickshell-service-mpris SET dbus) + +target_link_libraries(quickshell PRIVATE quickshell-service-mprisplugin) diff --git a/src/services/notifications/CMakeLists.txt b/src/services/notifications/CMakeLists.txt index 4ba8d3cc..0cbb42eb 100644 --- a/src/services/notifications/CMakeLists.txt +++ b/src/services/notifications/CMakeLists.txt @@ -20,13 +20,13 @@ target_include_directories(quickshell-service-notifications PRIVATE ${CMAKE_CURR qt_add_qml_module(quickshell-service-notifications URI Quickshell.Services.Notifications VERSION 0.1 - DEPENDENCIES QtQml Quickshell ) +qs_add_module_deps_light(quickshell-service-notifications Quickshell) + install_qml_module(quickshell-service-notifications) -target_link_libraries(quickshell-service-notifications PRIVATE ${QT_DEPS} quickshell-dbus) +target_link_libraries(quickshell-service-notifications PRIVATE Qt::Quick Qt::DBus) target_link_libraries(quickshell PRIVATE quickshell-service-notificationsplugin) -qs_pch(quickshell-service-notifications) -qs_pch(quickshell-service-notificationsplugin) +qs_module_pch(quickshell-service-notifications SET dbus) diff --git a/src/services/pam/CMakeLists.txt b/src/services/pam/CMakeLists.txt index f9d017e7..c35e74af 100644 --- a/src/services/pam/CMakeLists.txt +++ b/src/services/pam/CMakeLists.txt @@ -13,9 +13,8 @@ qt_add_qml_module(quickshell-service-pam install_qml_module(quickshell-service-pam) -target_link_libraries(quickshell-service-pam PRIVATE ${QT_DEPS} pam ${PAM_LIBRARIES}) +target_link_libraries(quickshell-service-pam PRIVATE Qt::Qml pam ${PAM_LIBRARIES}) -qs_pch(quickshell-service-pam) -qs_pch(quickshell-service-pamplugin) +qs_module_pch(quickshell-service-pam) target_link_libraries(quickshell PRIVATE quickshell-service-pamplugin) diff --git a/src/services/pipewire/CMakeLists.txt b/src/services/pipewire/CMakeLists.txt index bb74a078..35aaa137 100644 --- a/src/services/pipewire/CMakeLists.txt +++ b/src/services/pipewire/CMakeLists.txt @@ -16,14 +16,17 @@ qt_add_library(quickshell-service-pipewire STATIC qt_add_qml_module(quickshell-service-pipewire URI Quickshell.Services.Pipewire VERSION 0.1 - DEPENDENCIES QtQml Quickshell + DEPENDENCIES QtQml ) +qs_add_module_deps_light(quickshell-service-pipewire Quickshell) + install_qml_module(quickshell-service-pipewire) -target_link_libraries(quickshell-service-pipewire PRIVATE ${QT_DEPS} PkgConfig::pipewire) +target_link_libraries(quickshell-service-pipewire PRIVATE + Qt::Qml PkgConfig::pipewire +) -qs_pch(quickshell-service-pipewire) -qs_pch(quickshell-service-pipewireplugin) +qs_module_pch(quickshell-service-pipewire) target_link_libraries(quickshell PRIVATE quickshell-service-pipewireplugin) diff --git a/src/services/status_notifier/CMakeLists.txt b/src/services/status_notifier/CMakeLists.txt index 20de11a1..7e0bf2b9 100644 --- a/src/services/status_notifier/CMakeLists.txt +++ b/src/services/status_notifier/CMakeLists.txt @@ -41,13 +41,14 @@ target_include_directories(quickshell-service-statusnotifier PRIVATE ${CMAKE_CUR qt_add_qml_module(quickshell-service-statusnotifier URI Quickshell.Services.SystemTray VERSION 0.1 - DEPENDENCIES QtQml Quickshell Quickshell.DBusMenu + DEPENDENCIES QtQml ) +qs_add_module_deps_light(quickshell-service-statusnotifier Quickshell Quickshell.DBusMenu) + install_qml_module(quickshell-service-statusnotifier) -target_link_libraries(quickshell-service-statusnotifier PRIVATE ${QT_DEPS} quickshell-dbus quickshell-dbusmenuplugin) +target_link_libraries(quickshell-service-statusnotifier PRIVATE Qt::Quick Qt::DBus) target_link_libraries(quickshell PRIVATE quickshell-service-statusnotifierplugin) -qs_pch(quickshell-service-statusnotifier) -qs_pch(quickshell-service-statusnotifierplugin) +qs_module_pch(quickshell-service-statusnotifier SET dbus) diff --git a/src/services/upower/CMakeLists.txt b/src/services/upower/CMakeLists.txt index e913a550..fd0da2af 100644 --- a/src/services/upower/CMakeLists.txt +++ b/src/services/upower/CMakeLists.txt @@ -30,13 +30,14 @@ target_include_directories(quickshell-service-upower PRIVATE ${CMAKE_CURRENT_BIN qt_add_qml_module(quickshell-service-upower URI Quickshell.Services.UPower VERSION 0.1 - DEPENDENCIES QtQml Quickshell + DEPENDENCIES QtQml ) +qs_add_module_deps_light(quickshell-service-upower Quickshell) + install_qml_module(quickshell-service-upower) -target_link_libraries(quickshell-service-upower PRIVATE ${QT_DEPS} quickshell-dbus) +target_link_libraries(quickshell-service-upower PRIVATE Qt::Qml Qt::DBus quickshell-dbus) target_link_libraries(quickshell PRIVATE quickshell-service-upowerplugin) -qs_pch(quickshell-service-upower) -qs_pch(quickshell-service-upowerplugin) +qs_module_pch(quickshell-service-upower SET dbus) diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 19e74b90..8005a833 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -20,6 +20,14 @@ execute_process( message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}") +qs_add_pchset(wayland-protocol + DEPENDENCIES Qt::Core Qt::WaylandClient Qt::WaylandClientPrivate + HEADERS + + + +) + function (wl_proto target name path) set(PROTO_BUILD_PATH ${CMAKE_CURRENT_BINARY_DIR}/wl-proto/${name}) make_directory(${PROTO_BUILD_PATH}) @@ -53,13 +61,12 @@ function (wl_proto target name path) DEPENDS Qt6::qtwaylandscanner "${path}" ) - add_library(wl-proto-${name} - ${WS_CLIENT_HEADER} ${WS_CLIENT_CODE} - ${QWS_CLIENT_HEADER} ${QWS_CLIENT_CODE} - ) + add_library(wl-proto-${name}-wl STATIC ${WS_CLIENT_HEADER} ${WS_CLIENT_CODE}) + add_library(wl-proto-${name} STATIC ${QWS_CLIENT_HEADER} ${QWS_CLIENT_CODE}) target_include_directories(wl-proto-${name} INTERFACE ${PROTO_BUILD_PATH}) - target_link_libraries(wl-proto-${name} Qt6::WaylandClient Qt6::WaylandClientPrivate) + target_link_libraries(wl-proto-${name} wl-proto-${name}-wl Qt6::WaylandClient Qt6::WaylandClientPrivate) + qs_pch(wl-proto-${name} SET wayland-protocol) target_link_libraries(${target} PRIVATE wl-proto-${name}) endfunction() @@ -100,20 +107,24 @@ if (HYPRLAND) add_subdirectory(hyprland) endif() -target_link_libraries(quickshell-wayland PRIVATE ${QT_DEPS}) -target_link_libraries(quickshell-wayland-init PRIVATE ${QT_DEPS}) +# widgets for qmenu +target_link_libraries(quickshell-wayland PRIVATE + Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate +) + +target_link_libraries(quickshell-wayland-init PRIVATE Qt::Quick) qt_add_qml_module(quickshell-wayland URI Quickshell.Wayland VERSION 0.1 - DEPENDENCIES QtQuick Quickshell + DEPENDENCIES QtQuick IMPORTS ${WAYLAND_MODULES} ) +qs_add_module_deps_light(quickshell-wayland Quickshell) + install_qml_module(quickshell-wayland) -qs_pch(quickshell-wayland) -qs_pch(quickshell-waylandplugin) -qs_pch(quickshell-wayland-init) +qs_module_pch(quickshell-wayland SET large) target_link_libraries(quickshell PRIVATE quickshell-waylandplugin quickshell-wayland-init) diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt index 59458fe6..cb375358 100644 --- a/src/wayland/hyprland/CMakeLists.txt +++ b/src/wayland/hyprland/CMakeLists.txt @@ -27,7 +27,6 @@ qt_add_qml_module(quickshell-hyprland install_qml_module(quickshell-hyprland) -qs_pch(quickshell-hyprland) -qs_pch(quickshell-hyprlandplugin) +# intentionally no pch as the module is empty target_link_libraries(quickshell PRIVATE quickshell-hyprlandplugin) diff --git a/src/wayland/hyprland/focus_grab/CMakeLists.txt b/src/wayland/hyprland/focus_grab/CMakeLists.txt index 0fd1f85e..04b6e0a9 100644 --- a/src/wayland/hyprland/focus_grab/CMakeLists.txt +++ b/src/wayland/hyprland/focus_grab/CMakeLists.txt @@ -7,9 +7,11 @@ qt_add_library(quickshell-hyprland-focus-grab STATIC qt_add_qml_module(quickshell-hyprland-focus-grab URI Quickshell.Hyprland._FocusGrab VERSION 0.1 - DEPENDENCIES QtQml Quickshell + DEPENDENCIES QtQml ) +qs_add_module_deps_light(quickshell-hyprland-focus-grab Quickshell) + install_qml_module(quickshell-hyprland-focus-grab) wl_proto(quickshell-hyprland-focus-grab @@ -17,9 +19,10 @@ wl_proto(quickshell-hyprland-focus-grab "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml" ) -target_link_libraries(quickshell-hyprland-focus-grab PRIVATE ${QT_DEPS} wayland-client) +target_link_libraries(quickshell-hyprland-focus-grab PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client +) -qs_pch(quickshell-hyprland-focus-grab) -qs_pch(quickshell-hyprland-focus-grabplugin) +qs_module_pch(quickshell-hyprland-focus-grab SET large) target_link_libraries(quickshell PRIVATE quickshell-hyprland-focus-grabplugin) diff --git a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt index d2314177..8b2aa94f 100644 --- a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt +++ b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt @@ -17,9 +17,10 @@ wl_proto(quickshell-hyprland-global-shortcuts "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-global-shortcuts-v1.xml" ) -target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE ${QT_DEPS} wayland-client) +target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE + Qt::Qml Qt::WaylandClient Qt::WaylandClientPrivate wayland-client +) -qs_pch(quickshell-hyprland-global-shortcuts) -qs_pch(quickshell-hyprland-global-shortcutsplugin) +qs_module_pch(quickshell-hyprland-global-shortcuts) target_link_libraries(quickshell PRIVATE quickshell-hyprland-global-shortcutsplugin) diff --git a/src/wayland/hyprland/ipc/CMakeLists.txt b/src/wayland/hyprland/ipc/CMakeLists.txt index 367fa8f4..fd1da674 100644 --- a/src/wayland/hyprland/ipc/CMakeLists.txt +++ b/src/wayland/hyprland/ipc/CMakeLists.txt @@ -8,14 +8,15 @@ qt_add_library(quickshell-hyprland-ipc STATIC qt_add_qml_module(quickshell-hyprland-ipc URI Quickshell.Hyprland._Ipc VERSION 0.1 - DEPENDENCIES QtQml Quickshell + DEPENDENCIES QtQuick ) +qs_add_module_deps_light(quickshell-hyprland-ipc Quickshell) + install_qml_module(quickshell-hyprland-ipc) -target_link_libraries(quickshell-hyprland-ipc PRIVATE ${QT_DEPS}) +target_link_libraries(quickshell-hyprland-ipc PRIVATE Qt::Quick) -qs_pch(quickshell-hyprland-ipc) -qs_pch(quickshell-hyprland-ipcplugin) +qs_module_pch(quickshell-hyprland-ipc SET large) target_link_libraries(quickshell PRIVATE quickshell-hyprland-ipcplugin) diff --git a/src/wayland/platformmenu.cpp b/src/wayland/platformmenu.cpp index 80f9854e..e64e8880 100644 --- a/src/wayland/platformmenu.cpp +++ b/src/wayland/platformmenu.cpp @@ -8,6 +8,7 @@ #include #include "../core/platformmenu.hpp" +#include "../core/platformmenu_p.hpp" using namespace qs::menu::platform; diff --git a/src/wayland/session_lock/CMakeLists.txt b/src/wayland/session_lock/CMakeLists.txt index d6224a8b..63dc1295 100644 --- a/src/wayland/session_lock/CMakeLists.txt +++ b/src/wayland/session_lock/CMakeLists.txt @@ -8,6 +8,7 @@ qt_add_library(quickshell-wayland-sessionlock STATIC wl_proto(quickshell-wayland-sessionlock ext-session-lock-v1 "${WAYLAND_PROTOCOLS}/staging/ext-session-lock/ext-session-lock-v1.xml") target_link_libraries(quickshell-wayland-sessionlock PRIVATE ${QT_DEPS} wayland-client) -qs_pch(quickshell-wayland-sessionlock) + +qs_pch(quickshell-wayland-sessionlock SET large) target_link_libraries(quickshell-wayland PRIVATE quickshell-wayland-sessionlock) diff --git a/src/wayland/toplevel_management/CMakeLists.txt b/src/wayland/toplevel_management/CMakeLists.txt index 01c9d756..0db82aae 100644 --- a/src/wayland/toplevel_management/CMakeLists.txt +++ b/src/wayland/toplevel_management/CMakeLists.txt @@ -7,7 +7,11 @@ qt_add_library(quickshell-wayland-toplevel-management STATIC qt_add_qml_module(quickshell-wayland-toplevel-management URI Quickshell.Wayland._ToplevelManagement VERSION 0.1 - DEPENDENCIES QtQml Quickshell Quickshell.Wayland + DEPENDENCIES QtQml +) + +qs_add_module_deps_light(quickshell-wayland-toplevel-management + Quickshell Quickshell.Wayland ) install_qml_module(quickshell-wayland-toplevel-management) @@ -17,9 +21,10 @@ wl_proto(quickshell-wayland-toplevel-management "${CMAKE_CURRENT_SOURCE_DIR}/wlr-foreign-toplevel-management-unstable-v1.xml" ) -target_link_libraries(quickshell-wayland-toplevel-management PRIVATE ${QT_DEPS} wayland-client) +target_link_libraries(quickshell-wayland-toplevel-management PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client +) -qs_pch(quickshell-wayland-toplevel-management) -qs_pch(quickshell-wayland-toplevel-managementplugin) +qs_module_pch(quickshell-wayland-toplevel-management SET large) target_link_libraries(quickshell PRIVATE quickshell-wayland-toplevel-managementplugin) diff --git a/src/wayland/wlr_layershell/CMakeLists.txt b/src/wayland/wlr_layershell/CMakeLists.txt index 640b7ec2..11bedc6a 100644 --- a/src/wayland/wlr_layershell/CMakeLists.txt +++ b/src/wayland/wlr_layershell/CMakeLists.txt @@ -7,17 +7,19 @@ qt_add_library(quickshell-wayland-layershell STATIC qt_add_qml_module(quickshell-wayland-layershell URI Quickshell.Wayland._WlrLayerShell VERSION 0.1 - # Quickshell.Wayland currently creates a dependency cycle, add it here once the main - # ls class is moved to this module. - DEPENDENCIES QtQuick Quickshell + DEPENDENCIES QtQuick ) +qs_add_module_deps_light(quickshell-wayland-layershell Quickshell Quickshell.Wayland) + install_qml_module(quickshell-wayland-layershell) wl_proto(quickshell-wayland-layershell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/wlr-layer-shell-unstable-v1.xml") -target_link_libraries(quickshell-wayland-layershell PRIVATE ${QT_DEPS} wayland-client) -qs_pch(quickshell-wayland-layershell) -qs_pch(quickshell-wayland-layershellplugin) +target_link_libraries(quickshell-wayland-layershell PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client +) + +qs_module_pch(quickshell-wayland-layershell SET large) target_link_libraries(quickshell-wayland PRIVATE quickshell-wayland-layershellplugin) diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index 06671b13..226d950d 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -9,7 +9,6 @@ qt_add_qml_module(quickshell-widgets install_qml_module(quickshell-widgets) -qs_pch(quickshell-widgets) -qs_pch(quickshell-widgetsplugin) +qs_module_pch(quickshell-widgets) target_link_libraries(quickshell PRIVATE quickshell-widgetsplugin) diff --git a/src/window/CMakeLists.txt b/src/window/CMakeLists.txt index e7dd1977..89b2233e 100644 --- a/src/window/CMakeLists.txt +++ b/src/window/CMakeLists.txt @@ -9,20 +9,22 @@ qt_add_library(quickshell-window STATIC qt_add_qml_module(quickshell-window URI Quickshell._Window VERSION 0.1 - DEPENDENCIES QtQuick Quickshell + DEPENDENCIES QtQuick ) +qs_add_module_deps_light(quickshell-window Quickshell) + install_qml_module(quickshell-window) add_library(quickshell-window-init OBJECT init.cpp) -target_link_libraries(quickshell-window PRIVATE ${QT_DEPS} Qt6::QuickPrivate) -target_link_libraries(quickshell-windowplugin PRIVATE ${QT_DEPS}) -target_link_libraries(quickshell-window-init PRIVATE ${QT_DEPS}) +target_link_libraries(quickshell-window PRIVATE + Qt::Core Qt::Gui Qt::Quick Qt6::QuickPrivate +) -qs_pch(quickshell-window) -qs_pch(quickshell-windowplugin) -qs_pch(quickshell-window-init) +target_link_libraries(quickshell-window-init PRIVATE Qt::Qml) + +qs_module_pch(quickshell-window SET large) target_link_libraries(quickshell PRIVATE quickshell-windowplugin quickshell-window-init) diff --git a/src/window/init.cpp b/src/window/init.cpp index ef2b8c1d..9930b41b 100644 --- a/src/window/init.cpp +++ b/src/window/init.cpp @@ -1,3 +1,6 @@ +#include +#include + #include "../core/plugin.hpp" namespace { diff --git a/src/window/test/CMakeLists.txt b/src/window/test/CMakeLists.txt index ad9e5a0a..4197e4a5 100644 --- a/src/window/test/CMakeLists.txt +++ b/src/window/test/CMakeLists.txt @@ -1,6 +1,6 @@ function (qs_test name) add_executable(${name} ${ARGN}) - target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test quickshell-window quickshell-core) + target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-window quickshell-core) add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) endfunction() diff --git a/src/x11/CMakeLists.txt b/src/x11/CMakeLists.txt index d1079d29..b37b8fbc 100644 --- a/src/x11/CMakeLists.txt +++ b/src/x11/CMakeLists.txt @@ -8,17 +8,16 @@ qt_add_library(quickshell-x11 STATIC qt_add_qml_module(quickshell-x11 URI Quickshell.X11 VERSION 0.1 + DEPENDENCIES QtQuick ) install_qml_module(quickshell-x11) add_library(quickshell-x11-init OBJECT init.cpp) -target_link_libraries(quickshell-x11 PRIVATE ${QT_DEPS} ${XCB_LIBRARIES}) -target_link_libraries(quickshell-x11-init PRIVATE ${QT_DEPS} ${XCB_LIBRARIES}) +target_link_libraries(quickshell-x11 PRIVATE Qt::Quick ${XCB_LIBRARIES}) +target_link_libraries(quickshell-x11-init PRIVATE Qt::Quick Qt::Qml ${XCB_LIBRARIES}) -qs_pch(quickshell-x11) -qs_pch(quickshell-x11plugin) -qs_pch(quickshell-x11-init) +qs_module_pch(quickshell-x11 SET large) target_link_libraries(quickshell PRIVATE quickshell-x11plugin quickshell-x11-init) diff --git a/src/x11/init.cpp b/src/x11/init.cpp index 00080036..2e41e761 100644 --- a/src/x11/init.cpp +++ b/src/x11/init.cpp @@ -1,5 +1,7 @@ #include +#include #include +#include #include "../core/plugin.hpp" #include "panel_window.hpp" @@ -8,6 +10,8 @@ namespace { class X11Plugin: public QuickshellPlugin { + QList dependencies() override { return {"window"}; } + bool applies() override { return QGuiApplication::platformName() == "xcb"; } void init() override { XAtom::initAtoms(); } From 92252c36a3b7fc249580668634f08227085b216c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 5 Nov 2024 12:14:45 -0800 Subject: [PATCH 196/305] build: fix gcc --- src/crash/CMakeLists.txt | 5 +++-- src/services/pam/CMakeLists.txt | 5 ++++- src/services/pipewire/CMakeLists.txt | 1 + src/wayland/hyprland/global_shortcuts/CMakeLists.txt | 1 + src/wayland/session_lock/CMakeLists.txt | 5 ++++- 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/crash/CMakeLists.txt b/src/crash/CMakeLists.txt index 522b5b02..442859af 100644 --- a/src/crash/CMakeLists.txt +++ b/src/crash/CMakeLists.txt @@ -4,13 +4,14 @@ qt_add_library(quickshell-crash STATIC handler.cpp ) -qs_pch(quickshell-crash) +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) -target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt6::Widgets) +# quick linked for pch compat +target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt::Quick Qt::Widgets) target_link_libraries(quickshell-core PRIVATE quickshell-crash) diff --git a/src/services/pam/CMakeLists.txt b/src/services/pam/CMakeLists.txt index c35e74af..ec300ee6 100644 --- a/src/services/pam/CMakeLists.txt +++ b/src/services/pam/CMakeLists.txt @@ -13,7 +13,10 @@ qt_add_qml_module(quickshell-service-pam install_qml_module(quickshell-service-pam) -target_link_libraries(quickshell-service-pam PRIVATE Qt::Qml pam ${PAM_LIBRARIES}) +target_link_libraries(quickshell-service-pam PRIVATE + Qt::Qml pam ${PAM_LIBRARIES} + Qt::Quick # pch +) qs_module_pch(quickshell-service-pam) diff --git a/src/services/pipewire/CMakeLists.txt b/src/services/pipewire/CMakeLists.txt index 35aaa137..fddca6f5 100644 --- a/src/services/pipewire/CMakeLists.txt +++ b/src/services/pipewire/CMakeLists.txt @@ -25,6 +25,7 @@ install_qml_module(quickshell-service-pipewire) target_link_libraries(quickshell-service-pipewire PRIVATE Qt::Qml PkgConfig::pipewire + Qt::Quick # pch ) qs_module_pch(quickshell-service-pipewire) diff --git a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt index 8b2aa94f..986f2d8e 100644 --- a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt +++ b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt @@ -19,6 +19,7 @@ wl_proto(quickshell-hyprland-global-shortcuts target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE Qt::Qml Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + Qt::Quick # pch ) qs_module_pch(quickshell-hyprland-global-shortcuts) diff --git a/src/wayland/session_lock/CMakeLists.txt b/src/wayland/session_lock/CMakeLists.txt index 63dc1295..245d1f25 100644 --- a/src/wayland/session_lock/CMakeLists.txt +++ b/src/wayland/session_lock/CMakeLists.txt @@ -7,7 +7,10 @@ qt_add_library(quickshell-wayland-sessionlock STATIC ) wl_proto(quickshell-wayland-sessionlock ext-session-lock-v1 "${WAYLAND_PROTOCOLS}/staging/ext-session-lock/ext-session-lock-v1.xml") -target_link_libraries(quickshell-wayland-sessionlock PRIVATE ${QT_DEPS} wayland-client) + +target_link_libraries(quickshell-wayland-sessionlock PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client +) qs_pch(quickshell-wayland-sessionlock SET large) From b528be94260b572919ff47d2f5e3150ebc1ee3e9 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 5 Nov 2024 13:31:24 -0800 Subject: [PATCH 197/305] all: fix gcc warnings --- CMakeLists.txt | 3 +++ src/core/logging.cpp | 8 +++++++- src/core/model.hpp | 5 +++-- src/core/paths.cpp | 2 ++ src/core/util.hpp | 14 ++++++++++++++ src/io/fileview.cpp | 2 +- src/io/fileview.hpp | 5 +++++ src/io/ipchandler.cpp | 2 +- src/io/ipchandler.hpp | 4 +++- src/services/notifications/notification.cpp | 2 +- src/services/pipewire/device.hpp | 3 +-- src/services/pipewire/link.hpp | 3 +-- src/services/pipewire/metadata.hpp | 4 +--- src/services/pipewire/node.hpp | 3 +-- src/services/pipewire/qml.cpp | 2 +- src/services/pipewire/registry.cpp | 6 ++++++ src/services/pipewire/registry.hpp | 8 ++++---- src/wayland/hyprland/focus_grab/grab.cpp | 2 +- src/widgets/CMakeLists.txt | 1 + 19 files changed, 57 insertions(+), 22 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f951d968..23e6add8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,9 @@ include(cmake/util.cmake) add_compile_options(-Wall -Wextra) +# pipewire defines this, breaking PCH +add_compile_definitions(_REENTRANT) + if (FRAME_POINTERS) add_compile_options(-fno-omit-frame-pointer) endif() diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 1564e895..45f5b3e7 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -369,6 +369,7 @@ void ThreadLogging::initFs() { .l_whence = SEEK_SET, .l_start = 0, .l_len = 0, + .l_pid = 0, }; if (fcntl(detailedFile->handle(), F_SETLK, &lock) != 0) { // NOLINT @@ -455,6 +456,8 @@ CompressedLogType compressedTypeOf(QtMsgType type) { case QtCriticalMsg: case QtFatalMsg: return CompressedLogType::Critical; } + + return CompressedLogType::Info; // unreachable under normal conditions } QtMsgType typeOfCompressed(CompressedLogType type) { @@ -464,6 +467,8 @@ QtMsgType typeOfCompressed(CompressedLogType type) { case CompressedLogType::Warn: return QtWarningMsg; case CompressedLogType::Critical: return QtCriticalMsg; } + + return QtInfoMsg; // unreachable under normal conditions } void WriteBuffer::setDevice(QIODevice* device) { this->device = device; } @@ -636,7 +641,7 @@ start: if (!this->readVarInt(&secondDelta)) return false; } - if (index < 0 || index >= this->recentMessages.size()) return false; + if (index >= this->recentMessages.size()) return false; *slot = this->recentMessages.at(index); this->lastMessageTime = this->lastMessageTime.addSecs(static_cast(secondDelta)); slot->time = this->lastMessageTime; @@ -858,6 +863,7 @@ void LogFollower::FcntlWaitThread::run() { .l_whence = SEEK_SET, .l_start = 0, .l_len = 0, + .l_pid = 0, }; auto r = fcntl(this->follower->reader->file->handle(), F_SETLKW, &lock); // NOLINT diff --git a/src/core/model.hpp b/src/core/model.hpp index 5ab3e79f..d0981fd7 100644 --- a/src/core/model.hpp +++ b/src/core/model.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -85,11 +86,11 @@ public: explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {} [[nodiscard]] QVector& valueList() { - return *reinterpret_cast*>(&this->valuesList); // NOLINT + return *std::bit_cast*>(&this->valuesList); } [[nodiscard]] const QVector& valueList() const { - return *reinterpret_cast*>(&this->valuesList); // NOLINT + return *std::bit_cast*>(&this->valuesList); } void insertObject(T* object, qsizetype index = -1) { diff --git a/src/core/paths.cpp b/src/core/paths.cpp index e2b15307..e108da03 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -242,6 +242,7 @@ void QsPaths::createLock() { .l_whence = SEEK_SET, .l_start = 0, .l_len = 0, + .l_pid = 0, }; if (fcntl(file->handle(), F_SETLK, &lock) != 0) { // NOLINT @@ -268,6 +269,7 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info) { .l_whence = SEEK_SET, .l_start = 0, .l_len = 0, + .l_pid = 0, }; fcntl(file.handle(), F_GETLK, &lock); // NOLINT diff --git a/src/core/util.hpp b/src/core/util.hpp index 3ca095e4..1ff9b22b 100644 --- a/src/core/util.hpp +++ b/src/core/util.hpp @@ -1,10 +1,24 @@ #pragma once +#include #include +#include #include #include #include +template +struct StringLiteral { + constexpr StringLiteral(const char (&str)[Length]) { // NOLINT + std::copy_n(str, Length, this->value); + } + + constexpr operator const char*() const noexcept { return this->value; } + operator QLatin1StringView() const { return QLatin1String(this->value, Length); } + + char value[Length]; // NOLINT +}; + // NOLINTBEGIN #define DROP_EMIT(object, func) \ DropEmitter(object, static_cast([](typeof(object) o) { o->func(); })) diff --git a/src/io/fileview.cpp b/src/io/fileview.cpp index 40dde6d7..6cfe4bc4 100644 --- a/src/io/fileview.cpp +++ b/src/io/fileview.cpp @@ -153,7 +153,7 @@ void FileView::loadSync() { auto state = FileViewState(); this->updateState(state); } else if (!this->blockUntilLoaded()) { - auto state = FileViewState {.path = this->targetPath}; + auto state = FileViewState(this->targetPath); FileViewReader::read(state, false); this->updateState(state); } diff --git a/src/io/fileview.hpp b/src/io/fileview.hpp index 04ed421a..715962f6 100644 --- a/src/io/fileview.hpp +++ b/src/io/fileview.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -15,6 +17,9 @@ namespace qs::io { struct FileViewState { + FileViewState() = default; + explicit FileViewState(QString path): path(std::move(path)) {} + QString path; QString text; QByteArray data; diff --git a/src/io/ipchandler.cpp b/src/io/ipchandler.cpp index d2a549b2..510b2055 100644 --- a/src/io/ipchandler.cpp +++ b/src/io/ipchandler.cpp @@ -247,7 +247,7 @@ void IpcHandlerRegistry::deregisterHandler(IpcHandler* handler) { } } - handler->registeredState = {.enabled = false, .target = ""}; + handler->registeredState = IpcHandler::RegistrationState(false); } QString IpcHandler::listMembers(qsizetype indent) { diff --git a/src/io/ipchandler.hpp b/src/io/ipchandler.hpp index 97519807..cc4ee5f4 100644 --- a/src/io/ipchandler.hpp +++ b/src/io/ipchandler.hpp @@ -172,12 +172,14 @@ private: void updateRegistration(bool destroying = false); struct RegistrationState { + explicit RegistrationState(bool enabled = false): enabled(enabled) {} + bool enabled = false; QString target; }; RegistrationState registeredState; - RegistrationState targetState {.enabled = true}; + RegistrationState targetState {true}; bool complete = false; QHash functionMap; diff --git a/src/services/notifications/notification.cpp b/src/services/notifications/notification.cpp index c090c135..18c8ff15 100644 --- a/src/services/notifications/notification.cpp +++ b/src/services/notifications/notification.cpp @@ -85,7 +85,7 @@ void Notification::updateProperties( qint32 expireTimeout ) { auto urgency = hints.contains("urgency") ? hints.value("urgency").value() - : NotificationUrgency::Normal; + : static_cast(NotificationUrgency::Normal); auto hasActionIcons = hints.value("action-icons").value(); auto resident = hints.value("resident").value(); diff --git a/src/services/pipewire/device.hpp b/src/services/pipewire/device.hpp index ed6b6c53..2e14d615 100644 --- a/src/services/pipewire/device.hpp +++ b/src/services/pipewire/device.hpp @@ -17,8 +17,7 @@ namespace qs::service::pipewire { class PwDevice; -constexpr const char TYPE_INTERFACE_Device[] = PW_TYPE_INTERFACE_Device; // NOLINT -class PwDevice: public PwBindable { +class PwDevice: public PwBindable { Q_OBJECT; public: diff --git a/src/services/pipewire/link.hpp b/src/services/pipewire/link.hpp index 0c7bde29..55bbcf0e 100644 --- a/src/services/pipewire/link.hpp +++ b/src/services/pipewire/link.hpp @@ -35,8 +35,7 @@ public: Q_INVOKABLE static QString toString(qs::service::pipewire::PwLinkState::Enum value); }; -constexpr const char TYPE_INTERFACE_Link[] = PW_TYPE_INTERFACE_Link; // NOLINT -class PwLink: public PwBindable { // NOLINT +class PwLink: public PwBindable { Q_OBJECT; public: diff --git a/src/services/pipewire/metadata.hpp b/src/services/pipewire/metadata.hpp index 812a8534..f257dc23 100644 --- a/src/services/pipewire/metadata.hpp +++ b/src/services/pipewire/metadata.hpp @@ -11,9 +11,7 @@ namespace qs::service::pipewire { -constexpr const char TYPE_INTERFACE_Metadata[] = PW_TYPE_INTERFACE_Metadata; // NOLINT -class PwMetadata - : public PwBindable { // NOLINT +class PwMetadata: public PwBindable { Q_OBJECT; public: diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index 783614ac..5a67db7d 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -156,8 +156,7 @@ private: PwNode* node; }; -constexpr const char TYPE_INTERFACE_Node[] = PW_TYPE_INTERFACE_Node; // NOLINT -class PwNode: public PwBindable { // NOLINT +class PwNode: public PwBindable { Q_OBJECT; public: diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp index a8186ea3..be50ec6e 100644 --- a/src/services/pipewire/qml.cpp +++ b/src/services/pipewire/qml.cpp @@ -429,7 +429,7 @@ void PwObjectTracker::setObjects(const QList& objects) { // connect destroy for (auto* object: objects) { - if (auto* pwObject = dynamic_cast(object)) { + if (dynamic_cast(object) != nullptr) { QObject::connect(object, &QObject::destroyed, this, &PwObjectTracker::objectDestroyed); } } diff --git a/src/services/pipewire/registry.cpp b/src/services/pipewire/registry.cpp index 1370fa10..04bd9ace 100644 --- a/src/services/pipewire/registry.cpp +++ b/src/services/pipewire/registry.cpp @@ -63,6 +63,12 @@ void PwBindableObject::unref() { if (this->refcount == 0) this->unbind(); } +void PwBindableObject::registryBind(const char* interface, quint32 version) { + // NOLINTNEXTLINE + auto* object = pw_registry_bind(this->registry->object, this->id, interface, version, 0); + this->object = static_cast(object); +} + void PwBindableObject::bind() { qCDebug(logRegistry) << "Bound object" << this; this->bindHooks(); diff --git a/src/services/pipewire/registry.hpp b/src/services/pipewire/registry.hpp index c61773b2..59aac757 100644 --- a/src/services/pipewire/registry.hpp +++ b/src/services/pipewire/registry.hpp @@ -12,6 +12,7 @@ #include #include +#include "../../core/util.hpp" #include "core.hpp" namespace qs::service::pipewire { @@ -50,6 +51,7 @@ signals: void destroying(PwBindableObject* self); protected: + void registryBind(const char* interface, quint32 version); virtual void bind(); void unbind(); virtual void bindHooks() {}; @@ -62,7 +64,7 @@ protected: QDebug operator<<(QDebug debug, const PwBindableObject* object); -template +template class PwBindable: public PwBindableObject { public: T* proxy() { @@ -72,9 +74,7 @@ public: protected: void bind() override { if (this->object != nullptr) return; - auto* object = - pw_registry_bind(this->registry->object, this->id, INTERFACE, VERSION, 0); // NOLINT - this->object = static_cast(object); + this->registryBind(INTERFACE, VERSION); this->PwBindableObject::bind(); } diff --git a/src/wayland/hyprland/focus_grab/grab.cpp b/src/wayland/hyprland/focus_grab/grab.cpp index 62298699..188c2063 100644 --- a/src/wayland/hyprland/focus_grab/grab.cpp +++ b/src/wayland/hyprland/focus_grab/grab.cpp @@ -39,7 +39,7 @@ void FocusGrab::addWindow(QWindow* window) { if (auto* waylandWindow = dynamic_cast(window->handle())) { tryAddWayland(waylandWindow); } else { - QObject::connect(window, &QWindow::visibleChanged, this, [this, window, tryAddWayland]() { + QObject::connect(window, &QWindow::visibleChanged, this, [window, tryAddWayland]() { if (window->isVisible()) { if (window->handle() == nullptr) { window->create(); diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index 226d950d..def0aaf0 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -11,4 +11,5 @@ install_qml_module(quickshell-widgets) qs_module_pch(quickshell-widgets) +target_link_libraries(quickshell-widgets PRIVATE Qt::Quick) target_link_libraries(quickshell PRIVATE quickshell-widgetsplugin) From 74f371850df400a9b403ce61a290149e2fc08323 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 11 Nov 2024 22:01:08 -0800 Subject: [PATCH 198/305] launch: fix use after free of command options --- src/launch/launch_p.hpp | 2 ++ src/launch/parsecommand.cpp | 30 ++++++++++++++++-------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/launch/launch_p.hpp b/src/launch/launch_p.hpp index d1916d50..d752edbc 100644 --- a/src/launch/launch_p.hpp +++ b/src/launch/launch_p.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -26,6 +27,7 @@ private: }; struct CommandState { + std::unique_ptr app; struct { int argc = 0; char** argv = nullptr; diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp index 14fd9203..91f7dc04 100644 --- a/src/launch/parsecommand.cpp +++ b/src/launch/parsecommand.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include // NOLINT: Need to include this for impls of some CLI11 classes @@ -90,30 +91,31 @@ int parseCommand(int argc, char** argv, CommandState& state) { return group; }; - auto cli = CLI::App(); + state.app = std::make_unique(); + auto* cli = state.app.get(); // Require 0-1 subcommands. Without this, positionals can be parsed as more subcommands. - cli.require_subcommand(0, 1); + cli->require_subcommand(0, 1); - addConfigSelection(&cli); - addLoggingOptions(&cli, false); - addDebugOptions(&cli); + addConfigSelection(cli); + addLoggingOptions(cli, false); + addDebugOptions(cli); { - cli.add_option_group("")->add_flag("--private-check-compat", state.misc.checkCompat); + cli->add_option_group("")->add_flag("--private-check-compat", state.misc.checkCompat); - cli.add_flag("-V,--version", state.misc.printVersion) + cli->add_flag("-V,--version", state.misc.printVersion) ->description("Print quickshell's version and exit."); - cli.add_flag("-n,--no-duplicate", state.misc.noDuplicate) + cli->add_flag("-n,--no-duplicate", state.misc.noDuplicate) ->description("Exit immediately if another instance of the given config is running."); - cli.add_flag("-d,--daemonize", state.misc.daemonize) + cli->add_flag("-d,--daemonize", state.misc.daemonize) ->description("Detach from the controlling terminal."); } { - auto* sub = cli.add_subcommand("log", "Print quickshell logs."); + auto* sub = cli->add_subcommand("log", "Print quickshell logs."); auto* file = sub->add_option("file", state.log.file, "Log file to read."); @@ -135,7 +137,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { } { - auto* sub = cli.add_subcommand("list", "List running quickshell instances."); + 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" @@ -151,7 +153,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { } { - auto* sub = cli.add_subcommand("kill", "Kill quickshell instances."); + auto* sub = cli->add_subcommand("kill", "Kill quickshell instances."); //sub->add_flag("-a,--all", "Kill all matching instances instead of just one."); auto* instance = addInstanceSelection(sub); addConfigSelection(sub)->excludes(instance); @@ -161,7 +163,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { } { - auto* sub = cli.add_subcommand("msg", "Send messages to IpcHandlers.")->require_option(); + auto* sub = cli->add_subcommand("msg", "Send messages to IpcHandlers.")->require_option(); auto* target = sub->add_option("target", state.ipc.target, "The target to message."); @@ -188,7 +190,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { state.subcommand.msg = sub; } - CLI11_PARSE(cli, argc, argv); + CLI11_PARSE(*cli, argc, argv); return 0; } From 2c0e46cedb6305671b1eafa1e0c6071a8ab48b27 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 12 Nov 2024 03:23:59 -0800 Subject: [PATCH 199/305] core/lazyloader: fix incubator UAF in forceCompletion The incubator was deleted via onIncubationCompleted before it was done working when completed via forceCompletion(). --- src/core/lazyloader.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/lazyloader.cpp b/src/core/lazyloader.cpp index 76317223..be0eb78b 100644 --- a/src/core/lazyloader.cpp +++ b/src/core/lazyloader.cpp @@ -179,7 +179,9 @@ void LazyLoader::incubateIfReady(bool overrideReloadCheck) { void LazyLoader::onIncubationCompleted() { this->setItem(this->incubator->object()); - delete this->incubator; + // The incubator is not necessarily inert at the time of this callback, + // so deleteLater is required. + this->incubator->deleteLater(); this->incubator = nullptr; this->targetLoading = false; emit this->loadingChanged(); From 0dd19d4a181378df5f61de9b5f54b3f2bba96e8a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 12 Nov 2024 04:35:42 -0800 Subject: [PATCH 200/305] core/proxywindow: remove blank frame when destroying window Removes the blank frame caused by removing the content item from the window. Fixes an issue with hyprland's window exit animations. --- src/window/proxywindow.cpp | 12 +++++++----- src/window/proxywindow.hpp | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 07f8a233..3d01224d 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -44,7 +44,7 @@ ProxyWindowBase::ProxyWindowBase(QObject* parent) // clang-format on } -ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(); } +ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(true); } void ProxyWindowBase::onReload(QObject* oldInstance) { this->window = this->retrieveWindow(oldInstance); @@ -90,9 +90,9 @@ void ProxyWindowBase::createWindow() { emit this->windowConnected(); } -void ProxyWindowBase::deleteWindow() { +void ProxyWindowBase::deleteWindow(bool keepItemOwnership) { if (this->window != nullptr) emit this->windowDestroyed(); - if (auto* window = this->disownWindow()) { + if (auto* window = this->disownWindow(keepItemOwnership)) { if (auto* generation = EngineGeneration::findObjectGeneration(this)) { generation->deregisterIncubationController(window->incubationController()); } @@ -101,12 +101,14 @@ void ProxyWindowBase::deleteWindow() { } } -QQuickWindow* ProxyWindowBase::disownWindow() { +QQuickWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) { if (this->window == nullptr) return nullptr; QObject::disconnect(this->window, nullptr, this, nullptr); - this->mContentItem->setParentItem(nullptr); + if (!keepItemOwnership) { + this->mContentItem->setParentItem(nullptr); + } auto* window = this->window; this->window = nullptr; diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index dbbf1910..79d326e3 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -57,10 +57,10 @@ public: void onReload(QObject* oldInstance) override; void createWindow(); - void deleteWindow(); + void deleteWindow(bool keepItemOwnership = false); // Disown the backing window and delete all its children. - virtual QQuickWindow* disownWindow(); + virtual QQuickWindow* disownWindow(bool keepItemOwnership = false); virtual QQuickWindow* retrieveWindow(QObject* oldInstance); virtual QQuickWindow* createQQuickWindow(); From 60dfa67ec7f20e574a1ecf112a759aac30523ce2 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 14 Nov 2024 17:54:16 -0800 Subject: [PATCH 201/305] io/fileview: support zero-sized files (/proc) --- src/io/fileview.cpp | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/io/fileview.cpp b/src/io/fileview.cpp index 6cfe4bc4..063ea831 100644 --- a/src/io/fileview.cpp +++ b/src/io/fileview.cpp @@ -1,4 +1,5 @@ #include "fileview.hpp" +#include #include #include @@ -68,22 +69,37 @@ void FileViewReader::read(FileViewState& state, bool doStringConversion) { } auto& data = state.data; - data = QByteArray(file.size(), Qt::Uninitialized); + if (file.size() != 0) { + data = QByteArray(file.size(), Qt::Uninitialized); + qint64 i = 0; - qint64 i = 0; + while (true) { + auto r = file.read(data.data() + i, data.length() - i); // NOLINT - while (true) { - auto r = file.read(data.data() + i, data.length() - i); // NOLINT + if (r == -1) { + qCCritical(logFileView) << "Failed to read" << state.path; + goto error; + } else if (r == 0) { + data.resize(i); + break; + } - if (r == -1) { - qCCritical(logFileView) << "Failed to read" << state.path; - goto error; - } else if (r == 0) { - data.resize(i); - break; + i += r; } + } else { + auto buf = std::array(); - i += r; + while (true) { + auto r = file.read(buf.data(), buf.size()); // NOLINT + + if (r == -1) { + qCCritical(logFileView) << "Failed to read" << state.path; + goto error; + } else { + data.append(buf.data(), r); + if (r == 0) break; + } + } } if (doStringConversion) { From 0445eee33a5c2e88ce5b23fbe4924185420e7983 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 00:47:22 -0800 Subject: [PATCH 202/305] io/process: support commands at file:// and root:// paths. --- src/io/process.cpp | 11 +++++++++++ src/io/process.hpp | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/src/io/process.cpp b/src/io/process.cpp index 9fae90e2..3e3292d0 100644 --- a/src/io/process.cpp +++ b/src/io/process.cpp @@ -12,6 +12,7 @@ #include #include +#include "../core/generation.hpp" #include "../core/qmlglobal.hpp" #include "datastream.hpp" @@ -53,6 +54,16 @@ QList Process::command() const { return this->mCommand; } void Process::setCommand(QList command) { if (this->mCommand == command) return; this->mCommand = std::move(command); + + auto& cmd = this->mCommand.first(); + if (cmd.startsWith("file://")) { + cmd = cmd.sliced(7); + } else if (cmd.startsWith("root://")) { + cmd = cmd.sliced(7); + auto& root = EngineGeneration::findObjectGeneration(this)->rootPath; + cmd = root.filePath(cmd.startsWith('/') ? cmd.sliced(1) : cmd); + } + emit this->commandChanged(); this->startProcessIfReady(); diff --git a/src/io/process.hpp b/src/io/process.hpp index 521ee2ca..43db3165 100644 --- a/src/io/process.hpp +++ b/src/io/process.hpp @@ -51,6 +51,14 @@ class Process: public QObject { /// 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. /// + /// > [!WARNING] This does not run command in a shell. All arguments to the command + /// > must be in separate values in the list, e.g. `["echo", "hello"]` + /// > and not `["echo hello"]`. + /// > + /// > Additionally, shell scripts must be run by your shell, + /// > e.g. `["sh", "script.sh"]` instead of `["script.sh"]` unless the script + /// > has a shebang. + /// /// > [!INFO] You can use `["sh", "-c", ]` to execute your command with /// > the system shell. Q_PROPERTY(QList command READ command WRITE setCommand NOTIFY commandChanged); From 36d1dbeb69854767c70b44b520c7cd1a44a36f93 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 01:30:54 -0800 Subject: [PATCH 203/305] service/tray: report misbehaving tray hosts I've debugged broken tray items that just end up being a bad host far too many times. --- src/dbus/properties.cpp | 3 ++- src/dbus/properties.hpp | 1 + src/services/status_notifier/item.cpp | 16 ++++++++++++++++ src/services/status_notifier/item.hpp | 1 + src/services/status_notifier/watcher.cpp | 4 +++- src/services/status_notifier/watcher.hpp | 2 ++ 6 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index 1a40ca23..6156b2a3 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -268,14 +268,15 @@ void DBusPropertyGroup::updateAllViaGetAll() { qCWarning(logDbusProperties).noquote() << "Error updating properties of" << this->toString() << "via GetAll"; qCWarning(logDbusProperties) << reply.error(); + emit this->getAllFailed(reply.error()); } else { qCDebug(logDbusProperties).noquote() << "Received GetAll property set for" << this->toString(); this->updatePropertySet(reply.value(), true); + emit this->getAllFinished(); } delete call; - emit this->getAllFinished(); }; QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index e24d23fb..65f51afc 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -135,6 +135,7 @@ public: signals: void getAllFinished(); + void getAllFailed(QDBusError error); private slots: void onPropertiesChanged( diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index 7f990a9f..f6e16a24 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -26,6 +26,7 @@ #include "dbus_item.h" #include "dbus_item_types.hpp" #include "host.hpp" +#include "watcher.hpp" using namespace qs::dbus; using namespace qs::dbus::dbusmenu; @@ -76,6 +77,7 @@ StatusNotifierItem::StatusNotifierItem(const QString& address, QObject* parent) QObject::connect(&this->overlayIconPixmaps, &AbstractDBusProperty::changed, this, &StatusNotifierItem::updateIcon); QObject::connect(&this->properties, &DBusPropertyGroup::getAllFinished, this, &StatusNotifierItem::onGetAllFinished); + QObject::connect(&this->properties, &DBusPropertyGroup::getAllFailed, this, &StatusNotifierItem::onGetAllFailed); QObject::connect(&this->menuPath, &AbstractDBusProperty::changed, this, &StatusNotifierItem::onMenuPathChanged); // clang-format on @@ -246,6 +248,19 @@ void StatusNotifierItem::onGetAllFinished() { emit this->ready(); } +void StatusNotifierItem::onGetAllFailed() { + // Not changing the item to ready, as it is almost definitely broken. + if (!this->mReady) { + qWarning(logStatusNotifierItem) << "Failed to load tray item" << this->properties.toString(); + + if (!StatusNotifierWatcher::instance()->isRegistered()) { + qWarning(logStatusNotifierItem) + << "Another StatusNotifier host seems to be running. Please disable it and check that " + "the problem persists before reporting an issue."; + } + } +} + TrayImageHandle::TrayImageHandle(StatusNotifierItem* item) : QsImageHandle(QQmlImageProviderBase::Pixmap, item) , item(item) {} @@ -257,6 +272,7 @@ TrayImageHandle::requestPixmap(const QString& /*unused*/, QSize* size, const QSi auto pixmap = this->item->createPixmap(targetSize); if (pixmap.isNull()) { + qCWarning(logStatusNotifierItem) << "Unable to create pixmap for tray icon" << this->item; pixmap = IconImageProvider::missingPixmap(targetSize); } diff --git a/src/services/status_notifier/item.hpp b/src/services/status_notifier/item.hpp index efe31591..2a22e2ec 100644 --- a/src/services/status_notifier/item.hpp +++ b/src/services/status_notifier/item.hpp @@ -75,6 +75,7 @@ signals: private slots: void updateIcon(); void onGetAllFinished(); + void onGetAllFailed(); void onMenuPathChanged(); private: diff --git a/src/services/status_notifier/watcher.cpp b/src/services/status_notifier/watcher.cpp index a6fd2179..4917077c 100644 --- a/src/services/status_notifier/watcher.cpp +++ b/src/services/status_notifier/watcher.cpp @@ -47,12 +47,15 @@ StatusNotifierWatcher::StatusNotifierWatcher(QObject* parent): QObject(parent) { this->tryRegister(); } +bool StatusNotifierWatcher::isRegistered() const { return this->registered; } + void StatusNotifierWatcher::tryRegister() { // NOLINT auto bus = QDBusConnection::sessionBus(); auto success = bus.registerService("org.kde.StatusNotifierWatcher"); if (success) { qCDebug(logStatusNotifierWatcher) << "Registered watcher at org.kde.StatusNotifierWatcher"; + this->registered = true; } else { qCDebug(logStatusNotifierWatcher) << "Could not register watcher at org.kde.StatusNotifierWatcher, presumably because one is " @@ -68,7 +71,6 @@ void StatusNotifierWatcher::onServiceUnregistered(const QString& service) { << "Active StatusNotifierWatcher unregistered, attempting registration"; this->tryRegister(); return; - ; } else { QString qualifiedItem; this->items.removeIf([&](const QString& item) { diff --git a/src/services/status_notifier/watcher.hpp b/src/services/status_notifier/watcher.hpp index 5fd41e5a..4a042257 100644 --- a/src/services/status_notifier/watcher.hpp +++ b/src/services/status_notifier/watcher.hpp @@ -29,6 +29,7 @@ public: [[nodiscard]] qint32 protocolVersion() const { return 0; } // NOLINT [[nodiscard]] bool isHostRegistered() const; [[nodiscard]] QList registeredItems() const; + [[nodiscard]] bool isRegistered() const; // NOLINTBEGIN void RegisterStatusNotifierHost(const QString& host); @@ -54,6 +55,7 @@ private: QDBusServiceWatcher serviceWatcher; QList hosts; QList items; + bool registered = false; }; } // namespace qs::service::sni From 29d31f5d3bde681facbd623f0d0fca1652f77fae Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 01:36:25 -0800 Subject: [PATCH 204/305] docs: add note that private qt headers are required for some libs --- BUILD.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/BUILD.md b/BUILD.md index 5a0652fe..8fa78845 100644 --- a/BUILD.md +++ b/BUILD.md @@ -44,6 +44,12 @@ Quickshell has a set of base dependencies you will always need, names vary by di - `pkg-config` - `cli11` +On some distros, private Qt headers are in separate packages which you may have to install. +We currently require private headers for the following libraries: + +- `qt6declarative` +- `qt6wayland` + We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and svg icons will not work, including system ones. From 7db37726412fd071886cf2303e1da4a54bcd1c05 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 01:46:49 -0800 Subject: [PATCH 205/305] core/generation: short circuit findObjectGeneration if only one exists --- src/core/generation.cpp | 8 +++++--- src/core/generation.hpp | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 147e2f93..f4fcde63 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -24,7 +24,7 @@ #include "reload.hpp" #include "scan.hpp" -static QHash g_generations; // NOLINT +static QHash g_generations; // NOLINT EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) : rootPath(rootPath) @@ -326,11 +326,13 @@ EngineGeneration* EngineGeneration::currentGeneration() { } else return nullptr; } -EngineGeneration* EngineGeneration::findEngineGeneration(QQmlEngine* engine) { +EngineGeneration* EngineGeneration::findEngineGeneration(const QQmlEngine* engine) { return g_generations.value(engine); } -EngineGeneration* EngineGeneration::findObjectGeneration(QObject* object) { +EngineGeneration* EngineGeneration::findObjectGeneration(const QObject* object) { + if (g_generations.size() == 1) return EngineGeneration::currentGeneration(); + while (object != nullptr) { auto* context = QQmlEngine::contextForObject(object); diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 043d2f70..2d842829 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -45,8 +45,8 @@ public: void registerExtension(const void* key, EngineGenerationExt* extension); EngineGenerationExt* findExtension(const void* key); - static EngineGeneration* findEngineGeneration(QQmlEngine* engine); - static EngineGeneration* findObjectGeneration(QObject* object); + static EngineGeneration* findEngineGeneration(const QQmlEngine* engine); + static EngineGeneration* findObjectGeneration(const QObject* object); // Returns the current generation if there is only one generation, // otherwise null. From d2667369e18d44974a34921707e9a114b5534271 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 01:49:27 -0800 Subject: [PATCH 206/305] core/qmlglobal: add shellRoot property --- src/core/qmlglobal.cpp | 6 ++++++ src/core/qmlglobal.hpp | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 3321fcf3..9328d90a 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -166,6 +166,12 @@ void QuickshellGlobal::reload(bool hard) { root->reloadGraph(hard); } +QString QuickshellGlobal::shellRoot() const { + auto* generation = EngineGeneration::findObjectGeneration(this); + // already canonical + return generation->rootPath.path(); +} + QString QuickshellGlobal::workingDirectory() const { // NOLINT return QuickshellSettings::instance()->workingDirectory(); } diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index fb1853fb..7efdb413 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -98,6 +98,11 @@ class QuickshellGlobal: public QObject { /// This creates an instance of your window once on every screen. /// As screens are added or removed your window will be created or destroyed on those screens. Q_PROPERTY(QQmlListProperty screens READ screens NOTIFY screensChanged); + /// The full path to the root directory of your shell. + /// + /// The root directory is the folder containing the entrypoint to your shell, often referred + /// to as `shell.qml`. + Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT); /// 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. @@ -133,6 +138,8 @@ public: /// > of your icon theme. Q_INVOKABLE static QString iconPath(const QString& icon); + [[nodiscard]] QString shellRoot() const; + [[nodiscard]] QString workingDirectory() const; void setWorkingDirectory(QString workingDirectory); From 68ba5005ce02d251c40a6ad941bb17f064eab878 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 14:38:29 -0800 Subject: [PATCH 207/305] core/icon: ability to specify a fallback or check if an icon exists --- src/core/iconimageprovider.cpp | 21 +++++++++++++++++++-- src/core/iconimageprovider.hpp | 7 ++++++- src/core/qmlglobal.cpp | 12 +++++++++++- src/core/qmlglobal.hpp | 6 ++++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/core/iconimageprovider.cpp b/src/core/iconimageprovider.cpp index f4710fb8..cf24d37d 100644 --- a/src/core/iconimageprovider.cpp +++ b/src/core/iconimageprovider.cpp @@ -11,7 +11,9 @@ QPixmap IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) { QString iconName; + QString fallbackName; QString path; + auto splitIdx = id.indexOf("?path="); if (splitIdx != -1) { iconName = id.sliced(0, splitIdx); @@ -19,10 +21,17 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for" << id; } else { - iconName = id; + splitIdx = id.indexOf("?fallback="); + if (splitIdx != -1) { + iconName = id.sliced(0, splitIdx); + fallbackName = id.sliced(splitIdx + 10); + } else { + iconName = id; + } } auto icon = QIcon::fromTheme(iconName); + if (icon.isNull()) icon = QIcon::fromTheme(fallbackName); auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100); if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2); @@ -55,12 +64,20 @@ QPixmap IconImageProvider::missingPixmap(const QSize& size) { return pixmap; } -QString IconImageProvider::requestString(const QString& icon, const QString& path) { +QString IconImageProvider::requestString( + const QString& icon, + const QString& path, + const QString& fallback +) { auto req = "image://icon/" + icon; if (!path.isEmpty()) { req += "?path=" + path; } + if (!fallback.isEmpty()) { + req += "?fallback=" + fallback; + } + return req; } diff --git a/src/core/iconimageprovider.hpp b/src/core/iconimageprovider.hpp index 167d93bd..57e26049 100644 --- a/src/core/iconimageprovider.hpp +++ b/src/core/iconimageprovider.hpp @@ -10,5 +10,10 @@ public: QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override; static QPixmap missingPixmap(const QSize& size); - static QString requestString(const QString& icon, const QString& path); + + static QString requestString( + const QString& icon, + const QString& path = QString(), + const QString& fallback = QString() + ); }; diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 9328d90a..23f238da 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -196,7 +197,16 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT } QString QuickshellGlobal::iconPath(const QString& icon) { - return IconImageProvider::requestString(icon, ""); + return IconImageProvider::requestString(icon); +} + +QString QuickshellGlobal::iconPath(const QString& icon, bool check) { + if (check && QIcon::fromTheme(icon).isNull()) return ""; + return IconImageProvider::requestString(icon); +} + +QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback) { + return IconImageProvider::requestString(icon, "", fallback); } QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) { diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 7efdb413..4f46d238 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -137,6 +137,12 @@ public: /// > at the top of your root config file or set the `QS_ICON_THEME` variable to the name /// > of your icon theme. Q_INVOKABLE static QString iconPath(const QString& icon); + /// Setting the `check` parameter of `iconPath` to true will return an empty string + /// if the icon does not exist, instead of an image showing a missing texture. + Q_INVOKABLE static QString iconPath(const QString& icon, bool check); + /// 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); [[nodiscard]] QString shellRoot() const; From fdc13023b7a2cf11a68756c673e6bf0f9d7a37fc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 17:05:44 -0800 Subject: [PATCH 208/305] widgets: add ClippingRectangle --- BUILD.md | 1 + CMakeLists.txt | 2 +- default.nix | 3 ++ src/widgets/CMakeLists.txt | 20 +++++++- src/widgets/ClippingRectangle.qml | 82 +++++++++++++++++++++++++++++++ src/widgets/cliprect.cpp | 1 + src/widgets/cliprect.hpp | 20 ++++++++ src/widgets/module.md | 1 + src/widgets/shaders/cliprect.frag | 40 +++++++++++++++ 9 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 src/widgets/ClippingRectangle.qml create mode 100644 src/widgets/cliprect.cpp create mode 100644 src/widgets/cliprect.hpp create mode 100644 src/widgets/shaders/cliprect.frag diff --git a/BUILD.md b/BUILD.md index 8fa78845..2085a5b3 100644 --- a/BUILD.md +++ b/BUILD.md @@ -41,6 +41,7 @@ Quickshell has a set of base dependencies you will always need, names vary by di - `cmake` - `qt6base` - `qt6declarative` +- `qtshadertools` (build-time only) - `pkg-config` - `cli11` diff --git a/CMakeLists.txt b/CMakeLists.txt index 23e6add8..61cc2963 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,7 +91,7 @@ if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Debug) endif() -set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets) +set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools) include(cmake/pch.cmake) diff --git a/default.nix b/default.nix index 298e561e..83c5e542 100644 --- a/default.nix +++ b/default.nix @@ -8,6 +8,7 @@ cmake, ninja, qt6, + spirv-tools, cli11, breakpad, jemalloc, @@ -45,6 +46,8 @@ nativeBuildInputs = with pkgs; [ cmake ninja + qt6.qtshadertools + spirv-tools qt6.wrapQtAppsHook pkg-config ] ++ (lib.optionals withWayland [ diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index def0aaf0..5fbcc5b5 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -1,10 +1,28 @@ -qt_add_library(quickshell-widgets STATIC) +qt_add_library(quickshell-widgets STATIC + cliprect.cpp +) qt_add_qml_module(quickshell-widgets URI Quickshell.Widgets VERSION 0.1 QML_FILES IconImage.qml + ClippingRectangle.qml +) + +qt6_add_shaders(quickshell-widgets "widgets-cliprect" + NOHLSL NOMSL BATCHABLE PRECOMPILE OPTIMIZED QUIET + PREFIX "/Quickshell/Widgets" + FILES shaders/cliprect.frag + OUTPUTS shaders/cliprect.frag.qsb +) + +qt6_add_shaders(quickshell-widgets "widgets-cliprect-ub" + NOHLSL NOMSL BATCHABLE PRECOMPILE OPTIMIZED QUIET + PREFIX "/Quickshell/Widgets" + FILES shaders/cliprect.frag + OUTPUTS shaders/cliprect-ub.frag.qsb + DEFINES CONTENT_UNDER_BORDER ) install_qml_module(quickshell-widgets) diff --git a/src/widgets/ClippingRectangle.qml b/src/widgets/ClippingRectangle.qml new file mode 100644 index 00000000..ca8ae5a5 --- /dev/null +++ b/src/widgets/ClippingRectangle.qml @@ -0,0 +1,82 @@ +import QtQuick + +///! Rectangle capable of clipping content inside its border. +/// > [!WARNING] This type requires at least Qt 6.7. +/// +/// This is a specialized version of @@QtQuick.Rectangle that clips content +/// inside of its border, including rounded rectangles. It costs more than +/// @@QtQuick.Rectangle, so it should not be used unless you need to clip +/// items inside of it to the border. +Item { + id: root + + /// If content should be displayed underneath the border. + /// + /// Defaults to false, does nothing if the border is opaque. + property bool contentUnderBorder: false; + /// If the content item should be resized to fit inside the border. + /// + /// Defaults to `!contentUnderBorder`. Most useful when combined with + /// `anchors.fill: parent` on an item passed to the ClippingRectangle. + property bool contentInsideBorder: !root.contentUnderBorder; + /// If the rectangle should be antialiased. + /// + /// 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; + /// See @@QtQuick.Rectangle.border. + property clippingRectangleBorder border; + /// Radius of all corners. Defaults to 0. + property /*real*/alias radius: rectangle.radius + /// Radius of the top left corner. Defaults to @@radius. + property /*real*/alias topLeftRadius: rectangle.topLeftRadius + /// Radius of the top right corner. Defaults to @@radius. + property /*real*/alias topRightRadius: rectangle.topRightRadius + /// Radius of the bottom left corner. Defaults to @@radius. + property /*real*/alias bottomLeftRadius: rectangle.bottomLeftRadius + /// Radius of the bottom right corner. Defaults to @@radius. + property /*real*/alias borromRightRadius: rectangle.bottomRightRadius + + /// Visual children of the ClippingRectangle's @@contentItem. (`list`). + /// + /// See @@QtQuick.Item.children for details. + default property alias children: contentItem.children; + /// The item containing the rectangle's content. + /// There is usually no reason to use this directly. + readonly property alias contentItem: contentItem; + + Rectangle { + id: rectangle + anchors.fill: root + color: "#ffff0000" + border.color: "#ff00ff00" + border.pixelAligned: root.border.pixelAligned + border.width: root.border.width + layer.enabled: true + visible: false + } + + 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 + } + } + + ShaderEffect { + id: shader + anchors.fill: root + fragmentShader: `qrc:/Quickshell/Widgets/shaders/cliprect${root.contentUnderBorder ? "-ub" : ""}.frag.qsb` + property Rectangle rect: rectangle; + property color backgroundColor; + property color borderColor: root.border.color; + property Item content: contentItemContainer; + } +} diff --git a/src/widgets/cliprect.cpp b/src/widgets/cliprect.cpp new file mode 100644 index 00000000..a66147ea --- /dev/null +++ b/src/widgets/cliprect.cpp @@ -0,0 +1 @@ +#include "cliprect.hpp" // NOLINT diff --git a/src/widgets/cliprect.hpp b/src/widgets/cliprect.hpp new file mode 100644 index 00000000..adc1381a --- /dev/null +++ b/src/widgets/cliprect.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include +#include +#include + +class ClippingRectangleBorder { + Q_GADGET; + Q_PROPERTY(QColor color MEMBER color); + Q_PROPERTY(bool pixelAligned MEMBER pixelAligned); + Q_PROPERTY(int width MEMBER width); + QML_VALUE_TYPE(clippingRectangleBorder); + +public: + QColor color = Qt::black; + bool pixelAligned = true; + int width = 0; +}; diff --git a/src/widgets/module.md b/src/widgets/module.md index 9a51894c..c24bb876 100644 --- a/src/widgets/module.md +++ b/src/widgets/module.md @@ -2,5 +2,6 @@ name = "Quickshell.Widgets" description = "Bundled widgets" qml_files = [ "IconImage.qml", + "ClippingRectangle.qml", ] ----- diff --git a/src/widgets/shaders/cliprect.frag b/src/widgets/shaders/cliprect.frag new file mode 100644 index 00000000..f1c004a1 --- /dev/null +++ b/src/widgets/shaders/cliprect.frag @@ -0,0 +1,40 @@ +#version 440 +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + vec4 backgroundColor; + vec4 borderColor; +}; + +layout(binding = 1) uniform sampler2D rect; +layout(binding = 2) uniform sampler2D content; + +vec4 overlay(vec4 base, vec4 overlay) { + if (overlay.a == 0.0) return base; + + float baseMul = 1.0 - overlay.a; + float newAlpha = overlay.a + base.a * baseMul; + vec3 rgb = (overlay.rgb * overlay.a + base.rgb * base.a * baseMul) / newAlpha; + return vec4(rgb, newAlpha); +} + +void main() { + vec4 contentColor = texture(content, qt_TexCoord0.xy); + vec4 rectColor = texture(rect, qt_TexCoord0.xy); + +#ifdef CONTENT_UNDER_BORDER + float contentAlpha = rectColor.a; +#else + float contentAlpha = rectColor.r; +#endif + + float borderAlpha = rectColor.g; + + vec4 innerColor = overlay(backgroundColor, contentColor) * contentAlpha; + vec4 borderColor = borderColor * borderAlpha; + + fragColor = (innerColor * (1.0 - borderColor.a) + borderColor) * qt_Opacity; +} From 36174854ada58d7fdf47b92edadf200e2084d03a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 17:06:06 -0800 Subject: [PATCH 209/305] services/tray: fix const lint in item --- src/services/status_notifier/item.cpp | 2 +- src/services/status_notifier/item.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index f6e16a24..e3bb0120 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -248,7 +248,7 @@ void StatusNotifierItem::onGetAllFinished() { emit this->ready(); } -void StatusNotifierItem::onGetAllFailed() { +void StatusNotifierItem::onGetAllFailed() const { // Not changing the item to ready, as it is almost definitely broken. if (!this->mReady) { qWarning(logStatusNotifierItem) << "Failed to load tray item" << this->properties.toString(); diff --git a/src/services/status_notifier/item.hpp b/src/services/status_notifier/item.hpp index 2a22e2ec..eccf79b0 100644 --- a/src/services/status_notifier/item.hpp +++ b/src/services/status_notifier/item.hpp @@ -75,7 +75,7 @@ signals: private slots: void updateIcon(); void onGetAllFinished(); - void onGetAllFailed(); + void onGetAllFailed() const; void onMenuPathChanged(); private: From 79fca3cab88e1911d4203507ccce47ebaeaaff9c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 21:38:17 -0800 Subject: [PATCH 210/305] docs: mention spirv-tools in BUILD.md --- BUILD.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index 2085a5b3..659a6166 100644 --- a/BUILD.md +++ b/BUILD.md @@ -42,7 +42,8 @@ Quickshell has a set of base dependencies you will always need, names vary by di - `qt6base` - `qt6declarative` - `qtshadertools` (build-time only) -- `pkg-config` +- `spirv-tools` (build-time only) +- `pkg-config` (build-time only) - `cli11` On some distros, private Qt headers are in separate packages which you may have to install. From 401ee4cec6dec6eb846e7b525231a259f6da8549 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 02:02:55 -0800 Subject: [PATCH 211/305] widgets: add wrapper components and managers --- src/widgets/CMakeLists.txt | 4 + src/widgets/WrapperItem.qml | 44 ++++++++++ src/widgets/WrapperRectangle.qml | 35 ++++++++ src/widgets/marginwrapper.cpp | 146 +++++++++++++++++++++++++++++++ src/widgets/marginwrapper.hpp | 75 ++++++++++++++++ src/widgets/module.md | 8 ++ src/widgets/wrapper.cpp | 127 +++++++++++++++++++++++++++ src/widgets/wrapper.hpp | 139 +++++++++++++++++++++++++++++ 8 files changed, 578 insertions(+) create mode 100644 src/widgets/WrapperItem.qml create mode 100644 src/widgets/WrapperRectangle.qml create mode 100644 src/widgets/marginwrapper.cpp create mode 100644 src/widgets/marginwrapper.hpp create mode 100644 src/widgets/wrapper.cpp create mode 100644 src/widgets/wrapper.hpp diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index 5fbcc5b5..3f8de41f 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -1,5 +1,7 @@ qt_add_library(quickshell-widgets STATIC cliprect.cpp + wrapper.cpp + marginwrapper.cpp ) qt_add_qml_module(quickshell-widgets @@ -8,6 +10,8 @@ qt_add_qml_module(quickshell-widgets QML_FILES IconImage.qml ClippingRectangle.qml + WrapperItem.qml + WrapperRectangle.qml ) qt6_add_shaders(quickshell-widgets "widgets-cliprect" diff --git a/src/widgets/WrapperItem.qml b/src/widgets/WrapperItem.qml new file mode 100644 index 00000000..dfa7c0fd --- /dev/null +++ b/src/widgets/WrapperItem.qml @@ -0,0 +1,44 @@ +import QtQuick +import Quickshell.Widgets + +///! Item that handles sizes and positioning for a single visual child. +/// This component is useful when you need to wrap a single component in +/// an item, or give a single component a margin. See [QtQuick.Layouts] +/// for positioning multiple items. +/// +/// > [!NOTE] WrapperItem is a @@MarginWrapperManager based component. +/// > You should read its documentation as well. +/// +/// ### Example: Adding a margin to an item +/// The snippet below adds a 10px margin to all sides of the @@QtQuick.Text item. +/// +/// ```qml +/// WrapperItem { +/// margin: 10 +/// +/// @@QtQuick.Text { text: "Hello!" } +/// } +/// ``` +/// +/// > [!NOTE] The child item can be specified by writing it inline in the wrapper, +/// > as in the example above, or by using the @@child property. See +/// > @@WrapperManager.child for details. +/// +/// > [!WARNING] You should not set @@Item.x, @@Item.y, @@Item.width, +/// > @@Item.height or @@Item.anchors on the child item, as they are used +/// > by WrapperItem to position it. Instead set @@Item.implicitWidth and +/// > @@Item.implicitHeight. +/// +/// [QtQuick.Layouts]: https://doc.qt.io/qt-6/qtquicklayouts-index.html +Item { + /// The minimum margin between the child item and the WrapperItem's edges. + /// Defaults to 0. + property /*real*/alias margin: manager.margin + /// If the child item should be resized larger than its implicit size if + /// the WrapperItem is resized larger than its implicit size. Defaults to false. + property /*bool*/alias resizeChild: manager.resizeChild + /// See @@WrapperManager.child for details. + property /*Item*/alias child: manager.child + + MarginWrapperManager { id: manager } +} diff --git a/src/widgets/WrapperRectangle.qml b/src/widgets/WrapperRectangle.qml new file mode 100644 index 00000000..c198c47b --- /dev/null +++ b/src/widgets/WrapperRectangle.qml @@ -0,0 +1,35 @@ +import QtQuick +import Quickshell.Widgets + +///! Rectangle that handles sizes and positioning for a single visual child. +/// This component is useful for adding a border or background rectangle to +/// a child item. +/// +/// > [!NOTE] WrapperRectangle is a @@MarginWrapperManager based component. +/// > You should read its documentation as well. +/// +/// > [!WARNING] You should not set @@Item.x, @@Item.y, @@Item.width, +/// > @@Item.height or @@Item.anchors on the child item, as they are used +/// > by WrapperItem to position it. Instead set @@Item.implicitWidth and +/// > @@Item.implicitHeight. +Rectangle { + id: root + + /// If true (default), the rectangle's border width will be added + /// to the margin. + property bool contentInsideBorder: true + /// The minimum margin between the child item and the WrapperRectangle's + /// edges. If @@contentInsideBorder is true, this excludes the border, + /// otherwise it includes it. Defaults to 0. + property real margin: 0 + /// If the child item should be resized larger than its implicit size if + /// the WrapperRectangle is resized larger than its implicit size. Defaults to false. + property /*bool*/alias resizeChild: manager.resizeChild + /// See @@WrapperManager.child for details. + property alias child: manager.child + + MarginWrapperManager { + id: manager + margin: (root.contentInsideBorder ? root.border.width : 0) + root.margin + } +} diff --git a/src/widgets/marginwrapper.cpp b/src/widgets/marginwrapper.cpp new file mode 100644 index 00000000..b960a9f7 --- /dev/null +++ b/src/widgets/marginwrapper.cpp @@ -0,0 +1,146 @@ +#include "marginwrapper.hpp" +#include + +#include +#include +#include +#include + +#include "wrapper.hpp" + +namespace qs::widgets { + +MarginWrapperManager::MarginWrapperManager(QObject* parent): WrapperManager(parent) { + QObject::connect( + this, + &WrapperManager::initializedChildChanged, + this, + &MarginWrapperManager::onChildChanged + ); +} + +void MarginWrapperManager::componentComplete() { + if (this->mWrapper) { + QObject::connect( + this->mWrapper, + &QQuickItem::widthChanged, + this, + &MarginWrapperManager::onWrapperWidthChanged + ); + + QObject::connect( + this->mWrapper, + &QQuickItem::heightChanged, + this, + &MarginWrapperManager::onWrapperHeightChanged + ); + } + + this->WrapperManager::componentComplete(); + + if (!this->mChild) this->updateGeometry(); +} + +qreal MarginWrapperManager::margin() const { return this->mMargin; } + +void MarginWrapperManager::setMargin(qreal margin) { + if (margin == this->mMargin) return; + this->mMargin = margin; + this->updateGeometry(); + emit this->marginChanged(); +} + +bool MarginWrapperManager::resizeChild() const { return this->mResizeChild; } + +void MarginWrapperManager::setResizeChild(bool resizeChild) { + if (resizeChild == this->mResizeChild) return; + this->mResizeChild = resizeChild; + this->updateGeometry(); + emit this->resizeChildChanged(); +} + +void MarginWrapperManager::onChildChanged() { + // QObject::disconnect in MarginWrapper handles disconnecting old item + + if (this->mChild) { + QObject::connect( + this->mChild, + &QQuickItem::implicitWidthChanged, + this, + &MarginWrapperManager::onChildImplicitWidthChanged + ); + + QObject::connect( + this->mChild, + &QQuickItem::implicitHeightChanged, + this, + &MarginWrapperManager::onChildImplicitHeightChanged + ); + } + + this->updateGeometry(); +} + +qreal MarginWrapperManager::targetChildWidth() const { + auto max = this->mWrapper->width() - this->mMargin * 2; + + if (this->mResizeChild) return max; + else return std::min(this->mChild->implicitWidth(), max); +} + +qreal MarginWrapperManager::targetChildHeight() const { + auto max = this->mWrapper->height() - this->mMargin * 2; + + if (this->mResizeChild) return max; + else return std::min(this->mChild->implicitHeight(), max); +} + +qreal MarginWrapperManager::targetChildX() const { + if (this->mResizeChild) return this->mMargin; + else return this->mWrapper->width() / 2 - this->mChild->implicitWidth() / 2; +} + +qreal MarginWrapperManager::targetChildY() const { + if (this->mResizeChild) return this->mMargin; + else return this->mWrapper->height() / 2 - this->mChild->implicitHeight() / 2; +} + +void MarginWrapperManager::onWrapperWidthChanged() { + if (!this->mChild || !this->mWrapper) return; + this->mChild->setX(this->targetChildX()); + this->mChild->setWidth(this->targetChildWidth()); +} + +void MarginWrapperManager::onWrapperHeightChanged() { + if (!this->mChild || !this->mWrapper) return; + this->mChild->setY(this->targetChildY()); + this->mChild->setHeight(this->targetChildHeight()); +} + +void MarginWrapperManager::onChildImplicitWidthChanged() { + if (!this->mChild || !this->mWrapper) return; + this->mWrapper->setImplicitWidth(this->mChild->implicitWidth() + this->mMargin * 2); +} + +void MarginWrapperManager::onChildImplicitHeightChanged() { + if (!this->mChild || !this->mWrapper) return; + this->mWrapper->setImplicitHeight(this->mChild->implicitHeight() + this->mMargin * 2); +} + +void MarginWrapperManager::updateGeometry() { + if (!this->mWrapper) return; + + if (this->mChild) { + this->mWrapper->setImplicitWidth(this->mChild->implicitWidth() + this->mMargin * 2); + this->mWrapper->setImplicitHeight(this->mChild->implicitHeight() + this->mMargin * 2); + this->mChild->setX(this->targetChildX()); + this->mChild->setY(this->targetChildY()); + this->mChild->setWidth(this->targetChildWidth()); + this->mChild->setHeight(this->targetChildHeight()); + } else { + this->mWrapper->setImplicitWidth(this->mMargin * 2); + this->mWrapper->setImplicitHeight(this->mMargin * 2); + } +} + +} // namespace qs::widgets diff --git a/src/widgets/marginwrapper.hpp b/src/widgets/marginwrapper.hpp new file mode 100644 index 00000000..7946951a --- /dev/null +++ b/src/widgets/marginwrapper.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include + +#include "wrapper.hpp" + +namespace qs::widgets { + +///! Helper object for applying sizes and margins to a single child item. +/// > [!NOTE] MarginWrapperManager is an extension of @@WrapperManager. +/// > You should read its documentation to understand wrapper types. +/// +/// MarginWrapperManager can be used to apply margins to a child item, +/// in addition to handling the size / implicit size relationship +/// between the parent and the child. @@WrapperItem and @@WrapperRectangle +/// exist for Item and Rectangle implementations respectively. +/// +/// > [!WARNING] MarginWrapperManager based types set the child item's +/// > @@QtQuick.Item.x, @@QtQuick.Item.y, @@QtQuick.Item.width, @@QtQuick.Item.height +/// > or @@QtQuick.Item.anchors properties. Do not set them yourself, +/// > instead set @@Item.implicitWidth and @@Item.implicitHeight. +/// +/// ### Implementing a margin wrapper type +/// Follow the directions in @@WrapperManager$'s documentation, and or +/// alias the @@margin property if you wish to expose it. +class MarginWrapperManager: public WrapperManager { + Q_OBJECT; + // clang-format off + /// The minimum margin between the child item and the parent item's edges. + /// Defaults to 0. + Q_PROPERTY(qreal margin READ margin WRITE setMargin NOTIFY marginChanged FINAL); + /// If the child item should be resized larger than its implicit size if + /// the parent is resized larger than its implicit size. Defaults to false. + Q_PROPERTY(bool resizeChild READ resizeChild WRITE setResizeChild NOTIFY resizeChildChanged FINAL); + // clang-format on + QML_ELEMENT; + +public: + explicit MarginWrapperManager(QObject* parent = nullptr); + + void componentComplete() override; + + [[nodiscard]] qreal margin() const; + void setMargin(qreal margin); + + [[nodiscard]] bool resizeChild() const; + void setResizeChild(bool resizeChild); + +signals: + void marginChanged(); + void resizeChildChanged(); + +private slots: + void onChildChanged(); + void onWrapperWidthChanged(); + void onWrapperHeightChanged(); + void onChildImplicitWidthChanged(); + void onChildImplicitHeightChanged(); + +private: + void updateGeometry(); + + [[nodiscard]] qreal targetChildX() const; + [[nodiscard]] qreal targetChildY() const; + [[nodiscard]] qreal targetChildWidth() const; + [[nodiscard]] qreal targetChildHeight() const; + + qreal mMargin = 0; + bool mResizeChild = false; +}; + +} // namespace qs::widgets diff --git a/src/widgets/module.md b/src/widgets/module.md index c24bb876..77d4a3a5 100644 --- a/src/widgets/module.md +++ b/src/widgets/module.md @@ -1,7 +1,15 @@ name = "Quickshell.Widgets" description = "Bundled widgets" + +headers = [ + "wrapper.hpp", + "marginwrapper.hpp", +] + qml_files = [ "IconImage.qml", "ClippingRectangle.qml", + "WrapperItem.qml", + "WrapperRectangle.qml", ] ----- diff --git a/src/widgets/wrapper.cpp b/src/widgets/wrapper.cpp new file mode 100644 index 00000000..4e502cee --- /dev/null +++ b/src/widgets/wrapper.cpp @@ -0,0 +1,127 @@ +#include "wrapper.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace qs::widgets { + +void WrapperManager::componentComplete() { + this->mWrapper = qobject_cast(this->parent()); + + if (!this->mWrapper) { + QString pstr; + QDebug(&pstr) << this->parent(); + + qmlWarning(this) << "Parent of WrapperManager is not a QQuickItem. Parent: " << pstr; + return; + } + + QQuickItem* child = this->mChild; + this->mChild = nullptr; // avoids checks for the old item in setChild. + + const auto& childItems = this->mWrapper->childItems(); + + if (childItems.length() == 1) { + this->mDefaultChild = childItems.first(); + } else if (childItems.length() != 0) { + this->flags.setFlag(WrapperManager::HasMultipleChildren); + + if (!child && !this->flags.testFlags(WrapperManager::NullChild)) { + this->printChildCountWarning(); + } + } + + for (auto* item: childItems) { + if (item != child) item->setParentItem(nullptr); + } + + if (child && !this->flags.testFlag(WrapperManager::NullChild)) { + this->setChild(child); + } +} + +QQuickItem* WrapperManager::child() const { return this->mChild; } + +void WrapperManager::setChild(QQuickItem* child) { + if (child && child == this->mChild) return; + + if (this->mChild != nullptr) { + QObject::disconnect(this->mChild, nullptr, this, nullptr); + + if (this->mChild->parentItem() == this->mWrapper) { + this->mChild->setParentItem(nullptr); + } + } + + this->mChild = child; + this->flags.setFlag(WrapperManager::NullChild, child == nullptr); + + if (child) { + QObject::connect( + child, + &QObject::destroyed, + this, + &WrapperManager::onChildDestroyed, + Qt::UniqueConnection + ); + + if (auto* wrapper = this->mWrapper) { + child->setParentItem(wrapper); + } + } + + emit this->initializedChildChanged(); + emit this->childChanged(); +} + +void WrapperManager::setProspectiveChild(QQuickItem* child) { + if (child && child == this->mChild) return; + + if (!this->mWrapper) { + if (this->mChild) { + QObject::disconnect(this->mChild, nullptr, this, nullptr); + } + + this->mChild = child; + this->flags.setFlag(WrapperManager::NullChild, child == nullptr); + + if (child) { + QObject::connect(child, &QObject::destroyed, this, &WrapperManager::onChildDestroyed); + } + } else { + this->setChild(child); + } +} + +void WrapperManager::unsetChild() { + if (!this->mWrapper) { + this->setProspectiveChild(nullptr); + } else { + this->setChild(this->mDefaultChild); + + if (!this->mDefaultChild && this->flags.testFlag(WrapperManager::HasMultipleChildren)) { + this->printChildCountWarning(); + } + } + + this->flags.setFlag(WrapperManager::NullChild, false); +} + +void WrapperManager::onChildDestroyed() { + this->mChild = nullptr; + this->unsetChild(); + emit this->childChanged(); +} + +void WrapperManager::printChildCountWarning() const { + qmlWarning(this->mWrapper) << "Wrapper component cannot have more than one visual child."; + qmlWarning(this->mWrapper) << "Remove all additional children, or pick a specific component " + "to wrap using the child property."; +} + +} // namespace qs::widgets diff --git a/src/widgets/wrapper.hpp b/src/widgets/wrapper.hpp new file mode 100644 index 00000000..95b3adea --- /dev/null +++ b/src/widgets/wrapper.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/doc.hpp" + +namespace qs::widgets { + +///! Helper object for creating components with a single visual child. +/// WrapperManager determines which child of an Item should be its visual +/// child, and exposes it for further operations. See @@MarginWrapperManager +/// for a subclass that implements automatic sizing and margins. +/// +/// ### Using wrapper types +/// WrapperManager based types have a single visual child item. +/// You can specify the child item using the default property, or by +/// setting the @@child property. You must use the @@child property if +/// the widget has more than one @@QtQuick.Item based child. +/// +/// #### Example using the default property +/// ```qml +/// WrapperWidget { // a widget that uses WrapperManager +/// // Putting the item inline uses the default property of WrapperWidget. +/// @@QtQuick.Text { text: "Hello" } +/// +/// // Scope does not extend Item, so it can be placed in the +/// // default property without issue. +/// @@Quickshell.Scope {} +/// } +/// ``` +/// +/// #### Example using the child property +/// ```qml +/// WrapperWidget { +/// @@QtQuick.Text { +/// id: text +/// text: "Hello" +/// } +/// +/// @@QtQuick.Text { +/// id: otherText +/// text: "Other Text" +/// } +/// +/// // Both text and otherText extend Item, so one must be specified. +/// child: text +/// } +/// ``` +/// +/// See @@child for more details on how the child property can be used. +/// +/// ### Implementing wrapper types +/// In addition to the bundled wrapper types, you can make your own using +/// WrapperManager. To implement a wrapper, create a WrapperManager inside +/// your wrapper component 's default property, then alias a new property +/// to the WrapperManager's @@child property. +/// +/// #### Example +/// ```qml +/// Item { // your wrapper component +/// WrapperManager { id: wrapperManager } +/// +/// // Allows consumers of your wrapper component to use the child property. +/// property alias child: wrapperManager.child +/// +/// // The rest of your component logic. You can use +/// // `wrapperManager.child` or `this.child` to refer to the selected child. +/// } +/// ``` +/// +/// ### See also +/// - @@WrapperItem - A @@MarginWrapperManager based component that sizes itself +/// to its child. +/// - @@WrapperRectangle - A @@MarginWrapperManager based component that sizes +/// itself to its child, and provides an option to use its border as an inset. +class WrapperManager + : public QObject + , public QQmlParserStatus { + Q_OBJECT; + // clang-format off + /// The wrapper component's selected child. + /// + /// Setting this property override's WrapperManager's default selection, + /// and resolve ambiguity when more than one visual child is present. + /// The property can additionally be defined inline or reference a component + /// that is not already a child of the wrapper, in which case it will be + /// reparented to the wrapper. Setting child to `null` will select no child, + /// and `undefined` will restore the default child. + /// + /// When read, `child` will always return the (potentially null) selected child, + /// and not `undefined`. + Q_PROPERTY(QQuickItem* child READ child WRITE setProspectiveChild RESET unsetChild NOTIFY childChanged FINAL); + // clang-format on + QML_ELEMENT; + +public: + explicit WrapperManager(QObject* parent = nullptr): QObject(parent) {} + + void classBegin() override {} + void componentComplete() override; + + [[nodiscard]] QQuickItem* child() const; + void setChild(QQuickItem* child); + void setProspectiveChild(QQuickItem* child); + void unsetChild(); + +signals: + void childChanged(); + QSDOC_HIDE void initializedChildChanged(); + +private slots: + void onChildDestroyed(); + +protected: + enum Flag : quint8 { + NoFlags = 0x0, + NullChild = 0x1, + HasMultipleChildren = 0x2, + }; + Q_DECLARE_FLAGS(Flags, Flag); + + void printChildCountWarning() const; + void updateGeometry(); + + QQuickItem* mWrapper = nullptr; + QPointer mDefaultChild; + QQuickItem* mChild = nullptr; + Flags flags; +}; + +} // namespace qs::widgets From 033e8108716bb348b97574c5a978ba715fd4edd4 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 02:52:49 -0800 Subject: [PATCH 212/305] widgets: add ClippingWrapperRectangle --- src/widgets/CMakeLists.txt | 1 + src/widgets/ClippingWrapperRectangle.qml | 35 ++++++++++++++++++++++++ src/widgets/WrapperItem.qml | 1 - src/widgets/WrapperRectangle.qml | 4 +-- src/widgets/module.md | 1 + src/widgets/wrapper.cpp | 17 +++++++++++- src/widgets/wrapper.hpp | 8 ++++++ 7 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 src/widgets/ClippingWrapperRectangle.qml diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index 3f8de41f..29e760a3 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -12,6 +12,7 @@ qt_add_qml_module(quickshell-widgets ClippingRectangle.qml WrapperItem.qml WrapperRectangle.qml + ClippingWrapperRectangle.qml ) qt6_add_shaders(quickshell-widgets "widgets-cliprect" diff --git a/src/widgets/ClippingWrapperRectangle.qml b/src/widgets/ClippingWrapperRectangle.qml new file mode 100644 index 00000000..26014a68 --- /dev/null +++ b/src/widgets/ClippingWrapperRectangle.qml @@ -0,0 +1,35 @@ +import QtQuick + +///! ClippingRectangle that handles sizes and positioning for a single visual child. +/// This component is useful for adding a clipping border or background rectangle to +/// a child item. If you don't need clipping, use @@WrapperRectangle. +/// +/// > [!NOTE] ClippingWrapperRectangle is a @@MarginWrapperManager based component. +/// > You should read its documentation as well. +/// +/// > [!WARNING] You should not set @@Item.x, @@Item.y, @@Item.width, +/// > @@Item.height or @@Item.anchors on the child item, as they are used +/// > by WrapperItem to position it. Instead set @@Item.implicitWidth and +/// > @@Item.implicitHeight. +ClippingRectangle { + id: root + + /// The minimum margin between the child item and the ClippingWrapperRectangle's + /// edges. Defaults to 0. + property /*real*/alias margin: manager.margin + /// If the child item should be resized larger than its implicit size if + /// the WrapperRectangle is resized larger than its implicit size. Defaults to false. + property /*bool*/alias resizeChild: manager.resizeChild + /// See @@WrapperManager.child for details. + property alias child: manager.child + + implicitWidth: root.contentItem.implicitWidth + (root.contentInsideBorder ? root.border.width * 2 : 0) + implicitHeight: root.contentItem.implicitHeight + (root.contentInsideBorder ? root.border.width * 2 : 0) + + resources: [ + MarginWrapperManager { + id: manager + wrapper: root.contentItem + } + ] +} diff --git a/src/widgets/WrapperItem.qml b/src/widgets/WrapperItem.qml index dfa7c0fd..e1701cac 100644 --- a/src/widgets/WrapperItem.qml +++ b/src/widgets/WrapperItem.qml @@ -1,5 +1,4 @@ import QtQuick -import Quickshell.Widgets ///! Item that handles sizes and positioning for a single visual child. /// This component is useful when you need to wrap a single component in diff --git a/src/widgets/WrapperRectangle.qml b/src/widgets/WrapperRectangle.qml index c198c47b..e1c2c833 100644 --- a/src/widgets/WrapperRectangle.qml +++ b/src/widgets/WrapperRectangle.qml @@ -1,9 +1,9 @@ import QtQuick -import Quickshell.Widgets ///! Rectangle that handles sizes and positioning for a single visual child. /// This component is useful for adding a border or background rectangle to -/// a child item. +/// a child item. If you need to clip the child item to the rectangle's +/// border, see @@ClippingWrapperRectangle. /// /// > [!NOTE] WrapperRectangle is a @@MarginWrapperManager based component. /// > You should read its documentation as well. diff --git a/src/widgets/module.md b/src/widgets/module.md index 77d4a3a5..4009b790 100644 --- a/src/widgets/module.md +++ b/src/widgets/module.md @@ -11,5 +11,6 @@ qml_files = [ "ClippingRectangle.qml", "WrapperItem.qml", "WrapperRectangle.qml", + "ClippingWrapperRectangle.qml", ] ----- diff --git a/src/widgets/wrapper.cpp b/src/widgets/wrapper.cpp index 4e502cee..40d7755d 100644 --- a/src/widgets/wrapper.cpp +++ b/src/widgets/wrapper.cpp @@ -11,7 +11,11 @@ namespace qs::widgets { void WrapperManager::componentComplete() { - this->mWrapper = qobject_cast(this->parent()); + if (this->mAssignedWrapper) { + this->mWrapper = this->mAssignedWrapper; + } else { + this->mWrapper = qobject_cast(this->parent()); + } if (!this->mWrapper) { QString pstr; @@ -118,6 +122,17 @@ void WrapperManager::onChildDestroyed() { emit this->childChanged(); } +QQuickItem* WrapperManager::wrapper() const { return this->mWrapper; } + +void WrapperManager::setWrapper(QQuickItem* wrapper) { + if (this->mWrapper) { + qmlWarning(this) << "Cannot set wrapper after WrapperManager initialization."; + return; + } + + this->mAssignedWrapper = wrapper; +} + void WrapperManager::printChildCountWarning() const { qmlWarning(this->mWrapper) << "Wrapper component cannot have more than one visual child."; qmlWarning(this->mWrapper) << "Remove all additional children, or pick a specific component " diff --git a/src/widgets/wrapper.hpp b/src/widgets/wrapper.hpp index 95b3adea..993cfd51 100644 --- a/src/widgets/wrapper.hpp +++ b/src/widgets/wrapper.hpp @@ -98,6 +98,9 @@ class WrapperManager /// When read, `child` will always return the (potentially null) selected child, /// and not `undefined`. Q_PROPERTY(QQuickItem* child READ child WRITE setProspectiveChild RESET unsetChild NOTIFY childChanged FINAL); + /// The wrapper managed by this manager. Defaults to the manager's parent. + /// 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; @@ -112,8 +115,12 @@ public: void setProspectiveChild(QQuickItem* child); void unsetChild(); + [[nodiscard]] QQuickItem* wrapper() const; + void setWrapper(QQuickItem* wrapper); + signals: void childChanged(); + void wrapperChanged(); QSDOC_HIDE void initializedChildChanged(); private slots: @@ -131,6 +138,7 @@ protected: void updateGeometry(); QQuickItem* mWrapper = nullptr; + QQuickItem* mAssignedWrapper = nullptr; QPointer mDefaultChild; QQuickItem* mChild = nullptr; Flags flags; From ee93306312f0a253ab6bf2af4f8b140fb0772c66 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 02:57:04 -0800 Subject: [PATCH 213/305] widgets/wrapper: fix margin wrapper reactvity and margins Fixed reactivity of the paren't actual size not working before child had been assigned. Fixed incorrect margins when actual size is less than implicit size. --- src/widgets/marginwrapper.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/widgets/marginwrapper.cpp b/src/widgets/marginwrapper.cpp index b960a9f7..92de2935 100644 --- a/src/widgets/marginwrapper.cpp +++ b/src/widgets/marginwrapper.cpp @@ -20,6 +20,8 @@ MarginWrapperManager::MarginWrapperManager(QObject* parent): WrapperManager(pare } void MarginWrapperManager::componentComplete() { + this->WrapperManager::componentComplete(); + if (this->mWrapper) { QObject::connect( this->mWrapper, @@ -36,8 +38,6 @@ void MarginWrapperManager::componentComplete() { ); } - this->WrapperManager::componentComplete(); - if (!this->mChild) this->updateGeometry(); } @@ -97,12 +97,19 @@ qreal MarginWrapperManager::targetChildHeight() const { qreal MarginWrapperManager::targetChildX() const { if (this->mResizeChild) return this->mMargin; - else return this->mWrapper->width() / 2 - this->mChild->implicitWidth() / 2; + else { + return std::max(this->mMargin, this->mWrapper->width() / 2 - this->mChild->implicitWidth() / 2); + } } qreal MarginWrapperManager::targetChildY() const { if (this->mResizeChild) return this->mMargin; - else return this->mWrapper->height() / 2 - this->mChild->implicitHeight() / 2; + else { + return std::max( + this->mMargin, + this->mWrapper->height() / 2 - this->mChild->implicitHeight() / 2 + ); + } } void MarginWrapperManager::onWrapperWidthChanged() { From f4066cb4edd96b0152ab9b2b1bec821cd5c9da57 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 03:29:31 -0800 Subject: [PATCH 214/305] core/popupanchor: add anchoring signal for last second repositioning --- src/core/popupanchor.cpp | 2 ++ src/core/popupanchor.hpp | 19 +++++++++++++++++-- src/wayland/popupanchor.cpp | 3 +++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index 0dc9c4a4..aa570b3a 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -151,6 +151,8 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only if (onlyIfDirty && !anchor->isDirty()) return; anchor->markClean(); + emit anchor->anchoring(); + auto adjustment = anchor->adjustment(); auto screenGeometry = parentWindow->screen()->geometry(); auto anchorRectGeometry = anchor->rect().qrect().translated(parentGeometry.topLeft()); diff --git a/src/core/popupanchor.hpp b/src/core/popupanchor.hpp index a0f6353c..f9a4b997 100644 --- a/src/core/popupanchor.hpp +++ b/src/core/popupanchor.hpp @@ -77,9 +77,18 @@ class PopupAnchor: public QObject { /// determined by the @@edges, @@gravity, and @@adjustment. /// /// If you leave @@edges, @@gravity and @@adjustment at their default values, - /// setting more than `x` and `y` does not matter. + /// setting more than `x` and `y` does not matter. The anchor rect cannot + /// be smaller than 1x1 pixels. /// - /// > [!INFO] The anchor rect cannot be smaller than 1x1 pixels. + /// > [!INFO] To position a popup relative to an item inside a window, + /// > you can use [coordinate mapping functions] (note the warning below). + /// + /// > [!WARNING] Using [coordinate mapping functions] in a binding to + /// > this property will position the anchor incorrectly. + /// > If you want to use them, do so in @@anchoring(s), or use + /// > @@TransformWatcher if you need real-time updates to mapped coordinates. + /// + /// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method Q_PROPERTY(Box rect READ rect WRITE setRect NOTIFY rectChanged); /// The point on the anchor rectangle the popup should anchor to. /// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed. @@ -127,6 +136,12 @@ public: void updatePlacement(const QPoint& anchorpoint, const QSize& size); signals: + /// Emitted when this anchor is about to be used. Mostly useful for modifying + /// the anchor @@rect using [coordinate mapping functions], which are not reactive. + /// + /// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method + void anchoring(); + void windowChanged(); QSDOC_HIDE void backingWindowVisibilityChanged(); void rectChanged(); diff --git a/src/wayland/popupanchor.cpp b/src/wayland/popupanchor.cpp index e38eeff0..b13fb480 100644 --- a/src/wayland/popupanchor.cpp +++ b/src/wayland/popupanchor.cpp @@ -41,6 +41,8 @@ void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bo positioner.set_constraint_adjustment(anchor->adjustment().toInt()); + emit anchor->anchoring(); + auto anchorRect = anchor->rect(); if (auto* p = window->transientParent()) { @@ -101,6 +103,7 @@ void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bo bool WaylandPopupPositioner::shouldRepositionOnMove() const { return true; } void WaylandPopupPositioner::setFlags(PopupAnchor* anchor, QWindow* window) { + emit anchor->anchoring(); auto anchorRect = anchor->rect(); if (auto* p = window->transientParent()) { From 66b494d760b0f99d51b477e61fd6498bbbc68b70 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 13:58:34 -0800 Subject: [PATCH 215/305] build: add qs_add_link_dependencies Further inspection as to what libraries actually require which others will be required before this can be used as a hint for shared builds. --- cmake/util.cmake | 9 +++++++++ src/CMakeLists.txt | 2 +- src/crash/CMakeLists.txt | 2 +- src/dbus/dbusmenu/CMakeLists.txt | 3 ++- src/services/mpris/CMakeLists.txt | 1 + src/services/upower/CMakeLists.txt | 3 ++- 6 files changed, 16 insertions(+), 4 deletions(-) diff --git a/cmake/util.cmake b/cmake/util.cmake index 5d261a40..14fa7c2d 100644 --- a/cmake/util.cmake +++ b/cmake/util.cmake @@ -1,3 +1,12 @@ +# Adds a dependency hint to the link order, but does not block build on the dependency. +function (qs_add_link_dependencies target) + set_property( + TARGET ${target} + APPEND PROPERTY INTERFACE_LINK_LIBRARIES + ${ARGN} + ) +endfunction() + function (qs_append_qmldir target text) get_property(qmldir_content TARGET ${target} PROPERTY _qt_internal_qmldir_content) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c518a1c9..707bc37c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,8 +2,8 @@ qt_add_executable(quickshell main.cpp) install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) -add_subdirectory(launch) add_subdirectory(build) +add_subdirectory(launch) add_subdirectory(core) add_subdirectory(ipc) add_subdirectory(window) diff --git a/src/crash/CMakeLists.txt b/src/crash/CMakeLists.txt index 442859af..7fdd8305 100644 --- a/src/crash/CMakeLists.txt +++ b/src/crash/CMakeLists.txt @@ -14,4 +14,4 @@ target_link_libraries(quickshell-crash PRIVATE PkgConfig::breakpad -lbreakpad_cl # quick linked for pch compat target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt::Quick Qt::Widgets) -target_link_libraries(quickshell-core PRIVATE quickshell-crash) +target_link_libraries(quickshell PRIVATE quickshell-crash) diff --git a/src/dbus/dbusmenu/CMakeLists.txt b/src/dbus/dbusmenu/CMakeLists.txt index ac50b28a..61cee42c 100644 --- a/src/dbus/dbusmenu/CMakeLists.txt +++ b/src/dbus/dbusmenu/CMakeLists.txt @@ -27,7 +27,8 @@ install_qml_module(quickshell-dbusmenu) # dbus headers target_include_directories(quickshell-dbusmenu PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) -target_link_libraries(quickshell-dbusmenu PRIVATE Qt::Quick Qt::DBus quickshell-dbus) +target_link_libraries(quickshell-dbusmenu PRIVATE Qt::Quick Qt::DBus) +qs_add_link_dependencies(quickshell-dbusmenu quickshell-dbus) qs_module_pch(quickshell-dbusmenu SET dbus) diff --git a/src/services/mpris/CMakeLists.txt b/src/services/mpris/CMakeLists.txt index 122a0c5c..4440c0a7 100644 --- a/src/services/mpris/CMakeLists.txt +++ b/src/services/mpris/CMakeLists.txt @@ -38,6 +38,7 @@ qs_add_module_deps_light(quickshell-service-mpris Quickshell) install_qml_module(quickshell-service-mpris) target_link_libraries(quickshell-service-mpris PRIVATE Qt::Qml Qt::DBus) +qs_add_link_dependencies(quickshell-service-mpris quickshell-dbus) qs_module_pch(quickshell-service-mpris SET dbus) diff --git a/src/services/upower/CMakeLists.txt b/src/services/upower/CMakeLists.txt index fd0da2af..ca87f6ae 100644 --- a/src/services/upower/CMakeLists.txt +++ b/src/services/upower/CMakeLists.txt @@ -37,7 +37,8 @@ qs_add_module_deps_light(quickshell-service-upower Quickshell) install_qml_module(quickshell-service-upower) -target_link_libraries(quickshell-service-upower PRIVATE Qt::Qml Qt::DBus quickshell-dbus) +target_link_libraries(quickshell-service-upower PRIVATE Qt::Qml Qt::DBus) +qs_add_link_dependencies(quickshell-service-upower quickshell-dbus) target_link_libraries(quickshell PRIVATE quickshell-service-upowerplugin) qs_module_pch(quickshell-service-upower SET dbus) From 6ceee06884eace853c9179764d8b8677e3cba2ee Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 15:25:42 -0800 Subject: [PATCH 216/305] debug: add lint for zero sized items --- src/CMakeLists.txt | 1 + src/debug/CMakeLists.txt | 7 ++++ src/debug/lint.cpp | 81 ++++++++++++++++++++++++++++++++++++++ src/debug/lint.hpp | 11 ++++++ src/window/CMakeLists.txt | 2 + src/window/proxywindow.cpp | 6 +++ src/window/proxywindow.hpp | 1 + 7 files changed, 109 insertions(+) create mode 100644 src/debug/CMakeLists.txt create mode 100644 src/debug/lint.cpp create mode 100644 src/debug/lint.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 707bc37c..882d2bae 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,7 @@ install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) add_subdirectory(build) add_subdirectory(launch) add_subdirectory(core) +add_subdirectory(debug) add_subdirectory(ipc) add_subdirectory(window) add_subdirectory(io) diff --git a/src/debug/CMakeLists.txt b/src/debug/CMakeLists.txt new file mode 100644 index 00000000..55da4fcb --- /dev/null +++ b/src/debug/CMakeLists.txt @@ -0,0 +1,7 @@ +qt_add_library(quickshell-debug STATIC + lint.cpp +) + +qs_pch(quickshell-debug) +target_link_libraries(quickshell-debug PRIVATE Qt::Quick) +target_link_libraries(quickshell PRIVATE quickshell-debug) diff --git a/src/debug/lint.cpp b/src/debug/lint.cpp new file mode 100644 index 00000000..1ec0086f --- /dev/null +++ b/src/debug/lint.cpp @@ -0,0 +1,81 @@ +#include "lint.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qs::debug { + +Q_LOGGING_CATEGORY(logLint, "quickshell.linter", QtWarningMsg); + +void lintZeroSized(QQuickItem* item); +bool isRenderable(QQuickItem* item); + +void lintObjectTree(QObject* object) { + if (!logLint().isWarningEnabled()) return; + + qCDebug(logLint) << "Walking children of object" << object; + + for (auto* child: object->children()) { + if (child->isQuickItemType()) { + auto* item = static_cast(child); // NOLINT; + lintItemTree(item); + } else { + lintObjectTree(child); + } + } +} + +void lintItemTree(QQuickItem* item) { + if (!logLint().isWarningEnabled()) return; + + qCDebug(logLint) << "Running lints for item" << item; + lintZeroSized(item); + + qCDebug(logLint) << "Walking visual children of item" << item; + for (auto* child: item->childItems()) { + lintItemTree(child); + } +} + +void lintZeroSized(QQuickItem* item) { + if (!item->isEnabled() || !item->isVisible()) return; + if (item->childItems().isEmpty()) return; + + auto zeroWidth = item->width() == 0; + auto zeroHeight = item->height() == 0; + + if (!zeroWidth && !zeroHeight) return; + + if (!isRenderable(item)) return; + + auto* ctx = QQmlEngine::contextForObject(item); + if (!ctx || ctx->baseUrl().scheme() != QStringLiteral("qsintercept")) return; + + qmlWarning(item) << "Item is visible and has visible children, but has zero " + << (zeroWidth && zeroHeight ? "width and height" + : zeroWidth ? "width" + : "height"); +} + +bool isRenderable(QQuickItem* item) { + if (!item->isEnabled() || !item->isVisible()) return false; + + if (item->flags().testFlags(QQuickItem::ItemHasContents)) { + return true; + } + + return std::ranges::any_of(item->childItems(), [](auto* item) { return isRenderable(item); }); +} + +} // namespace qs::debug diff --git a/src/debug/lint.hpp b/src/debug/lint.hpp new file mode 100644 index 00000000..5b5420ea --- /dev/null +++ b/src/debug/lint.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +namespace qs::debug { + +void lintObjectTree(QObject* object); +void lintItemTree(QQuickItem* item); + +} // namespace qs::debug diff --git a/src/window/CMakeLists.txt b/src/window/CMakeLists.txt index 89b2233e..47b546d4 100644 --- a/src/window/CMakeLists.txt +++ b/src/window/CMakeLists.txt @@ -22,6 +22,8 @@ target_link_libraries(quickshell-window PRIVATE Qt::Core Qt::Gui Qt::Quick Qt6::QuickPrivate ) +qs_add_link_dependencies(quickshell-window quickshell-debug) + target_link_libraries(quickshell-window-init PRIVATE Qt::Qml) qs_module_pch(quickshell-window SET large) diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 3d01224d..426b4057 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -19,6 +19,7 @@ #include "../core/qmlscreen.hpp" #include "../core/region.hpp" #include "../core/reload.hpp" +#include "../debug/lint.hpp" #include "windowinterface.hpp" ProxyWindowBase::ProxyWindowBase(QObject* parent) @@ -214,6 +215,11 @@ void ProxyWindowBase::polishItems() { // This hack manually polishes the item tree right before showing the window so it will // always be created with the correct size. QQuickWindowPrivate::get(this->window)->polishItems(); + + if (!this->ranLints) { + qs::debug::lintItemTree(this->mContentItem); + this->ranLints = true; + } } qint32 ProxyWindowBase::x() const { diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index 79d326e3..8ab8bfd0 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -130,6 +130,7 @@ protected: QQuickWindow* window = nullptr; QQuickItem* mContentItem = nullptr; bool reloadComplete = false; + bool ranLints = false; private: void polishItems(); From eb5a5b8b6795cda71d4a06921b64ba300c185473 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 15:58:55 -0800 Subject: [PATCH 217/305] debug: run lints after window expose Ensures items are at their final sizes before checking them, fixing some false positives. --- src/wayland/wlr_layershell.cpp | 6 +++--- src/wayland/wlr_layershell.hpp | 4 ++-- src/window/proxywindow.cpp | 15 ++++++++++++--- src/window/proxywindow.hpp | 25 +++++++++++++++++++++---- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/wayland/wlr_layershell.cpp b/src/wayland/wlr_layershell.cpp index 1ce7b7fc..9b4f32f2 100644 --- a/src/wayland/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell.cpp @@ -18,7 +18,7 @@ WlrLayershell::WlrLayershell(QObject* parent) : ProxyWindowBase(parent) , ext(new LayershellWindowExtension(this)) {} -QQuickWindow* WlrLayershell::retrieveWindow(QObject* oldInstance) { +ProxiedWindow* WlrLayershell::retrieveWindow(QObject* oldInstance) { auto* old = qobject_cast(oldInstance); auto* window = old == nullptr ? nullptr : old->disownWindow(); @@ -33,8 +33,8 @@ QQuickWindow* WlrLayershell::retrieveWindow(QObject* oldInstance) { return this->createQQuickWindow(); } -QQuickWindow* WlrLayershell::createQQuickWindow() { - auto* window = new QQuickWindow(); +ProxiedWindow* WlrLayershell::createQQuickWindow() { + auto* window = this->ProxyWindowBase::createQQuickWindow(); if (!this->ext->attach(window)) { qWarning() << "Could not attach Layershell extension to new QQuickWindow. Layer will not " diff --git a/src/wayland/wlr_layershell.hpp b/src/wayland/wlr_layershell.hpp index e7a1a077..f6f6988a 100644 --- a/src/wayland/wlr_layershell.hpp +++ b/src/wayland/wlr_layershell.hpp @@ -64,8 +64,8 @@ class WlrLayershell: public ProxyWindowBase { public: explicit WlrLayershell(QObject* parent = nullptr); - QQuickWindow* retrieveWindow(QObject* oldInstance) override; - QQuickWindow* createQQuickWindow() override; + ProxiedWindow* retrieveWindow(QObject* oldInstance) override; + ProxiedWindow* createQQuickWindow() override; void connectWindow() override; [[nodiscard]] bool deleteOnInvisible() const override; diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 426b4057..2a1f51d9 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -1,6 +1,7 @@ #include "proxywindow.hpp" #include +#include #include #include #include @@ -80,7 +81,7 @@ void ProxyWindowBase::onReload(QObject* oldInstance) { void ProxyWindowBase::postCompleteWindow() { this->setVisible(this->mVisible); } -QQuickWindow* ProxyWindowBase::createQQuickWindow() { return new QQuickWindow(); } +ProxiedWindow* ProxyWindowBase::createQQuickWindow() { return new ProxiedWindow(); } void ProxyWindowBase::createWindow() { if (this->window != nullptr) return; @@ -102,7 +103,7 @@ void ProxyWindowBase::deleteWindow(bool keepItemOwnership) { } } -QQuickWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) { +ProxiedWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) { if (this->window == nullptr) return nullptr; QObject::disconnect(this->window, nullptr, this, nullptr); @@ -116,7 +117,7 @@ QQuickWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) { return window; } -QQuickWindow* ProxyWindowBase::retrieveWindow(QObject* oldInstance) { +ProxiedWindow* ProxyWindowBase::retrieveWindow(QObject* oldInstance) { auto* old = qobject_cast(oldInstance); return old == nullptr ? nullptr : old->disownWindow(); } @@ -136,6 +137,7 @@ void ProxyWindowBase::connectWindow() { QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyWindowBase::heightChanged); QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyWindowBase::screenChanged); QObject::connect(this->window, &QQuickWindow::colorChanged, this, &ProxyWindowBase::colorChanged); + QObject::connect(this->window, &ProxiedWindow::exposed, this, &ProxyWindowBase::onWindowExposeEvent); // clang-format on } @@ -215,7 +217,9 @@ void ProxyWindowBase::polishItems() { // This hack manually polishes the item tree right before showing the window so it will // always be created with the correct size. QQuickWindowPrivate::get(this->window)->polishItems(); +} +void ProxyWindowBase::onWindowExposeEvent() { if (!this->ranLints) { qs::debug::lintItemTree(this->mContentItem); this->ranLints = true; @@ -368,3 +372,8 @@ void ProxyWindowBase::onHeightChanged() { this->mContentItem->setHeight(this->he QObject* ProxyWindowAttached::window() const { return this->mWindow; } QQuickItem* ProxyWindowAttached::contentItem() const { return this->mWindow->contentItem(); } + +void ProxiedWindow::exposeEvent(QExposeEvent* event) { + this->QQuickWindow::exposeEvent(event); + emit this->exposed(); +} diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index 8ab8bfd0..769dad03 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -11,12 +11,15 @@ #include #include #include +#include #include "../core/qmlscreen.hpp" #include "../core/region.hpp" #include "../core/reload.hpp" #include "windowinterface.hpp" +class ProxiedWindow; + // Proxy to an actual window exposing a limited property set with the ability to // transfer it to a new window. @@ -60,10 +63,10 @@ public: void deleteWindow(bool keepItemOwnership = false); // Disown the backing window and delete all its children. - virtual QQuickWindow* disownWindow(bool keepItemOwnership = false); + virtual ProxiedWindow* disownWindow(bool keepItemOwnership = false); - virtual QQuickWindow* retrieveWindow(QObject* oldInstance); - virtual QQuickWindow* createQQuickWindow(); + virtual ProxiedWindow* retrieveWindow(QObject* oldInstance); + virtual ProxiedWindow* createQQuickWindow(); virtual void connectWindow(); virtual void completeWindow(); virtual void postCompleteWindow(); @@ -119,6 +122,7 @@ protected slots: void onMaskChanged(); void onMaskDestroyed(); void onScreenDestroyed(); + void onWindowExposeEvent(); protected: bool mVisible = true; @@ -127,7 +131,7 @@ protected: QScreen* mScreen = nullptr; QColor mColor = Qt::white; PendingRegion* mMask = nullptr; - QQuickWindow* window = nullptr; + ProxiedWindow* window = nullptr; QQuickItem* mContentItem = nullptr; bool reloadComplete = false; bool ranLints = false; @@ -151,3 +155,16 @@ public: private: ProxyWindowBase* mWindow; }; + +class ProxiedWindow: public QQuickWindow { + Q_OBJECT; + +public: + explicit ProxiedWindow(QWindow* parent = nullptr): QQuickWindow(parent) {} + +signals: + void exposed(); + +protected: + void exposeEvent(QExposeEvent* event) override; +}; From dbaaf55eb62bc8cea6c7cf327486fd1476a56931 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 17:20:53 -0800 Subject: [PATCH 218/305] core/popupwindow: remove parentWindow deprecation message Was being falsely triggered by lints. --- src/debug/lint.cpp | 3 --- src/wayland/popupanchor.cpp | 1 + src/window/popupwindow.cpp | 15 +++++++-------- src/window/popupwindow.hpp | 6 +++--- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/debug/lint.cpp b/src/debug/lint.cpp index 1ec0086f..60d8224f 100644 --- a/src/debug/lint.cpp +++ b/src/debug/lint.cpp @@ -7,10 +7,7 @@ #include #include #include -#include #include -#include -#include #include #include diff --git a/src/wayland/popupanchor.cpp b/src/wayland/popupanchor.cpp index b13fb480..81c1cb1d 100644 --- a/src/wayland/popupanchor.cpp +++ b/src/wayland/popupanchor.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "../core/popupanchor.hpp" #include "../core/types.hpp" diff --git a/src/window/popupwindow.cpp b/src/window/popupwindow.cpp index b355238e..df68474f 100644 --- a/src/window/popupwindow.cpp +++ b/src/window/popupwindow.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include @@ -38,12 +38,11 @@ void ProxyPopupWindow::completeWindow() { void ProxyPopupWindow::postCompleteWindow() { this->updateTransientParent(); } void ProxyPopupWindow::setParentWindow(QObject* parent) { - qWarning() << "PopupWindow.parentWindow is deprecated. Use PopupWindow.anchor.window."; + qmlWarning(this) << "PopupWindow.parentWindow is deprecated. Use PopupWindow.anchor.window."; this->mAnchor.setWindow(parent); } QObject* ProxyPopupWindow::parentWindow() const { - qWarning() << "PopupWindow.parentWindow is deprecated. Use PopupWindow.anchor.window."; return this->mAnchor.window(); } @@ -71,7 +70,7 @@ void ProxyPopupWindow::updateTransientParent() { void ProxyPopupWindow::onParentUpdated() { this->updateTransientParent(); } void ProxyPopupWindow::setScreen(QuickshellScreenInfo* /*unused*/) { - qWarning() << "Cannot set screen of popup window, as that is controlled by the parent window"; + qmlWarning(this) << "Cannot set screen of popup window, as that is controlled by the parent window"; } void ProxyPopupWindow::setVisible(bool visible) { @@ -101,7 +100,7 @@ void ProxyPopupWindow::onVisibleChanged() { } void ProxyPopupWindow::setRelativeX(qint32 x) { - qWarning() << "PopupWindow.relativeX is deprecated. Use PopupWindow.anchor.rect.x."; + qmlWarning(this) << "PopupWindow.relativeX is deprecated. Use PopupWindow.anchor.rect.x."; auto rect = this->mAnchor.rect(); if (x == rect.x) return; rect.x = x; @@ -109,12 +108,12 @@ void ProxyPopupWindow::setRelativeX(qint32 x) { } qint32 ProxyPopupWindow::relativeX() const { - qWarning() << "PopupWindow.relativeX is deprecated. Use PopupWindow.anchor.rect.x."; + qmlWarning(this) << "PopupWindow.relativeX is deprecated. Use PopupWindow.anchor.rect.x."; return this->mAnchor.rect().x; } void ProxyPopupWindow::setRelativeY(qint32 y) { - qWarning() << "PopupWindow.relativeY is deprecated. Use PopupWindow.anchor.rect.y."; + qmlWarning(this) << "PopupWindow.relativeY is deprecated. Use PopupWindow.anchor.rect.y."; auto rect = this->mAnchor.rect(); if (y == rect.y) return; rect.y = y; @@ -122,7 +121,7 @@ void ProxyPopupWindow::setRelativeY(qint32 y) { } qint32 ProxyPopupWindow::relativeY() const { - qWarning() << "PopupWindow.relativeY is deprecated. Use PopupWindow.anchor.rect.y."; + qmlWarning(this) << "PopupWindow.relativeY is deprecated. Use PopupWindow.anchor.rect.y."; return this->mAnchor.rect().y; } diff --git a/src/window/popupwindow.hpp b/src/window/popupwindow.hpp index bb245eb8..a1e535f7 100644 --- a/src/window/popupwindow.hpp +++ b/src/window/popupwindow.hpp @@ -30,9 +30,9 @@ /// } /// /// PopupWindow { -/// parentWindow: toplevel -/// relativeX: parentWindow.width / 2 - width / 2 -/// relativeY: parentWindow.height +/// anchor.window: toplevel +/// anchor.rect.x: parentWindow.width / 2 - width / 2 +/// anchor.rect.y: parentWindow.height /// width: 500 /// height: 500 /// visible: true From 8450543e09125fa4a5797bc03146da29e004692e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 18:28:19 -0800 Subject: [PATCH 219/305] service/mpris!: convert trackArtists from list to string Most people treat it as a string already, which breaks in Qt 6.8, and I have not seen a meaningful multi-artist response. --- src/services/mpris/player.cpp | 2 +- src/services/mpris/player.hpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index b2d4af60..8629d592 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -301,7 +301,7 @@ void MprisPlayer::onMetadataChanged() { auto trackTitleChanged = this->setTrackTitle(trackTitle.isNull() ? "Unknown Track" : trackTitle); auto trackArtists = this->pMetadata.get().value("xesam:artist").value>(); - auto trackArtistsChanged = this->setTrackArtists(trackArtists); + auto trackArtistsChanged = this->setTrackArtists(trackArtists.join(", ")); auto trackAlbum = this->pMetadata.get().value("xesam:album").toString(); auto trackAlbumChanged = this->setTrackAlbum(trackAlbum.isNull() ? "Unknown Album" : trackAlbum); diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 47ea7922..9e633555 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -141,7 +141,7 @@ class MprisPlayer: public QObject { /// The current track's album artist, or "Unknown Artist" if none was provided. Q_PROPERTY(QString trackAlbumArtist READ trackAlbumArtist NOTIFY trackAlbumArtistChanged); /// The current track's artists, or an empty list if none were provided. - Q_PROPERTY(QVector trackArtists READ trackArtists NOTIFY trackArtistsChanged); + Q_PROPERTY(QString trackArtists READ trackArtists NOTIFY trackArtistsChanged); /// The current track's art url, or `""` if none was provided. Q_PROPERTY(QString trackArtUrl READ trackArtUrl NOTIFY trackArtUrlChanged); /// The playback state of the media player. @@ -373,7 +373,7 @@ private: QString mTrackId; QString mTrackUrl; QString mTrackTitle; - QVector mTrackArtists; + QString mTrackArtists; QString mTrackAlbum; QString mTrackAlbumArtist; QString mTrackArtUrl; From dca75b7d6a8e60a9c5c93f8c0ee84326aa9f5322 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 20 Nov 2024 00:52:47 -0800 Subject: [PATCH 220/305] service/mpris: clarify trackinfo emit order and use QBindings --- src/core/util.hpp | 10 +++++ src/services/mpris/player.cpp | 69 +++++++++++++++------------------- src/services/mpris/player.hpp | 71 +++++++++++++++++++++-------------- src/wayland/popupanchor.cpp | 2 +- src/window/popupwindow.cpp | 8 ++-- 5 files changed, 86 insertions(+), 74 deletions(-) diff --git a/src/core/util.hpp b/src/core/util.hpp index 1ff9b22b..5dae122f 100644 --- a/src/core/util.hpp +++ b/src/core/util.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -258,3 +259,12 @@ template bool setSimpleObjectHandle(auto* parent, auto* value) { return SimpleObjectHandleOps::setObject(parent, value); } + +// NOLINTBEGIN +#define QS_TRIVIAL_GETTER(Type, member, getter) \ + [[nodiscard]] Type getter() { return this->member; } + +#define QS_BINDABLE_GETTER(Type, member, getter, bindable) \ + [[nodiscard]] Type getter() { return this->member.value(); } \ + [[nodiscard]] QBindable bindable() { return &this->member; } +// NOLINTEND diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 8629d592..a97020b0 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -256,19 +257,13 @@ void MprisPlayer::setVolume(qreal volume) { this->pVolume.write(); } -const QVariantMap& MprisPlayer::metadata() const { return this->pMetadata.get(); } - void MprisPlayer::onMetadataChanged() { - emit this->metadataChanged(); - auto lengthVariant = this->pMetadata.get().value("mpris:length"); qlonglong length = -1; if (lengthVariant.isValid() && lengthVariant.canConvert()) { length = lengthVariant.value(); } - auto lengthChanged = this->setLength(length); - auto trackChanged = false; QString trackId; @@ -297,47 +292,43 @@ void MprisPlayer::onMetadataChanged() { } } - auto trackTitle = this->pMetadata.get().value("xesam:title").toString(); - auto trackTitleChanged = this->setTrackTitle(trackTitle.isNull() ? "Unknown Track" : trackTitle); - - auto trackArtists = this->pMetadata.get().value("xesam:artist").value>(); - auto trackArtistsChanged = this->setTrackArtists(trackArtists.join(", ")); - - auto trackAlbum = this->pMetadata.get().value("xesam:album").toString(); - auto trackAlbumChanged = this->setTrackAlbum(trackAlbum.isNull() ? "Unknown Album" : trackAlbum); - - auto trackAlbumArtist = this->pMetadata.get().value("xesam:albumArtist").toString(); - auto trackAlbumArtistChanged = this->setTrackAlbumArtist(trackAlbumArtist); - - auto trackArtUrl = this->pMetadata.get().value("mpris:artUrl").toString(); - auto trackArtUrlChanged = this->setTrackArtUrl(trackArtUrl); - if (trackChanged) { - this->mUniqueId++; - // Some players don't seem to send position updates or seeks on track change. - this->pPosition.update(); emit this->trackChanged(); } - DropEmitter::call( - trackTitleChanged, - trackArtistsChanged, - trackAlbumChanged, - trackAlbumArtistChanged, - trackArtUrlChanged, - lengthChanged - ); + Qt::beginPropertyUpdateGroup(); + + this->bMetadata = this->pMetadata.get(); + + auto trackTitle = this->pMetadata.get().value("xesam:title").toString(); + this->bTrackTitle = trackTitle.isNull() ? "Unknown Track" : trackTitle; + + auto trackArtist = this->pMetadata.get().value("xesam:artist").value>(); + this->bTrackArtist = trackArtist.join(", "); + + auto trackAlbum = this->pMetadata.get().value("xesam:album").toString(); + this->bTrackAlbum = trackAlbum.isNull() ? "Unknown Album" : trackAlbum; + + this->bTrackAlbumArtist = this->pMetadata.get().value("xesam:albumArtist").toString(); + this->bTrackArtUrl = this->pMetadata.get().value("mpris:artUrl").toString(); + + if (trackChanged) { + emit this->trackChanged(); + this->bUniqueId = this->bUniqueId + 1; + + // Some players don't seem to send position updates or seeks on track change. + this->pPosition.update(); + } + + Qt::endPropertyUpdateGroup(); + + this->setLength(length); + + emit this->postTrackChanged(); } -DEFINE_MEMBER_GET(MprisPlayer, uniqueId); DEFINE_MEMBER_SET(MprisPlayer, length, setLength); -DEFINE_MEMBER_GETSET(MprisPlayer, trackTitle, setTrackTitle); -DEFINE_MEMBER_GETSET(MprisPlayer, trackArtists, setTrackArtists); -DEFINE_MEMBER_GETSET(MprisPlayer, trackAlbum, setTrackAlbum); -DEFINE_MEMBER_GETSET(MprisPlayer, trackAlbumArtist, setTrackAlbumArtist); -DEFINE_MEMBER_GETSET(MprisPlayer, trackArtUrl, setTrackArtUrl); - MprisPlaybackState::Enum MprisPlayer::playbackState() const { return this->mPlaybackState; } void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) { diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 9e633555..b509980b 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -125,25 +126,27 @@ class MprisPlayer: public QObject { /// A map of common properties is available [here](https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata). /// Do not count on any of them actually being present. /// - /// Note that the @@trackTitle, @@trackAlbum, @@trackAlbumArtist, @@trackArtists and @@trackArtUrl + /// Note that the @@trackTitle, @@trackAlbum, @@trackAlbumArtist, @@trackArtist and @@trackArtUrl /// properties have extra logic to guard against bad players sending weird metadata, and should /// be used over grabbing the properties directly from the metadata. - Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged); + Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged BINDABLE bindableMetadata); /// An opaque identifier for the current track unique within the current player. /// /// > [!WARNING] This is NOT `mpris:trackid` as that is sometimes missing or nonunique /// > in some players. - Q_PROPERTY(quint32 uniqueId READ uniqueId NOTIFY trackChanged); + Q_PROPERTY(quint32 uniqueId READ uniqueId NOTIFY trackChanged BINDABLE bindableUniqueId); /// The title of the current track, or "Unknown Track" if none was provided. - Q_PROPERTY(QString trackTitle READ trackTitle NOTIFY trackTitleChanged); + Q_PROPERTY(QString trackTitle READ trackTitle NOTIFY trackTitleChanged BINDABLE bindableTrackTitle); + /// The current track's artist, or an empty string if none was provided. + Q_PROPERTY(QString trackArtist READ trackArtist NOTIFY trackArtistChanged BINDABLE bindableTrackArtist); + /// > [!ERROR] deprecated in favor of @@trackArtist. + Q_PROPERTY(QString trackArtists READ trackArtist NOTIFY trackArtistChanged BINDABLE bindableTrackArtist); /// The current track's album, or "Unknown Album" if none was provided. - Q_PROPERTY(QString trackAlbum READ trackAlbum NOTIFY trackAlbumChanged); + Q_PROPERTY(QString trackAlbum READ trackAlbum NOTIFY trackAlbumChanged BINDABLE bindableTrackAlbum); /// The current track's album artist, or "Unknown Artist" if none was provided. - Q_PROPERTY(QString trackAlbumArtist READ trackAlbumArtist NOTIFY trackAlbumArtistChanged); - /// The current track's artists, or an empty list if none were provided. - Q_PROPERTY(QString trackArtists READ trackArtists NOTIFY trackArtistsChanged); + Q_PROPERTY(QString trackAlbumArtist READ trackAlbumArtist NOTIFY trackAlbumArtistChanged BINDABLE bindableTrackAlbumArtist); /// The current track's art url, or `""` if none was provided. - Q_PROPERTY(QString trackArtUrl READ trackArtUrl NOTIFY trackArtUrlChanged); + Q_PROPERTY(QString trackArtUrl READ trackArtUrl NOTIFY trackArtUrlChanged BINDABLE bindableTrackArtUrl); /// The playback state of the media player. /// /// - If @@canPlay is false, you cannot assign the `Playing` state. @@ -254,7 +257,13 @@ public: [[nodiscard]] bool volumeSupported() const; void setVolume(qreal volume); - [[nodiscard]] const QVariantMap& metadata() const; + QS_BINDABLE_GETTER(quint32, bUniqueId, uniqueId, bindableUniqueId); + QS_BINDABLE_GETTER(QVariantMap, bMetadata, metadata, bindableMetadata); + QS_BINDABLE_GETTER(QString, bTrackTitle, trackTitle, bindableTrackTitle); + QS_BINDABLE_GETTER(QString, bTrackAlbum, trackAlbum, bindableTrackAlbum); + QS_BINDABLE_GETTER(QString, bTrackAlbumArtist, trackAlbumArtist, bindableTrackAlbumArtist); + QS_BINDABLE_GETTER(QString, bTrackArtist, trackArtist, bindableTrackArtist); + QS_BINDABLE_GETTER(QString, bTrackArtUrl, trackArtUrl, bindableTrackArtUrl); [[nodiscard]] MprisPlaybackState::Enum playbackState() const; void setPlaybackState(MprisPlaybackState::Enum playbackState); @@ -281,9 +290,22 @@ public: signals: /// The track has changed. /// - /// All track info change signalss will fire immediately after if applicable, - /// but their values will be updated before the signal fires. + /// All track information properties that were sent by the player + /// will be updated immediately following this signal. @@postTrackChanged + /// will be sent after they update. + /// + /// Track information properties: @@uniqueId, @@metadata, @@trackTitle, + /// @@trackArtist, @@trackAlbum, @@trackAlbumArtist, @@trackArtUrl + /// + /// > [!WARNING] Some particularly poorly behaved players will update metadata + /// > *before* indicating the track has changed. void trackChanged(); + /// Sent after track info related properties have been updated, following @@trackChanged. + /// + /// > [!WARNING] It is not safe to assume all track information is up to date after + /// > this signal is emitted. A large number of players will update track information, + /// > particularly @@trackArtUrl, slightly after this signal. + void postTrackChanged(); QSDOC_HIDE void ready(); void canControlChanged(); @@ -306,9 +328,9 @@ signals: void volumeSupportedChanged(); void metadataChanged(); void trackTitleChanged(); + void trackArtistChanged(); void trackAlbumChanged(); void trackAlbumArtistChanged(); - void trackArtistsChanged(); void trackArtUrlChanged(); void playbackStateChanged(); void loopStateChanged(); @@ -369,30 +391,21 @@ private: DBusMprisPlayerApp* app = nullptr; DBusMprisPlayer* player = nullptr; - quint32 mUniqueId = 0; QString mTrackId; QString mTrackUrl; - QString mTrackTitle; - QString mTrackArtists; - QString mTrackAlbum; - QString mTrackAlbumArtist; - QString mTrackArtUrl; - - DECLARE_MEMBER_NS(MprisPlayer, uniqueId, mUniqueId); DECLARE_MEMBER(MprisPlayer, length, mLength, lengthChanged); DECLARE_MEMBER_SET(length, setLength); // clang-format off - DECLARE_PRIVATE_MEMBER(MprisPlayer, trackTitle, setTrackTitle, mTrackTitle, trackTitleChanged); - DECLARE_PRIVATE_MEMBER(MprisPlayer, trackArtists, setTrackArtists, mTrackArtists, trackArtistsChanged); - DECLARE_PRIVATE_MEMBER(MprisPlayer, trackAlbum, setTrackAlbum, mTrackAlbum, trackAlbumChanged); - DECLARE_PRIVATE_MEMBER(MprisPlayer, trackAlbumArtist, setTrackAlbumArtist, mTrackAlbumArtist, trackAlbumArtistChanged); - DECLARE_PRIVATE_MEMBER(MprisPlayer, trackArtUrl, setTrackArtUrl, mTrackArtUrl, trackArtUrlChanged); + Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, quint32, bUniqueId); + Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QVariantMap, bMetadata, &MprisPlayer::metadataChanged); + Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bTrackArtist, &MprisPlayer::trackArtistChanged); + Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bTrackTitle, &MprisPlayer::trackTitleChanged); + Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bTrackAlbum, &MprisPlayer::trackAlbumChanged); + Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bTrackAlbumArtist, &MprisPlayer::trackAlbumArtistChanged); + Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bTrackArtUrl, &MprisPlayer::trackArtUrlChanged); // clang-format on - -public: - DECLARE_MEMBER_GET(uniqueId); }; } // namespace qs::service::mpris diff --git a/src/wayland/popupanchor.cpp b/src/wayland/popupanchor.cpp index 81c1cb1d..c6a6412d 100644 --- a/src/wayland/popupanchor.cpp +++ b/src/wayland/popupanchor.cpp @@ -4,9 +4,9 @@ #include #include #include +#include #include #include -#include #include "../core/popupanchor.hpp" #include "../core/types.hpp" diff --git a/src/window/popupwindow.cpp b/src/window/popupwindow.cpp index df68474f..7454d363 100644 --- a/src/window/popupwindow.cpp +++ b/src/window/popupwindow.cpp @@ -1,6 +1,5 @@ #include "popupwindow.hpp" -#include #include #include #include @@ -42,9 +41,7 @@ void ProxyPopupWindow::setParentWindow(QObject* parent) { this->mAnchor.setWindow(parent); } -QObject* ProxyPopupWindow::parentWindow() const { - return this->mAnchor.window(); -} +QObject* ProxyPopupWindow::parentWindow() const { return this->mAnchor.window(); } void ProxyPopupWindow::updateTransientParent() { auto* bw = this->mAnchor.backingWindow(); @@ -70,7 +67,8 @@ void ProxyPopupWindow::updateTransientParent() { 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"; + qmlWarning(this + ) << "Cannot set screen of popup window, as that is controlled by the parent window"; } void ProxyPopupWindow::setVisible(bool visible) { From 4163713bc4af4c79008b61d0f3c28580e6edae03 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 20 Nov 2024 03:15:09 -0800 Subject: [PATCH 221/305] dbus/properties: decouple properties from AbstractDBusProperty Importantly, this decouples properties from having to be QObjects, allowing future property types to be much lighter. --- src/dbus/properties.cpp | 180 +++++++++++++++++++++------------------- src/dbus/properties.hpp | 70 ++++++++++------ 2 files changed, 137 insertions(+), 113 deletions(-) diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index 6156b2a3..a1cd7e68 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -110,100 +110,25 @@ void asyncReadPropertyInternal( QObject::connect(call, &QDBusPendingCallWatcher::finished, &interface, responseCallback); } -void AbstractDBusProperty::tryUpdate(const QVariant& variant) { - this->mExists = true; - - auto error = this->read(variant); - if (error.isValid()) { - qCWarning(logDbusProperties).noquote() - << "Error demarshalling property update for" << this->toString(); - qCWarning(logDbusProperties) << error; - } else { - qCDebug(logDbusProperties).noquote() - << "Updated property" << this->toString() << "to" << this->valueString(); - } -} - void AbstractDBusProperty::update() { if (this->group == nullptr) { - qFatal(logDbusProperties) << "Tried to update dbus property" << this->name + qFatal(logDbusProperties) << "Tried to update dbus property" << this->nameRef() << "which is not attached to a group"; } else { - const QString propStr = this->toString(); - - if (this->group->interface == nullptr) { - qFatal(logDbusProperties).noquote() - << "Tried to update property" << propStr << "of a disconnected interface"; - } - - qCDebug(logDbusProperties).noquote() << "Updating property" << propStr; - - auto pendingCall = - this->group->propertyInterface->Get(this->group->interface->interface(), this->name); - - auto* call = new QDBusPendingCallWatcher(pendingCall, this); - - auto responseCallback = [this, propStr](QDBusPendingCallWatcher* call) { - const QDBusPendingReply reply = *call; - - if (reply.isError()) { - qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr; - qCWarning(logDbusProperties) << reply.error(); - } else { - this->tryUpdate(reply.value().variant()); - } - - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); + this->group->requestPropertyUpdate(this); } } void AbstractDBusProperty::write() { if (this->group == nullptr) { - qFatal(logDbusProperties) << "Tried to write dbus property" << this->name + qFatal(logDbusProperties) << "Tried to write dbus property" << this->nameRef() << "which is not attached to a group"; } else { - const QString propStr = this->toString(); - - if (this->group->interface == nullptr) { - qFatal(logDbusProperties).noquote() - << "Tried to write property" << propStr << "of a disconnected interface"; - } - - qCDebug(logDbusProperties).noquote() << "Writing property" << propStr; - - auto pendingCall = this->group->propertyInterface->Set( - this->group->interface->interface(), - this->name, - QDBusVariant(this->serialize()) - ); - - auto* call = new QDBusPendingCallWatcher(pendingCall, this); - - auto responseCallback = [propStr](QDBusPendingCallWatcher* call) { - const QDBusPendingReply<> reply = *call; - - if (reply.isError()) { - qCWarning(logDbusProperties).noquote() << "Error writing property" << propStr; - qCWarning(logDbusProperties) << reply.error(); - } - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); + this->group->pushPropertyUpdate(this); } } -bool AbstractDBusProperty::exists() const { return this->mExists; } - -QString AbstractDBusProperty::toString() const { - const QString group = this->group == nullptr ? "{ NO GROUP }" : this->group->toString(); - return group + ':' + this->name; -} - -DBusPropertyGroup::DBusPropertyGroup(QVector properties, QObject* parent) +DBusPropertyGroup::DBusPropertyGroup(QVector properties, QObject* parent) : QObject(parent) , properties(std::move(properties)) {} @@ -246,7 +171,7 @@ void DBusPropertyGroup::updateAllDirect() { } for (auto* property: this->properties) { - property->update(); + this->requestPropertyUpdate(property); } } @@ -287,27 +212,102 @@ void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool co auto prop = std::find_if( this->properties.begin(), this->properties.end(), - [&name](AbstractDBusProperty* prop) { return prop->name == name; } + [&name](DBusPropertyCore* prop) { return prop->nameRef() == name; } ); if (prop == this->properties.end()) { qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" << this->toString(); } else { - (*prop)->tryUpdate(value); + this->tryUpdateProperty(*prop, value); } } if (complainMissing) { for (const auto* prop: this->properties) { - if (prop->required && !properties.contains(prop->name)) { + if (prop->isRequired() && !properties.contains(prop->name())) { qCWarning(logDbusProperties) - << prop->name << "missing from property set for" << this->toString(); + << prop->nameRef() << "missing from property set for" << this->toString(); } } } } +void DBusPropertyGroup::tryUpdateProperty(DBusPropertyCore* property, const QVariant& variant) + const { + property->mExists = true; + + auto error = property->store(variant); + if (error.isValid()) { + qCWarning(logDbusProperties).noquote() + << "Error demarshalling property update for" << this->propertyString(property); + qCWarning(logDbusProperties) << error; + } else { + qCDebug(logDbusProperties).noquote() + << "Updated property" << this->propertyString(property) << "to" << property->valueString(); + } +} + +void DBusPropertyGroup::requestPropertyUpdate(DBusPropertyCore* property) { + const QString propStr = this->propertyString(property); + + if (this->interface == nullptr) { + qFatal(logDbusProperties).noquote() + << "Tried to update property" << propStr << "of a disconnected interface"; + } + + qCDebug(logDbusProperties).noquote() << "Updating property" << propStr; + + auto pendingCall = this->propertyInterface->Get(this->interface->interface(), property->name()); + auto* call = new QDBusPendingCallWatcher(pendingCall, this); + + auto responseCallback = [this, propStr, property](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr; + qCWarning(logDbusProperties) << reply.error(); + } else { + this->tryUpdateProperty(property, reply.value().variant()); + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void DBusPropertyGroup::pushPropertyUpdate(DBusPropertyCore* property) { + const QString propStr = this->propertyString(property); + + if (this->interface == nullptr) { + qFatal(logDbusProperties).noquote() + << "Tried to write property" << propStr << "of a disconnected interface"; + } + + qCDebug(logDbusProperties).noquote() << "Writing property" << propStr; + + auto pendingCall = this->propertyInterface->Set( + this->interface->interface(), + property->name(), + QDBusVariant(property->serialize()) + ); + + auto* call = new QDBusPendingCallWatcher(pendingCall, this); + + auto responseCallback = [propStr](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + + if (reply.isError()) { + qCWarning(logDbusProperties).noquote() << "Error writing property" << propStr; + qCWarning(logDbusProperties) << reply.error(); + } + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + QString DBusPropertyGroup::toString() const { if (this->interface == nullptr) { return "{ DISCONNECTED }"; @@ -317,6 +317,12 @@ QString DBusPropertyGroup::toString() const { } } +QString DBusPropertyGroup::propertyString(const DBusPropertyCore* property) const { + return this->toString() % ':' % property->nameRef(); +} + +QString AbstractDBusProperty::toString() const { return this->group->propertyString(this); } + void DBusPropertyGroup::onPropertiesChanged( const QString& interfaceName, const QVariantMap& changedProperties, @@ -330,14 +336,14 @@ void DBusPropertyGroup::onPropertiesChanged( auto prop = std::find_if( this->properties.begin(), this->properties.end(), - [&name](AbstractDBusProperty* prop) { return prop->name == name; } + [&name](DBusPropertyCore* prop) { return prop->nameRef() == name; } ); if (prop == this->properties.end()) { qCDebug(logDbusProperties) << "Ignoring untracked property invalidation" << name << "for" << this; } else { - (*prop)->update(); + this->requestPropertyUpdate(*prop); } } diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index 65f51afc..3008d357 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -75,24 +76,44 @@ void asyncReadProperty( class DBusPropertyGroup; -class AbstractDBusProperty: public QObject { +class DBusPropertyCore { +public: + DBusPropertyCore() = default; + virtual ~DBusPropertyCore() = default; + Q_DISABLE_COPY_MOVE(DBusPropertyCore); + + [[nodiscard]] virtual QString name() const = 0; + [[nodiscard]] virtual QStringView nameRef() const = 0; + [[nodiscard]] virtual QString valueString() = 0; + [[nodiscard]] virtual bool isRequired() const = 0; + [[nodiscard]] bool exists() const { return this->mExists; } + +protected: + virtual QDBusError store(const QVariant& variant) = 0; + [[nodiscard]] virtual QVariant serialize() = 0; + +private: + bool mExists : 1 = false; + + friend class DBusPropertyGroup; +}; + +class AbstractDBusProperty + : public QObject + , public DBusPropertyCore { Q_OBJECT; public: - explicit AbstractDBusProperty( - QString name, - const QMetaType& type, - bool required, - QObject* parent = nullptr - ) + explicit AbstractDBusProperty(QString name, bool required, QObject* parent = nullptr) : QObject(parent) - , name(std::move(name)) - , type(type) - , required(required) {} + , required(required) + , mName(std::move(name)) {} + + [[nodiscard]] QString name() const override { return this->mName; }; + [[nodiscard]] QStringView nameRef() const override { return this->mName; }; + [[nodiscard]] bool isRequired() const override { return this->required; }; - [[nodiscard]] bool exists() const; [[nodiscard]] QString toString() const; - [[nodiscard]] virtual QString valueString() = 0; public slots: void update(); @@ -101,19 +122,12 @@ public slots: signals: void changed(); -protected: - virtual QDBusError read(const QVariant& variant) = 0; - virtual QVariant serialize() = 0; - private: - void tryUpdate(const QVariant& variant); + bool required : 1; + bool mExists : 1 = false; DBusPropertyGroup* group = nullptr; - - QString name; - QMetaType type; - bool required; - bool mExists = false; + QString mName; friend class DBusPropertyGroup; }; @@ -123,7 +137,7 @@ class DBusPropertyGroup: public QObject { public: explicit DBusPropertyGroup( - QVector properties = QVector(), + QVector properties = QVector(), QObject* parent = nullptr ); @@ -146,10 +160,14 @@ private slots: private: void updatePropertySet(const QVariantMap& properties, bool complainMissing); + void requestPropertyUpdate(DBusPropertyCore* property); + void pushPropertyUpdate(DBusPropertyCore* property); + void tryUpdateProperty(DBusPropertyCore* property, const QVariant& variant) const; + [[nodiscard]] QString propertyString(const DBusPropertyCore* property) const; DBusPropertiesInterface* propertyInterface = nullptr; QDBusAbstractInterface* interface = nullptr; - QVector properties; + QVector properties; friend class AbstractDBusProperty; }; @@ -163,7 +181,7 @@ public: bool required = true, QObject* parent = nullptr ) - : AbstractDBusProperty(std::move(name), QMetaType::fromType(), required, parent) + : AbstractDBusProperty(std::move(name), required, parent) , value(std::move(value)) {} explicit DBusProperty( @@ -191,7 +209,7 @@ public: } protected: - QDBusError read(const QVariant& variant) override { + QDBusError store(const QVariant& variant) override { auto result = demarshallVariant(variant); if (result.isValid()) { From 1955deee74c31ce05cdc288df65b517fd225e2fc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 20 Nov 2024 19:22:23 -0800 Subject: [PATCH 222/305] dbus/properties: add QObjectBindableProperty based dbus property Many times more lightweight than the original QObject-based one. --- src/core/util.hpp | 38 +++++++++--- src/dbus/properties.cpp | 4 ++ src/dbus/properties.hpp | 124 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 151 insertions(+), 15 deletions(-) diff --git a/src/core/util.hpp b/src/core/util.hpp index 5dae122f..62264b93 100644 --- a/src/core/util.hpp +++ b/src/core/util.hpp @@ -2,22 +2,45 @@ #include #include +#include #include #include #include +#include #include #include -template +template struct StringLiteral { - constexpr StringLiteral(const char (&str)[Length]) { // NOLINT - std::copy_n(str, Length, this->value); + constexpr StringLiteral(const char (&str)[length]) { // NOLINT + std::copy_n(str, length, this->value); } constexpr operator const char*() const noexcept { return this->value; } - operator QLatin1StringView() const { return QLatin1String(this->value, Length); } + operator QLatin1StringView() const { return QLatin1String(this->value, length); } - char value[Length]; // NOLINT + char value[length]; // NOLINT +}; + +template +struct StringLiteral16 { + constexpr StringLiteral16(const char16_t (&str)[length]) { // NOLINT + std::copy_n(str, length, this->value); + } + + [[nodiscard]] constexpr const QChar* qCharPtr() const noexcept { + return std::bit_cast(&this->value); + } + + [[nodiscard]] Q_ALWAYS_INLINE operator QString() const noexcept { + return QString::fromRawData(this->qCharPtr(), static_cast(length - 1)); + } + + [[nodiscard]] Q_ALWAYS_INLINE operator QStringView() const noexcept { + return QStringView(this->qCharPtr(), static_cast(length - 1)); + } + + char16_t value[length]; // NOLINT }; // NOLINTBEGIN @@ -149,11 +172,10 @@ private: // NOLINTEND template -class MemberPointerTraits; +struct MemberPointerTraits; template -class MemberPointerTraits { -public: +struct MemberPointerTraits { using Class = C; using Type = T; }; diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index a1cd7e68..08146637 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -162,6 +162,10 @@ void DBusPropertyGroup::attachProperty(AbstractDBusProperty* property) { property->group = this; } +void DBusPropertyGroup::attachProperty(DBusPropertyCore* property) { + this->properties.append(property); +} + void DBusPropertyGroup::updateAllDirect() { qCDebug(logDbusProperties).noquote() << "Updating all properties of" << this->toString() << "via individual queries"; diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index 3008d357..11e7e063 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -14,10 +15,14 @@ #include #include #include +#include +#include #include #include #include +#include "../core/util.hpp" + class DBusPropertiesInterface; Q_DECLARE_LOGGING_CATEGORY(logDbusProperties); @@ -132,21 +137,94 @@ private: friend class DBusPropertyGroup; }; +namespace bindable_p { + +template +struct BindableParams; + +template