diff --git a/.clang-tidy b/.clang-tidy index da14682..c83ed8f 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -20,7 +20,6 @@ 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 d624a06..29aecac 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)` -If you are forking quickshell, please change `CRASHREPORT_URL` to your own issue tracker. +Please leave at least symbol names attached to the binary for debugging purposes. ### QML Module dir Currently all QML modules are statically linked to quickshell, but this is where @@ -33,7 +33,6 @@ 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) @@ -68,13 +67,7 @@ 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. - -*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. +When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the package manager or fetched with FetchContent. ### Jemalloc We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused @@ -147,6 +140,7 @@ Enables streaming video from monitors and toplevel windows through various proto To disable: `-DSCREENCOPY=OFF` Dependencies: +- `libdrm` - `libgbm` - `vulkan-headers` (build-time) @@ -242,7 +236,7 @@ Only `ninja` builds are tested, but makefiles may work. #### Configuring the build ```sh -$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release [additional disable flags from above here] +$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here] ``` Note that features you do not supply dependencies for MUST be disabled with their associated flags diff --git a/CMakeLists.txt b/CMakeLists.txt index 1226342..d57e322 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,9 +9,6 @@ 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 73e7931..39fab13 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,40 +1,235 @@ -# Contributing +# Contributing / Development +Instructions for development setup and upstreaming patches. -Thank you for taking the time to contribute. -To ensure nobody's time is wasted, please follow the rules below. +If you just want to build or package quickshell see [BUILD.md](BUILD.md). -## Acceptable Code Contributions +## Development -- 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. +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. -- 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**. +Quickshell also uses `just` for common development command aliases. -- 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. +The dependencies are also available as a nix shell or nix flake which we recommend +using with nix-direnv. -- Changes must follow the guidelines outlined in [HACKING.md](HACKING.md) for style and substance. +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 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. +### Formatting +All contributions should be formatted similarly to what already exists. +Group related functionality together. -## Acceptable Non-code Contributions +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`. -- Bug and crash reports. You must follow the instructions in the issue templates and provide the - information requested. +#### 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. -- Feature requests can be made via Issues. Please check to ensure nobody else has requested the same feature. +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. -- Do not make insubstantial or pointless changes. +```cpp +auto x = ; // ok +auto x = QString::number(3); // ok +QString x; // ok +QString x = "foo"; // ok +auto x = QString("foo"); // ok -- 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. +auto x = QString(); // avoid +QString x(); // avoid +QString x("foo"); // avoid +``` -## Merge timelines +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 -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. +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. diff --git a/HACKING.md b/HACKING.md deleted file mode 100644 index 69357f1..0000000 --- a/HACKING.md +++ /dev/null @@ -1,226 +0,0 @@ -## 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 365bdb5..4491d24 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,7 @@ This repo is hosted at: - https://github.com/quickshell-mirror/quickshell # Contributing / Development -- [HACKING.md](HACKING.md) - Development instructions and policy. -- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution policy. -- [BUILD.md](BUILD.md) - Packaging and build instructions. +See [CONTRIBUTING.md](CONTRIBUTING.md) for details. #### License diff --git a/changelog/next.md b/changelog/next.md index cbfd51b..ef63323 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -26,7 +26,6 @@ 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 @@ -34,10 +33,6 @@ 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 @@ -55,9 +50,7 @@ set shell id. - Fixed ClippingRectangle related crashes. - Fixed crashes when monitors are unplugged. - Fixed crashes when default pipewire devices are lost. -- Fixed ToplevelManager not clearing activeToplevel on deactivation. - Desktop action order is now preserved. -- Fixed partial socket reads in greetd and hyprland on slow machines. ## Packaging Changes @@ -65,4 +58,3 @@ 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 749ef49..02b8659 100644 --- a/default.nix +++ b/default.nix @@ -76,7 +76,6 @@ buildInputs = [ qt6.qtbase qt6.qtdeclarative - libdrm cli11 ] ++ lib.optional withQtSvg qt6.qtsvg @@ -89,7 +88,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) [ libgbm vulkan-headers ] + ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm vulkan-headers ] ++ lib.optional withX11 libxcb ++ lib.optional withPam pam ++ lib.optional withPipewire pipewire diff --git a/src/build/build.hpp.in b/src/build/build.hpp.in index acc3c58..2ab2db2 100644 --- a/src/build/build.hpp.in +++ b/src/build/build.hpp.in @@ -13,5 +13,4 @@ #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 4824965..fb63f40 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,4 +1,3 @@ -pkg_check_modules(libdrm REQUIRED IMPORTED_TARGET libdrm) qt_add_library(quickshell-core STATIC plugin.cpp shell.cpp @@ -41,8 +40,6 @@ qt_add_library(quickshell-core STATIC scriptmodel.cpp colorquantizer.cpp toolsupport.cpp - streamreader.cpp - debuginfo.cpp ) qt_add_qml_module(quickshell-core @@ -55,7 +52,7 @@ qt_add_qml_module(quickshell-core install_qml_module(quickshell-core) -target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build PkgConfig::libdrm) +target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build) qs_module_pch(quickshell-core SET large) diff --git a/src/core/debuginfo.cpp b/src/core/debuginfo.cpp deleted file mode 100644 index ae227f8..0000000 --- a/src/core/debuginfo.cpp +++ /dev/null @@ -1,175 +0,0 @@ -#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 deleted file mode 100644 index fc766fc..0000000 --- a/src/core/debuginfo.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#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 21febc3..c68af71 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -209,8 +209,6 @@ 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); } } @@ -231,11 +229,6 @@ 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(); } } @@ -244,11 +237,6 @@ 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 35504f6..6c26609 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -60,9 +60,7 @@ void QuickshellSettings::setWorkingDirectory(QString workingDirectory) { // NOLI emit this->workingDirectoryChanged(); } -bool QuickshellSettings::watchFiles() const { - return this->mWatchFiles && qEnvironmentVariableIsEmpty("QS_DISABLE_FILE_WATCHER"); -} +bool QuickshellSettings::watchFiles() const { return this->mWatchFiles; } void QuickshellSettings::setWatchFiles(bool watchFiles) { if (watchFiles == this->mWatchFiles) return; diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 58da38c..37b0fac 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -3,7 +3,6 @@ #include #include -#include #include #include #include @@ -22,25 +21,6 @@ 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); @@ -129,13 +109,13 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna qCDebug(logQmlScanner) << "Scanning qml file" << path; - QByteArray fileData; - if (!this->readAndHashFile(path, fileData)) { + auto file = QFile(path); + if (!file.open(QFile::ReadOnly | QFile::Text)) { qCWarning(logQmlScanner) << "Failed to open file" << path; return false; } - auto stream = QTextStream(&fileData); + auto stream = QTextStream(&file); auto imports = QVector(); bool inHeader = true; @@ -239,6 +219,8 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna postError("unclosed preprocessor if block"); } + file.close(); + if (isOverridden) { this->fileIntercepts.insert(path, overrideText); } @@ -275,11 +257,8 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna continue; } - if (import.endsWith(".js")) { - this->scannedFiles.push_back(cpath); - QByteArray jsData; - this->readAndHashFile(cpath, jsData); - } else this->scanDir(cpath); + if (import.endsWith(".js")) this->scannedFiles.push_back(cpath); + else this->scanDir(cpath); } return true; @@ -294,12 +273,14 @@ void QmlScanner::scanQmlRoot(const QString& path) { bool QmlScanner::scanQmlJson(const QString& path) { qCDebug(logQmlScanner) << "Scanning qml.json file" << path; - QByteArray data; - if (!this->readAndHashFile(path, data)) { + auto file = QFile(path); + if (!file.open(QFile::ReadOnly | QFile::Text)) { 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 26034e1..29f8f6a 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -22,7 +21,6 @@ public: QVector scannedDirs; QVector scannedFiles; - QHash fileHashes; QHash fileIntercepts; struct ScanError { @@ -33,9 +31,6 @@ 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 deleted file mode 100644 index 1f66e29..0000000 --- a/src/core/streamreader.cpp +++ /dev/null @@ -1,98 +0,0 @@ -#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 deleted file mode 100644 index abf14ef..0000000 --- a/src/core/streamreader.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#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 8f37085..c875c2e 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -81,11 +81,6 @@ 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 6a370ce..a3422d3 100644 --- a/src/crash/interface.cpp +++ b/src/crash/interface.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -13,22 +12,11 @@ #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) { @@ -79,16 +67,22 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid) if (qtVersionMatches) { mainLayout->addWidget( - new QLabel("Please open a bug report for this issue on the issue tracker.") + new QLabel("Please open a bug report for this issue via github or email.") ); } else { mainLayout->addWidget(new QLabel( "Please rebuild Quickshell against the current Qt version.\n" - "If this does not solve the problem, please open a bug report on the issue tracker." + "If this does not solve the problem, please open a bug report via github or email." )); } - mainLayout->addWidget(new ReportLabel("Tracker:", crashreportUrl(), this)); + 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)); auto* buttons = new QWidget(this); buttons->setMinimumWidth(900); @@ -118,5 +112,10 @@ void CrashReporterGui::openFolder() { QDesktopServices::openUrl(QUrl::fromLocalFile(this->reportFolder)); } -void CrashReporterGui::openReportUrl() { QDesktopServices::openUrl(QUrl(crashreportUrl())); } +void CrashReporterGui::openReportUrl() { + QDesktopServices::openUrl( + QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml") + ); +} + void CrashReporterGui::cancel() { QApplication::quit(); } diff --git a/src/crash/main.cpp b/src/crash/main.cpp index 05927f2..c406ba6 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -14,18 +15,19 @@ #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 { @@ -169,15 +171,41 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { qCCritical(logCrashReporter) << "Failed to open crash info file for writing."; } else { auto stream = QTextStream(&extraInfoFile); - stream << qs::debuginfo::combinedInfo(); + stream << "===== Build Information =====\n"; + stream << "Git Revision: " << GIT_REVISION << '\n'; + stream << "Buildtime Qt Version: " << QT_VERSION_STR << "\n"; + stream << "Build Type: " << BUILD_TYPE << '\n'; + stream << "Compiler: " << COMPILER << '\n'; + stream << "Complie Flags: " << COMPILE_FLAGS << "\n\n"; + stream << "Build configuration:\n" << BUILD_CONFIGURATION << "\n"; - stream << "\n===== Instance Information =====\n"; + stream << "\n===== Runtime Information =====\n"; + stream << "Runtime Qt Version: " << qVersion() << '\n'; stream << "Signal: " << strsignal(crashSignal) << " (" << crashSignal << ")\n"; // NOLINT stream << "Crashed process ID: " << crashProc << '\n'; stream << "Run ID: " << instance.instanceId << '\n'; stream << "Shell ID: " << instance.shellId << '\n'; stream << "Config Path: " << instance.configPath << '\n'; + stream << "\n===== 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 807eb24..151fc24 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,10 +519,20 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { } if (state.misc.printVersion) { - if (state.log.verbosity == 0) { - qCInfo(logBare).noquote() << "Quickshell" << qs::debuginfo::qsVersion(); - } else { - qCInfo(logBare).noquote() << qs::debuginfo::combinedInfo(); + 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; } } else if (*state.subcommand.log) { return readLogFile(state); diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index 3a9a2a5..ee7ca64 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -138,11 +138,9 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio }; #if CRASH_HANDLER - if (qEnvironmentVariableIsSet("QS_DISABLE_CRASH_HANDLER")) { - qInfo() << "Crash handling disabled."; - } else { - crash::CrashHandler::init(); + 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 a103531..2252f8c 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 quickshell-core) +target_link_libraries(quickshell-service-greetd PRIVATE Qt::Quick) qs_module_pch(quickshell-service-greetd) diff --git a/src/services/greetd/connection.cpp b/src/services/greetd/connection.cpp index 3b8fa24..7130870 100644 --- a/src/services/greetd/connection.cpp +++ b/src/services/greetd/connection.cpp @@ -145,7 +145,6 @@ 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) { @@ -161,84 +160,82 @@ void GreetdConnection::onSocketError(QLocalSocket::LocalSocketError error) { } void GreetdConnection::onSocketReady() { - while (true) { - this->reader.startTransaction(); - auto length = this->reader.readI32(); - auto text = this->reader.readBytes(length); - if (!this->reader.commitTransaction()) return; + qint32 length = 0; - auto json = QJsonDocument::fromJson(text).object(); - auto type = json.value("type").toString(); + this->socket.read(reinterpret_cast(&length), sizeof(qint32)); - qCDebug(logGreetd).noquote() << "Received greetd response:" << text; + auto text = this->socket.read(length); + auto json = QJsonDocument::fromJson(text).object(); + auto type = json.value("type").toString(); - 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(); + qCDebug(logGreetd).noquote() << "Received greetd response:" << text; - if (this->mExitAfterLaunch) { - qCDebug(logGreetd) << "Quitting."; - EngineGeneration::currentGeneration()->quit(); - } + 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(); - break; - default: goto unexpected; - } - } else if (type == "error") { - auto errorType = json.value("error_type").toString(); - auto desc = json.value("description").toString(); - - // Special case this error in case a session was already running. - // This cancels and restarts the session. - if (errorType == "error" && desc == "a session is already being configured") { - qCDebug( - logGreetd - ) << "A session was already in progress, cancelling it and starting a new one."; - this->setActive(false); - this->setActive(true); - return; + if (this->mExitAfterLaunch) { + qCDebug(logGreetd) << "Quitting."; + EngineGeneration::currentGeneration()->quit(); } - 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; + break; + default: goto unexpected; + } + } else if (type == "error") { + auto errorType = json.value("error_type").toString(); + auto desc = json.value("description").toString(); - // 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"; + // 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; + } - this->mResponseRequired = responseRequired; - emit this->authMessage(message, error, responseRequired, echoResponse); - - if (!responseRequired) { - this->sendRequest({{"type", "post_auth_message_response"}}); - } + 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; - continue; - unexpected: - qCCritical(logGreetd) << "Received unexpected greetd response" << text; - this->setActive(false); - } + // 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); } void GreetdConnection::sendRequest(const QJsonObject& json) { diff --git a/src/services/greetd/connection.hpp b/src/services/greetd/connection.hpp index 89348dc..0c1d1eb 100644 --- a/src/services/greetd/connection.hpp +++ b/src/services/greetd/connection.hpp @@ -8,8 +8,6 @@ #include #include -#include "../../core/streamreader.hpp" - ///! State of the Greetd connection. /// See @@Greetd.state. class GreetdState: public QObject { @@ -76,5 +74,4 @@ private: bool mResponseRequired = false; QString mUser; QLocalSocket socket; - StreamReader reader; }; diff --git a/src/wayland/hyprland/ipc/CMakeLists.txt b/src/wayland/hyprland/ipc/CMakeLists.txt index 9e42520..fd01463 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 quickshell-core) +target_link_libraries(quickshell-hyprland-ipc PRIVATE Qt::Quick) 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 d2d5105..ad091a6 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -93,7 +93,6 @@ 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) { @@ -105,11 +104,11 @@ void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) void HyprlandIpc::eventSocketReady() { while (true) { - this->eventReader.startTransaction(); - auto rawEvent = this->eventReader.readUntil('\n'); - if (!this->eventReader.commitTransaction()) return; + auto rawEvent = this->eventSocket.readLine(); + if (rawEvent.isEmpty()) break; - rawEvent.chop(1); // remove trailing \n + // remove trailing \n + rawEvent.truncate(rawEvent.length() - 1); 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 ba1e7c9..e15d5cd 100644 --- a/src/wayland/hyprland/ipc/connection.hpp +++ b/src/wayland/hyprland/ipc/connection.hpp @@ -14,7 +14,6 @@ #include "../../../core/model.hpp" #include "../../../core/qmlscreen.hpp" -#include "../../../core/streamreader.hpp" #include "../../../wayland/toplevel_management/handle.hpp" namespace qs::hyprland::ipc { @@ -140,7 +139,6 @@ 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 6a1d96b..0eae3de 100644 --- a/src/wayland/toplevel_management/qml.cpp +++ b/src/wayland/toplevel_management/qml.cpp @@ -161,11 +161,7 @@ void ToplevelManager::onToplevelReady(impl::ToplevelHandle* handle) { void ToplevelManager::onToplevelActiveChanged() { auto* toplevel = qobject_cast(this->sender()); - if (toplevel->activated()) { - this->setActiveToplevel(toplevel); - } else if (toplevel == this->mActiveToplevel) { - this->setActiveToplevel(nullptr); - } + if (toplevel->activated()) this->setActiveToplevel(toplevel); } void ToplevelManager::onToplevelClosed() { diff --git a/src/wayland/windowmanager/CMakeLists.txt b/src/wayland/windowmanager/CMakeLists.txt index 76d1d89..9e03b14 100644 --- a/src/wayland/windowmanager/CMakeLists.txt +++ b/src/wayland/windowmanager/CMakeLists.txt @@ -1,19 +1,20 @@ qt_add_library(quickshell-wayland-windowsystem STATIC windowmanager.cpp - windowset.cpp + workspace.cpp ext_workspace.cpp ) add_library(quickshell-wayland-windowsystem-init OBJECT init.cpp) target_link_libraries(quickshell-wayland-windowsystem-init PRIVATE Qt::Quick) +#wl_proto(wlp-ext-foreign-toplevel ext-foreign-toplevel-list-v1 "${WAYLAND_PROTOCOLS}/staging/ext-foreign-toplevel-list") wl_proto(wlp-ext-workspace ext-workspace-v1 "${WAYLAND_PROTOCOLS}/staging/ext-workspace") target_link_libraries(quickshell-wayland-windowsystem PRIVATE - Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client - wlp-ext-workspace + Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + Qt::Quick # for pch? potentially, check w/ gcc + + wlp-ext-foreign-toplevel 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 index fcb9ffa..3e4c099 100644 --- a/src/wayland/windowmanager/ext_workspace.cpp +++ b/src/wayland/windowmanager/ext_workspace.cpp @@ -1,20 +1,15 @@ #include "ext_workspace.hpp" -#include -#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); +Q_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.wayland.workspace"); WorkspaceManager::WorkspaceManager(): QWaylandClientExtensionTemplate(1) { this->initialize(); } @@ -131,7 +126,7 @@ WorkspaceGroup::~WorkspaceGroup() { if (this->isInitialized()) this->destroy(); } -void WorkspaceGroup::ext_workspace_group_handle_v1_capabilities(quint32 capabilities) { +void WorkspaceGroup::ext_workspace_group_handle_v1_capabilities(uint32_t capabilities) { this->canCreateWorkspace = capabilities & ext_workspace_group_handle_v1::group_capabilities_create_workspace; @@ -149,8 +144,7 @@ void WorkspaceGroup::ext_workspace_group_handle_v1_output_leave(::wl_output* out this->screens.removeOutput(output); } -void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_enter( - ::ext_workspace_handle_v1* handle +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; @@ -158,8 +152,7 @@ void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_enter( if (workspace) workspace->enterGroup(this); } -void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_leave( - ::ext_workspace_handle_v1* handle +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; diff --git a/src/wayland/windowmanager/ext_workspace.hpp b/src/wayland/windowmanager/ext_workspace.hpp index 6aff209..9dfac1b 100644 --- a/src/wayland/windowmanager/ext_workspace.hpp +++ b/src/wayland/windowmanager/ext_workspace.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -11,12 +12,11 @@ #include #include -#include "../../core/logcat.hpp" #include "../output_tracking.hpp" namespace qs::wayland::workspace { -QS_DECLARE_LOGGING_CATEGORY(logWorkspace); +Q_DECLARE_LOGGING_CATEGORY(logWorkspace); class WorkspaceGroup; class Workspace; @@ -83,8 +83,8 @@ 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_state(uint32_t state) override; + void ext_workspace_handle_v1_capabilities(uint32_t capabilities) override; void ext_workspace_handle_v1_removed() override; private: @@ -106,7 +106,7 @@ public: bool canCreateWorkspace : 1 = false; protected: - void ext_workspace_group_handle_v1_capabilities(quint32 capabilities) override; + void ext_workspace_group_handle_v1_capabilities(uint32_t capabilities) override; void ext_workspace_group_handle_v1_output_enter(::wl_output* output) override; void ext_workspace_group_handle_v1_output_leave(::wl_output* output) override; void ext_workspace_group_handle_v1_workspace_enter(::ext_workspace_handle_v1* handle) override; diff --git a/src/wayland/windowmanager/init.cpp b/src/wayland/windowmanager/init.cpp index 88be01a..fa336d7 100644 --- a/src/wayland/windowmanager/init.cpp +++ b/src/wayland/windowmanager/init.cpp @@ -1,6 +1,4 @@ -#include #include -#include #include "../../core/plugin.hpp" diff --git a/src/wayland/windowmanager/windowmanager.cpp b/src/wayland/windowmanager/windowmanager.cpp index 16245d0..5f4a450 100644 --- a/src/wayland/windowmanager/windowmanager.cpp +++ b/src/wayland/windowmanager/windowmanager.cpp @@ -1,20 +1,13 @@ #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; - }(); + static auto* instance = new WaylandWindowManager(); return instance; } -void installWmProvider() { // NOLINT (misc-use-internal-linkage) +void installWmProvider() { qs::wm::WindowManager::setProvider([]() { return WaylandWindowManager::instance(); }); } diff --git a/src/wayland/windowmanager/windowmanager.hpp b/src/wayland/windowmanager/windowmanager.hpp index 9d48efd..c732d6a 100644 --- a/src/wayland/windowmanager/windowmanager.hpp +++ b/src/wayland/windowmanager/windowmanager.hpp @@ -3,7 +3,7 @@ #include #include "../../windowmanager/windowmanager.hpp" -#include "windowset.hpp" +#include "workspace.hpp" namespace qs::wm::wayland { @@ -12,6 +12,14 @@ class WaylandWindowManager: public WindowManager { public: static WaylandWindowManager* instance(); + + [[nodiscard]] UntypedObjectModel* workspaces() const override { + return &WorkspaceManager::instance()->mWorkspaces; + } + + [[nodiscard]] UntypedObjectModel* workspaceGroups() const override { + return &WorkspaceManager::instance()->mWorkspaceGroups; + } }; } // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/windowset.cpp b/src/wayland/windowmanager/windowset.cpp deleted file mode 100644 index 796cfe2..0000000 --- a/src/wayland/windowmanager/windowset.cpp +++ /dev/null @@ -1,252 +0,0 @@ -#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 deleted file mode 100644 index 52d1c63..0000000 --- a/src/wayland/windowmanager/windowset.hpp +++ /dev/null @@ -1,85 +0,0 @@ -#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/wayland/windowmanager/workspace.cpp b/src/wayland/windowmanager/workspace.cpp new file mode 100644 index 0000000..07bf3da --- /dev/null +++ b/src/wayland/windowmanager/workspace.cpp @@ -0,0 +1,198 @@ +#include "workspace.hpp" + +#include +#include +#include +#include +#include + +#include "ext_workspace.hpp" + +namespace qs::wm::wayland { + +WorkspaceManager::WorkspaceManager() { + auto* impl = impl::WorkspaceManager::instance(); + + QObject::connect( + impl, + &impl::WorkspaceManager::serverCommit, + this, + &WorkspaceManager::onServerCommit + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::workspaceCreated, + this, + &WorkspaceManager::onWorkspaceCreated + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::workspaceDestroyed, + this, + &WorkspaceManager::onWorkspaceDestroyed + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::groupCreated, + this, + &WorkspaceManager::onGroupCreated + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::groupDestroyed, + this, + &WorkspaceManager::onGroupDestroyed + ); +} + +void WorkspaceManager::commit() { + qCDebug(impl::logWorkspace) << "Committing workspaces"; + impl::WorkspaceManager::instance()->commit(); +} + +void WorkspaceManager::onServerCommit() { + // Groups are created/destroyed around workspaces to avoid any nulls making it + // to the qml engine. + + for (auto* groupImpl: this->pendingGroupCreations) { + auto* group = new WlWorkspaceGroup(this, groupImpl); + this->groupsByImpl.insert(groupImpl, group); + this->mWorkspaceGroups.insertObject(group); + } + + for (auto* wsImpl: this->pendingWorkspaceCreations) { + auto* ws = new WlWorkspace(this, wsImpl); + this->workspaceByImpl.insert(wsImpl, ws); + this->mWorkspaces.insertObject(ws); + } + + for (auto* wsImpl: this->pendingWorkspaceDestructions) { + this->mWorkspaces.removeObject(this->workspaceByImpl.value(wsImpl)); + this->workspaceByImpl.remove(wsImpl); + } + + for (auto* groupImpl: this->pendingGroupDestructions) { + this->mWorkspaceGroups.removeObject(this->groupsByImpl.value(groupImpl)); + this->groupsByImpl.remove(groupImpl); + } + + for (auto* ws: this->mWorkspaces.valueList()) ws->commitImpl(); + for (auto* group: this->mWorkspaceGroups.valueList()) group->commitImpl(); + + this->pendingWorkspaceCreations.clear(); + this->pendingWorkspaceDestructions.clear(); + this->pendingGroupCreations.clear(); + this->pendingGroupDestructions.clear(); +} + +void WorkspaceManager::onWorkspaceCreated(impl::Workspace* workspace) { + this->pendingWorkspaceCreations.append(workspace); +} + +void WorkspaceManager::onWorkspaceDestroyed(impl::Workspace* workspace) { + if (!this->pendingWorkspaceCreations.removeOne(workspace)) { + this->pendingWorkspaceDestructions.append(workspace); + } +} + +void WorkspaceManager::onGroupCreated(impl::WorkspaceGroup* group) { + this->pendingGroupCreations.append(group); +} + +void WorkspaceManager::onGroupDestroyed(impl::WorkspaceGroup* group) { + if (!this->pendingGroupCreations.removeOne(group)) { + this->pendingGroupDestructions.append(group); + } +} + +WorkspaceManager* WorkspaceManager::instance() { + static auto* instance = new WorkspaceManager(); + return instance; +} + +WlWorkspace::WlWorkspace(WorkspaceManager* manager, impl::Workspace* impl) + : Workspace(manager) + , impl(impl) { + this->commitImpl(); +} + +void WlWorkspace::commitImpl() { + Qt::beginPropertyUpdateGroup(); + this->bId = this->impl->id; + this->bName = this->impl->name; + this->bActive = this->impl->active; + this->bShouldDisplay = !this->impl->hidden; + this->bUrgent = this->impl->urgent; + this->bCanActivate = this->impl->canActivate; + this->bCanDeactivate = this->impl->canDeactivate; + this->bCanSetGroup = this->impl->canAssign; + this->bGroup = this->manager()->groupsByImpl.value(this->impl->group); + Qt::endPropertyUpdateGroup(); +} + +void WlWorkspace::activate() { + if (!this->bCanActivate) { + qCritical(logWorkspace) << this << "cannot be activated"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling activate() for" << this; + this->impl->activate(); + WorkspaceManager::commit(); +} + +void WlWorkspace::deactivate() { + if (!this->bCanDeactivate) { + qCritical(logWorkspace) << this << "cannot be deactivated"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling deactivate() for" << this; + this->impl->deactivate(); + WorkspaceManager::commit(); +} + +void WlWorkspace::remove() { + if (!this->bCanRemove) { + qCritical(logWorkspace) << this << "cannot be removed"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling remove() for" << this; + this->impl->remove(); + WorkspaceManager::commit(); +} + +void WlWorkspace::setGroup(WorkspaceGroup* group) { + if (!this->bCanSetGroup) { + qCritical(logWorkspace) << this << "cannot be assigned to a group"; + return; + } + + if (!group) { + qCritical(logWorkspace) << "Cannot set a workspace's group to null"; + return; + } + + qCDebug(impl::logWorkspace) << "Assigning" << this << "to" << group; + // NOLINTNEXTLINE: A WorkspaceGroup will always be a WlWorkspaceGroup under wayland. + this->impl->assign(static_cast(group)->impl->object()); + WorkspaceManager::commit(); +} + +WlWorkspaceGroup::WlWorkspaceGroup(WorkspaceManager* manager, impl::WorkspaceGroup* impl) + : WorkspaceGroup(manager) + , impl(impl) { + this->commitImpl(); +} + +void WlWorkspaceGroup::commitImpl() { + // TODO: will not commit the correct screens if missing qt repr at commit time + this->bScreens = this->impl->screens.screens(); +} + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/workspace.hpp b/src/wayland/windowmanager/workspace.hpp new file mode 100644 index 0000000..82742a9 --- /dev/null +++ b/src/wayland/windowmanager/workspace.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include + +#include "../../core/model.hpp" +#include "../../windowmanager/workspace.hpp" +#include "ext_workspace.hpp" + +namespace qs::wm::wayland { +namespace impl = qs::wayland::workspace; + +class WlWorkspace; +class WlWorkspaceGroup; + +class WorkspaceManager: public QObject { + Q_OBJECT; + +public: + static WorkspaceManager* instance(); + + ObjectModel mWorkspaces {this}; + ObjectModel mWorkspaceGroups {this}; + + static void commit(); + +private slots: + void onServerCommit(); + void onWorkspaceCreated(impl::Workspace* workspace); + void onWorkspaceDestroyed(impl::Workspace* workspace); + void onGroupCreated(impl::WorkspaceGroup* group); + void onGroupDestroyed(impl::WorkspaceGroup* group); + +private: + WorkspaceManager(); + + QList pendingWorkspaceCreations; + QList pendingWorkspaceDestructions; + QHash workspaceByImpl; + + QList pendingGroupCreations; + QList pendingGroupDestructions; + QHash groupsByImpl; + + friend class WlWorkspace; +}; + +class WlWorkspace: public Workspace { +public: + WlWorkspace(WorkspaceManager* manager, impl::Workspace* impl); + + void commitImpl(); + + void activate() override; + void deactivate() override; + void remove() override; + void setGroup(WorkspaceGroup* group) override; + + [[nodiscard]] WorkspaceManager* manager() { + return static_cast(this->parent()); // NOLINT + } + +private: + impl::Workspace* impl = nullptr; +}; + +class WlWorkspaceGroup: public WorkspaceGroup { +public: + WlWorkspaceGroup(WorkspaceManager* manager, impl::WorkspaceGroup* impl); + + void commitImpl(); + + [[nodiscard]] WorkspaceManager* manager() { + return static_cast(this->parent()); // NOLINT + } + +private: + impl::WorkspaceGroup* impl = nullptr; + + friend class WlWorkspace; +}; + +} // namespace qs::wm::wayland diff --git a/src/windowmanager/CMakeLists.txt b/src/windowmanager/CMakeLists.txt index 3c032f4..365c43a 100644 --- a/src/windowmanager/CMakeLists.txt +++ b/src/windowmanager/CMakeLists.txt @@ -1,7 +1,7 @@ qt_add_library(quickshell-windowmanager STATIC - screenprojection.cpp windowmanager.cpp - windowset.cpp + workspace.cpp + workspacemodel.cpp ) qt_add_qml_module(quickshell-windowmanager @@ -14,7 +14,5 @@ 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) +target_link_libraries(quickshell PRIVATE quickshell-windowmanager) diff --git a/src/windowmanager/module.md b/src/windowmanager/module.md deleted file mode 100644 index 3480d60..0000000 --- a/src/windowmanager/module.md +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index c09e6f0..0000000 --- a/src/windowmanager/screenprojection.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#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 deleted file mode 100644 index 6b0f31e..0000000 --- a/src/windowmanager/screenprojection.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#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 deleted file mode 100644 index 4ebd7f2..0000000 --- a/src/windowmanager/test/manual/WorkspaceDelegate.qml +++ /dev/null @@ -1,86 +0,0 @@ -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 deleted file mode 100644 index d06036c..0000000 --- a/src/windowmanager/test/manual/screenproj.qml +++ /dev/null @@ -1,45 +0,0 @@ -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 index d6fdf05..b379b47 100644 --- a/src/windowmanager/test/manual/workspaces.qml +++ b/src/windowmanager/test/manual/workspaces.qml @@ -11,11 +11,11 @@ FloatingWindow { ColumnLayout { Repeater { - model: WindowManager.windowsetProjections + model: WindowManager.workspaceGroups WrapperRectangle { id: delegate - required property WindowsetProjection modelData + required property WorkspaceGroup modelData color: "slategray" margin: 5 @@ -25,7 +25,7 @@ FloatingWindow { Repeater { model: ScriptModel { - values: delegate.modelData.windowsets + values: [...WindowManager.workspaces.values].filter(w => w.group == delegate.modelData) } WorkspaceDelegate {} @@ -36,11 +36,77 @@ FloatingWindow { Repeater { model: ScriptModel { - values: WindowManager.windowsets.filter(w => w.projection == null) + values: WindowManager.workspaces.values.filter(w => w.group == null) } WorkspaceDelegate {} } } } + + component WorkspaceDelegate: WrapperRectangle { + id: delegate + required property Workspace modelData; + color: modelData.active ? "green" : "gray" + + ColumnLayout { + Label { text: delegate.modelData.toString() } + Label { text: `Id: ${delegate.modelData.id} Name: ${delegate.modelData.name}` } + + RowLayout { + Label { text: "Group:" } + ComboBox { + Layout.fillWidth: true + implicitContentWidthPolicy: ComboBox.WidestText + enabled: delegate.modelData.canSetGroup + model: [...WindowManager.workspaceGroups.values].map(w => w.toString()) + currentIndex: WindowManager.workspaceGroups.values.indexOf(delegate.modelData.group) + onActivated: i => delegate.modelData.setGroup(WindowManager.workspaceGroups.values[i]) + } + } + + RowLayout { + DisplayCheckBox { + text: "Active" + checked: delegate.modelData.active + } + + DisplayCheckBox { + text: "Urgent" + checked: delegate.modelData.urgent + } + + DisplayCheckBox { + text: "Should Display" + checked: delegate.modelData.shouldDisplay + } + } + + RowLayout { + Button { + text: "Activate" + enabled: delegate.modelData.canActivate + onClicked: delegate.modelData.activate() + } + + Button { + text: "Deactivate" + enabled: delegate.modelData.canDeactivate + onClicked: delegate.modelData.deactivate() + } + + Button { + text: "Remove" + enabled: delegate.modelData.canRemove + onClicked: delegate.modelData.remove() + } + } + } + } + + component DisplayCheckBox: CheckBox { + enabled: false + palette.disabled.text: parent.palette.active.text + palette.disabled.windowText: parent.palette.active.windowText + } } diff --git a/src/windowmanager/windowmanager.cpp b/src/windowmanager/windowmanager.cpp index 6b51db1..7c6a6cc 100644 --- a/src/windowmanager/windowmanager.cpp +++ b/src/windowmanager/windowmanager.cpp @@ -2,11 +2,6 @@ #include #include -#include - -#include "../core/qmlscreen.hpp" -#include "screenprojection.hpp" - namespace qs::wm { std::function WindowManager::provider; @@ -20,22 +15,4 @@ WindowManager* WindowManager::instance() { 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 index 054e485..03ebfc1 100644 --- a/src/windowmanager/windowmanager.hpp +++ b/src/windowmanager/windowmanager.hpp @@ -2,17 +2,12 @@ #include -#include -#include #include -#include #include -#include #include -#include "../core/qmlscreen.hpp" -#include "screenprojection.hpp" -#include "windowset.hpp" +#include "../core/model.hpp" +#include "workspace.hpp" namespace qs::wm { @@ -23,68 +18,32 @@ public: static void setProvider(std::function provider); static WindowManager* instance(); - Q_INVOKABLE ScreenProjection* screenProjection(QuickshellScreenInfo* screen); - - [[nodiscard]] QBindable> bindableWindowsets() const { - return &this->bWindowsets; + [[nodiscard]] virtual UntypedObjectModel* workspaces() const { + return UntypedObjectModel::emptyInstance(); } - [[nodiscard]] QBindable> bindableWindowsetProjections() const { - return &this->bWindowsetProjections; + [[nodiscard]] virtual UntypedObjectModel* workspaceGroups() const { + return UntypedObjectModel::emptyInstance(); } -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 + Q_PROPERTY(UntypedObjectModel* workspaces READ workspaces CONSTANT); + Q_PROPERTY(UntypedObjectModel* workspaceGroups READ workspaceGroups CONSTANT); 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 UntypedObjectModel* workspaces() { + return WindowManager::instance()->workspaces(); } - [[nodiscard]] static QBindable> bindableWindowsets() { - return WindowManager::instance()->bindableWindowsets(); - } - - [[nodiscard]] static QBindable> bindableWindowsetProjections() { - return WindowManager::instance()->bindableWindowsetProjections(); + [[nodiscard]] static UntypedObjectModel* workspaceGroups() { + return WindowManager::instance()->workspaceGroups(); } }; diff --git a/src/windowmanager/windowset.cpp b/src/windowmanager/windowset.cpp deleted file mode 100644 index 6231c40..0000000 --- a/src/windowmanager/windowset.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#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 deleted file mode 100644 index 51cbd9b..0000000 --- a/src/windowmanager/windowset.hpp +++ /dev/null @@ -1,175 +0,0 @@ -#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/windowmanager/workspace.cpp b/src/windowmanager/workspace.cpp new file mode 100644 index 0000000..472234e --- /dev/null +++ b/src/windowmanager/workspace.cpp @@ -0,0 +1,31 @@ +#include "workspace.hpp" + +#include +#include +#include + +#include "../core/qmlglobal.hpp" + +namespace qs::wm { + +Q_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.workspace", QtWarningMsg); + +void Workspace::activate() { qCCritical(logWorkspace) << this << "cannot be activated"; } +void Workspace::deactivate() { qCCritical(logWorkspace) << this << "cannot be deactivated"; } +void Workspace::remove() { qCCritical(logWorkspace) << this << "cannot be removed"; } + +void Workspace::setGroup(WorkspaceGroup* /*group*/) { + qCCritical(logWorkspace) << this << "cannot be assigned to a group"; +} + +void WorkspaceGroup::onScreensChanged() { + mCachedScreens.clear(); + + for (auto* screen: this->bScreens.value()) { + mCachedScreens.append(QuickshellTracked::instance()->screenInfo(screen)); + } + + emit this->screensChanged(); +} + +} // namespace qs::wm diff --git a/src/windowmanager/workspace.hpp b/src/windowmanager/workspace.hpp new file mode 100644 index 0000000..aa148a0 --- /dev/null +++ b/src/windowmanager/workspace.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QuickshellScreenInfo; + +namespace qs::wm { + +Q_DECLARE_LOGGING_CATEGORY(logWorkspace); + +class WorkspaceGroup; + +class Workspace: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + // clang-format off + // persistent id + Q_PROPERTY(QString id READ default BINDABLE bindableId NOTIFY idChanged); + Q_PROPERTY(QString name READ default BINDABLE bindableName NOTIFY nameChanged); + // currently visible + Q_PROPERTY(bool active READ default BINDABLE bindableActive NOTIFY activeChanged); + Q_PROPERTY(WorkspaceGroup* group READ default BINDABLE bindableGroup NOTIFY groupChanged); + // in workspace pickers + Q_PROPERTY(bool shouldDisplay READ default BINDABLE bindableShouldDisplay NOTIFY shouldDisplayChanged); + Q_PROPERTY(bool urgent READ default BINDABLE bindableUrgent NOTIFY urgentChanged); + Q_PROPERTY(bool canActivate READ default BINDABLE bindableCanActivate NOTIFY canActivateChanged); + Q_PROPERTY(bool canDeactivate READ default BINDABLE bindableCanDeactivate NOTIFY canDeactivateChanged); + Q_PROPERTY(bool canRemove READ default BINDABLE bindableCanRemove NOTIFY canRemoveChanged); + Q_PROPERTY(bool canSetGroup READ default BINDABLE bindableCanSetGroup NOTIFY canSetGroupChanged); + // clang-format on + +public: + explicit Workspace(QObject* parent): QObject(parent) {} + + Q_INVOKABLE virtual void activate(); + Q_INVOKABLE virtual void deactivate(); + Q_INVOKABLE virtual void remove(); + Q_INVOKABLE virtual void setGroup(WorkspaceGroup* group); + + [[nodiscard]] QBindable bindableId() const { return &this->bId; } + [[nodiscard]] QBindable bindableName() const { return &this->bName; } + [[nodiscard]] QBindable bindableActive() const { return &this->bActive; } + [[nodiscard]] QBindable bindableGroup() const { return &this->bGroup; } + [[nodiscard]] QBindable bindableShouldDisplay() const { return &this->bShouldDisplay; } + [[nodiscard]] QBindable bindableUrgent() const { return &this->bUrgent; } + [[nodiscard]] QBindable bindableCanActivate() const { return &this->bCanActivate; } + [[nodiscard]] QBindable bindableCanDeactivate() const { return &this->bCanDeactivate; } + [[nodiscard]] QBindable bindableCanRemove() const { return &this->bCanRemove; } + [[nodiscard]] QBindable bindableCanSetGroup() const { return &this->bCanSetGroup; } + +signals: + void idChanged(); + void nameChanged(); + void activeChanged(); + void groupChanged(); + void shouldDisplayChanged(); + void urgentChanged(); + void canActivateChanged(); + void canDeactivateChanged(); + void canRemoveChanged(); + void canSetGroupChanged(); + +protected: + Q_OBJECT_BINDABLE_PROPERTY(Workspace, QString, bId, &Workspace::idChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, QString, bName, &Workspace::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bActive, &Workspace::activeChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, WorkspaceGroup*, bGroup, &Workspace::groupChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bShouldDisplay, &Workspace::shouldDisplayChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bUrgent, &Workspace::urgentChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanActivate, &Workspace::canActivateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanDeactivate, &Workspace::canDeactivateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanRemove, &Workspace::canRemoveChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanSetGroup, &Workspace::canSetGroupChanged); + //Q_OBJECT_BINDABLE_PROPERTY(Workspace, qint32, bIndex); +}; + +class WorkspaceGroup: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + /// Screens the workspace group is present on. + /// + /// > [!WARNING] This is not a model. Use @@Quickshell.ScriptModel if you need it to + /// > behave like one. + Q_PROPERTY(QList screens READ screens NOTIFY screensChanged); + +public: + explicit WorkspaceGroup(QObject* parent): QObject(parent) {} + + [[nodiscard]] const QList& screens() const { return this->mCachedScreens; } + +signals: + void screensChanged(); + +private slots: + void onScreensChanged(); + +protected: + Q_OBJECT_BINDABLE_PROPERTY( + WorkspaceGroup, + QList, + bScreens, + &WorkspaceGroup::onScreensChanged + ); + +private: + QList mCachedScreens; +}; + +} // namespace qs::wm diff --git a/src/windowmanager/workspacemodel.cpp b/src/windowmanager/workspacemodel.cpp new file mode 100644 index 0000000..f6941fb --- /dev/null +++ b/src/windowmanager/workspacemodel.cpp @@ -0,0 +1 @@ +#include "workspacemodel.hpp" diff --git a/src/windowmanager/workspacemodel.hpp b/src/windowmanager/workspacemodel.hpp new file mode 100644 index 0000000..aeb855f --- /dev/null +++ b/src/windowmanager/workspacemodel.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace qs::windowsystem { + +class WorkspaceModel: public QObject { + Q_OBJECT; + QML_ELEMENT; + +public: + enum ConflictStrategy : quint8 { + KeepFirst = 0, + ShowDuplicates, + }; + Q_ENUM(ConflictStrategy); + +signals: + void fromChanged(); + void toChanged(); + void screensChanged(); + void conflictStrategyChanged(); + +private: + Q_OBJECT_BINDABLE_PROPERTY(WorkspaceModel, qint32, bFrom, &WorkspaceModel::fromChanged); + Q_OBJECT_BINDABLE_PROPERTY(WorkspaceModel, qint32, bTo, &WorkspaceModel::toChanged); + Q_OBJECT_BINDABLE_PROPERTY( + WorkspaceModel, + ConflictStrategy, + bConflictStrategy, + &WorkspaceModel::conflictStrategyChanged + ); +}; + +} // namespace qs::windowsystem diff --git a/src/x11/i3/ipc/CMakeLists.txt b/src/x11/i3/ipc/CMakeLists.txt index a073459..c228ae3 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 quickshell-core) +target_link_libraries(quickshell-i3-ipc PRIVATE Qt::Quick) 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 976167b..b765ebc 100644 --- a/src/x11/i3/ipc/connection.cpp +++ b/src/x11/i3/ipc/connection.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -14,7 +15,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -86,6 +89,9 @@ 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) { @@ -139,21 +145,34 @@ void I3Ipc::reconnectIPC() { } QVector I3Ipc::parseResponse() { - QVector events; + QVector> events; + const int magicLen = 6; - 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; + while (!this->liveEventSocketDs.atEnd()) { + this->liveEventSocketDs.startTransaction(); + this->liveEventSocketDs.startTransaction(); - if (magic.size() < 6 || strncmp(magic.data(), MAGIC.data(), 6) != 0) { + 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) { qCWarning(logI3Ipc) << "No magic sequence found in string."; this->reconnectIPC(); break; - } + }; if (I3IpcEvent::intToEvent(type) == EventCode::Unknown) { qCWarning(logI3Ipc) << "Received unknown event"; @@ -185,7 +204,6 @@ 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 7d03ecd..6100f7e 100644 --- a/src/x11/i3/ipc/connection.hpp +++ b/src/x11/i3/ipc/connection.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -8,8 +9,6 @@ #include #include -#include "../../../core/streamreader.hpp" - namespace qs::i3::ipc { constexpr std::string MAGIC = "i3-ipc"; @@ -93,7 +92,7 @@ protected: QVector> parseResponse(); QLocalSocket liveEventSocket; - StreamReader eventReader; + QDataStream liveEventSocketDs; QString mSocketPath; bool valid = false;