Compare commits

..

11 commits

Author SHA1 Message Date
outfoxxed
6705e2da77
wm: add WindowManager module with ext-workspace support 2026-03-16 01:08:26 -07:00
outfoxxed
9e8eecf2b8
core: log qt related environment variables in debuginfo 2026-03-15 21:13:35 -07:00
outfoxxed
1b2519d9f3
core: log gpu information in debuginfo 2026-03-14 02:31:47 -07:00
outfoxxed
1123d5ab4f
core: move crash/version debug info to one place 2026-03-14 02:31:28 -07:00
outfoxxed
4b77936c80
crash: allow overriding crash reporter url 2026-03-13 02:04:01 -07:00
outfoxxed
e32b909354
core: add disable env vars for file watcher and crash handler 2026-03-13 01:10:09 -07:00
outfoxxed
178c04b59c
docs: revise contribution policy and related files 2026-03-13 00:33:36 -07:00
outfoxxed
706d6de7b0
crash: unmask signals in coredump fork
Fixes the fork just sticking around and not dumping a core.
2026-03-12 04:02:24 -07:00
outfoxxed
9a9c605250
core: hash scanned files and don't trigger a reload if matching
Nix builds often trip QFileSystemWatcher, causing random reloads.
2026-03-11 21:52:13 -07:00
outfoxxed
bd62179277
all: retry incomplete socket reads
Fixes greetd and hyprland ipc sockets reads being incomplete and
breaking said integrations on slow machines.
2026-03-10 00:54:45 -07:00
-k
cf1a2aeb2d
wayland/toplevel: clear activeToplevel on deactivation 2026-03-09 19:37:15 -07:00
58 changed files with 1661 additions and 992 deletions

View file

@ -20,6 +20,7 @@ Checks: >
-cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-pro-type-reinterpret-cast,
-cppcoreguidelines-pro-type-vararg,
-cppcoreguidelines-pro-type-union-access,
-cppcoreguidelines-use-enum-class,
google-global-names-in-headers,
google-readability-casting,

View file

@ -15,7 +15,7 @@ Please make this descriptive enough to identify your specific package, for examp
- `Nixpkgs`
- `Fedora COPR (errornointernet/quickshell)`
Please leave at least symbol names attached to the binary for debugging purposes.
If you are forking quickshell, please change `CRASHREPORT_URL` to your own issue tracker.
### QML Module dir
Currently all QML modules are statically linked to quickshell, but this is where
@ -33,6 +33,7 @@ Quickshell has a set of base dependencies you will always need, names vary by di
- `cmake`
- `qt6base`
- `qt6declarative`
- `libdrm`
- `qtshadertools` (build-time)
- `spirv-tools` (build-time)
- `pkg-config` (build-time)
@ -67,7 +68,13 @@ Dependencies: `cpptrace`
Note: `-DVENDOR_CPPTRACE=ON` can be set to vendor cpptrace using FetchContent.
When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the package manager or fetched with FetchContent.
When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the
package manager or fetched with FetchContent.
*Please ensure binaries have usable symbols.* We do not necessarily need full debuginfo, but
leaving symbols in the binary is extremely helpful. You can check if symbols are useful
by sending a SIGSEGV to the process and ensuring symbols for the quickshell binary are present
in the trace.
### Jemalloc
We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused
@ -140,7 +147,6 @@ Enables streaming video from monitors and toplevel windows through various proto
To disable: `-DSCREENCOPY=OFF`
Dependencies:
- `libdrm`
- `libgbm`
- `vulkan-headers` (build-time)
@ -236,7 +242,7 @@ Only `ninja` builds are tested, but makefiles may work.
#### Configuring the build
```sh
$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here]
$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release [additional disable flags from above here]
```
Note that features you do not supply dependencies for MUST be disabled with their associated flags

View file

@ -9,6 +9,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(QS_BUILD_OPTIONS "")
# should be changed for forks
set(CRASHREPORT_URL "https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml" CACHE STRING "Bugreport URL")
function(boption VAR NAME DEFAULT)
cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "")

View file

@ -1,235 +1,40 @@
# Contributing / Development
Instructions for development setup and upstreaming patches.
# Contributing
If you just want to build or package quickshell see [BUILD.md](BUILD.md).
Thank you for taking the time to contribute.
To ensure nobody's time is wasted, please follow the rules below.
## Development
## Acceptable Code Contributions
Install the dependencies listed in [BUILD.md](BUILD.md).
You probably want all of them even if you don't use all of them
to ensure tests work correctly and avoid passing a bunch of configure
flags when you need to wipe the build directory.
- All changes submitted MUST be **fully understood by the submitter**. If you do not know why or how
your change works, do not submit it to be merged. You must be able to explain your reasoning
for every change.
Quickshell also uses `just` for common development command aliases.
- Changes MUST be submitted by a human who will be responsible for them. Changes submitted without
a human in the loop such as automated tooling and AI Agents are **strictly disallowed**. Accounts
responsible for such contribution attempts **will be banned**.
The dependencies are also available as a nix shell or nix flake which we recommend
using with nix-direnv.
- Changes MUST respect Quickshell's license and the license of any source works. Changes including
code from any other works must disclose the source of the code, explain why it was used, and
ensure the license is compatible.
Common aliases:
- `just configure [<debug|release> [extra cmake args]]` (note that you must specify debug/release to specify extra args)
- `just build` - runs the build, configuring if not configured already.
- `just run [args]` - runs quickshell with the given arguments
- `just clean` - clean up build artifacts. `just clean build` is somewhat common.
- Changes must follow the guidelines outlined in [HACKING.md](HACKING.md) for style and substance.
### Formatting
All contributions should be formatted similarly to what already exists.
Group related functionality together.
- Changes must stand on their own as a unit. Do not make multiple unrelated changes in one PR.
Changes depending on prior merges should be marked as a draft.
Run the formatter using `just fmt`.
If the results look stupid, fix the clang-format file if possible,
or disable clang-format in the affected area
using `// clang-format off` and `// clang-format on`.
## Acceptable Non-code Contributions
#### Style preferences not caught by clang-format
These are flexible. You can ignore them if it looks or works better to
for one reason or another.
- Bug and crash reports. You must follow the instructions in the issue templates and provide the
information requested.
Use `auto` if the type of a variable can be deduced automatically, instead of
redeclaring the returned value's type. Additionally, auto should be used when a
constructor takes arguments.
- Feature requests can be made via Issues. Please check to ensure nobody else has requested the same feature.
```cpp
auto x = <expr>; // ok
auto x = QString::number(3); // ok
QString x; // ok
QString x = "foo"; // ok
auto x = QString("foo"); // ok
- Do not make insubstantial or pointless changes.
auto x = QString(); // avoid
QString x(); // avoid
QString x("foo"); // avoid
```
- Changes to project rules / policy / governance will not be entertained, except from significant
long-term contributors. These changes should not be addressed through contribution channels.
Put newlines around logical units of code, and after closing braces. If the
most reasonable logical unit of code takes only a single line, it should be
merged into the next single line logical unit if applicable.
```cpp
// multiple units
auto x = <expr>; // unit 1
auto y = <expr>; // unit 2
## Merge timelines
auto x = <expr>; // unit 1
emit this->y(); // unit 2
auto x1 = <expr>; // unit 1
auto x2 = <expr>; // unit 1
auto x3 = <expr>; // unit 1
auto y1 = <expr>; // unit 2
auto y2 = <expr>; // unit 2
auto y3 = <expr>; // unit 2
// one unit
auto x = <expr>;
if (x...) {
// ...
}
// if more than one variable needs to be used then add a newline
auto x = <expr>;
auto y = <expr>;
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<T> bindableFoo() { return &this->bFoo; }
[[nodiscard]] T foo() const { return this->foo; }
void setFoo();
[[nodiscard]] T bar() const { return this->foo; }
void setBar();
signals:
// Signals that are not property change related go first.
// Property change signals go in property definition order.
void asd();
void asd2();
void fooChanged();
void barChanged();
public slots:
// generally Q_INVOKABLEs are preferred to public slots.
void slot();
private slots:
// ...
private:
// statics, then functions, then fields
static const foo BAR;
static void foo();
void foo();
void bar();
// property related members are prefixed with `m`.
QString mFoo;
QString bar;
// Bindables go last and should be prefixed with `b`.
Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged);
};
```
### Linter
All contributions should pass the linter.
Note that running the linter requires disabling precompiled
headers and including the test codepaths:
```sh
$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
$ just lint-changed
```
If the linter is complaining about something that you think it should not,
please disable the lint in your MR and explain your reasoning if it isn't obvious.
### Tests
If you feel like the feature you are working on is very complex or likely to break,
please write some tests. We will ask you to directly if you send in an MR for an
overly complex or breakable feature.
At least all tests that passed before your changes should still be passing
by the time your contribution is ready.
You can run the tests using `just test` but you must enable them first
using `-DBUILD_TESTING=ON`.
### Documentation
Most of quickshell's documentation is automatically generated from the source code.
You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser
cannot handle random line breaks and will usually require you to disable clang-format if the
lines are too long.
Before submitting an MR, if adding new features please make sure the documentation is generated
reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo.
Doc comments take the form `///` or `///!` (summary) and work with markdown.
You can reference other types using the `@@[Module.][Type.][member]` shorthand
where all parts are optional. If module or type are not specified they will
be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`.
Look at existing code for how it works.
Quickshell modules additionally have a `module.md` file which contains a summary, description,
and list of headers to scan for documentation.
## Contributing
### Commits
Please structure your commit messages as `scope[!]: commit` where
the scope is something like `core` or `service/mpris`. (pick what has been
used historically or what makes sense if new). Add `!` for changes that break
existing APIs or functionality.
Commit descriptions should contain a summary of the changes if they are not
sufficiently addressed in the commit message.
Please squash/rebase additions or edits to previous changes and follow the
commit style to keep the history easily searchable at a glance.
Depending on the change, it is often reasonable to squash it into just
a single commit. (If you do not follow this we will squash your changes
for you.)
### Sending patches
You may contribute by submitting a pull request on github, asking for
an account on our git server, or emailing patches / git bundles
directly to `outfoxxed@outfoxxed.me`.
### Getting help
If you're getting stuck, you can come talk to us in the
[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me)
for help on implementation, conventions, etc.
Feel free to ask for advice early in your implementation if you are
unsure.
We handle work for the most part on a push basis. If your PR has been ignored for a while
and is still relevant please bump it.

226
HACKING.md Normal file
View file

@ -0,0 +1,226 @@
## Development
Install the dependencies listed in [BUILD.md](BUILD.md).
You probably want all of them even if you don't use all of them
to ensure tests work correctly and avoid passing a bunch of configure
flags when you need to wipe the build directory.
The dependencies are also available as a nix shell or nix flake which we recommend
using with nix-direnv.
Quickshell uses `just` for common development command aliases.
Common aliases:
- `just configure [<debug|release> [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 = <expr>; // 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 = <expr>; // unit 1
auto y = <expr>; // unit 2
auto x = <expr>; // unit 1
emit this->y(); // unit 2
auto x1 = <expr>; // unit 1
auto x2 = <expr>; // unit 1
auto x3 = <expr>; // unit 1
auto y1 = <expr>; // unit 2
auto y2 = <expr>; // unit 2
auto y3 = <expr>; // unit 2
// one unit
auto x = <expr>;
if (x...) {
// ...
}
// if more than one variable needs to be used then add a newline
auto x = <expr>;
auto y = <expr>;
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<T> 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. `<qwindow.h>` over `<QWindow>`.
### 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.

View file

@ -7,7 +7,9 @@ This repo is hosted at:
- https://github.com/quickshell-mirror/quickshell
# Contributing / Development
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
- [HACKING.md](HACKING.md) - Development instructions and policy.
- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution policy.
- [BUILD.md](BUILD.md) - Packaging and build instructions.
#### License

View file

@ -26,6 +26,7 @@ set shell id.
- Added Quickshell version checking and version gated preprocessing.
- Added a way to detect if an icon is from the system icon theme or not.
- Added vulkan support to screencopy.
- Added generic WindowManager interface implementing ext-workspace.
## Other Changes
@ -33,6 +34,10 @@ set shell id.
- IPC operations filter available instances to the current display connection by default.
- PwNodeLinkTracker ignores sound level monitoring programs.
- Replaced breakpad with cpptrace.
- Reloads are prevented if no file content has changed.
- Added `QS_DISABLE_FILE_WATCHER` environment variable to disable file watching.
- Added `QS_DISABLE_CRASH_HANDLER` environment variable to disable crash handling.
- Added `QS_CRASHREPORT_URL` environment variable to allow overriding the crash reporter link.
## Bug Fixes
@ -50,7 +55,9 @@ set shell id.
- Fixed ClippingRectangle related crashes.
- Fixed crashes when monitors are unplugged.
- Fixed crashes when default pipewire devices are lost.
- Fixed ToplevelManager not clearing activeToplevel on deactivation.
- Desktop action order is now preserved.
- Fixed partial socket reads in greetd and hyprland on slow machines.
## Packaging Changes
@ -58,3 +65,4 @@ set shell id.
- `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support).
- `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore.
- `DISTRIBUTOR_DEBUGINFO_AVAILABLE` was removed as it is no longer important without breakpad.
- `libdrm` is now unconditionally required as a direct dependency.

View file

@ -76,6 +76,7 @@
buildInputs = [
qt6.qtbase
qt6.qtdeclarative
libdrm
cli11
]
++ lib.optional withQtSvg qt6.qtsvg
@ -88,7 +89,7 @@
++ lib.optional withJemalloc jemalloc
++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
++ lib.optionals withWayland [ wayland wayland-protocols ]
++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm vulkan-headers ]
++ lib.optionals (withWayland && libgbm != null) [ libgbm vulkan-headers ]
++ lib.optional withX11 libxcb
++ lib.optional withPam pam
++ lib.optional withPipewire pipewire

View file

@ -13,4 +13,5 @@
#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)"
#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@"
#define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@"
#define CRASHREPORT_URL "@CRASHREPORT_URL@"
// NOLINTEND

View file

@ -1,3 +1,4 @@
pkg_check_modules(libdrm REQUIRED IMPORTED_TARGET libdrm)
qt_add_library(quickshell-core STATIC
plugin.cpp
shell.cpp
@ -40,6 +41,8 @@ qt_add_library(quickshell-core STATIC
scriptmodel.cpp
colorquantizer.cpp
toolsupport.cpp
streamreader.cpp
debuginfo.cpp
)
qt_add_qml_module(quickshell-core
@ -52,7 +55,7 @@ qt_add_qml_module(quickshell-core
install_qml_module(quickshell-core)
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build)
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build PkgConfig::libdrm)
qs_module_pch(quickshell-core SET large)

175
src/core/debuginfo.cpp Normal file
View file

@ -0,0 +1,175 @@
#include "debuginfo.hpp"
#include <array>
#include <cstring>
#include <string_view>
#include <fcntl.h>
#include <qconfig.h>
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qfile.h>
#include <qfloat16.h>
#include <qhashfunctions.h>
#include <qscopeguard.h>
#include <qtversion.h>
#include <unistd.h>
#include <xf86drm.h>
#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 "<failed to open device node>";
auto fdGuard = qScopeGuard([&] { close(fd); });
auto* ver = drmGetVersion(fd);
if (!ver) return "<drmGetVersion failed>";
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<std::string_view, 5> {
"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

14
src/core/debuginfo.hpp Normal file
View file

@ -0,0 +1,14 @@
#pragma once
#include <qcontainerfwd.h>
namespace qs::debuginfo {
QString qsVersion();
QString qtVersion();
QString gpuInfo();
QString systemInfo();
QString envInfo();
QString combinedInfo();
} // namespace qs::debuginfo

View file

@ -209,6 +209,8 @@ bool EngineGeneration::setExtraWatchedFiles(const QVector<QString>& files) {
for (const auto& file: files) {
if (!this->scanner.scannedFiles.contains(file)) {
this->extraWatchedFiles.append(file);
QByteArray data;
this->scanner.readAndHashFile(file, data);
}
}
@ -229,6 +231,11 @@ void EngineGeneration::onFileChanged(const QString& name) {
auto fileInfo = QFileInfo(name);
if (fileInfo.isFile() && fileInfo.size() == 0) return;
if (!this->scanner.hasFileContentChanged(name)) {
qCDebug(logQmlScanner) << "Ignoring file change with unchanged content:" << name;
return;
}
emit this->filesChanged();
}
}
@ -237,6 +244,11 @@ void EngineGeneration::onDirectoryChanged() {
// try to find any files that were just deleted from a replace operation
for (auto& file: this->deletedWatchedFiles) {
if (QFileInfo(file).exists()) {
if (!this->scanner.hasFileContentChanged(file)) {
qCDebug(logQmlScanner) << "Ignoring restored file with unchanged content:" << file;
continue;
}
emit this->filesChanged();
break;
}

View file

@ -60,7 +60,9 @@ void QuickshellSettings::setWorkingDirectory(QString workingDirectory) { // NOLI
emit this->workingDirectoryChanged();
}
bool QuickshellSettings::watchFiles() const { return this->mWatchFiles; }
bool QuickshellSettings::watchFiles() const {
return this->mWatchFiles && qEnvironmentVariableIsEmpty("QS_DISABLE_FILE_WATCHER");
}
void QuickshellSettings::setWatchFiles(bool watchFiles) {
if (watchFiles == this->mWatchFiles) return;

View file

@ -3,6 +3,7 @@
#include <utility>
#include <qcontainerfwd.h>
#include <qcryptographichash.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qjsengine.h>
@ -21,6 +22,25 @@
QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg);
bool QmlScanner::readAndHashFile(const QString& path, QByteArray& data) {
auto file = QFile(path);
if (!file.open(QFile::ReadOnly)) return false;
data = file.readAll();
this->fileHashes.insert(path, QCryptographicHash::hash(data, QCryptographicHash::Md5));
return true;
}
bool QmlScanner::hasFileContentChanged(const QString& path) const {
auto it = this->fileHashes.constFind(path);
if (it == this->fileHashes.constEnd()) return true;
auto file = QFile(path);
if (!file.open(QFile::ReadOnly)) return true;
auto newHash = QCryptographicHash::hash(file.readAll(), QCryptographicHash::Md5);
return newHash != it.value();
}
void QmlScanner::scanDir(const QDir& dir) {
if (this->scannedDirs.contains(dir)) return;
this->scannedDirs.push_back(dir);
@ -109,13 +129,13 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna
qCDebug(logQmlScanner) << "Scanning qml file" << path;
auto file = QFile(path);
if (!file.open(QFile::ReadOnly | QFile::Text)) {
QByteArray fileData;
if (!this->readAndHashFile(path, fileData)) {
qCWarning(logQmlScanner) << "Failed to open file" << path;
return false;
}
auto stream = QTextStream(&file);
auto stream = QTextStream(&fileData);
auto imports = QVector<QString>();
bool inHeader = true;
@ -219,8 +239,6 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna
postError("unclosed preprocessor if block");
}
file.close();
if (isOverridden) {
this->fileIntercepts.insert(path, overrideText);
}
@ -257,8 +275,11 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna
continue;
}
if (import.endsWith(".js")) this->scannedFiles.push_back(cpath);
else this->scanDir(cpath);
if (import.endsWith(".js")) {
this->scannedFiles.push_back(cpath);
QByteArray jsData;
this->readAndHashFile(cpath, jsData);
} else this->scanDir(cpath);
}
return true;
@ -273,14 +294,12 @@ void QmlScanner::scanQmlRoot(const QString& path) {
bool QmlScanner::scanQmlJson(const QString& path) {
qCDebug(logQmlScanner) << "Scanning qml.json file" << path;
auto file = QFile(path);
if (!file.open(QFile::ReadOnly | QFile::Text)) {
QByteArray data;
if (!this->readAndHashFile(path, data)) {
qCWarning(logQmlScanner) << "Failed to open file" << path;
return false;
}
auto data = file.readAll();
// Importing this makes CI builds fail for some reason.
QJsonParseError error; // NOLINT (misc-include-cleaner)
auto json = QJsonDocument::fromJson(data, &error);

View file

@ -1,5 +1,6 @@
#pragma once
#include <qbytearray.h>
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qhash.h>
@ -21,6 +22,7 @@ public:
QVector<QDir> scannedDirs;
QVector<QString> scannedFiles;
QHash<QString, QByteArray> fileHashes;
QHash<QString, QString> fileIntercepts;
struct ScanError {
@ -31,6 +33,9 @@ public:
QVector<ScanError> scanErrors;
bool readAndHashFile(const QString& path, QByteArray& data);
[[nodiscard]] bool hasFileContentChanged(const QString& path) const;
private:
QDir rootPath;

98
src/core/streamreader.cpp Normal file
View file

@ -0,0 +1,98 @@
#include "streamreader.hpp"
#include <cstring>
#include <qbytearray.h>
#include <qiodevice.h>
#include <qtypes.h>
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<char*>(&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;
}

26
src/core/streamreader.hpp Normal file
View file

@ -0,0 +1,26 @@
#pragma once
#include <qbytearray.h>
#include <qiodevice.h>
#include <qtypes.h>
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;
};

View file

@ -81,6 +81,11 @@ void signalHandler(
auto coredumpPid = fork();
if (coredumpPid == 0) {
// NOLINTBEGIN (misc-include-cleaner)
sigset_t set;
sigfillset(&set);
sigprocmask(SIG_UNBLOCK, &set, nullptr);
// NOLINTEND
raise(sig);
_exit(-1);
}

View file

@ -5,6 +5,7 @@
#include <qapplication.h>
#include <qboxlayout.h>
#include <qconfig.h>
#include <qcontainerfwd.h>
#include <qdesktopservices.h>
#include <qfont.h>
#include <qfontinfo.h>
@ -12,11 +13,22 @@
#include <qnamespace.h>
#include <qobject.h>
#include <qpushbutton.h>
#include <qtenvironmentvariables.h>
#include <qtversion.h>
#include <qwidget.h>
#include "build.hpp"
namespace {
QString crashreportUrl() {
if (auto url = qEnvironmentVariable("QS_CRASHREPORT_URL"); !url.isEmpty()) {
return url;
}
return CRASHREPORT_URL;
}
} // namespace
class ReportLabel: public QWidget {
public:
ReportLabel(const QString& label, const QString& content, QWidget* parent): QWidget(parent) {
@ -67,22 +79,16 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid)
if (qtVersionMatches) {
mainLayout->addWidget(
new QLabel("Please open a bug report for this issue via github or email.")
new QLabel("Please open a bug report for this issue on the issue tracker.")
);
} else {
mainLayout->addWidget(new QLabel(
"Please rebuild Quickshell against the current Qt version.\n"
"If this does not solve the problem, please open a bug report via github or email."
"If this does not solve the problem, please open a bug report on the issue tracker."
));
}
mainLayout->addWidget(new ReportLabel(
"Github:",
"https://github.com/quickshell-mirror/quickshell/issues/new?template=crash2.yml",
this
));
mainLayout->addWidget(new ReportLabel("Email:", "quickshell-bugs@outfoxxed.me", this));
mainLayout->addWidget(new ReportLabel("Tracker:", crashreportUrl(), this));
auto* buttons = new QWidget(this);
buttons->setMinimumWidth(900);
@ -112,10 +118,5 @@ void CrashReporterGui::openFolder() {
QDesktopServices::openUrl(QUrl::fromLocalFile(this->reportFolder));
}
void CrashReporterGui::openReportUrl() {
QDesktopServices::openUrl(
QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml")
);
}
void CrashReporterGui::openReportUrl() { QDesktopServices::openUrl(QUrl(crashreportUrl())); }
void CrashReporterGui::cancel() { QApplication::quit(); }

View file

@ -6,7 +6,6 @@
#include <cpptrace/basic.hpp>
#include <cpptrace/formatting.hpp>
#include <qapplication.h>
#include <qconfig.h>
#include <qcoreapplication.h>
#include <qdatastream.h>
#include <qdir.h>
@ -15,19 +14,18 @@
#include <qloggingcategory.h>
#include <qtenvironmentvariables.h>
#include <qtextstream.h>
#include <qtversion.h>
#include <qtypes.h>
#include <sys/sendfile.h>
#include <sys/types.h>
#include <unistd.h>
#include "../core/debuginfo.hpp"
#include "../core/instanceinfo.hpp"
#include "../core/logcat.hpp"
#include "../core/logging.hpp"
#include "../core/logging_p.hpp"
#include "../core/paths.hpp"
#include "../core/ringbuf.hpp"
#include "build.hpp"
#include "interface.hpp"
namespace {
@ -171,41 +169,15 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) {
qCCritical(logCrashReporter) << "Failed to open crash info file for writing.";
} else {
auto stream = QTextStream(&extraInfoFile);
stream << "===== Build Information =====\n";
stream << "Git Revision: " << GIT_REVISION << '\n';
stream << "Buildtime Qt Version: " << QT_VERSION_STR << "\n";
stream << "Build Type: " << BUILD_TYPE << '\n';
stream << "Compiler: " << COMPILER << '\n';
stream << "Complie Flags: " << COMPILE_FLAGS << "\n\n";
stream << "Build configuration:\n" << BUILD_CONFIGURATION << "\n";
stream << qs::debuginfo::combinedInfo();
stream << "\n===== Runtime Information =====\n";
stream << "Runtime Qt Version: " << qVersion() << '\n';
stream << "\n===== Instance Information =====\n";
stream << "Signal: " << strsignal(crashSignal) << " (" << crashSignal << ")\n"; // NOLINT
stream << "Crashed process ID: " << crashProc << '\n';
stream << "Run ID: " << instance.instanceId << '\n';
stream << "Shell ID: " << instance.shellId << '\n';
stream << "Config Path: " << instance.configPath << '\n';
stream << "\n===== System Information =====\n\n";
stream << "/etc/os-release:";
auto osReleaseFile = QFile("/etc/os-release");
if (osReleaseFile.open(QFile::ReadOnly)) {
stream << '\n' << osReleaseFile.readAll() << '\n';
osReleaseFile.close();
} else {
stream << "FAILED TO OPEN\n";
}
stream << "/etc/lsb-release:";
auto lsbReleaseFile = QFile("/etc/lsb-release");
if (lsbReleaseFile.open(QFile::ReadOnly)) {
stream << '\n' << lsbReleaseFile.readAll();
lsbReleaseFile.close();
} else {
stream << "FAILED TO OPEN\n";
}
stream << "\n===== Stacktrace =====\n";
if (stacktrace.empty()) {
stream << "(no trace available)\n";

View file

@ -25,12 +25,12 @@
#include <qtversion.h>
#include <unistd.h>
#include "../core/debuginfo.hpp"
#include "../core/instanceinfo.hpp"
#include "../core/logging.hpp"
#include "../core/paths.hpp"
#include "../io/ipccomm.hpp"
#include "../ipc/ipc.hpp"
#include "build.hpp"
#include "launch_p.hpp"
namespace qs::launch {
@ -519,20 +519,10 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
}
if (state.misc.printVersion) {
qCInfo(logBare).noquote().nospace() << "quickshell " << QS_VERSION << ", revision "
<< GIT_REVISION << ", distributed by: " << DISTRIBUTOR;
if (state.log.verbosity > 1) {
qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR;
qCInfo(logBare).noquote() << "Runtime Qt Version:" << qVersion();
qCInfo(logBare).noquote() << "Compiler:" << COMPILER;
qCInfo(logBare).noquote() << "Compile Flags:" << COMPILE_FLAGS;
}
if (state.log.verbosity > 0) {
qCInfo(logBare).noquote() << "\nBuild Type:" << BUILD_TYPE;
qCInfo(logBare).noquote() << "Build configuration:";
qCInfo(logBare).noquote().nospace() << BUILD_CONFIGURATION;
if (state.log.verbosity == 0) {
qCInfo(logBare).noquote() << "Quickshell" << qs::debuginfo::qsVersion();
} else {
qCInfo(logBare).noquote() << qs::debuginfo::combinedInfo();
}
} else if (*state.subcommand.log) {
return readLogFile(state);

View file

@ -138,9 +138,11 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio
};
#if CRASH_HANDLER
crash::CrashHandler::init();
if (qEnvironmentVariableIsSet("QS_DISABLE_CRASH_HANDLER")) {
qInfo() << "Crash handling disabled.";
} else {
crash::CrashHandler::init();
{
auto* log = LogManager::instance();
crash::CrashHandler::setRelaunchInfo({
.instance = InstanceInfo::CURRENT,

View file

@ -12,7 +12,7 @@ qt_add_qml_module(quickshell-service-greetd
install_qml_module(quickshell-service-greetd)
# can't be Qt::Qml because generation.hpp pulls in gui types
target_link_libraries(quickshell-service-greetd PRIVATE Qt::Quick)
target_link_libraries(quickshell-service-greetd PRIVATE Qt::Quick quickshell-core)
qs_module_pch(quickshell-service-greetd)

View file

@ -145,6 +145,7 @@ void GreetdConnection::setInactive() {
QString GreetdConnection::user() const { return this->mUser; }
void GreetdConnection::onSocketConnected() {
this->reader.setDevice(&this->socket);
qCDebug(logGreetd) << "Connected to greetd socket.";
if (this->mTargetActive) {
@ -160,82 +161,84 @@ void GreetdConnection::onSocketError(QLocalSocket::LocalSocketError error) {
}
void GreetdConnection::onSocketReady() {
qint32 length = 0;
while (true) {
this->reader.startTransaction();
auto length = this->reader.readI32();
auto text = this->reader.readBytes(length);
if (!this->reader.commitTransaction()) return;
this->socket.read(reinterpret_cast<char*>(&length), sizeof(qint32));
auto json = QJsonDocument::fromJson(text).object();
auto type = json.value("type").toString();
auto text = this->socket.read(length);
auto json = QJsonDocument::fromJson(text).object();
auto type = json.value("type").toString();
qCDebug(logGreetd).noquote() << "Received greetd response:" << text;
qCDebug(logGreetd).noquote() << "Received greetd response:" << text;
if (type == "success") {
switch (this->mState) {
case GreetdState::Authenticating:
qCDebug(logGreetd) << "Authentication complete.";
this->mState = GreetdState::ReadyToLaunch;
emit this->stateChanged();
emit this->readyToLaunch();
break;
case GreetdState::Launching:
qCDebug(logGreetd) << "Target session set successfully.";
this->mState = GreetdState::Launched;
emit this->stateChanged();
emit this->launched();
if (type == "success") {
switch (this->mState) {
case GreetdState::Authenticating:
qCDebug(logGreetd) << "Authentication complete.";
this->mState = GreetdState::ReadyToLaunch;
emit this->stateChanged();
emit this->readyToLaunch();
break;
case GreetdState::Launching:
qCDebug(logGreetd) << "Target session set successfully.";
this->mState = GreetdState::Launched;
emit this->stateChanged();
emit this->launched();
if (this->mExitAfterLaunch) {
qCDebug(logGreetd) << "Quitting.";
EngineGeneration::currentGeneration()->quit();
}
if (this->mExitAfterLaunch) {
qCDebug(logGreetd) << "Quitting.";
EngineGeneration::currentGeneration()->quit();
break;
default: goto unexpected;
}
} else if (type == "error") {
auto errorType = json.value("error_type").toString();
auto desc = json.value("description").toString();
// Special case this error in case a session was already running.
// This cancels and restarts the session.
if (errorType == "error" && desc == "a session is already being configured") {
qCDebug(
logGreetd
) << "A session was already in progress, cancelling it and starting a new one.";
this->setActive(false);
this->setActive(true);
return;
}
break;
default: goto unexpected;
}
} else if (type == "error") {
auto errorType = json.value("error_type").toString();
auto desc = json.value("description").toString();
if (errorType == "auth_error") {
emit this->authFailure(desc);
this->setActive(false);
} else if (errorType == "error") {
qCWarning(logGreetd) << "Greetd error occurred" << desc;
emit this->error(desc);
} else goto unexpected;
// Special case this error in case a session was already running.
// This cancels and restarts the session.
if (errorType == "error" && desc == "a session is already being configured") {
qCDebug(
logGreetd
) << "A session was already in progress, cancelling it and starting a new one.";
this->setActive(false);
this->setActive(true);
return;
}
// errors terminate the session
this->setInactive();
} else if (type == "auth_message") {
auto message = json.value("auth_message").toString();
auto type = json.value("auth_message_type").toString();
auto error = type == "error";
auto responseRequired = type == "visible" || type == "secret";
auto echoResponse = type != "secret";
if (errorType == "auth_error") {
emit this->authFailure(desc);
this->setActive(false);
} else if (errorType == "error") {
qCWarning(logGreetd) << "Greetd error occurred" << desc;
emit this->error(desc);
this->mResponseRequired = responseRequired;
emit this->authMessage(message, error, responseRequired, echoResponse);
if (!responseRequired) {
this->sendRequest({{"type", "post_auth_message_response"}});
}
} else goto unexpected;
// errors terminate the session
this->setInactive();
} else if (type == "auth_message") {
auto message = json.value("auth_message").toString();
auto type = json.value("auth_message_type").toString();
auto error = type == "error";
auto responseRequired = type == "visible" || type == "secret";
auto echoResponse = type != "secret";
this->mResponseRequired = responseRequired;
emit this->authMessage(message, error, responseRequired, echoResponse);
if (!responseRequired) {
this->sendRequest({{"type", "post_auth_message_response"}});
}
} else goto unexpected;
return;
unexpected:
qCCritical(logGreetd) << "Received unexpected greetd response" << text;
this->setActive(false);
continue;
unexpected:
qCCritical(logGreetd) << "Received unexpected greetd response" << text;
this->setActive(false);
}
}
void GreetdConnection::sendRequest(const QJsonObject& json) {

View file

@ -8,6 +8,8 @@
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../core/streamreader.hpp"
///! State of the Greetd connection.
/// See @@Greetd.state.
class GreetdState: public QObject {
@ -74,4 +76,5 @@ private:
bool mResponseRequired = false;
QString mUser;
QLocalSocket socket;
StreamReader reader;
};

View file

@ -15,7 +15,7 @@ qs_add_module_deps_light(quickshell-hyprland-ipc Quickshell)
install_qml_module(quickshell-hyprland-ipc)
target_link_libraries(quickshell-hyprland-ipc PRIVATE Qt::Quick)
target_link_libraries(quickshell-hyprland-ipc PRIVATE Qt::Quick quickshell-core)
if (WAYLAND_TOPLEVEL_MANAGEMENT)
target_sources(quickshell-hyprland-ipc PRIVATE

View file

@ -93,6 +93,7 @@ void HyprlandIpc::eventSocketError(QLocalSocket::LocalSocketError error) const {
void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) {
if (state == QLocalSocket::ConnectedState) {
this->eventReader.setDevice(&this->eventSocket);
qCInfo(logHyprlandIpc) << "Hyprland event socket connected.";
emit this->connected();
} else if (state == QLocalSocket::UnconnectedState && this->valid) {
@ -104,11 +105,11 @@ void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state)
void HyprlandIpc::eventSocketReady() {
while (true) {
auto rawEvent = this->eventSocket.readLine();
if (rawEvent.isEmpty()) break;
this->eventReader.startTransaction();
auto rawEvent = this->eventReader.readUntil('\n');
if (!this->eventReader.commitTransaction()) return;
// remove trailing \n
rawEvent.truncate(rawEvent.length() - 1);
rawEvent.chop(1); // remove trailing \n
auto splitIdx = rawEvent.indexOf(">>");
auto event = QByteArrayView(rawEvent.data(), splitIdx);
auto data = QByteArrayView(

View file

@ -14,6 +14,7 @@
#include "../../../core/model.hpp"
#include "../../../core/qmlscreen.hpp"
#include "../../../core/streamreader.hpp"
#include "../../../wayland/toplevel_management/handle.hpp"
namespace qs::hyprland::ipc {
@ -139,6 +140,7 @@ private:
static bool compareWorkspaces(HyprlandWorkspace* a, HyprlandWorkspace* b);
QLocalSocket eventSocket;
StreamReader eventReader;
QString mRequestSocketPath;
QString mEventSocketPath;
bool valid = false;

View file

@ -161,7 +161,11 @@ void ToplevelManager::onToplevelReady(impl::ToplevelHandle* handle) {
void ToplevelManager::onToplevelActiveChanged() {
auto* toplevel = qobject_cast<Toplevel*>(this->sender());
if (toplevel->activated()) this->setActiveToplevel(toplevel);
if (toplevel->activated()) {
this->setActiveToplevel(toplevel);
} else if (toplevel == this->mActiveToplevel) {
this->setActiveToplevel(nullptr);
}
}
void ToplevelManager::onToplevelClosed() {

View file

@ -1,20 +1,19 @@
qt_add_library(quickshell-wayland-windowsystem STATIC
windowmanager.cpp
workspace.cpp
windowset.cpp
ext_workspace.cpp
)
add_library(quickshell-wayland-windowsystem-init OBJECT init.cpp)
target_link_libraries(quickshell-wayland-windowsystem-init PRIVATE Qt::Quick)
#wl_proto(wlp-ext-foreign-toplevel ext-foreign-toplevel-list-v1 "${WAYLAND_PROTOCOLS}/staging/ext-foreign-toplevel-list")
wl_proto(wlp-ext-workspace ext-workspace-v1 "${WAYLAND_PROTOCOLS}/staging/ext-workspace")
target_link_libraries(quickshell-wayland-windowsystem PRIVATE
Qt::WaylandClient Qt::WaylandClientPrivate wayland-client
Qt::Quick # for pch? potentially, check w/ gcc
wlp-ext-foreign-toplevel wlp-ext-workspace
Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client
wlp-ext-workspace
)
qs_pch(quickshell-wayland-windowsystem SET large)
target_link_libraries(quickshell PRIVATE quickshell-wayland-windowsystem quickshell-wayland-windowsystem-init)

View file

@ -1,15 +1,20 @@
#include "ext_workspace.hpp"
#include <qcontainerfwd.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwayland-ext-workspace-v1.h>
#include <qwaylandclientextension.h>
#include <wayland-ext-workspace-v1-client-protocol.h>
#include <wayland-util.h>
#include "../../core/logcat.hpp"
namespace qs::wayland::workspace {
Q_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.wayland.workspace");
QS_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.wayland.workspace", QtWarningMsg);
WorkspaceManager::WorkspaceManager(): QWaylandClientExtensionTemplate(1) { this->initialize(); }
@ -126,7 +131,7 @@ WorkspaceGroup::~WorkspaceGroup() {
if (this->isInitialized()) this->destroy();
}
void WorkspaceGroup::ext_workspace_group_handle_v1_capabilities(uint32_t capabilities) {
void WorkspaceGroup::ext_workspace_group_handle_v1_capabilities(quint32 capabilities) {
this->canCreateWorkspace =
capabilities & ext_workspace_group_handle_v1::group_capabilities_create_workspace;
@ -144,7 +149,8 @@ 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;
@ -152,7 +158,8 @@ void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_enter(::ext_workspa
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;

View file

@ -4,7 +4,6 @@
#include <qlist.h>
#include <qloggingcategory.h>
#include <qscreen.h>
#include <qscreen_platform.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include <qtypes.h>
@ -12,11 +11,12 @@
#include <qwaylandclientextension.h>
#include <wayland-ext-workspace-v1-client-protocol.h>
#include "../../core/logcat.hpp"
#include "../output_tracking.hpp"
namespace qs::wayland::workspace {
Q_DECLARE_LOGGING_CATEGORY(logWorkspace);
QS_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(uint32_t state) override;
void ext_workspace_handle_v1_capabilities(uint32_t capabilities) override;
void ext_workspace_handle_v1_state(quint32 state) override;
void ext_workspace_handle_v1_capabilities(quint32 capabilities) override;
void ext_workspace_handle_v1_removed() override;
private:
@ -106,7 +106,7 @@ public:
bool canCreateWorkspace : 1 = false;
protected:
void ext_workspace_group_handle_v1_capabilities(uint32_t capabilities) override;
void ext_workspace_group_handle_v1_capabilities(quint32 capabilities) override;
void ext_workspace_group_handle_v1_output_enter(::wl_output* output) override;
void ext_workspace_group_handle_v1_output_leave(::wl_output* output) override;
void ext_workspace_group_handle_v1_workspace_enter(::ext_workspace_handle_v1* handle) override;

View file

@ -1,4 +1,6 @@
#include <qcontainerfwd.h>
#include <qguiapplication.h>
#include <qlist.h>
#include "../../core/plugin.hpp"

View file

@ -1,13 +1,20 @@
#include "windowmanager.hpp"
#include "../../windowmanager/windowmanager.hpp"
#include "windowset.hpp"
namespace qs::wm::wayland {
WaylandWindowManager* WaylandWindowManager::instance() {
static auto* instance = new WaylandWindowManager();
static auto* instance = []() {
auto* wm = new WaylandWindowManager();
WindowsetManager::instance();
return wm;
}();
return instance;
}
void installWmProvider() {
void installWmProvider() { // NOLINT (misc-use-internal-linkage)
qs::wm::WindowManager::setProvider([]() { return WaylandWindowManager::instance(); });
}

View file

@ -3,7 +3,7 @@
#include <qtmetamacros.h>
#include "../../windowmanager/windowmanager.hpp"
#include "workspace.hpp"
#include "windowset.hpp"
namespace qs::wm::wayland {
@ -12,14 +12,6 @@ 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

View file

@ -0,0 +1,252 @@
#include "windowset.hpp"
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qproperty.h>
#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<WlWindowset*>(ws)->commitImpl(); // NOLINT
}
for (auto* projection: projections) {
static_cast<WlWindowsetProjection*>(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<WlWindowsetProjection*>(projection)) {
wlProjection = p;
} else if (auto* p = dynamic_cast<ScreenProjection*>(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

View file

@ -0,0 +1,85 @@
#pragma once
#include <qhash.h>
#include <qlist.h>
#include <qobject.h>
#include <qproperty.h>
#include <qtmetamacros.h>
#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<impl::Workspace*> pendingWindowsetCreations;
QList<impl::Workspace*> pendingWindowsetDestructions;
QHash<impl::Workspace*, WlWindowset*> windowsetByImpl;
QList<impl::WorkspaceGroup*> pendingProjectionCreations;
QList<impl::WorkspaceGroup*> pendingProjectionDestructions;
QHash<impl::WorkspaceGroup*, WlWindowsetProjection*> 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<WindowsetManager*>(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<WindowsetManager*>(this->parent()); // NOLINT
}
private:
impl::WorkspaceGroup* impl = nullptr;
friend class WlWindowset;
};
} // namespace qs::wm::wayland

View file

@ -1,198 +0,0 @@
#include "workspace.hpp"
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qproperty.h>
#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<WlWorkspaceGroup*>(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

View file

@ -1,85 +0,0 @@
#pragma once
#include <qhash.h>
#include <qobject.h>
#include <qproperty.h>
#include <qtmetamacros.h>
#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<WlWorkspace> mWorkspaces {this};
ObjectModel<WlWorkspaceGroup> 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<impl::Workspace*> pendingWorkspaceCreations;
QList<impl::Workspace*> pendingWorkspaceDestructions;
QHash<impl::Workspace*, WlWorkspace*> workspaceByImpl;
QList<impl::WorkspaceGroup*> pendingGroupCreations;
QList<impl::WorkspaceGroup*> pendingGroupDestructions;
QHash<impl::WorkspaceGroup*, WlWorkspaceGroup*> 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<WorkspaceManager*>(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<WorkspaceManager*>(this->parent()); // NOLINT
}
private:
impl::WorkspaceGroup* impl = nullptr;
friend class WlWorkspace;
};
} // namespace qs::wm::wayland

View file

@ -1,7 +1,7 @@
qt_add_library(quickshell-windowmanager STATIC
screenprojection.cpp
windowmanager.cpp
workspace.cpp
workspacemodel.cpp
windowset.cpp
)
qt_add_qml_module(quickshell-windowmanager
@ -14,5 +14,7 @@ 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-windowmanager)
target_link_libraries(quickshell PRIVATE quickshell-windowmanagerplugin)

View file

@ -0,0 +1,10 @@
name = "Quickshell.WindowManager"
description = "Window manager interface"
headers = [
"windowmanager.hpp",
"windowset.hpp",
"screenprojection.hpp",
]
-----
Currently only supports the [ext-workspace-v1](https://wayland.app/protocols/ext-workspace-v1) wayland protocol.
Support will be expanded in future releases.

View file

@ -0,0 +1,30 @@
#include "screenprojection.hpp"
#include <qlist.h>
#include <qobject.h>
#include <qscreen.h>
#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<Windowset*> 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

View file

@ -0,0 +1,34 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#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

View file

@ -0,0 +1,86 @@
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.WindowManager
WrapperRectangle {
id: delegate
required property Windowset modelData;
color: modelData.active ? "green" : "gray"
ColumnLayout {
Label { text: delegate.modelData.toString() }
Label { text: `Id: ${delegate.modelData.id} Name: ${delegate.modelData.name}` }
Label { text: `Coordinates: ${delegate.modelData.coordinates.toString()}`}
RowLayout {
Label { text: "Group:" }
ComboBox {
Layout.fillWidth: true
implicitContentWidthPolicy: ComboBox.WidestText
enabled: delegate.modelData.canSetProjection
model: [...WindowManager.windowsetProjections].map(w => w.toString())
currentIndex: WindowManager.windowsetProjections.indexOf(delegate.modelData.projection)
onActivated: i => delegate.modelData.setProjection(WindowManager.windowsetProjections[i])
}
}
RowLayout {
Label { text: "Screen:" }
ComboBox {
Layout.fillWidth: true
implicitContentWidthPolicy: ComboBox.WidestText
enabled: delegate.modelData.canSetProjection
model: [...Quickshell.screens].map(w => w.name)
currentIndex: Quickshell.screens.indexOf(delegate.modelData.projection.screens[0])
onActivated: i => delegate.modelData.setProjection(WindowManager.screenProjection(Quickshell.screens[i]))
}
}
RowLayout {
DisplayCheckBox {
text: "Active"
checked: delegate.modelData.active
}
DisplayCheckBox {
text: "Urgent"
checked: delegate.modelData.urgent
}
DisplayCheckBox {
text: "Should Display"
checked: delegate.modelData.shouldDisplay
}
}
RowLayout {
Button {
text: "Activate"
enabled: delegate.modelData.canActivate
onClicked: delegate.modelData.activate()
}
Button {
text: "Deactivate"
enabled: delegate.modelData.canDeactivate
onClicked: delegate.modelData.deactivate()
}
Button {
text: "Remove"
enabled: delegate.modelData.canRemove
onClicked: delegate.modelData.remove()
}
}
}
component DisplayCheckBox: CheckBox {
enabled: false
palette.disabled.text: parent.palette.active.text
palette.disabled.windowText: parent.palette.active.windowText
}
}

View file

@ -0,0 +1,45 @@
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.WindowManager
FloatingWindow {
ScrollView {
anchors.fill: parent
ColumnLayout {
Repeater {
model: Quickshell.screens
WrapperRectangle {
id: delegate
required property ShellScreen modelData
color: "slategray"
margin: 5
ColumnLayout {
Label { text: `Screen: ${delegate.modelData.name}` }
Repeater {
model: ScriptModel {
values: WindowManager.screenProjection(delegate.modelData).windowsets
}
WorkspaceDelegate {}
}
}
}
}
Repeater {
model: ScriptModel {
values: WindowManager.windowsets.filter(w => w.projection == null)
}
WorkspaceDelegate {}
}
}
}
}

View file

@ -11,11 +11,11 @@ FloatingWindow {
ColumnLayout {
Repeater {
model: WindowManager.workspaceGroups
model: WindowManager.windowsetProjections
WrapperRectangle {
id: delegate
required property WorkspaceGroup modelData
required property WindowsetProjection modelData
color: "slategray"
margin: 5
@ -25,7 +25,7 @@ FloatingWindow {
Repeater {
model: ScriptModel {
values: [...WindowManager.workspaces.values].filter(w => w.group == delegate.modelData)
values: delegate.modelData.windowsets
}
WorkspaceDelegate {}
@ -36,77 +36,11 @@ FloatingWindow {
Repeater {
model: ScriptModel {
values: WindowManager.workspaces.values.filter(w => w.group == null)
values: WindowManager.windowsets.filter(w => w.projection == 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
}
}

View file

@ -2,6 +2,11 @@
#include <functional>
#include <utility>
#include <qobject.h>
#include "../core/qmlscreen.hpp"
#include "screenprojection.hpp"
namespace qs::wm {
std::function<WindowManager*()> WindowManager::provider;
@ -15,4 +20,22 @@ 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

View file

@ -2,12 +2,17 @@
#include <functional>
#include <qhash.h>
#include <qlist.h>
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#include "../core/model.hpp"
#include "workspace.hpp"
#include "../core/qmlscreen.hpp"
#include "screenprojection.hpp"
#include "windowset.hpp"
namespace qs::wm {
@ -18,32 +23,68 @@ public:
static void setProvider(std::function<WindowManager*()> provider);
static WindowManager* instance();
[[nodiscard]] virtual UntypedObjectModel* workspaces() const {
return UntypedObjectModel::emptyInstance();
Q_INVOKABLE ScreenProjection* screenProjection(QuickshellScreenInfo* screen);
[[nodiscard]] QBindable<QList<Windowset*>> bindableWindowsets() const {
return &this->bWindowsets;
}
[[nodiscard]] virtual UntypedObjectModel* workspaceGroups() const {
return UntypedObjectModel::emptyInstance();
[[nodiscard]] QBindable<QList<WindowsetProjection*>> bindableWindowsetProjections() const {
return &this->bWindowsetProjections;
}
signals:
void windowsetsChanged();
void windowsetProjectionsChanged();
public:
Q_OBJECT_BINDABLE_PROPERTY(
WindowManager,
QList<Windowset*>,
bWindowsets,
&WindowManager::windowsetsChanged
);
Q_OBJECT_BINDABLE_PROPERTY(
WindowManager,
QList<WindowsetProjection*>,
bWindowsetProjections,
&WindowManager::windowsetProjectionsChanged
);
private:
static std::function<WindowManager*()> provider;
QHash<QScreen*, ScreenProjection*> mScreenProjections;
};
///! Window management interfaces exposed by the window manager.
class WindowManagerQml: public QObject {
Q_OBJECT;
QML_NAMED_ELEMENT(WindowManager);
QML_SINGLETON;
Q_PROPERTY(UntypedObjectModel* workspaces READ workspaces CONSTANT);
Q_PROPERTY(UntypedObjectModel* workspaceGroups READ workspaceGroups CONSTANT);
// clang-format off
/// All windowsets tracked by the WM across all projections.
Q_PROPERTY(QList<Windowset*> windowsets READ default BINDABLE bindableWindowsets);
/// All windowset projections tracked by the WM. Does not include
/// internal projections from @@screenProjection().
Q_PROPERTY(QList<WindowsetProjection*> windowsetProjections READ default BINDABLE bindableWindowsetProjections);
// clang-format on
public:
[[nodiscard]] static UntypedObjectModel* workspaces() {
return WindowManager::instance()->workspaces();
/// 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* workspaceGroups() {
return WindowManager::instance()->workspaceGroups();
[[nodiscard]] static QBindable<QList<Windowset*>> bindableWindowsets() {
return WindowManager::instance()->bindableWindowsets();
}
[[nodiscard]] static QBindable<QList<WindowsetProjection*>> bindableWindowsetProjections() {
return WindowManager::instance()->bindableWindowsetProjections();
}
};

View file

@ -0,0 +1,45 @@
#include "windowset.hpp"
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#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<Windowset*> result;
for (auto* ws: WindowManager::instance()->bindableWindowsets().value()) {
if (ws->bindableProjection().value() == this) {
result.append(ws);
}
}
return result;
});
this->bScreens.setBinding([this] {
QList<QuickshellScreenInfo*> screens;
for (auto* screen: this->bQScreens.value()) {
screens.append(QuickshellTracked::instance()->screenInfo(screen));
}
return screens;
});
}
} // namespace qs::wm

View file

@ -0,0 +1,175 @@
#pragma once
#include <qcontainerfwd.h>
#include <qlist.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#include <qtypes.h>
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<qint32> 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<QString> bindableId() const { return &this->bId; }
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
[[nodiscard]] QBindable<QList<qint32>> bindableCoordinates() const { return &this->bCoordinates; }
[[nodiscard]] QBindable<bool> bindableActive() const { return &this->bActive; }
[[nodiscard]] QBindable<WindowsetProjection*> bindableProjection() const {
return &this->bProjection;
}
[[nodiscard]] QBindable<bool> bindableShouldDisplay() const { return &this->bShouldDisplay; }
[[nodiscard]] QBindable<bool> bindableUrgent() const { return &this->bUrgent; }
[[nodiscard]] QBindable<bool> bindableCanActivate() const { return &this->bCanActivate; }
[[nodiscard]] QBindable<bool> bindableCanDeactivate() const { return &this->bCanDeactivate; }
[[nodiscard]] QBindable<bool> bindableCanRemove() const { return &this->bCanRemove; }
[[nodiscard]] QBindable<bool> 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<qint32>, 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<QuickshellScreenInfo*> screens READ default NOTIFY screensChanged BINDABLE bindableScreens);
/// Windowsets that are currently present on the projection.
Q_PROPERTY(QList<Windowset*> windowsets READ default NOTIFY windowsetsChanged BINDABLE bindableWindowsets);
// clang-format on
public:
explicit WindowsetProjection(QObject* parent);
[[nodiscard]] QBindable<QList<QuickshellScreenInfo*>> bindableScreens() const {
return &this->bScreens;
}
[[nodiscard]] QBindable<QList<QScreen*>> bindableQScreens() const { return &this->bQScreens; }
[[nodiscard]] QBindable<QList<Windowset*>> bindableWindowsets() const {
return &this->bWindowsets;
}
signals:
void screensChanged();
void windowsetsChanged();
protected:
Q_OBJECT_BINDABLE_PROPERTY(WindowsetProjection, QList<QScreen*>, bQScreens);
Q_OBJECT_BINDABLE_PROPERTY(
WindowsetProjection,
QList<QuickshellScreenInfo*>,
bScreens,
&WindowsetProjection::screensChanged
);
Q_OBJECT_BINDABLE_PROPERTY(
WindowsetProjection,
QList<Windowset*>,
bWindowsets,
&WindowsetProjection::windowsetsChanged
);
};
} // namespace qs::wm

View file

@ -1,31 +0,0 @@
#include "workspace.hpp"
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qtmetamacros.h>
#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

View file

@ -1,119 +0,0 @@
#pragma once
#include <qcontainerfwd.h>
#include <qlist.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#include <qtypes.h>
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<QString> bindableId() const { return &this->bId; }
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
[[nodiscard]] QBindable<bool> bindableActive() const { return &this->bActive; }
[[nodiscard]] QBindable<WorkspaceGroup*> bindableGroup() const { return &this->bGroup; }
[[nodiscard]] QBindable<bool> bindableShouldDisplay() const { return &this->bShouldDisplay; }
[[nodiscard]] QBindable<bool> bindableUrgent() const { return &this->bUrgent; }
[[nodiscard]] QBindable<bool> bindableCanActivate() const { return &this->bCanActivate; }
[[nodiscard]] QBindable<bool> bindableCanDeactivate() const { return &this->bCanDeactivate; }
[[nodiscard]] QBindable<bool> bindableCanRemove() const { return &this->bCanRemove; }
[[nodiscard]] QBindable<bool> 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<QuickshellScreenInfo*> screens READ screens NOTIFY screensChanged);
public:
explicit WorkspaceGroup(QObject* parent): QObject(parent) {}
[[nodiscard]] const QList<QuickshellScreenInfo*>& screens() const { return this->mCachedScreens; }
signals:
void screensChanged();
private slots:
void onScreensChanged();
protected:
Q_OBJECT_BINDABLE_PROPERTY(
WorkspaceGroup,
QList<QScreen*>,
bScreens,
&WorkspaceGroup::onScreensChanged
);
private:
QList<QuickshellScreenInfo*> mCachedScreens;
};
} // namespace qs::wm

View file

@ -1 +0,0 @@
#include "workspacemodel.hpp"

View file

@ -1,39 +0,0 @@
#pragma once
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
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

View file

@ -17,7 +17,7 @@ qs_add_module_deps_light(quickshell-i3-ipc Quickshell)
install_qml_module(quickshell-i3-ipc)
target_link_libraries(quickshell-i3-ipc PRIVATE Qt::Quick)
target_link_libraries(quickshell-i3-ipc PRIVATE Qt::Quick quickshell-core)
qs_module_pch(quickshell-i3-ipc SET large)

View file

@ -7,7 +7,6 @@
#include <qbytearray.h>
#include <qbytearrayview.h>
#include <qcontainerfwd.h>
#include <qdatastream.h>
#include <qjsonarray.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
@ -15,9 +14,7 @@
#include <qlocalsocket.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qsysinfo.h>
#include <qtenvironmentvariables.h>
#include <qtmetamacros.h>
#include <qtypes.h>
@ -89,9 +86,6 @@ I3Ipc::I3Ipc(const QList<QString>& 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<QDataStream::ByteOrder>(QSysInfo::ByteOrder));
}
void I3Ipc::makeRequest(const QByteArray& request) {
@ -145,34 +139,21 @@ void I3Ipc::reconnectIPC() {
}
QVector<Event> I3Ipc::parseResponse() {
QVector<std::tuple<EventCode, QJsonDocument>> events;
const int magicLen = 6;
QVector<Event> events;
while (!this->liveEventSocketDs.atEnd()) {
this->liveEventSocketDs.startTransaction();
this->liveEventSocketDs.startTransaction();
while (true) {
this->eventReader.startTransaction();
auto magic = this->eventReader.readBytes(6);
auto size = this->eventReader.readI32();
auto type = this->eventReader.readI32();
auto payload = this->eventReader.readBytes(size);
if (!this->eventReader.commitTransaction()) return events;
std::array<char, 6> buffer = {};
qint32 size = 0;
qint32 type = EventCode::Unknown;
this->liveEventSocketDs.readRawData(buffer.data(), magicLen);
this->liveEventSocketDs >> size;
this->liveEventSocketDs >> type;
if (!this->liveEventSocketDs.commitTransaction()) break;
QByteArray payload(size, Qt::Uninitialized);
this->liveEventSocketDs.readRawData(payload.data(), size);
if (!this->liveEventSocketDs.commitTransaction()) break;
if (strncmp(buffer.data(), MAGIC.data(), 6) != 0) {
if (magic.size() < 6 || strncmp(magic.data(), MAGIC.data(), 6) != 0) {
qCWarning(logI3Ipc) << "No magic sequence found in string.";
this->reconnectIPC();
break;
};
}
if (I3IpcEvent::intToEvent(type) == EventCode::Unknown) {
qCWarning(logI3Ipc) << "Received unknown event";
@ -204,6 +185,7 @@ void I3Ipc::eventSocketError(QLocalSocket::LocalSocketError error) const {
void I3Ipc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) {
if (state == QLocalSocket::ConnectedState) {
this->eventReader.setDevice(&this->liveEventSocket);
qCInfo(logI3Ipc) << "I3 event socket connected.";
emit this->connected();
} else if (state == QLocalSocket::UnconnectedState && this->valid) {

View file

@ -1,7 +1,6 @@
#pragma once
#include <qbytearrayview.h>
#include <qdatastream.h>
#include <qjsondocument.h>
#include <qlocalsocket.h>
#include <qobject.h>
@ -9,6 +8,8 @@
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../../core/streamreader.hpp"
namespace qs::i3::ipc {
constexpr std::string MAGIC = "i3-ipc";
@ -92,7 +93,7 @@ protected:
QVector<std::tuple<EventCode, QJsonDocument>> parseResponse();
QLocalSocket liveEventSocket;
QDataStream liveEventSocketDs;
StreamReader eventReader;
QString mSocketPath;
bool valid = false;