Compare commits

...

26 commits

Author SHA1 Message Date
outfoxxed
b7005e09e7
docs: ask users not to submit v1 crash reports 2026-03-17 03:57:04 -07:00
-k
a51dcd0a01
wayland: use patched surfaceRole accessor on FreeBSD 2026-03-17 00:08:25 -07:00
-k
97b2688ad6
core/log: fix non-linux typo and import unistd on freebsd 2026-03-17 00:01:34 -07:00
-k
0a859d51f2
service/pam: include signal.h on freebsd 2026-03-17 00:01:24 -07:00
outfoxxed
1bd5b083cb
hyprland/ipc: add null checks and ws preinit to toplevel object init
Previously HyprlandToplevel::updateFromObject did not call
findWorkspaceByName with createIfMissing=true, leaving bWorkspace null
for a later insertToplevel call from HyprlandIpc::refreshToplevels.
2026-03-16 22:47:01 -07:00
outfoxxed
365bf16b1e
wayland: hook wl_proxy_get_listener avoiding QTBUG-145022 crash
Co-authored-by: Lemmy <studio@quadbyte.net>
2026-03-16 21:42:20 -07:00
outfoxxed
6705e2da77
wm: add WindowManager module with ext-workspace support 2026-03-16 01:08:26 -07:00
outfoxxed
9e8eecf2b8
core: log qt related environment variables in debuginfo 2026-03-15 21:13:35 -07:00
outfoxxed
1b2519d9f3
core: log gpu information in debuginfo 2026-03-14 02:31:47 -07:00
outfoxxed
1123d5ab4f
core: move crash/version debug info to one place 2026-03-14 02:31:28 -07:00
outfoxxed
4b77936c80
crash: allow overriding crash reporter url 2026-03-13 02:04:01 -07:00
outfoxxed
e32b909354
core: add disable env vars for file watcher and crash handler 2026-03-13 01:10:09 -07:00
outfoxxed
178c04b59c
docs: revise contribution policy and related files 2026-03-13 00:33:36 -07:00
outfoxxed
706d6de7b0
crash: unmask signals in coredump fork
Fixes the fork just sticking around and not dumping a core.
2026-03-12 04:02:24 -07:00
outfoxxed
9a9c605250
core: hash scanned files and don't trigger a reload if matching
Nix builds often trip QFileSystemWatcher, causing random reloads.
2026-03-11 21:52:13 -07:00
outfoxxed
bd62179277
all: retry incomplete socket reads
Fixes greetd and hyprland ipc sockets reads being incomplete and
breaking said integrations on slow machines.
2026-03-10 00:54:45 -07:00
-k
cf1a2aeb2d
wayland/toplevel: clear activeToplevel on deactivation 2026-03-09 19:37:15 -07:00
outfoxxed
15a8409765
ipc: handle null currentGeneration in IpcKillCommand::exec 2026-03-07 15:19:36 -08:00
Moraxyc
6bcd3d9bbf
nix: use libxcb directly 2026-03-06 03:15:20 -08:00
outfoxxed
c030300191
core/desktopentry: preserve desktop action order 2026-03-06 01:40:02 -08:00
outfoxxed
5721955686
services/pipewire: ignore ENOENT errors
Pipewire describes all errors as fatal, however these just aren't,
don't seem to be squashable, and resetting for them breaks users.
2026-03-04 23:26:33 -08:00
outfoxxed
a849a88893
build: remove DISTRIBUTOR_DEBUGINFO_AVAILABLE 2026-03-03 00:40:36 -08:00
outfoxxed
cdde4c63f4
crash: switch to cpptrace from breakpad 2026-03-02 19:35:38 -08:00
Carson Powers
cddb4f061b
build: fix lint-staged to ignore deleted files
Some checks failed
Build / Nix-6 (push) Has been cancelled
Build / Nix-7 (push) Has been cancelled
Build / Nix-8 (push) Has been cancelled
Build / Nix-9 (push) Has been cancelled
Build / Nix-10 (push) Has been cancelled
Build / Nix-11 (push) Has been cancelled
Build / Nix-12 (push) Has been cancelled
Build / Nix-13 (push) Has been cancelled
Build / Nix-14 (push) Has been cancelled
Build / Nix-15 (push) Has been cancelled
Build / Nix-16 (push) Has been cancelled
Build / Nix-17 (push) Has been cancelled
Build / Nix-18 (push) Has been cancelled
Build / Nix-19 (push) Has been cancelled
Build / Nix-20 (push) Has been cancelled
Build / Nix-21 (push) Has been cancelled
Build / Nix-22 (push) Has been cancelled
Build / Nix-23 (push) Has been cancelled
Build / Nix-24 (push) Has been cancelled
Build / Nix-25 (push) Has been cancelled
Build / Nix-26 (push) Has been cancelled
Build / Nix-27 (push) Has been cancelled
Build / Nix-28 (push) Has been cancelled
Build / Nix-29 (push) Has been cancelled
Build / Nix-30 (push) Has been cancelled
Build / Nix-31 (push) Has been cancelled
Build / Nix-32 (push) Has been cancelled
Build / Nix-33 (push) Has been cancelled
Build / Archlinux (push) Has been cancelled
Lint / Lint (push) Has been cancelled
2026-02-24 01:43:02 -08:00
outfoxxed
6e17efab83
wayland/screencopy: enable vulkan dmabuf support on session locks
Also reformat dmabuf
2026-02-24 00:05:20 -08:00
bbedward
36517a2c10
services/pipewire: manage default objs using normal qt properties
Fixes use after free bugs due to pointer mismatches in destructors.
Drops SimpleObjectHandle.
2026-02-23 23:17:42 -08:00
79 changed files with 2727 additions and 788 deletions

View file

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

View file

@ -1,82 +1,17 @@
name: Crash Report name: Crash Report (v1)
description: Quickshell has crashed description: Quickshell has crashed (old)
labels: ["bug", "crash"] labels: ["bug", "crash"]
body: body:
- type: textarea - type: markdown
id: crashinfo
attributes: attributes:
label: General crash information value: |
description: | Thank you for taking the time to click the report button.
Paste the contents of the `info.txt` file in your crash folder here. At this point most of the worst issues in 0.2.1 and before have been fixed and we are
value: "<details> <summary>General information</summary> preparing for a new release. Please do not report crashes from 0.2.1 or before for now.
- type: checkboxes
id: donotcheck
``` attributes:
label: Do not check this box
<Paste the contents of the file here inside of the triple backticks> options:
- label: Do not check this box
```
</details>"
validations:
required: true required: true
- 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: 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 <path-to-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 <pid>` 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.

49
.github/ISSUE_TEMPLATE/crash2.yml vendored Normal file
View file

@ -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 <path-to-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 <pid>` 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.

View file

@ -55,10 +55,11 @@ jobs:
libpipewire \ libpipewire \
cli11 \ cli11 \
polkit \ polkit \
jemalloc jemalloc \
libunwind \
git # for cpptrace clone
- name: Build - name: Build
# breakpad is annoying to build in ci due to makepkg not running as root
run: | run: |
cmake -GNinja -B build -DCRASH_REPORTER=OFF cmake -GNinja -B build -DVENDOR_CPPTRACE=ON
cmake --build build cmake --build build

View file

@ -15,15 +15,7 @@ Please make this descriptive enough to identify your specific package, for examp
- `Nixpkgs` - `Nixpkgs`
- `Fedora COPR (errornointernet/quickshell)` - `Fedora COPR (errornointernet/quickshell)`
`-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=YES/NO` If you are forking quickshell, please change `CRASHREPORT_URL` to your own issue tracker.
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).
### QML Module dir ### QML Module dir
Currently all QML modules are statically linked to quickshell, but this is where Currently all QML modules are statically linked to quickshell, but this is where
@ -41,6 +33,7 @@ Quickshell has a set of base dependencies you will always need, names vary by di
- `cmake` - `cmake`
- `qt6base` - `qt6base`
- `qt6declarative` - `qt6declarative`
- `libdrm`
- `qtshadertools` (build-time) - `qtshadertools` (build-time)
- `spirv-tools` (build-time) - `spirv-tools` (build-time)
- `pkg-config` (build-time) - `pkg-config` (build-time)
@ -64,14 +57,24 @@ At least Qt 6.6 is required.
All features are enabled by default and some have their own dependencies. All features are enabled by default and some have their own dependencies.
### Crash Reporter ### Crash Handler
The crash reporter catches crashes, restarts quickshell when it crashes, The crash reporter catches crashes, restarts Quickshell when it crashes,
and collects useful crash information in one place. Leaving this enabled will and collects useful crash information in one place. Leaving this enabled will
enable us to fix bugs far more easily. 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 ### Jemalloc
We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused
@ -144,7 +147,6 @@ Enables streaming video from monitors and toplevel windows through various proto
To disable: `-DSCREENCOPY=OFF` To disable: `-DSCREENCOPY=OFF`
Dependencies: Dependencies:
- `libdrm`
- `libgbm` - `libgbm`
- `vulkan-headers` (build-time) - `vulkan-headers` (build-time)
@ -240,7 +242,7 @@ Only `ninja` builds are tested, but makefiles may work.
#### Configuring the build #### Configuring the build
```sh ```sh
$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here] $ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release [additional disable flags from above here]
``` ```
Note that features you do not supply dependencies for MUST be disabled with their associated flags Note that features you do not supply dependencies for MUST be disabled with their associated flags

View file

@ -9,6 +9,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(QS_BUILD_OPTIONS "") set(QS_BUILD_OPTIONS "")
# should be changed for forks
set(CRASHREPORT_URL "https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml" CACHE STRING "Bugreport URL")
function(boption VAR NAME DEFAULT) function(boption VAR NAME DEFAULT)
cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "") cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "")
@ -40,19 +43,17 @@ string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}")
message(STATUS "Quickshell configuration") message(STATUS "Quickshell configuration")
message(STATUS " Distributor: ${DISTRIBUTOR}") message(STATUS " Distributor: ${DISTRIBUTOR}")
boption(DISTRIBUTOR_DEBUGINFO_AVAILABLE "Distributor provided debuginfo" NO)
boption(NO_PCH "Disable precompild headers (dev)" OFF) boption(NO_PCH "Disable precompild headers (dev)" OFF)
boption(BUILD_TESTING "Build tests (dev)" OFF) boption(BUILD_TESTING "Build tests (dev)" OFF)
boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang
boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN}) boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN})
if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
boption(CRASH_REPORTER "Crash Handling" OFF)
boption(USE_JEMALLOC "Use jemalloc" OFF) boption(USE_JEMALLOC "Use jemalloc" OFF)
else() else()
boption(CRASH_REPORTER "Crash Handling" ON)
boption(USE_JEMALLOC "Use jemalloc" ON) boption(USE_JEMALLOC "Use jemalloc" ON)
endif() endif()
boption(CRASH_HANDLER "Crash Handling" ON)
boption(SOCKETS "Unix Sockets" ON) boption(SOCKETS "Unix Sockets" ON)
boption(WAYLAND "Wayland" ON) boption(WAYLAND "Wayland" ON)
boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND) boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND)

View file

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

226
HACKING.md Normal file
View file

@ -0,0 +1,226 @@
## Development
Install the dependencies listed in [BUILD.md](BUILD.md).
You probably want all of them even if you don't use all of them
to ensure tests work correctly and avoid passing a bunch of configure
flags when you need to wipe the build directory.
The dependencies are also available as a nix shell or nix flake which we recommend
using with nix-direnv.
Quickshell uses `just` for common development command aliases.
Common aliases:
- `just configure [<debug|release> [extra cmake args]]` (note that you must specify debug/release to specify extra args)
- `just build` - runs the build, configuring if not configured already.
- `just run [args]` - runs quickshell with the given arguments
- `just clean` - clean up build artifacts. `just clean build` is somewhat common.
### Formatting
All contributions should be formatted similarly to what already exists.
Group related functionality together.
Run the formatter using `just fmt`.
If the results look stupid, fix the clang-format file if possible,
or disable clang-format in the affected area
using `// clang-format off` and `// clang-format on`.
#### Style preferences not caught by clang-format
These are flexible. You can ignore them if it looks or works better to
for one reason or another.
Use `auto` if the type of a variable can be deduced automatically, instead of
redeclaring the returned value's type. Additionally, auto should be used when a
constructor takes arguments.
```cpp
auto x = <expr>; // ok
auto x = QString::number(3); // ok
QString x; // ok
QString x = "foo"; // ok
auto x = QString("foo"); // ok
auto x = QString(); // avoid
QString x(); // avoid
QString x("foo"); // avoid
```
Put newlines around logical units of code, and after closing braces. If the
most reasonable logical unit of code takes only a single line, it should be
merged into the next single line logical unit if applicable.
```cpp
// multiple units
auto x = <expr>; // unit 1
auto y = <expr>; // unit 2
auto x = <expr>; // unit 1
emit this->y(); // unit 2
auto x1 = <expr>; // unit 1
auto x2 = <expr>; // unit 1
auto x3 = <expr>; // unit 1
auto y1 = <expr>; // unit 2
auto y2 = <expr>; // unit 2
auto y3 = <expr>; // unit 2
// one unit
auto x = <expr>;
if (x...) {
// ...
}
// if more than one variable needs to be used then add a newline
auto x = <expr>;
auto y = <expr>;
if (x && y) {
// ...
}
```
Class formatting:
```cpp
//! Doc comment summary
/// Doc comment body
class Foo: public QObject {
// The Q_OBJECT macro comes first. Macros are ; terminated.
Q_OBJECT;
QML_ELEMENT;
QML_CLASSINFO(...);
// Properties must stay on a single line or the doc generator won't be able to pick them up
Q_PROPERTY(...);
/// Doc comment
Q_PROPERTY(...);
/// Doc comment
Q_PROPERTY(...);
public:
// Classes should have explicit constructors if they aren't intended to
// implicitly cast. The constructor can be inline in the header if it has no body.
explicit Foo(QObject* parent = nullptr): QObject(parent) {}
// Instance functions if applicable.
static Foo* instance();
// Member functions unrelated to properties come next
void function();
void function();
void function();
// Then Q_INVOKABLEs
Q_INVOKABLE function();
/// Doc comment
Q_INVOKABLE function();
/// Doc comment
Q_INVOKABLE function();
// Then property related functions, in the order (bindable, getter, setter).
// Related functions may be included here as well. Function bodies may be inline
// if they are a single expression. There should be a newline between each
// property's methods.
[[nodiscard]] QBindable<T> bindableFoo() { return &this->bFoo; }
[[nodiscard]] T foo() const { return this->foo; }
void setFoo();
[[nodiscard]] T bar() const { return this->foo; }
void setBar();
signals:
// Signals that are not property change related go first.
// Property change signals go in property definition order.
void asd();
void asd2();
void fooChanged();
void barChanged();
public slots:
// generally Q_INVOKABLEs are preferred to public slots.
void slot();
private slots:
// ...
private:
// statics, then functions, then fields
static const foo BAR;
static void foo();
void foo();
void bar();
// property related members are prefixed with `m`.
QString mFoo;
QString bar;
// Bindables go last and should be prefixed with `b`.
Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged);
};
```
Use lowercase .h suffixed Qt headers, e.g. `<qwindow.h>` over `<QWindow>`.
### Linter
All contributions should pass the linter.
Note that running the linter requires disabling precompiled
headers and including the test codepaths:
```sh
$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
$ just lint-changed
```
If the linter is complaining about something that you think it should not,
please disable the lint in your MR and explain your reasoning if it isn't obvious.
### Tests
If you feel like the feature you are working on is very complex or likely to break,
please write some tests. We will ask you to directly if you send in an MR for an
overly complex or breakable feature.
At least all tests that passed before your changes should still be passing
by the time your contribution is ready.
You can run the tests using `just test` but you must enable them first
using `-DBUILD_TESTING=ON`.
### Documentation
Most of quickshell's documentation is automatically generated from the source code.
You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser
cannot handle random line breaks and will usually require you to disable clang-format if the
lines are too long.
Make sure new files containing doc comments are added to a `module.md` file.
See existing module files for reference.
Doc comments take the form `///` or `///!` (summary) and work with markdown.
You can reference other types using the `@@[Module.][Type.][member]` shorthand
where all parts are optional. If module or type are not specified they will
be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`.
Look at existing code for how it works.
If you have made a user visible change since the last tagged release, describe it in
[changelog/next.md](changelog/next.md).
## Contributing
### Commits
Please structure your commit messages as `scope: commit` where
the scope is something like `core` or `service/mpris`. (pick what has been
used historically or what makes sense if new).
Commit descriptions should contain a summary of the changes if they are not
sufficiently addressed in the commit message.
Please squash/rebase additions or edits to previous changes and follow the
commit style to keep the history easily searchable at a glance.
Depending on the change, it is often reasonable to squash it into just
a single commit. (If you do not follow this we will squash your changes
for you.)
### Getting help
If you're getting stuck, you can come talk to us in the
[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me)
for help on implementation, conventions, etc. There is also a bridged [discord server](https://discord.gg/UtZeT3xNyT).
Feel free to ask for advice early in your implementation if you are
unsure.

View file

@ -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") }} git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
lint-staged: 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='': configure target='debug' *FLAGS='':
cmake -GNinja -B {{builddir}} \ cmake -GNinja -B {{builddir}} \

View file

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

View file

@ -26,12 +26,18 @@ set shell id.
- Added Quickshell version checking and version gated preprocessing. - Added Quickshell version checking and version gated preprocessing.
- Added a way to detect if an icon is from the system icon theme or not. - Added a way to detect if an icon is from the system icon theme or not.
- Added vulkan support to screencopy. - Added vulkan support to screencopy.
- Added generic WindowManager interface implementing ext-workspace.
## Other Changes ## Other Changes
- FreeBSD is now partially supported. - FreeBSD is now partially supported.
- IPC operations filter available instances to the current display connection by default. - IPC operations filter available instances to the current display connection by default.
- PwNodeLinkTracker ignores sound level monitoring programs. - PwNodeLinkTracker ignores sound level monitoring programs.
- Replaced breakpad with cpptrace.
- Reloads are prevented if no file content has changed.
- Added `QS_DISABLE_FILE_WATCHER` environment variable to disable file watching.
- Added `QS_DISABLE_CRASH_HANDLER` environment variable to disable crash handling.
- Added `QS_CRASHREPORT_URL` environment variable to allow overriding the crash reporter link.
## Bug Fixes ## Bug Fixes
@ -42,14 +48,23 @@ set shell id.
- Fixed volumes not initializing if a pipewire device was already loaded before its node. - 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 active toplevel not resetting after window closes.
- Fixed hyprland ipc window names and titles being reversed. - 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 missing signals for system tray item title and description updates.
- Fixed asynchronous loaders not working after reload. - Fixed asynchronous loaders not working after reload.
- Fixed asynchronous loaders not working before window creation. - Fixed asynchronous loaders not working before window creation.
- Fixed memory leak in IPC handlers. - Fixed memory leak in IPC handlers.
- Fixed ClippingRectangle related crashes. - Fixed ClippingRectangle related crashes.
- Fixed crashes when monitors are unplugged. - Fixed crashes when monitors are unplugged.
- Fixed crashes when default pipewire devices are lost.
- Fixed 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 ## Packaging Changes
`glib` and `polkit` have been added as dependencies when compiling with polkit agent 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). - `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support).
- `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore.
- `DISTRIBUTOR_DEBUGINFO_AVAILABLE` was removed as it is no longer important without breakpad.
- `libdrm` is now unconditionally required as a direct dependency.

View file

@ -10,13 +10,16 @@
ninja, ninja,
spirv-tools, spirv-tools,
qt6, qt6,
breakpad, cpptrace ? null,
libunwind,
libdwarf,
jemalloc, jemalloc,
cli11, cli11,
wayland, wayland,
wayland-protocols, wayland-protocols,
wayland-scanner, wayland-scanner,
xorg, xorg,
libxcb ? xorg.libxcb,
libdrm, libdrm,
libgbm ? null, libgbm ? null,
vulkan-headers, vulkan-headers,
@ -49,6 +52,8 @@
withPolkit ? true, withPolkit ? true,
withNetworkManager ? true, withNetworkManager ? true,
}: let }: let
withCrashHandler = withCrashReporter && cpptrace != null && lib.strings.compareVersions cpptrace.version "0.7.2" >= 0;
unwrapped = stdenv.mkDerivation { unwrapped = stdenv.mkDerivation {
pname = "quickshell${lib.optionalString debug "-debug"}"; pname = "quickshell${lib.optionalString debug "-debug"}";
version = "0.2.1"; version = "0.2.1";
@ -71,15 +76,21 @@
buildInputs = [ buildInputs = [
qt6.qtbase qt6.qtbase
qt6.qtdeclarative qt6.qtdeclarative
libdrm
cli11 cli11
] ]
++ lib.optional withQtSvg qt6.qtsvg ++ 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 withJemalloc jemalloc
++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland ++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
++ lib.optionals withWayland [ wayland wayland-protocols ] ++ lib.optionals withWayland [ wayland wayland-protocols ]
++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm vulkan-headers ] ++ lib.optionals (withWayland && libgbm != null) [ libgbm vulkan-headers ]
++ lib.optional withX11 xorg.libxcb ++ lib.optional withX11 libxcb
++ lib.optional withPam pam ++ lib.optional withPam pam
++ lib.optional withPipewire pipewire ++ lib.optional withPipewire pipewire
++ lib.optionals withPolkit [ polkit glib ]; ++ lib.optionals withPolkit [ polkit glib ];
@ -91,7 +102,7 @@
(lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix) (lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
(lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true) (lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true)
(lib.cmakeFeature "GIT_REVISION" gitRev) (lib.cmakeFeature "GIT_REVISION" gitRev)
(lib.cmakeBool "CRASH_REPORTER" withCrashReporter) (lib.cmakeBool "CRASH_HANDLER" withCrashHandler)
(lib.cmakeBool "USE_JEMALLOC" withJemalloc) (lib.cmakeBool "USE_JEMALLOC" withJemalloc)
(lib.cmakeBool "WAYLAND" withWayland) (lib.cmakeBool "WAYLAND" withWayland)
(lib.cmakeBool "SCREENCOPY" (libgbm != null)) (lib.cmakeBool "SCREENCOPY" (libgbm != null))

View file

@ -56,8 +56,7 @@
#~(list "-GNinja" #~(list "-GNinja"
"-DDISTRIBUTOR=\"In-tree Guix channel\"" "-DDISTRIBUTOR=\"In-tree Guix channel\""
"-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO" "-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO"
;; Breakpad is not currently packaged for Guix. "-DCRASH_HANDLER=OFF")
"-DCRASH_REPORTER=OFF")
#:phases #:phases
#~(modify-phases %standard-phases #~(modify-phases %standard-phases
(replace 'build (lambda _ (invoke "cmake" "--build" "."))) (replace 'build (lambda _ (invoke "cmake" "--build" ".")))

View file

@ -11,8 +11,9 @@ add_subdirectory(window)
add_subdirectory(io) add_subdirectory(io)
add_subdirectory(widgets) add_subdirectory(widgets)
add_subdirectory(ui) add_subdirectory(ui)
add_subdirectory(windowmanager)
if (CRASH_REPORTER) if (CRASH_HANDLER)
add_subdirectory(crash) add_subdirectory(crash)
endif() endif()

View file

@ -9,16 +9,10 @@ if (NOT DEFINED GIT_REVISION)
) )
endif() endif()
if (CRASH_REPORTER) if (CRASH_HANDLER)
set(CRASH_REPORTER_DEF 1) set(CRASH_HANDLER_DEF 1)
else() else()
set(CRASH_REPORTER_DEF 0) set(CRASH_HANDLER_DEF 0)
endif()
if (DISTRIBUTOR_DEBUGINFO_AVAILABLE)
set(DEBUGINFO_AVAILABLE 1)
else()
set(DEBUGINFO_AVAILABLE 0)
endif() endif()
configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES) configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES)

View file

@ -8,10 +8,10 @@
#define QS_UNRELEASED_FEATURES "@UNRELEASED_FEATURES@" #define QS_UNRELEASED_FEATURES "@UNRELEASED_FEATURES@"
#define GIT_REVISION "@GIT_REVISION@" #define GIT_REVISION "@GIT_REVISION@"
#define DISTRIBUTOR "@DISTRIBUTOR@" #define DISTRIBUTOR "@DISTRIBUTOR@"
#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@ #define CRASH_HANDLER @CRASH_HANDLER_DEF@
#define CRASH_REPORTER @CRASH_REPORTER_DEF@
#define BUILD_TYPE "@CMAKE_BUILD_TYPE@" #define BUILD_TYPE "@CMAKE_BUILD_TYPE@"
#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)" #define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)"
#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@" #define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@"
#define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@" #define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@"
#define CRASHREPORT_URL "@CRASHREPORT_URL@"
// NOLINTEND // NOLINTEND

View file

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

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

@ -0,0 +1,175 @@
#include "debuginfo.hpp"
#include <array>
#include <cstring>
#include <string_view>
#include <fcntl.h>
#include <qconfig.h>
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qfile.h>
#include <qfloat16.h>
#include <qhashfunctions.h>
#include <qscopeguard.h>
#include <qtversion.h>
#include <unistd.h>
#include <xf86drm.h>
#include "build.hpp"
extern char** environ; // NOLINT
namespace qs::debuginfo {
QString qsVersion() {
return QS_VERSION " (revision " GIT_REVISION ", distributed by " DISTRIBUTOR ")";
}
QString qtVersion() { return qVersion() % QStringLiteral(" (built against " QT_VERSION_STR ")"); }
QString gpuInfo() {
auto deviceCount = drmGetDevices2(0, nullptr, 0);
if (deviceCount < 0) return "Failed to get DRM device count: " % QString::number(deviceCount);
auto* devices = new drmDevicePtr[deviceCount];
auto devicesArrayGuard = qScopeGuard([&] { delete[] devices; });
auto r = drmGetDevices2(0, devices, deviceCount);
if (deviceCount < 0) return "Failed to get DRM devices: " % QString::number(r);
auto devicesGuard = qScopeGuard([&] {
for (auto i = 0; i != deviceCount; ++i) drmFreeDevice(&devices[i]); // NOLINT
});
QString info;
auto stream = QTextStream(&info);
for (auto i = 0; i != deviceCount; ++i) {
auto* device = devices[i]; // NOLINT
int deviceNodeType = -1;
if (device->available_nodes & (1 << DRM_NODE_RENDER)) deviceNodeType = DRM_NODE_RENDER;
else if (device->available_nodes & (1 << DRM_NODE_PRIMARY)) deviceNodeType = DRM_NODE_PRIMARY;
if (deviceNodeType == -1) continue;
auto* deviceNode = device->nodes[DRM_NODE_RENDER]; // NOLINT
auto driver = [&]() -> QString {
auto fd = open(deviceNode, O_RDWR | O_CLOEXEC);
if (fd == -1) return "<failed to open device node>";
auto fdGuard = qScopeGuard([&] { close(fd); });
auto* ver = drmGetVersion(fd);
if (!ver) return "<drmGetVersion failed>";
auto verGuard = qScopeGuard([&] { drmFreeVersion(ver); });
// clang-format off
return QString(ver->name)
% ' ' % QString::number(ver->version_major)
% '.' % QString::number(ver->version_minor)
% '.' % QString::number(ver->version_patchlevel)
% " (" % ver->desc % ')';
// clang-format on
}();
QString product = "unknown";
QString address = "unknown";
auto hex = [](int num, int pad) { return QString::number(num, 16).rightJustified(pad, '0'); };
switch (device->bustype) {
case DRM_BUS_PCI: {
auto* b = device->businfo.pci;
auto* d = device->deviceinfo.pci;
address = "PCI " % hex(b->bus, 2) % ':' % hex(b->dev, 2) % '.' % hex(b->func, 1);
product = hex(d->vendor_id, 4) % ':' % hex(d->device_id, 4);
} break;
case DRM_BUS_USB: {
auto* b = device->businfo.usb;
auto* d = device->deviceinfo.usb;
address = "USB " % QString::number(b->bus) % ':' % QString::number(b->dev);
product = hex(d->vendor, 4) % ':' % hex(d->product, 4);
} break;
default: break;
}
stream << "GPU " << deviceNode << "\n Driver: " << driver << "\n Model: " << product
<< "\n Address: " << address << '\n';
}
return info;
}
QString systemInfo() {
QString info;
auto stream = QTextStream(&info);
stream << gpuInfo() << '\n';
stream << "/etc/os-release:";
auto osReleaseFile = QFile("/etc/os-release");
if (osReleaseFile.open(QFile::ReadOnly)) {
stream << '\n' << osReleaseFile.readAll() << '\n';
osReleaseFile.close();
} else {
stream << "FAILED TO OPEN\n";
}
stream << "/etc/lsb-release:";
auto lsbReleaseFile = QFile("/etc/lsb-release");
if (lsbReleaseFile.open(QFile::ReadOnly)) {
stream << '\n' << lsbReleaseFile.readAll();
lsbReleaseFile.close();
} else {
stream << "FAILED TO OPEN\n";
}
return info;
}
QString envInfo() {
QString info;
auto stream = QTextStream(&info);
for (auto** envp = environ; *envp != nullptr; ++envp) { // NOLINT
auto prefixes = std::array<std::string_view, 5> {
"QS_",
"QT_",
"QML_",
"QML2_",
"QSG_",
};
for (const auto& prefix: prefixes) {
if (strncmp(prefix.data(), *envp, prefix.length()) == 0) goto print;
}
continue;
print:
stream << *envp << '\n';
}
return info;
}
QString combinedInfo() {
QString info;
auto stream = QTextStream(&info);
stream << "===== Version Information =====\n";
stream << "Quickshell: " << qsVersion() << '\n';
stream << "Qt: " << qtVersion() << '\n';
stream << "\n===== Build Information =====\n";
stream << "Build Type: " << BUILD_TYPE << '\n';
stream << "Compiler: " << COMPILER << '\n';
stream << "Compile Flags: " << COMPILE_FLAGS << '\n';
stream << "Configuration:\n" << BUILD_CONFIGURATION << '\n';
stream << "\n===== System Information =====\n";
stream << systemInfo();
stream << "\n===== Environment (trimmed) =====\n";
stream << envInfo();
return info;
}
} // namespace qs::debuginfo

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

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

View file

@ -107,7 +107,10 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString&
auto groupName = QString(); auto groupName = QString();
auto entries = QHash<QString, QPair<Locale, QString>>(); auto entries = QHash<QString, QPair<Locale, QString>>();
auto finishCategory = [&data, &groupName, &entries]() { auto actionOrder = QStringList();
auto pendingActions = QHash<QString, DesktopActionData>();
auto finishCategory = [&data, &groupName, &entries, &actionOrder, &pendingActions]() {
if (groupName == "Desktop Entry") { if (groupName == "Desktop Entry") {
if (entries.value("Type").second != "Application") return; 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 == "Terminal") data.terminal = value == "true";
else if (key == "Categories") data.categories = value.split(u';', Qt::SkipEmptyParts); 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 == "Keywords") data.keywords = value.split(u';', Qt::SkipEmptyParts);
else if (key == "Actions") actionOrder = value.split(u';', Qt::SkipEmptyParts);
} }
} else if (groupName.startsWith("Desktop Action ")) { } else if (groupName.startsWith("Desktop Action ")) {
auto actionName = groupName.sliced(16); auto actionName = groupName.sliced(15);
DesktopActionData action; DesktopActionData action;
action.id = actionName; 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(); entries.clear();
@ -193,6 +197,13 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString&
} }
finishCategory(); finishCategory();
for (const auto& actionId: actionOrder) {
if (pendingActions.contains(actionId)) {
data.actions.append(pendingActions.value(actionId));
}
}
return data; return data;
} }
@ -216,17 +227,18 @@ void DesktopEntry::updateState(const ParsedDesktopEntryData& newState) {
this->updateActions(newState.actions); this->updateActions(newState.actions);
} }
void DesktopEntry::updateActions(const QHash<QString, DesktopActionData>& newActions) { void DesktopEntry::updateActions(const QVector<DesktopActionData>& newActions) {
auto old = this->mActions; auto old = this->mActions;
this->mActions.clear();
for (const auto& [key, d]: newActions.asKeyValueRange()) { for (const auto& d: newActions) {
DesktopAction* act = nullptr; DesktopAction* act = nullptr;
if (auto found = old.find(key); found != old.end()) { auto found = std::ranges::find(old, d.id, &DesktopAction::mId);
act = found.value(); if (found != old.end()) {
act = *found;
old.erase(found); old.erase(found);
} else { } else {
act = new DesktopAction(d.id, this); act = new DesktopAction(d.id, this);
this->mActions.insert(key, act);
} }
Qt::beginPropertyUpdateGroup(); Qt::beginPropertyUpdateGroup();
@ -237,6 +249,7 @@ void DesktopEntry::updateActions(const QHash<QString, DesktopActionData>& newAct
Qt::endPropertyUpdateGroup(); Qt::endPropertyUpdateGroup();
act->mEntries = d.entries; act->mEntries = d.entries;
this->mActions.append(act);
} }
for (auto* leftover: old) { for (auto* leftover: old) {
@ -250,7 +263,7 @@ void DesktopEntry::execute() const {
bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); } bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); }
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); } QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions; }
QVector<QString> DesktopEntry::parseExecString(const QString& execString) { QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
QVector<QString> arguments; QVector<QString> arguments;

View file

@ -43,7 +43,7 @@ struct ParsedDesktopEntryData {
QVector<QString> categories; QVector<QString> categories;
QVector<QString> keywords; QVector<QString> keywords;
QHash<QString, QString> entries; QHash<QString, QString> entries;
QHash<QString, DesktopActionData> actions; QVector<DesktopActionData> actions;
}; };
/// A desktop entry. See @@DesktopEntries for details. /// A desktop entry. See @@DesktopEntries for details.
@ -164,10 +164,10 @@ public:
// clang-format on // clang-format on
private: private:
void updateActions(const QHash<QString, DesktopActionData>& newActions); void updateActions(const QVector<DesktopActionData>& newActions);
ParsedDesktopEntryData state; ParsedDesktopEntryData state;
QHash<QString, DesktopAction*> mActions; QVector<DesktopAction*> mActions;
friend class DesktopAction; friend class DesktopAction;
}; };

View file

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

View file

@ -35,6 +35,8 @@ namespace qs::crash {
struct CrashInfo { struct CrashInfo {
int logFd = -1; int logFd = -1;
int traceFd = -1;
int infoFd = -1;
static CrashInfo INSTANCE; // NOLINT static CrashInfo INSTANCE; // NOLINT
}; };

View file

@ -31,6 +31,9 @@
#include <sys/sendfile.h> #include <sys/sendfile.h>
#include <sys/types.h> #include <sys/types.h>
#endif #endif
#ifdef __FreeBSD__
#include <unistd.h>
#endif
#include "instanceinfo.hpp" #include "instanceinfo.hpp"
#include "logcat.hpp" #include "logcat.hpp"
@ -67,7 +70,7 @@ bool copyFileData(int sourceFd, int destFd, qint64 size) {
return true; return true;
#else #else
std::array<char, 64 * 1024> buffer = {}; std::array<char, 64 * 1024> buffer = {};
auto remaining = totalTarget; auto remaining = usize;
while (remaining > 0) { while (remaining > 0) {
auto chunk = std::min(remaining, buffer.size()); auto chunk = std::min(remaining, buffer.size());

View file

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

View file

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

View file

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

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

@ -0,0 +1,98 @@
#include "streamreader.hpp"
#include <cstring>
#include <qbytearray.h>
#include <qiodevice.h>
#include <qtypes.h>
void StreamReader::setDevice(QIODevice* device) {
this->reset();
this->device = device;
}
void StreamReader::startTransaction() {
this->cursor = 0;
this->failed = false;
}
bool StreamReader::fill() {
auto available = this->device->bytesAvailable();
if (available <= 0) return false;
auto oldSize = this->buffer.size();
this->buffer.resize(oldSize + available);
auto bytesRead = this->device->read(this->buffer.data() + oldSize, available); // NOLINT
if (bytesRead <= 0) {
this->buffer.resize(oldSize);
return false;
}
this->buffer.resize(oldSize + bytesRead);
return true;
}
QByteArray StreamReader::readBytes(qsizetype count) {
if (this->failed) return {};
auto needed = this->cursor + count;
while (this->buffer.size() < needed) {
if (!this->fill()) {
this->failed = true;
return {};
}
}
auto result = this->buffer.mid(this->cursor, count);
this->cursor += count;
return result;
}
QByteArray StreamReader::readUntil(char terminator) {
if (this->failed) return {};
auto searchFrom = this->cursor;
auto idx = this->buffer.indexOf(terminator, searchFrom);
while (idx == -1) {
searchFrom = this->buffer.size();
if (!this->fill()) {
this->failed = true;
return {};
}
idx = this->buffer.indexOf(terminator, searchFrom);
}
auto length = idx - this->cursor + 1;
auto result = this->buffer.mid(this->cursor, length);
this->cursor += length;
return result;
}
void StreamReader::readInto(char* ptr, qsizetype count) {
auto data = this->readBytes(count);
if (!data.isEmpty()) memcpy(ptr, data.data(), count);
}
qint32 StreamReader::readI32() {
qint32 value = 0;
this->readInto(reinterpret_cast<char*>(&value), sizeof(qint32));
return value;
}
bool StreamReader::commitTransaction() {
if (this->failed) {
this->cursor = 0;
return false;
}
this->buffer.remove(0, this->cursor);
this->cursor = 0;
return true;
}
void StreamReader::reset() {
this->buffer.clear();
this->cursor = 0;
}

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

@ -0,0 +1,26 @@
#pragma once
#include <qbytearray.h>
#include <qiodevice.h>
#include <qtypes.h>
class StreamReader {
public:
void setDevice(QIODevice* device);
void startTransaction();
QByteArray readBytes(qsizetype count);
QByteArray readUntil(char terminator);
void readInto(char* ptr, qsizetype count);
qint32 readI32();
bool commitTransaction();
void reset();
private:
bool fill();
QIODevice* device = nullptr;
QByteArray buffer;
qsizetype cursor = 0;
bool failed = false;
};

View file

@ -251,37 +251,6 @@ public:
GuardedEmitBlocker block() { return GuardedEmitBlocker(&this->blocked); } GuardedEmitBlocker block() { return GuardedEmitBlocker(&this->blocked); }
}; };
template <auto member, auto destroyedSlot, auto changedSignal>
class SimpleObjectHandleOps {
using Traits = MemberPointerTraits<decltype(member)>;
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 <auto member, auto destroyedSlot, auto changedSignal = nullptr>
bool setSimpleObjectHandle(auto* parent, auto* value) {
return SimpleObjectHandleOps<member, destroyedSlot, changedSignal>::setObject(parent, value);
}
template <auto methodPtr> template <auto methodPtr>
class MethodFunctor { class MethodFunctor {
using PtrMeta = MemberPointerTraits<decltype(methodPtr)>; using PtrMeta = MemberPointerTraits<decltype(methodPtr)>;

View file

@ -6,12 +6,51 @@ qt_add_library(quickshell-crash STATIC
qs_pch(quickshell-crash SET large) qs_pch(quickshell-crash SET large)
find_package(PkgConfig REQUIRED) if (VENDOR_CPPTRACE)
pkg_check_modules(breakpad REQUIRED IMPORTED_TARGET breakpad) message(STATUS "Vendoring cpptrace...")
# only need client?? take only includes from pkg config todo include(FetchContent)
target_link_libraries(quickshell-crash PRIVATE PkgConfig::breakpad -lbreakpad_client)
# 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 <cpptrace/basic.hpp>
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 # 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) target_link_libraries(quickshell PRIVATE quickshell-crash)

View file

@ -1,12 +1,13 @@
#include "handler.hpp" #include "handler.hpp"
#include <algorithm>
#include <array> #include <array>
#include <cerrno>
#include <csignal>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <bits/types/sigset_t.h> #include <cpptrace/basic.hpp>
#include <breakpad/client/linux/handler/exception_handler.h> #include <cpptrace/forward.hpp>
#include <breakpad/client/linux/handler/minidump_descriptor.h>
#include <breakpad/common/linux/linux_libc_support.h>
#include <qdatastream.h> #include <qdatastream.h>
#include <qfile.h> #include <qfile.h>
#include <qlogging.h> #include <qlogging.h>
@ -19,98 +20,75 @@
extern char** environ; // NOLINT extern char** environ; // NOLINT
using namespace google_breakpad;
namespace qs::crash { namespace qs::crash {
namespace { namespace {
QS_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg); QS_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg);
}
struct CrashHandlerPrivate { void writeEnvInt(char* buf, const char* name, int value) {
ExceptionHandler* exceptionHandler = nullptr; // NOLINTBEGIN (cppcoreguidelines-pro-bounds-pointer-arithmetic)
int minidumpFd = -1; while (*name != '\0') *buf++ = *name++;
int infoFd = -1; *buf++ = '=';
static bool minidumpCallback(const MinidumpDescriptor& descriptor, void* context, bool succeeded); if (value < 0) {
}; *buf++ = '-';
value = -value;
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));
} }
qCInfo(logCrashHandler) << "Crash handler initialized."; if (value == 0) {
} *buf++ = '0';
*buf = '\0';
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.";
return; return;
} }
QFile file; auto* start = buf;
while (value > 0) {
if (!file.open(this->d->infoFd, QFile::ReadWrite)) { *buf++ = static_cast<char>('0' + (value % 10));
qCCritical( value /= 10;
logCrashHandler
) << "Failed to open instance info memfd, crash recovery will not work.";
} }
QDataStream ds(&file); *buf = '\0';
ds << info; std::reverse(start, buf);
file.flush(); // NOLINTEND
qCDebug(logCrashHandler) << "Stored instance info in memfd" << this->d->infoFd;
} }
CrashHandler::~CrashHandler() { void signalHandler(
delete this->d->exceptionHandler; int sig,
delete this->d; siginfo_t* /*info*/, // NOLINT (misc-include-cleaner)
} void* /*context*/
bool CrashHandlerPrivate::minidumpCallback(
const MinidumpDescriptor& /*descriptor*/,
void* context,
bool /*success*/
) { ) {
// A fork that just dies to ensure the coredump is caught by the system. if (CrashInfo::INSTANCE.traceFd != -1) {
auto coredumpPid = fork(); auto traceBuffer = std::array<cpptrace::frame_ptr, 1024>();
auto frameCount = cpptrace::safe_generate_raw_trace(traceBuffer.data(), traceBuffer.size(), 1);
if (coredumpPid == 0) { for (size_t i = 0; i < static_cast<size_t>(frameCount); i++) {
return false; auto frame = cpptrace::safe_object_frame();
cpptrace::get_safe_object_frame(traceBuffer[i], &frame);
auto* wptr = reinterpret_cast<char*>(&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
}
} }
auto* self = static_cast<CrashHandlerPrivate*>(context); fail:;
}
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<char, 4096>(); auto exe = std::array<char, 4096>();
if (readlink("/proc/self/exe", exe.data(), exe.size() - 1) == -1) { if (readlink("/proc/self/exe", exe.data(), exe.size() - 1) == -1) {
@ -123,17 +101,19 @@ bool CrashHandlerPrivate::minidumpCallback(
auto env = std::array<char*, 4096>(); auto env = std::array<char*, 4096>();
auto envi = 0; auto envi = 0;
auto infoFd = dup(self->infoFd); // dup to remove CLOEXEC
auto infoFdStr = std::array<char, 38>(); auto infoFdStr = std::array<char, 48>();
memcpy(infoFdStr.data(), "__QUICKSHELL_CRASH_INFO_FD=-1" /*\0*/, 30); writeEnvInt(infoFdStr.data(), "__QUICKSHELL_CRASH_INFO_FD", dup(CrashInfo::INSTANCE.infoFd));
if (infoFd != -1) my_uitos(&infoFdStr[27], infoFd, 10);
env[envi++] = infoFdStr.data(); env[envi++] = infoFdStr.data();
auto corePidStr = std::array<char, 39>(); auto corePidStr = std::array<char, 48>();
memcpy(corePidStr.data(), "__QUICKSHELL_CRASH_DUMP_PID=-1" /*\0*/, 31); writeEnvInt(corePidStr.data(), "__QUICKSHELL_CRASH_DUMP_PID", coredumpPid);
if (coredumpPid != -1) my_uitos(&corePidStr[28], coredumpPid, 10);
env[envi++] = corePidStr.data(); env[envi++] = corePidStr.data();
auto sigStr = std::array<char, 48>();
writeEnvInt(sigStr.data(), "__QUICKSHELL_CRASH_SIGNAL", sig);
env[envi++] = sigStr.data();
auto populateEnv = [&]() { auto populateEnv = [&]() {
auto senvi = 0; auto senvi = 0;
while (envi != 4095) { while (envi != 4095) {
@ -145,30 +125,18 @@ bool CrashHandlerPrivate::minidumpCallback(
env[envi] = nullptr; env[envi] = nullptr;
}; };
sigset_t sigset;
sigemptyset(&sigset); // NOLINT (include)
sigprocmask(SIG_SETMASK, &sigset, nullptr); // NOLINT
auto pid = fork(); auto pid = fork();
if (pid == -1) { if (pid == -1) {
perror("Failed to fork and launch crash reporter.\n"); perror("Failed to fork and launch crash reporter.\n");
return false; _exit(-1);
} else if (pid == 0) { } else if (pid == 0) {
// dup to remove CLOEXEC // dup to remove CLOEXEC
// if already -1 will return -1 auto dumpFdStr = std::array<char, 48>();
auto dumpFd = dup(self->minidumpFd); auto logFdStr = std::array<char, 48>();
auto logFd = dup(CrashInfo::INSTANCE.logFd); writeEnvInt(dumpFdStr.data(), "__QUICKSHELL_CRASH_DUMP_FD", dup(CrashInfo::INSTANCE.traceFd));
writeEnvInt(logFdStr.data(), "__QUICKSHELL_CRASH_LOG_FD", dup(CrashInfo::INSTANCE.logFd));
// allow up to 10 digits, which should never happen
auto dumpFdStr = std::array<char, 38>();
auto logFdStr = std::array<char, 37>();
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);
env[envi++] = dumpFdStr.data(); env[envi++] = dumpFdStr.data();
env[envi++] = logFdStr.data(); env[envi++] = logFdStr.data();
@ -185,8 +153,82 @@ bool CrashHandlerPrivate::minidumpCallback(
perror("Failed to relaunch quickshell.\n"); perror("Failed to relaunch quickshell.\n");
_exit(-1); _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::frame_ptr, 10>();
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 } // namespace qs::crash

View file

@ -5,19 +5,10 @@
#include "../core/instanceinfo.hpp" #include "../core/instanceinfo.hpp"
namespace qs::crash { namespace qs::crash {
struct CrashHandlerPrivate;
class CrashHandler { class CrashHandler {
public: public:
explicit CrashHandler(); static void init();
~CrashHandler(); static void setRelaunchInfo(const RelaunchInfo& info);
Q_DISABLE_COPY_MOVE(CrashHandler);
void init();
void setRelaunchInfo(const RelaunchInfo& info);
private:
CrashHandlerPrivate* d;
}; };
} // namespace qs::crash } // namespace qs::crash

View file

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

View file

@ -1,9 +1,11 @@
#include "main.hpp" #include "main.hpp"
#include <cerrno> #include <cerrno>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include <cpptrace/basic.hpp>
#include <cpptrace/formatting.hpp>
#include <qapplication.h> #include <qapplication.h>
#include <qconfig.h>
#include <qcoreapplication.h> #include <qcoreapplication.h>
#include <qdatastream.h> #include <qdatastream.h>
#include <qdir.h> #include <qdir.h>
@ -12,15 +14,18 @@
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qtenvironmentvariables.h> #include <qtenvironmentvariables.h>
#include <qtextstream.h> #include <qtextstream.h>
#include <qtversion.h> #include <qtypes.h>
#include <sys/sendfile.h> #include <sys/sendfile.h>
#include <sys/types.h> #include <sys/types.h>
#include <unistd.h>
#include "../core/debuginfo.hpp"
#include "../core/instanceinfo.hpp" #include "../core/instanceinfo.hpp"
#include "../core/logcat.hpp" #include "../core/logcat.hpp"
#include "../core/logging.hpp" #include "../core/logging.hpp"
#include "../core/logging_p.hpp"
#include "../core/paths.hpp" #include "../core/paths.hpp"
#include "build.hpp" #include "../core/ringbuf.hpp"
#include "interface.hpp" #include "interface.hpp"
namespace { namespace {
@ -61,6 +66,76 @@ int tryDup(int fd, const QString& path) {
return 0; 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<qs::log::LogMessage>(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<size_t>(data.size()) / sizeof(cpptrace::safe_object_frame);
if (frameCount == 0) return {};
const auto* frames = reinterpret_cast<const cpptrace::safe_object_frame*>(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) { void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) {
qCDebug(logCrashReporter) << "Recording crash information at" << crashDir.path(); 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 crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt();
auto crashSignal = qEnvironmentVariable("__QUICKSHELL_CRASH_SIGNAL").toInt();
auto dumpFd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD").toInt(); auto dumpFd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD").toInt();
auto logFd = qEnvironmentVariable("__QUICKSHELL_CRASH_LOG_FD").toInt(); auto logFd = qEnvironmentVariable("__QUICKSHELL_CRASH_LOG_FD").toInt();
qCDebug(logCrashReporter) << "Saving minidump from fd" << dumpFd; qCDebug(logCrashReporter) << "Resolving stacktrace from fd" << dumpFd;
auto dumpDupStatus = tryDup(dumpFd, crashDir.filePath("minidump.dmp.log")); auto stacktrace = resolveStacktrace(dumpFd);
if (dumpDupStatus != 0) {
qCCritical(logCrashReporter) << "Failed to write minidump:" << dumpDupStatus;
}
qCDebug(logCrashReporter) << "Saving log from fd" << logFd; qCDebug(logCrashReporter) << "Reading recent log lines from fd" << logFd;
auto logDupStatus = tryDup(logFd, crashDir.filePath("log.qslog.log")); 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) { if (logDupStatus != 0) {
qCCritical(logCrashReporter) << "Failed to save log:" << logDupStatus; 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)) { if (!extraInfoFile.open(QFile::WriteOnly)) {
qCCritical(logCrashReporter) << "Failed to open crash info file for writing."; qCCritical(logCrashReporter) << "Failed to open crash info file for writing.";
} else { } else {
auto stream = QTextStream(&extraInfoFile); auto stream = QTextStream(&extraInfoFile);
stream << "===== Build Information =====\n"; stream << qs::debuginfo::combinedInfo();
stream << "Git Revision: " << GIT_REVISION << '\n';
stream << "Buildtime Qt Version: " << QT_VERSION_STR << "\n";
stream << "Build Type: " << BUILD_TYPE << '\n';
stream << "Compiler: " << COMPILER << '\n';
stream << "Complie Flags: " << COMPILE_FLAGS << "\n\n";
stream << "Build configuration:\n" << BUILD_CONFIGURATION << "\n";
stream << "\n===== Runtime Information =====\n"; stream << "\n===== Instance Information =====\n";
stream << "Runtime Qt Version: " << qVersion() << '\n'; stream << "Signal: " << strsignal(crashSignal) << " (" << crashSignal << ")\n"; // NOLINT
stream << "Crashed process ID: " << crashProc << '\n'; stream << "Crashed process ID: " << crashProc << '\n';
stream << "Run ID: " << instance.instanceId << '\n'; stream << "Run ID: " << instance.instanceId << '\n';
stream << "Shell ID: " << instance.shellId << '\n'; stream << "Shell ID: " << instance.shellId << '\n';
stream << "Config Path: " << instance.configPath << '\n'; stream << "Config Path: " << instance.configPath << '\n';
stream << "\n===== Report Integrity =====\n"; stream << "\n===== Stacktrace =====\n";
stream << "Minidump save status: " << dumpDupStatus << '\n'; if (stacktrace.empty()) {
stream << "Log save status: " << logDupStatus << '\n'; stream << "(no trace available)\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();
} else { } 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:"; stream << "\n===== Log Tail =====\n";
auto lsbReleaseFile = QFile("/etc/lsb-release"); stream << recentLogs;
if (lsbReleaseFile.open(QFile::ReadOnly)) {
stream << '\n' << lsbReleaseFile.readAll();
lsbReleaseFile.close();
} else {
stream << "FAILED TO OPEN\n";
}
extraInfoFile.close(); extraInfoFile.close();
} }

View file

@ -3,6 +3,7 @@
#include <variant> #include <variant>
#include <qbuffer.h> #include <qbuffer.h>
#include <qcoreapplication.h>
#include <qlocalserver.h> #include <qlocalserver.h>
#include <qlocalsocket.h> #include <qlocalsocket.h>
#include <qlogging.h> #include <qlogging.h>
@ -127,7 +128,9 @@ int IpcClient::connect(const QString& id, const std::function<void(IpcClient& cl
void IpcKillCommand::exec(IpcServerConnection* /*unused*/) { void IpcKillCommand::exec(IpcServerConnection* /*unused*/) {
qInfo() << "Exiting due to IPC request."; qInfo() << "Exiting due to IPC request.";
EngineGeneration::currentGeneration()->quit(); auto* generation = EngineGeneration::currentGeneration();
if (generation) generation->quit();
else QCoreApplication::exit(0);
} }
} // namespace qs::ipc } // namespace qs::ipc

View file

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

View file

@ -27,7 +27,7 @@
#include "build.hpp" #include "build.hpp"
#include "launch_p.hpp" #include "launch_p.hpp"
#if CRASH_REPORTER #if CRASH_HANDLER
#include "../crash/handler.hpp" #include "../crash/handler.hpp"
#endif #endif
@ -137,13 +137,14 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio
.display = getDisplayConnection(), .display = getDisplayConnection(),
}; };
#if CRASH_REPORTER #if CRASH_HANDLER
auto crashHandler = crash::CrashHandler(); if (qEnvironmentVariableIsSet("QS_DISABLE_CRASH_HANDLER")) {
crashHandler.init(); qInfo() << "Crash handling disabled.";
} else {
crash::CrashHandler::init();
{
auto* log = LogManager::instance(); auto* log = LogManager::instance();
crashHandler.setRelaunchInfo({ crash::CrashHandler::setRelaunchInfo({
.instance = InstanceInfo::CURRENT, .instance = InstanceInfo::CURRENT,
.noColor = !log->colorLogs, .noColor = !log->colorLogs,
.timestamp = log->timestampLogs, .timestamp = log->timestampLogs,

View file

@ -16,7 +16,7 @@
#include "build.hpp" #include "build.hpp"
#include "launch_p.hpp" #include "launch_p.hpp"
#if CRASH_REPORTER #if CRASH_HANDLER
#include "../crash/main.hpp" #include "../crash/main.hpp"
#endif #endif
@ -25,7 +25,7 @@ namespace qs::launch {
namespace { namespace {
void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) { void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) {
#if CRASH_REPORTER #if CRASH_HANDLER
auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD"); auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD");
if (!lastInfoFdStr.isEmpty()) { if (!lastInfoFdStr.isEmpty()) {
@ -104,7 +104,7 @@ void exitDaemon(int code) {
int main(int argc, char** argv) { int main(int argc, char** argv) {
QCoreApplication::setApplicationName("quickshell"); QCoreApplication::setApplicationName("quickshell");
#if CRASH_REPORTER #if CRASH_HANDLER
qsCheckCrash(argc, argv); qsCheckCrash(argc, argv);
#endif #endif

View file

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

View file

@ -145,6 +145,7 @@ void GreetdConnection::setInactive() {
QString GreetdConnection::user() const { return this->mUser; } QString GreetdConnection::user() const { return this->mUser; }
void GreetdConnection::onSocketConnected() { void GreetdConnection::onSocketConnected() {
this->reader.setDevice(&this->socket);
qCDebug(logGreetd) << "Connected to greetd socket."; qCDebug(logGreetd) << "Connected to greetd socket.";
if (this->mTargetActive) { if (this->mTargetActive) {
@ -160,11 +161,12 @@ void GreetdConnection::onSocketError(QLocalSocket::LocalSocketError error) {
} }
void GreetdConnection::onSocketReady() { void GreetdConnection::onSocketReady() {
qint32 length = 0; while (true) {
this->reader.startTransaction();
auto length = this->reader.readI32();
auto text = this->reader.readBytes(length);
if (!this->reader.commitTransaction()) return;
this->socket.read(reinterpret_cast<char*>(&length), sizeof(qint32));
auto text = this->socket.read(length);
auto json = QJsonDocument::fromJson(text).object(); auto json = QJsonDocument::fromJson(text).object();
auto type = json.value("type").toString(); auto type = json.value("type").toString();
@ -232,10 +234,11 @@ void GreetdConnection::onSocketReady() {
} }
} else goto unexpected; } else goto unexpected;
return; continue;
unexpected: unexpected:
qCCritical(logGreetd) << "Received unexpected greetd response" << text; qCCritical(logGreetd) << "Received unexpected greetd response" << text;
this->setActive(false); this->setActive(false);
}
} }
void GreetdConnection::sendRequest(const QJsonObject& json) { void GreetdConnection::sendRequest(const QJsonObject& json) {

View file

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

View file

@ -8,6 +8,9 @@
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <sys/signal.h> #include <sys/signal.h>
#include <sys/wait.h> #include <sys/wait.h>
#ifdef __FreeBSD__
#include <signal.h>
#endif
#include "../../core/logcat.hpp" #include "../../core/logcat.hpp"
#include "ipc.hpp" #include "ipc.hpp"

View file

@ -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) { void PwCore::onError(void* data, quint32 id, qint32 /*seq*/, qint32 res, const char* message) {
auto* self = static_cast<PwCore*>(data); auto* self = static_cast<PwCore*>(data);
if (message != nullptr) { // Pipewire's documentation describes the error event as being fatal, however it isn't.
qCWarning(logLoop) << "Fatal pipewire error on object" << id << "with code" << res << message; // We're not sure what causes these ENOENTs on device removal, presumably something in
} else { // the teardown sequence, but they're harmless. Attempting to handle them as a fatal
qCWarning(logLoop) << "Fatal pipewire error on object" << id << "with code" << res; // 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(); emit self->fatalError();
} }

View file

@ -12,7 +12,6 @@
#include <spa/utils/json.h> #include <spa/utils/json.h>
#include "../../core/logcat.hpp" #include "../../core/logcat.hpp"
#include "../../core/util.hpp"
#include "metadata.hpp" #include "metadata.hpp"
#include "node.hpp" #include "node.hpp"
#include "registry.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) { void PwDefaultTracker::changeConfiguredSink(PwNode* node) {
if (node != nullptr) { if (node != nullptr) {
if (!node->type.testFlags(PwNodeType::AudioSink)) { if (!node->type.testFlags(PwNodeType::AudioSink)) {
@ -240,10 +213,23 @@ void PwDefaultTracker::setDefaultSink(PwNode* node) {
if (node == this->mDefaultSink) return; if (node == this->mDefaultSink) return;
qCInfo(logDefaults) << "Default sink changed to" << node; qCInfo(logDefaults) << "Default sink changed to" << node;
setSimpleObjectHandle< if (this->mDefaultSink != nullptr) {
&PwDefaultTracker::mDefaultSink, QObject::disconnect(this->mDefaultSink, nullptr, this, nullptr);
&PwDefaultTracker::onNodeDestroyed, }
&PwDefaultTracker::defaultSinkChanged>(this, node);
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) { void PwDefaultTracker::setDefaultSinkName(const QString& name) {
@ -257,10 +243,23 @@ void PwDefaultTracker::setDefaultSource(PwNode* node) {
if (node == this->mDefaultSource) return; if (node == this->mDefaultSource) return;
qCInfo(logDefaults) << "Default source changed to" << node; qCInfo(logDefaults) << "Default source changed to" << node;
setSimpleObjectHandle< if (this->mDefaultSource != nullptr) {
&PwDefaultTracker::mDefaultSource, QObject::disconnect(this->mDefaultSource, nullptr, this, nullptr);
&PwDefaultTracker::onNodeDestroyed, }
&PwDefaultTracker::defaultSourceChanged>(this, node);
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) { void PwDefaultTracker::setDefaultSourceName(const QString& name) {
@ -274,10 +273,28 @@ void PwDefaultTracker::setDefaultConfiguredSink(PwNode* node) {
if (node == this->mDefaultConfiguredSink) return; if (node == this->mDefaultConfiguredSink) return;
qCInfo(logDefaults) << "Default configured sink changed to" << node; qCInfo(logDefaults) << "Default configured sink changed to" << node;
setSimpleObjectHandle< if (this->mDefaultConfiguredSink != nullptr) {
&PwDefaultTracker::mDefaultConfiguredSink, QObject::disconnect(this->mDefaultConfiguredSink, nullptr, this, nullptr);
&PwDefaultTracker::onNodeDestroyed, }
&PwDefaultTracker::defaultConfiguredSinkChanged>(this, node);
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) { void PwDefaultTracker::setDefaultConfiguredSinkName(const QString& name) {
@ -291,10 +308,28 @@ void PwDefaultTracker::setDefaultConfiguredSource(PwNode* node) {
if (node == this->mDefaultConfiguredSource) return; if (node == this->mDefaultConfiguredSource) return;
qCInfo(logDefaults) << "Default configured source changed to" << node; qCInfo(logDefaults) << "Default configured source changed to" << node;
setSimpleObjectHandle< if (this->mDefaultConfiguredSource != nullptr) {
&PwDefaultTracker::mDefaultConfiguredSource, QObject::disconnect(this->mDefaultConfiguredSource, nullptr, this, nullptr);
&PwDefaultTracker::onNodeDestroyed, }
&PwDefaultTracker::defaultConfiguredSourceChanged>(this, node);
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) { void PwDefaultTracker::setDefaultConfiguredSourceName(const QString& name) {

View file

@ -44,7 +44,10 @@ private slots:
void onMetadataAdded(PwMetadata* metadata); void onMetadataAdded(PwMetadata* metadata);
void onMetadataProperty(const char* key, const char* type, const char* value); void onMetadataProperty(const char* key, const char* type, const char* value);
void onNodeAdded(PwNode* node); void onNodeAdded(PwNode* node);
void onNodeDestroyed(QObject* node); void onDefaultSinkDestroyed();
void onDefaultSourceDestroyed();
void onDefaultConfiguredSinkDestroyed();
void onDefaultConfiguredSourceDestroyed();
private: private:
void setDefaultSink(PwNode* node); void setDefaultSink(PwNode* node);

View file

@ -73,6 +73,7 @@ endfunction()
# ----- # -----
qt_add_library(quickshell-wayland STATIC qt_add_library(quickshell-wayland STATIC
wl_proxy_safe_deref.cpp
platformmenu.cpp platformmenu.cpp
popupanchor.cpp popupanchor.cpp
xdgshell.cpp xdgshell.cpp
@ -80,6 +81,13 @@ qt_add_library(quickshell-wayland STATIC
output_tracking.cpp 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 # required to make sure the constructor is linked
add_library(quickshell-wayland-init OBJECT init.cpp) add_library(quickshell-wayland-init OBJECT init.cpp)
@ -123,6 +131,8 @@ list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleNotify)
add_subdirectory(shortcuts_inhibit) add_subdirectory(shortcuts_inhibit)
list(APPEND WAYLAND_MODULES Quickshell.Wayland._ShortcutsInhibitor) list(APPEND WAYLAND_MODULES Quickshell.Wayland._ShortcutsInhibitor)
add_subdirectory(windowmanager)
# widgets for qmenu # widgets for qmenu
target_link_libraries(quickshell-wayland PRIVATE target_link_libraries(quickshell-wayland PRIVATE
Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate

View file

@ -28,6 +28,7 @@
#include <qscopedpointer.h> #include <qscopedpointer.h>
#include <qsgrendererinterface.h> #include <qsgrendererinterface.h>
#include <qsgtexture_platform.h> #include <qsgtexture_platform.h>
#include <qtypes.h>
#include <qvulkanfunctions.h> #include <qvulkanfunctions.h>
#include <qvulkaninstance.h> #include <qvulkaninstance.h>
#include <qwayland-linux-dmabuf-v1.h> #include <qwayland-linux-dmabuf-v1.h>
@ -35,7 +36,6 @@
#include <sys/mman.h> #include <sys/mman.h>
#include <sys/types.h> #include <sys/types.h>
#include <unistd.h> #include <unistd.h>
#include <qtypes.h>
#include <vulkan/vulkan_core.h> #include <vulkan/vulkan_core.h>
#include <wayland-client-protocol.h> #include <wayland-client-protocol.h>
#include <wayland-linux-dmabuf-v1-client-protocol.h> #include <wayland-linux-dmabuf-v1-client-protocol.h>
@ -80,10 +80,8 @@ bool drmFormatHasAlpha(uint32_t drmFormat) {
case DRM_FORMAT_ABGR8888: case DRM_FORMAT_ABGR8888:
case DRM_FORMAT_ARGB2101010: case DRM_FORMAT_ARGB2101010:
case DRM_FORMAT_ABGR2101010: case DRM_FORMAT_ABGR2101010:
case DRM_FORMAT_ABGR16161616F: case DRM_FORMAT_ABGR16161616F: return true;
return true; default: return false;
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 // 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. // 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) { if (dupFd < 0) {
qCWarning(logDmabuf) << "Failed to dup() fd for DMA-BUF import"; qCWarning(logDmabuf) << "Failed to dup() fd for DMA-BUF import";
goto cleanup_fail; // NOLINT goto cleanup_fail; // NOLINT
@ -909,12 +908,12 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) co
// find the graphics queue family index for the ownrship transfer. // find the graphics queue family index for the ownrship transfer.
uint32_t graphicsQueueFamily = 0; uint32_t graphicsQueueFamily = 0;
uint32_t queueFamilyCount = 0; uint32_t queueFamilyCount = 0;
instFuncs->vkGetPhysicalDeviceQueueFamilyProperties( instFuncs->vkGetPhysicalDeviceQueueFamilyProperties(physDevice, &queueFamilyCount, nullptr);
physDevice, &queueFamilyCount, nullptr
);
std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount); std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
instFuncs->vkGetPhysicalDeviceQueueFamilyProperties( instFuncs->vkGetPhysicalDeviceQueueFamilyProperties(
physDevice, &queueFamilyCount, queueFamilies.data() physDevice,
&queueFamilyCount,
queueFamilies.data()
); );
for (uint32_t i = 0; i < queueFamilyCount; ++i) { for (uint32_t i = 0; i < queueFamilyCount; ++i) {
if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
@ -989,13 +988,7 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) co
} }
} }
auto* tex = new WlDmaBufferVulkanQSGTexture( auto* tex = new WlDmaBufferVulkanQSGTexture(devFuncs, device, image, memory, qsgTexture);
devFuncs,
device,
image,
memory,
qsgTexture
);
qCDebug(logDmabuf) << "Created WlDmaBufferVulkanQSGTexture" << tex << "from" << this; qCDebug(logDmabuf) << "Created WlDmaBufferVulkanQSGTexture" << tex << "from" << this;
return tex; return tex;
} }

View file

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

View file

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

View file

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

View file

@ -72,20 +72,16 @@ void HyprlandToplevel::updateFromObject(const QVariantMap& object) {
Qt::beginPropertyUpdateGroup(); Qt::beginPropertyUpdateGroup();
bool ok = false; bool ok = false;
auto address = addressStr.toULongLong(&ok, 16); auto address = addressStr.toULongLong(&ok, 16);
if (!ok || !address) { if (ok && address) this->setAddress(address);
return;
}
this->setAddress(address);
this->bTitle = title; this->bTitle = title;
auto workspaceMap = object.value("workspace").toMap(); auto workspaceMap = object.value("workspace").toMap();
auto workspaceName = workspaceMap.value("name").toString(); auto workspaceName = workspaceMap.value("name").toString();
auto* workspace = this->ipc->findWorkspaceByName(workspaceName, false); auto* workspace = this->ipc->findWorkspaceByName(workspaceName, true);
if (!workspace) return; if (workspace) this->setWorkspace(workspace);
this->setWorkspace(workspace);
this->bLastIpcObject = object; this->bLastIpcObject = object;
Qt::endPropertyUpdateGroup(); Qt::endPropertyUpdateGroup();
} }

View file

@ -10,6 +10,7 @@
#include "wlr_layershell/wlr_layershell.hpp" #include "wlr_layershell/wlr_layershell.hpp"
#endif #endif
void installWlProxySafeDeref(); // NOLINT(misc-use-internal-linkage)
void installPlatformMenuHook(); // NOLINT(misc-use-internal-linkage) void installPlatformMenuHook(); // NOLINT(misc-use-internal-linkage)
void installPopupPositioner(); // NOLINT(misc-use-internal-linkage) void installPopupPositioner(); // NOLINT(misc-use-internal-linkage)
@ -33,6 +34,7 @@ class WaylandPlugin: public QsEnginePlugin {
} }
void init() override { void init() override {
installWlProxySafeDeref();
installPlatformMenuHook(); installPlatformMenuHook();
installPopupPositioner(); installPopupPositioner();
} }

View file

@ -9,6 +9,7 @@
#include <qqmlcomponent.h> #include <qqmlcomponent.h>
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qqmllist.h> #include <qqmllist.h>
#include <qquickgraphicsconfiguration.h>
#include <qquickitem.h> #include <qquickitem.h>
#include <qquickwindow.h> #include <qquickwindow.h>
#include <qscreen.h> #include <qscreen.h>
@ -216,6 +217,15 @@ void WlSessionLockSurface::onReload(QObject* oldInstance) {
if (this->window == nullptr) { if (this->window == nullptr) {
this->window = new QQuickWindow(); 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()); this->mContentItem->setParentItem(this->window->contentItem());

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,252 @@
#include "windowset.hpp"
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qproperty.h>
#include "../../windowmanager/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<WlWindowset*>(ws)->commitImpl(); // NOLINT
}
for (auto* projection: projections) {
static_cast<WlWindowsetProjection*>(projection)->commitImpl(); // NOLINT
}
this->pendingWindowsetCreations.clear();
this->pendingWindowsetDestructions.clear();
this->pendingProjectionCreations.clear();
this->pendingProjectionDestructions.clear();
wm->bWindowsets = windowsets;
wm->bWindowsetProjections = projections;
Qt::endPropertyUpdateGroup();
}
void WindowsetManager::onWindowsetCreated(impl::Workspace* workspace) {
this->pendingWindowsetCreations.append(workspace);
}
void WindowsetManager::onWindowsetDestroyed(impl::Workspace* workspace) {
if (!this->pendingWindowsetCreations.removeOne(workspace)) {
this->pendingWindowsetDestructions.append(workspace);
}
}
void WindowsetManager::onProjectionCreated(impl::WorkspaceGroup* group) {
this->pendingProjectionCreations.append(group);
}
void WindowsetManager::onProjectionDestroyed(impl::WorkspaceGroup* group) {
if (!this->pendingProjectionCreations.removeOne(group)) {
this->pendingProjectionDestructions.append(group);
}
}
WindowsetManager* WindowsetManager::instance() {
static auto* instance = new WindowsetManager();
return instance;
}
WlWindowset::WlWindowset(WindowsetManager* manager, impl::Workspace* impl)
: Windowset(manager)
, impl(impl) {
this->commitImpl();
}
void WlWindowset::commitImpl() {
Qt::beginPropertyUpdateGroup();
this->bId = this->impl->id;
this->bName = this->impl->name;
this->bCoordinates = this->impl->coordinates;
this->bActive = this->impl->active;
this->bShouldDisplay = !this->impl->hidden;
this->bUrgent = this->impl->urgent;
this->bCanActivate = this->impl->canActivate;
this->bCanDeactivate = this->impl->canDeactivate;
this->bCanSetProjection = this->impl->canAssign;
this->bProjection = this->manager()->projectionsByImpl.value(this->impl->group);
Qt::endPropertyUpdateGroup();
}
void WlWindowset::activate() {
if (!this->bCanActivate) {
qCritical(logWorkspace) << this << "cannot be activated";
return;
}
qCDebug(impl::logWorkspace) << "Calling activate() for" << this;
this->impl->activate();
WindowsetManager::instance()->scheduleCommit();
}
void WlWindowset::deactivate() {
if (!this->bCanDeactivate) {
qCritical(logWorkspace) << this << "cannot be deactivated";
return;
}
qCDebug(impl::logWorkspace) << "Calling deactivate() for" << this;
this->impl->deactivate();
WindowsetManager::instance()->scheduleCommit();
}
void WlWindowset::remove() {
if (!this->bCanRemove) {
qCritical(logWorkspace) << this << "cannot be removed";
return;
}
qCDebug(impl::logWorkspace) << "Calling remove() for" << this;
this->impl->remove();
WindowsetManager::instance()->scheduleCommit();
}
void WlWindowset::setProjection(WindowsetProjection* projection) {
if (!this->bCanSetProjection) {
qCritical(logWorkspace) << this << "cannot be assigned to a projection";
return;
}
if (!projection) {
qCritical(logWorkspace) << "Cannot set a windowset's projection to null";
return;
}
WlWindowsetProjection* wlProjection = nullptr;
if (auto* p = dynamic_cast<WlWindowsetProjection*>(projection)) {
wlProjection = p;
} else if (auto* p = dynamic_cast<ScreenProjection*>(projection)) {
// In the 99% case, there will only be a single windowset on a screen.
// In the 1% case, the oldest projection (first in list) is most likely the desired one.
auto* screen = p->screen();
for (const auto& proj: WindowsetManager::instance()->projectionsByImpl.values()) {
if (proj->bQScreens.value().contains(screen)) {
wlProjection = proj;
break;
}
}
}
if (!wlProjection) {
qCritical(logWorkspace) << "Cannot set a windowset's projection to" << projection
<< "as no wayland projection could be derived.";
return;
}
qCDebug(impl::logWorkspace) << "Assigning" << this << "to" << projection;
this->impl->assign(wlProjection->impl->object());
WindowsetManager::instance()->scheduleCommit();
}
WlWindowsetProjection::WlWindowsetProjection(WindowsetManager* manager, impl::WorkspaceGroup* impl)
: WindowsetProjection(manager)
, impl(impl) {
this->commitImpl();
}
void WlWindowsetProjection::commitImpl() {
// TODO: will not commit the correct screens if missing qt repr at commit time
this->bQScreens = this->impl->screens.screens();
}
} // namespace qs::wm::wayland

View file

@ -0,0 +1,85 @@
#pragma once
#include <qhash.h>
#include <qlist.h>
#include <qobject.h>
#include <qproperty.h>
#include <qtmetamacros.h>
#include "../../windowmanager/windowset.hpp"
#include "ext_workspace.hpp"
namespace qs::wm::wayland {
namespace impl = qs::wayland::workspace;
class WlWindowset;
class WlWindowsetProjection;
class WindowsetManager: public QObject {
Q_OBJECT;
public:
static WindowsetManager* instance();
void scheduleCommit();
private slots:
void doCommit();
void onServerCommit();
void onWindowsetCreated(impl::Workspace* workspace);
void onWindowsetDestroyed(impl::Workspace* workspace);
void onProjectionCreated(impl::WorkspaceGroup* group);
void onProjectionDestroyed(impl::WorkspaceGroup* group);
private:
WindowsetManager();
bool commitScheduled = false;
QList<impl::Workspace*> pendingWindowsetCreations;
QList<impl::Workspace*> pendingWindowsetDestructions;
QHash<impl::Workspace*, WlWindowset*> windowsetByImpl;
QList<impl::WorkspaceGroup*> pendingProjectionCreations;
QList<impl::WorkspaceGroup*> pendingProjectionDestructions;
QHash<impl::WorkspaceGroup*, WlWindowsetProjection*> projectionsByImpl;
friend class WlWindowset;
};
class WlWindowset: public Windowset {
public:
WlWindowset(WindowsetManager* manager, impl::Workspace* impl);
void commitImpl();
void activate() override;
void deactivate() override;
void remove() override;
void setProjection(WindowsetProjection* projection) override;
[[nodiscard]] WindowsetManager* manager() {
return static_cast<WindowsetManager*>(this->parent()); // NOLINT
}
private:
impl::Workspace* impl = nullptr;
};
class WlWindowsetProjection: public WindowsetProjection {
public:
WlWindowsetProjection(WindowsetManager* manager, impl::WorkspaceGroup* impl);
void commitImpl();
[[nodiscard]] WindowsetManager* manager() {
return static_cast<WindowsetManager*>(this->parent()); // NOLINT
}
private:
impl::WorkspaceGroup* impl = nullptr;
friend class WlWindowset;
};
} // namespace qs::wm::wayland

View file

@ -0,0 +1,42 @@
#include <dlfcn.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <wayland-client-core.h>
#include <wayland-util.h>
#include "../core/logcat.hpp"
namespace {
QS_LOGGING_CATEGORY(logDeref, "quickshell.wayland.safederef", QtWarningMsg);
using wl_proxy_get_listener_t = const void* (*) (wl_proxy*);
wl_proxy_get_listener_t original_wl_proxy_get_listener = nullptr; // NOLINT
} // namespace
extern "C" {
WL_EXPORT const void* wl_proxy_get_listener(struct wl_proxy* proxy) {
// Avoid null derefs of protocol objects in qtbase.
// https://qt-project.atlassian.net/browse/QTBUG-145022
if (!proxy) [[unlikely]] {
qCCritical(logDeref) << "wl_proxy_get_listener called with a null proxy!";
return nullptr;
}
return original_wl_proxy_get_listener(proxy);
}
}
// NOLINTBEGIN (concurrency-mt-unsafe)
void installWlProxySafeDeref() {
dlerror(); // clear old errors
original_wl_proxy_get_listener =
reinterpret_cast<wl_proxy_get_listener_t>(dlsym(RTLD_NEXT, "wl_proxy_get_listener"));
if (auto* error = dlerror()) {
qCCritical(logDeref) << "Failed to find wl_proxy_get_listener for hooking:" << error;
} else {
qCInfo(logDeref) << "Installed wl_proxy_get_listener hook.";
}
}
// NOLINTEND

View file

@ -17,6 +17,7 @@
#include <qvariant.h> #include <qvariant.h>
#include <qwayland-wlr-layer-shell-unstable-v1.h> #include <qwayland-wlr-layer-shell-unstable-v1.h>
#include <qwindow.h> #include <qwindow.h>
#include <wayland-xdg-shell-client-protocol.h>
#include "../../window/panelinterface.hpp" #include "../../window/panelinterface.hpp"
#include "shell_integration.hpp" #include "shell_integration.hpp"
@ -247,9 +248,19 @@ void LayerSurface::commit() {
} }
void LayerSurface::attachPopup(QtWaylandClient::QWaylandShellSurface* popup) { void LayerSurface::attachPopup(QtWaylandClient::QWaylandShellSurface* popup) {
std::any role = popup->surfaceRole(); #ifdef __FreeBSD__
// FreeBSD uses an alternate RTTI matching strategy by default which does
if (auto* popupRole = std::any_cast<::xdg_popup*>(&role)) { // NOLINT // not work across modules, preventing std::any from downcasting. On
// FreeBSD, Qt is built with a patch to expose the surface role through a
// pointer instead of an any, which does not have this problem.
// See https://bugs.kde.org/show_bug.cgi?id=479679
if (auto* xdgPopup = static_cast<::xdg_popup*>(popup->nativeResource("xdg_popup"))) {
this->get_popup(xdgPopup);
return;
}
#endif
auto role = popup->surfaceRole(); // NOLINT
if (auto* popupRole = std::any_cast<::xdg_popup*>(&role)) {
this->get_popup(*popupRole); this->get_popup(*popupRole);
} else { } else {
qWarning() << "Cannot attach popup" << popup << "to shell surface" << this qWarning() << "Cannot attach popup" << popup << "to shell surface" << this

View file

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

View file

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

View file

@ -0,0 +1,30 @@
#include "screenprojection.hpp"
#include <qlist.h>
#include <qobject.h>
#include <qscreen.h>
#include "windowmanager.hpp"
#include "windowset.hpp"
namespace qs::wm {
ScreenProjection::ScreenProjection(QScreen* screen, QObject* parent)
: WindowsetProjection(parent)
, mScreen(screen) {
this->bQScreens = {screen};
this->bWindowsets.setBinding([this]() {
QList<Windowset*> result;
for (auto* ws: WindowManager::instance()->bindableWindowsets().value()) {
auto* proj = ws->bindableProjection().value();
if (proj && proj->bindableQScreens().value().contains(this->mScreen)) {
result.append(ws);
}
}
return result;
});
}
QScreen* ScreenProjection::screen() const { return this->mScreen; }
} // namespace qs::wm

View file

@ -0,0 +1,34 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#include "windowset.hpp"
namespace qs::wm {
///! WindowsetProjection covering one specific screen.
/// A ScreenProjection is a special type of @@WindowsetProjection which aggregates
/// all windowsets across all projections covering a specific screen.
///
/// When used with @@Windowset.setProjection(), an arbitrary projection on the screen
/// will be picked. Usually there is only one.
///
/// Use @@WindowManager.screenProjection() to get a ScreenProjection for a given screen.
class ScreenProjection: public WindowsetProjection {
Q_OBJECT;
QML_ELEMENT;
QML_UNCREATABLE("");
public:
ScreenProjection(QScreen* screen, QObject* parent);
[[nodiscard]] QScreen* screen() const;
private:
QScreen* mScreen;
};
} // namespace qs::wm

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,91 @@
#pragma once
#include <functional>
#include <qhash.h>
#include <qlist.h>
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#include "../core/qmlscreen.hpp"
#include "screenprojection.hpp"
#include "windowset.hpp"
namespace qs::wm {
class WindowManager: public QObject {
Q_OBJECT;
public:
static void setProvider(std::function<WindowManager*()> provider);
static WindowManager* instance();
Q_INVOKABLE ScreenProjection* screenProjection(QuickshellScreenInfo* screen);
[[nodiscard]] QBindable<QList<Windowset*>> bindableWindowsets() const {
return &this->bWindowsets;
}
[[nodiscard]] QBindable<QList<WindowsetProjection*>> bindableWindowsetProjections() const {
return &this->bWindowsetProjections;
}
signals:
void windowsetsChanged();
void windowsetProjectionsChanged();
public:
Q_OBJECT_BINDABLE_PROPERTY(
WindowManager,
QList<Windowset*>,
bWindowsets,
&WindowManager::windowsetsChanged
);
Q_OBJECT_BINDABLE_PROPERTY(
WindowManager,
QList<WindowsetProjection*>,
bWindowsetProjections,
&WindowManager::windowsetProjectionsChanged
);
private:
static std::function<WindowManager*()> provider;
QHash<QScreen*, ScreenProjection*> mScreenProjections;
};
///! Window management interfaces exposed by the window manager.
class WindowManagerQml: public QObject {
Q_OBJECT;
QML_NAMED_ELEMENT(WindowManager);
QML_SINGLETON;
// clang-format off
/// All windowsets tracked by the WM across all projections.
Q_PROPERTY(QList<Windowset*> windowsets READ default BINDABLE bindableWindowsets);
/// All windowset projections tracked by the WM. Does not include
/// internal projections from @@screenProjection().
Q_PROPERTY(QList<WindowsetProjection*> windowsetProjections READ default BINDABLE bindableWindowsetProjections);
// clang-format on
public:
/// Returns an internal WindowsetProjection that covers a single screen and contains all
/// windowsets on that screen, regardless of the WM-specified projection. Depending on
/// how the WM lays out its actual projections, multiple ScreenProjections may contain
/// the same Windowsets.
Q_INVOKABLE static ScreenProjection* screenProjection(QuickshellScreenInfo* screen) {
return WindowManager::instance()->screenProjection(screen);
}
[[nodiscard]] static QBindable<QList<Windowset*>> bindableWindowsets() {
return WindowManager::instance()->bindableWindowsets();
}
[[nodiscard]] static QBindable<QList<WindowsetProjection*>> bindableWindowsetProjections() {
return WindowManager::instance()->bindableWindowsetProjections();
}
};
} // namespace qs::wm

View file

@ -0,0 +1,45 @@
#include "windowset.hpp"
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include "../core/qmlglobal.hpp"
#include "windowmanager.hpp"
namespace qs::wm {
Q_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.workspace", QtWarningMsg);
void Windowset::activate() { qCCritical(logWorkspace) << this << "cannot be activated"; }
void Windowset::deactivate() { qCCritical(logWorkspace) << this << "cannot be deactivated"; }
void Windowset::remove() { qCCritical(logWorkspace) << this << "cannot be removed"; }
void Windowset::setProjection(WindowsetProjection* /*projection*/) {
qCCritical(logWorkspace) << this << "cannot be assigned to a projection";
}
WindowsetProjection::WindowsetProjection(QObject* parent): QObject(parent) {
this->bWindowsets.setBinding([this] {
QList<Windowset*> result;
for (auto* ws: WindowManager::instance()->bindableWindowsets().value()) {
if (ws->bindableProjection().value() == this) {
result.append(ws);
}
}
return result;
});
this->bScreens.setBinding([this] {
QList<QuickshellScreenInfo*> screens;
for (auto* screen: this->bQScreens.value()) {
screens.append(QuickshellTracked::instance()->screenInfo(screen));
}
return screens;
});
}
} // namespace qs::wm

View file

@ -0,0 +1,175 @@
#pragma once
#include <qcontainerfwd.h>
#include <qlist.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qscreen.h>
#include <qtmetamacros.h>
#include <qtypes.h>
class QuickshellScreenInfo;
namespace qs::wm {
Q_DECLARE_LOGGING_CATEGORY(logWorkspace);
class WindowsetProjection;
///! A group of windows worked with by a user, usually known as a Workspace or Tag.
/// A Windowset is a generic type that encompasses both "Workspaces" and "Tags" in window managers.
/// Because the definition encompasses both you may not necessarily need all features.
class Windowset: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_UNCREATABLE("");
// clang-format off
/// A persistent internal identifier for the windowset. This property should be identical
/// across restarts and destruction/recreation of a windowset.
Q_PROPERTY(QString id READ default NOTIFY idChanged BINDABLE bindableId);
/// Human readable name of the windowset.
Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName);
/// Coordinates of the workspace, represented as an N-dimensional array. Most WMs
/// will only expose one coordinate. If more than one is exposed, the first is
/// conventionally X, the second Y, and the third Z.
Q_PROPERTY(QList<qint32> coordinates READ default NOTIFY coordinatesChanged BINDABLE bindableCoordinates);
/// True if the windowset is currently active. In a workspace based WM, this means the
/// represented workspace is current. In a tag based WM, this means the represented tag
/// is active.
Q_PROPERTY(bool active READ default NOTIFY activeChanged BINDABLE bindableActive);
/// The projection this windowset is a member of. A projection is the set of screens covered by
/// a windowset.
Q_PROPERTY(WindowsetProjection* projection READ default NOTIFY projectionChanged BINDABLE bindableProjection);
/// If false, this windowset should generally be hidden from workspace pickers.
Q_PROPERTY(bool shouldDisplay READ default NOTIFY shouldDisplayChanged BINDABLE bindableShouldDisplay);
/// If true, a window in this windowset has been marked as urgent.
Q_PROPERTY(bool urgent READ default NOTIFY urgentChanged BINDABLE bindableUrgent);
/// If true, the windowset can be activated. In a workspace based WM, this will make the workspace
/// current, in a tag based wm, the tag will be activated.
Q_PROPERTY(bool canActivate READ default NOTIFY canActivateChanged BINDABLE bindableCanActivate);
/// If true, the windowset can be deactivated. In a workspace based WM, deactivation is usually implicit
/// and based on activation of another workspace.
Q_PROPERTY(bool canDeactivate READ default NOTIFY canDeactivateChanged BINDABLE bindableCanDeactivate);
/// If true, the windowset can be removed. This may be done implicitly by the WM as well.
Q_PROPERTY(bool canRemove READ default NOTIFY canRemoveChanged BINDABLE bindableCanRemove);
/// If true, the windowset can be moved to a different projection.
Q_PROPERTY(bool canSetProjection READ default NOTIFY canSetProjectionChanged BINDABLE bindableCanSetProjection);
// clang-format on
public:
explicit Windowset(QObject* parent): QObject(parent) {}
/// Activate the windowset, making it the current workspace on a workspace based WM, or activating
/// the tag on a tag based WM. Requires @@canActivate.
Q_INVOKABLE virtual void activate();
/// Deactivate the windowset, hiding it. Requires @@canDeactivate.
Q_INVOKABLE virtual void deactivate();
/// Remove or destroy the windowset. Requires @@canRemove.
Q_INVOKABLE virtual void remove();
/// Move the windowset to a different projection. A projection represents the set of screens
/// a workspace spans. Requires @@canSetProjection.
Q_INVOKABLE virtual void setProjection(WindowsetProjection* projection);
[[nodiscard]] QBindable<QString> bindableId() const { return &this->bId; }
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
[[nodiscard]] QBindable<QList<qint32>> bindableCoordinates() const { return &this->bCoordinates; }
[[nodiscard]] QBindable<bool> bindableActive() const { return &this->bActive; }
[[nodiscard]] QBindable<WindowsetProjection*> bindableProjection() const {
return &this->bProjection;
}
[[nodiscard]] QBindable<bool> bindableShouldDisplay() const { return &this->bShouldDisplay; }
[[nodiscard]] QBindable<bool> bindableUrgent() const { return &this->bUrgent; }
[[nodiscard]] QBindable<bool> bindableCanActivate() const { return &this->bCanActivate; }
[[nodiscard]] QBindable<bool> bindableCanDeactivate() const { return &this->bCanDeactivate; }
[[nodiscard]] QBindable<bool> bindableCanRemove() const { return &this->bCanRemove; }
[[nodiscard]] QBindable<bool> bindableCanSetProjection() const {
return &this->bCanSetProjection;
}
signals:
void idChanged();
void nameChanged();
void coordinatesChanged();
void activeChanged();
void projectionChanged();
void shouldDisplayChanged();
void urgentChanged();
void canActivateChanged();
void canDeactivateChanged();
void canRemoveChanged();
void canSetProjectionChanged();
protected:
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(Windowset, QString, bId, &Windowset::idChanged);
Q_OBJECT_BINDABLE_PROPERTY(Windowset, QString, bName, &Windowset::nameChanged);
Q_OBJECT_BINDABLE_PROPERTY(Windowset, QList<qint32>, bCoordinates);
Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bActive, &Windowset::activeChanged);
Q_OBJECT_BINDABLE_PROPERTY(Windowset, WindowsetProjection*, bProjection, &Windowset::projectionChanged);
Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bShouldDisplay, &Windowset::shouldDisplayChanged);
Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bUrgent, &Windowset::urgentChanged);
Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanActivate, &Windowset::canActivateChanged);
Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanDeactivate, &Windowset::canDeactivateChanged);
Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanRemove, &Windowset::canRemoveChanged);
Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanSetProjection, &Windowset::canSetProjectionChanged);
// clang-format on
};
///! A space occupiable by a Windowset.
/// A WindowsetProjection represents a space that can be occupied by one or more @@Windowset$s.
/// The space is one or more screens. Multiple projections may occupy the same screens.
///
/// @@WindowManager.screenProjection() can be used to get a projection representing all
/// @@Windowset$s on a given screen regardless of the WM's actual projection layout.
class WindowsetProjection: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_UNCREATABLE("");
// clang-format off
/// Screens the windowset projection spans, often a single screen or all screens.
Q_PROPERTY(QList<QuickshellScreenInfo*> screens READ default NOTIFY screensChanged BINDABLE bindableScreens);
/// Windowsets that are currently present on the projection.
Q_PROPERTY(QList<Windowset*> windowsets READ default NOTIFY windowsetsChanged BINDABLE bindableWindowsets);
// clang-format on
public:
explicit WindowsetProjection(QObject* parent);
[[nodiscard]] QBindable<QList<QuickshellScreenInfo*>> bindableScreens() const {
return &this->bScreens;
}
[[nodiscard]] QBindable<QList<QScreen*>> bindableQScreens() const { return &this->bQScreens; }
[[nodiscard]] QBindable<QList<Windowset*>> bindableWindowsets() const {
return &this->bWindowsets;
}
signals:
void screensChanged();
void windowsetsChanged();
protected:
Q_OBJECT_BINDABLE_PROPERTY(WindowsetProjection, QList<QScreen*>, bQScreens);
Q_OBJECT_BINDABLE_PROPERTY(
WindowsetProjection,
QList<QuickshellScreenInfo*>,
bScreens,
&WindowsetProjection::screensChanged
);
Q_OBJECT_BINDABLE_PROPERTY(
WindowsetProjection,
QList<Windowset*>,
bWindowsets,
&WindowsetProjection::windowsetsChanged
);
};
} // namespace qs::wm

View file

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

View file

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

View file

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