mirror of
https://git.outfoxxed.me/quickshell/quickshell.git
synced 2026-04-10 06:11:54 +10:00
Compare commits
26 commits
8e8f27a22a
...
b7005e09e7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7005e09e7 | ||
|
|
a51dcd0a01 | ||
|
|
97b2688ad6 | ||
|
|
0a859d51f2 | ||
|
|
1bd5b083cb | ||
|
|
365bf16b1e | ||
|
|
6705e2da77 | ||
|
|
9e8eecf2b8 | ||
|
|
1b2519d9f3 | ||
|
|
1123d5ab4f | ||
|
|
4b77936c80 | ||
|
|
e32b909354 | ||
|
|
178c04b59c | ||
|
|
706d6de7b0 | ||
|
|
9a9c605250 | ||
|
|
bd62179277 | ||
|
|
cf1a2aeb2d | ||
|
|
15a8409765 | ||
|
|
6bcd3d9bbf | ||
|
|
c030300191 | ||
|
|
5721955686 | ||
|
|
a849a88893 | ||
|
|
cdde4c63f4 | ||
|
|
cddb4f061b | ||
|
|
6e17efab83 | ||
|
|
36517a2c10 |
79 changed files with 2727 additions and 788 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
91
.github/ISSUE_TEMPLATE/crash.yml
vendored
91
.github/ISSUE_TEMPLATE/crash.yml
vendored
|
|
@ -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
49
.github/ISSUE_TEMPLATE/crash2.yml
vendored
Normal 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.
|
||||||
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
32
BUILD.md
32
BUILD.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
247
CONTRIBUTING.md
247
CONTRIBUTING.md
|
|
@ -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
226
HACKING.md
Normal 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.
|
||||||
2
Justfile
2
Justfile
|
|
@ -13,7 +13,7 @@ lint-changed:
|
||||||
git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
|
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}} \
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
21
default.nix
21
default.nix
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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" ".")))
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
175
src/core/debuginfo.cpp
Normal 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
14
src/core/debuginfo.hpp
Normal 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
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
98
src/core/streamreader.cpp
Normal 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
26
src/core/streamreader.hpp
Normal 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;
|
||||||
|
};
|
||||||
|
|
@ -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)>;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(); }
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
19
src/wayland/windowmanager/CMakeLists.txt
Normal file
19
src/wayland/windowmanager/CMakeLists.txt
Normal 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)
|
||||||
176
src/wayland/windowmanager/ext_workspace.cpp
Normal file
176
src/wayland/windowmanager/ext_workspace.cpp
Normal 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
|
||||||
117
src/wayland/windowmanager/ext_workspace.hpp
Normal file
117
src/wayland/windowmanager/ext_workspace.hpp
Normal 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
|
||||||
23
src/wayland/windowmanager/init.cpp
Normal file
23
src/wayland/windowmanager/init.cpp
Normal 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
|
||||||
21
src/wayland/windowmanager/windowmanager.cpp
Normal file
21
src/wayland/windowmanager/windowmanager.cpp
Normal 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
|
||||||
17
src/wayland/windowmanager/windowmanager.hpp
Normal file
17
src/wayland/windowmanager/windowmanager.hpp
Normal 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
|
||||||
252
src/wayland/windowmanager/windowset.cpp
Normal file
252
src/wayland/windowmanager/windowset.cpp
Normal 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
|
||||||
85
src/wayland/windowmanager/windowset.hpp
Normal file
85
src/wayland/windowmanager/windowset.hpp
Normal 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
|
||||||
42
src/wayland/wl_proxy_safe_deref.cpp
Normal file
42
src/wayland/wl_proxy_safe_deref.cpp
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
20
src/windowmanager/CMakeLists.txt
Normal file
20
src/windowmanager/CMakeLists.txt
Normal 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)
|
||||||
10
src/windowmanager/module.md
Normal file
10
src/windowmanager/module.md
Normal 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.
|
||||||
30
src/windowmanager/screenprojection.cpp
Normal file
30
src/windowmanager/screenprojection.cpp
Normal 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
|
||||||
34
src/windowmanager/screenprojection.hpp
Normal file
34
src/windowmanager/screenprojection.hpp
Normal 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
|
||||||
86
src/windowmanager/test/manual/WorkspaceDelegate.qml
Normal file
86
src/windowmanager/test/manual/WorkspaceDelegate.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/windowmanager/test/manual/screenproj.qml
Normal file
45
src/windowmanager/test/manual/screenproj.qml
Normal 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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/windowmanager/test/manual/workspaces.qml
Normal file
46
src/windowmanager/test/manual/workspaces.qml
Normal 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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/windowmanager/windowmanager.cpp
Normal file
41
src/windowmanager/windowmanager.cpp
Normal 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
|
||||||
91
src/windowmanager/windowmanager.hpp
Normal file
91
src/windowmanager/windowmanager.hpp
Normal 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
|
||||||
45
src/windowmanager/windowset.cpp
Normal file
45
src/windowmanager/windowset.cpp
Normal 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
|
||||||
175
src/windowmanager/windowset.hpp
Normal file
175
src/windowmanager/windowset.hpp
Normal 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
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue