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 29aecac..d624a06 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 @@ -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) @@ -67,7 +68,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 @@ -140,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) @@ -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/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/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 diff --git a/changelog/next.md b/changelog/next.md index ef63323..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 @@ -33,6 +34,10 @@ 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. +- 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 @@ -50,7 +55,9 @@ 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. +- Fixed partial socket reads in greetd and hyprland on slow machines. ## Packaging Changes @@ -58,3 +65,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/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/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/core/CMakeLists.txt b/src/core/CMakeLists.txt index fb63f40..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 @@ -40,6 +41,8 @@ qt_add_library(quickshell-core STATIC scriptmodel.cpp colorquantizer.cpp toolsupport.cpp + streamreader.cpp + debuginfo.cpp ) qt_add_qml_module(quickshell-core @@ -52,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 new file mode 100644 index 0000000..ae227f8 --- /dev/null +++ b/src/core/debuginfo.cpp @@ -0,0 +1,175 @@ +#include "debuginfo.hpp" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "build.hpp" + +extern char** environ; // NOLINT + +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 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)) { + 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 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); + + 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(); + + stream << "\n===== Environment (trimmed) =====\n"; + stream << envInfo(); + + return info; +} + +} // namespace qs::debuginfo diff --git a/src/core/debuginfo.hpp b/src/core/debuginfo.hpp new file mode 100644 index 0000000..fc766fc --- /dev/null +++ b/src/core/debuginfo.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace qs::debuginfo { + +QString qsVersion(); +QString qtVersion(); +QString gpuInfo(); +QString systemInfo(); +QString envInfo(); +QString combinedInfo(); + +} // namespace qs::debuginfo 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/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/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; 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/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); } 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(); } 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); 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, 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/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/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/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() { 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 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;