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 |
77 changed files with 2707 additions and 771 deletions
|
|
@ -20,6 +20,7 @@ Checks: >
|
|||
-cppcoreguidelines-avoid-do-while,
|
||||
-cppcoreguidelines-pro-type-reinterpret-cast,
|
||||
-cppcoreguidelines-pro-type-vararg,
|
||||
-cppcoreguidelines-pro-type-union-access,
|
||||
-cppcoreguidelines-use-enum-class,
|
||||
google-global-names-in-headers,
|
||||
google-readability-casting,
|
||||
|
|
|
|||
91
.github/ISSUE_TEMPLATE/crash.yml
vendored
91
.github/ISSUE_TEMPLATE/crash.yml
vendored
|
|
@ -1,82 +1,17 @@
|
|||
name: Crash Report
|
||||
description: Quickshell has crashed
|
||||
name: Crash Report (v1)
|
||||
description: Quickshell has crashed (old)
|
||||
labels: ["bug", "crash"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: crashinfo
|
||||
- type: markdown
|
||||
attributes:
|
||||
label: General crash information
|
||||
description: |
|
||||
Paste the contents of the `info.txt` file in your crash folder here.
|
||||
value: "<details> <summary>General information</summary>
|
||||
|
||||
|
||||
```
|
||||
|
||||
<Paste the contents of the file here inside of the triple backticks>
|
||||
|
||||
```
|
||||
|
||||
|
||||
</details>"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: userinfo
|
||||
value: |
|
||||
Thank you for taking the time to click the report button.
|
||||
At this point most of the worst issues in 0.2.1 and before have been fixed and we are
|
||||
preparing for a new release. Please do not report crashes from 0.2.1 or before for now.
|
||||
- type: checkboxes
|
||||
id: donotcheck
|
||||
attributes:
|
||||
label: What caused the crash
|
||||
description: |
|
||||
Any information likely to help debug the crash. What were you doing when the crash occurred,
|
||||
what changes did you make, can you get it to happen again?
|
||||
- type: textarea
|
||||
id: dump
|
||||
attributes:
|
||||
label: Minidump
|
||||
description: |
|
||||
Attach `minidump.dmp.log` here. If it is too big to upload, compress it.
|
||||
|
||||
You may skip this step if quickshell crashed while processing a password
|
||||
or other sensitive information. If you skipped it write why instead.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Log file
|
||||
description: |
|
||||
Attach `log.qslog.log` here. If it is too big to upload, compress it.
|
||||
|
||||
You can preview the log if you'd like using `quickshell read-log <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.
|
||||
label: Do not check this box
|
||||
options:
|
||||
- label: Do not check this box
|
||||
required: true
|
||||
|
|
|
|||
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 \
|
||||
cli11 \
|
||||
polkit \
|
||||
jemalloc
|
||||
jemalloc \
|
||||
libunwind \
|
||||
git # for cpptrace clone
|
||||
|
||||
- name: Build
|
||||
# breakpad is annoying to build in ci due to makepkg not running as root
|
||||
run: |
|
||||
cmake -GNinja -B build -DCRASH_REPORTER=OFF
|
||||
cmake -GNinja -B build -DVENDOR_CPPTRACE=ON
|
||||
cmake --build build
|
||||
|
|
|
|||
32
BUILD.md
32
BUILD.md
|
|
@ -15,15 +15,7 @@ Please make this descriptive enough to identify your specific package, for examp
|
|||
- `Nixpkgs`
|
||||
- `Fedora COPR (errornointernet/quickshell)`
|
||||
|
||||
`-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=YES/NO`
|
||||
|
||||
If we can retrieve binaries and debug information for the package without actually running your
|
||||
distribution (e.g. from an website), and you would like to strip the binary, please set this to `YES`.
|
||||
|
||||
If we cannot retrieve debug information, please set this to `NO` and
|
||||
**ensure you aren't distributing stripped (non debuggable) binaries**.
|
||||
|
||||
In both cases you should build with `-DCMAKE_BUILD_TYPE=RelWithDebInfo` (then split or keep the debuginfo).
|
||||
If you are forking quickshell, please change `CRASHREPORT_URL` to your own issue tracker.
|
||||
|
||||
### QML Module dir
|
||||
Currently all QML modules are statically linked to quickshell, but this is where
|
||||
|
|
@ -41,6 +33,7 @@ Quickshell has a set of base dependencies you will always need, names vary by di
|
|||
- `cmake`
|
||||
- `qt6base`
|
||||
- `qt6declarative`
|
||||
- `libdrm`
|
||||
- `qtshadertools` (build-time)
|
||||
- `spirv-tools` (build-time)
|
||||
- `pkg-config` (build-time)
|
||||
|
|
@ -64,14 +57,24 @@ At least Qt 6.6 is required.
|
|||
|
||||
All features are enabled by default and some have their own dependencies.
|
||||
|
||||
### Crash Reporter
|
||||
The crash reporter catches crashes, restarts quickshell when it crashes,
|
||||
### Crash Handler
|
||||
The crash reporter catches crashes, restarts Quickshell when it crashes,
|
||||
and collects useful crash information in one place. Leaving this enabled will
|
||||
enable us to fix bugs far more easily.
|
||||
|
||||
To disable: `-DCRASH_REPORTER=OFF`
|
||||
To disable: `-DCRASH_HANDLER=OFF`
|
||||
|
||||
Dependencies: `google-breakpad` (static library)
|
||||
Dependencies: `cpptrace`
|
||||
|
||||
Note: `-DVENDOR_CPPTRACE=ON` can be set to vendor cpptrace using FetchContent.
|
||||
|
||||
When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the
|
||||
package manager or fetched with FetchContent.
|
||||
|
||||
*Please ensure binaries have usable symbols.* We do not necessarily need full debuginfo, but
|
||||
leaving symbols in the binary is extremely helpful. You can check if symbols are useful
|
||||
by sending a SIGSEGV to the process and ensuring symbols for the quickshell binary are present
|
||||
in the trace.
|
||||
|
||||
### Jemalloc
|
||||
We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused
|
||||
|
|
@ -144,7 +147,6 @@ Enables streaming video from monitors and toplevel windows through various proto
|
|||
To disable: `-DSCREENCOPY=OFF`
|
||||
|
||||
Dependencies:
|
||||
- `libdrm`
|
||||
- `libgbm`
|
||||
- `vulkan-headers` (build-time)
|
||||
|
||||
|
|
@ -240,7 +242,7 @@ Only `ninja` builds are tested, but makefiles may work.
|
|||
|
||||
#### Configuring the build
|
||||
```sh
|
||||
$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here]
|
||||
$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release [additional disable flags from above here]
|
||||
```
|
||||
|
||||
Note that features you do not supply dependencies for MUST be disabled with their associated flags
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||
|
||||
set(QS_BUILD_OPTIONS "")
|
||||
|
||||
# should be changed for forks
|
||||
set(CRASHREPORT_URL "https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml" CACHE STRING "Bugreport URL")
|
||||
|
||||
function(boption VAR NAME DEFAULT)
|
||||
cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "")
|
||||
|
||||
|
|
@ -40,19 +43,17 @@ string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}")
|
|||
|
||||
message(STATUS "Quickshell configuration")
|
||||
message(STATUS " Distributor: ${DISTRIBUTOR}")
|
||||
boption(DISTRIBUTOR_DEBUGINFO_AVAILABLE "Distributor provided debuginfo" NO)
|
||||
boption(NO_PCH "Disable precompild headers (dev)" OFF)
|
||||
boption(BUILD_TESTING "Build tests (dev)" OFF)
|
||||
boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang
|
||||
boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN})
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
|
||||
boption(CRASH_REPORTER "Crash Handling" OFF)
|
||||
boption(USE_JEMALLOC "Use jemalloc" OFF)
|
||||
else()
|
||||
boption(CRASH_REPORTER "Crash Handling" ON)
|
||||
boption(USE_JEMALLOC "Use jemalloc" ON)
|
||||
endif()
|
||||
boption(CRASH_HANDLER "Crash Handling" ON)
|
||||
boption(SOCKETS "Unix Sockets" ON)
|
||||
boption(WAYLAND "Wayland" ON)
|
||||
boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND)
|
||||
|
|
|
|||
247
CONTRIBUTING.md
247
CONTRIBUTING.md
|
|
@ -1,235 +1,40 @@
|
|||
# Contributing / Development
|
||||
Instructions for development setup and upstreaming patches.
|
||||
# Contributing
|
||||
|
||||
If you just want to build or package quickshell see [BUILD.md](BUILD.md).
|
||||
Thank you for taking the time to contribute.
|
||||
To ensure nobody's time is wasted, please follow the rules below.
|
||||
|
||||
## Development
|
||||
## Acceptable Code Contributions
|
||||
|
||||
Install the dependencies listed in [BUILD.md](BUILD.md).
|
||||
You probably want all of them even if you don't use all of them
|
||||
to ensure tests work correctly and avoid passing a bunch of configure
|
||||
flags when you need to wipe the build directory.
|
||||
- All changes submitted MUST be **fully understood by the submitter**. If you do not know why or how
|
||||
your change works, do not submit it to be merged. You must be able to explain your reasoning
|
||||
for every change.
|
||||
|
||||
Quickshell also uses `just` for common development command aliases.
|
||||
- Changes MUST be submitted by a human who will be responsible for them. Changes submitted without
|
||||
a human in the loop such as automated tooling and AI Agents are **strictly disallowed**. Accounts
|
||||
responsible for such contribution attempts **will be banned**.
|
||||
|
||||
The dependencies are also available as a nix shell or nix flake which we recommend
|
||||
using with nix-direnv.
|
||||
- Changes MUST respect Quickshell's license and the license of any source works. Changes including
|
||||
code from any other works must disclose the source of the code, explain why it was used, and
|
||||
ensure the license is compatible.
|
||||
|
||||
Common aliases:
|
||||
- `just configure [<debug|release> [extra cmake args]]` (note that you must specify debug/release to specify extra args)
|
||||
- `just build` - runs the build, configuring if not configured already.
|
||||
- `just run [args]` - runs quickshell with the given arguments
|
||||
- `just clean` - clean up build artifacts. `just clean build` is somewhat common.
|
||||
- Changes must follow the guidelines outlined in [HACKING.md](HACKING.md) for style and substance.
|
||||
|
||||
### Formatting
|
||||
All contributions should be formatted similarly to what already exists.
|
||||
Group related functionality together.
|
||||
- Changes must stand on their own as a unit. Do not make multiple unrelated changes in one PR.
|
||||
Changes depending on prior merges should be marked as a draft.
|
||||
|
||||
Run the formatter using `just fmt`.
|
||||
If the results look stupid, fix the clang-format file if possible,
|
||||
or disable clang-format in the affected area
|
||||
using `// clang-format off` and `// clang-format on`.
|
||||
## Acceptable Non-code Contributions
|
||||
|
||||
#### Style preferences not caught by clang-format
|
||||
These are flexible. You can ignore them if it looks or works better to
|
||||
for one reason or another.
|
||||
- Bug and crash reports. You must follow the instructions in the issue templates and provide the
|
||||
information requested.
|
||||
|
||||
Use `auto` if the type of a variable can be deduced automatically, instead of
|
||||
redeclaring the returned value's type. Additionally, auto should be used when a
|
||||
constructor takes arguments.
|
||||
- Feature requests can be made via Issues. Please check to ensure nobody else has requested the same feature.
|
||||
|
||||
```cpp
|
||||
auto x = <expr>; // ok
|
||||
auto x = QString::number(3); // ok
|
||||
QString x; // ok
|
||||
QString x = "foo"; // ok
|
||||
auto x = QString("foo"); // ok
|
||||
- Do not make insubstantial or pointless changes.
|
||||
|
||||
auto x = QString(); // avoid
|
||||
QString x(); // avoid
|
||||
QString x("foo"); // avoid
|
||||
```
|
||||
- Changes to project rules / policy / governance will not be entertained, except from significant
|
||||
long-term contributors. These changes should not be addressed through contribution channels.
|
||||
|
||||
Put newlines around logical units of code, and after closing braces. If the
|
||||
most reasonable logical unit of code takes only a single line, it should be
|
||||
merged into the next single line logical unit if applicable.
|
||||
```cpp
|
||||
// multiple units
|
||||
auto x = <expr>; // unit 1
|
||||
auto y = <expr>; // unit 2
|
||||
## Merge timelines
|
||||
|
||||
auto x = <expr>; // unit 1
|
||||
emit this->y(); // unit 2
|
||||
|
||||
auto x1 = <expr>; // unit 1
|
||||
auto x2 = <expr>; // unit 1
|
||||
auto x3 = <expr>; // unit 1
|
||||
|
||||
auto y1 = <expr>; // unit 2
|
||||
auto y2 = <expr>; // unit 2
|
||||
auto y3 = <expr>; // unit 2
|
||||
|
||||
// one unit
|
||||
auto x = <expr>;
|
||||
if (x...) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// if more than one variable needs to be used then add a newline
|
||||
auto x = <expr>;
|
||||
auto y = <expr>;
|
||||
|
||||
if (x && y) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Class formatting:
|
||||
```cpp
|
||||
//! Doc comment summary
|
||||
/// Doc comment body
|
||||
class Foo: public QObject {
|
||||
// The Q_OBJECT macro comes first. Macros are ; terminated.
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_CLASSINFO(...);
|
||||
// Properties must stay on a single line or the doc generator won't be able to pick them up
|
||||
Q_PROPERTY(...);
|
||||
/// Doc comment
|
||||
Q_PROPERTY(...);
|
||||
/// Doc comment
|
||||
Q_PROPERTY(...);
|
||||
|
||||
public:
|
||||
// Classes should have explicit constructors if they aren't intended to
|
||||
// implicitly cast. The constructor can be inline in the header if it has no body.
|
||||
explicit Foo(QObject* parent = nullptr): QObject(parent) {}
|
||||
|
||||
// Instance functions if applicable.
|
||||
static Foo* instance();
|
||||
|
||||
// Member functions unrelated to properties come next
|
||||
void function();
|
||||
void function();
|
||||
void function();
|
||||
|
||||
// Then Q_INVOKABLEs
|
||||
Q_INVOKABLE function();
|
||||
/// Doc comment
|
||||
Q_INVOKABLE function();
|
||||
/// Doc comment
|
||||
Q_INVOKABLE function();
|
||||
|
||||
// Then property related functions, in the order (bindable, getter, setter).
|
||||
// Related functions may be included here as well. Function bodies may be inline
|
||||
// if they are a single expression. There should be a newline between each
|
||||
// property's methods.
|
||||
[[nodiscard]] QBindable<T> bindableFoo() { return &this->bFoo; }
|
||||
[[nodiscard]] T foo() const { return this->foo; }
|
||||
void setFoo();
|
||||
|
||||
[[nodiscard]] T bar() const { return this->foo; }
|
||||
void setBar();
|
||||
|
||||
signals:
|
||||
// Signals that are not property change related go first.
|
||||
// Property change signals go in property definition order.
|
||||
void asd();
|
||||
void asd2();
|
||||
void fooChanged();
|
||||
void barChanged();
|
||||
|
||||
public slots:
|
||||
// generally Q_INVOKABLEs are preferred to public slots.
|
||||
void slot();
|
||||
|
||||
private slots:
|
||||
// ...
|
||||
|
||||
private:
|
||||
// statics, then functions, then fields
|
||||
static const foo BAR;
|
||||
static void foo();
|
||||
|
||||
void foo();
|
||||
void bar();
|
||||
|
||||
// property related members are prefixed with `m`.
|
||||
QString mFoo;
|
||||
QString bar;
|
||||
|
||||
// Bindables go last and should be prefixed with `b`.
|
||||
Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged);
|
||||
};
|
||||
```
|
||||
|
||||
### Linter
|
||||
All contributions should pass the linter.
|
||||
|
||||
Note that running the linter requires disabling precompiled
|
||||
headers and including the test codepaths:
|
||||
```sh
|
||||
$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
|
||||
$ just lint-changed
|
||||
```
|
||||
|
||||
If the linter is complaining about something that you think it should not,
|
||||
please disable the lint in your MR and explain your reasoning if it isn't obvious.
|
||||
|
||||
### Tests
|
||||
If you feel like the feature you are working on is very complex or likely to break,
|
||||
please write some tests. We will ask you to directly if you send in an MR for an
|
||||
overly complex or breakable feature.
|
||||
|
||||
At least all tests that passed before your changes should still be passing
|
||||
by the time your contribution is ready.
|
||||
|
||||
You can run the tests using `just test` but you must enable them first
|
||||
using `-DBUILD_TESTING=ON`.
|
||||
|
||||
### Documentation
|
||||
Most of quickshell's documentation is automatically generated from the source code.
|
||||
You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser
|
||||
cannot handle random line breaks and will usually require you to disable clang-format if the
|
||||
lines are too long.
|
||||
|
||||
Before submitting an MR, if adding new features please make sure the documentation is generated
|
||||
reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo.
|
||||
|
||||
Doc comments take the form `///` or `///!` (summary) and work with markdown.
|
||||
You can reference other types using the `@@[Module.][Type.][member]` shorthand
|
||||
where all parts are optional. If module or type are not specified they will
|
||||
be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`.
|
||||
Look at existing code for how it works.
|
||||
|
||||
Quickshell modules additionally have a `module.md` file which contains a summary, description,
|
||||
and list of headers to scan for documentation.
|
||||
|
||||
## Contributing
|
||||
|
||||
### Commits
|
||||
Please structure your commit messages as `scope[!]: commit` where
|
||||
the scope is something like `core` or `service/mpris`. (pick what has been
|
||||
used historically or what makes sense if new). Add `!` for changes that break
|
||||
existing APIs or functionality.
|
||||
|
||||
Commit descriptions should contain a summary of the changes if they are not
|
||||
sufficiently addressed in the commit message.
|
||||
|
||||
Please squash/rebase additions or edits to previous changes and follow the
|
||||
commit style to keep the history easily searchable at a glance.
|
||||
Depending on the change, it is often reasonable to squash it into just
|
||||
a single commit. (If you do not follow this we will squash your changes
|
||||
for you.)
|
||||
|
||||
### Sending patches
|
||||
You may contribute by submitting a pull request on github, asking for
|
||||
an account on our git server, or emailing patches / git bundles
|
||||
directly to `outfoxxed@outfoxxed.me`.
|
||||
|
||||
### Getting help
|
||||
If you're getting stuck, you can come talk to us in the
|
||||
[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me)
|
||||
for help on implementation, conventions, etc.
|
||||
Feel free to ask for advice early in your implementation if you are
|
||||
unsure.
|
||||
We handle work for the most part on a push basis. If your PR has been ignored for a while
|
||||
and is still relevant please bump it.
|
||||
|
|
|
|||
226
HACKING.md
Normal file
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") }}
|
||||
|
||||
lint-staged:
|
||||
git diff --staged --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
|
||||
git diff --staged --name-only --diff-filter=d HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
|
||||
|
||||
configure target='debug' *FLAGS='':
|
||||
cmake -GNinja -B {{builddir}} \
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ This repo is hosted at:
|
|||
- https://github.com/quickshell-mirror/quickshell
|
||||
|
||||
# Contributing / Development
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
||||
- [HACKING.md](HACKING.md) - Development instructions and policy.
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution policy.
|
||||
- [BUILD.md](BUILD.md) - Packaging and build instructions.
|
||||
|
||||
#### License
|
||||
|
||||
|
|
|
|||
|
|
@ -26,12 +26,18 @@ set shell id.
|
|||
- Added Quickshell version checking and version gated preprocessing.
|
||||
- Added a way to detect if an icon is from the system icon theme or not.
|
||||
- Added vulkan support to screencopy.
|
||||
- Added generic WindowManager interface implementing ext-workspace.
|
||||
|
||||
## Other Changes
|
||||
|
||||
- FreeBSD is now partially supported.
|
||||
- IPC operations filter available instances to the current display connection by default.
|
||||
- PwNodeLinkTracker ignores sound level monitoring programs.
|
||||
- Replaced breakpad with cpptrace.
|
||||
- Reloads are prevented if no file content has changed.
|
||||
- Added `QS_DISABLE_FILE_WATCHER` environment variable to disable file watching.
|
||||
- Added `QS_DISABLE_CRASH_HANDLER` environment variable to disable crash handling.
|
||||
- Added `QS_CRASHREPORT_URL` environment variable to allow overriding the crash reporter link.
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
|
|
@ -42,14 +48,23 @@ set shell id.
|
|||
- Fixed volumes not initializing if a pipewire device was already loaded before its node.
|
||||
- Fixed hyprland active toplevel not resetting after window closes.
|
||||
- Fixed hyprland ipc window names and titles being reversed.
|
||||
- Fixed a hyprland ipc crash when refreshing toplevels before workspaces.
|
||||
- Fixed missing signals for system tray item title and description updates.
|
||||
- Fixed asynchronous loaders not working after reload.
|
||||
- Fixed asynchronous loaders not working before window creation.
|
||||
- Fixed memory leak in IPC handlers.
|
||||
- Fixed ClippingRectangle related crashes.
|
||||
- Fixed crashes when monitors are unplugged.
|
||||
- Fixed crashes when default pipewire devices are lost.
|
||||
- Fixed ToplevelManager not clearing activeToplevel on deactivation.
|
||||
- Desktop action order is now preserved.
|
||||
- Fixed partial socket reads in greetd and hyprland on slow machines.
|
||||
- Worked around Qt bug causing crashes when plugging and unplugging monitors.
|
||||
|
||||
## Packaging Changes
|
||||
|
||||
`glib` and `polkit` have been added as dependencies when compiling with polkit agent support.
|
||||
`vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support).
|
||||
- `glib` and `polkit` have been added as dependencies when compiling with polkit agent support.
|
||||
- `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support).
|
||||
- `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore.
|
||||
- `DISTRIBUTOR_DEBUGINFO_AVAILABLE` was removed as it is no longer important without breakpad.
|
||||
- `libdrm` is now unconditionally required as a direct dependency.
|
||||
|
|
|
|||
21
default.nix
21
default.nix
|
|
@ -10,13 +10,16 @@
|
|||
ninja,
|
||||
spirv-tools,
|
||||
qt6,
|
||||
breakpad,
|
||||
cpptrace ? null,
|
||||
libunwind,
|
||||
libdwarf,
|
||||
jemalloc,
|
||||
cli11,
|
||||
wayland,
|
||||
wayland-protocols,
|
||||
wayland-scanner,
|
||||
xorg,
|
||||
libxcb ? xorg.libxcb,
|
||||
libdrm,
|
||||
libgbm ? null,
|
||||
vulkan-headers,
|
||||
|
|
@ -49,6 +52,8 @@
|
|||
withPolkit ? true,
|
||||
withNetworkManager ? true,
|
||||
}: let
|
||||
withCrashHandler = withCrashReporter && cpptrace != null && lib.strings.compareVersions cpptrace.version "0.7.2" >= 0;
|
||||
|
||||
unwrapped = stdenv.mkDerivation {
|
||||
pname = "quickshell${lib.optionalString debug "-debug"}";
|
||||
version = "0.2.1";
|
||||
|
|
@ -71,15 +76,21 @@
|
|||
buildInputs = [
|
||||
qt6.qtbase
|
||||
qt6.qtdeclarative
|
||||
libdrm
|
||||
cli11
|
||||
]
|
||||
++ lib.optional withQtSvg qt6.qtsvg
|
||||
++ lib.optional withCrashReporter breakpad
|
||||
++ lib.optional withCrashHandler (cpptrace.overrideAttrs (prev: {
|
||||
cmakeFlags = prev.cmakeFlags ++ [
|
||||
"-DCPPTRACE_UNWIND_WITH_LIBUNWIND=TRUE"
|
||||
];
|
||||
buildInputs = prev.buildInputs ++ [ libunwind ];
|
||||
}))
|
||||
++ lib.optional withJemalloc jemalloc
|
||||
++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
|
||||
++ lib.optionals withWayland [ wayland wayland-protocols ]
|
||||
++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm vulkan-headers ]
|
||||
++ lib.optional withX11 xorg.libxcb
|
||||
++ lib.optionals (withWayland && libgbm != null) [ libgbm vulkan-headers ]
|
||||
++ lib.optional withX11 libxcb
|
||||
++ lib.optional withPam pam
|
||||
++ lib.optional withPipewire pipewire
|
||||
++ lib.optionals withPolkit [ polkit glib ];
|
||||
|
|
@ -91,7 +102,7 @@
|
|||
(lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
|
||||
(lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true)
|
||||
(lib.cmakeFeature "GIT_REVISION" gitRev)
|
||||
(lib.cmakeBool "CRASH_REPORTER" withCrashReporter)
|
||||
(lib.cmakeBool "CRASH_HANDLER" withCrashHandler)
|
||||
(lib.cmakeBool "USE_JEMALLOC" withJemalloc)
|
||||
(lib.cmakeBool "WAYLAND" withWayland)
|
||||
(lib.cmakeBool "SCREENCOPY" (libgbm != null))
|
||||
|
|
|
|||
|
|
@ -56,8 +56,7 @@
|
|||
#~(list "-GNinja"
|
||||
"-DDISTRIBUTOR=\"In-tree Guix channel\""
|
||||
"-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO"
|
||||
;; Breakpad is not currently packaged for Guix.
|
||||
"-DCRASH_REPORTER=OFF")
|
||||
"-DCRASH_HANDLER=OFF")
|
||||
#:phases
|
||||
#~(modify-phases %standard-phases
|
||||
(replace 'build (lambda _ (invoke "cmake" "--build" ".")))
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ add_subdirectory(window)
|
|||
add_subdirectory(io)
|
||||
add_subdirectory(widgets)
|
||||
add_subdirectory(ui)
|
||||
add_subdirectory(windowmanager)
|
||||
|
||||
if (CRASH_REPORTER)
|
||||
if (CRASH_HANDLER)
|
||||
add_subdirectory(crash)
|
||||
endif()
|
||||
|
||||
|
|
|
|||
|
|
@ -9,16 +9,10 @@ if (NOT DEFINED GIT_REVISION)
|
|||
)
|
||||
endif()
|
||||
|
||||
if (CRASH_REPORTER)
|
||||
set(CRASH_REPORTER_DEF 1)
|
||||
if (CRASH_HANDLER)
|
||||
set(CRASH_HANDLER_DEF 1)
|
||||
else()
|
||||
set(CRASH_REPORTER_DEF 0)
|
||||
endif()
|
||||
|
||||
if (DISTRIBUTOR_DEBUGINFO_AVAILABLE)
|
||||
set(DEBUGINFO_AVAILABLE 1)
|
||||
else()
|
||||
set(DEBUGINFO_AVAILABLE 0)
|
||||
set(CRASH_HANDLER_DEF 0)
|
||||
endif()
|
||||
|
||||
configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES)
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@
|
|||
#define QS_UNRELEASED_FEATURES "@UNRELEASED_FEATURES@"
|
||||
#define GIT_REVISION "@GIT_REVISION@"
|
||||
#define DISTRIBUTOR "@DISTRIBUTOR@"
|
||||
#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@
|
||||
#define CRASH_REPORTER @CRASH_REPORTER_DEF@
|
||||
#define CRASH_HANDLER @CRASH_HANDLER_DEF@
|
||||
#define BUILD_TYPE "@CMAKE_BUILD_TYPE@"
|
||||
#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)"
|
||||
#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@"
|
||||
#define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@"
|
||||
#define CRASHREPORT_URL "@CRASHREPORT_URL@"
|
||||
// NOLINTEND
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
pkg_check_modules(libdrm REQUIRED IMPORTED_TARGET libdrm)
|
||||
qt_add_library(quickshell-core STATIC
|
||||
plugin.cpp
|
||||
shell.cpp
|
||||
|
|
@ -40,6 +41,8 @@ qt_add_library(quickshell-core STATIC
|
|||
scriptmodel.cpp
|
||||
colorquantizer.cpp
|
||||
toolsupport.cpp
|
||||
streamreader.cpp
|
||||
debuginfo.cpp
|
||||
)
|
||||
|
||||
qt_add_qml_module(quickshell-core
|
||||
|
|
@ -52,7 +55,7 @@ qt_add_qml_module(quickshell-core
|
|||
|
||||
install_qml_module(quickshell-core)
|
||||
|
||||
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build)
|
||||
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build PkgConfig::libdrm)
|
||||
|
||||
qs_module_pch(quickshell-core SET large)
|
||||
|
||||
|
|
|
|||
175
src/core/debuginfo.cpp
Normal file
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 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 (entries.value("Type").second != "Application") return;
|
||||
|
||||
|
|
@ -129,9 +132,10 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString&
|
|||
else if (key == "Terminal") data.terminal = value == "true";
|
||||
else if (key == "Categories") data.categories = value.split(u';', Qt::SkipEmptyParts);
|
||||
else if (key == "Keywords") data.keywords = value.split(u';', Qt::SkipEmptyParts);
|
||||
else if (key == "Actions") actionOrder = value.split(u';', Qt::SkipEmptyParts);
|
||||
}
|
||||
} else if (groupName.startsWith("Desktop Action ")) {
|
||||
auto actionName = groupName.sliced(16);
|
||||
auto actionName = groupName.sliced(15);
|
||||
DesktopActionData action;
|
||||
action.id = actionName;
|
||||
|
||||
|
|
@ -147,7 +151,7 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString&
|
|||
}
|
||||
}
|
||||
|
||||
data.actions.insert(actionName, action);
|
||||
pendingActions.insert(actionName, action);
|
||||
}
|
||||
|
||||
entries.clear();
|
||||
|
|
@ -193,6 +197,13 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString&
|
|||
}
|
||||
|
||||
finishCategory();
|
||||
|
||||
for (const auto& actionId: actionOrder) {
|
||||
if (pendingActions.contains(actionId)) {
|
||||
data.actions.append(pendingActions.value(actionId));
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
|
@ -216,17 +227,18 @@ void DesktopEntry::updateState(const ParsedDesktopEntryData& newState) {
|
|||
this->updateActions(newState.actions);
|
||||
}
|
||||
|
||||
void DesktopEntry::updateActions(const QHash<QString, DesktopActionData>& newActions) {
|
||||
void DesktopEntry::updateActions(const QVector<DesktopActionData>& newActions) {
|
||||
auto old = this->mActions;
|
||||
this->mActions.clear();
|
||||
|
||||
for (const auto& [key, d]: newActions.asKeyValueRange()) {
|
||||
for (const auto& d: newActions) {
|
||||
DesktopAction* act = nullptr;
|
||||
if (auto found = old.find(key); found != old.end()) {
|
||||
act = found.value();
|
||||
auto found = std::ranges::find(old, d.id, &DesktopAction::mId);
|
||||
if (found != old.end()) {
|
||||
act = *found;
|
||||
old.erase(found);
|
||||
} else {
|
||||
act = new DesktopAction(d.id, this);
|
||||
this->mActions.insert(key, act);
|
||||
}
|
||||
|
||||
Qt::beginPropertyUpdateGroup();
|
||||
|
|
@ -237,6 +249,7 @@ void DesktopEntry::updateActions(const QHash<QString, DesktopActionData>& newAct
|
|||
Qt::endPropertyUpdateGroup();
|
||||
|
||||
act->mEntries = d.entries;
|
||||
this->mActions.append(act);
|
||||
}
|
||||
|
||||
for (auto* leftover: old) {
|
||||
|
|
@ -250,7 +263,7 @@ void DesktopEntry::execute() const {
|
|||
|
||||
bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); }
|
||||
|
||||
QVector<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> arguments;
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ struct ParsedDesktopEntryData {
|
|||
QVector<QString> categories;
|
||||
QVector<QString> keywords;
|
||||
QHash<QString, QString> entries;
|
||||
QHash<QString, DesktopActionData> actions;
|
||||
QVector<DesktopActionData> actions;
|
||||
};
|
||||
|
||||
/// A desktop entry. See @@DesktopEntries for details.
|
||||
|
|
@ -164,10 +164,10 @@ public:
|
|||
// clang-format on
|
||||
|
||||
private:
|
||||
void updateActions(const QHash<QString, DesktopActionData>& newActions);
|
||||
void updateActions(const QVector<DesktopActionData>& newActions);
|
||||
|
||||
ParsedDesktopEntryData state;
|
||||
QHash<QString, DesktopAction*> mActions;
|
||||
QVector<DesktopAction*> mActions;
|
||||
|
||||
friend class DesktopAction;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -209,6 +209,8 @@ bool EngineGeneration::setExtraWatchedFiles(const QVector<QString>& files) {
|
|||
for (const auto& file: files) {
|
||||
if (!this->scanner.scannedFiles.contains(file)) {
|
||||
this->extraWatchedFiles.append(file);
|
||||
QByteArray data;
|
||||
this->scanner.readAndHashFile(file, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -229,6 +231,11 @@ void EngineGeneration::onFileChanged(const QString& name) {
|
|||
auto fileInfo = QFileInfo(name);
|
||||
if (fileInfo.isFile() && fileInfo.size() == 0) return;
|
||||
|
||||
if (!this->scanner.hasFileContentChanged(name)) {
|
||||
qCDebug(logQmlScanner) << "Ignoring file change with unchanged content:" << name;
|
||||
return;
|
||||
}
|
||||
|
||||
emit this->filesChanged();
|
||||
}
|
||||
}
|
||||
|
|
@ -237,6 +244,11 @@ void EngineGeneration::onDirectoryChanged() {
|
|||
// try to find any files that were just deleted from a replace operation
|
||||
for (auto& file: this->deletedWatchedFiles) {
|
||||
if (QFileInfo(file).exists()) {
|
||||
if (!this->scanner.hasFileContentChanged(file)) {
|
||||
qCDebug(logQmlScanner) << "Ignoring restored file with unchanged content:" << file;
|
||||
continue;
|
||||
}
|
||||
|
||||
emit this->filesChanged();
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ namespace qs::crash {
|
|||
|
||||
struct CrashInfo {
|
||||
int logFd = -1;
|
||||
int traceFd = -1;
|
||||
int infoFd = -1;
|
||||
|
||||
static CrashInfo INSTANCE; // NOLINT
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@
|
|||
#include <sys/sendfile.h>
|
||||
#include <sys/types.h>
|
||||
#endif
|
||||
#ifdef __FreeBSD__
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "instanceinfo.hpp"
|
||||
#include "logcat.hpp"
|
||||
|
|
@ -67,7 +70,7 @@ bool copyFileData(int sourceFd, int destFd, qint64 size) {
|
|||
return true;
|
||||
#else
|
||||
std::array<char, 64 * 1024> buffer = {};
|
||||
auto remaining = totalTarget;
|
||||
auto remaining = usize;
|
||||
|
||||
while (remaining > 0) {
|
||||
auto chunk = std::min(remaining, buffer.size());
|
||||
|
|
|
|||
|
|
@ -60,7 +60,9 @@ void QuickshellSettings::setWorkingDirectory(QString workingDirectory) { // NOLI
|
|||
emit this->workingDirectoryChanged();
|
||||
}
|
||||
|
||||
bool QuickshellSettings::watchFiles() const { return this->mWatchFiles; }
|
||||
bool QuickshellSettings::watchFiles() const {
|
||||
return this->mWatchFiles && qEnvironmentVariableIsEmpty("QS_DISABLE_FILE_WATCHER");
|
||||
}
|
||||
|
||||
void QuickshellSettings::setWatchFiles(bool watchFiles) {
|
||||
if (watchFiles == this->mWatchFiles) return;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <utility>
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qcryptographichash.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qjsengine.h>
|
||||
|
|
@ -21,6 +22,25 @@
|
|||
|
||||
QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg);
|
||||
|
||||
bool QmlScanner::readAndHashFile(const QString& path, QByteArray& data) {
|
||||
auto file = QFile(path);
|
||||
if (!file.open(QFile::ReadOnly)) return false;
|
||||
data = file.readAll();
|
||||
this->fileHashes.insert(path, QCryptographicHash::hash(data, QCryptographicHash::Md5));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QmlScanner::hasFileContentChanged(const QString& path) const {
|
||||
auto it = this->fileHashes.constFind(path);
|
||||
if (it == this->fileHashes.constEnd()) return true;
|
||||
|
||||
auto file = QFile(path);
|
||||
if (!file.open(QFile::ReadOnly)) return true;
|
||||
|
||||
auto newHash = QCryptographicHash::hash(file.readAll(), QCryptographicHash::Md5);
|
||||
return newHash != it.value();
|
||||
}
|
||||
|
||||
void QmlScanner::scanDir(const QDir& dir) {
|
||||
if (this->scannedDirs.contains(dir)) return;
|
||||
this->scannedDirs.push_back(dir);
|
||||
|
|
@ -109,13 +129,13 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna
|
|||
|
||||
qCDebug(logQmlScanner) << "Scanning qml file" << path;
|
||||
|
||||
auto file = QFile(path);
|
||||
if (!file.open(QFile::ReadOnly | QFile::Text)) {
|
||||
QByteArray fileData;
|
||||
if (!this->readAndHashFile(path, fileData)) {
|
||||
qCWarning(logQmlScanner) << "Failed to open file" << path;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto stream = QTextStream(&file);
|
||||
auto stream = QTextStream(&fileData);
|
||||
auto imports = QVector<QString>();
|
||||
|
||||
bool inHeader = true;
|
||||
|
|
@ -219,8 +239,6 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna
|
|||
postError("unclosed preprocessor if block");
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
if (isOverridden) {
|
||||
this->fileIntercepts.insert(path, overrideText);
|
||||
}
|
||||
|
|
@ -257,8 +275,11 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna
|
|||
continue;
|
||||
}
|
||||
|
||||
if (import.endsWith(".js")) this->scannedFiles.push_back(cpath);
|
||||
else this->scanDir(cpath);
|
||||
if (import.endsWith(".js")) {
|
||||
this->scannedFiles.push_back(cpath);
|
||||
QByteArray jsData;
|
||||
this->readAndHashFile(cpath, jsData);
|
||||
} else this->scanDir(cpath);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -273,14 +294,12 @@ void QmlScanner::scanQmlRoot(const QString& path) {
|
|||
bool QmlScanner::scanQmlJson(const QString& path) {
|
||||
qCDebug(logQmlScanner) << "Scanning qml.json file" << path;
|
||||
|
||||
auto file = QFile(path);
|
||||
if (!file.open(QFile::ReadOnly | QFile::Text)) {
|
||||
QByteArray data;
|
||||
if (!this->readAndHashFile(path, data)) {
|
||||
qCWarning(logQmlScanner) << "Failed to open file" << path;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto data = file.readAll();
|
||||
|
||||
// Importing this makes CI builds fail for some reason.
|
||||
QJsonParseError error; // NOLINT (misc-include-cleaner)
|
||||
auto json = QJsonDocument::fromJson(data, &error);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <qbytearray.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdir.h>
|
||||
#include <qhash.h>
|
||||
|
|
@ -21,6 +22,7 @@ public:
|
|||
|
||||
QVector<QDir> scannedDirs;
|
||||
QVector<QString> scannedFiles;
|
||||
QHash<QString, QByteArray> fileHashes;
|
||||
QHash<QString, QString> fileIntercepts;
|
||||
|
||||
struct ScanError {
|
||||
|
|
@ -31,6 +33,9 @@ public:
|
|||
|
||||
QVector<ScanError> scanErrors;
|
||||
|
||||
bool readAndHashFile(const QString& path, QByteArray& data);
|
||||
[[nodiscard]] bool hasFileContentChanged(const QString& path) const;
|
||||
|
||||
private:
|
||||
QDir rootPath;
|
||||
|
||||
|
|
|
|||
98
src/core/streamreader.cpp
Normal file
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); }
|
||||
};
|
||||
|
||||
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>
|
||||
class MethodFunctor {
|
||||
using PtrMeta = MemberPointerTraits<decltype(methodPtr)>;
|
||||
|
|
|
|||
|
|
@ -6,12 +6,51 @@ qt_add_library(quickshell-crash STATIC
|
|||
|
||||
qs_pch(quickshell-crash SET large)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(breakpad REQUIRED IMPORTED_TARGET breakpad)
|
||||
# only need client?? take only includes from pkg config todo
|
||||
target_link_libraries(quickshell-crash PRIVATE PkgConfig::breakpad -lbreakpad_client)
|
||||
if (VENDOR_CPPTRACE)
|
||||
message(STATUS "Vendoring cpptrace...")
|
||||
include(FetchContent)
|
||||
|
||||
# For use without internet access see: https://cmake.org/cmake/help/latest/module/FetchContent.html#variable:FETCHCONTENT_SOURCE_DIR_%3CuppercaseName%3E
|
||||
FetchContent_Declare(
|
||||
cpptrace
|
||||
GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git
|
||||
GIT_TAG v1.0.4
|
||||
)
|
||||
|
||||
set(CPPTRACE_UNWIND_WITH_LIBUNWIND TRUE)
|
||||
FetchContent_MakeAvailable(cpptrace)
|
||||
else ()
|
||||
find_package(cpptrace REQUIRED)
|
||||
|
||||
# useful for cross after you have already checked cpptrace is built correctly
|
||||
if (NOT DO_NOT_CHECK_CPPTRACE_USABILITY)
|
||||
try_run(CPPTRACE_SIGNAL_SAFE_UNWIND CPPTRACE_SIGNAL_SAFE_UNWIND_COMP
|
||||
SOURCE_FROM_CONTENT check.cxx "
|
||||
#include <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
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
#include "handler.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include <bits/types/sigset_t.h>
|
||||
#include <breakpad/client/linux/handler/exception_handler.h>
|
||||
#include <breakpad/client/linux/handler/minidump_descriptor.h>
|
||||
#include <breakpad/common/linux/linux_libc_support.h>
|
||||
#include <cpptrace/basic.hpp>
|
||||
#include <cpptrace/forward.hpp>
|
||||
#include <qdatastream.h>
|
||||
#include <qfile.h>
|
||||
#include <qlogging.h>
|
||||
|
|
@ -19,98 +20,75 @@
|
|||
|
||||
extern char** environ; // NOLINT
|
||||
|
||||
using namespace google_breakpad;
|
||||
|
||||
namespace qs::crash {
|
||||
|
||||
namespace {
|
||||
|
||||
QS_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg);
|
||||
}
|
||||
|
||||
struct CrashHandlerPrivate {
|
||||
ExceptionHandler* exceptionHandler = nullptr;
|
||||
int minidumpFd = -1;
|
||||
int infoFd = -1;
|
||||
void writeEnvInt(char* buf, const char* name, int value) {
|
||||
// NOLINTBEGIN (cppcoreguidelines-pro-bounds-pointer-arithmetic)
|
||||
while (*name != '\0') *buf++ = *name++;
|
||||
*buf++ = '=';
|
||||
|
||||
static bool minidumpCallback(const MinidumpDescriptor& descriptor, void* context, bool succeeded);
|
||||
};
|
||||
|
||||
CrashHandler::CrashHandler(): d(new CrashHandlerPrivate()) {}
|
||||
|
||||
void CrashHandler::init() {
|
||||
// MinidumpDescriptor has no move constructor and the copy constructor breaks fds.
|
||||
auto createHandler = [this](const MinidumpDescriptor& desc) {
|
||||
this->d->exceptionHandler = new ExceptionHandler(
|
||||
desc,
|
||||
nullptr,
|
||||
&CrashHandlerPrivate::minidumpCallback,
|
||||
this->d,
|
||||
true,
|
||||
-1
|
||||
);
|
||||
};
|
||||
|
||||
qCDebug(logCrashHandler) << "Starting crash handler...";
|
||||
|
||||
this->d->minidumpFd = memfd_create("quickshell:minidump", MFD_CLOEXEC);
|
||||
|
||||
if (this->d->minidumpFd == -1) {
|
||||
qCCritical(
|
||||
logCrashHandler
|
||||
) << "Failed to allocate minidump memfd, minidumps will be saved in the working directory.";
|
||||
createHandler(MinidumpDescriptor("."));
|
||||
} else {
|
||||
qCDebug(logCrashHandler) << "Created memfd" << this->d->minidumpFd
|
||||
<< "for holding possible minidumps.";
|
||||
createHandler(MinidumpDescriptor(this->d->minidumpFd));
|
||||
if (value < 0) {
|
||||
*buf++ = '-';
|
||||
value = -value;
|
||||
}
|
||||
|
||||
qCInfo(logCrashHandler) << "Crash handler initialized.";
|
||||
}
|
||||
|
||||
void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) {
|
||||
this->d->infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC);
|
||||
|
||||
if (this->d->infoFd == -1) {
|
||||
qCCritical(
|
||||
logCrashHandler
|
||||
) << "Failed to allocate instance info memfd, crash recovery will not work.";
|
||||
if (value == 0) {
|
||||
*buf++ = '0';
|
||||
*buf = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file;
|
||||
|
||||
if (!file.open(this->d->infoFd, QFile::ReadWrite)) {
|
||||
qCCritical(
|
||||
logCrashHandler
|
||||
) << "Failed to open instance info memfd, crash recovery will not work.";
|
||||
auto* start = buf;
|
||||
while (value > 0) {
|
||||
*buf++ = static_cast<char>('0' + (value % 10));
|
||||
value /= 10;
|
||||
}
|
||||
|
||||
QDataStream ds(&file);
|
||||
ds << info;
|
||||
file.flush();
|
||||
|
||||
qCDebug(logCrashHandler) << "Stored instance info in memfd" << this->d->infoFd;
|
||||
*buf = '\0';
|
||||
std::reverse(start, buf);
|
||||
// NOLINTEND
|
||||
}
|
||||
|
||||
CrashHandler::~CrashHandler() {
|
||||
delete this->d->exceptionHandler;
|
||||
delete this->d;
|
||||
}
|
||||
|
||||
bool CrashHandlerPrivate::minidumpCallback(
|
||||
const MinidumpDescriptor& /*descriptor*/,
|
||||
void* context,
|
||||
bool /*success*/
|
||||
void signalHandler(
|
||||
int sig,
|
||||
siginfo_t* /*info*/, // NOLINT (misc-include-cleaner)
|
||||
void* /*context*/
|
||||
) {
|
||||
// A fork that just dies to ensure the coredump is caught by the system.
|
||||
auto coredumpPid = fork();
|
||||
if (CrashInfo::INSTANCE.traceFd != -1) {
|
||||
auto traceBuffer = std::array<cpptrace::frame_ptr, 1024>();
|
||||
auto frameCount = cpptrace::safe_generate_raw_trace(traceBuffer.data(), traceBuffer.size(), 1);
|
||||
|
||||
if (coredumpPid == 0) {
|
||||
return false;
|
||||
for (size_t i = 0; i < static_cast<size_t>(frameCount); i++) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
fail:;
|
||||
}
|
||||
|
||||
auto* self = static_cast<CrashHandlerPrivate*>(context);
|
||||
auto coredumpPid = fork();
|
||||
if (coredumpPid == 0) {
|
||||
// NOLINTBEGIN (misc-include-cleaner)
|
||||
sigset_t set;
|
||||
sigfillset(&set);
|
||||
sigprocmask(SIG_UNBLOCK, &set, nullptr);
|
||||
// NOLINTEND
|
||||
raise(sig);
|
||||
_exit(-1);
|
||||
}
|
||||
|
||||
auto exe = std::array<char, 4096>();
|
||||
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 envi = 0;
|
||||
|
||||
auto infoFd = dup(self->infoFd);
|
||||
auto infoFdStr = std::array<char, 38>();
|
||||
memcpy(infoFdStr.data(), "__QUICKSHELL_CRASH_INFO_FD=-1" /*\0*/, 30);
|
||||
if (infoFd != -1) my_uitos(&infoFdStr[27], infoFd, 10);
|
||||
// dup to remove CLOEXEC
|
||||
auto infoFdStr = std::array<char, 48>();
|
||||
writeEnvInt(infoFdStr.data(), "__QUICKSHELL_CRASH_INFO_FD", dup(CrashInfo::INSTANCE.infoFd));
|
||||
env[envi++] = infoFdStr.data();
|
||||
|
||||
auto corePidStr = std::array<char, 39>();
|
||||
memcpy(corePidStr.data(), "__QUICKSHELL_CRASH_DUMP_PID=-1" /*\0*/, 31);
|
||||
if (coredumpPid != -1) my_uitos(&corePidStr[28], coredumpPid, 10);
|
||||
auto corePidStr = std::array<char, 48>();
|
||||
writeEnvInt(corePidStr.data(), "__QUICKSHELL_CRASH_DUMP_PID", coredumpPid);
|
||||
env[envi++] = corePidStr.data();
|
||||
|
||||
auto sigStr = std::array<char, 48>();
|
||||
writeEnvInt(sigStr.data(), "__QUICKSHELL_CRASH_SIGNAL", sig);
|
||||
env[envi++] = sigStr.data();
|
||||
|
||||
auto populateEnv = [&]() {
|
||||
auto senvi = 0;
|
||||
while (envi != 4095) {
|
||||
|
|
@ -145,30 +125,18 @@ bool CrashHandlerPrivate::minidumpCallback(
|
|||
env[envi] = nullptr;
|
||||
};
|
||||
|
||||
sigset_t sigset;
|
||||
sigemptyset(&sigset); // NOLINT (include)
|
||||
sigprocmask(SIG_SETMASK, &sigset, nullptr); // NOLINT
|
||||
|
||||
auto pid = fork();
|
||||
|
||||
if (pid == -1) {
|
||||
perror("Failed to fork and launch crash reporter.\n");
|
||||
return false;
|
||||
_exit(-1);
|
||||
} else if (pid == 0) {
|
||||
|
||||
// dup to remove CLOEXEC
|
||||
// if already -1 will return -1
|
||||
auto dumpFd = dup(self->minidumpFd);
|
||||
auto logFd = dup(CrashInfo::INSTANCE.logFd);
|
||||
|
||||
// allow up to 10 digits, which should never happen
|
||||
auto dumpFdStr = std::array<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);
|
||||
auto dumpFdStr = std::array<char, 48>();
|
||||
auto logFdStr = std::array<char, 48>();
|
||||
writeEnvInt(dumpFdStr.data(), "__QUICKSHELL_CRASH_DUMP_FD", dup(CrashInfo::INSTANCE.traceFd));
|
||||
writeEnvInt(logFdStr.data(), "__QUICKSHELL_CRASH_LOG_FD", dup(CrashInfo::INSTANCE.logFd));
|
||||
|
||||
env[envi++] = dumpFdStr.data();
|
||||
env[envi++] = logFdStr.data();
|
||||
|
|
@ -185,8 +153,82 @@ bool CrashHandlerPrivate::minidumpCallback(
|
|||
perror("Failed to relaunch quickshell.\n");
|
||||
_exit(-1);
|
||||
}
|
||||
}
|
||||
|
||||
return false; // should make sure it hits the system coredump handler
|
||||
} // namespace
|
||||
|
||||
void CrashHandler::init() {
|
||||
qCDebug(logCrashHandler) << "Starting crash handler...";
|
||||
|
||||
CrashInfo::INSTANCE.traceFd = memfd_create("quickshell:trace", MFD_CLOEXEC);
|
||||
|
||||
if (CrashInfo::INSTANCE.traceFd == -1) {
|
||||
qCCritical(logCrashHandler) << "Failed to allocate trace memfd, stack traces will not be "
|
||||
"available in crash reports.";
|
||||
} else {
|
||||
qCDebug(logCrashHandler) << "Created memfd" << CrashInfo::INSTANCE.traceFd
|
||||
<< "for holding possible stack traces.";
|
||||
}
|
||||
|
||||
{
|
||||
// Preload anything dynamically linked to avoid malloc etc in the dynamic loader.
|
||||
// See cpptrace documentation for more information.
|
||||
auto buffer = std::array<cpptrace::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
|
||||
|
|
|
|||
|
|
@ -5,19 +5,10 @@
|
|||
#include "../core/instanceinfo.hpp"
|
||||
namespace qs::crash {
|
||||
|
||||
struct CrashHandlerPrivate;
|
||||
|
||||
class CrashHandler {
|
||||
public:
|
||||
explicit CrashHandler();
|
||||
~CrashHandler();
|
||||
Q_DISABLE_COPY_MOVE(CrashHandler);
|
||||
|
||||
void init();
|
||||
void setRelaunchInfo(const RelaunchInfo& info);
|
||||
|
||||
private:
|
||||
CrashHandlerPrivate* d;
|
||||
static void init();
|
||||
static void setRelaunchInfo(const RelaunchInfo& info);
|
||||
};
|
||||
|
||||
} // namespace qs::crash
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include <qapplication.h>
|
||||
#include <qboxlayout.h>
|
||||
#include <qconfig.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdesktopservices.h>
|
||||
#include <qfont.h>
|
||||
#include <qfontinfo.h>
|
||||
|
|
@ -12,11 +13,22 @@
|
|||
#include <qnamespace.h>
|
||||
#include <qobject.h>
|
||||
#include <qpushbutton.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
#include <qtversion.h>
|
||||
#include <qwidget.h>
|
||||
|
||||
#include "build.hpp"
|
||||
|
||||
namespace {
|
||||
QString crashreportUrl() {
|
||||
if (auto url = qEnvironmentVariable("QS_CRASHREPORT_URL"); !url.isEmpty()) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return CRASHREPORT_URL;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class ReportLabel: public QWidget {
|
||||
public:
|
||||
ReportLabel(const QString& label, const QString& content, QWidget* parent): QWidget(parent) {
|
||||
|
|
@ -67,22 +79,16 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid)
|
|||
|
||||
if (qtVersionMatches) {
|
||||
mainLayout->addWidget(
|
||||
new QLabel("Please open a bug report for this issue via github or email.")
|
||||
new QLabel("Please open a bug report for this issue on the issue tracker.")
|
||||
);
|
||||
} else {
|
||||
mainLayout->addWidget(new QLabel(
|
||||
"Please rebuild Quickshell against the current Qt version.\n"
|
||||
"If this does not solve the problem, please open a bug report via github or email."
|
||||
"If this does not solve the problem, please open a bug report on the issue tracker."
|
||||
));
|
||||
}
|
||||
|
||||
mainLayout->addWidget(new ReportLabel(
|
||||
"Github:",
|
||||
"https://github.com/quickshell-mirror/quickshell/issues/new?template=crash.yml",
|
||||
this
|
||||
));
|
||||
|
||||
mainLayout->addWidget(new ReportLabel("Email:", "quickshell-bugs@outfoxxed.me", this));
|
||||
mainLayout->addWidget(new ReportLabel("Tracker:", crashreportUrl(), this));
|
||||
|
||||
auto* buttons = new QWidget(this);
|
||||
buttons->setMinimumWidth(900);
|
||||
|
|
@ -112,10 +118,5 @@ void CrashReporterGui::openFolder() {
|
|||
QDesktopServices::openUrl(QUrl::fromLocalFile(this->reportFolder));
|
||||
}
|
||||
|
||||
void CrashReporterGui::openReportUrl() {
|
||||
QDesktopServices::openUrl(
|
||||
QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash.yml")
|
||||
);
|
||||
}
|
||||
|
||||
void CrashReporterGui::openReportUrl() { QDesktopServices::openUrl(QUrl(crashreportUrl())); }
|
||||
void CrashReporterGui::cancel() { QApplication::quit(); }
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
#include "main.hpp"
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include <cpptrace/basic.hpp>
|
||||
#include <cpptrace/formatting.hpp>
|
||||
#include <qapplication.h>
|
||||
#include <qconfig.h>
|
||||
#include <qcoreapplication.h>
|
||||
#include <qdatastream.h>
|
||||
#include <qdir.h>
|
||||
|
|
@ -12,15 +14,18 @@
|
|||
#include <qloggingcategory.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
#include <qtextstream.h>
|
||||
#include <qtversion.h>
|
||||
#include <qtypes.h>
|
||||
#include <sys/sendfile.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "../core/debuginfo.hpp"
|
||||
#include "../core/instanceinfo.hpp"
|
||||
#include "../core/logcat.hpp"
|
||||
#include "../core/logging.hpp"
|
||||
#include "../core/logging_p.hpp"
|
||||
#include "../core/paths.hpp"
|
||||
#include "build.hpp"
|
||||
#include "../core/ringbuf.hpp"
|
||||
#include "interface.hpp"
|
||||
|
||||
namespace {
|
||||
|
|
@ -61,6 +66,76 @@ int tryDup(int fd, const QString& path) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
QString readRecentLogs(int logFd, int maxLines, qint64 maxAgeSecs) {
|
||||
QFile file;
|
||||
if (!file.open(logFd, QFile::ReadOnly, QFile::AutoCloseHandle)) {
|
||||
return QStringLiteral("(failed to open log fd)\n");
|
||||
}
|
||||
|
||||
file.seek(0);
|
||||
|
||||
qs::log::EncodedLogReader reader;
|
||||
reader.setDevice(&file);
|
||||
|
||||
bool readable = false;
|
||||
quint8 logVersion = 0;
|
||||
quint8 readerVersion = 0;
|
||||
if (!reader.readHeader(&readable, &logVersion, &readerVersion) || !readable) {
|
||||
return QStringLiteral("(failed to read log header)\n");
|
||||
}
|
||||
|
||||
// Read all messages, keeping last maxLines in a ring buffer
|
||||
auto tail = RingBuffer<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) {
|
||||
qCDebug(logCrashReporter) << "Recording crash information at" << crashDir.path();
|
||||
|
||||
|
|
@ -71,74 +146,49 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) {
|
|||
}
|
||||
|
||||
auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt();
|
||||
auto crashSignal = qEnvironmentVariable("__QUICKSHELL_CRASH_SIGNAL").toInt();
|
||||
auto dumpFd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD").toInt();
|
||||
auto logFd = qEnvironmentVariable("__QUICKSHELL_CRASH_LOG_FD").toInt();
|
||||
|
||||
qCDebug(logCrashReporter) << "Saving minidump from fd" << dumpFd;
|
||||
auto dumpDupStatus = tryDup(dumpFd, crashDir.filePath("minidump.dmp.log"));
|
||||
if (dumpDupStatus != 0) {
|
||||
qCCritical(logCrashReporter) << "Failed to write minidump:" << dumpDupStatus;
|
||||
}
|
||||
qCDebug(logCrashReporter) << "Resolving stacktrace from fd" << dumpFd;
|
||||
auto stacktrace = resolveStacktrace(dumpFd);
|
||||
|
||||
qCDebug(logCrashReporter) << "Saving log from fd" << logFd;
|
||||
auto logDupStatus = tryDup(logFd, crashDir.filePath("log.qslog.log"));
|
||||
qCDebug(logCrashReporter) << "Reading recent log lines from fd" << logFd;
|
||||
auto logDupFd = dup(logFd);
|
||||
auto recentLogs = readRecentLogs(logFd, 100, 10);
|
||||
|
||||
qCDebug(logCrashReporter) << "Saving log from fd" << logDupFd;
|
||||
auto logDupStatus = tryDup(logDupFd, crashDir.filePath("log.qslog.log"));
|
||||
if (logDupStatus != 0) {
|
||||
qCCritical(logCrashReporter) << "Failed to save log:" << logDupStatus;
|
||||
}
|
||||
|
||||
auto copyBinStatus = 0;
|
||||
if (!DISTRIBUTOR_DEBUGINFO_AVAILABLE) {
|
||||
qCDebug(logCrashReporter) << "Copying binary to crash folder";
|
||||
if (!QFile(QCoreApplication::applicationFilePath()).copy(crashDir.filePath("executable.txt"))) {
|
||||
copyBinStatus = 1;
|
||||
qCCritical(logCrashReporter) << "Failed to copy binary.";
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto extraInfoFile = QFile(crashDir.filePath("info.txt"));
|
||||
auto extraInfoFile = QFile(crashDir.filePath("report.txt"));
|
||||
if (!extraInfoFile.open(QFile::WriteOnly)) {
|
||||
qCCritical(logCrashReporter) << "Failed to open crash info file for writing.";
|
||||
} else {
|
||||
auto stream = QTextStream(&extraInfoFile);
|
||||
stream << "===== Build Information =====\n";
|
||||
stream << "Git Revision: " << GIT_REVISION << '\n';
|
||||
stream << "Buildtime Qt Version: " << QT_VERSION_STR << "\n";
|
||||
stream << "Build Type: " << BUILD_TYPE << '\n';
|
||||
stream << "Compiler: " << COMPILER << '\n';
|
||||
stream << "Complie Flags: " << COMPILE_FLAGS << "\n\n";
|
||||
stream << "Build configuration:\n" << BUILD_CONFIGURATION << "\n";
|
||||
stream << qs::debuginfo::combinedInfo();
|
||||
|
||||
stream << "\n===== Runtime Information =====\n";
|
||||
stream << "Runtime Qt Version: " << qVersion() << '\n';
|
||||
stream << "\n===== Instance Information =====\n";
|
||||
stream << "Signal: " << strsignal(crashSignal) << " (" << crashSignal << ")\n"; // NOLINT
|
||||
stream << "Crashed process ID: " << crashProc << '\n';
|
||||
stream << "Run ID: " << instance.instanceId << '\n';
|
||||
stream << "Shell ID: " << instance.shellId << '\n';
|
||||
stream << "Config Path: " << instance.configPath << '\n';
|
||||
|
||||
stream << "\n===== Report Integrity =====\n";
|
||||
stream << "Minidump save status: " << dumpDupStatus << '\n';
|
||||
stream << "Log save status: " << logDupStatus << '\n';
|
||||
stream << "Binary copy status: " << copyBinStatus << '\n';
|
||||
|
||||
stream << "\n===== System Information =====\n\n";
|
||||
stream << "/etc/os-release:";
|
||||
auto osReleaseFile = QFile("/etc/os-release");
|
||||
if (osReleaseFile.open(QFile::ReadOnly)) {
|
||||
stream << '\n' << osReleaseFile.readAll() << '\n';
|
||||
osReleaseFile.close();
|
||||
stream << "\n===== Stacktrace =====\n";
|
||||
if (stacktrace.empty()) {
|
||||
stream << "(no trace available)\n";
|
||||
} else {
|
||||
stream << "FAILED TO OPEN\n";
|
||||
auto formatter = cpptrace::formatter().header(std::string());
|
||||
auto traceStr = formatter.format(stacktrace);
|
||||
stream << QString::fromStdString(traceStr) << '\n';
|
||||
}
|
||||
|
||||
stream << "/etc/lsb-release:";
|
||||
auto lsbReleaseFile = QFile("/etc/lsb-release");
|
||||
if (lsbReleaseFile.open(QFile::ReadOnly)) {
|
||||
stream << '\n' << lsbReleaseFile.readAll();
|
||||
lsbReleaseFile.close();
|
||||
} else {
|
||||
stream << "FAILED TO OPEN\n";
|
||||
}
|
||||
stream << "\n===== Log Tail =====\n";
|
||||
stream << recentLogs;
|
||||
|
||||
extraInfoFile.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <variant>
|
||||
|
||||
#include <qbuffer.h>
|
||||
#include <qcoreapplication.h>
|
||||
#include <qlocalserver.h>
|
||||
#include <qlocalsocket.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*/) {
|
||||
qInfo() << "Exiting due to IPC request.";
|
||||
EngineGeneration::currentGeneration()->quit();
|
||||
auto* generation = EngineGeneration::currentGeneration();
|
||||
if (generation) generation->quit();
|
||||
else QCoreApplication::exit(0);
|
||||
}
|
||||
|
||||
} // namespace qs::ipc
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@
|
|||
#include <qtversion.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "../core/debuginfo.hpp"
|
||||
#include "../core/instanceinfo.hpp"
|
||||
#include "../core/logging.hpp"
|
||||
#include "../core/paths.hpp"
|
||||
#include "../io/ipccomm.hpp"
|
||||
#include "../ipc/ipc.hpp"
|
||||
#include "build.hpp"
|
||||
#include "launch_p.hpp"
|
||||
|
||||
namespace qs::launch {
|
||||
|
|
@ -519,20 +519,10 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
|
|||
}
|
||||
|
||||
if (state.misc.printVersion) {
|
||||
qCInfo(logBare).noquote().nospace() << "quickshell " << QS_VERSION << ", revision "
|
||||
<< GIT_REVISION << ", distributed by: " << DISTRIBUTOR;
|
||||
|
||||
if (state.log.verbosity > 1) {
|
||||
qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR;
|
||||
qCInfo(logBare).noquote() << "Runtime Qt Version:" << qVersion();
|
||||
qCInfo(logBare).noquote() << "Compiler:" << COMPILER;
|
||||
qCInfo(logBare).noquote() << "Compile Flags:" << COMPILE_FLAGS;
|
||||
}
|
||||
|
||||
if (state.log.verbosity > 0) {
|
||||
qCInfo(logBare).noquote() << "\nBuild Type:" << BUILD_TYPE;
|
||||
qCInfo(logBare).noquote() << "Build configuration:";
|
||||
qCInfo(logBare).noquote().nospace() << BUILD_CONFIGURATION;
|
||||
if (state.log.verbosity == 0) {
|
||||
qCInfo(logBare).noquote() << "Quickshell" << qs::debuginfo::qsVersion();
|
||||
} else {
|
||||
qCInfo(logBare).noquote() << qs::debuginfo::combinedInfo();
|
||||
}
|
||||
} else if (*state.subcommand.log) {
|
||||
return readLogFile(state);
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
#include "build.hpp"
|
||||
#include "launch_p.hpp"
|
||||
|
||||
#if CRASH_REPORTER
|
||||
#if CRASH_HANDLER
|
||||
#include "../crash/handler.hpp"
|
||||
#endif
|
||||
|
||||
|
|
@ -137,13 +137,14 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio
|
|||
.display = getDisplayConnection(),
|
||||
};
|
||||
|
||||
#if CRASH_REPORTER
|
||||
auto crashHandler = crash::CrashHandler();
|
||||
crashHandler.init();
|
||||
#if CRASH_HANDLER
|
||||
if (qEnvironmentVariableIsSet("QS_DISABLE_CRASH_HANDLER")) {
|
||||
qInfo() << "Crash handling disabled.";
|
||||
} else {
|
||||
crash::CrashHandler::init();
|
||||
|
||||
{
|
||||
auto* log = LogManager::instance();
|
||||
crashHandler.setRelaunchInfo({
|
||||
crash::CrashHandler::setRelaunchInfo({
|
||||
.instance = InstanceInfo::CURRENT,
|
||||
.noColor = !log->colorLogs,
|
||||
.timestamp = log->timestampLogs,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
#include "build.hpp"
|
||||
#include "launch_p.hpp"
|
||||
|
||||
#if CRASH_REPORTER
|
||||
#if CRASH_HANDLER
|
||||
#include "../crash/main.hpp"
|
||||
#endif
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ namespace qs::launch {
|
|||
namespace {
|
||||
|
||||
void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) {
|
||||
#if CRASH_REPORTER
|
||||
#if CRASH_HANDLER
|
||||
auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD");
|
||||
|
||||
if (!lastInfoFdStr.isEmpty()) {
|
||||
|
|
@ -104,7 +104,7 @@ void exitDaemon(int code) {
|
|||
int main(int argc, char** argv) {
|
||||
QCoreApplication::setApplicationName("quickshell");
|
||||
|
||||
#if CRASH_REPORTER
|
||||
#if CRASH_HANDLER
|
||||
qsCheckCrash(argc, argv);
|
||||
#endif
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ qt_add_qml_module(quickshell-service-greetd
|
|||
install_qml_module(quickshell-service-greetd)
|
||||
|
||||
# can't be Qt::Qml because generation.hpp pulls in gui types
|
||||
target_link_libraries(quickshell-service-greetd PRIVATE Qt::Quick)
|
||||
target_link_libraries(quickshell-service-greetd PRIVATE Qt::Quick quickshell-core)
|
||||
|
||||
qs_module_pch(quickshell-service-greetd)
|
||||
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ void GreetdConnection::setInactive() {
|
|||
QString GreetdConnection::user() const { return this->mUser; }
|
||||
|
||||
void GreetdConnection::onSocketConnected() {
|
||||
this->reader.setDevice(&this->socket);
|
||||
qCDebug(logGreetd) << "Connected to greetd socket.";
|
||||
|
||||
if (this->mTargetActive) {
|
||||
|
|
@ -160,82 +161,84 @@ void GreetdConnection::onSocketError(QLocalSocket::LocalSocketError error) {
|
|||
}
|
||||
|
||||
void GreetdConnection::onSocketReady() {
|
||||
qint32 length = 0;
|
||||
while (true) {
|
||||
this->reader.startTransaction();
|
||||
auto length = this->reader.readI32();
|
||||
auto text = this->reader.readBytes(length);
|
||||
if (!this->reader.commitTransaction()) return;
|
||||
|
||||
this->socket.read(reinterpret_cast<char*>(&length), sizeof(qint32));
|
||||
auto json = QJsonDocument::fromJson(text).object();
|
||||
auto type = json.value("type").toString();
|
||||
|
||||
auto text = this->socket.read(length);
|
||||
auto json = QJsonDocument::fromJson(text).object();
|
||||
auto type = json.value("type").toString();
|
||||
qCDebug(logGreetd).noquote() << "Received greetd response:" << text;
|
||||
|
||||
qCDebug(logGreetd).noquote() << "Received greetd response:" << text;
|
||||
if (type == "success") {
|
||||
switch (this->mState) {
|
||||
case GreetdState::Authenticating:
|
||||
qCDebug(logGreetd) << "Authentication complete.";
|
||||
this->mState = GreetdState::ReadyToLaunch;
|
||||
emit this->stateChanged();
|
||||
emit this->readyToLaunch();
|
||||
break;
|
||||
case GreetdState::Launching:
|
||||
qCDebug(logGreetd) << "Target session set successfully.";
|
||||
this->mState = GreetdState::Launched;
|
||||
emit this->stateChanged();
|
||||
emit this->launched();
|
||||
|
||||
if (type == "success") {
|
||||
switch (this->mState) {
|
||||
case GreetdState::Authenticating:
|
||||
qCDebug(logGreetd) << "Authentication complete.";
|
||||
this->mState = GreetdState::ReadyToLaunch;
|
||||
emit this->stateChanged();
|
||||
emit this->readyToLaunch();
|
||||
break;
|
||||
case GreetdState::Launching:
|
||||
qCDebug(logGreetd) << "Target session set successfully.";
|
||||
this->mState = GreetdState::Launched;
|
||||
emit this->stateChanged();
|
||||
emit this->launched();
|
||||
if (this->mExitAfterLaunch) {
|
||||
qCDebug(logGreetd) << "Quitting.";
|
||||
EngineGeneration::currentGeneration()->quit();
|
||||
}
|
||||
|
||||
if (this->mExitAfterLaunch) {
|
||||
qCDebug(logGreetd) << "Quitting.";
|
||||
EngineGeneration::currentGeneration()->quit();
|
||||
break;
|
||||
default: goto unexpected;
|
||||
}
|
||||
} else if (type == "error") {
|
||||
auto errorType = json.value("error_type").toString();
|
||||
auto desc = json.value("description").toString();
|
||||
|
||||
// Special case this error in case a session was already running.
|
||||
// This cancels and restarts the session.
|
||||
if (errorType == "error" && desc == "a session is already being configured") {
|
||||
qCDebug(
|
||||
logGreetd
|
||||
) << "A session was already in progress, cancelling it and starting a new one.";
|
||||
this->setActive(false);
|
||||
this->setActive(true);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
default: goto unexpected;
|
||||
}
|
||||
} else if (type == "error") {
|
||||
auto errorType = json.value("error_type").toString();
|
||||
auto desc = json.value("description").toString();
|
||||
if (errorType == "auth_error") {
|
||||
emit this->authFailure(desc);
|
||||
this->setActive(false);
|
||||
} else if (errorType == "error") {
|
||||
qCWarning(logGreetd) << "Greetd error occurred" << desc;
|
||||
emit this->error(desc);
|
||||
} else goto unexpected;
|
||||
|
||||
// Special case this error in case a session was already running.
|
||||
// This cancels and restarts the session.
|
||||
if (errorType == "error" && desc == "a session is already being configured") {
|
||||
qCDebug(
|
||||
logGreetd
|
||||
) << "A session was already in progress, cancelling it and starting a new one.";
|
||||
this->setActive(false);
|
||||
this->setActive(true);
|
||||
return;
|
||||
}
|
||||
// errors terminate the session
|
||||
this->setInactive();
|
||||
} else if (type == "auth_message") {
|
||||
auto message = json.value("auth_message").toString();
|
||||
auto type = json.value("auth_message_type").toString();
|
||||
auto error = type == "error";
|
||||
auto responseRequired = type == "visible" || type == "secret";
|
||||
auto echoResponse = type != "secret";
|
||||
|
||||
if (errorType == "auth_error") {
|
||||
emit this->authFailure(desc);
|
||||
this->setActive(false);
|
||||
} else if (errorType == "error") {
|
||||
qCWarning(logGreetd) << "Greetd error occurred" << desc;
|
||||
emit this->error(desc);
|
||||
this->mResponseRequired = responseRequired;
|
||||
emit this->authMessage(message, error, responseRequired, echoResponse);
|
||||
|
||||
if (!responseRequired) {
|
||||
this->sendRequest({{"type", "post_auth_message_response"}});
|
||||
}
|
||||
} else goto unexpected;
|
||||
|
||||
// errors terminate the session
|
||||
this->setInactive();
|
||||
} else if (type == "auth_message") {
|
||||
auto message = json.value("auth_message").toString();
|
||||
auto type = json.value("auth_message_type").toString();
|
||||
auto error = type == "error";
|
||||
auto responseRequired = type == "visible" || type == "secret";
|
||||
auto echoResponse = type != "secret";
|
||||
|
||||
this->mResponseRequired = responseRequired;
|
||||
emit this->authMessage(message, error, responseRequired, echoResponse);
|
||||
|
||||
if (!responseRequired) {
|
||||
this->sendRequest({{"type", "post_auth_message_response"}});
|
||||
}
|
||||
} else goto unexpected;
|
||||
|
||||
return;
|
||||
unexpected:
|
||||
qCCritical(logGreetd) << "Received unexpected greetd response" << text;
|
||||
this->setActive(false);
|
||||
continue;
|
||||
unexpected:
|
||||
qCCritical(logGreetd) << "Received unexpected greetd response" << text;
|
||||
this->setActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
void GreetdConnection::sendRequest(const QJsonObject& json) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../../core/streamreader.hpp"
|
||||
|
||||
///! State of the Greetd connection.
|
||||
/// See @@Greetd.state.
|
||||
class GreetdState: public QObject {
|
||||
|
|
@ -74,4 +76,5 @@ private:
|
|||
bool mResponseRequired = false;
|
||||
QString mUser;
|
||||
QLocalSocket socket;
|
||||
StreamReader reader;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
#include <qtmetamacros.h>
|
||||
#include <sys/signal.h>
|
||||
#include <sys/wait.h>
|
||||
#ifdef __FreeBSD__
|
||||
#include <signal.h>
|
||||
#endif
|
||||
|
||||
#include "../../core/logcat.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) {
|
||||
auto* self = static_cast<PwCore*>(data);
|
||||
|
||||
if (message != nullptr) {
|
||||
qCWarning(logLoop) << "Fatal pipewire error on object" << id << "with code" << res << message;
|
||||
} else {
|
||||
qCWarning(logLoop) << "Fatal pipewire error on object" << id << "with code" << res;
|
||||
// Pipewire's documentation describes the error event as being fatal, however it isn't.
|
||||
// We're not sure what causes these ENOENTs on device removal, presumably something in
|
||||
// the teardown sequence, but they're harmless. Attempting to handle them as a fatal
|
||||
// error causes unnecessary triggers for shells.
|
||||
if (res == -ENOENT) {
|
||||
qCDebug(logLoop) << "Pipewire ENOENT on object" << id << "with code" << res << message;
|
||||
return;
|
||||
}
|
||||
|
||||
qCWarning(logLoop) << "Pipewire error on object" << id << "with code" << res << message;
|
||||
|
||||
emit self->fatalError();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
#include <spa/utils/json.h>
|
||||
|
||||
#include "../../core/logcat.hpp"
|
||||
#include "../../core/util.hpp"
|
||||
#include "metadata.hpp"
|
||||
#include "node.hpp"
|
||||
#include "registry.hpp"
|
||||
|
|
@ -138,32 +137,6 @@ void PwDefaultTracker::onNodeAdded(PwNode* node) {
|
|||
}
|
||||
}
|
||||
|
||||
void PwDefaultTracker::onNodeDestroyed(QObject* node) {
|
||||
if (node == this->mDefaultSink) {
|
||||
qCInfo(logDefaults) << "Default sink destroyed.";
|
||||
this->mDefaultSink = nullptr;
|
||||
emit this->defaultSinkChanged();
|
||||
}
|
||||
|
||||
if (node == this->mDefaultSource) {
|
||||
qCInfo(logDefaults) << "Default source destroyed.";
|
||||
this->mDefaultSource = nullptr;
|
||||
emit this->defaultSourceChanged();
|
||||
}
|
||||
|
||||
if (node == this->mDefaultConfiguredSink) {
|
||||
qCInfo(logDefaults) << "Default configured sink destroyed.";
|
||||
this->mDefaultConfiguredSink = nullptr;
|
||||
emit this->defaultConfiguredSinkChanged();
|
||||
}
|
||||
|
||||
if (node == this->mDefaultConfiguredSource) {
|
||||
qCInfo(logDefaults) << "Default configured source destroyed.";
|
||||
this->mDefaultConfiguredSource = nullptr;
|
||||
emit this->defaultConfiguredSourceChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void PwDefaultTracker::changeConfiguredSink(PwNode* node) {
|
||||
if (node != nullptr) {
|
||||
if (!node->type.testFlags(PwNodeType::AudioSink)) {
|
||||
|
|
@ -240,10 +213,23 @@ void PwDefaultTracker::setDefaultSink(PwNode* node) {
|
|||
if (node == this->mDefaultSink) return;
|
||||
qCInfo(logDefaults) << "Default sink changed to" << node;
|
||||
|
||||
setSimpleObjectHandle<
|
||||
&PwDefaultTracker::mDefaultSink,
|
||||
&PwDefaultTracker::onNodeDestroyed,
|
||||
&PwDefaultTracker::defaultSinkChanged>(this, node);
|
||||
if (this->mDefaultSink != nullptr) {
|
||||
QObject::disconnect(this->mDefaultSink, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
this->mDefaultSink = node;
|
||||
|
||||
if (node != nullptr) {
|
||||
QObject::connect(node, &QObject::destroyed, this, &PwDefaultTracker::onDefaultSinkDestroyed);
|
||||
}
|
||||
|
||||
emit this->defaultSinkChanged();
|
||||
}
|
||||
|
||||
void PwDefaultTracker::onDefaultSinkDestroyed() {
|
||||
qCInfo(logDefaults) << "Default sink destroyed.";
|
||||
this->mDefaultSink = nullptr;
|
||||
emit this->defaultSinkChanged();
|
||||
}
|
||||
|
||||
void PwDefaultTracker::setDefaultSinkName(const QString& name) {
|
||||
|
|
@ -257,10 +243,23 @@ void PwDefaultTracker::setDefaultSource(PwNode* node) {
|
|||
if (node == this->mDefaultSource) return;
|
||||
qCInfo(logDefaults) << "Default source changed to" << node;
|
||||
|
||||
setSimpleObjectHandle<
|
||||
&PwDefaultTracker::mDefaultSource,
|
||||
&PwDefaultTracker::onNodeDestroyed,
|
||||
&PwDefaultTracker::defaultSourceChanged>(this, node);
|
||||
if (this->mDefaultSource != nullptr) {
|
||||
QObject::disconnect(this->mDefaultSource, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
this->mDefaultSource = node;
|
||||
|
||||
if (node != nullptr) {
|
||||
QObject::connect(node, &QObject::destroyed, this, &PwDefaultTracker::onDefaultSourceDestroyed);
|
||||
}
|
||||
|
||||
emit this->defaultSourceChanged();
|
||||
}
|
||||
|
||||
void PwDefaultTracker::onDefaultSourceDestroyed() {
|
||||
qCInfo(logDefaults) << "Default source destroyed.";
|
||||
this->mDefaultSource = nullptr;
|
||||
emit this->defaultSourceChanged();
|
||||
}
|
||||
|
||||
void PwDefaultTracker::setDefaultSourceName(const QString& name) {
|
||||
|
|
@ -274,10 +273,28 @@ void PwDefaultTracker::setDefaultConfiguredSink(PwNode* node) {
|
|||
if (node == this->mDefaultConfiguredSink) return;
|
||||
qCInfo(logDefaults) << "Default configured sink changed to" << node;
|
||||
|
||||
setSimpleObjectHandle<
|
||||
&PwDefaultTracker::mDefaultConfiguredSink,
|
||||
&PwDefaultTracker::onNodeDestroyed,
|
||||
&PwDefaultTracker::defaultConfiguredSinkChanged>(this, node);
|
||||
if (this->mDefaultConfiguredSink != nullptr) {
|
||||
QObject::disconnect(this->mDefaultConfiguredSink, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
this->mDefaultConfiguredSink = node;
|
||||
|
||||
if (node != nullptr) {
|
||||
QObject::connect(
|
||||
node,
|
||||
&QObject::destroyed,
|
||||
this,
|
||||
&PwDefaultTracker::onDefaultConfiguredSinkDestroyed
|
||||
);
|
||||
}
|
||||
|
||||
emit this->defaultConfiguredSinkChanged();
|
||||
}
|
||||
|
||||
void PwDefaultTracker::onDefaultConfiguredSinkDestroyed() {
|
||||
qCInfo(logDefaults) << "Default configured sink destroyed.";
|
||||
this->mDefaultConfiguredSink = nullptr;
|
||||
emit this->defaultConfiguredSinkChanged();
|
||||
}
|
||||
|
||||
void PwDefaultTracker::setDefaultConfiguredSinkName(const QString& name) {
|
||||
|
|
@ -291,10 +308,28 @@ void PwDefaultTracker::setDefaultConfiguredSource(PwNode* node) {
|
|||
if (node == this->mDefaultConfiguredSource) return;
|
||||
qCInfo(logDefaults) << "Default configured source changed to" << node;
|
||||
|
||||
setSimpleObjectHandle<
|
||||
&PwDefaultTracker::mDefaultConfiguredSource,
|
||||
&PwDefaultTracker::onNodeDestroyed,
|
||||
&PwDefaultTracker::defaultConfiguredSourceChanged>(this, node);
|
||||
if (this->mDefaultConfiguredSource != nullptr) {
|
||||
QObject::disconnect(this->mDefaultConfiguredSource, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
this->mDefaultConfiguredSource = node;
|
||||
|
||||
if (node != nullptr) {
|
||||
QObject::connect(
|
||||
node,
|
||||
&QObject::destroyed,
|
||||
this,
|
||||
&PwDefaultTracker::onDefaultConfiguredSourceDestroyed
|
||||
);
|
||||
}
|
||||
|
||||
emit this->defaultConfiguredSourceChanged();
|
||||
}
|
||||
|
||||
void PwDefaultTracker::onDefaultConfiguredSourceDestroyed() {
|
||||
qCInfo(logDefaults) << "Default configured source destroyed.";
|
||||
this->mDefaultConfiguredSource = nullptr;
|
||||
emit this->defaultConfiguredSourceChanged();
|
||||
}
|
||||
|
||||
void PwDefaultTracker::setDefaultConfiguredSourceName(const QString& name) {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,10 @@ private slots:
|
|||
void onMetadataAdded(PwMetadata* metadata);
|
||||
void onMetadataProperty(const char* key, const char* type, const char* value);
|
||||
void onNodeAdded(PwNode* node);
|
||||
void onNodeDestroyed(QObject* node);
|
||||
void onDefaultSinkDestroyed();
|
||||
void onDefaultSourceDestroyed();
|
||||
void onDefaultConfiguredSinkDestroyed();
|
||||
void onDefaultConfiguredSourceDestroyed();
|
||||
|
||||
private:
|
||||
void setDefaultSink(PwNode* node);
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ endfunction()
|
|||
# -----
|
||||
|
||||
qt_add_library(quickshell-wayland STATIC
|
||||
wl_proxy_safe_deref.cpp
|
||||
platformmenu.cpp
|
||||
popupanchor.cpp
|
||||
xdgshell.cpp
|
||||
|
|
@ -80,6 +81,13 @@ qt_add_library(quickshell-wayland STATIC
|
|||
output_tracking.cpp
|
||||
)
|
||||
|
||||
# required for wl_proxy_safe_deref
|
||||
target_link_libraries(quickshell-wayland PRIVATE ${CMAKE_DL_LIBS})
|
||||
target_link_options(quickshell PRIVATE
|
||||
"LINKER:--export-dynamic-symbol=wl_proxy_get_listener"
|
||||
"LINKER:--require-defined=wl_proxy_get_listener"
|
||||
)
|
||||
|
||||
# required to make sure the constructor is linked
|
||||
add_library(quickshell-wayland-init OBJECT init.cpp)
|
||||
|
||||
|
|
@ -123,6 +131,8 @@ list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleNotify)
|
|||
add_subdirectory(shortcuts_inhibit)
|
||||
list(APPEND WAYLAND_MODULES Quickshell.Wayland._ShortcutsInhibitor)
|
||||
|
||||
add_subdirectory(windowmanager)
|
||||
|
||||
# widgets for qmenu
|
||||
target_link_libraries(quickshell-wayland PRIVATE
|
||||
Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ qs_add_module_deps_light(quickshell-hyprland-ipc Quickshell)
|
|||
|
||||
install_qml_module(quickshell-hyprland-ipc)
|
||||
|
||||
target_link_libraries(quickshell-hyprland-ipc PRIVATE Qt::Quick)
|
||||
target_link_libraries(quickshell-hyprland-ipc PRIVATE Qt::Quick quickshell-core)
|
||||
|
||||
if (WAYLAND_TOPLEVEL_MANAGEMENT)
|
||||
target_sources(quickshell-hyprland-ipc PRIVATE
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ void HyprlandIpc::eventSocketError(QLocalSocket::LocalSocketError error) const {
|
|||
|
||||
void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) {
|
||||
if (state == QLocalSocket::ConnectedState) {
|
||||
this->eventReader.setDevice(&this->eventSocket);
|
||||
qCInfo(logHyprlandIpc) << "Hyprland event socket connected.";
|
||||
emit this->connected();
|
||||
} else if (state == QLocalSocket::UnconnectedState && this->valid) {
|
||||
|
|
@ -104,11 +105,11 @@ void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state)
|
|||
|
||||
void HyprlandIpc::eventSocketReady() {
|
||||
while (true) {
|
||||
auto rawEvent = this->eventSocket.readLine();
|
||||
if (rawEvent.isEmpty()) break;
|
||||
this->eventReader.startTransaction();
|
||||
auto rawEvent = this->eventReader.readUntil('\n');
|
||||
if (!this->eventReader.commitTransaction()) return;
|
||||
|
||||
// remove trailing \n
|
||||
rawEvent.truncate(rawEvent.length() - 1);
|
||||
rawEvent.chop(1); // remove trailing \n
|
||||
auto splitIdx = rawEvent.indexOf(">>");
|
||||
auto event = QByteArrayView(rawEvent.data(), splitIdx);
|
||||
auto data = QByteArrayView(
|
||||
|
|
@ -728,7 +729,7 @@ void HyprlandIpc::refreshToplevels() {
|
|||
}
|
||||
|
||||
auto* workspace = toplevel->bindableWorkspace().value();
|
||||
workspace->insertToplevel(toplevel);
|
||||
if (workspace) workspace->insertToplevel(toplevel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
#include "../../../core/model.hpp"
|
||||
#include "../../../core/qmlscreen.hpp"
|
||||
#include "../../../core/streamreader.hpp"
|
||||
#include "../../../wayland/toplevel_management/handle.hpp"
|
||||
|
||||
namespace qs::hyprland::ipc {
|
||||
|
|
@ -139,6 +140,7 @@ private:
|
|||
static bool compareWorkspaces(HyprlandWorkspace* a, HyprlandWorkspace* b);
|
||||
|
||||
QLocalSocket eventSocket;
|
||||
StreamReader eventReader;
|
||||
QString mRequestSocketPath;
|
||||
QString mEventSocketPath;
|
||||
bool valid = false;
|
||||
|
|
|
|||
|
|
@ -72,20 +72,16 @@ void HyprlandToplevel::updateFromObject(const QVariantMap& object) {
|
|||
Qt::beginPropertyUpdateGroup();
|
||||
bool ok = false;
|
||||
auto address = addressStr.toULongLong(&ok, 16);
|
||||
if (!ok || !address) {
|
||||
return;
|
||||
}
|
||||
if (ok && address) this->setAddress(address);
|
||||
|
||||
this->setAddress(address);
|
||||
this->bTitle = title;
|
||||
|
||||
auto workspaceMap = object.value("workspace").toMap();
|
||||
auto workspaceName = workspaceMap.value("name").toString();
|
||||
|
||||
auto* workspace = this->ipc->findWorkspaceByName(workspaceName, false);
|
||||
if (!workspace) return;
|
||||
auto* workspace = this->ipc->findWorkspaceByName(workspaceName, true);
|
||||
if (workspace) this->setWorkspace(workspace);
|
||||
|
||||
this->setWorkspace(workspace);
|
||||
this->bLastIpcObject = object;
|
||||
Qt::endPropertyUpdateGroup();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "wlr_layershell/wlr_layershell.hpp"
|
||||
#endif
|
||||
|
||||
void installWlProxySafeDeref(); // NOLINT(misc-use-internal-linkage)
|
||||
void installPlatformMenuHook(); // NOLINT(misc-use-internal-linkage)
|
||||
void installPopupPositioner(); // NOLINT(misc-use-internal-linkage)
|
||||
|
||||
|
|
@ -33,6 +34,7 @@ class WaylandPlugin: public QsEnginePlugin {
|
|||
}
|
||||
|
||||
void init() override {
|
||||
installWlProxySafeDeref();
|
||||
installPlatformMenuHook();
|
||||
installPopupPositioner();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,11 @@ void ToplevelManager::onToplevelReady(impl::ToplevelHandle* handle) {
|
|||
|
||||
void ToplevelManager::onToplevelActiveChanged() {
|
||||
auto* toplevel = qobject_cast<Toplevel*>(this->sender());
|
||||
if (toplevel->activated()) this->setActiveToplevel(toplevel);
|
||||
if (toplevel->activated()) {
|
||||
this->setActiveToplevel(toplevel);
|
||||
} else if (toplevel == this->mActiveToplevel) {
|
||||
this->setActiveToplevel(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void ToplevelManager::onToplevelClosed() {
|
||||
|
|
|
|||
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 <qwayland-wlr-layer-shell-unstable-v1.h>
|
||||
#include <qwindow.h>
|
||||
#include <wayland-xdg-shell-client-protocol.h>
|
||||
|
||||
#include "../../window/panelinterface.hpp"
|
||||
#include "shell_integration.hpp"
|
||||
|
|
@ -247,9 +248,19 @@ void LayerSurface::commit() {
|
|||
}
|
||||
|
||||
void LayerSurface::attachPopup(QtWaylandClient::QWaylandShellSurface* popup) {
|
||||
std::any role = popup->surfaceRole();
|
||||
|
||||
if (auto* popupRole = std::any_cast<::xdg_popup*>(&role)) { // NOLINT
|
||||
#ifdef __FreeBSD__
|
||||
// FreeBSD uses an alternate RTTI matching strategy by default which does
|
||||
// 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);
|
||||
} else {
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
#include <qbytearray.h>
|
||||
#include <qbytearrayview.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdatastream.h>
|
||||
#include <qjsonarray.h>
|
||||
#include <qjsondocument.h>
|
||||
#include <qjsonobject.h>
|
||||
|
|
@ -15,9 +14,7 @@
|
|||
#include <qlocalsocket.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qobject.h>
|
||||
#include <qsysinfo.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
|
@ -89,9 +86,6 @@ I3Ipc::I3Ipc(const QList<QString>& events): mEvents(events) {
|
|||
QObject::connect(&this->liveEventSocket, &QLocalSocket::readyRead, this, &I3Ipc::eventSocketReady);
|
||||
QObject::connect(&this->liveEventSocket, &QLocalSocket::connected, this, &I3Ipc::subscribe);
|
||||
// clang-format on
|
||||
|
||||
this->liveEventSocketDs.setDevice(&this->liveEventSocket);
|
||||
this->liveEventSocketDs.setByteOrder(static_cast<QDataStream::ByteOrder>(QSysInfo::ByteOrder));
|
||||
}
|
||||
|
||||
void I3Ipc::makeRequest(const QByteArray& request) {
|
||||
|
|
@ -145,34 +139,21 @@ void I3Ipc::reconnectIPC() {
|
|||
}
|
||||
|
||||
QVector<Event> I3Ipc::parseResponse() {
|
||||
QVector<std::tuple<EventCode, QJsonDocument>> events;
|
||||
const int magicLen = 6;
|
||||
QVector<Event> events;
|
||||
|
||||
while (!this->liveEventSocketDs.atEnd()) {
|
||||
this->liveEventSocketDs.startTransaction();
|
||||
this->liveEventSocketDs.startTransaction();
|
||||
while (true) {
|
||||
this->eventReader.startTransaction();
|
||||
auto magic = this->eventReader.readBytes(6);
|
||||
auto size = this->eventReader.readI32();
|
||||
auto type = this->eventReader.readI32();
|
||||
auto payload = this->eventReader.readBytes(size);
|
||||
if (!this->eventReader.commitTransaction()) return events;
|
||||
|
||||
std::array<char, 6> buffer = {};
|
||||
qint32 size = 0;
|
||||
qint32 type = EventCode::Unknown;
|
||||
|
||||
this->liveEventSocketDs.readRawData(buffer.data(), magicLen);
|
||||
this->liveEventSocketDs >> size;
|
||||
this->liveEventSocketDs >> type;
|
||||
|
||||
if (!this->liveEventSocketDs.commitTransaction()) break;
|
||||
|
||||
QByteArray payload(size, Qt::Uninitialized);
|
||||
|
||||
this->liveEventSocketDs.readRawData(payload.data(), size);
|
||||
|
||||
if (!this->liveEventSocketDs.commitTransaction()) break;
|
||||
|
||||
if (strncmp(buffer.data(), MAGIC.data(), 6) != 0) {
|
||||
if (magic.size() < 6 || strncmp(magic.data(), MAGIC.data(), 6) != 0) {
|
||||
qCWarning(logI3Ipc) << "No magic sequence found in string.";
|
||||
this->reconnectIPC();
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
if (I3IpcEvent::intToEvent(type) == EventCode::Unknown) {
|
||||
qCWarning(logI3Ipc) << "Received unknown event";
|
||||
|
|
@ -204,6 +185,7 @@ void I3Ipc::eventSocketError(QLocalSocket::LocalSocketError error) const {
|
|||
|
||||
void I3Ipc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) {
|
||||
if (state == QLocalSocket::ConnectedState) {
|
||||
this->eventReader.setDevice(&this->liveEventSocket);
|
||||
qCInfo(logI3Ipc) << "I3 event socket connected.";
|
||||
emit this->connected();
|
||||
} else if (state == QLocalSocket::UnconnectedState && this->valid) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <qbytearrayview.h>
|
||||
#include <qdatastream.h>
|
||||
#include <qjsondocument.h>
|
||||
#include <qlocalsocket.h>
|
||||
#include <qobject.h>
|
||||
|
|
@ -9,6 +8,8 @@
|
|||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../../../core/streamreader.hpp"
|
||||
|
||||
namespace qs::i3::ipc {
|
||||
|
||||
constexpr std::string MAGIC = "i3-ipc";
|
||||
|
|
@ -92,7 +93,7 @@ protected:
|
|||
QVector<std::tuple<EventCode, QJsonDocument>> parseResponse();
|
||||
|
||||
QLocalSocket liveEventSocket;
|
||||
QDataStream liveEventSocketDs;
|
||||
StreamReader eventReader;
|
||||
|
||||
QString mSocketPath;
|
||||
bool valid = false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue