From 36517a2c10d206bbde30f6a43e0002b3c3ce139f Mon Sep 17 00:00:00 2001 From: bbedward Date: Fri, 13 Feb 2026 17:54:43 -0500 Subject: [PATCH 01/26] services/pipewire: manage default objs using normal qt properties Fixes use after free bugs due to pointer mismatches in destructors. Drops SimpleObjectHandle. --- changelog/next.md | 1 + src/core/util.hpp | 31 -------- src/services/pipewire/defaults.cpp | 121 +++++++++++++++++++---------- src/services/pipewire/defaults.hpp | 5 +- 4 files changed, 83 insertions(+), 75 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 7180d53..b9000c2 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -48,6 +48,7 @@ set shell id. - Fixed memory leak in IPC handlers. - Fixed ClippingRectangle related crashes. - Fixed crashes when monitors are unplugged. +- Fixed crashes when default pipewire devices are lost. ## Packaging Changes diff --git a/src/core/util.hpp b/src/core/util.hpp index 3b86d28..bb8dd85 100644 --- a/src/core/util.hpp +++ b/src/core/util.hpp @@ -251,37 +251,6 @@ public: GuardedEmitBlocker block() { return GuardedEmitBlocker(&this->blocked); } }; -template -class SimpleObjectHandleOps { - using Traits = MemberPointerTraits; - -public: - static bool setObject(Traits::Class* parent, Traits::Type value) { - if (value == parent->*member) return false; - - if (parent->*member != nullptr) { - QObject::disconnect(parent->*member, &QObject::destroyed, parent, destroyedSlot); - } - - parent->*member = value; - - if (value != nullptr) { - QObject::connect(parent->*member, &QObject::destroyed, parent, destroyedSlot); - } - - if constexpr (changedSignal != nullptr) { - emit(parent->*changedSignal)(); - } - - return true; - } -}; - -template -bool setSimpleObjectHandle(auto* parent, auto* value) { - return SimpleObjectHandleOps::setObject(parent, value); -} - template class MethodFunctor { using PtrMeta = MemberPointerTraits; diff --git a/src/services/pipewire/defaults.cpp b/src/services/pipewire/defaults.cpp index 02463f4..7a24a65 100644 --- a/src/services/pipewire/defaults.cpp +++ b/src/services/pipewire/defaults.cpp @@ -12,7 +12,6 @@ #include #include "../../core/logcat.hpp" -#include "../../core/util.hpp" #include "metadata.hpp" #include "node.hpp" #include "registry.hpp" @@ -138,32 +137,6 @@ void PwDefaultTracker::onNodeAdded(PwNode* node) { } } -void PwDefaultTracker::onNodeDestroyed(QObject* node) { - if (node == this->mDefaultSink) { - qCInfo(logDefaults) << "Default sink destroyed."; - this->mDefaultSink = nullptr; - emit this->defaultSinkChanged(); - } - - if (node == this->mDefaultSource) { - qCInfo(logDefaults) << "Default source destroyed."; - this->mDefaultSource = nullptr; - emit this->defaultSourceChanged(); - } - - if (node == this->mDefaultConfiguredSink) { - qCInfo(logDefaults) << "Default configured sink destroyed."; - this->mDefaultConfiguredSink = nullptr; - emit this->defaultConfiguredSinkChanged(); - } - - if (node == this->mDefaultConfiguredSource) { - qCInfo(logDefaults) << "Default configured source destroyed."; - this->mDefaultConfiguredSource = nullptr; - emit this->defaultConfiguredSourceChanged(); - } -} - void PwDefaultTracker::changeConfiguredSink(PwNode* node) { if (node != nullptr) { if (!node->type.testFlags(PwNodeType::AudioSink)) { @@ -240,10 +213,23 @@ void PwDefaultTracker::setDefaultSink(PwNode* node) { if (node == this->mDefaultSink) return; qCInfo(logDefaults) << "Default sink changed to" << node; - setSimpleObjectHandle< - &PwDefaultTracker::mDefaultSink, - &PwDefaultTracker::onNodeDestroyed, - &PwDefaultTracker::defaultSinkChanged>(this, node); + if (this->mDefaultSink != nullptr) { + QObject::disconnect(this->mDefaultSink, nullptr, this, nullptr); + } + + this->mDefaultSink = node; + + if (node != nullptr) { + QObject::connect(node, &QObject::destroyed, this, &PwDefaultTracker::onDefaultSinkDestroyed); + } + + emit this->defaultSinkChanged(); +} + +void PwDefaultTracker::onDefaultSinkDestroyed() { + qCInfo(logDefaults) << "Default sink destroyed."; + this->mDefaultSink = nullptr; + emit this->defaultSinkChanged(); } void PwDefaultTracker::setDefaultSinkName(const QString& name) { @@ -257,10 +243,23 @@ void PwDefaultTracker::setDefaultSource(PwNode* node) { if (node == this->mDefaultSource) return; qCInfo(logDefaults) << "Default source changed to" << node; - setSimpleObjectHandle< - &PwDefaultTracker::mDefaultSource, - &PwDefaultTracker::onNodeDestroyed, - &PwDefaultTracker::defaultSourceChanged>(this, node); + if (this->mDefaultSource != nullptr) { + QObject::disconnect(this->mDefaultSource, nullptr, this, nullptr); + } + + this->mDefaultSource = node; + + if (node != nullptr) { + QObject::connect(node, &QObject::destroyed, this, &PwDefaultTracker::onDefaultSourceDestroyed); + } + + emit this->defaultSourceChanged(); +} + +void PwDefaultTracker::onDefaultSourceDestroyed() { + qCInfo(logDefaults) << "Default source destroyed."; + this->mDefaultSource = nullptr; + emit this->defaultSourceChanged(); } void PwDefaultTracker::setDefaultSourceName(const QString& name) { @@ -274,10 +273,28 @@ void PwDefaultTracker::setDefaultConfiguredSink(PwNode* node) { if (node == this->mDefaultConfiguredSink) return; qCInfo(logDefaults) << "Default configured sink changed to" << node; - setSimpleObjectHandle< - &PwDefaultTracker::mDefaultConfiguredSink, - &PwDefaultTracker::onNodeDestroyed, - &PwDefaultTracker::defaultConfiguredSinkChanged>(this, node); + if (this->mDefaultConfiguredSink != nullptr) { + QObject::disconnect(this->mDefaultConfiguredSink, nullptr, this, nullptr); + } + + this->mDefaultConfiguredSink = node; + + if (node != nullptr) { + QObject::connect( + node, + &QObject::destroyed, + this, + &PwDefaultTracker::onDefaultConfiguredSinkDestroyed + ); + } + + emit this->defaultConfiguredSinkChanged(); +} + +void PwDefaultTracker::onDefaultConfiguredSinkDestroyed() { + qCInfo(logDefaults) << "Default configured sink destroyed."; + this->mDefaultConfiguredSink = nullptr; + emit this->defaultConfiguredSinkChanged(); } void PwDefaultTracker::setDefaultConfiguredSinkName(const QString& name) { @@ -291,10 +308,28 @@ void PwDefaultTracker::setDefaultConfiguredSource(PwNode* node) { if (node == this->mDefaultConfiguredSource) return; qCInfo(logDefaults) << "Default configured source changed to" << node; - setSimpleObjectHandle< - &PwDefaultTracker::mDefaultConfiguredSource, - &PwDefaultTracker::onNodeDestroyed, - &PwDefaultTracker::defaultConfiguredSourceChanged>(this, node); + if (this->mDefaultConfiguredSource != nullptr) { + QObject::disconnect(this->mDefaultConfiguredSource, nullptr, this, nullptr); + } + + this->mDefaultConfiguredSource = node; + + if (node != nullptr) { + QObject::connect( + node, + &QObject::destroyed, + this, + &PwDefaultTracker::onDefaultConfiguredSourceDestroyed + ); + } + + emit this->defaultConfiguredSourceChanged(); +} + +void PwDefaultTracker::onDefaultConfiguredSourceDestroyed() { + qCInfo(logDefaults) << "Default configured source destroyed."; + this->mDefaultConfiguredSource = nullptr; + emit this->defaultConfiguredSourceChanged(); } void PwDefaultTracker::setDefaultConfiguredSourceName(const QString& name) { diff --git a/src/services/pipewire/defaults.hpp b/src/services/pipewire/defaults.hpp index 591c4fd..f31669e 100644 --- a/src/services/pipewire/defaults.hpp +++ b/src/services/pipewire/defaults.hpp @@ -44,7 +44,10 @@ private slots: void onMetadataAdded(PwMetadata* metadata); void onMetadataProperty(const char* key, const char* type, const char* value); void onNodeAdded(PwNode* node); - void onNodeDestroyed(QObject* node); + void onDefaultSinkDestroyed(); + void onDefaultSourceDestroyed(); + void onDefaultConfiguredSinkDestroyed(); + void onDefaultConfiguredSourceDestroyed(); private: void setDefaultSink(PwNode* node); From 6e17efab83d3a5ad5d6e59bc08d26095c6660502 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 23 Feb 2026 23:03:48 -0800 Subject: [PATCH 02/26] wayland/screencopy: enable vulkan dmabuf support on session locks Also reformat dmabuf --- src/wayland/buffer/dmabuf.cpp | 27 ++++++++++----------------- src/wayland/session_lock.cpp | 10 ++++++++++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index 7d17884..ed9dbeb 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -35,7 +36,6 @@ #include #include #include -#include #include #include #include @@ -80,10 +80,8 @@ bool drmFormatHasAlpha(uint32_t drmFormat) { case DRM_FORMAT_ABGR8888: case DRM_FORMAT_ARGB2101010: case DRM_FORMAT_ABGR2101010: - case DRM_FORMAT_ABGR16161616F: - return true; - default: - return false; + case DRM_FORMAT_ABGR16161616F: return true; + default: return false; } } @@ -818,7 +816,8 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) co // dup() is required because vkAllocateMemory with VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT // takes ownership of the fd on succcess. Without dup, WlDmaBuffer would double-close. - const int dupFd = dup(this->planes[0].fd); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + const int dupFd = + dup(this->planes[0].fd); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) if (dupFd < 0) { qCWarning(logDmabuf) << "Failed to dup() fd for DMA-BUF import"; goto cleanup_fail; // NOLINT @@ -909,12 +908,12 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) co // find the graphics queue family index for the ownrship transfer. uint32_t graphicsQueueFamily = 0; uint32_t queueFamilyCount = 0; - instFuncs->vkGetPhysicalDeviceQueueFamilyProperties( - physDevice, &queueFamilyCount, nullptr - ); + instFuncs->vkGetPhysicalDeviceQueueFamilyProperties(physDevice, &queueFamilyCount, nullptr); std::vector queueFamilies(queueFamilyCount); instFuncs->vkGetPhysicalDeviceQueueFamilyProperties( - physDevice, &queueFamilyCount, queueFamilies.data() + physDevice, + &queueFamilyCount, + queueFamilies.data() ); for (uint32_t i = 0; i < queueFamilyCount; ++i) { if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { @@ -989,13 +988,7 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) co } } - auto* tex = new WlDmaBufferVulkanQSGTexture( - devFuncs, - device, - image, - memory, - qsgTexture - ); + auto* tex = new WlDmaBufferVulkanQSGTexture(devFuncs, device, image, memory, qsgTexture); qCDebug(logDmabuf) << "Created WlDmaBufferVulkanQSGTexture" << tex << "from" << this; return tex; } diff --git a/src/wayland/session_lock.cpp b/src/wayland/session_lock.cpp index d5a3e53..2ebe3fd 100644 --- a/src/wayland/session_lock.cpp +++ b/src/wayland/session_lock.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -216,6 +217,15 @@ void WlSessionLockSurface::onReload(QObject* oldInstance) { if (this->window == nullptr) { this->window = new QQuickWindow(); + + // needed for vulkan dmabuf import, qt ignores these if not applicable + auto graphicsConfig = this->window->graphicsConfiguration(); + graphicsConfig.setDeviceExtensions({ + "VK_KHR_external_memory_fd", + "VK_EXT_external_memory_dma_buf", + "VK_EXT_image_drm_format_modifier", + }); + this->window->setGraphicsConfiguration(graphicsConfig); } this->mContentItem->setParentItem(this->window->contentItem()); From cddb4f061bab495f4473ca5f2c571b6c710efef7 Mon Sep 17 00:00:00 2001 From: Carson Powers Date: Fri, 6 Feb 2026 17:25:50 -0600 Subject: [PATCH 03/26] build: fix lint-staged to ignore deleted files --- Justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Justfile b/Justfile index 2d6377e..801eb2a 100644 --- a/Justfile +++ b/Justfile @@ -13,7 +13,7 @@ lint-changed: git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} lint-staged: - git diff --staged --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} + git diff --staged --name-only --diff-filter=d HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} configure target='debug' *FLAGS='': cmake -GNinja -B {{builddir}} \ From cdde4c63f4dd09e92a960e27f1202ca2e0d830d1 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 2 Mar 2026 08:09:57 -0800 Subject: [PATCH 04/26] crash: switch to cpptrace from breakpad --- .github/ISSUE_TEMPLATE/crash.yml | 2 +- .github/ISSUE_TEMPLATE/crash2.yml | 49 +++++++ .github/workflows/build.yml | 7 +- BUILD.md | 12 +- CMakeLists.txt | 3 +- changelog/next.md | 6 +- default.nix | 15 +- quickshell.scm | 3 +- src/CMakeLists.txt | 2 +- src/build/CMakeLists.txt | 6 +- src/build/build.hpp.in | 2 +- src/core/instanceinfo.hpp | 2 + src/crash/CMakeLists.txt | 49 ++++++- src/crash/handler.cpp | 233 +++++++++++++++++------------- src/crash/handler.hpp | 13 +- src/crash/interface.cpp | 4 +- src/crash/main.cpp | 122 +++++++++++++--- src/launch/launch.cpp | 9 +- src/launch/main.cpp | 6 +- 19 files changed, 372 insertions(+), 173 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/crash2.yml diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml index c8b4804..80fa827 100644 --- a/.github/ISSUE_TEMPLATE/crash.yml +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -1,4 +1,4 @@ -name: Crash Report +name: Crash Report (v1) description: Quickshell has crashed labels: ["bug", "crash"] body: diff --git a/.github/ISSUE_TEMPLATE/crash2.yml b/.github/ISSUE_TEMPLATE/crash2.yml new file mode 100644 index 0000000..84beef8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/crash2.yml @@ -0,0 +1,49 @@ +name: Crash Report (v2) +description: Quickshell has crashed +labels: ["bug", "crash"] +body: + - type: textarea + id: userinfo + attributes: + label: What caused the crash + description: | + Any information likely to help debug the crash. What were you doing when the crash occurred, + what changes did you make, can you get it to happen again? + - type: textarea + id: report + attributes: + label: Report file + description: Attach `report.txt` here. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Log file + description: | + Attach `log.qslog.log` here. If it is too big to upload, compress it. + + You can preview the log if you'd like using `quickshell read-log `. + validations: + required: true + - type: textarea + id: config + attributes: + label: Configuration + description: | + Attach your configuration here, preferrably in full (not just one file). + Compress it into a zip, tar, etc. + + This will help us reproduce the crash ourselves. + - type: textarea + id: bt + attributes: + label: Backtrace + description: | + GDB usually produces better stacktraces than quickshell can. Consider attaching a gdb backtrace + following the instructions below. + + 1. Run `coredumpctl debug ` where `pid` is the number shown after "Crashed process ID" + in the crash reporter. + 2. Once it loads, type `bt -full` (then enter) + 3. Copy the output and attach it as a file or in a spoiler. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8d19f58..7b8cbce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,10 +55,11 @@ jobs: libpipewire \ cli11 \ polkit \ - jemalloc + jemalloc \ + libunwind \ + git # for cpptrace clone - name: Build - # breakpad is annoying to build in ci due to makepkg not running as root run: | - cmake -GNinja -B build -DCRASH_REPORTER=OFF + cmake -GNinja -B build -DVENDOR_CPPTRACE=ON cmake --build build diff --git a/BUILD.md b/BUILD.md index c9459b5..6a3f422 100644 --- a/BUILD.md +++ b/BUILD.md @@ -64,14 +64,18 @@ At least Qt 6.6 is required. All features are enabled by default and some have their own dependencies. -### Crash Reporter -The crash reporter catches crashes, restarts quickshell when it crashes, +### Crash Handler +The crash reporter catches crashes, restarts Quickshell when it crashes, and collects useful crash information in one place. Leaving this enabled will enable us to fix bugs far more easily. -To disable: `-DCRASH_REPORTER=OFF` +To disable: `-DCRASH_HANDLER=OFF` -Dependencies: `google-breakpad` (static library) +Dependencies: `cpptrace` + +Note: `-DVENDOR_CPPTRACE=ON` can be set to vendor cpptrace using FetchContent. + +When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the package manager or fetched with FetchContent. ### Jemalloc We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused diff --git a/CMakeLists.txt b/CMakeLists.txt index 7633f4f..fabda0e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,12 +47,11 @@ boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN}) if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") - boption(CRASH_REPORTER "Crash Handling" OFF) boption(USE_JEMALLOC "Use jemalloc" OFF) else() - boption(CRASH_REPORTER "Crash Handling" ON) boption(USE_JEMALLOC "Use jemalloc" ON) endif() +boption(CRASH_HANDLER "Crash Handling" ON) boption(SOCKETS "Unix Sockets" ON) boption(WAYLAND "Wayland" ON) boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND) diff --git a/changelog/next.md b/changelog/next.md index b9000c2..2083462 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -32,6 +32,7 @@ set shell id. - FreeBSD is now partially supported. - IPC operations filter available instances to the current display connection by default. - PwNodeLinkTracker ignores sound level monitoring programs. +- Replaced breakpad with cpptrace. ## Bug Fixes @@ -52,5 +53,6 @@ set shell id. ## Packaging Changes -`glib` and `polkit` have been added as dependencies when compiling with polkit agent support. -`vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support). +- `glib` and `polkit` have been added as dependencies when compiling with polkit agent support. +- `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support). +- `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore. diff --git a/default.nix b/default.nix index 7783774..59e68b0 100644 --- a/default.nix +++ b/default.nix @@ -10,7 +10,9 @@ ninja, spirv-tools, qt6, - breakpad, + cpptrace ? null, + libunwind, + libdwarf, jemalloc, cli11, wayland, @@ -49,6 +51,8 @@ withPolkit ? true, withNetworkManager ? true, }: let + withCrashHandler = withCrashReporter && cpptrace != null && lib.strings.compareVersions cpptrace.version "0.7.2" >= 0; + unwrapped = stdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; version = "0.2.1"; @@ -74,7 +78,12 @@ cli11 ] ++ lib.optional withQtSvg qt6.qtsvg - ++ lib.optional withCrashReporter breakpad + ++ lib.optional withCrashHandler (cpptrace.overrideAttrs (prev: { + cmakeFlags = prev.cmakeFlags ++ [ + "-DCPPTRACE_UNWIND_WITH_LIBUNWIND=TRUE" + ]; + buildInputs = prev.buildInputs ++ [ libunwind ]; + })) ++ lib.optional withJemalloc jemalloc ++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland ++ lib.optionals withWayland [ wayland wayland-protocols ] @@ -91,7 +100,7 @@ (lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix) (lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true) (lib.cmakeFeature "GIT_REVISION" gitRev) - (lib.cmakeBool "CRASH_REPORTER" withCrashReporter) + (lib.cmakeBool "CRASH_HANDLER" withCrashHandler) (lib.cmakeBool "USE_JEMALLOC" withJemalloc) (lib.cmakeBool "WAYLAND" withWayland) (lib.cmakeBool "SCREENCOPY" (libgbm != null)) diff --git a/quickshell.scm b/quickshell.scm index 3f82160..780bb96 100644 --- a/quickshell.scm +++ b/quickshell.scm @@ -56,8 +56,7 @@ #~(list "-GNinja" "-DDISTRIBUTOR=\"In-tree Guix channel\"" "-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO" - ;; Breakpad is not currently packaged for Guix. - "-DCRASH_REPORTER=OFF") + "-DCRASH_HANDLER=OFF") #:phases #~(modify-phases %standard-phases (replace 'build (lambda _ (invoke "cmake" "--build" "."))) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c95ecf7..4b13d45 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,7 +12,7 @@ add_subdirectory(io) add_subdirectory(widgets) add_subdirectory(ui) -if (CRASH_REPORTER) +if (CRASH_HANDLER) add_subdirectory(crash) endif() diff --git a/src/build/CMakeLists.txt b/src/build/CMakeLists.txt index bb35da9..62574d9 100644 --- a/src/build/CMakeLists.txt +++ b/src/build/CMakeLists.txt @@ -9,10 +9,10 @@ if (NOT DEFINED GIT_REVISION) ) endif() -if (CRASH_REPORTER) - set(CRASH_REPORTER_DEF 1) +if (CRASH_HANDLER) + set(CRASH_HANDLER_DEF 1) else() - set(CRASH_REPORTER_DEF 0) + set(CRASH_HANDLER_DEF 0) endif() if (DISTRIBUTOR_DEBUGINFO_AVAILABLE) diff --git a/src/build/build.hpp.in b/src/build/build.hpp.in index 66fb664..93e78a9 100644 --- a/src/build/build.hpp.in +++ b/src/build/build.hpp.in @@ -9,7 +9,7 @@ #define GIT_REVISION "@GIT_REVISION@" #define DISTRIBUTOR "@DISTRIBUTOR@" #define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@ -#define CRASH_REPORTER @CRASH_REPORTER_DEF@ +#define CRASH_HANDLER @CRASH_HANDLER_DEF@ #define BUILD_TYPE "@CMAKE_BUILD_TYPE@" #define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)" #define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@" diff --git a/src/core/instanceinfo.hpp b/src/core/instanceinfo.hpp index d462f6e..977e4c2 100644 --- a/src/core/instanceinfo.hpp +++ b/src/core/instanceinfo.hpp @@ -35,6 +35,8 @@ namespace qs::crash { struct CrashInfo { int logFd = -1; + int traceFd = -1; + int infoFd = -1; static CrashInfo INSTANCE; // NOLINT }; diff --git a/src/crash/CMakeLists.txt b/src/crash/CMakeLists.txt index 7fdd830..a891ee9 100644 --- a/src/crash/CMakeLists.txt +++ b/src/crash/CMakeLists.txt @@ -6,12 +6,51 @@ qt_add_library(quickshell-crash STATIC qs_pch(quickshell-crash SET large) -find_package(PkgConfig REQUIRED) -pkg_check_modules(breakpad REQUIRED IMPORTED_TARGET breakpad) -# only need client?? take only includes from pkg config todo -target_link_libraries(quickshell-crash PRIVATE PkgConfig::breakpad -lbreakpad_client) +if (VENDOR_CPPTRACE) + message(STATUS "Vendoring cpptrace...") + include(FetchContent) + + # For use without internet access see: https://cmake.org/cmake/help/latest/module/FetchContent.html#variable:FETCHCONTENT_SOURCE_DIR_%3CuppercaseName%3E + FetchContent_Declare( + cpptrace + GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git + GIT_TAG v1.0.4 + ) + + set(CPPTRACE_UNWIND_WITH_LIBUNWIND TRUE) + FetchContent_MakeAvailable(cpptrace) +else () + find_package(cpptrace REQUIRED) + + # useful for cross after you have already checked cpptrace is built correctly + if (NOT DO_NOT_CHECK_CPPTRACE_USABILITY) + try_run(CPPTRACE_SIGNAL_SAFE_UNWIND CPPTRACE_SIGNAL_SAFE_UNWIND_COMP + SOURCE_FROM_CONTENT check.cxx " + #include + int main() { + return cpptrace::can_signal_safe_unwind() ? 0 : 1; + } + " + LOG_DESCRIPTION "Checking ${CPPTRACE_SIGNAL_SAFE_UNWIND}" + LINK_LIBRARIES cpptrace::cpptrace + COMPILE_OUTPUT_VARIABLE CPPTRACE_SIGNAL_SAFE_UNWIND_LOG + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + + if (NOT CPPTRACE_SIGNAL_SAFE_UNWIND_COMP) + message(STATUS "${CPPTRACE_SIGNAL_SAFE_UNWIND_LOG}") + message(FATAL_ERROR "Failed to compile cpptrace signal safe unwind tester.") + endif() + + if (NOT CPPTRACE_SIGNAL_SAFE_UNWIND EQUAL 0) + message(STATUS "Cpptrace signal safe unwind test exited with: ${CPPTRACE_SIGNAL_SAFE_UNWIND}") + message(FATAL_ERROR "Cpptrace was built without CPPTRACE_UNWIND_WITH_LIBUNWIND set to true. Enable libunwind support in the package or set VENDOR_CPPTRACE to true when building Quickshell.") + endif() + endif () +endif () # quick linked for pch compat -target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt::Quick Qt::Widgets) +target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt::Quick Qt::Widgets cpptrace::cpptrace) target_link_libraries(quickshell PRIVATE quickshell-crash) diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index 0baa8e6..fd40f94 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -1,12 +1,12 @@ #include "handler.hpp" +#include #include +#include #include #include -#include -#include -#include -#include +#include +#include #include #include #include @@ -19,98 +19,60 @@ extern char** environ; // NOLINT -using namespace google_breakpad; - namespace qs::crash { namespace { + QS_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg); -} -struct CrashHandlerPrivate { - ExceptionHandler* exceptionHandler = nullptr; - int minidumpFd = -1; - int infoFd = -1; +void writeEnvInt(char* buf, const char* name, int value) { + // NOLINTBEGIN (cppcoreguidelines-pro-bounds-pointer-arithmetic) + while (*name != '\0') *buf++ = *name++; + *buf++ = '='; - static bool minidumpCallback(const MinidumpDescriptor& descriptor, void* context, bool succeeded); -}; - -CrashHandler::CrashHandler(): d(new CrashHandlerPrivate()) {} - -void CrashHandler::init() { - // MinidumpDescriptor has no move constructor and the copy constructor breaks fds. - auto createHandler = [this](const MinidumpDescriptor& desc) { - this->d->exceptionHandler = new ExceptionHandler( - desc, - nullptr, - &CrashHandlerPrivate::minidumpCallback, - this->d, - true, - -1 - ); - }; - - qCDebug(logCrashHandler) << "Starting crash handler..."; - - this->d->minidumpFd = memfd_create("quickshell:minidump", MFD_CLOEXEC); - - if (this->d->minidumpFd == -1) { - qCCritical( - logCrashHandler - ) << "Failed to allocate minidump memfd, minidumps will be saved in the working directory."; - createHandler(MinidumpDescriptor(".")); - } else { - qCDebug(logCrashHandler) << "Created memfd" << this->d->minidumpFd - << "for holding possible minidumps."; - createHandler(MinidumpDescriptor(this->d->minidumpFd)); + if (value < 0) { + *buf++ = '-'; + value = -value; } - qCInfo(logCrashHandler) << "Crash handler initialized."; -} - -void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) { - this->d->infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC); - - if (this->d->infoFd == -1) { - qCCritical( - logCrashHandler - ) << "Failed to allocate instance info memfd, crash recovery will not work."; + if (value == 0) { + *buf++ = '0'; + *buf = '\0'; return; } - QFile file; - - if (!file.open(this->d->infoFd, QFile::ReadWrite)) { - qCCritical( - logCrashHandler - ) << "Failed to open instance info memfd, crash recovery will not work."; + auto* start = buf; + while (value > 0) { + *buf++ = static_cast('0' + (value % 10)); + value /= 10; } - QDataStream ds(&file); - ds << info; - file.flush(); - - qCDebug(logCrashHandler) << "Stored instance info in memfd" << this->d->infoFd; + *buf = '\0'; + std::reverse(start, buf); + // NOLINTEND } -CrashHandler::~CrashHandler() { - delete this->d->exceptionHandler; - delete this->d; -} - -bool CrashHandlerPrivate::minidumpCallback( - const MinidumpDescriptor& /*descriptor*/, - void* context, - bool /*success*/ +void signalHandler( + int sig, + siginfo_t* /*info*/, // NOLINT (misc-include-cleaner) + void* /*context*/ ) { - // A fork that just dies to ensure the coredump is caught by the system. - auto coredumpPid = fork(); + if (CrashInfo::INSTANCE.traceFd != -1) { + auto traceBuffer = std::array(); + auto frameCount = cpptrace::safe_generate_raw_trace(traceBuffer.data(), traceBuffer.size(), 1); - if (coredumpPid == 0) { - return false; + for (size_t i = 0; i < static_cast(frameCount); i++) { + auto frame = cpptrace::safe_object_frame(); + cpptrace::get_safe_object_frame(traceBuffer[i], &frame); + write(CrashInfo::INSTANCE.traceFd, &frame, sizeof(cpptrace::safe_object_frame)); + } } - auto* self = static_cast(context); + auto coredumpPid = fork(); + if (coredumpPid == 0) { + raise(sig); + _exit(-1); + } auto exe = std::array(); if (readlink("/proc/self/exe", exe.data(), exe.size() - 1) == -1) { @@ -123,17 +85,19 @@ bool CrashHandlerPrivate::minidumpCallback( auto env = std::array(); auto envi = 0; - auto infoFd = dup(self->infoFd); - auto infoFdStr = std::array(); - memcpy(infoFdStr.data(), "__QUICKSHELL_CRASH_INFO_FD=-1" /*\0*/, 30); - if (infoFd != -1) my_uitos(&infoFdStr[27], infoFd, 10); + // dup to remove CLOEXEC + auto infoFdStr = std::array(); + writeEnvInt(infoFdStr.data(), "__QUICKSHELL_CRASH_INFO_FD", dup(CrashInfo::INSTANCE.infoFd)); env[envi++] = infoFdStr.data(); - auto corePidStr = std::array(); - memcpy(corePidStr.data(), "__QUICKSHELL_CRASH_DUMP_PID=-1" /*\0*/, 31); - if (coredumpPid != -1) my_uitos(&corePidStr[28], coredumpPid, 10); + auto corePidStr = std::array(); + writeEnvInt(corePidStr.data(), "__QUICKSHELL_CRASH_DUMP_PID", coredumpPid); env[envi++] = corePidStr.data(); + auto sigStr = std::array(); + writeEnvInt(sigStr.data(), "__QUICKSHELL_CRASH_SIGNAL", sig); + env[envi++] = sigStr.data(); + auto populateEnv = [&]() { auto senvi = 0; while (envi != 4095) { @@ -145,30 +109,18 @@ bool CrashHandlerPrivate::minidumpCallback( env[envi] = nullptr; }; - sigset_t sigset; - sigemptyset(&sigset); // NOLINT (include) - sigprocmask(SIG_SETMASK, &sigset, nullptr); // NOLINT - auto pid = fork(); if (pid == -1) { perror("Failed to fork and launch crash reporter.\n"); - return false; + _exit(-1); } else if (pid == 0) { + // dup to remove CLOEXEC - // if already -1 will return -1 - auto dumpFd = dup(self->minidumpFd); - auto logFd = dup(CrashInfo::INSTANCE.logFd); - - // allow up to 10 digits, which should never happen - auto dumpFdStr = std::array(); - auto logFdStr = std::array(); - - memcpy(dumpFdStr.data(), "__QUICKSHELL_CRASH_DUMP_FD=-1" /*\0*/, 30); - memcpy(logFdStr.data(), "__QUICKSHELL_CRASH_LOG_FD=-1" /*\0*/, 29); - - if (dumpFd != -1) my_uitos(&dumpFdStr[27], dumpFd, 10); - if (logFd != -1) my_uitos(&logFdStr[26], logFd, 10); + auto dumpFdStr = std::array(); + auto logFdStr = std::array(); + writeEnvInt(dumpFdStr.data(), "__QUICKSHELL_CRASH_DUMP_FD", dup(CrashInfo::INSTANCE.traceFd)); + writeEnvInt(logFdStr.data(), "__QUICKSHELL_CRASH_LOG_FD", dup(CrashInfo::INSTANCE.logFd)); env[envi++] = dumpFdStr.data(); env[envi++] = logFdStr.data(); @@ -185,8 +137,83 @@ bool CrashHandlerPrivate::minidumpCallback( perror("Failed to relaunch quickshell.\n"); _exit(-1); } +} - return false; // should make sure it hits the system coredump handler +} // namespace + +void CrashHandler::init() { + qCDebug(logCrashHandler) << "Starting crash handler..."; + + CrashInfo::INSTANCE.traceFd = memfd_create("quickshell:trace", MFD_CLOEXEC); + + if (CrashInfo::INSTANCE.traceFd == -1) { + qCCritical(logCrashHandler) << "Failed to allocate trace memfd, stack traces will not be " + "available in crash reports."; + } else { + qCDebug(logCrashHandler) << "Created memfd" << CrashInfo::INSTANCE.traceFd + << "for holding possible stack traces."; + } + + { + // Preload anything dynamically linked to avoid malloc etc in the dynamic loader. + // See cpptrace documentation for more information. + auto buffer = std::array(); + cpptrace::safe_generate_raw_trace(buffer.data(), buffer.size()); + auto frame = cpptrace::safe_object_frame(); + cpptrace::get_safe_object_frame(buffer[0], &frame); + } + + // NOLINTBEGIN (misc-include-cleaner) + + // Set up alternate signal stack for stack overflow handling + auto ss = stack_t(); + ss.ss_sp = new char[SIGSTKSZ]; + ; + ss.ss_size = SIGSTKSZ; + ss.ss_flags = 0; + sigaltstack(&ss, nullptr); + + // Install signal handlers + struct sigaction sa {}; + sa.sa_sigaction = &signalHandler; + sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESETHAND; + sigemptyset(&sa.sa_mask); + + sigaction(SIGSEGV, &sa, nullptr); + sigaction(SIGABRT, &sa, nullptr); + sigaction(SIGFPE, &sa, nullptr); + sigaction(SIGILL, &sa, nullptr); + sigaction(SIGBUS, &sa, nullptr); + sigaction(SIGTRAP, &sa, nullptr); + + // NOLINTEND (misc-include-cleaner) + + qCInfo(logCrashHandler) << "Crash handler initialized."; +} + +void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) { + CrashInfo::INSTANCE.infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC); + + if (CrashInfo::INSTANCE.infoFd == -1) { + qCCritical( + logCrashHandler + ) << "Failed to allocate instance info memfd, crash recovery will not work."; + return; + } + + QFile file; + + if (!file.open(CrashInfo::INSTANCE.infoFd, QFile::ReadWrite)) { + qCCritical( + logCrashHandler + ) << "Failed to open instance info memfd, crash recovery will not work."; + } + + QDataStream ds(&file); + ds << info; + file.flush(); + + qCDebug(logCrashHandler) << "Stored instance info in memfd" << CrashInfo::INSTANCE.infoFd; } } // namespace qs::crash diff --git a/src/crash/handler.hpp b/src/crash/handler.hpp index 2a1d86f..9488d71 100644 --- a/src/crash/handler.hpp +++ b/src/crash/handler.hpp @@ -5,19 +5,10 @@ #include "../core/instanceinfo.hpp" namespace qs::crash { -struct CrashHandlerPrivate; - class CrashHandler { public: - explicit CrashHandler(); - ~CrashHandler(); - Q_DISABLE_COPY_MOVE(CrashHandler); - - void init(); - void setRelaunchInfo(const RelaunchInfo& info); - -private: - CrashHandlerPrivate* d; + static void init(); + static void setRelaunchInfo(const RelaunchInfo& info); }; } // namespace qs::crash diff --git a/src/crash/interface.cpp b/src/crash/interface.cpp index 326216a..a3422d3 100644 --- a/src/crash/interface.cpp +++ b/src/crash/interface.cpp @@ -78,7 +78,7 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid) mainLayout->addWidget(new ReportLabel( "Github:", - "https://github.com/quickshell-mirror/quickshell/issues/new?template=crash.yml", + "https://github.com/quickshell-mirror/quickshell/issues/new?template=crash2.yml", this )); @@ -114,7 +114,7 @@ void CrashReporterGui::openFolder() { void CrashReporterGui::openReportUrl() { QDesktopServices::openUrl( - QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash.yml") + QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml") ); } diff --git a/src/crash/main.cpp b/src/crash/main.cpp index 6571660..c406ba6 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -1,7 +1,10 @@ #include "main.hpp" #include #include +#include +#include +#include #include #include #include @@ -13,13 +16,17 @@ #include #include #include +#include #include #include +#include #include "../core/instanceinfo.hpp" #include "../core/logcat.hpp" #include "../core/logging.hpp" +#include "../core/logging_p.hpp" #include "../core/paths.hpp" +#include "../core/ringbuf.hpp" #include "build.hpp" #include "interface.hpp" @@ -61,6 +68,76 @@ int tryDup(int fd, const QString& path) { return 0; } +QString readRecentLogs(int logFd, int maxLines, qint64 maxAgeSecs) { + QFile file; + if (!file.open(logFd, QFile::ReadOnly, QFile::AutoCloseHandle)) { + return QStringLiteral("(failed to open log fd)\n"); + } + + file.seek(0); + + qs::log::EncodedLogReader reader; + reader.setDevice(&file); + + bool readable = false; + quint8 logVersion = 0; + quint8 readerVersion = 0; + if (!reader.readHeader(&readable, &logVersion, &readerVersion) || !readable) { + return QStringLiteral("(failed to read log header)\n"); + } + + // Read all messages, keeping last maxLines in a ring buffer + auto tail = RingBuffer(maxLines); + qs::log::LogMessage message; + while (reader.read(&message)) { + tail.emplace(message); + } + + if (tail.size() == 0) { + return QStringLiteral("(no logs)\n"); + } + + // Filter to only messages within maxAgeSecs of the newest message + auto cutoff = tail.at(0).time.addSecs(-maxAgeSecs); + + QString result; + auto stream = QTextStream(&result); + for (auto i = tail.size() - 1; i != -1; i--) { + if (tail.at(i).time < cutoff) continue; + qs::log::LogMessage::formatMessage(stream, tail.at(i), false, true); + stream << '\n'; + } + + if (result.isEmpty()) { + return QStringLiteral("(no recent logs)\n"); + } + + return result; +} + +cpptrace::stacktrace resolveStacktrace(int dumpFd) { + QFile sourceFile; + if (!sourceFile.open(dumpFd, QFile::ReadOnly, QFile::AutoCloseHandle)) { + qCCritical(logCrashReporter) << "Failed to open trace memfd."; + return {}; + } + + sourceFile.seek(0); + auto data = sourceFile.readAll(); + + auto frameCount = static_cast(data.size()) / sizeof(cpptrace::safe_object_frame); + if (frameCount == 0) return {}; + + const auto* frames = reinterpret_cast(data.constData()); + + cpptrace::object_trace objectTrace; + for (size_t i = 0; i < frameCount; i++) { + objectTrace.frames.push_back(frames[i].resolve()); // NOLINT + } + + return objectTrace.resolve(); +} + void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { qCDebug(logCrashReporter) << "Recording crash information at" << crashDir.path(); @@ -71,32 +148,25 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { } auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt(); + auto crashSignal = qEnvironmentVariable("__QUICKSHELL_CRASH_SIGNAL").toInt(); auto dumpFd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD").toInt(); auto logFd = qEnvironmentVariable("__QUICKSHELL_CRASH_LOG_FD").toInt(); - qCDebug(logCrashReporter) << "Saving minidump from fd" << dumpFd; - auto dumpDupStatus = tryDup(dumpFd, crashDir.filePath("minidump.dmp.log")); - if (dumpDupStatus != 0) { - qCCritical(logCrashReporter) << "Failed to write minidump:" << dumpDupStatus; - } + qCDebug(logCrashReporter) << "Resolving stacktrace from fd" << dumpFd; + auto stacktrace = resolveStacktrace(dumpFd); - qCDebug(logCrashReporter) << "Saving log from fd" << logFd; - auto logDupStatus = tryDup(logFd, crashDir.filePath("log.qslog.log")); + qCDebug(logCrashReporter) << "Reading recent log lines from fd" << logFd; + auto logDupFd = dup(logFd); + auto recentLogs = readRecentLogs(logFd, 100, 10); + + qCDebug(logCrashReporter) << "Saving log from fd" << logDupFd; + auto logDupStatus = tryDup(logDupFd, crashDir.filePath("log.qslog.log")); if (logDupStatus != 0) { qCCritical(logCrashReporter) << "Failed to save log:" << logDupStatus; } - auto copyBinStatus = 0; - if (!DISTRIBUTOR_DEBUGINFO_AVAILABLE) { - qCDebug(logCrashReporter) << "Copying binary to crash folder"; - if (!QFile(QCoreApplication::applicationFilePath()).copy(crashDir.filePath("executable.txt"))) { - copyBinStatus = 1; - qCCritical(logCrashReporter) << "Failed to copy binary."; - } - } - { - auto extraInfoFile = QFile(crashDir.filePath("info.txt")); + auto extraInfoFile = QFile(crashDir.filePath("report.txt")); if (!extraInfoFile.open(QFile::WriteOnly)) { qCCritical(logCrashReporter) << "Failed to open crash info file for writing."; } else { @@ -111,16 +181,12 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { stream << "\n===== Runtime Information =====\n"; stream << "Runtime Qt Version: " << qVersion() << '\n'; + stream << "Signal: " << strsignal(crashSignal) << " (" << crashSignal << ")\n"; // NOLINT stream << "Crashed process ID: " << crashProc << '\n'; stream << "Run ID: " << instance.instanceId << '\n'; stream << "Shell ID: " << instance.shellId << '\n'; stream << "Config Path: " << instance.configPath << '\n'; - stream << "\n===== Report Integrity =====\n"; - stream << "Minidump save status: " << dumpDupStatus << '\n'; - stream << "Log save status: " << logDupStatus << '\n'; - stream << "Binary copy status: " << copyBinStatus << '\n'; - stream << "\n===== System Information =====\n\n"; stream << "/etc/os-release:"; auto osReleaseFile = QFile("/etc/os-release"); @@ -140,6 +206,18 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { stream << "FAILED TO OPEN\n"; } + stream << "\n===== Stacktrace =====\n"; + if (stacktrace.empty()) { + stream << "(no trace available)\n"; + } else { + auto formatter = cpptrace::formatter().header(std::string()); + auto traceStr = formatter.format(stacktrace); + stream << QString::fromStdString(traceStr) << '\n'; + } + + stream << "\n===== Log Tail =====\n"; + stream << recentLogs; + extraInfoFile.close(); } } diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index f269f61..ee7ca64 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -27,7 +27,7 @@ #include "build.hpp" #include "launch_p.hpp" -#if CRASH_REPORTER +#if CRASH_HANDLER #include "../crash/handler.hpp" #endif @@ -137,13 +137,12 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio .display = getDisplayConnection(), }; -#if CRASH_REPORTER - auto crashHandler = crash::CrashHandler(); - crashHandler.init(); +#if CRASH_HANDLER + crash::CrashHandler::init(); { auto* log = LogManager::instance(); - crashHandler.setRelaunchInfo({ + crash::CrashHandler::setRelaunchInfo({ .instance = InstanceInfo::CURRENT, .noColor = !log->colorLogs, .timestamp = log->timestampLogs, diff --git a/src/launch/main.cpp b/src/launch/main.cpp index 7a801fc..a324e09 100644 --- a/src/launch/main.cpp +++ b/src/launch/main.cpp @@ -16,7 +16,7 @@ #include "build.hpp" #include "launch_p.hpp" -#if CRASH_REPORTER +#if CRASH_HANDLER #include "../crash/main.hpp" #endif @@ -25,7 +25,7 @@ namespace qs::launch { namespace { void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) { -#if CRASH_REPORTER +#if CRASH_HANDLER auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD"); if (!lastInfoFdStr.isEmpty()) { @@ -104,7 +104,7 @@ void exitDaemon(int code) { int main(int argc, char** argv) { QCoreApplication::setApplicationName("quickshell"); -#if CRASH_REPORTER +#if CRASH_HANDLER qsCheckCrash(argc, argv); #endif From a849a88893c71d409aecef0b999e6cc3d9b50034 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 3 Mar 2026 00:40:36 -0800 Subject: [PATCH 05/26] build: remove DISTRIBUTOR_DEBUGINFO_AVAILABLE --- BUILD.md | 10 +--------- CMakeLists.txt | 1 - changelog/next.md | 1 + src/build/CMakeLists.txt | 6 ------ src/build/build.hpp.in | 1 - src/crash/handler.cpp | 14 ++++++++++++-- 6 files changed, 14 insertions(+), 19 deletions(-) diff --git a/BUILD.md b/BUILD.md index 6a3f422..29aecac 100644 --- a/BUILD.md +++ b/BUILD.md @@ -15,15 +15,7 @@ Please make this descriptive enough to identify your specific package, for examp - `Nixpkgs` - `Fedora COPR (errornointernet/quickshell)` -`-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=YES/NO` - -If we can retrieve binaries and debug information for the package without actually running your -distribution (e.g. from an website), and you would like to strip the binary, please set this to `YES`. - -If we cannot retrieve debug information, please set this to `NO` and -**ensure you aren't distributing stripped (non debuggable) binaries**. - -In both cases you should build with `-DCMAKE_BUILD_TYPE=RelWithDebInfo` (then split or keep the debuginfo). +Please leave at least symbol names attached to the binary for debugging purposes. ### QML Module dir Currently all QML modules are statically linked to quickshell, but this is where diff --git a/CMakeLists.txt b/CMakeLists.txt index fabda0e..d57e322 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,7 +40,6 @@ string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}") message(STATUS "Quickshell configuration") message(STATUS " Distributor: ${DISTRIBUTOR}") -boption(DISTRIBUTOR_DEBUGINFO_AVAILABLE "Distributor provided debuginfo" NO) boption(NO_PCH "Disable precompild headers (dev)" OFF) boption(BUILD_TESTING "Build tests (dev)" OFF) boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang diff --git a/changelog/next.md b/changelog/next.md index 2083462..0feffe1 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -56,3 +56,4 @@ set shell id. - `glib` and `polkit` have been added as dependencies when compiling with polkit agent support. - `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support). - `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore. +- `DISTRIBUTOR_DEBUGINFO_AVAILABLE` was removed as it is no longer important without breakpad. diff --git a/src/build/CMakeLists.txt b/src/build/CMakeLists.txt index 62574d9..c1ffa59 100644 --- a/src/build/CMakeLists.txt +++ b/src/build/CMakeLists.txt @@ -15,12 +15,6 @@ else() set(CRASH_HANDLER_DEF 0) endif() -if (DISTRIBUTOR_DEBUGINFO_AVAILABLE) - set(DEBUGINFO_AVAILABLE 1) -else() - set(DEBUGINFO_AVAILABLE 0) -endif() - configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES) target_include_directories(quickshell-build INTERFACE ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/src/build/build.hpp.in b/src/build/build.hpp.in index 93e78a9..2ab2db2 100644 --- a/src/build/build.hpp.in +++ b/src/build/build.hpp.in @@ -8,7 +8,6 @@ #define QS_UNRELEASED_FEATURES "@UNRELEASED_FEATURES@" #define GIT_REVISION "@GIT_REVISION@" #define DISTRIBUTOR "@DISTRIBUTOR@" -#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@ #define CRASH_HANDLER @CRASH_HANDLER_DEF@ #define BUILD_TYPE "@CMAKE_BUILD_TYPE@" #define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)" diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index fd40f94..c875c2e 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -1,6 +1,7 @@ #include "handler.hpp" #include #include +#include #include #include #include @@ -64,8 +65,18 @@ void signalHandler( for (size_t i = 0; i < static_cast(frameCount); i++) { auto frame = cpptrace::safe_object_frame(); cpptrace::get_safe_object_frame(traceBuffer[i], &frame); - write(CrashInfo::INSTANCE.traceFd, &frame, sizeof(cpptrace::safe_object_frame)); + + auto* wptr = reinterpret_cast(&frame); + auto* end = wptr + sizeof(cpptrace::safe_object_frame); // NOLINT + while (wptr != end) { + auto r = write(CrashInfo::INSTANCE.traceFd, &frame, sizeof(cpptrace::safe_object_frame)); + if (r < 0 && errno == EINTR) continue; + if (r <= 0) goto fail; + wptr += r; // NOLINT + } } + + fail:; } auto coredumpPid = fork(); @@ -168,7 +179,6 @@ void CrashHandler::init() { // Set up alternate signal stack for stack overflow handling auto ss = stack_t(); ss.ss_sp = new char[SIGSTKSZ]; - ; ss.ss_size = SIGSTKSZ; ss.ss_flags = 0; sigaltstack(&ss, nullptr); From 5721955686a474b814c27bc0ec743f86e473ac4f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 4 Mar 2026 23:26:33 -0800 Subject: [PATCH 06/26] services/pipewire: ignore ENOENT errors Pipewire describes all errors as fatal, however these just aren't, don't seem to be squashable, and resetting for them breaks users. --- src/services/pipewire/core.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/services/pipewire/core.cpp b/src/services/pipewire/core.cpp index e40bc54..5077abe 100644 --- a/src/services/pipewire/core.cpp +++ b/src/services/pipewire/core.cpp @@ -143,12 +143,17 @@ void PwCore::onSync(void* data, quint32 id, qint32 seq) { void PwCore::onError(void* data, quint32 id, qint32 /*seq*/, qint32 res, const char* message) { auto* self = static_cast(data); - if (message != nullptr) { - qCWarning(logLoop) << "Fatal pipewire error on object" << id << "with code" << res << message; - } else { - qCWarning(logLoop) << "Fatal pipewire error on object" << id << "with code" << res; + // Pipewire's documentation describes the error event as being fatal, however it isn't. + // We're not sure what causes these ENOENTs on device removal, presumably something in + // the teardown sequence, but they're harmless. Attempting to handle them as a fatal + // error causes unnecessary triggers for shells. + if (res == -ENOENT) { + qCDebug(logLoop) << "Pipewire ENOENT on object" << id << "with code" << res << message; + return; } + qCWarning(logLoop) << "Pipewire error on object" << id << "with code" << res << message; + emit self->fatalError(); } From c03030019100718d473ae86c89656e98124f5b3a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 6 Mar 2026 01:39:24 -0800 Subject: [PATCH 07/26] core/desktopentry: preserve desktop action order --- changelog/next.md | 1 + src/core/desktopentry.cpp | 31 ++++++++++++++++++++++--------- src/core/desktopentry.hpp | 6 +++--- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 0feffe1..ef63323 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -50,6 +50,7 @@ set shell id. - Fixed ClippingRectangle related crashes. - Fixed crashes when monitors are unplugged. - Fixed crashes when default pipewire devices are lost. +- Desktop action order is now preserved. ## Packaging Changes diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 2dbafea..637f758 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -107,7 +107,10 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& auto groupName = QString(); auto entries = QHash>(); - auto finishCategory = [&data, &groupName, &entries]() { + auto actionOrder = QStringList(); + auto pendingActions = QHash(); + + auto finishCategory = [&data, &groupName, &entries, &actionOrder, &pendingActions]() { if (groupName == "Desktop Entry") { if (entries.value("Type").second != "Application") return; @@ -129,9 +132,10 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& else if (key == "Terminal") data.terminal = value == "true"; else if (key == "Categories") data.categories = value.split(u';', Qt::SkipEmptyParts); else if (key == "Keywords") data.keywords = value.split(u';', Qt::SkipEmptyParts); + else if (key == "Actions") actionOrder = value.split(u';', Qt::SkipEmptyParts); } } else if (groupName.startsWith("Desktop Action ")) { - auto actionName = groupName.sliced(16); + auto actionName = groupName.sliced(15); DesktopActionData action; action.id = actionName; @@ -147,7 +151,7 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& } } - data.actions.insert(actionName, action); + pendingActions.insert(actionName, action); } entries.clear(); @@ -193,6 +197,13 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& } finishCategory(); + + for (const auto& actionId: actionOrder) { + if (pendingActions.contains(actionId)) { + data.actions.append(pendingActions.value(actionId)); + } + } + return data; } @@ -216,17 +227,18 @@ void DesktopEntry::updateState(const ParsedDesktopEntryData& newState) { this->updateActions(newState.actions); } -void DesktopEntry::updateActions(const QHash& newActions) { +void DesktopEntry::updateActions(const QVector& newActions) { auto old = this->mActions; + this->mActions.clear(); - for (const auto& [key, d]: newActions.asKeyValueRange()) { + for (const auto& d: newActions) { DesktopAction* act = nullptr; - if (auto found = old.find(key); found != old.end()) { - act = found.value(); + auto found = std::ranges::find(old, d.id, &DesktopAction::mId); + if (found != old.end()) { + act = *found; old.erase(found); } else { act = new DesktopAction(d.id, this); - this->mActions.insert(key, act); } Qt::beginPropertyUpdateGroup(); @@ -237,6 +249,7 @@ void DesktopEntry::updateActions(const QHash& newAct Qt::endPropertyUpdateGroup(); act->mEntries = d.entries; + this->mActions.append(act); } for (auto* leftover: old) { @@ -250,7 +263,7 @@ void DesktopEntry::execute() const { bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); } -QVector DesktopEntry::actions() const { return this->mActions.values(); } +QVector DesktopEntry::actions() const { return this->mActions; } QVector DesktopEntry::parseExecString(const QString& execString) { QVector arguments; diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index 623019d..0d1eff2 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -43,7 +43,7 @@ struct ParsedDesktopEntryData { QVector categories; QVector keywords; QHash entries; - QHash actions; + QVector actions; }; /// A desktop entry. See @@DesktopEntries for details. @@ -164,10 +164,10 @@ public: // clang-format on private: - void updateActions(const QHash& newActions); + void updateActions(const QVector& newActions); ParsedDesktopEntryData state; - QHash mActions; + QVector mActions; friend class DesktopAction; }; From 6bcd3d9bbf81efdd8620409b268b90310bc1374c Mon Sep 17 00:00:00 2001 From: Moraxyc Date: Mon, 9 Feb 2026 22:03:45 +0800 Subject: [PATCH 08/26] nix: use libxcb directly --- default.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 59e68b0..02b8659 100644 --- a/default.nix +++ b/default.nix @@ -19,6 +19,7 @@ wayland-protocols, wayland-scanner, xorg, + libxcb ? xorg.libxcb, libdrm, libgbm ? null, vulkan-headers, @@ -88,7 +89,7 @@ ++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland ++ lib.optionals withWayland [ wayland wayland-protocols ] ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm vulkan-headers ] - ++ lib.optional withX11 xorg.libxcb + ++ lib.optional withX11 libxcb ++ lib.optional withPam pam ++ lib.optional withPipewire pipewire ++ lib.optionals withPolkit [ polkit glib ]; From 15a84097653593dd15fad59a56befc2b7bdc270d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 7 Mar 2026 14:36:59 -0800 Subject: [PATCH 09/26] ipc: handle null currentGeneration in IpcKillCommand::exec --- src/ipc/ipc.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ipc/ipc.cpp b/src/ipc/ipc.cpp index 40e8f0c..4bfea4c 100644 --- a/src/ipc/ipc.cpp +++ b/src/ipc/ipc.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -127,7 +128,9 @@ int IpcClient::connect(const QString& id, const std::functionquit(); + auto* generation = EngineGeneration::currentGeneration(); + if (generation) generation->quit(); + else QCoreApplication::exit(0); } } // namespace qs::ipc From cf1a2aeb2d01e446346fcd37c4b8f4e7d40d6f2c Mon Sep 17 00:00:00 2001 From: -k Date: Mon, 9 Mar 2026 09:11:52 -0400 Subject: [PATCH 10/26] wayland/toplevel: clear activeToplevel on deactivation --- changelog/next.md | 1 + src/wayland/toplevel_management/qml.cpp | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/changelog/next.md b/changelog/next.md index ef63323..fee8599 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -50,6 +50,7 @@ set shell id. - Fixed ClippingRectangle related crashes. - Fixed crashes when monitors are unplugged. - Fixed crashes when default pipewire devices are lost. +- Fixed ToplevelManager not clearing activeToplevel on deactivation. - Desktop action order is now preserved. ## Packaging Changes diff --git a/src/wayland/toplevel_management/qml.cpp b/src/wayland/toplevel_management/qml.cpp index 0eae3de..6a1d96b 100644 --- a/src/wayland/toplevel_management/qml.cpp +++ b/src/wayland/toplevel_management/qml.cpp @@ -161,7 +161,11 @@ void ToplevelManager::onToplevelReady(impl::ToplevelHandle* handle) { void ToplevelManager::onToplevelActiveChanged() { auto* toplevel = qobject_cast(this->sender()); - if (toplevel->activated()) this->setActiveToplevel(toplevel); + if (toplevel->activated()) { + this->setActiveToplevel(toplevel); + } else if (toplevel == this->mActiveToplevel) { + this->setActiveToplevel(nullptr); + } } void ToplevelManager::onToplevelClosed() { From bd6217927739a79c1c4ff279051f9625cd4b2b5e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 10 Mar 2026 00:54:45 -0700 Subject: [PATCH 11/26] all: retry incomplete socket reads Fixes greetd and hyprland ipc sockets reads being incomplete and breaking said integrations on slow machines. --- changelog/next.md | 1 + src/core/CMakeLists.txt | 1 + src/core/streamreader.cpp | 98 +++++++++++++++++ src/core/streamreader.hpp | 26 +++++ src/services/greetd/CMakeLists.txt | 2 +- src/services/greetd/connection.cpp | 133 ++++++++++++------------ src/services/greetd/connection.hpp | 3 + src/wayland/hyprland/ipc/CMakeLists.txt | 2 +- src/wayland/hyprland/ipc/connection.cpp | 9 +- src/wayland/hyprland/ipc/connection.hpp | 2 + src/x11/i3/ipc/CMakeLists.txt | 2 +- src/x11/i3/ipc/connection.cpp | 40 ++----- src/x11/i3/ipc/connection.hpp | 5 +- 13 files changed, 221 insertions(+), 103 deletions(-) create mode 100644 src/core/streamreader.cpp create mode 100644 src/core/streamreader.hpp diff --git a/changelog/next.md b/changelog/next.md index fee8599..4f550e8 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -52,6 +52,7 @@ set shell id. - Fixed crashes when default pipewire devices are lost. - Fixed ToplevelManager not clearing activeToplevel on deactivation. - Desktop action order is now preserved. +- Fixed partial socket reads in greetd and hyprland on slow machines. ## Packaging Changes diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index fb63f40..f0ca8ef 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -40,6 +40,7 @@ qt_add_library(quickshell-core STATIC scriptmodel.cpp colorquantizer.cpp toolsupport.cpp + streamreader.cpp ) qt_add_qml_module(quickshell-core diff --git a/src/core/streamreader.cpp b/src/core/streamreader.cpp new file mode 100644 index 0000000..1f66e29 --- /dev/null +++ b/src/core/streamreader.cpp @@ -0,0 +1,98 @@ +#include "streamreader.hpp" +#include + +#include +#include +#include + +void StreamReader::setDevice(QIODevice* device) { + this->reset(); + this->device = device; +} + +void StreamReader::startTransaction() { + this->cursor = 0; + this->failed = false; +} + +bool StreamReader::fill() { + auto available = this->device->bytesAvailable(); + if (available <= 0) return false; + auto oldSize = this->buffer.size(); + this->buffer.resize(oldSize + available); + auto bytesRead = this->device->read(this->buffer.data() + oldSize, available); // NOLINT + + if (bytesRead <= 0) { + this->buffer.resize(oldSize); + return false; + } + + this->buffer.resize(oldSize + bytesRead); + return true; +} + +QByteArray StreamReader::readBytes(qsizetype count) { + if (this->failed) return {}; + + auto needed = this->cursor + count; + + while (this->buffer.size() < needed) { + if (!this->fill()) { + this->failed = true; + return {}; + } + } + + auto result = this->buffer.mid(this->cursor, count); + this->cursor += count; + return result; +} + +QByteArray StreamReader::readUntil(char terminator) { + if (this->failed) return {}; + + auto searchFrom = this->cursor; + auto idx = this->buffer.indexOf(terminator, searchFrom); + + while (idx == -1) { + searchFrom = this->buffer.size(); + if (!this->fill()) { + this->failed = true; + return {}; + } + + idx = this->buffer.indexOf(terminator, searchFrom); + } + + auto length = idx - this->cursor + 1; + auto result = this->buffer.mid(this->cursor, length); + this->cursor += length; + return result; +} + +void StreamReader::readInto(char* ptr, qsizetype count) { + auto data = this->readBytes(count); + if (!data.isEmpty()) memcpy(ptr, data.data(), count); +} + +qint32 StreamReader::readI32() { + qint32 value = 0; + this->readInto(reinterpret_cast(&value), sizeof(qint32)); + return value; +} + +bool StreamReader::commitTransaction() { + if (this->failed) { + this->cursor = 0; + return false; + } + + this->buffer.remove(0, this->cursor); + this->cursor = 0; + return true; +} + +void StreamReader::reset() { + this->buffer.clear(); + this->cursor = 0; +} diff --git a/src/core/streamreader.hpp b/src/core/streamreader.hpp new file mode 100644 index 0000000..abf14ef --- /dev/null +++ b/src/core/streamreader.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +class StreamReader { +public: + void setDevice(QIODevice* device); + + void startTransaction(); + QByteArray readBytes(qsizetype count); + QByteArray readUntil(char terminator); + void readInto(char* ptr, qsizetype count); + qint32 readI32(); + bool commitTransaction(); + void reset(); + +private: + bool fill(); + + QIODevice* device = nullptr; + QByteArray buffer; + qsizetype cursor = 0; + bool failed = false; +}; diff --git a/src/services/greetd/CMakeLists.txt b/src/services/greetd/CMakeLists.txt index 2252f8c..a103531 100644 --- a/src/services/greetd/CMakeLists.txt +++ b/src/services/greetd/CMakeLists.txt @@ -12,7 +12,7 @@ qt_add_qml_module(quickshell-service-greetd install_qml_module(quickshell-service-greetd) # can't be Qt::Qml because generation.hpp pulls in gui types -target_link_libraries(quickshell-service-greetd PRIVATE Qt::Quick) +target_link_libraries(quickshell-service-greetd PRIVATE Qt::Quick quickshell-core) qs_module_pch(quickshell-service-greetd) diff --git a/src/services/greetd/connection.cpp b/src/services/greetd/connection.cpp index 7130870..3b8fa24 100644 --- a/src/services/greetd/connection.cpp +++ b/src/services/greetd/connection.cpp @@ -145,6 +145,7 @@ void GreetdConnection::setInactive() { QString GreetdConnection::user() const { return this->mUser; } void GreetdConnection::onSocketConnected() { + this->reader.setDevice(&this->socket); qCDebug(logGreetd) << "Connected to greetd socket."; if (this->mTargetActive) { @@ -160,82 +161,84 @@ void GreetdConnection::onSocketError(QLocalSocket::LocalSocketError error) { } void GreetdConnection::onSocketReady() { - qint32 length = 0; + while (true) { + this->reader.startTransaction(); + auto length = this->reader.readI32(); + auto text = this->reader.readBytes(length); + if (!this->reader.commitTransaction()) return; - this->socket.read(reinterpret_cast(&length), sizeof(qint32)); + auto json = QJsonDocument::fromJson(text).object(); + auto type = json.value("type").toString(); - auto text = this->socket.read(length); - auto json = QJsonDocument::fromJson(text).object(); - auto type = json.value("type").toString(); + qCDebug(logGreetd).noquote() << "Received greetd response:" << text; - qCDebug(logGreetd).noquote() << "Received greetd response:" << text; + if (type == "success") { + switch (this->mState) { + case GreetdState::Authenticating: + qCDebug(logGreetd) << "Authentication complete."; + this->mState = GreetdState::ReadyToLaunch; + emit this->stateChanged(); + emit this->readyToLaunch(); + break; + case GreetdState::Launching: + qCDebug(logGreetd) << "Target session set successfully."; + this->mState = GreetdState::Launched; + emit this->stateChanged(); + emit this->launched(); - if (type == "success") { - switch (this->mState) { - case GreetdState::Authenticating: - qCDebug(logGreetd) << "Authentication complete."; - this->mState = GreetdState::ReadyToLaunch; - emit this->stateChanged(); - emit this->readyToLaunch(); - break; - case GreetdState::Launching: - qCDebug(logGreetd) << "Target session set successfully."; - this->mState = GreetdState::Launched; - emit this->stateChanged(); - emit this->launched(); + if (this->mExitAfterLaunch) { + qCDebug(logGreetd) << "Quitting."; + EngineGeneration::currentGeneration()->quit(); + } - if (this->mExitAfterLaunch) { - qCDebug(logGreetd) << "Quitting."; - EngineGeneration::currentGeneration()->quit(); + break; + default: goto unexpected; + } + } else if (type == "error") { + auto errorType = json.value("error_type").toString(); + auto desc = json.value("description").toString(); + + // Special case this error in case a session was already running. + // This cancels and restarts the session. + if (errorType == "error" && desc == "a session is already being configured") { + qCDebug( + logGreetd + ) << "A session was already in progress, cancelling it and starting a new one."; + this->setActive(false); + this->setActive(true); + return; } - break; - default: goto unexpected; - } - } else if (type == "error") { - auto errorType = json.value("error_type").toString(); - auto desc = json.value("description").toString(); + if (errorType == "auth_error") { + emit this->authFailure(desc); + this->setActive(false); + } else if (errorType == "error") { + qCWarning(logGreetd) << "Greetd error occurred" << desc; + emit this->error(desc); + } else goto unexpected; - // Special case this error in case a session was already running. - // This cancels and restarts the session. - if (errorType == "error" && desc == "a session is already being configured") { - qCDebug( - logGreetd - ) << "A session was already in progress, cancelling it and starting a new one."; - this->setActive(false); - this->setActive(true); - return; - } + // errors terminate the session + this->setInactive(); + } else if (type == "auth_message") { + auto message = json.value("auth_message").toString(); + auto type = json.value("auth_message_type").toString(); + auto error = type == "error"; + auto responseRequired = type == "visible" || type == "secret"; + auto echoResponse = type != "secret"; - if (errorType == "auth_error") { - emit this->authFailure(desc); - this->setActive(false); - } else if (errorType == "error") { - qCWarning(logGreetd) << "Greetd error occurred" << desc; - emit this->error(desc); + this->mResponseRequired = responseRequired; + emit this->authMessage(message, error, responseRequired, echoResponse); + + if (!responseRequired) { + this->sendRequest({{"type", "post_auth_message_response"}}); + } } else goto unexpected; - // errors terminate the session - this->setInactive(); - } else if (type == "auth_message") { - auto message = json.value("auth_message").toString(); - auto type = json.value("auth_message_type").toString(); - auto error = type == "error"; - auto responseRequired = type == "visible" || type == "secret"; - auto echoResponse = type != "secret"; - - this->mResponseRequired = responseRequired; - emit this->authMessage(message, error, responseRequired, echoResponse); - - if (!responseRequired) { - this->sendRequest({{"type", "post_auth_message_response"}}); - } - } else goto unexpected; - - return; -unexpected: - qCCritical(logGreetd) << "Received unexpected greetd response" << text; - this->setActive(false); + continue; + unexpected: + qCCritical(logGreetd) << "Received unexpected greetd response" << text; + this->setActive(false); + } } void GreetdConnection::sendRequest(const QJsonObject& json) { diff --git a/src/services/greetd/connection.hpp b/src/services/greetd/connection.hpp index 0c1d1eb..89348dc 100644 --- a/src/services/greetd/connection.hpp +++ b/src/services/greetd/connection.hpp @@ -8,6 +8,8 @@ #include #include +#include "../../core/streamreader.hpp" + ///! State of the Greetd connection. /// See @@Greetd.state. class GreetdState: public QObject { @@ -74,4 +76,5 @@ private: bool mResponseRequired = false; QString mUser; QLocalSocket socket; + StreamReader reader; }; diff --git a/src/wayland/hyprland/ipc/CMakeLists.txt b/src/wayland/hyprland/ipc/CMakeLists.txt index fd01463..9e42520 100644 --- a/src/wayland/hyprland/ipc/CMakeLists.txt +++ b/src/wayland/hyprland/ipc/CMakeLists.txt @@ -15,7 +15,7 @@ qs_add_module_deps_light(quickshell-hyprland-ipc Quickshell) install_qml_module(quickshell-hyprland-ipc) -target_link_libraries(quickshell-hyprland-ipc PRIVATE Qt::Quick) +target_link_libraries(quickshell-hyprland-ipc PRIVATE Qt::Quick quickshell-core) if (WAYLAND_TOPLEVEL_MANAGEMENT) target_sources(quickshell-hyprland-ipc PRIVATE diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index ad091a6..d2d5105 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -93,6 +93,7 @@ void HyprlandIpc::eventSocketError(QLocalSocket::LocalSocketError error) const { void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) { if (state == QLocalSocket::ConnectedState) { + this->eventReader.setDevice(&this->eventSocket); qCInfo(logHyprlandIpc) << "Hyprland event socket connected."; emit this->connected(); } else if (state == QLocalSocket::UnconnectedState && this->valid) { @@ -104,11 +105,11 @@ void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) void HyprlandIpc::eventSocketReady() { while (true) { - auto rawEvent = this->eventSocket.readLine(); - if (rawEvent.isEmpty()) break; + this->eventReader.startTransaction(); + auto rawEvent = this->eventReader.readUntil('\n'); + if (!this->eventReader.commitTransaction()) return; - // remove trailing \n - rawEvent.truncate(rawEvent.length() - 1); + rawEvent.chop(1); // remove trailing \n auto splitIdx = rawEvent.indexOf(">>"); auto event = QByteArrayView(rawEvent.data(), splitIdx); auto data = QByteArrayView( diff --git a/src/wayland/hyprland/ipc/connection.hpp b/src/wayland/hyprland/ipc/connection.hpp index e15d5cd..ba1e7c9 100644 --- a/src/wayland/hyprland/ipc/connection.hpp +++ b/src/wayland/hyprland/ipc/connection.hpp @@ -14,6 +14,7 @@ #include "../../../core/model.hpp" #include "../../../core/qmlscreen.hpp" +#include "../../../core/streamreader.hpp" #include "../../../wayland/toplevel_management/handle.hpp" namespace qs::hyprland::ipc { @@ -139,6 +140,7 @@ private: static bool compareWorkspaces(HyprlandWorkspace* a, HyprlandWorkspace* b); QLocalSocket eventSocket; + StreamReader eventReader; QString mRequestSocketPath; QString mEventSocketPath; bool valid = false; diff --git a/src/x11/i3/ipc/CMakeLists.txt b/src/x11/i3/ipc/CMakeLists.txt index c228ae3..a073459 100644 --- a/src/x11/i3/ipc/CMakeLists.txt +++ b/src/x11/i3/ipc/CMakeLists.txt @@ -17,7 +17,7 @@ qs_add_module_deps_light(quickshell-i3-ipc Quickshell) install_qml_module(quickshell-i3-ipc) -target_link_libraries(quickshell-i3-ipc PRIVATE Qt::Quick) +target_link_libraries(quickshell-i3-ipc PRIVATE Qt::Quick quickshell-core) qs_module_pch(quickshell-i3-ipc SET large) diff --git a/src/x11/i3/ipc/connection.cpp b/src/x11/i3/ipc/connection.cpp index b765ebc..976167b 100644 --- a/src/x11/i3/ipc/connection.cpp +++ b/src/x11/i3/ipc/connection.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -15,9 +14,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -89,9 +86,6 @@ I3Ipc::I3Ipc(const QList& events): mEvents(events) { QObject::connect(&this->liveEventSocket, &QLocalSocket::readyRead, this, &I3Ipc::eventSocketReady); QObject::connect(&this->liveEventSocket, &QLocalSocket::connected, this, &I3Ipc::subscribe); // clang-format on - - this->liveEventSocketDs.setDevice(&this->liveEventSocket); - this->liveEventSocketDs.setByteOrder(static_cast(QSysInfo::ByteOrder)); } void I3Ipc::makeRequest(const QByteArray& request) { @@ -145,34 +139,21 @@ void I3Ipc::reconnectIPC() { } QVector I3Ipc::parseResponse() { - QVector> events; - const int magicLen = 6; + QVector events; - while (!this->liveEventSocketDs.atEnd()) { - this->liveEventSocketDs.startTransaction(); - this->liveEventSocketDs.startTransaction(); + while (true) { + this->eventReader.startTransaction(); + auto magic = this->eventReader.readBytes(6); + auto size = this->eventReader.readI32(); + auto type = this->eventReader.readI32(); + auto payload = this->eventReader.readBytes(size); + if (!this->eventReader.commitTransaction()) return events; - std::array buffer = {}; - qint32 size = 0; - qint32 type = EventCode::Unknown; - - this->liveEventSocketDs.readRawData(buffer.data(), magicLen); - this->liveEventSocketDs >> size; - this->liveEventSocketDs >> type; - - if (!this->liveEventSocketDs.commitTransaction()) break; - - QByteArray payload(size, Qt::Uninitialized); - - this->liveEventSocketDs.readRawData(payload.data(), size); - - if (!this->liveEventSocketDs.commitTransaction()) break; - - if (strncmp(buffer.data(), MAGIC.data(), 6) != 0) { + if (magic.size() < 6 || strncmp(magic.data(), MAGIC.data(), 6) != 0) { qCWarning(logI3Ipc) << "No magic sequence found in string."; this->reconnectIPC(); break; - }; + } if (I3IpcEvent::intToEvent(type) == EventCode::Unknown) { qCWarning(logI3Ipc) << "Received unknown event"; @@ -204,6 +185,7 @@ void I3Ipc::eventSocketError(QLocalSocket::LocalSocketError error) const { void I3Ipc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) { if (state == QLocalSocket::ConnectedState) { + this->eventReader.setDevice(&this->liveEventSocket); qCInfo(logI3Ipc) << "I3 event socket connected."; emit this->connected(); } else if (state == QLocalSocket::UnconnectedState && this->valid) { diff --git a/src/x11/i3/ipc/connection.hpp b/src/x11/i3/ipc/connection.hpp index 6100f7e..7d03ecd 100644 --- a/src/x11/i3/ipc/connection.hpp +++ b/src/x11/i3/ipc/connection.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -9,6 +8,8 @@ #include #include +#include "../../../core/streamreader.hpp" + namespace qs::i3::ipc { constexpr std::string MAGIC = "i3-ipc"; @@ -92,7 +93,7 @@ protected: QVector> parseResponse(); QLocalSocket liveEventSocket; - QDataStream liveEventSocketDs; + StreamReader eventReader; QString mSocketPath; bool valid = false; From 9a9c60525014bcdf83aace03db4b53c19168edcc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 11 Mar 2026 19:45:14 -0700 Subject: [PATCH 12/26] core: hash scanned files and don't trigger a reload if matching Nix builds often trip QFileSystemWatcher, causing random reloads. --- changelog/next.md | 1 + src/core/generation.cpp | 12 ++++++++++++ src/core/scan.cpp | 41 ++++++++++++++++++++++++++++++----------- src/core/scan.hpp | 5 +++++ 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 4f550e8..4883c93 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -33,6 +33,7 @@ set shell id. - IPC operations filter available instances to the current display connection by default. - PwNodeLinkTracker ignores sound level monitoring programs. - Replaced breakpad with cpptrace. +- Reloads are prevented if no file content has changed. ## Bug Fixes diff --git a/src/core/generation.cpp b/src/core/generation.cpp index c68af71..21febc3 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -209,6 +209,8 @@ bool EngineGeneration::setExtraWatchedFiles(const QVector& files) { for (const auto& file: files) { if (!this->scanner.scannedFiles.contains(file)) { this->extraWatchedFiles.append(file); + QByteArray data; + this->scanner.readAndHashFile(file, data); } } @@ -229,6 +231,11 @@ void EngineGeneration::onFileChanged(const QString& name) { auto fileInfo = QFileInfo(name); if (fileInfo.isFile() && fileInfo.size() == 0) return; + if (!this->scanner.hasFileContentChanged(name)) { + qCDebug(logQmlScanner) << "Ignoring file change with unchanged content:" << name; + return; + } + emit this->filesChanged(); } } @@ -237,6 +244,11 @@ void EngineGeneration::onDirectoryChanged() { // try to find any files that were just deleted from a replace operation for (auto& file: this->deletedWatchedFiles) { if (QFileInfo(file).exists()) { + if (!this->scanner.hasFileContentChanged(file)) { + qCDebug(logQmlScanner) << "Ignoring restored file with unchanged content:" << file; + continue; + } + emit this->filesChanged(); break; } diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 37b0fac..58da38c 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,25 @@ QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg); +bool QmlScanner::readAndHashFile(const QString& path, QByteArray& data) { + auto file = QFile(path); + if (!file.open(QFile::ReadOnly)) return false; + data = file.readAll(); + this->fileHashes.insert(path, QCryptographicHash::hash(data, QCryptographicHash::Md5)); + return true; +} + +bool QmlScanner::hasFileContentChanged(const QString& path) const { + auto it = this->fileHashes.constFind(path); + if (it == this->fileHashes.constEnd()) return true; + + auto file = QFile(path); + if (!file.open(QFile::ReadOnly)) return true; + + auto newHash = QCryptographicHash::hash(file.readAll(), QCryptographicHash::Md5); + return newHash != it.value(); +} + void QmlScanner::scanDir(const QDir& dir) { if (this->scannedDirs.contains(dir)) return; this->scannedDirs.push_back(dir); @@ -109,13 +129,13 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna qCDebug(logQmlScanner) << "Scanning qml file" << path; - auto file = QFile(path); - if (!file.open(QFile::ReadOnly | QFile::Text)) { + QByteArray fileData; + if (!this->readAndHashFile(path, fileData)) { qCWarning(logQmlScanner) << "Failed to open file" << path; return false; } - auto stream = QTextStream(&file); + auto stream = QTextStream(&fileData); auto imports = QVector(); bool inHeader = true; @@ -219,8 +239,6 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna postError("unclosed preprocessor if block"); } - file.close(); - if (isOverridden) { this->fileIntercepts.insert(path, overrideText); } @@ -257,8 +275,11 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna continue; } - if (import.endsWith(".js")) this->scannedFiles.push_back(cpath); - else this->scanDir(cpath); + if (import.endsWith(".js")) { + this->scannedFiles.push_back(cpath); + QByteArray jsData; + this->readAndHashFile(cpath, jsData); + } else this->scanDir(cpath); } return true; @@ -273,14 +294,12 @@ void QmlScanner::scanQmlRoot(const QString& path) { bool QmlScanner::scanQmlJson(const QString& path) { qCDebug(logQmlScanner) << "Scanning qml.json file" << path; - auto file = QFile(path); - if (!file.open(QFile::ReadOnly | QFile::Text)) { + QByteArray data; + if (!this->readAndHashFile(path, data)) { qCWarning(logQmlScanner) << "Failed to open file" << path; return false; } - auto data = file.readAll(); - // Importing this makes CI builds fail for some reason. QJsonParseError error; // NOLINT (misc-include-cleaner) auto json = QJsonDocument::fromJson(data, &error); diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 29f8f6a..26034e1 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -21,6 +22,7 @@ public: QVector scannedDirs; QVector scannedFiles; + QHash fileHashes; QHash fileIntercepts; struct ScanError { @@ -31,6 +33,9 @@ public: QVector scanErrors; + bool readAndHashFile(const QString& path, QByteArray& data); + [[nodiscard]] bool hasFileContentChanged(const QString& path) const; + private: QDir rootPath; From 706d6de7b0236cec2c25556e284b91104a4e834b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 12 Mar 2026 03:57:14 -0700 Subject: [PATCH 13/26] crash: unmask signals in coredump fork Fixes the fork just sticking around and not dumping a core. --- src/crash/handler.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index c875c2e..8f37085 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -81,6 +81,11 @@ void signalHandler( auto coredumpPid = fork(); if (coredumpPid == 0) { + // NOLINTBEGIN (misc-include-cleaner) + sigset_t set; + sigfillset(&set); + sigprocmask(SIG_UNBLOCK, &set, nullptr); + // NOLINTEND raise(sig); _exit(-1); } From 178c04b59cfc387efb90fbf2460f5171512ebfc4 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 13 Mar 2026 00:32:43 -0700 Subject: [PATCH 14/26] docs: revise contribution policy and related files --- BUILD.md | 10 +- CONTRIBUTING.md | 247 +++++------------------------------------------- HACKING.md | 226 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 +- 4 files changed, 263 insertions(+), 224 deletions(-) create mode 100644 HACKING.md diff --git a/BUILD.md b/BUILD.md index 29aecac..aa04bbd 100644 --- a/BUILD.md +++ b/BUILD.md @@ -67,7 +67,13 @@ Dependencies: `cpptrace` Note: `-DVENDOR_CPPTRACE=ON` can be set to vendor cpptrace using FetchContent. -When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the package manager or fetched with FetchContent. +When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the +package manager or fetched with FetchContent. + +*Please ensure binaries have usable symbols.* We do not necessarily need full debuginfo, but +leaving symbols in the binary is extremely helpful. You can check if symbols are useful +by sending a SIGSEGV to the process and ensuring symbols for the quickshell binary are present +in the trace. ### Jemalloc We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused @@ -236,7 +242,7 @@ Only `ninja` builds are tested, but makefiles may work. #### Configuring the build ```sh -$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here] +$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release [additional disable flags from above here] ``` Note that features you do not supply dependencies for MUST be disabled with their associated flags diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 39fab13..73e7931 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,235 +1,40 @@ -# Contributing / Development -Instructions for development setup and upstreaming patches. +# Contributing -If you just want to build or package quickshell see [BUILD.md](BUILD.md). +Thank you for taking the time to contribute. +To ensure nobody's time is wasted, please follow the rules below. -## Development +## Acceptable Code Contributions -Install the dependencies listed in [BUILD.md](BUILD.md). -You probably want all of them even if you don't use all of them -to ensure tests work correctly and avoid passing a bunch of configure -flags when you need to wipe the build directory. +- All changes submitted MUST be **fully understood by the submitter**. If you do not know why or how + your change works, do not submit it to be merged. You must be able to explain your reasoning + for every change. -Quickshell also uses `just` for common development command aliases. +- Changes MUST be submitted by a human who will be responsible for them. Changes submitted without + a human in the loop such as automated tooling and AI Agents are **strictly disallowed**. Accounts + responsible for such contribution attempts **will be banned**. -The dependencies are also available as a nix shell or nix flake which we recommend -using with nix-direnv. +- Changes MUST respect Quickshell's license and the license of any source works. Changes including + code from any other works must disclose the source of the code, explain why it was used, and + ensure the license is compatible. -Common aliases: -- `just configure [ [extra cmake args]]` (note that you must specify debug/release to specify extra args) -- `just build` - runs the build, configuring if not configured already. -- `just run [args]` - runs quickshell with the given arguments -- `just clean` - clean up build artifacts. `just clean build` is somewhat common. +- Changes must follow the guidelines outlined in [HACKING.md](HACKING.md) for style and substance. -### Formatting -All contributions should be formatted similarly to what already exists. -Group related functionality together. +- Changes must stand on their own as a unit. Do not make multiple unrelated changes in one PR. + Changes depending on prior merges should be marked as a draft. -Run the formatter using `just fmt`. -If the results look stupid, fix the clang-format file if possible, -or disable clang-format in the affected area -using `// clang-format off` and `// clang-format on`. +## Acceptable Non-code Contributions -#### Style preferences not caught by clang-format -These are flexible. You can ignore them if it looks or works better to -for one reason or another. +- Bug and crash reports. You must follow the instructions in the issue templates and provide the + information requested. -Use `auto` if the type of a variable can be deduced automatically, instead of -redeclaring the returned value's type. Additionally, auto should be used when a -constructor takes arguments. +- Feature requests can be made via Issues. Please check to ensure nobody else has requested the same feature. -```cpp -auto x = ; // ok -auto x = QString::number(3); // ok -QString x; // ok -QString x = "foo"; // ok -auto x = QString("foo"); // ok +- Do not make insubstantial or pointless changes. -auto x = QString(); // avoid -QString x(); // avoid -QString x("foo"); // avoid -``` +- Changes to project rules / policy / governance will not be entertained, except from significant + long-term contributors. These changes should not be addressed through contribution channels. -Put newlines around logical units of code, and after closing braces. If the -most reasonable logical unit of code takes only a single line, it should be -merged into the next single line logical unit if applicable. -```cpp -// multiple units -auto x = ; // unit 1 -auto y = ; // unit 2 +## Merge timelines -auto x = ; // unit 1 -emit this->y(); // unit 2 - -auto x1 = ; // unit 1 -auto x2 = ; // unit 1 -auto x3 = ; // unit 1 - -auto y1 = ; // unit 2 -auto y2 = ; // unit 2 -auto y3 = ; // unit 2 - -// one unit -auto x = ; -if (x...) { - // ... -} - -// if more than one variable needs to be used then add a newline -auto x = ; -auto y = ; - -if (x && y) { - // ... -} -``` - -Class formatting: -```cpp -//! Doc comment summary -/// Doc comment body -class Foo: public QObject { - // The Q_OBJECT macro comes first. Macros are ; terminated. - Q_OBJECT; - QML_ELEMENT; - QML_CLASSINFO(...); - // Properties must stay on a single line or the doc generator won't be able to pick them up - Q_PROPERTY(...); - /// Doc comment - Q_PROPERTY(...); - /// Doc comment - Q_PROPERTY(...); - -public: - // Classes should have explicit constructors if they aren't intended to - // implicitly cast. The constructor can be inline in the header if it has no body. - explicit Foo(QObject* parent = nullptr): QObject(parent) {} - - // Instance functions if applicable. - static Foo* instance(); - - // Member functions unrelated to properties come next - void function(); - void function(); - void function(); - - // Then Q_INVOKABLEs - Q_INVOKABLE function(); - /// Doc comment - Q_INVOKABLE function(); - /// Doc comment - Q_INVOKABLE function(); - - // Then property related functions, in the order (bindable, getter, setter). - // Related functions may be included here as well. Function bodies may be inline - // if they are a single expression. There should be a newline between each - // property's methods. - [[nodiscard]] QBindable bindableFoo() { return &this->bFoo; } - [[nodiscard]] T foo() const { return this->foo; } - void setFoo(); - - [[nodiscard]] T bar() const { return this->foo; } - void setBar(); - -signals: - // Signals that are not property change related go first. - // Property change signals go in property definition order. - void asd(); - void asd2(); - void fooChanged(); - void barChanged(); - -public slots: - // generally Q_INVOKABLEs are preferred to public slots. - void slot(); - -private slots: - // ... - -private: - // statics, then functions, then fields - static const foo BAR; - static void foo(); - - void foo(); - void bar(); - - // property related members are prefixed with `m`. - QString mFoo; - QString bar; - - // Bindables go last and should be prefixed with `b`. - Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged); -}; -``` - -### Linter -All contributions should pass the linter. - -Note that running the linter requires disabling precompiled -headers and including the test codepaths: -```sh -$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON -$ just lint-changed -``` - -If the linter is complaining about something that you think it should not, -please disable the lint in your MR and explain your reasoning if it isn't obvious. - -### Tests -If you feel like the feature you are working on is very complex or likely to break, -please write some tests. We will ask you to directly if you send in an MR for an -overly complex or breakable feature. - -At least all tests that passed before your changes should still be passing -by the time your contribution is ready. - -You can run the tests using `just test` but you must enable them first -using `-DBUILD_TESTING=ON`. - -### Documentation -Most of quickshell's documentation is automatically generated from the source code. -You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser -cannot handle random line breaks and will usually require you to disable clang-format if the -lines are too long. - -Before submitting an MR, if adding new features please make sure the documentation is generated -reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo. - -Doc comments take the form `///` or `///!` (summary) and work with markdown. -You can reference other types using the `@@[Module.][Type.][member]` shorthand -where all parts are optional. If module or type are not specified they will -be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`. -Look at existing code for how it works. - -Quickshell modules additionally have a `module.md` file which contains a summary, description, -and list of headers to scan for documentation. - -## Contributing - -### Commits -Please structure your commit messages as `scope[!]: commit` where -the scope is something like `core` or `service/mpris`. (pick what has been -used historically or what makes sense if new). Add `!` for changes that break -existing APIs or functionality. - -Commit descriptions should contain a summary of the changes if they are not -sufficiently addressed in the commit message. - -Please squash/rebase additions or edits to previous changes and follow the -commit style to keep the history easily searchable at a glance. -Depending on the change, it is often reasonable to squash it into just -a single commit. (If you do not follow this we will squash your changes -for you.) - -### Sending patches -You may contribute by submitting a pull request on github, asking for -an account on our git server, or emailing patches / git bundles -directly to `outfoxxed@outfoxxed.me`. - -### Getting help -If you're getting stuck, you can come talk to us in the -[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me) -for help on implementation, conventions, etc. -Feel free to ask for advice early in your implementation if you are -unsure. +We handle work for the most part on a push basis. If your PR has been ignored for a while +and is still relevant please bump it. diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 0000000..69357f1 --- /dev/null +++ b/HACKING.md @@ -0,0 +1,226 @@ +## Development + +Install the dependencies listed in [BUILD.md](BUILD.md). +You probably want all of them even if you don't use all of them +to ensure tests work correctly and avoid passing a bunch of configure +flags when you need to wipe the build directory. + +The dependencies are also available as a nix shell or nix flake which we recommend +using with nix-direnv. + +Quickshell uses `just` for common development command aliases. + +Common aliases: +- `just configure [ [extra cmake args]]` (note that you must specify debug/release to specify extra args) +- `just build` - runs the build, configuring if not configured already. +- `just run [args]` - runs quickshell with the given arguments +- `just clean` - clean up build artifacts. `just clean build` is somewhat common. + +### Formatting +All contributions should be formatted similarly to what already exists. +Group related functionality together. + +Run the formatter using `just fmt`. +If the results look stupid, fix the clang-format file if possible, +or disable clang-format in the affected area +using `// clang-format off` and `// clang-format on`. + +#### Style preferences not caught by clang-format +These are flexible. You can ignore them if it looks or works better to +for one reason or another. + +Use `auto` if the type of a variable can be deduced automatically, instead of +redeclaring the returned value's type. Additionally, auto should be used when a +constructor takes arguments. + +```cpp +auto x = ; // ok +auto x = QString::number(3); // ok +QString x; // ok +QString x = "foo"; // ok +auto x = QString("foo"); // ok + +auto x = QString(); // avoid +QString x(); // avoid +QString x("foo"); // avoid +``` + +Put newlines around logical units of code, and after closing braces. If the +most reasonable logical unit of code takes only a single line, it should be +merged into the next single line logical unit if applicable. +```cpp +// multiple units +auto x = ; // unit 1 +auto y = ; // unit 2 + +auto x = ; // unit 1 +emit this->y(); // unit 2 + +auto x1 = ; // unit 1 +auto x2 = ; // unit 1 +auto x3 = ; // unit 1 + +auto y1 = ; // unit 2 +auto y2 = ; // unit 2 +auto y3 = ; // unit 2 + +// one unit +auto x = ; +if (x...) { + // ... +} + +// if more than one variable needs to be used then add a newline +auto x = ; +auto y = ; + +if (x && y) { + // ... +} +``` + +Class formatting: +```cpp +//! Doc comment summary +/// Doc comment body +class Foo: public QObject { + // The Q_OBJECT macro comes first. Macros are ; terminated. + Q_OBJECT; + QML_ELEMENT; + QML_CLASSINFO(...); + // Properties must stay on a single line or the doc generator won't be able to pick them up + Q_PROPERTY(...); + /// Doc comment + Q_PROPERTY(...); + /// Doc comment + Q_PROPERTY(...); + +public: + // Classes should have explicit constructors if they aren't intended to + // implicitly cast. The constructor can be inline in the header if it has no body. + explicit Foo(QObject* parent = nullptr): QObject(parent) {} + + // Instance functions if applicable. + static Foo* instance(); + + // Member functions unrelated to properties come next + void function(); + void function(); + void function(); + + // Then Q_INVOKABLEs + Q_INVOKABLE function(); + /// Doc comment + Q_INVOKABLE function(); + /// Doc comment + Q_INVOKABLE function(); + + // Then property related functions, in the order (bindable, getter, setter). + // Related functions may be included here as well. Function bodies may be inline + // if they are a single expression. There should be a newline between each + // property's methods. + [[nodiscard]] QBindable bindableFoo() { return &this->bFoo; } + [[nodiscard]] T foo() const { return this->foo; } + void setFoo(); + + [[nodiscard]] T bar() const { return this->foo; } + void setBar(); + +signals: + // Signals that are not property change related go first. + // Property change signals go in property definition order. + void asd(); + void asd2(); + void fooChanged(); + void barChanged(); + +public slots: + // generally Q_INVOKABLEs are preferred to public slots. + void slot(); + +private slots: + // ... + +private: + // statics, then functions, then fields + static const foo BAR; + static void foo(); + + void foo(); + void bar(); + + // property related members are prefixed with `m`. + QString mFoo; + QString bar; + + // Bindables go last and should be prefixed with `b`. + Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged); +}; +``` + +Use lowercase .h suffixed Qt headers, e.g. `` over ``. + +### Linter +All contributions should pass the linter. + +Note that running the linter requires disabling precompiled +headers and including the test codepaths: +```sh +$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON +$ just lint-changed +``` + +If the linter is complaining about something that you think it should not, +please disable the lint in your MR and explain your reasoning if it isn't obvious. + +### Tests +If you feel like the feature you are working on is very complex or likely to break, +please write some tests. We will ask you to directly if you send in an MR for an +overly complex or breakable feature. + +At least all tests that passed before your changes should still be passing +by the time your contribution is ready. + +You can run the tests using `just test` but you must enable them first +using `-DBUILD_TESTING=ON`. + +### Documentation +Most of quickshell's documentation is automatically generated from the source code. +You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser +cannot handle random line breaks and will usually require you to disable clang-format if the +lines are too long. + +Make sure new files containing doc comments are added to a `module.md` file. +See existing module files for reference. + +Doc comments take the form `///` or `///!` (summary) and work with markdown. +You can reference other types using the `@@[Module.][Type.][member]` shorthand +where all parts are optional. If module or type are not specified they will +be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`. +Look at existing code for how it works. + +If you have made a user visible change since the last tagged release, describe it in +[changelog/next.md](changelog/next.md). + +## Contributing + +### Commits +Please structure your commit messages as `scope: commit` where +the scope is something like `core` or `service/mpris`. (pick what has been +used historically or what makes sense if new). + +Commit descriptions should contain a summary of the changes if they are not +sufficiently addressed in the commit message. + +Please squash/rebase additions or edits to previous changes and follow the +commit style to keep the history easily searchable at a glance. +Depending on the change, it is often reasonable to squash it into just +a single commit. (If you do not follow this we will squash your changes +for you.) + +### Getting help +If you're getting stuck, you can come talk to us in the +[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me) +for help on implementation, conventions, etc. There is also a bridged [discord server](https://discord.gg/UtZeT3xNyT). +Feel free to ask for advice early in your implementation if you are +unsure. diff --git a/README.md b/README.md index 4491d24..365bdb5 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ This repo is hosted at: - https://github.com/quickshell-mirror/quickshell # Contributing / Development -See [CONTRIBUTING.md](CONTRIBUTING.md) for details. +- [HACKING.md](HACKING.md) - Development instructions and policy. +- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution policy. +- [BUILD.md](BUILD.md) - Packaging and build instructions. #### License From e32b9093545e7719bd91d8e219bb30aabd688230 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 13 Mar 2026 01:10:09 -0700 Subject: [PATCH 15/26] core: add disable env vars for file watcher and crash handler --- changelog/next.md | 2 ++ src/core/qmlglobal.cpp | 4 +++- src/launch/launch.cpp | 6 ++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 4883c93..fa6d845 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -34,6 +34,8 @@ set shell id. - PwNodeLinkTracker ignores sound level monitoring programs. - Replaced breakpad with cpptrace. - Reloads are prevented if no file content has changed. +- Added `QS_DISABLE_FILE_WATCHER` environment variable to disable file watching. +- Added `QS_DISABLE_CRASH_HANDLER` environment variable to disable crash handling. ## Bug Fixes diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 6c26609..35504f6 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -60,7 +60,9 @@ void QuickshellSettings::setWorkingDirectory(QString workingDirectory) { // NOLI emit this->workingDirectoryChanged(); } -bool QuickshellSettings::watchFiles() const { return this->mWatchFiles; } +bool QuickshellSettings::watchFiles() const { + return this->mWatchFiles && qEnvironmentVariableIsEmpty("QS_DISABLE_FILE_WATCHER"); +} void QuickshellSettings::setWatchFiles(bool watchFiles) { if (watchFiles == this->mWatchFiles) return; diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index ee7ca64..3a9a2a5 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -138,9 +138,11 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio }; #if CRASH_HANDLER - crash::CrashHandler::init(); + if (qEnvironmentVariableIsSet("QS_DISABLE_CRASH_HANDLER")) { + qInfo() << "Crash handling disabled."; + } else { + crash::CrashHandler::init(); - { auto* log = LogManager::instance(); crash::CrashHandler::setRelaunchInfo({ .instance = InstanceInfo::CURRENT, From 4b77936c8019e0f51e0e62414c6de3556d5f8870 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 13 Mar 2026 02:04:01 -0700 Subject: [PATCH 16/26] crash: allow overriding crash reporter url --- BUILD.md | 2 +- CMakeLists.txt | 3 +++ changelog/next.md | 1 + src/build/build.hpp.in | 1 + src/crash/interface.cpp | 31 ++++++++++++++++--------------- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/BUILD.md b/BUILD.md index aa04bbd..04421c0 100644 --- a/BUILD.md +++ b/BUILD.md @@ -15,7 +15,7 @@ Please make this descriptive enough to identify your specific package, for examp - `Nixpkgs` - `Fedora COPR (errornointernet/quickshell)` -Please leave at least symbol names attached to the binary for debugging purposes. +If you are forking quickshell, please change `CRASHREPORT_URL` to your own issue tracker. ### QML Module dir Currently all QML modules are statically linked to quickshell, but this is where diff --git a/CMakeLists.txt b/CMakeLists.txt index d57e322..1226342 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(QS_BUILD_OPTIONS "") +# should be changed for forks +set(CRASHREPORT_URL "https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml" CACHE STRING "Bugreport URL") + function(boption VAR NAME DEFAULT) cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "") diff --git a/changelog/next.md b/changelog/next.md index fa6d845..587e667 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -36,6 +36,7 @@ set shell id. - Reloads are prevented if no file content has changed. - Added `QS_DISABLE_FILE_WATCHER` environment variable to disable file watching. - Added `QS_DISABLE_CRASH_HANDLER` environment variable to disable crash handling. +- Added `QS_CRASHREPORT_URL` environment variable to allow overriding the crash reporter link. ## Bug Fixes diff --git a/src/build/build.hpp.in b/src/build/build.hpp.in index 2ab2db2..acc3c58 100644 --- a/src/build/build.hpp.in +++ b/src/build/build.hpp.in @@ -13,4 +13,5 @@ #define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)" #define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@" #define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@" +#define CRASHREPORT_URL "@CRASHREPORT_URL@" // NOLINTEND diff --git a/src/crash/interface.cpp b/src/crash/interface.cpp index a3422d3..6a370ce 100644 --- a/src/crash/interface.cpp +++ b/src/crash/interface.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -12,11 +13,22 @@ #include #include #include +#include #include #include #include "build.hpp" +namespace { +QString crashreportUrl() { + if (auto url = qEnvironmentVariable("QS_CRASHREPORT_URL"); !url.isEmpty()) { + return url; + } + + return CRASHREPORT_URL; +} +} // namespace + class ReportLabel: public QWidget { public: ReportLabel(const QString& label, const QString& content, QWidget* parent): QWidget(parent) { @@ -67,22 +79,16 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid) if (qtVersionMatches) { mainLayout->addWidget( - new QLabel("Please open a bug report for this issue via github or email.") + new QLabel("Please open a bug report for this issue on the issue tracker.") ); } else { mainLayout->addWidget(new QLabel( "Please rebuild Quickshell against the current Qt version.\n" - "If this does not solve the problem, please open a bug report via github or email." + "If this does not solve the problem, please open a bug report on the issue tracker." )); } - mainLayout->addWidget(new ReportLabel( - "Github:", - "https://github.com/quickshell-mirror/quickshell/issues/new?template=crash2.yml", - this - )); - - mainLayout->addWidget(new ReportLabel("Email:", "quickshell-bugs@outfoxxed.me", this)); + mainLayout->addWidget(new ReportLabel("Tracker:", crashreportUrl(), this)); auto* buttons = new QWidget(this); buttons->setMinimumWidth(900); @@ -112,10 +118,5 @@ void CrashReporterGui::openFolder() { QDesktopServices::openUrl(QUrl::fromLocalFile(this->reportFolder)); } -void CrashReporterGui::openReportUrl() { - QDesktopServices::openUrl( - QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml") - ); -} - +void CrashReporterGui::openReportUrl() { QDesktopServices::openUrl(QUrl(crashreportUrl())); } void CrashReporterGui::cancel() { QApplication::quit(); } From 1123d5ab4fa9bdde1d0888ed56f6987449eaf267 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 14 Mar 2026 00:30:53 -0700 Subject: [PATCH 17/26] core: move crash/version debug info to one place --- src/core/CMakeLists.txt | 1 + src/core/debuginfo.cpp | 68 +++++++++++++++++++++++++++++++++++++++++ src/core/debuginfo.hpp | 12 ++++++++ src/crash/main.cpp | 34 ++------------------- src/launch/command.cpp | 20 +++--------- 5 files changed, 89 insertions(+), 46 deletions(-) create mode 100644 src/core/debuginfo.cpp create mode 100644 src/core/debuginfo.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index f0ca8ef..076ab90 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -41,6 +41,7 @@ qt_add_library(quickshell-core STATIC colorquantizer.cpp toolsupport.cpp streamreader.cpp + debuginfo.cpp ) qt_add_qml_module(quickshell-core diff --git a/src/core/debuginfo.cpp b/src/core/debuginfo.cpp new file mode 100644 index 0000000..f948d42 --- /dev/null +++ b/src/core/debuginfo.cpp @@ -0,0 +1,68 @@ +#include "debuginfo.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "build.hpp" + +namespace qs::debuginfo { + +QString qsVersion() { + return QS_VERSION " (revision " GIT_REVISION ", distributed by " DISTRIBUTOR ")"; +} + +QString qtVersion() { return qVersion() % QStringLiteral(" (built against " QT_VERSION_STR ")"); } + +QString systemInfo() { + QString info; + auto stream = QTextStream(&info); + + stream << "/etc/os-release:"; + auto osReleaseFile = QFile("/etc/os-release"); + if (osReleaseFile.open(QFile::ReadOnly)) { + stream << '\n' << osReleaseFile.readAll() << '\n'; + osReleaseFile.close(); + } else { + stream << "FAILED TO OPEN\n"; + } + + stream << "/etc/lsb-release:"; + auto lsbReleaseFile = QFile("/etc/lsb-release"); + if (lsbReleaseFile.open(QFile::ReadOnly)) { + stream << '\n' << lsbReleaseFile.readAll(); + lsbReleaseFile.close(); + } else { + stream << "FAILED TO OPEN\n"; + } + + return info; +} + +QString combinedInfo() { + QString info; + auto stream = QTextStream(&info); + + stream << "===== Version Information =====\n"; + stream << "Quickshell: " << qsVersion() << '\n'; + stream << "Qt: " << qtVersion() << '\n'; + + stream << "\n===== Build Information =====\n"; + stream << "Build Type: " << BUILD_TYPE << '\n'; + stream << "Compiler: " << COMPILER << '\n'; + stream << "Compile Flags: " << COMPILE_FLAGS << '\n'; + stream << "Configuration:\n" << BUILD_CONFIGURATION << '\n'; + + stream << "\n===== System Information =====\n"; + stream << systemInfo(); + + return info; +} + +} // namespace qs::debuginfo diff --git a/src/core/debuginfo.hpp b/src/core/debuginfo.hpp new file mode 100644 index 0000000..7759d53 --- /dev/null +++ b/src/core/debuginfo.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace qs::debuginfo { + +QString qsVersion(); +QString qtVersion(); +QString systemInfo(); +QString combinedInfo(); + +} // namespace qs::debuginfo diff --git a/src/crash/main.cpp b/src/crash/main.cpp index c406ba6..05927f2 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -15,19 +14,18 @@ #include #include #include -#include #include #include #include #include +#include "../core/debuginfo.hpp" #include "../core/instanceinfo.hpp" #include "../core/logcat.hpp" #include "../core/logging.hpp" #include "../core/logging_p.hpp" #include "../core/paths.hpp" #include "../core/ringbuf.hpp" -#include "build.hpp" #include "interface.hpp" namespace { @@ -171,41 +169,15 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { qCCritical(logCrashReporter) << "Failed to open crash info file for writing."; } else { auto stream = QTextStream(&extraInfoFile); - stream << "===== Build Information =====\n"; - stream << "Git Revision: " << GIT_REVISION << '\n'; - stream << "Buildtime Qt Version: " << QT_VERSION_STR << "\n"; - stream << "Build Type: " << BUILD_TYPE << '\n'; - stream << "Compiler: " << COMPILER << '\n'; - stream << "Complie Flags: " << COMPILE_FLAGS << "\n\n"; - stream << "Build configuration:\n" << BUILD_CONFIGURATION << "\n"; + stream << qs::debuginfo::combinedInfo(); - stream << "\n===== Runtime Information =====\n"; - stream << "Runtime Qt Version: " << qVersion() << '\n'; + stream << "\n===== Instance Information =====\n"; stream << "Signal: " << strsignal(crashSignal) << " (" << crashSignal << ")\n"; // NOLINT stream << "Crashed process ID: " << crashProc << '\n'; stream << "Run ID: " << instance.instanceId << '\n'; stream << "Shell ID: " << instance.shellId << '\n'; stream << "Config Path: " << instance.configPath << '\n'; - stream << "\n===== System Information =====\n\n"; - stream << "/etc/os-release:"; - auto osReleaseFile = QFile("/etc/os-release"); - if (osReleaseFile.open(QFile::ReadOnly)) { - stream << '\n' << osReleaseFile.readAll() << '\n'; - osReleaseFile.close(); - } else { - stream << "FAILED TO OPEN\n"; - } - - stream << "/etc/lsb-release:"; - auto lsbReleaseFile = QFile("/etc/lsb-release"); - if (lsbReleaseFile.open(QFile::ReadOnly)) { - stream << '\n' << lsbReleaseFile.readAll(); - lsbReleaseFile.close(); - } else { - stream << "FAILED TO OPEN\n"; - } - stream << "\n===== Stacktrace =====\n"; if (stacktrace.empty()) { stream << "(no trace available)\n"; diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 151fc24..807eb24 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -25,12 +25,12 @@ #include #include +#include "../core/debuginfo.hpp" #include "../core/instanceinfo.hpp" #include "../core/logging.hpp" #include "../core/paths.hpp" #include "../io/ipccomm.hpp" #include "../ipc/ipc.hpp" -#include "build.hpp" #include "launch_p.hpp" namespace qs::launch { @@ -519,20 +519,10 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { } if (state.misc.printVersion) { - qCInfo(logBare).noquote().nospace() << "quickshell " << QS_VERSION << ", revision " - << GIT_REVISION << ", distributed by: " << DISTRIBUTOR; - - if (state.log.verbosity > 1) { - qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; - qCInfo(logBare).noquote() << "Runtime Qt Version:" << qVersion(); - qCInfo(logBare).noquote() << "Compiler:" << COMPILER; - qCInfo(logBare).noquote() << "Compile Flags:" << COMPILE_FLAGS; - } - - if (state.log.verbosity > 0) { - qCInfo(logBare).noquote() << "\nBuild Type:" << BUILD_TYPE; - qCInfo(logBare).noquote() << "Build configuration:"; - qCInfo(logBare).noquote().nospace() << BUILD_CONFIGURATION; + if (state.log.verbosity == 0) { + qCInfo(logBare).noquote() << "Quickshell" << qs::debuginfo::qsVersion(); + } else { + qCInfo(logBare).noquote() << qs::debuginfo::combinedInfo(); } } else if (*state.subcommand.log) { return readLogFile(state); From 1b2519d9f3d963e575b8a1ef08fab47c7af0d1b3 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 14 Mar 2026 02:31:47 -0700 Subject: [PATCH 18/26] core: log gpu information in debuginfo --- .clang-tidy | 1 + BUILD.md | 2 +- changelog/next.md | 1 + default.nix | 3 +- src/core/CMakeLists.txt | 3 +- src/core/debuginfo.cpp | 74 +++++++++++++++++++++++++++++++++++++++++ src/core/debuginfo.hpp | 1 + 7 files changed, 82 insertions(+), 3 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index c83ed8f..da14682 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -20,6 +20,7 @@ Checks: > -cppcoreguidelines-avoid-do-while, -cppcoreguidelines-pro-type-reinterpret-cast, -cppcoreguidelines-pro-type-vararg, + -cppcoreguidelines-pro-type-union-access, -cppcoreguidelines-use-enum-class, google-global-names-in-headers, google-readability-casting, diff --git a/BUILD.md b/BUILD.md index 04421c0..d624a06 100644 --- a/BUILD.md +++ b/BUILD.md @@ -33,6 +33,7 @@ Quickshell has a set of base dependencies you will always need, names vary by di - `cmake` - `qt6base` - `qt6declarative` +- `libdrm` - `qtshadertools` (build-time) - `spirv-tools` (build-time) - `pkg-config` (build-time) @@ -146,7 +147,6 @@ Enables streaming video from monitors and toplevel windows through various proto To disable: `-DSCREENCOPY=OFF` Dependencies: -- `libdrm` - `libgbm` - `vulkan-headers` (build-time) diff --git a/changelog/next.md b/changelog/next.md index 587e667..e9b297c 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -64,3 +64,4 @@ set shell id. - `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support). - `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore. - `DISTRIBUTOR_DEBUGINFO_AVAILABLE` was removed as it is no longer important without breakpad. +- `libdrm` is now unconditionally required as a direct dependency. diff --git a/default.nix b/default.nix index 02b8659..749ef49 100644 --- a/default.nix +++ b/default.nix @@ -76,6 +76,7 @@ buildInputs = [ qt6.qtbase qt6.qtdeclarative + libdrm cli11 ] ++ lib.optional withQtSvg qt6.qtsvg @@ -88,7 +89,7 @@ ++ lib.optional withJemalloc jemalloc ++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland ++ lib.optionals withWayland [ wayland wayland-protocols ] - ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm vulkan-headers ] + ++ lib.optionals (withWayland && libgbm != null) [ libgbm vulkan-headers ] ++ lib.optional withX11 libxcb ++ lib.optional withPam pam ++ lib.optional withPipewire pipewire diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 076ab90..4824965 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,3 +1,4 @@ +pkg_check_modules(libdrm REQUIRED IMPORTED_TARGET libdrm) qt_add_library(quickshell-core STATIC plugin.cpp shell.cpp @@ -54,7 +55,7 @@ qt_add_qml_module(quickshell-core install_qml_module(quickshell-core) -target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build) +target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build PkgConfig::libdrm) qs_module_pch(quickshell-core SET large) diff --git a/src/core/debuginfo.cpp b/src/core/debuginfo.cpp index f948d42..f26c72e 100644 --- a/src/core/debuginfo.cpp +++ b/src/core/debuginfo.cpp @@ -7,8 +7,10 @@ #include #include #include +#include #include #include +#include #include "build.hpp" @@ -20,10 +22,82 @@ QString qsVersion() { QString qtVersion() { return qVersion() % QStringLiteral(" (built against " QT_VERSION_STR ")"); } +QString gpuInfo() { + auto deviceCount = drmGetDevices2(0, nullptr, 0); + if (deviceCount < 0) return "Failed to get DRM device count: " % QString::number(deviceCount); + auto* devices = new drmDevicePtr[deviceCount]; + auto devicesArrayGuard = qScopeGuard([&] { delete[] devices; }); + auto r = drmGetDevices2(0, devices, deviceCount); + if (deviceCount < 0) return "Failed to get DRM devices: " % QString::number(r); + auto devicesGuard = qScopeGuard([&] { + for (auto i = 0; i != deviceCount; ++i) drmFreeDevice(&devices[i]); // NOLINT + }); + + QString info; + auto stream = QTextStream(&info); + + for (auto i = 0; i != deviceCount; ++i) { + auto* device = devices[i]; // NOLINT + + int deviceNodeType = -1; + if (device->available_nodes & (1 << DRM_NODE_RENDER)) deviceNodeType = DRM_NODE_RENDER; + else if (device->available_nodes & (1 << DRM_NODE_PRIMARY)) deviceNodeType = DRM_NODE_PRIMARY; + + if (deviceNodeType == -1) continue; + + auto* deviceNode = device->nodes[DRM_NODE_RENDER]; // NOLINT + + auto driver = [&]() -> QString { + auto fd = open(deviceNode, O_RDWR | O_CLOEXEC); + if (fd == -1) return ""; + auto fdGuard = qScopeGuard([&] { close(fd); }); + auto* ver = drmGetVersion(fd); + if (!ver) return ""; + auto verGuard = qScopeGuard([&] { drmFreeVersion(ver); }); + + // clang-format off + return QString(ver->name) + % ' ' % QString::number(ver->version_major) + % '.' % QString::number(ver->version_minor) + % '.' % QString::number(ver->version_patchlevel) + % " (" % ver->desc % ')'; + // clang-format on + }(); + + QString product = "unknown"; + QString address = "unknown"; + + auto hex = [](int num, int pad) { return QString::number(num, 16).rightJustified(pad, '0'); }; + + switch (device->bustype) { + case DRM_BUS_PCI: { + auto* b = device->businfo.pci; + auto* d = device->deviceinfo.pci; + address = "PCI " % hex(b->bus, 2) % ':' % hex(b->dev, 2) % '.' % hex(b->func, 1); + product = hex(d->vendor_id, 4) % ':' % hex(d->device_id, 4); + } break; + case DRM_BUS_USB: { + auto* b = device->businfo.usb; + auto* d = device->deviceinfo.usb; + address = "USB " % QString::number(b->bus) % ':' % QString::number(b->dev); + product = hex(d->vendor, 4) % ':' % hex(d->product, 4); + } break; + default: break; + } + + stream << "GPU " << deviceNode << "\n Driver: " << driver << "\n Model: " << product + << "\n Address: " << address << '\n'; + } + + return info; +} + QString systemInfo() { QString info; auto stream = QTextStream(&info); + stream << gpuInfo() << '\n'; + stream << "/etc/os-release:"; auto osReleaseFile = QFile("/etc/os-release"); if (osReleaseFile.open(QFile::ReadOnly)) { diff --git a/src/core/debuginfo.hpp b/src/core/debuginfo.hpp index 7759d53..cc13f97 100644 --- a/src/core/debuginfo.hpp +++ b/src/core/debuginfo.hpp @@ -6,6 +6,7 @@ namespace qs::debuginfo { QString qsVersion(); QString qtVersion(); +QString gpuInfo(); QString systemInfo(); QString combinedInfo(); From 9e8eecf2b8bfa9dd3eed5712d5856d7b041ea909 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 15 Mar 2026 21:13:35 -0700 Subject: [PATCH 19/26] core: log qt related environment variables in debuginfo --- src/core/debuginfo.cpp | 33 +++++++++++++++++++++++++++++++++ src/core/debuginfo.hpp | 1 + 2 files changed, 34 insertions(+) diff --git a/src/core/debuginfo.cpp b/src/core/debuginfo.cpp index f26c72e..ae227f8 100644 --- a/src/core/debuginfo.cpp +++ b/src/core/debuginfo.cpp @@ -1,4 +1,7 @@ #include "debuginfo.hpp" +#include +#include +#include #include #include @@ -14,6 +17,8 @@ #include "build.hpp" +extern char** environ; // NOLINT + namespace qs::debuginfo { QString qsVersion() { @@ -119,6 +124,31 @@ QString systemInfo() { return info; } +QString envInfo() { + QString info; + auto stream = QTextStream(&info); + + for (auto** envp = environ; *envp != nullptr; ++envp) { // NOLINT + auto prefixes = std::array { + "QS_", + "QT_", + "QML_", + "QML2_", + "QSG_", + }; + + for (const auto& prefix: prefixes) { + if (strncmp(prefix.data(), *envp, prefix.length()) == 0) goto print; + } + continue; + + print: + stream << *envp << '\n'; + } + + return info; +} + QString combinedInfo() { QString info; auto stream = QTextStream(&info); @@ -136,6 +166,9 @@ QString combinedInfo() { stream << "\n===== System Information =====\n"; stream << systemInfo(); + stream << "\n===== Environment (trimmed) =====\n"; + stream << envInfo(); + return info; } diff --git a/src/core/debuginfo.hpp b/src/core/debuginfo.hpp index cc13f97..fc766fc 100644 --- a/src/core/debuginfo.hpp +++ b/src/core/debuginfo.hpp @@ -8,6 +8,7 @@ QString qsVersion(); QString qtVersion(); QString gpuInfo(); QString systemInfo(); +QString envInfo(); QString combinedInfo(); } // namespace qs::debuginfo From 6705e2da778d216e81dbdc3764a3f50e89bfd87d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 21 Jun 2025 12:57:15 -0700 Subject: [PATCH 20/26] wm: add WindowManager module with ext-workspace support --- changelog/next.md | 1 + src/CMakeLists.txt | 1 + src/wayland/CMakeLists.txt | 2 + src/wayland/windowmanager/CMakeLists.txt | 19 ++ src/wayland/windowmanager/ext_workspace.cpp | 176 ++++++++++++ src/wayland/windowmanager/ext_workspace.hpp | 117 ++++++++ src/wayland/windowmanager/init.cpp | 23 ++ src/wayland/windowmanager/windowmanager.cpp | 21 ++ src/wayland/windowmanager/windowmanager.hpp | 17 ++ src/wayland/windowmanager/windowset.cpp | 252 ++++++++++++++++++ src/wayland/windowmanager/windowset.hpp | 85 ++++++ src/windowmanager/CMakeLists.txt | 20 ++ src/windowmanager/module.md | 10 + src/windowmanager/screenprojection.cpp | 30 +++ src/windowmanager/screenprojection.hpp | 34 +++ .../test/manual/WorkspaceDelegate.qml | 86 ++++++ src/windowmanager/test/manual/screenproj.qml | 45 ++++ src/windowmanager/test/manual/workspaces.qml | 46 ++++ src/windowmanager/windowmanager.cpp | 41 +++ src/windowmanager/windowmanager.hpp | 91 +++++++ src/windowmanager/windowset.cpp | 45 ++++ src/windowmanager/windowset.hpp | 175 ++++++++++++ 22 files changed, 1337 insertions(+) create mode 100644 src/wayland/windowmanager/CMakeLists.txt create mode 100644 src/wayland/windowmanager/ext_workspace.cpp create mode 100644 src/wayland/windowmanager/ext_workspace.hpp create mode 100644 src/wayland/windowmanager/init.cpp create mode 100644 src/wayland/windowmanager/windowmanager.cpp create mode 100644 src/wayland/windowmanager/windowmanager.hpp create mode 100644 src/wayland/windowmanager/windowset.cpp create mode 100644 src/wayland/windowmanager/windowset.hpp create mode 100644 src/windowmanager/CMakeLists.txt create mode 100644 src/windowmanager/module.md create mode 100644 src/windowmanager/screenprojection.cpp create mode 100644 src/windowmanager/screenprojection.hpp create mode 100644 src/windowmanager/test/manual/WorkspaceDelegate.qml create mode 100644 src/windowmanager/test/manual/screenproj.qml create mode 100644 src/windowmanager/test/manual/workspaces.qml create mode 100644 src/windowmanager/windowmanager.cpp create mode 100644 src/windowmanager/windowmanager.hpp create mode 100644 src/windowmanager/windowset.cpp create mode 100644 src/windowmanager/windowset.hpp diff --git a/changelog/next.md b/changelog/next.md index e9b297c..cbfd51b 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -26,6 +26,7 @@ set shell id. - Added Quickshell version checking and version gated preprocessing. - Added a way to detect if an icon is from the system icon theme or not. - Added vulkan support to screencopy. +- Added generic WindowManager interface implementing ext-workspace. ## Other Changes diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4b13d45..0c05419 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ add_subdirectory(window) add_subdirectory(io) add_subdirectory(widgets) add_subdirectory(ui) +add_subdirectory(windowmanager) if (CRASH_HANDLER) add_subdirectory(crash) diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index ca49c8f..db53f37 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -123,6 +123,8 @@ list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleNotify) add_subdirectory(shortcuts_inhibit) list(APPEND WAYLAND_MODULES Quickshell.Wayland._ShortcutsInhibitor) +add_subdirectory(windowmanager) + # widgets for qmenu target_link_libraries(quickshell-wayland PRIVATE Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate diff --git a/src/wayland/windowmanager/CMakeLists.txt b/src/wayland/windowmanager/CMakeLists.txt new file mode 100644 index 0000000..76d1d89 --- /dev/null +++ b/src/wayland/windowmanager/CMakeLists.txt @@ -0,0 +1,19 @@ +qt_add_library(quickshell-wayland-windowsystem STATIC + windowmanager.cpp + windowset.cpp + ext_workspace.cpp +) + +add_library(quickshell-wayland-windowsystem-init OBJECT init.cpp) +target_link_libraries(quickshell-wayland-windowsystem-init PRIVATE Qt::Quick) + +wl_proto(wlp-ext-workspace ext-workspace-v1 "${WAYLAND_PROTOCOLS}/staging/ext-workspace") + +target_link_libraries(quickshell-wayland-windowsystem PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-ext-workspace +) + +qs_pch(quickshell-wayland-windowsystem SET large) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-windowsystem quickshell-wayland-windowsystem-init) diff --git a/src/wayland/windowmanager/ext_workspace.cpp b/src/wayland/windowmanager/ext_workspace.cpp new file mode 100644 index 0000000..fcb9ffa --- /dev/null +++ b/src/wayland/windowmanager/ext_workspace.cpp @@ -0,0 +1,176 @@ +#include "ext_workspace.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" + +namespace qs::wayland::workspace { + +QS_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.wayland.workspace", QtWarningMsg); + +WorkspaceManager::WorkspaceManager(): QWaylandClientExtensionTemplate(1) { this->initialize(); } + +WorkspaceManager* WorkspaceManager::instance() { + static auto* instance = new WorkspaceManager(); + return instance; +} + +void WorkspaceManager::ext_workspace_manager_v1_workspace_group( + ::ext_workspace_group_handle_v1* handle +) { + auto* group = new WorkspaceGroup(handle); + qCDebug(logWorkspace) << "Created group" << group; + this->mGroups.insert(handle, group); + emit this->groupCreated(group); +} + +void WorkspaceManager::ext_workspace_manager_v1_workspace(::ext_workspace_handle_v1* handle) { + auto* workspace = new Workspace(handle); + qCDebug(logWorkspace) << "Created workspace" << workspace; + this->mWorkspaces.insert(handle, workspace); + emit this->workspaceCreated(workspace); +}; + +void WorkspaceManager::destroyWorkspace(Workspace* workspace) { + this->mWorkspaces.remove(workspace->object()); + this->destroyedWorkspaces.append(workspace); + emit this->workspaceDestroyed(workspace); +} + +void WorkspaceManager::destroyGroup(WorkspaceGroup* group) { + this->mGroups.remove(group->object()); + this->destroyedGroups.append(group); + emit this->groupDestroyed(group); +} + +void WorkspaceManager::ext_workspace_manager_v1_done() { + qCDebug(logWorkspace) << "Workspace changes done"; + emit this->serverCommit(); + + for (auto* workspace: this->destroyedWorkspaces) delete workspace; + for (auto* group: this->destroyedGroups) delete group; + this->destroyedWorkspaces.clear(); + this->destroyedGroups.clear(); +} + +void WorkspaceManager::ext_workspace_manager_v1_finished() { + qCWarning(logWorkspace) << "ext_workspace_manager_v1.finished() was received"; +} + +Workspace::~Workspace() { + if (this->isInitialized()) this->destroy(); +} + +void Workspace::ext_workspace_handle_v1_id(const QString& id) { + qCDebug(logWorkspace) << "Updated id for workspace" << this << "to" << id; + this->id = id; +} + +void Workspace::ext_workspace_handle_v1_name(const QString& name) { + qCDebug(logWorkspace) << "Updated name for workspace" << this << "to" << name; + this->name = name; +} + +void Workspace::ext_workspace_handle_v1_coordinates(wl_array* coordinates) { + this->coordinates.clear(); + + auto* data = static_cast(coordinates->data); + auto size = static_cast(coordinates->size / sizeof(qint32)); + + for (auto i = 0; i != size; ++i) { + this->coordinates.append(data[i]); // NOLINT + } + + qCDebug(logWorkspace) << "Updated coordinates for workspace" << this << "to" << this->coordinates; +} + +void Workspace::ext_workspace_handle_v1_state(quint32 state) { + this->active = state & ext_workspace_handle_v1::state_active; + this->urgent = state & ext_workspace_handle_v1::state_urgent; + this->hidden = state & ext_workspace_handle_v1::state_hidden; + + qCDebug(logWorkspace).nospace() << "Updated state for workspace " << this + << " to [active: " << this->active << ", urgent: " << this->urgent + << ", hidden: " << this->hidden << ']'; +} + +void Workspace::ext_workspace_handle_v1_capabilities(quint32 capabilities) { + this->canActivate = capabilities & ext_workspace_handle_v1::workspace_capabilities_activate; + this->canDeactivate = capabilities & ext_workspace_handle_v1::workspace_capabilities_deactivate; + this->canRemove = capabilities & ext_workspace_handle_v1::workspace_capabilities_remove; + this->canAssign = capabilities & ext_workspace_handle_v1::workspace_capabilities_assign; + + qCDebug(logWorkspace).nospace() << "Updated capabilities for workspace " << this + << " to [activate: " << this->canActivate + << ", deactivate: " << this->canDeactivate + << ", remove: " << this->canRemove + << ", assign: " << this->canAssign << ']'; +} + +void Workspace::ext_workspace_handle_v1_removed() { + qCDebug(logWorkspace) << "Destroyed workspace" << this; + WorkspaceManager::instance()->destroyWorkspace(this); + this->destroy(); +} + +void Workspace::enterGroup(WorkspaceGroup* group) { this->group = group; } + +void Workspace::leaveGroup(WorkspaceGroup* group) { + if (this->group == group) this->group = nullptr; +} + +WorkspaceGroup::~WorkspaceGroup() { + if (this->isInitialized()) this->destroy(); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_capabilities(quint32 capabilities) { + this->canCreateWorkspace = + capabilities & ext_workspace_group_handle_v1::group_capabilities_create_workspace; + + qCDebug(logWorkspace).nospace() << "Updated capabilities for group " << this + << " to [create_workspace: " << this->canCreateWorkspace << ']'; +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_output_enter(::wl_output* output) { + qCDebug(logWorkspace) << "Output" << output << "added to group" << this; + this->screens.addOutput(output); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_output_leave(::wl_output* output) { + qCDebug(logWorkspace) << "Output" << output << "removed from group" << this; + this->screens.removeOutput(output); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_enter( + ::ext_workspace_handle_v1* handle +) { + auto* workspace = WorkspaceManager::instance()->mWorkspaces.value(handle); + qCDebug(logWorkspace) << "Workspace" << workspace << "added to group" << this; + + if (workspace) workspace->enterGroup(this); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_leave( + ::ext_workspace_handle_v1* handle +) { + auto* workspace = WorkspaceManager::instance()->mWorkspaces.value(handle); + qCDebug(logWorkspace) << "Workspace" << workspace << "removed from group" << this; + + if (workspace) workspace->leaveGroup(this); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_removed() { + qCDebug(logWorkspace) << "Destroyed group" << this; + WorkspaceManager::instance()->destroyGroup(this); + this->destroy(); +} + +} // namespace qs::wayland::workspace diff --git a/src/wayland/windowmanager/ext_workspace.hpp b/src/wayland/windowmanager/ext_workspace.hpp new file mode 100644 index 0000000..6aff209 --- /dev/null +++ b/src/wayland/windowmanager/ext_workspace.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../output_tracking.hpp" + +namespace qs::wayland::workspace { + +QS_DECLARE_LOGGING_CATEGORY(logWorkspace); + +class WorkspaceGroup; +class Workspace; + +class WorkspaceManager + : public QWaylandClientExtensionTemplate + , public QtWayland::ext_workspace_manager_v1 { + Q_OBJECT; + +public: + static WorkspaceManager* instance(); + + [[nodiscard]] QList workspaces() { return this->mWorkspaces.values(); } + +signals: + void serverCommit(); + void workspaceCreated(Workspace* workspace); + void workspaceDestroyed(Workspace* workspace); + void groupCreated(WorkspaceGroup* group); + void groupDestroyed(WorkspaceGroup* group); + +protected: + void ext_workspace_manager_v1_workspace_group(::ext_workspace_group_handle_v1* handle) override; + void ext_workspace_manager_v1_workspace(::ext_workspace_handle_v1* handle) override; + void ext_workspace_manager_v1_done() override; + void ext_workspace_manager_v1_finished() override; + +private: + WorkspaceManager(); + + void destroyGroup(WorkspaceGroup* group); + void destroyWorkspace(Workspace* workspace); + + QHash<::ext_workspace_handle_v1*, Workspace*> mWorkspaces; + QHash<::ext_workspace_group_handle_v1*, WorkspaceGroup*> mGroups; + QList destroyedGroups; + QList destroyedWorkspaces; + + friend class Workspace; + friend class WorkspaceGroup; +}; + +class Workspace: public QtWayland::ext_workspace_handle_v1 { +public: + Workspace(::ext_workspace_handle_v1* handle): QtWayland::ext_workspace_handle_v1(handle) {} + ~Workspace() override; + Q_DISABLE_COPY_MOVE(Workspace); + + QString id; + QString name; + QList coordinates; + WorkspaceGroup* group = nullptr; + + bool active : 1 = false; + bool urgent : 1 = false; + bool hidden : 1 = false; + + bool canActivate : 1 = false; + bool canDeactivate : 1 = false; + bool canRemove : 1 = false; + bool canAssign : 1 = false; + +protected: + void ext_workspace_handle_v1_id(const QString& id) override; + void ext_workspace_handle_v1_name(const QString& name) override; + void ext_workspace_handle_v1_coordinates(wl_array* coordinates) override; + void ext_workspace_handle_v1_state(quint32 state) override; + void ext_workspace_handle_v1_capabilities(quint32 capabilities) override; + void ext_workspace_handle_v1_removed() override; + +private: + void enterGroup(WorkspaceGroup* group); + void leaveGroup(WorkspaceGroup* group); + + friend class WorkspaceGroup; +}; + +class WorkspaceGroup: public QtWayland::ext_workspace_group_handle_v1 { +public: + WorkspaceGroup(::ext_workspace_group_handle_v1* handle) + : QtWayland::ext_workspace_group_handle_v1(handle) {} + + ~WorkspaceGroup() override; + Q_DISABLE_COPY_MOVE(WorkspaceGroup); + + WlOutputTracker screens; + bool canCreateWorkspace : 1 = false; + +protected: + void ext_workspace_group_handle_v1_capabilities(quint32 capabilities) override; + void ext_workspace_group_handle_v1_output_enter(::wl_output* output) override; + void ext_workspace_group_handle_v1_output_leave(::wl_output* output) override; + void ext_workspace_group_handle_v1_workspace_enter(::ext_workspace_handle_v1* handle) override; + void ext_workspace_group_handle_v1_workspace_leave(::ext_workspace_handle_v1* handle) override; + void ext_workspace_group_handle_v1_removed() override; +}; + +} // namespace qs::wayland::workspace diff --git a/src/wayland/windowmanager/init.cpp b/src/wayland/windowmanager/init.cpp new file mode 100644 index 0000000..88be01a --- /dev/null +++ b/src/wayland/windowmanager/init.cpp @@ -0,0 +1,23 @@ +#include +#include +#include + +#include "../../core/plugin.hpp" + +namespace qs::wm::wayland { +void installWmProvider(); +} + +namespace { + +class WaylandWmPlugin: public QsEnginePlugin { + QList dependencies() override { return {"window"}; } + + bool applies() override { return QGuiApplication::platformName() == "wayland"; } + + void init() override { qs::wm::wayland::installWmProvider(); } +}; + +QS_REGISTER_PLUGIN(WaylandWmPlugin); + +} // namespace diff --git a/src/wayland/windowmanager/windowmanager.cpp b/src/wayland/windowmanager/windowmanager.cpp new file mode 100644 index 0000000..16245d0 --- /dev/null +++ b/src/wayland/windowmanager/windowmanager.cpp @@ -0,0 +1,21 @@ +#include "windowmanager.hpp" + +#include "../../windowmanager/windowmanager.hpp" +#include "windowset.hpp" + +namespace qs::wm::wayland { + +WaylandWindowManager* WaylandWindowManager::instance() { + static auto* instance = []() { + auto* wm = new WaylandWindowManager(); + WindowsetManager::instance(); + return wm; + }(); + return instance; +} + +void installWmProvider() { // NOLINT (misc-use-internal-linkage) + qs::wm::WindowManager::setProvider([]() { return WaylandWindowManager::instance(); }); +} + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/windowmanager.hpp b/src/wayland/windowmanager/windowmanager.hpp new file mode 100644 index 0000000..9d48efd --- /dev/null +++ b/src/wayland/windowmanager/windowmanager.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "../../windowmanager/windowmanager.hpp" +#include "windowset.hpp" + +namespace qs::wm::wayland { + +class WaylandWindowManager: public WindowManager { + Q_OBJECT; + +public: + static WaylandWindowManager* instance(); +}; + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/windowset.cpp b/src/wayland/windowmanager/windowset.cpp new file mode 100644 index 0000000..796cfe2 --- /dev/null +++ b/src/wayland/windowmanager/windowset.cpp @@ -0,0 +1,252 @@ +#include "windowset.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../windowmanager/windowmanager.hpp" +#include "../../windowmanager/windowset.hpp" +#include "../../windowmanager/screenprojection.hpp" +#include "ext_workspace.hpp" + +namespace qs::wm::wayland { + +WindowsetManager::WindowsetManager() { + auto* impl = impl::WorkspaceManager::instance(); + + QObject::connect( + impl, + &impl::WorkspaceManager::serverCommit, + this, + &WindowsetManager::onServerCommit + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::workspaceCreated, + this, + &WindowsetManager::onWindowsetCreated + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::workspaceDestroyed, + this, + &WindowsetManager::onWindowsetDestroyed + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::groupCreated, + this, + &WindowsetManager::onProjectionCreated + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::groupDestroyed, + this, + &WindowsetManager::onProjectionDestroyed + ); +} + +void WindowsetManager::scheduleCommit() { + if (this->commitScheduled) { + qCDebug(impl::logWorkspace) << "Workspace commit already scheduled."; + return; + } + + qCDebug(impl::logWorkspace) << "Scheduling workspace commit..."; + this->commitScheduled = true; + QMetaObject::invokeMethod(this, &WindowsetManager::doCommit, Qt::QueuedConnection); +} + +void WindowsetManager::doCommit() { // NOLINT + qCDebug(impl::logWorkspace) << "Committing workspaces..."; + impl::WorkspaceManager::instance()->commit(); + this->commitScheduled = false; +} + +void WindowsetManager::onServerCommit() { + // Projections are created/destroyed around windowsets to avoid any nulls making it + // to the qml engine. + + Qt::beginPropertyUpdateGroup(); + + auto* wm = WindowManager::instance(); + auto windowsets = wm->bWindowsets.value(); + auto projections = wm->bWindowsetProjections.value(); + + for (auto* projImpl: this->pendingProjectionCreations) { + auto* projection = new WlWindowsetProjection(this, projImpl); + this->projectionsByImpl.insert(projImpl, projection); + projections.append(projection); + } + + for (auto* wsImpl: this->pendingWindowsetCreations) { + auto* ws = new WlWindowset(this, wsImpl); + this->windowsetByImpl.insert(wsImpl, ws); + windowsets.append(ws); + } + + for (auto* wsImpl: this->pendingWindowsetDestructions) { + windowsets.removeOne(this->windowsetByImpl.value(wsImpl)); + this->windowsetByImpl.remove(wsImpl); + } + + for (auto* projImpl: this->pendingProjectionDestructions) { + projections.removeOne(this->projectionsByImpl.value(projImpl)); + this->projectionsByImpl.remove(projImpl); + } + + for (auto* ws: windowsets) { + static_cast(ws)->commitImpl(); // NOLINT + } + + for (auto* projection: projections) { + static_cast(projection)->commitImpl(); // NOLINT + } + + this->pendingWindowsetCreations.clear(); + this->pendingWindowsetDestructions.clear(); + this->pendingProjectionCreations.clear(); + this->pendingProjectionDestructions.clear(); + + wm->bWindowsets = windowsets; + wm->bWindowsetProjections = projections; + + Qt::endPropertyUpdateGroup(); +} + +void WindowsetManager::onWindowsetCreated(impl::Workspace* workspace) { + this->pendingWindowsetCreations.append(workspace); +} + +void WindowsetManager::onWindowsetDestroyed(impl::Workspace* workspace) { + if (!this->pendingWindowsetCreations.removeOne(workspace)) { + this->pendingWindowsetDestructions.append(workspace); + } +} + +void WindowsetManager::onProjectionCreated(impl::WorkspaceGroup* group) { + this->pendingProjectionCreations.append(group); +} + +void WindowsetManager::onProjectionDestroyed(impl::WorkspaceGroup* group) { + if (!this->pendingProjectionCreations.removeOne(group)) { + this->pendingProjectionDestructions.append(group); + } +} + +WindowsetManager* WindowsetManager::instance() { + static auto* instance = new WindowsetManager(); + return instance; +} + +WlWindowset::WlWindowset(WindowsetManager* manager, impl::Workspace* impl) + : Windowset(manager) + , impl(impl) { + this->commitImpl(); +} + +void WlWindowset::commitImpl() { + Qt::beginPropertyUpdateGroup(); + this->bId = this->impl->id; + this->bName = this->impl->name; + this->bCoordinates = this->impl->coordinates; + this->bActive = this->impl->active; + this->bShouldDisplay = !this->impl->hidden; + this->bUrgent = this->impl->urgent; + this->bCanActivate = this->impl->canActivate; + this->bCanDeactivate = this->impl->canDeactivate; + this->bCanSetProjection = this->impl->canAssign; + this->bProjection = this->manager()->projectionsByImpl.value(this->impl->group); + Qt::endPropertyUpdateGroup(); +} + +void WlWindowset::activate() { + if (!this->bCanActivate) { + qCritical(logWorkspace) << this << "cannot be activated"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling activate() for" << this; + this->impl->activate(); + WindowsetManager::instance()->scheduleCommit(); +} + +void WlWindowset::deactivate() { + if (!this->bCanDeactivate) { + qCritical(logWorkspace) << this << "cannot be deactivated"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling deactivate() for" << this; + this->impl->deactivate(); + WindowsetManager::instance()->scheduleCommit(); +} + +void WlWindowset::remove() { + if (!this->bCanRemove) { + qCritical(logWorkspace) << this << "cannot be removed"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling remove() for" << this; + this->impl->remove(); + WindowsetManager::instance()->scheduleCommit(); +} + +void WlWindowset::setProjection(WindowsetProjection* projection) { + if (!this->bCanSetProjection) { + qCritical(logWorkspace) << this << "cannot be assigned to a projection"; + return; + } + + if (!projection) { + qCritical(logWorkspace) << "Cannot set a windowset's projection to null"; + return; + } + + WlWindowsetProjection* wlProjection = nullptr; + if (auto* p = dynamic_cast(projection)) { + wlProjection = p; + } else if (auto* p = dynamic_cast(projection)) { + // In the 99% case, there will only be a single windowset on a screen. + // In the 1% case, the oldest projection (first in list) is most likely the desired one. + auto* screen = p->screen(); + for (const auto& proj: WindowsetManager::instance()->projectionsByImpl.values()) { + if (proj->bQScreens.value().contains(screen)) { + wlProjection = proj; + break; + } + } + } + + if (!wlProjection) { + qCritical(logWorkspace) << "Cannot set a windowset's projection to" << projection + << "as no wayland projection could be derived."; + return; + } + + qCDebug(impl::logWorkspace) << "Assigning" << this << "to" << projection; + this->impl->assign(wlProjection->impl->object()); + WindowsetManager::instance()->scheduleCommit(); +} + +WlWindowsetProjection::WlWindowsetProjection(WindowsetManager* manager, impl::WorkspaceGroup* impl) + : WindowsetProjection(manager) + , impl(impl) { + this->commitImpl(); +} + +void WlWindowsetProjection::commitImpl() { + // TODO: will not commit the correct screens if missing qt repr at commit time + this->bQScreens = this->impl->screens.screens(); +} + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/windowset.hpp b/src/wayland/windowmanager/windowset.hpp new file mode 100644 index 0000000..52d1c63 --- /dev/null +++ b/src/wayland/windowmanager/windowset.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../../windowmanager/windowset.hpp" +#include "ext_workspace.hpp" + +namespace qs::wm::wayland { +namespace impl = qs::wayland::workspace; + +class WlWindowset; +class WlWindowsetProjection; + +class WindowsetManager: public QObject { + Q_OBJECT; + +public: + static WindowsetManager* instance(); + + void scheduleCommit(); + +private slots: + void doCommit(); + void onServerCommit(); + void onWindowsetCreated(impl::Workspace* workspace); + void onWindowsetDestroyed(impl::Workspace* workspace); + void onProjectionCreated(impl::WorkspaceGroup* group); + void onProjectionDestroyed(impl::WorkspaceGroup* group); + +private: + WindowsetManager(); + + bool commitScheduled = false; + + QList pendingWindowsetCreations; + QList pendingWindowsetDestructions; + QHash windowsetByImpl; + + QList pendingProjectionCreations; + QList pendingProjectionDestructions; + QHash projectionsByImpl; + + friend class WlWindowset; +}; + +class WlWindowset: public Windowset { +public: + WlWindowset(WindowsetManager* manager, impl::Workspace* impl); + + void commitImpl(); + + void activate() override; + void deactivate() override; + void remove() override; + void setProjection(WindowsetProjection* projection) override; + + [[nodiscard]] WindowsetManager* manager() { + return static_cast(this->parent()); // NOLINT + } + +private: + impl::Workspace* impl = nullptr; +}; + +class WlWindowsetProjection: public WindowsetProjection { +public: + WlWindowsetProjection(WindowsetManager* manager, impl::WorkspaceGroup* impl); + + void commitImpl(); + + [[nodiscard]] WindowsetManager* manager() { + return static_cast(this->parent()); // NOLINT + } + +private: + impl::WorkspaceGroup* impl = nullptr; + + friend class WlWindowset; +}; + +} // namespace qs::wm::wayland diff --git a/src/windowmanager/CMakeLists.txt b/src/windowmanager/CMakeLists.txt new file mode 100644 index 0000000..3c032f4 --- /dev/null +++ b/src/windowmanager/CMakeLists.txt @@ -0,0 +1,20 @@ +qt_add_library(quickshell-windowmanager STATIC + screenprojection.cpp + windowmanager.cpp + windowset.cpp +) + +qt_add_qml_module(quickshell-windowmanager + URI Quickshell.WindowManager + VERSION 0.1 + DEPENDENCIES QtQuick +) + +qs_add_module_deps_light(quickshell-windowmanager Quickshell) + +install_qml_module(quickshell-windowmanager) + +qs_module_pch(quickshell-windowmanager SET large) + +target_link_libraries(quickshell-windowmanager PRIVATE Qt::Quick) +target_link_libraries(quickshell PRIVATE quickshell-windowmanagerplugin) diff --git a/src/windowmanager/module.md b/src/windowmanager/module.md new file mode 100644 index 0000000..3480d60 --- /dev/null +++ b/src/windowmanager/module.md @@ -0,0 +1,10 @@ +name = "Quickshell.WindowManager" +description = "Window manager interface" +headers = [ + "windowmanager.hpp", + "windowset.hpp", + "screenprojection.hpp", +] +----- +Currently only supports the [ext-workspace-v1](https://wayland.app/protocols/ext-workspace-v1) wayland protocol. +Support will be expanded in future releases. diff --git a/src/windowmanager/screenprojection.cpp b/src/windowmanager/screenprojection.cpp new file mode 100644 index 0000000..c09e6f0 --- /dev/null +++ b/src/windowmanager/screenprojection.cpp @@ -0,0 +1,30 @@ +#include "screenprojection.hpp" + +#include +#include +#include + +#include "windowmanager.hpp" +#include "windowset.hpp" + +namespace qs::wm { + +ScreenProjection::ScreenProjection(QScreen* screen, QObject* parent) + : WindowsetProjection(parent) + , mScreen(screen) { + this->bQScreens = {screen}; + this->bWindowsets.setBinding([this]() { + QList result; + for (auto* ws: WindowManager::instance()->bindableWindowsets().value()) { + auto* proj = ws->bindableProjection().value(); + if (proj && proj->bindableQScreens().value().contains(this->mScreen)) { + result.append(ws); + } + } + return result; + }); +} + +QScreen* ScreenProjection::screen() const { return this->mScreen; } + +} // namespace qs::wm diff --git a/src/windowmanager/screenprojection.hpp b/src/windowmanager/screenprojection.hpp new file mode 100644 index 0000000..6b0f31e --- /dev/null +++ b/src/windowmanager/screenprojection.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + +#include "windowset.hpp" + +namespace qs::wm { + +///! WindowsetProjection covering one specific screen. +/// A ScreenProjection is a special type of @@WindowsetProjection which aggregates +/// all windowsets across all projections covering a specific screen. +/// +/// When used with @@Windowset.setProjection(), an arbitrary projection on the screen +/// will be picked. Usually there is only one. +/// +/// Use @@WindowManager.screenProjection() to get a ScreenProjection for a given screen. +class ScreenProjection: public WindowsetProjection { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + +public: + ScreenProjection(QScreen* screen, QObject* parent); + + [[nodiscard]] QScreen* screen() const; + +private: + QScreen* mScreen; +}; + +} // namespace qs::wm diff --git a/src/windowmanager/test/manual/WorkspaceDelegate.qml b/src/windowmanager/test/manual/WorkspaceDelegate.qml new file mode 100644 index 0000000..4ebd7f2 --- /dev/null +++ b/src/windowmanager/test/manual/WorkspaceDelegate.qml @@ -0,0 +1,86 @@ +import QtQuick +import QtQuick.Controls.Fusion +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.WindowManager + +WrapperRectangle { + id: delegate + required property Windowset modelData; + color: modelData.active ? "green" : "gray" + + ColumnLayout { + Label { text: delegate.modelData.toString() } + Label { text: `Id: ${delegate.modelData.id} Name: ${delegate.modelData.name}` } + Label { text: `Coordinates: ${delegate.modelData.coordinates.toString()}`} + + RowLayout { + Label { text: "Group:" } + ComboBox { + Layout.fillWidth: true + implicitContentWidthPolicy: ComboBox.WidestText + enabled: delegate.modelData.canSetProjection + model: [...WindowManager.windowsetProjections].map(w => w.toString()) + currentIndex: WindowManager.windowsetProjections.indexOf(delegate.modelData.projection) + onActivated: i => delegate.modelData.setProjection(WindowManager.windowsetProjections[i]) + } + } + + RowLayout { + Label { text: "Screen:" } + ComboBox { + Layout.fillWidth: true + implicitContentWidthPolicy: ComboBox.WidestText + enabled: delegate.modelData.canSetProjection + model: [...Quickshell.screens].map(w => w.name) + currentIndex: Quickshell.screens.indexOf(delegate.modelData.projection.screens[0]) + onActivated: i => delegate.modelData.setProjection(WindowManager.screenProjection(Quickshell.screens[i])) + } + } + + + RowLayout { + DisplayCheckBox { + text: "Active" + checked: delegate.modelData.active + } + + DisplayCheckBox { + text: "Urgent" + checked: delegate.modelData.urgent + } + + DisplayCheckBox { + text: "Should Display" + checked: delegate.modelData.shouldDisplay + } + } + + RowLayout { + Button { + text: "Activate" + enabled: delegate.modelData.canActivate + onClicked: delegate.modelData.activate() + } + + Button { + text: "Deactivate" + enabled: delegate.modelData.canDeactivate + onClicked: delegate.modelData.deactivate() + } + + Button { + text: "Remove" + enabled: delegate.modelData.canRemove + onClicked: delegate.modelData.remove() + } + } + } + + component DisplayCheckBox: CheckBox { + enabled: false + palette.disabled.text: parent.palette.active.text + palette.disabled.windowText: parent.palette.active.windowText + } +} diff --git a/src/windowmanager/test/manual/screenproj.qml b/src/windowmanager/test/manual/screenproj.qml new file mode 100644 index 0000000..d06036c --- /dev/null +++ b/src/windowmanager/test/manual/screenproj.qml @@ -0,0 +1,45 @@ +import QtQuick +import QtQuick.Controls.Fusion +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.WindowManager + +FloatingWindow { + ScrollView { + anchors.fill: parent + + ColumnLayout { + Repeater { + model: Quickshell.screens + + WrapperRectangle { + id: delegate + required property ShellScreen modelData + color: "slategray" + margin: 5 + + ColumnLayout { + Label { text: `Screen: ${delegate.modelData.name}` } + + Repeater { + model: ScriptModel { + values: WindowManager.screenProjection(delegate.modelData).windowsets + } + + WorkspaceDelegate {} + } + } + } + } + + Repeater { + model: ScriptModel { + values: WindowManager.windowsets.filter(w => w.projection == null) + } + + WorkspaceDelegate {} + } + } + } +} diff --git a/src/windowmanager/test/manual/workspaces.qml b/src/windowmanager/test/manual/workspaces.qml new file mode 100644 index 0000000..d6fdf05 --- /dev/null +++ b/src/windowmanager/test/manual/workspaces.qml @@ -0,0 +1,46 @@ +import QtQuick +import QtQuick.Controls.Fusion +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.WindowManager + +FloatingWindow { + ScrollView { + anchors.fill: parent + + ColumnLayout { + Repeater { + model: WindowManager.windowsetProjections + + WrapperRectangle { + id: delegate + required property WindowsetProjection modelData + color: "slategray" + margin: 5 + + ColumnLayout { + Label { text: delegate.modelData.toString() } + Label { text: `Screens: ${delegate.modelData.screens.map(s => s.name)}` } + + Repeater { + model: ScriptModel { + values: delegate.modelData.windowsets + } + + WorkspaceDelegate {} + } + } + } + } + + Repeater { + model: ScriptModel { + values: WindowManager.windowsets.filter(w => w.projection == null) + } + + WorkspaceDelegate {} + } + } + } +} diff --git a/src/windowmanager/windowmanager.cpp b/src/windowmanager/windowmanager.cpp new file mode 100644 index 0000000..6b51db1 --- /dev/null +++ b/src/windowmanager/windowmanager.cpp @@ -0,0 +1,41 @@ +#include "windowmanager.hpp" +#include +#include + +#include + +#include "../core/qmlscreen.hpp" +#include "screenprojection.hpp" + +namespace qs::wm { + +std::function WindowManager::provider; + +void WindowManager::setProvider(std::function provider) { + WindowManager::provider = std::move(provider); +} + +WindowManager* WindowManager::instance() { + static auto* instance = WindowManager::provider(); + return instance; +} + +ScreenProjection* WindowManager::screenProjection(QuickshellScreenInfo* screen) { + auto* qscreen = screen->screen; + auto it = this->mScreenProjections.find(qscreen); + if (it != this->mScreenProjections.end()) { + return *it; + } + + auto* projection = new ScreenProjection(qscreen, this); + this->mScreenProjections.insert(qscreen, projection); + + QObject::connect(qscreen, &QObject::destroyed, this, [this, projection, qscreen]() { + this->mScreenProjections.remove(qscreen); + delete projection; + }); + + return projection; +} + +} // namespace qs::wm diff --git a/src/windowmanager/windowmanager.hpp b/src/windowmanager/windowmanager.hpp new file mode 100644 index 0000000..054e485 --- /dev/null +++ b/src/windowmanager/windowmanager.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../core/qmlscreen.hpp" +#include "screenprojection.hpp" +#include "windowset.hpp" + +namespace qs::wm { + +class WindowManager: public QObject { + Q_OBJECT; + +public: + static void setProvider(std::function provider); + static WindowManager* instance(); + + Q_INVOKABLE ScreenProjection* screenProjection(QuickshellScreenInfo* screen); + + [[nodiscard]] QBindable> bindableWindowsets() const { + return &this->bWindowsets; + } + + [[nodiscard]] QBindable> bindableWindowsetProjections() const { + return &this->bWindowsetProjections; + } + +signals: + void windowsetsChanged(); + void windowsetProjectionsChanged(); + +public: + Q_OBJECT_BINDABLE_PROPERTY( + WindowManager, + QList, + bWindowsets, + &WindowManager::windowsetsChanged + ); + + Q_OBJECT_BINDABLE_PROPERTY( + WindowManager, + QList, + bWindowsetProjections, + &WindowManager::windowsetProjectionsChanged + ); + +private: + static std::function provider; + QHash mScreenProjections; +}; + +///! Window management interfaces exposed by the window manager. +class WindowManagerQml: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(WindowManager); + QML_SINGLETON; + // clang-format off + /// All windowsets tracked by the WM across all projections. + Q_PROPERTY(QList windowsets READ default BINDABLE bindableWindowsets); + /// All windowset projections tracked by the WM. Does not include + /// internal projections from @@screenProjection(). + Q_PROPERTY(QList windowsetProjections READ default BINDABLE bindableWindowsetProjections); + // clang-format on + +public: + /// Returns an internal WindowsetProjection that covers a single screen and contains all + /// windowsets on that screen, regardless of the WM-specified projection. Depending on + /// how the WM lays out its actual projections, multiple ScreenProjections may contain + /// the same Windowsets. + Q_INVOKABLE static ScreenProjection* screenProjection(QuickshellScreenInfo* screen) { + return WindowManager::instance()->screenProjection(screen); + } + + [[nodiscard]] static QBindable> bindableWindowsets() { + return WindowManager::instance()->bindableWindowsets(); + } + + [[nodiscard]] static QBindable> bindableWindowsetProjections() { + return WindowManager::instance()->bindableWindowsetProjections(); + } +}; + +} // namespace qs::wm diff --git a/src/windowmanager/windowset.cpp b/src/windowmanager/windowset.cpp new file mode 100644 index 0000000..6231c40 --- /dev/null +++ b/src/windowmanager/windowset.cpp @@ -0,0 +1,45 @@ +#include "windowset.hpp" + +#include +#include +#include +#include + +#include "../core/qmlglobal.hpp" +#include "windowmanager.hpp" + +namespace qs::wm { + +Q_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.workspace", QtWarningMsg); + +void Windowset::activate() { qCCritical(logWorkspace) << this << "cannot be activated"; } +void Windowset::deactivate() { qCCritical(logWorkspace) << this << "cannot be deactivated"; } +void Windowset::remove() { qCCritical(logWorkspace) << this << "cannot be removed"; } + +void Windowset::setProjection(WindowsetProjection* /*projection*/) { + qCCritical(logWorkspace) << this << "cannot be assigned to a projection"; +} + +WindowsetProjection::WindowsetProjection(QObject* parent): QObject(parent) { + this->bWindowsets.setBinding([this] { + QList result; + for (auto* ws: WindowManager::instance()->bindableWindowsets().value()) { + if (ws->bindableProjection().value() == this) { + result.append(ws); + } + } + return result; + }); + + this->bScreens.setBinding([this] { + QList screens; + + for (auto* screen: this->bQScreens.value()) { + screens.append(QuickshellTracked::instance()->screenInfo(screen)); + } + + return screens; + }); +} + +} // namespace qs::wm diff --git a/src/windowmanager/windowset.hpp b/src/windowmanager/windowset.hpp new file mode 100644 index 0000000..51cbd9b --- /dev/null +++ b/src/windowmanager/windowset.hpp @@ -0,0 +1,175 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QuickshellScreenInfo; + +namespace qs::wm { + +Q_DECLARE_LOGGING_CATEGORY(logWorkspace); + +class WindowsetProjection; + +///! A group of windows worked with by a user, usually known as a Workspace or Tag. +/// A Windowset is a generic type that encompasses both "Workspaces" and "Tags" in window managers. +/// Because the definition encompasses both you may not necessarily need all features. +class Windowset: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + // clang-format off + /// A persistent internal identifier for the windowset. This property should be identical + /// across restarts and destruction/recreation of a windowset. + Q_PROPERTY(QString id READ default NOTIFY idChanged BINDABLE bindableId); + /// Human readable name of the windowset. + Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName); + /// Coordinates of the workspace, represented as an N-dimensional array. Most WMs + /// will only expose one coordinate. If more than one is exposed, the first is + /// conventionally X, the second Y, and the third Z. + Q_PROPERTY(QList coordinates READ default NOTIFY coordinatesChanged BINDABLE bindableCoordinates); + /// True if the windowset is currently active. In a workspace based WM, this means the + /// represented workspace is current. In a tag based WM, this means the represented tag + /// is active. + Q_PROPERTY(bool active READ default NOTIFY activeChanged BINDABLE bindableActive); + /// The projection this windowset is a member of. A projection is the set of screens covered by + /// a windowset. + Q_PROPERTY(WindowsetProjection* projection READ default NOTIFY projectionChanged BINDABLE bindableProjection); + /// If false, this windowset should generally be hidden from workspace pickers. + Q_PROPERTY(bool shouldDisplay READ default NOTIFY shouldDisplayChanged BINDABLE bindableShouldDisplay); + /// If true, a window in this windowset has been marked as urgent. + Q_PROPERTY(bool urgent READ default NOTIFY urgentChanged BINDABLE bindableUrgent); + /// If true, the windowset can be activated. In a workspace based WM, this will make the workspace + /// current, in a tag based wm, the tag will be activated. + Q_PROPERTY(bool canActivate READ default NOTIFY canActivateChanged BINDABLE bindableCanActivate); + /// If true, the windowset can be deactivated. In a workspace based WM, deactivation is usually implicit + /// and based on activation of another workspace. + Q_PROPERTY(bool canDeactivate READ default NOTIFY canDeactivateChanged BINDABLE bindableCanDeactivate); + /// If true, the windowset can be removed. This may be done implicitly by the WM as well. + Q_PROPERTY(bool canRemove READ default NOTIFY canRemoveChanged BINDABLE bindableCanRemove); + /// If true, the windowset can be moved to a different projection. + Q_PROPERTY(bool canSetProjection READ default NOTIFY canSetProjectionChanged BINDABLE bindableCanSetProjection); + // clang-format on + +public: + explicit Windowset(QObject* parent): QObject(parent) {} + + /// Activate the windowset, making it the current workspace on a workspace based WM, or activating + /// the tag on a tag based WM. Requires @@canActivate. + Q_INVOKABLE virtual void activate(); + /// Deactivate the windowset, hiding it. Requires @@canDeactivate. + Q_INVOKABLE virtual void deactivate(); + /// Remove or destroy the windowset. Requires @@canRemove. + Q_INVOKABLE virtual void remove(); + /// Move the windowset to a different projection. A projection represents the set of screens + /// a workspace spans. Requires @@canSetProjection. + Q_INVOKABLE virtual void setProjection(WindowsetProjection* projection); + + [[nodiscard]] QBindable bindableId() const { return &this->bId; } + [[nodiscard]] QBindable bindableName() const { return &this->bName; } + [[nodiscard]] QBindable> bindableCoordinates() const { return &this->bCoordinates; } + [[nodiscard]] QBindable bindableActive() const { return &this->bActive; } + + [[nodiscard]] QBindable bindableProjection() const { + return &this->bProjection; + } + + [[nodiscard]] QBindable bindableShouldDisplay() const { return &this->bShouldDisplay; } + [[nodiscard]] QBindable bindableUrgent() const { return &this->bUrgent; } + [[nodiscard]] QBindable bindableCanActivate() const { return &this->bCanActivate; } + [[nodiscard]] QBindable bindableCanDeactivate() const { return &this->bCanDeactivate; } + [[nodiscard]] QBindable bindableCanRemove() const { return &this->bCanRemove; } + + [[nodiscard]] QBindable bindableCanSetProjection() const { + return &this->bCanSetProjection; + } + +signals: + void idChanged(); + void nameChanged(); + void coordinatesChanged(); + void activeChanged(); + void projectionChanged(); + void shouldDisplayChanged(); + void urgentChanged(); + void canActivateChanged(); + void canDeactivateChanged(); + void canRemoveChanged(); + void canSetProjectionChanged(); + +protected: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(Windowset, QString, bId, &Windowset::idChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, QString, bName, &Windowset::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, QList, bCoordinates); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bActive, &Windowset::activeChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, WindowsetProjection*, bProjection, &Windowset::projectionChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bShouldDisplay, &Windowset::shouldDisplayChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bUrgent, &Windowset::urgentChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanActivate, &Windowset::canActivateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanDeactivate, &Windowset::canDeactivateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanRemove, &Windowset::canRemoveChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanSetProjection, &Windowset::canSetProjectionChanged); + // clang-format on +}; + +///! A space occupiable by a Windowset. +/// A WindowsetProjection represents a space that can be occupied by one or more @@Windowset$s. +/// The space is one or more screens. Multiple projections may occupy the same screens. +/// +/// @@WindowManager.screenProjection() can be used to get a projection representing all +/// @@Windowset$s on a given screen regardless of the WM's actual projection layout. +class WindowsetProjection: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + // clang-format off + /// Screens the windowset projection spans, often a single screen or all screens. + Q_PROPERTY(QList screens READ default NOTIFY screensChanged BINDABLE bindableScreens); + /// Windowsets that are currently present on the projection. + Q_PROPERTY(QList windowsets READ default NOTIFY windowsetsChanged BINDABLE bindableWindowsets); + // clang-format on + +public: + explicit WindowsetProjection(QObject* parent); + + [[nodiscard]] QBindable> bindableScreens() const { + return &this->bScreens; + } + + [[nodiscard]] QBindable> bindableQScreens() const { return &this->bQScreens; } + + [[nodiscard]] QBindable> bindableWindowsets() const { + return &this->bWindowsets; + } + +signals: + void screensChanged(); + void windowsetsChanged(); + +protected: + Q_OBJECT_BINDABLE_PROPERTY(WindowsetProjection, QList, bQScreens); + + Q_OBJECT_BINDABLE_PROPERTY( + WindowsetProjection, + QList, + bScreens, + &WindowsetProjection::screensChanged + ); + + Q_OBJECT_BINDABLE_PROPERTY( + WindowsetProjection, + QList, + bWindowsets, + &WindowsetProjection::windowsetsChanged + ); +}; + +} // namespace qs::wm From 365bf16b1ebc221f6124e19a0fa5b6ef8dc1d517 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 16 Mar 2026 21:19:20 -0700 Subject: [PATCH 21/26] wayland: hook wl_proxy_get_listener avoiding QTBUG-145022 crash Co-authored-by: Lemmy --- changelog/next.md | 1 + src/wayland/CMakeLists.txt | 8 +++++ src/wayland/init.cpp | 2 ++ src/wayland/windowmanager/windowset.cpp | 2 +- src/wayland/wl_proxy_safe_deref.cpp | 42 +++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/wayland/wl_proxy_safe_deref.cpp diff --git a/changelog/next.md b/changelog/next.md index cbfd51b..3969d55 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -58,6 +58,7 @@ set shell id. - Fixed ToplevelManager not clearing activeToplevel on deactivation. - Desktop action order is now preserved. - Fixed partial socket reads in greetd and hyprland on slow machines. +- Worked around Qt bug causing crashes when plugging and unplugging monitors. ## Packaging Changes diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index db53f37..13e648a 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -73,6 +73,7 @@ endfunction() # ----- qt_add_library(quickshell-wayland STATIC + wl_proxy_safe_deref.cpp platformmenu.cpp popupanchor.cpp xdgshell.cpp @@ -80,6 +81,13 @@ qt_add_library(quickshell-wayland STATIC output_tracking.cpp ) +# required for wl_proxy_safe_deref +target_link_libraries(quickshell-wayland PRIVATE ${CMAKE_DL_LIBS}) +target_link_options(quickshell PRIVATE + "LINKER:--export-dynamic-symbol=wl_proxy_get_listener" + "LINKER:--require-defined=wl_proxy_get_listener" +) + # required to make sure the constructor is linked add_library(quickshell-wayland-init OBJECT init.cpp) diff --git a/src/wayland/init.cpp b/src/wayland/init.cpp index e56eee3..790cebb 100644 --- a/src/wayland/init.cpp +++ b/src/wayland/init.cpp @@ -10,6 +10,7 @@ #include "wlr_layershell/wlr_layershell.hpp" #endif +void installWlProxySafeDeref(); // NOLINT(misc-use-internal-linkage) void installPlatformMenuHook(); // NOLINT(misc-use-internal-linkage) void installPopupPositioner(); // NOLINT(misc-use-internal-linkage) @@ -33,6 +34,7 @@ class WaylandPlugin: public QsEnginePlugin { } void init() override { + installWlProxySafeDeref(); installPlatformMenuHook(); installPopupPositioner(); } diff --git a/src/wayland/windowmanager/windowset.cpp b/src/wayland/windowmanager/windowset.cpp index 796cfe2..74e273d 100644 --- a/src/wayland/windowmanager/windowset.cpp +++ b/src/wayland/windowmanager/windowset.cpp @@ -8,9 +8,9 @@ #include #include +#include "../../windowmanager/screenprojection.hpp" #include "../../windowmanager/windowmanager.hpp" #include "../../windowmanager/windowset.hpp" -#include "../../windowmanager/screenprojection.hpp" #include "ext_workspace.hpp" namespace qs::wm::wayland { diff --git a/src/wayland/wl_proxy_safe_deref.cpp b/src/wayland/wl_proxy_safe_deref.cpp new file mode 100644 index 0000000..0ebc258 --- /dev/null +++ b/src/wayland/wl_proxy_safe_deref.cpp @@ -0,0 +1,42 @@ + +#include +#include +#include +#include +#include + +#include "../core/logcat.hpp" + +namespace { +QS_LOGGING_CATEGORY(logDeref, "quickshell.wayland.safederef", QtWarningMsg); +using wl_proxy_get_listener_t = const void* (*) (wl_proxy*); +wl_proxy_get_listener_t original_wl_proxy_get_listener = nullptr; // NOLINT +} // namespace + +extern "C" { +WL_EXPORT const void* wl_proxy_get_listener(struct wl_proxy* proxy) { + // Avoid null derefs of protocol objects in qtbase. + // https://qt-project.atlassian.net/browse/QTBUG-145022 + if (!proxy) [[unlikely]] { + qCCritical(logDeref) << "wl_proxy_get_listener called with a null proxy!"; + return nullptr; + } + + return original_wl_proxy_get_listener(proxy); +} +} + +// NOLINTBEGIN (concurrency-mt-unsafe) +void installWlProxySafeDeref() { + dlerror(); // clear old errors + + original_wl_proxy_get_listener = + reinterpret_cast(dlsym(RTLD_NEXT, "wl_proxy_get_listener")); + + if (auto* error = dlerror()) { + qCCritical(logDeref) << "Failed to find wl_proxy_get_listener for hooking:" << error; + } else { + qCInfo(logDeref) << "Installed wl_proxy_get_listener hook."; + } +} +// NOLINTEND From 1bd5b083cb48c13f901f276fc5d94c1b0a1ef9a1 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 16 Mar 2026 22:38:32 -0700 Subject: [PATCH 22/26] hyprland/ipc: add null checks and ws preinit to toplevel object init Previously HyprlandToplevel::updateFromObject did not call findWorkspaceByName with createIfMissing=true, leaving bWorkspace null for a later insertToplevel call from HyprlandIpc::refreshToplevels. --- changelog/next.md | 1 + src/wayland/hyprland/ipc/connection.cpp | 2 +- src/wayland/hyprland/ipc/hyprland_toplevel.cpp | 10 +++------- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 3969d55..cceb79e 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -48,6 +48,7 @@ set shell id. - Fixed volumes not initializing if a pipewire device was already loaded before its node. - Fixed hyprland active toplevel not resetting after window closes. - Fixed hyprland ipc window names and titles being reversed. +- Fixed a hyprland ipc crash when refreshing toplevels before workspaces. - Fixed missing signals for system tray item title and description updates. - Fixed asynchronous loaders not working after reload. - Fixed asynchronous loaders not working before window creation. diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index d2d5105..d15701d 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -729,7 +729,7 @@ void HyprlandIpc::refreshToplevels() { } auto* workspace = toplevel->bindableWorkspace().value(); - workspace->insertToplevel(toplevel); + if (workspace) workspace->insertToplevel(toplevel); } }); } diff --git a/src/wayland/hyprland/ipc/hyprland_toplevel.cpp b/src/wayland/hyprland/ipc/hyprland_toplevel.cpp index 7b07bc8..43b9838 100644 --- a/src/wayland/hyprland/ipc/hyprland_toplevel.cpp +++ b/src/wayland/hyprland/ipc/hyprland_toplevel.cpp @@ -72,20 +72,16 @@ void HyprlandToplevel::updateFromObject(const QVariantMap& object) { Qt::beginPropertyUpdateGroup(); bool ok = false; auto address = addressStr.toULongLong(&ok, 16); - if (!ok || !address) { - return; - } + if (ok && address) this->setAddress(address); - this->setAddress(address); this->bTitle = title; auto workspaceMap = object.value("workspace").toMap(); auto workspaceName = workspaceMap.value("name").toString(); - auto* workspace = this->ipc->findWorkspaceByName(workspaceName, false); - if (!workspace) return; + auto* workspace = this->ipc->findWorkspaceByName(workspaceName, true); + if (workspace) this->setWorkspace(workspace); - this->setWorkspace(workspace); this->bLastIpcObject = object; Qt::endPropertyUpdateGroup(); } From 0a859d51f25e8fafccad8fa3bade7306e9f0da39 Mon Sep 17 00:00:00 2001 From: -k Date: Thu, 22 Jan 2026 21:40:56 -0500 Subject: [PATCH 23/26] service/pam: include `signal.h` on freebsd --- src/services/pam/conversation.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/pam/conversation.cpp b/src/services/pam/conversation.cpp index f8f5a09..1fb4c04 100644 --- a/src/services/pam/conversation.cpp +++ b/src/services/pam/conversation.cpp @@ -8,6 +8,9 @@ #include #include #include +#ifdef __FreeBSD__ +#include +#endif #include "../../core/logcat.hpp" #include "ipc.hpp" From 97b2688ad67d4af95c7378f2ca0cece8bd3f9952 Mon Sep 17 00:00:00 2001 From: -k Date: Tue, 3 Mar 2026 11:20:56 -0500 Subject: [PATCH 24/26] core/log: fix non-linux typo and import unistd on freebsd --- src/core/logging.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index d24225b..893c56e 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -31,6 +31,9 @@ #include #include #endif +#ifdef __FreeBSD__ +#include +#endif #include "instanceinfo.hpp" #include "logcat.hpp" @@ -67,7 +70,7 @@ bool copyFileData(int sourceFd, int destFd, qint64 size) { return true; #else std::array buffer = {}; - auto remaining = totalTarget; + auto remaining = usize; while (remaining > 0) { auto chunk = std::min(remaining, buffer.size()); From a51dcd0a015f72a9af5c2d188e056c58740948d2 Mon Sep 17 00:00:00 2001 From: -k Date: Mon, 16 Mar 2026 17:40:55 -0400 Subject: [PATCH 25/26] wayland: use patched surfaceRole accessor on FreeBSD --- src/wayland/wlr_layershell/surface.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/wayland/wlr_layershell/surface.cpp b/src/wayland/wlr_layershell/surface.cpp index 4a5015e..0b0e7d7 100644 --- a/src/wayland/wlr_layershell/surface.cpp +++ b/src/wayland/wlr_layershell/surface.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "../../window/panelinterface.hpp" #include "shell_integration.hpp" @@ -247,9 +248,19 @@ void LayerSurface::commit() { } void LayerSurface::attachPopup(QtWaylandClient::QWaylandShellSurface* popup) { - std::any role = popup->surfaceRole(); - - if (auto* popupRole = std::any_cast<::xdg_popup*>(&role)) { // NOLINT +#ifdef __FreeBSD__ + // FreeBSD uses an alternate RTTI matching strategy by default which does + // not work across modules, preventing std::any from downcasting. On + // FreeBSD, Qt is built with a patch to expose the surface role through a + // pointer instead of an any, which does not have this problem. + // See https://bugs.kde.org/show_bug.cgi?id=479679 + if (auto* xdgPopup = static_cast<::xdg_popup*>(popup->nativeResource("xdg_popup"))) { + this->get_popup(xdgPopup); + return; + } +#endif + auto role = popup->surfaceRole(); // NOLINT + if (auto* popupRole = std::any_cast<::xdg_popup*>(&role)) { this->get_popup(*popupRole); } else { qWarning() << "Cannot attach popup" << popup << "to shell surface" << this From b7005e09e7f7f170b554872e64b01483cc760e86 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 17 Mar 2026 03:49:20 -0700 Subject: [PATCH 26/26] docs: ask users not to submit v1 crash reports --- .github/ISSUE_TEMPLATE/crash.yml | 89 +++++-------------------------- .github/ISSUE_TEMPLATE/crash2.yml | 6 +-- 2 files changed, 15 insertions(+), 80 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml index 80fa827..b5a995a 100644 --- a/.github/ISSUE_TEMPLATE/crash.yml +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -1,82 +1,17 @@ name: Crash Report (v1) -description: Quickshell has crashed +description: Quickshell has crashed (old) labels: ["bug", "crash"] body: - - type: textarea - id: crashinfo + - type: markdown attributes: - label: General crash information - description: | - Paste the contents of the `info.txt` file in your crash folder here. - value: "
General information - - - ``` - - - - ``` - - -
" - validations: - required: true - - type: textarea - id: userinfo + value: | + Thank you for taking the time to click the report button. + At this point most of the worst issues in 0.2.1 and before have been fixed and we are + preparing for a new release. Please do not report crashes from 0.2.1 or before for now. + - type: checkboxes + id: donotcheck attributes: - label: What caused the crash - description: | - Any information likely to help debug the crash. What were you doing when the crash occurred, - what changes did you make, can you get it to happen again? - - type: textarea - id: dump - attributes: - label: Minidump - description: | - Attach `minidump.dmp.log` here. If it is too big to upload, compress it. - - You may skip this step if quickshell crashed while processing a password - or other sensitive information. If you skipped it write why instead. - validations: - required: true - - type: textarea - id: logs - attributes: - label: Log file - description: | - Attach `log.qslog.log` here. If it is too big to upload, compress it. - - You can preview the log if you'd like using `quickshell read-log `. - validations: - required: true - - type: textarea - id: config - attributes: - label: Configuration - description: | - Attach your configuration here, preferrably in full (not just one file). - Compress it into a zip, tar, etc. - - This will help us reproduce the crash ourselves. - - type: textarea - id: bt - attributes: - label: Backtrace - description: | - If you have gdb installed and use systemd, or otherwise know how to get a backtrace, - we would appreciate one. (You may have gdb installed without knowing it) - - 1. Run `coredumpctl debug ` where `pid` is the number shown after "Crashed process ID" - in the crash reporter. - 2. Once it loads, type `bt -full` (then enter) - 3. Copy the output and attach it as a file or in a spoiler. - - type: textarea - id: exe - attributes: - label: Executable - description: | - If the crash folder contains a executable.txt file, upload it here. If not you can ignore this field. - If it is too big to upload, compress it. - - Note: executable.txt is the quickshell binary. It has a .txt extension due to github's limitations on - filetypes. + label: Do not check this box + options: + - label: Do not check this box + required: true diff --git a/.github/ISSUE_TEMPLATE/crash2.yml b/.github/ISSUE_TEMPLATE/crash2.yml index 84beef8..6984460 100644 --- a/.github/ISSUE_TEMPLATE/crash2.yml +++ b/.github/ISSUE_TEMPLATE/crash2.yml @@ -9,14 +9,14 @@ body: description: | Any information likely to help debug the crash. What were you doing when the crash occurred, what changes did you make, can you get it to happen again? - - type: textarea + - type: upload id: report attributes: label: Report file description: Attach `report.txt` here. validations: required: true - - type: textarea + - type: upload id: logs attributes: label: Log file @@ -31,7 +31,7 @@ body: attributes: label: Configuration description: | - Attach your configuration here, preferrably in full (not just one file). + Attach or link your configuration here, preferrably in full (not just one file). Compress it into a zip, tar, etc. This will help us reproduce the crash ourselves.