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
54 changed files with 2116 additions and 407 deletions

View file

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

View file

@ -15,7 +15,7 @@ Please make this descriptive enough to identify your specific package, for examp
- `Nixpkgs` - `Nixpkgs`
- `Fedora COPR (errornointernet/quickshell)` - `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 ### QML Module dir
Currently all QML modules are statically linked to quickshell, but this is where 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` - `cmake`
- `qt6base` - `qt6base`
- `qt6declarative` - `qt6declarative`
- `libdrm`
- `qtshadertools` (build-time) - `qtshadertools` (build-time)
- `spirv-tools` (build-time) - `spirv-tools` (build-time)
- `pkg-config` (build-time) - `pkg-config` (build-time)
@ -67,7 +68,13 @@ Dependencies: `cpptrace`
Note: `-DVENDOR_CPPTRACE=ON` can be set to vendor cpptrace using FetchContent. 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 ### Jemalloc
We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused 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` To disable: `-DSCREENCOPY=OFF`
Dependencies: Dependencies:
- `libdrm`
- `libgbm` - `libgbm`
- `vulkan-headers` (build-time) - `vulkan-headers` (build-time)
@ -236,7 +242,7 @@ Only `ninja` builds are tested, but makefiles may work.
#### Configuring the build #### Configuring the build
```sh ```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 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 "") 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) function(boption VAR NAME DEFAULT)
cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "") cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "")

View file

@ -1,235 +1,40 @@
# Contributing / Development # Contributing
Instructions for development setup and upstreaming patches.
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). - All changes submitted MUST be **fully understood by the submitter**. If you do not know why or how
You probably want all of them even if you don't use all of them your change works, do not submit it to be merged. You must be able to explain your reasoning
to ensure tests work correctly and avoid passing a bunch of configure for every change.
flags when you need to wipe the build directory.
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 - Changes MUST respect Quickshell's license and the license of any source works. Changes including
using with nix-direnv. 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: - Changes must follow the guidelines outlined in [HACKING.md](HACKING.md) for style and substance.
- `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 - Changes must stand on their own as a unit. Do not make multiple unrelated changes in one PR.
All contributions should be formatted similarly to what already exists. Changes depending on prior merges should be marked as a draft.
Group related functionality together.
Run the formatter using `just fmt`. ## Acceptable Non-code Contributions
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 - Bug and crash reports. You must follow the instructions in the issue templates and provide the
These are flexible. You can ignore them if it looks or works better to information requested.
for one reason or another.
Use `auto` if the type of a variable can be deduced automatically, instead of - Feature requests can be made via Issues. Please check to ensure nobody else has requested the same feature.
redeclaring the returned value's type. Additionally, auto should be used when a
constructor takes arguments.
```cpp - Do not make insubstantial or pointless changes.
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 - Changes to project rules / policy / governance will not be entertained, except from significant
QString x(); // avoid long-term contributors. These changes should not be addressed through contribution channels.
QString x("foo"); // avoid
```
Put newlines around logical units of code, and after closing braces. If the ## Merge timelines
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 We handle work for the most part on a push basis. If your PR has been ignored for a while
emit this->y(); // unit 2 and is still relevant please bump it.
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.

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 - https://github.com/quickshell-mirror/quickshell
# Contributing / Development # 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 #### License

View file

@ -26,6 +26,7 @@ set shell id.
- Added Quickshell version checking and version gated preprocessing. - 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 a way to detect if an icon is from the system icon theme or not.
- Added vulkan support to screencopy. - Added vulkan support to screencopy.
- Added generic WindowManager interface implementing ext-workspace.
## Other Changes ## Other Changes
@ -33,6 +34,10 @@ set shell id.
- IPC operations filter available instances to the current display connection by default. - IPC operations filter available instances to the current display connection by default.
- PwNodeLinkTracker ignores sound level monitoring programs. - PwNodeLinkTracker ignores sound level monitoring programs.
- Replaced breakpad with cpptrace. - 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 ## Bug Fixes
@ -50,7 +55,9 @@ set shell id.
- Fixed ClippingRectangle related crashes. - Fixed ClippingRectangle related crashes.
- Fixed crashes when monitors are unplugged. - Fixed crashes when monitors are unplugged.
- Fixed crashes when default pipewire devices are lost. - Fixed crashes when default pipewire devices are lost.
- Fixed ToplevelManager not clearing activeToplevel on deactivation.
- Desktop action order is now preserved. - Desktop action order is now preserved.
- Fixed partial socket reads in greetd and hyprland on slow machines.
## Packaging Changes ## Packaging Changes
@ -58,3 +65,4 @@ set shell id.
- `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support). - `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support).
- `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore. - `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. - `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 = [ buildInputs = [
qt6.qtbase qt6.qtbase
qt6.qtdeclarative qt6.qtdeclarative
libdrm
cli11 cli11
] ]
++ lib.optional withQtSvg qt6.qtsvg ++ lib.optional withQtSvg qt6.qtsvg
@ -88,7 +89,7 @@
++ lib.optional withJemalloc jemalloc ++ lib.optional withJemalloc jemalloc
++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland ++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
++ lib.optionals withWayland [ wayland wayland-protocols ] ++ 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 withX11 libxcb
++ lib.optional withPam pam ++ lib.optional withPam pam
++ lib.optional withPipewire pipewire ++ lib.optional withPipewire pipewire

View file

@ -11,6 +11,7 @@ add_subdirectory(window)
add_subdirectory(io) add_subdirectory(io)
add_subdirectory(widgets) add_subdirectory(widgets)
add_subdirectory(ui) add_subdirectory(ui)
add_subdirectory(windowmanager)
if (CRASH_HANDLER) if (CRASH_HANDLER)
add_subdirectory(crash) add_subdirectory(crash)

View file

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

View file

@ -1,3 +1,4 @@
pkg_check_modules(libdrm REQUIRED IMPORTED_TARGET libdrm)
qt_add_library(quickshell-core STATIC qt_add_library(quickshell-core STATIC
plugin.cpp plugin.cpp
shell.cpp shell.cpp
@ -40,6 +41,8 @@ qt_add_library(quickshell-core STATIC
scriptmodel.cpp scriptmodel.cpp
colorquantizer.cpp colorquantizer.cpp
toolsupport.cpp toolsupport.cpp
streamreader.cpp
debuginfo.cpp
) )
qt_add_qml_module(quickshell-core qt_add_qml_module(quickshell-core
@ -52,7 +55,7 @@ qt_add_qml_module(quickshell-core
install_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) 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) { for (const auto& file: files) {
if (!this->scanner.scannedFiles.contains(file)) { if (!this->scanner.scannedFiles.contains(file)) {
this->extraWatchedFiles.append(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); auto fileInfo = QFileInfo(name);
if (fileInfo.isFile() && fileInfo.size() == 0) return; 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(); emit this->filesChanged();
} }
} }
@ -237,6 +244,11 @@ void EngineGeneration::onDirectoryChanged() {
// try to find any files that were just deleted from a replace operation // try to find any files that were just deleted from a replace operation
for (auto& file: this->deletedWatchedFiles) { for (auto& file: this->deletedWatchedFiles) {
if (QFileInfo(file).exists()) { if (QFileInfo(file).exists()) {
if (!this->scanner.hasFileContentChanged(file)) {
qCDebug(logQmlScanner) << "Ignoring restored file with unchanged content:" << file;
continue;
}
emit this->filesChanged(); emit this->filesChanged();
break; break;
} }

View file

@ -60,7 +60,9 @@ void QuickshellSettings::setWorkingDirectory(QString workingDirectory) { // NOLI
emit this->workingDirectoryChanged(); 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) { void QuickshellSettings::setWatchFiles(bool watchFiles) {
if (watchFiles == this->mWatchFiles) return; if (watchFiles == this->mWatchFiles) return;

View file

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

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <qbytearray.h>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qdir.h> #include <qdir.h>
#include <qhash.h> #include <qhash.h>
@ -21,6 +22,7 @@ public:
QVector<QDir> scannedDirs; QVector<QDir> scannedDirs;
QVector<QString> scannedFiles; QVector<QString> scannedFiles;
QHash<QString, QByteArray> fileHashes;
QHash<QString, QString> fileIntercepts; QHash<QString, QString> fileIntercepts;
struct ScanError { struct ScanError {
@ -31,6 +33,9 @@ public:
QVector<ScanError> scanErrors; QVector<ScanError> scanErrors;
bool readAndHashFile(const QString& path, QByteArray& data);
[[nodiscard]] bool hasFileContentChanged(const QString& path) const;
private: private:
QDir rootPath; 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(); auto coredumpPid = fork();
if (coredumpPid == 0) { if (coredumpPid == 0) {
// NOLINTBEGIN (misc-include-cleaner)
sigset_t set;
sigfillset(&set);
sigprocmask(SIG_UNBLOCK, &set, nullptr);
// NOLINTEND
raise(sig); raise(sig);
_exit(-1); _exit(-1);
} }

View file

@ -5,6 +5,7 @@
#include <qapplication.h> #include <qapplication.h>
#include <qboxlayout.h> #include <qboxlayout.h>
#include <qconfig.h> #include <qconfig.h>
#include <qcontainerfwd.h>
#include <qdesktopservices.h> #include <qdesktopservices.h>
#include <qfont.h> #include <qfont.h>
#include <qfontinfo.h> #include <qfontinfo.h>
@ -12,11 +13,22 @@
#include <qnamespace.h> #include <qnamespace.h>
#include <qobject.h> #include <qobject.h>
#include <qpushbutton.h> #include <qpushbutton.h>
#include <qtenvironmentvariables.h>
#include <qtversion.h> #include <qtversion.h>
#include <qwidget.h> #include <qwidget.h>
#include "build.hpp" #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 { class ReportLabel: public QWidget {
public: public:
ReportLabel(const QString& label, const QString& content, QWidget* parent): QWidget(parent) { ReportLabel(const QString& label, const QString& content, QWidget* parent): QWidget(parent) {
@ -67,22 +79,16 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid)
if (qtVersionMatches) { if (qtVersionMatches) {
mainLayout->addWidget( 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 { } else {
mainLayout->addWidget(new QLabel( mainLayout->addWidget(new QLabel(
"Please rebuild Quickshell against the current Qt version.\n" "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( mainLayout->addWidget(new ReportLabel("Tracker:", crashreportUrl(), this));
"Github:",
"https://github.com/quickshell-mirror/quickshell/issues/new?template=crash2.yml",
this
));
mainLayout->addWidget(new ReportLabel("Email:", "quickshell-bugs@outfoxxed.me", this));
auto* buttons = new QWidget(this); auto* buttons = new QWidget(this);
buttons->setMinimumWidth(900); buttons->setMinimumWidth(900);
@ -112,10 +118,5 @@ void CrashReporterGui::openFolder() {
QDesktopServices::openUrl(QUrl::fromLocalFile(this->reportFolder)); QDesktopServices::openUrl(QUrl::fromLocalFile(this->reportFolder));
} }
void CrashReporterGui::openReportUrl() { void CrashReporterGui::openReportUrl() { QDesktopServices::openUrl(QUrl(crashreportUrl())); }
QDesktopServices::openUrl(
QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml")
);
}
void CrashReporterGui::cancel() { QApplication::quit(); } void CrashReporterGui::cancel() { QApplication::quit(); }

View file

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

View file

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

View file

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

View file

@ -12,7 +12,7 @@ qt_add_qml_module(quickshell-service-greetd
install_qml_module(quickshell-service-greetd) install_qml_module(quickshell-service-greetd)
# can't be Qt::Qml because generation.hpp pulls in gui types # 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) qs_module_pch(quickshell-service-greetd)

View file

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

View file

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

View file

@ -123,6 +123,8 @@ list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleNotify)
add_subdirectory(shortcuts_inhibit) add_subdirectory(shortcuts_inhibit)
list(APPEND WAYLAND_MODULES Quickshell.Wayland._ShortcutsInhibitor) list(APPEND WAYLAND_MODULES Quickshell.Wayland._ShortcutsInhibitor)
add_subdirectory(windowmanager)
# widgets for qmenu # widgets for qmenu
target_link_libraries(quickshell-wayland PRIVATE target_link_libraries(quickshell-wayland PRIVATE
Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate

View file

@ -15,7 +15,7 @@ qs_add_module_deps_light(quickshell-hyprland-ipc Quickshell)
install_qml_module(quickshell-hyprland-ipc) 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) if (WAYLAND_TOPLEVEL_MANAGEMENT)
target_sources(quickshell-hyprland-ipc PRIVATE 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) { void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) {
if (state == QLocalSocket::ConnectedState) { if (state == QLocalSocket::ConnectedState) {
this->eventReader.setDevice(&this->eventSocket);
qCInfo(logHyprlandIpc) << "Hyprland event socket connected."; qCInfo(logHyprlandIpc) << "Hyprland event socket connected.";
emit this->connected(); emit this->connected();
} else if (state == QLocalSocket::UnconnectedState && this->valid) { } else if (state == QLocalSocket::UnconnectedState && this->valid) {
@ -104,11 +105,11 @@ void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state)
void HyprlandIpc::eventSocketReady() { void HyprlandIpc::eventSocketReady() {
while (true) { while (true) {
auto rawEvent = this->eventSocket.readLine(); this->eventReader.startTransaction();
if (rawEvent.isEmpty()) break; auto rawEvent = this->eventReader.readUntil('\n');
if (!this->eventReader.commitTransaction()) return;
// remove trailing \n rawEvent.chop(1); // remove trailing \n
rawEvent.truncate(rawEvent.length() - 1);
auto splitIdx = rawEvent.indexOf(">>"); auto splitIdx = rawEvent.indexOf(">>");
auto event = QByteArrayView(rawEvent.data(), splitIdx); auto event = QByteArrayView(rawEvent.data(), splitIdx);
auto data = QByteArrayView( auto data = QByteArrayView(

View file

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

View file

@ -161,7 +161,11 @@ void ToplevelManager::onToplevelReady(impl::ToplevelHandle* handle) {
void ToplevelManager::onToplevelActiveChanged() { void ToplevelManager::onToplevelActiveChanged() {
auto* toplevel = qobject_cast<Toplevel*>(this->sender()); 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() { void ToplevelManager::onToplevelClosed() {

View file

@ -0,0 +1,19 @@
qt_add_library(quickshell-wayland-windowsystem STATIC
windowmanager.cpp
windowset.cpp
ext_workspace.cpp
)
add_library(quickshell-wayland-windowsystem-init OBJECT init.cpp)
target_link_libraries(quickshell-wayland-windowsystem-init PRIVATE Qt::Quick)
wl_proto(wlp-ext-workspace ext-workspace-v1 "${WAYLAND_PROTOCOLS}/staging/ext-workspace")
target_link_libraries(quickshell-wayland-windowsystem PRIVATE
Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client
wlp-ext-workspace
)
qs_pch(quickshell-wayland-windowsystem SET large)
target_link_libraries(quickshell PRIVATE quickshell-wayland-windowsystem quickshell-wayland-windowsystem-init)

View file

@ -0,0 +1,176 @@
#include "ext_workspace.hpp"
#include <qcontainerfwd.h>
#include <qlogging.h>
#include <qloggingcategory.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 {
QS_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.wayland.workspace", QtWarningMsg);
WorkspaceManager::WorkspaceManager(): QWaylandClientExtensionTemplate(1) { this->initialize(); }
WorkspaceManager* WorkspaceManager::instance() {
static auto* instance = new WorkspaceManager();
return instance;
}
void WorkspaceManager::ext_workspace_manager_v1_workspace_group(
::ext_workspace_group_handle_v1* handle
) {
auto* group = new WorkspaceGroup(handle);
qCDebug(logWorkspace) << "Created group" << group;
this->mGroups.insert(handle, group);
emit this->groupCreated(group);
}
void WorkspaceManager::ext_workspace_manager_v1_workspace(::ext_workspace_handle_v1* handle) {
auto* workspace = new Workspace(handle);
qCDebug(logWorkspace) << "Created workspace" << workspace;
this->mWorkspaces.insert(handle, workspace);
emit this->workspaceCreated(workspace);
};
void WorkspaceManager::destroyWorkspace(Workspace* workspace) {
this->mWorkspaces.remove(workspace->object());
this->destroyedWorkspaces.append(workspace);
emit this->workspaceDestroyed(workspace);
}
void WorkspaceManager::destroyGroup(WorkspaceGroup* group) {
this->mGroups.remove(group->object());
this->destroyedGroups.append(group);
emit this->groupDestroyed(group);
}
void WorkspaceManager::ext_workspace_manager_v1_done() {
qCDebug(logWorkspace) << "Workspace changes done";
emit this->serverCommit();
for (auto* workspace: this->destroyedWorkspaces) delete workspace;
for (auto* group: this->destroyedGroups) delete group;
this->destroyedWorkspaces.clear();
this->destroyedGroups.clear();
}
void WorkspaceManager::ext_workspace_manager_v1_finished() {
qCWarning(logWorkspace) << "ext_workspace_manager_v1.finished() was received";
}
Workspace::~Workspace() {
if (this->isInitialized()) this->destroy();
}
void Workspace::ext_workspace_handle_v1_id(const QString& id) {
qCDebug(logWorkspace) << "Updated id for workspace" << this << "to" << id;
this->id = id;
}
void Workspace::ext_workspace_handle_v1_name(const QString& name) {
qCDebug(logWorkspace) << "Updated name for workspace" << this << "to" << name;
this->name = name;
}
void Workspace::ext_workspace_handle_v1_coordinates(wl_array* coordinates) {
this->coordinates.clear();
auto* data = static_cast<qint32*>(coordinates->data);
auto size = static_cast<qsizetype>(coordinates->size / sizeof(qint32));
for (auto i = 0; i != size; ++i) {
this->coordinates.append(data[i]); // NOLINT
}
qCDebug(logWorkspace) << "Updated coordinates for workspace" << this << "to" << this->coordinates;
}
void Workspace::ext_workspace_handle_v1_state(quint32 state) {
this->active = state & ext_workspace_handle_v1::state_active;
this->urgent = state & ext_workspace_handle_v1::state_urgent;
this->hidden = state & ext_workspace_handle_v1::state_hidden;
qCDebug(logWorkspace).nospace() << "Updated state for workspace " << this
<< " to [active: " << this->active << ", urgent: " << this->urgent
<< ", hidden: " << this->hidden << ']';
}
void Workspace::ext_workspace_handle_v1_capabilities(quint32 capabilities) {
this->canActivate = capabilities & ext_workspace_handle_v1::workspace_capabilities_activate;
this->canDeactivate = capabilities & ext_workspace_handle_v1::workspace_capabilities_deactivate;
this->canRemove = capabilities & ext_workspace_handle_v1::workspace_capabilities_remove;
this->canAssign = capabilities & ext_workspace_handle_v1::workspace_capabilities_assign;
qCDebug(logWorkspace).nospace() << "Updated capabilities for workspace " << this
<< " to [activate: " << this->canActivate
<< ", deactivate: " << this->canDeactivate
<< ", remove: " << this->canRemove
<< ", assign: " << this->canAssign << ']';
}
void Workspace::ext_workspace_handle_v1_removed() {
qCDebug(logWorkspace) << "Destroyed workspace" << this;
WorkspaceManager::instance()->destroyWorkspace(this);
this->destroy();
}
void Workspace::enterGroup(WorkspaceGroup* group) { this->group = group; }
void Workspace::leaveGroup(WorkspaceGroup* group) {
if (this->group == group) this->group = nullptr;
}
WorkspaceGroup::~WorkspaceGroup() {
if (this->isInitialized()) this->destroy();
}
void WorkspaceGroup::ext_workspace_group_handle_v1_capabilities(quint32 capabilities) {
this->canCreateWorkspace =
capabilities & ext_workspace_group_handle_v1::group_capabilities_create_workspace;
qCDebug(logWorkspace).nospace() << "Updated capabilities for group " << this
<< " to [create_workspace: " << this->canCreateWorkspace << ']';
}
void WorkspaceGroup::ext_workspace_group_handle_v1_output_enter(::wl_output* output) {
qCDebug(logWorkspace) << "Output" << output << "added to group" << this;
this->screens.addOutput(output);
}
void WorkspaceGroup::ext_workspace_group_handle_v1_output_leave(::wl_output* output) {
qCDebug(logWorkspace) << "Output" << output << "removed from group" << this;
this->screens.removeOutput(output);
}
void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_enter(
::ext_workspace_handle_v1* handle
) {
auto* workspace = WorkspaceManager::instance()->mWorkspaces.value(handle);
qCDebug(logWorkspace) << "Workspace" << workspace << "added to group" << this;
if (workspace) workspace->enterGroup(this);
}
void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_leave(
::ext_workspace_handle_v1* handle
) {
auto* workspace = WorkspaceManager::instance()->mWorkspaces.value(handle);
qCDebug(logWorkspace) << "Workspace" << workspace << "removed from group" << this;
if (workspace) workspace->leaveGroup(this);
}
void WorkspaceGroup::ext_workspace_group_handle_v1_removed() {
qCDebug(logWorkspace) << "Destroyed group" << this;
WorkspaceManager::instance()->destroyGroup(this);
this->destroy();
}
} // namespace qs::wayland::workspace

View file

@ -0,0 +1,117 @@
#pragma once
#include <qcontainerfwd.h>
#include <qlist.h>
#include <qloggingcategory.h>
#include <qscreen.h>
#include <qtclasshelpermacros.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 "../../core/logcat.hpp"
#include "../output_tracking.hpp"
namespace qs::wayland::workspace {
QS_DECLARE_LOGGING_CATEGORY(logWorkspace);
class WorkspaceGroup;
class Workspace;
class WorkspaceManager
: public QWaylandClientExtensionTemplate<WorkspaceManager>
, public QtWayland::ext_workspace_manager_v1 {
Q_OBJECT;
public:
static WorkspaceManager* instance();
[[nodiscard]] QList<Workspace*> workspaces() { return this->mWorkspaces.values(); }
signals:
void serverCommit();
void workspaceCreated(Workspace* workspace);
void workspaceDestroyed(Workspace* workspace);
void groupCreated(WorkspaceGroup* group);
void groupDestroyed(WorkspaceGroup* group);
protected:
void ext_workspace_manager_v1_workspace_group(::ext_workspace_group_handle_v1* handle) override;
void ext_workspace_manager_v1_workspace(::ext_workspace_handle_v1* handle) override;
void ext_workspace_manager_v1_done() override;
void ext_workspace_manager_v1_finished() override;
private:
WorkspaceManager();
void destroyGroup(WorkspaceGroup* group);
void destroyWorkspace(Workspace* workspace);
QHash<::ext_workspace_handle_v1*, Workspace*> mWorkspaces;
QHash<::ext_workspace_group_handle_v1*, WorkspaceGroup*> mGroups;
QList<WorkspaceGroup*> destroyedGroups;
QList<Workspace*> destroyedWorkspaces;
friend class Workspace;
friend class WorkspaceGroup;
};
class Workspace: public QtWayland::ext_workspace_handle_v1 {
public:
Workspace(::ext_workspace_handle_v1* handle): QtWayland::ext_workspace_handle_v1(handle) {}
~Workspace() override;
Q_DISABLE_COPY_MOVE(Workspace);
QString id;
QString name;
QList<qint32> coordinates;
WorkspaceGroup* group = nullptr;
bool active : 1 = false;
bool urgent : 1 = false;
bool hidden : 1 = false;
bool canActivate : 1 = false;
bool canDeactivate : 1 = false;
bool canRemove : 1 = false;
bool canAssign : 1 = false;
protected:
void ext_workspace_handle_v1_id(const QString& id) override;
void ext_workspace_handle_v1_name(const QString& name) override;
void ext_workspace_handle_v1_coordinates(wl_array* coordinates) override;
void ext_workspace_handle_v1_state(quint32 state) override;
void ext_workspace_handle_v1_capabilities(quint32 capabilities) override;
void ext_workspace_handle_v1_removed() override;
private:
void enterGroup(WorkspaceGroup* group);
void leaveGroup(WorkspaceGroup* group);
friend class WorkspaceGroup;
};
class WorkspaceGroup: public QtWayland::ext_workspace_group_handle_v1 {
public:
WorkspaceGroup(::ext_workspace_group_handle_v1* handle)
: QtWayland::ext_workspace_group_handle_v1(handle) {}
~WorkspaceGroup() override;
Q_DISABLE_COPY_MOVE(WorkspaceGroup);
WlOutputTracker screens;
bool canCreateWorkspace : 1 = false;
protected:
void ext_workspace_group_handle_v1_capabilities(quint32 capabilities) override;
void ext_workspace_group_handle_v1_output_enter(::wl_output* output) override;
void ext_workspace_group_handle_v1_output_leave(::wl_output* output) override;
void ext_workspace_group_handle_v1_workspace_enter(::ext_workspace_handle_v1* handle) override;
void ext_workspace_group_handle_v1_workspace_leave(::ext_workspace_handle_v1* handle) override;
void ext_workspace_group_handle_v1_removed() override;
};
} // namespace qs::wayland::workspace

View file

@ -0,0 +1,23 @@
#include <qcontainerfwd.h>
#include <qguiapplication.h>
#include <qlist.h>
#include "../../core/plugin.hpp"
namespace qs::wm::wayland {
void installWmProvider();
}
namespace {
class WaylandWmPlugin: public QsEnginePlugin {
QList<QString> dependencies() override { return {"window"}; }
bool applies() override { return QGuiApplication::platformName() == "wayland"; }
void init() override { qs::wm::wayland::installWmProvider(); }
};
QS_REGISTER_PLUGIN(WaylandWmPlugin);
} // namespace

View file

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

View file

@ -0,0 +1,17 @@
#pragma once
#include <qtmetamacros.h>
#include "../../windowmanager/windowmanager.hpp"
#include "windowset.hpp"
namespace qs::wm::wayland {
class WaylandWindowManager: public WindowManager {
Q_OBJECT;
public:
static WaylandWindowManager* instance();
};
} // namespace qs::wm::wayland

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

@ -0,0 +1,20 @@
qt_add_library(quickshell-windowmanager STATIC
screenprojection.cpp
windowmanager.cpp
windowset.cpp
)
qt_add_qml_module(quickshell-windowmanager
URI Quickshell.WindowManager
VERSION 0.1
DEPENDENCIES QtQuick
)
qs_add_module_deps_light(quickshell-windowmanager Quickshell)
install_qml_module(quickshell-windowmanager)
qs_module_pch(quickshell-windowmanager SET large)
target_link_libraries(quickshell-windowmanager PRIVATE Qt::Quick)
target_link_libraries(quickshell PRIVATE quickshell-windowmanagerplugin)

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

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

View file

@ -0,0 +1,41 @@
#include "windowmanager.hpp"
#include <functional>
#include <utility>
#include <qobject.h>
#include "../core/qmlscreen.hpp"
#include "screenprojection.hpp"
namespace qs::wm {
std::function<WindowManager*()> WindowManager::provider;
void WindowManager::setProvider(std::function<WindowManager*()> provider) {
WindowManager::provider = std::move(provider);
}
WindowManager* WindowManager::instance() {
static auto* instance = WindowManager::provider();
return instance;
}
ScreenProjection* WindowManager::screenProjection(QuickshellScreenInfo* screen) {
auto* qscreen = screen->screen;
auto it = this->mScreenProjections.find(qscreen);
if (it != this->mScreenProjections.end()) {
return *it;
}
auto* projection = new ScreenProjection(qscreen, this);
this->mScreenProjections.insert(qscreen, projection);
QObject::connect(qscreen, &QObject::destroyed, this, [this, projection, qscreen]() {
this->mScreenProjections.remove(qscreen);
delete projection;
});
return projection;
}
} // namespace qs::wm

View file

@ -0,0 +1,91 @@
#pragma once
#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/qmlscreen.hpp"
#include "screenprojection.hpp"
#include "windowset.hpp"
namespace qs::wm {
class WindowManager: public QObject {
Q_OBJECT;
public:
static void setProvider(std::function<WindowManager*()> provider);
static WindowManager* instance();
Q_INVOKABLE ScreenProjection* screenProjection(QuickshellScreenInfo* screen);
[[nodiscard]] QBindable<QList<Windowset*>> bindableWindowsets() const {
return &this->bWindowsets;
}
[[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;
// 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:
/// Returns an internal WindowsetProjection that covers a single screen and contains all
/// windowsets on that screen, regardless of the WM-specified projection. Depending on
/// how the WM lays out its actual projections, multiple ScreenProjections may contain
/// the same Windowsets.
Q_INVOKABLE static ScreenProjection* screenProjection(QuickshellScreenInfo* screen) {
return WindowManager::instance()->screenProjection(screen);
}
[[nodiscard]] static QBindable<QList<Windowset*>> bindableWindowsets() {
return WindowManager::instance()->bindableWindowsets();
}
[[nodiscard]] static QBindable<QList<WindowsetProjection*>> bindableWindowsetProjections() {
return WindowManager::instance()->bindableWindowsetProjections();
}
};
} // namespace qs::wm

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

@ -17,7 +17,7 @@ qs_add_module_deps_light(quickshell-i3-ipc Quickshell)
install_qml_module(quickshell-i3-ipc) 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) qs_module_pch(quickshell-i3-ipc SET large)

View file

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

View file

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