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