diff --git a/.clang-format b/.clang-format
index 610ee65..8ec602a 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,6 +1,6 @@
AlignArrayOfStructures: None
AlignAfterOpenBracket: BlockIndent
-AllowShortBlocksOnASingleLine: Always
+AllowShortBlocksOnASingleLine: Empty
AllowShortCaseLabelsOnASingleLine: true
AllowShortEnumsOnASingleLine: true
AllowShortFunctionsOnASingleLine: All
diff --git a/.clang-tidy b/.clang-tidy
index 002c444..da14682 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -20,6 +20,8 @@ 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,
google-runtime-int,
@@ -63,6 +65,8 @@ CheckOptions:
readability-identifier-naming.ParameterCase: camelBack
readability-identifier-naming.VariableCase: camelBack
+ misc-const-correctness.WarnPointersAsPointers: false
+
# does not appear to work
readability-operators-representation.BinaryOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^='
readability-operators-representation.OverloadedOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^='
diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml
index c8b4804..958c884 100644
--- a/.github/ISSUE_TEMPLATE/crash.yml
+++ b/.github/ISSUE_TEMPLATE/crash.yml
@@ -1,82 +1,17 @@
-name: Crash Report
-description: Quickshell has crashed
-labels: ["bug", "crash"]
+name: Crash Report (v1)
+description: Quickshell has crashed (old)
+labels: ["unactionable"]
body:
- - type: textarea
- id: crashinfo
+ - type: markdown
attributes:
- label: General crash information
- description: |
- Paste the contents of the `info.txt` file in your crash folder here.
- value: " General information
-
-
- ```
-
-
-
- ```
-
-
- "
- validations:
- required: true
- - type: textarea
- id: userinfo
+ value: |
+ Thank you for taking the time to click the report button.
+ At this point most of the worst issues in 0.2.1 and before have been fixed and we are
+ preparing for a new release. Please do not submit this report.
+ - type: checkboxes
+ id: donotcheck
attributes:
- label: What caused the crash
- description: |
- Any information likely to help debug the crash. What were you doing when the crash occurred,
- what changes did you make, can you get it to happen again?
- - type: textarea
- id: dump
- attributes:
- label: Minidump
- description: |
- Attach `minidump.dmp.log` here. If it is too big to upload, compress it.
-
- You may skip this step if quickshell crashed while processing a password
- or other sensitive information. If you skipped it write why instead.
- validations:
- required: true
- - type: textarea
- id: logs
- attributes:
- label: Log file
- description: |
- Attach `log.qslog.log` here. If it is too big to upload, compress it.
-
- You can preview the log if you'd like using `quickshell read-log `.
- validations:
- required: true
- - type: textarea
- id: config
- attributes:
- label: Configuration
- description: |
- Attach your configuration here, preferrably in full (not just one file).
- Compress it into a zip, tar, etc.
-
- This will help us reproduce the crash ourselves.
- - type: textarea
- id: bt
- attributes:
- label: Backtrace
- description: |
- If you have gdb installed and use systemd, or otherwise know how to get a backtrace,
- we would appreciate one. (You may have gdb installed without knowing it)
-
- 1. Run `coredumpctl debug ` where `pid` is the number shown after "Crashed process ID"
- in the crash reporter.
- 2. Once it loads, type `bt -full` (then enter)
- 3. Copy the output and attach it as a file or in a spoiler.
- - type: textarea
- id: exe
- attributes:
- label: Executable
- description: |
- If the crash folder contains a executable.txt file, upload it here. If not you can ignore this field.
- If it is too big to upload, compress it.
-
- Note: executable.txt is the quickshell binary. It has a .txt extension due to github's limitations on
- filetypes.
+ label: Read the text above. Do not submit the report.
+ options:
+ - label: Yes I want this report to be deleted.
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/crash2.yml b/.github/ISSUE_TEMPLATE/crash2.yml
new file mode 100644
index 0000000..86f490c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/crash2.yml
@@ -0,0 +1,49 @@
+name: Crash Report (v2)
+description: Quickshell has crashed
+labels: ["bug", "crash"]
+body:
+ - type: textarea
+ id: userinfo
+ attributes:
+ label: What caused the crash
+ description: |
+ Any information likely to help debug the crash. What were you doing when the crash occurred,
+ what changes did you make, can you get it to happen again?
+ - type: upload
+ id: report
+ attributes:
+ label: Report file
+ description: Attach `report.txt` here.
+ validations:
+ required: true
+ - type: upload
+ id: logs
+ attributes:
+ label: Log file
+ description: |
+ Attach `log.qslog.log` here. If it is too big to upload, compress it.
+
+ You can preview the log if you'd like using `qs log -r '*=true'`.
+ validations:
+ required: true
+ - type: textarea
+ id: config
+ attributes:
+ label: Configuration
+ description: |
+ Attach or link your configuration here, preferrably in full (not just one file).
+ Compress it into a zip, tar, etc.
+
+ This will help us reproduce the crash ourselves.
+ - type: textarea
+ id: bt
+ attributes:
+ label: Backtrace
+ description: |
+ GDB usually produces better stacktraces than quickshell can. Consider attaching a gdb backtrace
+ following the instructions below.
+
+ 1. Run `coredumpctl debug ` where `pid` is the number shown after "Crashed process ID"
+ in the crash reporter.
+ 2. Once it loads, type `bt -full` (then enter)
+ 3. Copy the output and attach it as a file or in a spoiler.
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 83957dc..7b8cbce 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,7 +6,7 @@ jobs:
name: Nix
strategy:
matrix:
- qtver: [qt6.9.2, qt6.9.1, qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0]
+ qtver: [qt6.10.1, qt6.10.0, qt6.9.2, qt6.9.1, qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0]
compiler: [clang, gcc]
runs-on: ubuntu-latest
permissions:
@@ -50,13 +50,16 @@ jobs:
wayland-protocols \
wayland \
libdrm \
+ vulkan-headers \
libxcb \
libpipewire \
cli11 \
- jemalloc
+ polkit \
+ jemalloc \
+ libunwind \
+ git # for cpptrace clone
- name: Build
- # breakpad is annoying to build in ci due to makepkg not running as root
run: |
- cmake -GNinja -B build -DCRASH_REPORTER=OFF
+ cmake -GNinja -B build -DVENDOR_CPPTRACE=ON
cmake --build build
diff --git a/BUILD.md b/BUILD.md
index 742baa7..d624a06 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -15,15 +15,7 @@ Please make this descriptive enough to identify your specific package, for examp
- `Nixpkgs`
- `Fedora COPR (errornointernet/quickshell)`
-`-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=YES/NO`
-
-If we can retrieve binaries and debug information for the package without actually running your
-distribution (e.g. from an website), and you would like to strip the binary, please set this to `YES`.
-
-If we cannot retrieve debug information, please set this to `NO` and
-**ensure you aren't distributing stripped (non debuggable) binaries**.
-
-In both cases you should build with `-DCMAKE_BUILD_TYPE=RelWithDebInfo` (then split or keep the debuginfo).
+If you are forking quickshell, please change `CRASHREPORT_URL` to your own issue tracker.
### QML Module dir
Currently all QML modules are statically linked to quickshell, but this is where
@@ -41,6 +33,7 @@ Quickshell has a set of base dependencies you will always need, names vary by di
- `cmake`
- `qt6base`
- `qt6declarative`
+- `libdrm`
- `qtshadertools` (build-time)
- `spirv-tools` (build-time)
- `pkg-config` (build-time)
@@ -64,14 +57,24 @@ At least Qt 6.6 is required.
All features are enabled by default and some have their own dependencies.
-### Crash Reporter
-The crash reporter catches crashes, restarts quickshell when it crashes,
+### Crash Handler
+The crash reporter catches crashes, restarts Quickshell when it crashes,
and collects useful crash information in one place. Leaving this enabled will
enable us to fix bugs far more easily.
-To disable: `-DCRASH_REPORTER=OFF`
+To disable: `-DCRASH_HANDLER=OFF`
-Dependencies: `google-breakpad` (static library)
+Dependencies: `cpptrace`
+
+Note: `-DVENDOR_CPPTRACE=ON` can be set to vendor cpptrace using FetchContent.
+
+When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the
+package manager or fetched with FetchContent.
+
+*Please ensure binaries have usable symbols.* We do not necessarily need full debuginfo, but
+leaving symbols in the binary is extremely helpful. You can check if symbols are useful
+by sending a SIGSEGV to the process and ensuring symbols for the quickshell binary are present
+in the trace.
### Jemalloc
We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused
@@ -144,8 +147,8 @@ Enables streaming video from monitors and toplevel windows through various proto
To disable: `-DSCREENCOPY=OFF`
Dependencies:
-- `libdrm`
- `libgbm`
+- `vulkan-headers` (build-time)
Specific protocols can also be disabled:
- `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1]
@@ -192,6 +195,13 @@ To disable: `-DSERVICE_PAM=OFF`
Dependencies: `pam`
+### Polkit
+This feature enables creating Polkit agents that can prompt user for authentication.
+
+To disable: `-DSERVICE_POLKIT=OFF`
+
+Dependencies: `polkit`, `glib`
+
### Hyprland
This feature enables hyprland specific integrations. It requires wayland support
but has no extra dependencies.
@@ -232,7 +242,7 @@ Only `ninja` builds are tested, but makefiles may work.
#### Configuring the build
```sh
-$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here]
+$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release [additional disable flags from above here]
```
Note that features you do not supply dependencies for MUST be disabled with their associated flags
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 880b9ca..8293f23 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,12 +1,21 @@
cmake_minimum_required(VERSION 3.20)
project(quickshell VERSION "0.2.1" LANGUAGES CXX C)
+set(UNRELEASED_FEATURES
+ "network.2"
+ "colorquant-imagerect"
+ "window-parent"
+)
+
set(QT_MIN_VERSION "6.6.0")
set(CMAKE_CXX_STANDARD 20)
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" "")
@@ -38,14 +47,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})
-boption(CRASH_REPORTER "Crash Handling" ON)
-boption(USE_JEMALLOC "Use jemalloc" ON)
+if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
+ boption(USE_JEMALLOC "Use jemalloc" OFF)
+else()
+ 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)
@@ -67,18 +79,21 @@ boption(SERVICE_STATUS_NOTIFIER "System Tray" ON)
boption(SERVICE_PIPEWIRE "PipeWire" ON)
boption(SERVICE_MPRIS "Mpris" ON)
boption(SERVICE_PAM "Pam" ON)
+boption(SERVICE_POLKIT "Polkit" ON)
boption(SERVICE_GREETD "Greetd" ON)
boption(SERVICE_UPOWER "UPower" ON)
boption(SERVICE_NOTIFICATIONS "Notifications" ON)
boption(BLUETOOTH "Bluetooth" ON)
+boption(NETWORK "Network" ON)
include(cmake/install-qml-module.cmake)
include(cmake/util.cmake)
add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension)
-# pipewire defines this, breaking PCH
+# pipewire defines these, breaking PCH
add_compile_definitions(_REENTRANT)
+add_compile_options(-fno-strict-overflow)
if (FRAME_POINTERS)
add_compile_options(-fno-omit-frame-pointer)
@@ -119,7 +134,7 @@ if (WAYLAND)
list(APPEND QT_PRIVDEPS WaylandClientPrivate)
endif()
-if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH)
+if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH OR NETWORK)
set(DBUS ON)
endif()
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 39fab13..73e7931 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,235 +1,40 @@
-# Contributing / Development
-Instructions for development setup and upstreaming patches.
+# Contributing
-If you just want to build or package quickshell see [BUILD.md](BUILD.md).
+Thank you for taking the time to contribute.
+To ensure nobody's time is wasted, please follow the rules below.
-## Development
+## Acceptable Code Contributions
-Install the dependencies listed in [BUILD.md](BUILD.md).
-You probably want all of them even if you don't use all of them
-to ensure tests work correctly and avoid passing a bunch of configure
-flags when you need to wipe the build directory.
+- All changes submitted MUST be **fully understood by the submitter**. If you do not know why or how
+ your change works, do not submit it to be merged. You must be able to explain your reasoning
+ for every change.
-Quickshell also uses `just` for common development command aliases.
+- Changes MUST be submitted by a human who will be responsible for them. Changes submitted without
+ a human in the loop such as automated tooling and AI Agents are **strictly disallowed**. Accounts
+ responsible for such contribution attempts **will be banned**.
-The dependencies are also available as a nix shell or nix flake which we recommend
-using with nix-direnv.
+- Changes MUST respect Quickshell's license and the license of any source works. Changes including
+ code from any other works must disclose the source of the code, explain why it was used, and
+ ensure the license is compatible.
-Common aliases:
-- `just configure [ [extra cmake args]]` (note that you must specify debug/release to specify extra args)
-- `just build` - runs the build, configuring if not configured already.
-- `just run [args]` - runs quickshell with the given arguments
-- `just clean` - clean up build artifacts. `just clean build` is somewhat common.
+- Changes must follow the guidelines outlined in [HACKING.md](HACKING.md) for style and substance.
-### Formatting
-All contributions should be formatted similarly to what already exists.
-Group related functionality together.
+- Changes must stand on their own as a unit. Do not make multiple unrelated changes in one PR.
+ Changes depending on prior merges should be marked as a draft.
-Run the formatter using `just fmt`.
-If the results look stupid, fix the clang-format file if possible,
-or disable clang-format in the affected area
-using `// clang-format off` and `// clang-format on`.
+## Acceptable Non-code Contributions
-#### Style preferences not caught by clang-format
-These are flexible. You can ignore them if it looks or works better to
-for one reason or another.
+- Bug and crash reports. You must follow the instructions in the issue templates and provide the
+ information requested.
-Use `auto` if the type of a variable can be deduced automatically, instead of
-redeclaring the returned value's type. Additionally, auto should be used when a
-constructor takes arguments.
+- Feature requests can be made via Issues. Please check to ensure nobody else has requested the same feature.
-```cpp
-auto x = ; // ok
-auto x = QString::number(3); // ok
-QString x; // ok
-QString x = "foo"; // ok
-auto x = QString("foo"); // ok
+- Do not make insubstantial or pointless changes.
-auto x = QString(); // avoid
-QString x(); // avoid
-QString x("foo"); // avoid
-```
+- Changes to project rules / policy / governance will not be entertained, except from significant
+ long-term contributors. These changes should not be addressed through contribution channels.
-Put newlines around logical units of code, and after closing braces. If the
-most reasonable logical unit of code takes only a single line, it should be
-merged into the next single line logical unit if applicable.
-```cpp
-// multiple units
-auto x = ; // unit 1
-auto y = ; // unit 2
+## Merge timelines
-auto x = ; // unit 1
-emit this->y(); // unit 2
-
-auto x1 = ; // unit 1
-auto x2 = ; // unit 1
-auto x3 = ; // unit 1
-
-auto y1 = ; // unit 2
-auto y2 = ; // unit 2
-auto y3 = ; // unit 2
-
-// one unit
-auto x = ;
-if (x...) {
- // ...
-}
-
-// if more than one variable needs to be used then add a newline
-auto x = ;
-auto y = ;
-
-if (x && y) {
- // ...
-}
-```
-
-Class formatting:
-```cpp
-//! Doc comment summary
-/// Doc comment body
-class Foo: public QObject {
- // The Q_OBJECT macro comes first. Macros are ; terminated.
- Q_OBJECT;
- QML_ELEMENT;
- QML_CLASSINFO(...);
- // Properties must stay on a single line or the doc generator won't be able to pick them up
- Q_PROPERTY(...);
- /// Doc comment
- Q_PROPERTY(...);
- /// Doc comment
- Q_PROPERTY(...);
-
-public:
- // Classes should have explicit constructors if they aren't intended to
- // implicitly cast. The constructor can be inline in the header if it has no body.
- explicit Foo(QObject* parent = nullptr): QObject(parent) {}
-
- // Instance functions if applicable.
- static Foo* instance();
-
- // Member functions unrelated to properties come next
- void function();
- void function();
- void function();
-
- // Then Q_INVOKABLEs
- Q_INVOKABLE function();
- /// Doc comment
- Q_INVOKABLE function();
- /// Doc comment
- Q_INVOKABLE function();
-
- // Then property related functions, in the order (bindable, getter, setter).
- // Related functions may be included here as well. Function bodies may be inline
- // if they are a single expression. There should be a newline between each
- // property's methods.
- [[nodiscard]] QBindable bindableFoo() { return &this->bFoo; }
- [[nodiscard]] T foo() const { return this->foo; }
- void setFoo();
-
- [[nodiscard]] T bar() const { return this->foo; }
- void setBar();
-
-signals:
- // Signals that are not property change related go first.
- // Property change signals go in property definition order.
- void asd();
- void asd2();
- void fooChanged();
- void barChanged();
-
-public slots:
- // generally Q_INVOKABLEs are preferred to public slots.
- void slot();
-
-private slots:
- // ...
-
-private:
- // statics, then functions, then fields
- static const foo BAR;
- static void foo();
-
- void foo();
- void bar();
-
- // property related members are prefixed with `m`.
- QString mFoo;
- QString bar;
-
- // Bindables go last and should be prefixed with `b`.
- Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged);
-};
-```
-
-### Linter
-All contributions should pass the linter.
-
-Note that running the linter requires disabling precompiled
-headers and including the test codepaths:
-```sh
-$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
-$ just lint-changed
-```
-
-If the linter is complaining about something that you think it should not,
-please disable the lint in your MR and explain your reasoning if it isn't obvious.
-
-### Tests
-If you feel like the feature you are working on is very complex or likely to break,
-please write some tests. We will ask you to directly if you send in an MR for an
-overly complex or breakable feature.
-
-At least all tests that passed before your changes should still be passing
-by the time your contribution is ready.
-
-You can run the tests using `just test` but you must enable them first
-using `-DBUILD_TESTING=ON`.
-
-### Documentation
-Most of quickshell's documentation is automatically generated from the source code.
-You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser
-cannot handle random line breaks and will usually require you to disable clang-format if the
-lines are too long.
-
-Before submitting an MR, if adding new features please make sure the documentation is generated
-reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo.
-
-Doc comments take the form `///` or `///!` (summary) and work with markdown.
-You can reference other types using the `@@[Module.][Type.][member]` shorthand
-where all parts are optional. If module or type are not specified they will
-be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`.
-Look at existing code for how it works.
-
-Quickshell modules additionally have a `module.md` file which contains a summary, description,
-and list of headers to scan for documentation.
-
-## Contributing
-
-### Commits
-Please structure your commit messages as `scope[!]: commit` where
-the scope is something like `core` or `service/mpris`. (pick what has been
-used historically or what makes sense if new). Add `!` for changes that break
-existing APIs or functionality.
-
-Commit descriptions should contain a summary of the changes if they are not
-sufficiently addressed in the commit message.
-
-Please squash/rebase additions or edits to previous changes and follow the
-commit style to keep the history easily searchable at a glance.
-Depending on the change, it is often reasonable to squash it into just
-a single commit. (If you do not follow this we will squash your changes
-for you.)
-
-### Sending patches
-You may contribute by submitting a pull request on github, asking for
-an account on our git server, or emailing patches / git bundles
-directly to `outfoxxed@outfoxxed.me`.
-
-### Getting help
-If you're getting stuck, you can come talk to us in the
-[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me)
-for help on implementation, conventions, etc.
-Feel free to ask for advice early in your implementation if you are
-unsure.
+We handle work for the most part on a push basis. If your PR has been ignored for a while
+and is still relevant please bump it.
diff --git a/HACKING.md b/HACKING.md
new file mode 100644
index 0000000..69357f1
--- /dev/null
+++ b/HACKING.md
@@ -0,0 +1,226 @@
+## Development
+
+Install the dependencies listed in [BUILD.md](BUILD.md).
+You probably want all of them even if you don't use all of them
+to ensure tests work correctly and avoid passing a bunch of configure
+flags when you need to wipe the build directory.
+
+The dependencies are also available as a nix shell or nix flake which we recommend
+using with nix-direnv.
+
+Quickshell uses `just` for common development command aliases.
+
+Common aliases:
+- `just configure [ [extra cmake args]]` (note that you must specify debug/release to specify extra args)
+- `just build` - runs the build, configuring if not configured already.
+- `just run [args]` - runs quickshell with the given arguments
+- `just clean` - clean up build artifacts. `just clean build` is somewhat common.
+
+### Formatting
+All contributions should be formatted similarly to what already exists.
+Group related functionality together.
+
+Run the formatter using `just fmt`.
+If the results look stupid, fix the clang-format file if possible,
+or disable clang-format in the affected area
+using `// clang-format off` and `// clang-format on`.
+
+#### Style preferences not caught by clang-format
+These are flexible. You can ignore them if it looks or works better to
+for one reason or another.
+
+Use `auto` if the type of a variable can be deduced automatically, instead of
+redeclaring the returned value's type. Additionally, auto should be used when a
+constructor takes arguments.
+
+```cpp
+auto x = ; // ok
+auto x = QString::number(3); // ok
+QString x; // ok
+QString x = "foo"; // ok
+auto x = QString("foo"); // ok
+
+auto x = QString(); // avoid
+QString x(); // avoid
+QString x("foo"); // avoid
+```
+
+Put newlines around logical units of code, and after closing braces. If the
+most reasonable logical unit of code takes only a single line, it should be
+merged into the next single line logical unit if applicable.
+```cpp
+// multiple units
+auto x = ; // unit 1
+auto y = ; // unit 2
+
+auto x = ; // unit 1
+emit this->y(); // unit 2
+
+auto x1 = ; // unit 1
+auto x2 = ; // unit 1
+auto x3 = ; // unit 1
+
+auto y1 = ; // unit 2
+auto y2 = ; // unit 2
+auto y3 = ; // unit 2
+
+// one unit
+auto x = ;
+if (x...) {
+ // ...
+}
+
+// if more than one variable needs to be used then add a newline
+auto x = ;
+auto y = ;
+
+if (x && y) {
+ // ...
+}
+```
+
+Class formatting:
+```cpp
+//! Doc comment summary
+/// Doc comment body
+class Foo: public QObject {
+ // The Q_OBJECT macro comes first. Macros are ; terminated.
+ Q_OBJECT;
+ QML_ELEMENT;
+ QML_CLASSINFO(...);
+ // Properties must stay on a single line or the doc generator won't be able to pick them up
+ Q_PROPERTY(...);
+ /// Doc comment
+ Q_PROPERTY(...);
+ /// Doc comment
+ Q_PROPERTY(...);
+
+public:
+ // Classes should have explicit constructors if they aren't intended to
+ // implicitly cast. The constructor can be inline in the header if it has no body.
+ explicit Foo(QObject* parent = nullptr): QObject(parent) {}
+
+ // Instance functions if applicable.
+ static Foo* instance();
+
+ // Member functions unrelated to properties come next
+ void function();
+ void function();
+ void function();
+
+ // Then Q_INVOKABLEs
+ Q_INVOKABLE function();
+ /// Doc comment
+ Q_INVOKABLE function();
+ /// Doc comment
+ Q_INVOKABLE function();
+
+ // Then property related functions, in the order (bindable, getter, setter).
+ // Related functions may be included here as well. Function bodies may be inline
+ // if they are a single expression. There should be a newline between each
+ // property's methods.
+ [[nodiscard]] QBindable bindableFoo() { return &this->bFoo; }
+ [[nodiscard]] T foo() const { return this->foo; }
+ void setFoo();
+
+ [[nodiscard]] T bar() const { return this->foo; }
+ void setBar();
+
+signals:
+ // Signals that are not property change related go first.
+ // Property change signals go in property definition order.
+ void asd();
+ void asd2();
+ void fooChanged();
+ void barChanged();
+
+public slots:
+ // generally Q_INVOKABLEs are preferred to public slots.
+ void slot();
+
+private slots:
+ // ...
+
+private:
+ // statics, then functions, then fields
+ static const foo BAR;
+ static void foo();
+
+ void foo();
+ void bar();
+
+ // property related members are prefixed with `m`.
+ QString mFoo;
+ QString bar;
+
+ // Bindables go last and should be prefixed with `b`.
+ Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged);
+};
+```
+
+Use lowercase .h suffixed Qt headers, e.g. `` over ``.
+
+### Linter
+All contributions should pass the linter.
+
+Note that running the linter requires disabling precompiled
+headers and including the test codepaths:
+```sh
+$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
+$ just lint-changed
+```
+
+If the linter is complaining about something that you think it should not,
+please disable the lint in your MR and explain your reasoning if it isn't obvious.
+
+### Tests
+If you feel like the feature you are working on is very complex or likely to break,
+please write some tests. We will ask you to directly if you send in an MR for an
+overly complex or breakable feature.
+
+At least all tests that passed before your changes should still be passing
+by the time your contribution is ready.
+
+You can run the tests using `just test` but you must enable them first
+using `-DBUILD_TESTING=ON`.
+
+### Documentation
+Most of quickshell's documentation is automatically generated from the source code.
+You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser
+cannot handle random line breaks and will usually require you to disable clang-format if the
+lines are too long.
+
+Make sure new files containing doc comments are added to a `module.md` file.
+See existing module files for reference.
+
+Doc comments take the form `///` or `///!` (summary) and work with markdown.
+You can reference other types using the `@@[Module.][Type.][member]` shorthand
+where all parts are optional. If module or type are not specified they will
+be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`.
+Look at existing code for how it works.
+
+If you have made a user visible change since the last tagged release, describe it in
+[changelog/next.md](changelog/next.md).
+
+## Contributing
+
+### Commits
+Please structure your commit messages as `scope: commit` where
+the scope is something like `core` or `service/mpris`. (pick what has been
+used historically or what makes sense if new).
+
+Commit descriptions should contain a summary of the changes if they are not
+sufficiently addressed in the commit message.
+
+Please squash/rebase additions or edits to previous changes and follow the
+commit style to keep the history easily searchable at a glance.
+Depending on the change, it is often reasonable to squash it into just
+a single commit. (If you do not follow this we will squash your changes
+for you.)
+
+### Getting help
+If you're getting stuck, you can come talk to us in the
+[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me)
+for help on implementation, conventions, etc. There is also a bridged [discord server](https://discord.gg/UtZeT3xNyT).
+Feel free to ask for advice early in your implementation if you are
+unsure.
diff --git a/Justfile b/Justfile
index 2d6377e..801eb2a 100644
--- a/Justfile
+++ b/Justfile
@@ -13,7 +13,7 @@ lint-changed:
git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
lint-staged:
- git diff --staged --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
+ git diff --staged --name-only --diff-filter=d HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
configure target='debug' *FLAGS='':
cmake -GNinja -B {{builddir}} \
diff --git a/README.md b/README.md
index 4491d24..365bdb5 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,9 @@ This repo is hosted at:
- https://github.com/quickshell-mirror/quickshell
# Contributing / Development
-See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
+- [HACKING.md](HACKING.md) - Development instructions and policy.
+- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution policy.
+- [BUILD.md](BUILD.md) - Packaging and build instructions.
#### License
diff --git a/changelog/next.md b/changelog/next.md
new file mode 100644
index 0000000..761161d
--- /dev/null
+++ b/changelog/next.md
@@ -0,0 +1,84 @@
+## Breaking Changes
+
+### Config paths are no longer canonicalized
+
+This fixes nix configs changing shell-ids on rebuild as the shell id is now derived from
+the symlink path. Configs with a symlink in their path will have a different shell id.
+
+Shell ids are used to derive the default config / state / cache folders, so those files
+will need to be manually moved if using a config behind a symlinked path without an explicitly
+set shell id.
+
+## New Features
+
+- Added support for creating Polkit agents.
+- Added support for creating wayland idle inhibitors.
+- Added support for wayland idle timeouts.
+- Added support for inhibiting wayland compositor shortcuts for focused windows.
+- Added the ability to override Quickshell.cacheDir with a custom path.
+- Added minimized, maximized, and fullscreen properties to FloatingWindow.
+- Added the ability to handle move and resize events to FloatingWindow.
+- Pipewire service now reconnects if pipewire dies or a protocol error occurs.
+- Added pipewire audio peak detection.
+- Added network management support.
+- Added support for grabbing focus from popup windows.
+- Added support for IPC signal listeners.
+- 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.
+- Added ext-background-effect window blur support.
+- Added per-corner radius support to Region.
+- Added ColorQuantizer region selection.
+- Added dialog window support to FloatingWindow.
+
+## 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.
+- Added `AppId` pragma and `QS_APP_ID` environment variable to allow overriding the desktop application ID.
+- Added `DropExpensiveFonts` pragma and `QS_DROP_EXPENSIVE_FONTS` environment variable which avoids loading fonts which may cause lag and excessive memory usage if many variants are used.
+- Unrecognized pragmas are no longer a hard error for future backward compatibility.
+
+## Bug Fixes
+
+- Fixed volume control breaking with pipewire pro audio mode.
+- Fixed volume control breaking with bluez streams and potentially others.
+- Fixed volume control breaking for devices without route definitions.
+- Fixed escape sequence handling in desktop entries.
+- 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.
+- Fixed HyprlandFocusGrab crashing if windows were destroyed after being passed to it.
+- Fixed ScreencopyView pixelation when scaled.
+- Fixed JsonAdapter crashing and providing bad data on read when using JsonObject.
+- Fixed JsonAdapter sending unnecessary property changes for primitive values.
+- Fixed JsonAdapter serialization for lists.
+- Fixed pipewire crashes after hotplugging devices and changing default outputs.
+- Fixed launches failing for `--daemonize` on some systems.
+
+## 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).
+- `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore.
+- `DISTRIBUTOR_DEBUGINFO_AVAILABLE` was removed as it is no longer important without breakpad.
+- `libdrm` is now unconditionally required as a direct dependency.
diff --git a/ci/nix-checkouts.nix b/ci/nix-checkouts.nix
index 5a95a34..945973c 100644
--- a/ci/nix-checkouts.nix
+++ b/ci/nix-checkouts.nix
@@ -8,7 +8,17 @@ let
inherit sha256;
}) {};
in rec {
- latest = qt6_9_0;
+ latest = qt6_10_0;
+
+ qt6_10_1 = byCommit {
+ commit = "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38";
+ sha256 = "0fvbizl7j5rv2rf8j76yw0xb3d9l06hahkjys2a7k1yraznvnafm";
+ };
+
+ qt6_10_0 = byCommit {
+ commit = "c5ae371f1a6a7fd27823bc500d9390b38c05fa55";
+ sha256 = "18g0f8cb9m8mxnz9cf48sks0hib79b282iajl2nysyszph993yp0";
+ };
qt6_9_2 = byCommit {
commit = "e9f00bd893984bc8ce46c895c3bf7cac95331127";
diff --git a/default.nix b/default.nix
index adb978b..749ef49 100644
--- a/default.nix
+++ b/default.nix
@@ -10,17 +10,23 @@
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,
pipewire,
pam,
+ polkit,
+ glib,
gitRev ? (let
headExists = builtins.pathExists ./.git/HEAD;
@@ -43,10 +49,14 @@
withPam ? true,
withHyprland ? true,
withI3 ? true,
+ 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.0";
+ version = "0.2.1";
src = nix-gitignore.gitignoreSource "/default.nix\n" ./.;
dontWrapQtApps = true; # see wrappers
@@ -66,17 +76,24 @@
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 ]
- ++ 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.optional withPipewire pipewire
+ ++ lib.optionals withPolkit [ polkit glib ];
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
@@ -85,12 +102,14 @@
(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))
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
(lib.cmakeBool "SERVICE_PAM" withPam)
+ (lib.cmakeBool "SERVICE_NETWORKMANAGER" withNetworkManager)
+ (lib.cmakeBool "SERVICE_POLKIT" withPolkit)
(lib.cmakeBool "HYPRLAND" withHyprland)
(lib.cmakeBool "I3" withI3)
];
diff --git a/flake.lock b/flake.lock
index 6971438..2f95a44 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1758690382,
- "narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=",
+ "lastModified": 1768127708,
+ "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "e643668fd71b949c53f8626614b21ff71a07379d",
+ "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38",
"type": "github"
},
"original": {
diff --git a/quickshell.scm b/quickshell.scm
index 26abdc0..780bb96 100644
--- a/quickshell.scm
+++ b/quickshell.scm
@@ -42,6 +42,7 @@
libxcb
libxkbcommon
linux-pam
+ polkit
mesa
pipewire
qtbase
@@ -55,8 +56,7 @@
#~(list "-GNinja"
"-DDISTRIBUTOR=\"In-tree Guix channel\""
"-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO"
- ;; Breakpad is not currently packaged for Guix.
- "-DCRASH_REPORTER=OFF")
+ "-DCRASH_HANDLER=OFF")
#:phases
#~(modify-phases %standard-phases
(replace 'build (lambda _ (invoke "cmake" "--build" ".")))
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 52db00a..0c05419 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -11,8 +11,9 @@ add_subdirectory(window)
add_subdirectory(io)
add_subdirectory(widgets)
add_subdirectory(ui)
+add_subdirectory(windowmanager)
-if (CRASH_REPORTER)
+if (CRASH_HANDLER)
add_subdirectory(crash)
endif()
@@ -33,3 +34,7 @@ add_subdirectory(services)
if (BLUETOOTH)
add_subdirectory(bluetooth)
endif()
+
+if (NETWORK)
+ add_subdirectory(network)
+endif()
diff --git a/src/bluetooth/adapter.cpp b/src/bluetooth/adapter.cpp
index 0d8a319..7f70a27 100644
--- a/src/bluetooth/adapter.cpp
+++ b/src/bluetooth/adapter.cpp
@@ -9,7 +9,6 @@
#include
#include
#include
-#include
#include
#include "../core/logcat.hpp"
diff --git a/src/bluetooth/device.cpp b/src/bluetooth/device.cpp
index 7265b24..b140aa0 100644
--- a/src/bluetooth/device.cpp
+++ b/src/bluetooth/device.cpp
@@ -8,7 +8,6 @@
#include
#include
#include
-#include
#include
#include
diff --git a/src/build/CMakeLists.txt b/src/build/CMakeLists.txt
index bb35da9..c1ffa59 100644
--- a/src/build/CMakeLists.txt
+++ b/src/build/CMakeLists.txt
@@ -9,16 +9,10 @@ if (NOT DEFINED GIT_REVISION)
)
endif()
-if (CRASH_REPORTER)
- set(CRASH_REPORTER_DEF 1)
+if (CRASH_HANDLER)
+ set(CRASH_HANDLER_DEF 1)
else()
- set(CRASH_REPORTER_DEF 0)
-endif()
-
-if (DISTRIBUTOR_DEBUGINFO_AVAILABLE)
- set(DEBUGINFO_AVAILABLE 1)
-else()
- set(DEBUGINFO_AVAILABLE 0)
+ set(CRASH_HANDLER_DEF 0)
endif()
configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES)
diff --git a/src/build/build.hpp.in b/src/build/build.hpp.in
index 075abd1..acc3c58 100644
--- a/src/build/build.hpp.in
+++ b/src/build/build.hpp.in
@@ -1,12 +1,17 @@
#pragma once
// NOLINTBEGIN
+#define QS_VERSION "@quickshell_VERSION@"
+#define QS_VERSION_MAJOR @quickshell_VERSION_MAJOR@
+#define QS_VERSION_MINOR @quickshell_VERSION_MINOR@
+#define QS_VERSION_PATCH @quickshell_VERSION_PATCH@
+#define QS_UNRELEASED_FEATURES "@UNRELEASED_FEATURES@"
#define GIT_REVISION "@GIT_REVISION@"
#define DISTRIBUTOR "@DISTRIBUTOR@"
-#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@
-#define CRASH_REPORTER @CRASH_REPORTER_DEF@
+#define CRASH_HANDLER @CRASH_HANDLER_DEF@
#define BUILD_TYPE "@CMAKE_BUILD_TYPE@"
#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)"
#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@"
#define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@"
+#define CRASHREPORT_URL "@CRASHREPORT_URL@"
// NOLINTEND
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 6029b42..4824965 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -1,3 +1,4 @@
+pkg_check_modules(libdrm REQUIRED IMPORTED_TARGET libdrm)
qt_add_library(quickshell-core STATIC
plugin.cpp
shell.cpp
@@ -12,6 +13,7 @@ qt_add_library(quickshell-core STATIC
singleton.cpp
generation.cpp
scan.cpp
+ scanenv.cpp
qsintercept.cpp
incubator.cpp
lazyloader.cpp
@@ -24,7 +26,6 @@ qt_add_library(quickshell-core STATIC
elapsedtimer.cpp
desktopentry.cpp
desktopentrymonitor.cpp
- objectrepeater.cpp
platformmenu.cpp
qsmenu.cpp
retainable.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::Widgets)
+target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build PkgConfig::libdrm)
qs_module_pch(quickshell-core SET large)
diff --git a/src/core/colorquantizer.cpp b/src/core/colorquantizer.cpp
index 4ac850b..d983f76 100644
--- a/src/core/colorquantizer.cpp
+++ b/src/core/colorquantizer.cpp
@@ -13,6 +13,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -24,9 +25,15 @@ namespace {
QS_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg);
}
-ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize)
+ColorQuantizerOperation::ColorQuantizerOperation(
+ QUrl* source,
+ qreal depth,
+ QRect imageRect,
+ qreal rescaleSize
+)
: source(source)
, maxDepth(depth)
+ , imageRect(imageRect)
, rescaleSize(rescaleSize) {
this->setAutoDelete(false);
}
@@ -37,6 +44,11 @@ void ColorQuantizerOperation::quantizeImage(const QAtomicInteger& shouldCa
this->colors.clear();
auto image = QImage(this->source->toLocalFile());
+
+ if (this->imageRect.isValid()) {
+ image = image.copy(this->imageRect);
+ }
+
if ((image.width() > this->rescaleSize || image.height() > this->rescaleSize)
&& this->rescaleSize > 0)
{
@@ -198,16 +210,27 @@ void ColorQuantizer::setDepth(qreal depth) {
this->mDepth = depth;
emit this->depthChanged();
- if (this->componentCompleted) this->quantizeAsync();
+ if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync();
}
}
+void ColorQuantizer::setImageRect(QRect imageRect) {
+ if (this->mImageRect != imageRect) {
+ this->mImageRect = imageRect;
+ emit this->imageRectChanged();
+
+ if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync();
+ }
+}
+
+void ColorQuantizer::resetImageRect() { this->setImageRect(QRect()); }
+
void ColorQuantizer::setRescaleSize(int rescaleSize) {
if (this->mRescaleSize != rescaleSize) {
this->mRescaleSize = rescaleSize;
emit this->rescaleSizeChanged();
- if (this->componentCompleted) this->quantizeAsync();
+ if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync();
}
}
@@ -221,8 +244,13 @@ void ColorQuantizer::quantizeAsync() {
if (this->liveOperation) this->cancelAsync();
qCDebug(logColorQuantizer) << "Starting color quantization asynchronously";
- this->liveOperation =
- new ColorQuantizerOperation(&this->mSource, this->mDepth, this->mRescaleSize);
+
+ this->liveOperation = new ColorQuantizerOperation(
+ &this->mSource,
+ this->mDepth,
+ this->mImageRect,
+ this->mRescaleSize
+ );
QObject::connect(
this->liveOperation,
diff --git a/src/core/colorquantizer.hpp b/src/core/colorquantizer.hpp
index f6e158d..0159181 100644
--- a/src/core/colorquantizer.hpp
+++ b/src/core/colorquantizer.hpp
@@ -5,6 +5,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -16,7 +17,7 @@ class ColorQuantizerOperation
Q_OBJECT;
public:
- explicit ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize);
+ explicit ColorQuantizerOperation(QUrl* source, qreal depth, QRect imageRect, qreal rescaleSize);
void run() override;
void tryCancel();
@@ -44,6 +45,7 @@ private:
QList colors;
QUrl* source;
qreal maxDepth;
+ QRect imageRect;
qreal rescaleSize;
};
@@ -78,6 +80,13 @@ class ColorQuantizer
/// binary split of the color space
Q_PROPERTY(qreal depth READ depth WRITE setDepth NOTIFY depthChanged);
+ // clang-format off
+ /// Rectangle that the source image is cropped to.
+ ///
+ /// Can be set to `undefined` to reset.
+ Q_PROPERTY(QRect imageRect READ imageRect WRITE setImageRect RESET resetImageRect NOTIFY imageRectChanged);
+ // clang-format on
+
/// The size to rescale the image to, when rescaleSize is 0 then no scaling will be done.
/// > [!NOTE] Results from color quantization doesn't suffer much when rescaling, it's
/// > reccommended to rescale, otherwise the quantization process will take much longer.
@@ -97,6 +106,10 @@ public:
[[nodiscard]] qreal depth() const { return this->mDepth; }
void setDepth(qreal depth);
+ [[nodiscard]] QRect imageRect() const { return this->mImageRect; }
+ void setImageRect(QRect imageRect);
+ void resetImageRect();
+
[[nodiscard]] qreal rescaleSize() const { return this->mRescaleSize; }
void setRescaleSize(int rescaleSize);
@@ -104,6 +117,7 @@ signals:
void colorsChanged();
void sourceChanged();
void depthChanged();
+ void imageRectChanged();
void rescaleSizeChanged();
public slots:
@@ -117,6 +131,7 @@ private:
ColorQuantizerOperation* liveOperation = nullptr;
QUrl mSource;
qreal mDepth = 0;
+ QRect mImageRect;
qreal mRescaleSize = 0;
Q_OBJECT_BINDABLE_PROPERTY(
diff --git a/src/core/debuginfo.cpp b/src/core/debuginfo.cpp
new file mode 100644
index 0000000..abc467d
--- /dev/null
+++ b/src/core/debuginfo.cpp
@@ -0,0 +1,176 @@
+#include "debuginfo.hpp"
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "build.hpp"
+
+extern char** environ; // NOLINT
+
+namespace qs::debuginfo {
+
+QString qsVersion() {
+ return QS_VERSION " (revision " GIT_REVISION ", distributed by " DISTRIBUTOR ")";
+}
+
+QString qtVersion() { return qVersion() % QStringLiteral(" (built against " QT_VERSION_STR ")"); }
+
+QString gpuInfo() {
+ auto deviceCount = drmGetDevices2(0, nullptr, 0);
+ if (deviceCount < 0) return "Failed to get DRM device count: " % QString::number(deviceCount);
+ auto* devices = new drmDevicePtr[deviceCount];
+ auto devicesArrayGuard = qScopeGuard([&] { delete[] devices; });
+ auto r = drmGetDevices2(0, devices, deviceCount);
+ if (deviceCount < 0) return "Failed to get DRM devices: " % QString::number(r);
+ auto devicesGuard = qScopeGuard([&] {
+ for (auto i = 0; i != deviceCount; ++i) drmFreeDevice(&devices[i]); // NOLINT
+ });
+
+ QString info;
+ auto stream = QTextStream(&info);
+
+ for (auto i = 0; i != deviceCount; ++i) {
+ auto* device = devices[i]; // NOLINT
+
+ int deviceNodeType = -1;
+ if (device->available_nodes & (1 << DRM_NODE_RENDER)) deviceNodeType = DRM_NODE_RENDER;
+ else if (device->available_nodes & (1 << DRM_NODE_PRIMARY)) deviceNodeType = DRM_NODE_PRIMARY;
+
+ if (deviceNodeType == -1) continue;
+
+ auto* deviceNode = device->nodes[DRM_NODE_RENDER]; // NOLINT
+
+ auto driver = [&]() -> QString {
+ auto fd = open(deviceNode, O_RDWR | O_CLOEXEC);
+ if (fd == -1) return "";
+ auto fdGuard = qScopeGuard([&] { close(fd); });
+ auto* ver = drmGetVersion(fd);
+ if (!ver) return "";
+ auto verGuard = qScopeGuard([&] { drmFreeVersion(ver); });
+
+ // clang-format off
+ return QString(ver->name)
+ % ' ' % QString::number(ver->version_major)
+ % '.' % QString::number(ver->version_minor)
+ % '.' % QString::number(ver->version_patchlevel)
+ % " (" % ver->desc % ')';
+ // clang-format on
+ }();
+
+ QString product = "unknown";
+ QString address = "unknown";
+
+ auto hex = [](int num, int pad) { return QString::number(num, 16).rightJustified(pad, '0'); };
+
+ switch (device->bustype) {
+ case DRM_BUS_PCI: {
+ auto* b = device->businfo.pci;
+ auto* d = device->deviceinfo.pci;
+ address = "PCI " % hex(b->bus, 2) % ':' % hex(b->dev, 2) % '.' % hex(b->func, 1);
+ product = hex(d->vendor_id, 4) % ':' % hex(d->device_id, 4);
+ } break;
+ case DRM_BUS_USB: {
+ auto* b = device->businfo.usb;
+ auto* d = device->deviceinfo.usb;
+ address = "USB " % QString::number(b->bus) % ':' % QString::number(b->dev);
+ product = hex(d->vendor, 4) % ':' % hex(d->product, 4);
+ } break;
+ default: break;
+ }
+
+ stream << "GPU " << deviceNode << "\n Driver: " << driver << "\n Model: " << product
+ << "\n Address: " << address << '\n';
+ }
+
+ return info;
+}
+
+QString systemInfo() {
+ QString info;
+ auto stream = QTextStream(&info);
+
+ stream << gpuInfo() << '\n';
+
+ stream << "/etc/os-release:";
+ auto osReleaseFile = QFile("/etc/os-release");
+ if (osReleaseFile.open(QFile::ReadOnly)) {
+ stream << '\n' << osReleaseFile.readAll() << '\n';
+ osReleaseFile.close();
+ } else {
+ stream << "FAILED TO OPEN\n";
+ }
+
+ stream << "/etc/lsb-release:";
+ auto lsbReleaseFile = QFile("/etc/lsb-release");
+ if (lsbReleaseFile.open(QFile::ReadOnly)) {
+ stream << '\n' << lsbReleaseFile.readAll();
+ lsbReleaseFile.close();
+ } else {
+ stream << "FAILED TO OPEN\n";
+ }
+
+ return info;
+}
+
+QString envInfo() {
+ QString info;
+ auto stream = QTextStream(&info);
+
+ for (auto** envp = environ; *envp != nullptr; ++envp) { // NOLINT
+ auto prefixes = std::array {
+ "QS_",
+ "QT_",
+ "QML_",
+ "QML2_",
+ "QSG_",
+ "XDG_CURRENT_DESKTOP=",
+ };
+
+ for (const auto& prefix: prefixes) {
+ if (strncmp(prefix.data(), *envp, prefix.length()) == 0) goto print;
+ }
+ continue;
+
+ print:
+ stream << *envp << '\n';
+ }
+
+ return info;
+}
+
+QString combinedInfo() {
+ QString info;
+ auto stream = QTextStream(&info);
+
+ stream << "===== Version Information =====\n";
+ stream << "Quickshell: " << qsVersion() << '\n';
+ stream << "Qt: " << qtVersion() << '\n';
+
+ stream << "\n===== Build Information =====\n";
+ stream << "Build Type: " << BUILD_TYPE << '\n';
+ stream << "Compiler: " << COMPILER << '\n';
+ stream << "Compile Flags: " << COMPILE_FLAGS << '\n';
+ stream << "Configuration:\n" << BUILD_CONFIGURATION << '\n';
+
+ stream << "\n===== System Information =====\n";
+ stream << systemInfo();
+
+ stream << "\n===== Environment (trimmed) =====\n";
+ stream << envInfo();
+
+ return info;
+}
+
+} // namespace qs::debuginfo
diff --git a/src/core/debuginfo.hpp b/src/core/debuginfo.hpp
new file mode 100644
index 0000000..fc766fc
--- /dev/null
+++ b/src/core/debuginfo.hpp
@@ -0,0 +1,14 @@
+#pragma once
+
+#include
+
+namespace qs::debuginfo {
+
+QString qsVersion();
+QString qtVersion();
+QString gpuInfo();
+QString systemInfo();
+QString envInfo();
+QString combinedInfo();
+
+} // namespace qs::debuginfo
diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp
index 941a405..637f758 100644
--- a/src/core/desktopentry.cpp
+++ b/src/core/desktopentry.cpp
@@ -107,7 +107,10 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString&
auto groupName = QString();
auto entries = QHash>();
- auto finishCategory = [&data, &groupName, &entries]() {
+ auto actionOrder = QStringList();
+ auto pendingActions = QHash();
+
+ auto finishCategory = [&data, &groupName, &entries, &actionOrder, &pendingActions]() {
if (groupName == "Desktop Entry") {
if (entries.value("Type").second != "Application") return;
@@ -129,9 +132,10 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString&
else if (key == "Terminal") data.terminal = value == "true";
else if (key == "Categories") data.categories = value.split(u';', Qt::SkipEmptyParts);
else if (key == "Keywords") data.keywords = value.split(u';', Qt::SkipEmptyParts);
+ else if (key == "Actions") actionOrder = value.split(u';', Qt::SkipEmptyParts);
}
} else if (groupName.startsWith("Desktop Action ")) {
- auto actionName = groupName.sliced(16);
+ auto actionName = groupName.sliced(15);
DesktopActionData action;
action.id = actionName;
@@ -147,7 +151,7 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString&
}
}
- data.actions.insert(actionName, action);
+ pendingActions.insert(actionName, action);
}
entries.clear();
@@ -193,6 +197,13 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString&
}
finishCategory();
+
+ for (const auto& actionId: actionOrder) {
+ if (pendingActions.contains(actionId)) {
+ data.actions.append(pendingActions.value(actionId));
+ }
+ }
+
return data;
}
@@ -216,17 +227,18 @@ void DesktopEntry::updateState(const ParsedDesktopEntryData& newState) {
this->updateActions(newState.actions);
}
-void DesktopEntry::updateActions(const QHash& newActions) {
+void DesktopEntry::updateActions(const QVector& newActions) {
auto old = this->mActions;
+ this->mActions.clear();
- for (const auto& [key, d]: newActions.asKeyValueRange()) {
+ for (const auto& d: newActions) {
DesktopAction* act = nullptr;
- if (auto found = old.find(key); found != old.end()) {
- act = found.value();
+ auto found = std::ranges::find(old, d.id, &DesktopAction::mId);
+ if (found != old.end()) {
+ act = *found;
old.erase(found);
} else {
act = new DesktopAction(d.id, this);
- this->mActions.insert(key, act);
}
Qt::beginPropertyUpdateGroup();
@@ -237,6 +249,7 @@ void DesktopEntry::updateActions(const QHash& newAct
Qt::endPropertyUpdateGroup();
act->mEntries = d.entries;
+ this->mActions.append(act);
}
for (auto* leftover: old) {
@@ -250,7 +263,7 @@ void DesktopEntry::execute() const {
bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); }
-QVector DesktopEntry::actions() const { return this->mActions.values(); }
+QVector DesktopEntry::actions() const { return this->mActions; }
QVector DesktopEntry::parseExecString(const QString& execString) {
QVector arguments;
@@ -269,16 +282,22 @@ QVector DesktopEntry::parseExecString(const QString& execString) {
currentArgument += '\\';
escape = 0;
}
+ } else if (escape == 2) {
+ currentArgument += c;
+ escape = 0;
} else if (escape != 0) {
- if (escape != 2) {
- // Technically this is an illegal state, but the spec has a terrible double escape
- // rule in strings for no discernable reason. Assuming someone might understandably
- // misunderstand it, treat it as a normal escape and log it.
+ switch (c.unicode()) {
+ case 's': currentArgument += u' '; break;
+ case 'n': currentArgument += u'\n'; break;
+ case 't': currentArgument += u'\t'; break;
+ case 'r': currentArgument += u'\r'; break;
+ case '\\': currentArgument += u'\\'; break;
+ default:
qCWarning(logDesktopEntry).noquote()
<< "Illegal escape sequence in desktop entry exec string:" << execString;
+ currentArgument += c;
+ break;
}
-
- currentArgument += c;
escape = 0;
} else if (c == u'"' || c == u'\'') {
parsingString = false;
diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp
index 623019d..0d1eff2 100644
--- a/src/core/desktopentry.hpp
+++ b/src/core/desktopentry.hpp
@@ -43,7 +43,7 @@ struct ParsedDesktopEntryData {
QVector categories;
QVector keywords;
QHash entries;
- QHash actions;
+ QVector actions;
};
/// A desktop entry. See @@DesktopEntries for details.
@@ -164,10 +164,10 @@ public:
// clang-format on
private:
- void updateActions(const QHash& newActions);
+ void updateActions(const QVector& newActions);
ParsedDesktopEntryData state;
- QHash mActions;
+ QVector mActions;
friend class DesktopAction;
};
diff --git a/src/core/generation.cpp b/src/core/generation.cpp
index e15103a..21febc3 100644
--- a/src/core/generation.cpp
+++ b/src/core/generation.cpp
@@ -49,7 +49,8 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
this->engine->addImportPath("qs:@/");
this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory);
- this->engine->setIncubationController(&this->delayedIncubationController);
+ this->incubationController.initLoop();
+ this->engine->setIncubationController(&this->incubationController);
this->engine->addImageProvider("icon", new IconImageProvider());
this->engine->addImageProvider("qsimage", new QsImageProvider());
@@ -134,7 +135,7 @@ void EngineGeneration::onReload(EngineGeneration* old) {
// new generation acquires it then incubators will hang intermittently
qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old;
old->incubationControllersLocked = true;
- old->assignIncubationController();
+ old->updateIncubationMode();
}
QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit);
@@ -208,6 +209,8 @@ bool EngineGeneration::setExtraWatchedFiles(const QVector& files) {
for (const auto& file: files) {
if (!this->scanner.scannedFiles.contains(file)) {
this->extraWatchedFiles.append(file);
+ QByteArray data;
+ this->scanner.readAndHashFile(file, data);
}
}
@@ -228,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();
}
}
@@ -236,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;
}
@@ -288,29 +301,18 @@ void EngineGeneration::trackWindowIncubationController(QQuickWindow* window) {
QObject::connect(window, &QObject::destroyed, this, &EngineGeneration::onTrackedWindowDestroyed);
this->trackedWindows.append(window);
- this->assignIncubationController();
+ this->updateIncubationMode();
}
void EngineGeneration::onTrackedWindowDestroyed(QObject* object) {
this->trackedWindows.removeAll(static_cast(object)); // NOLINT
- this->assignIncubationController();
+ this->updateIncubationMode();
}
-void EngineGeneration::assignIncubationController() {
- QQmlIncubationController* controller = &this->delayedIncubationController;
-
- for (auto* window: this->trackedWindows) {
- if (auto* wctl = window->incubationController()) {
- controller = wctl;
- break;
- }
- }
-
- qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"
- << this
- << "fallback:" << (controller == &this->delayedIncubationController);
-
- this->engine->setIncubationController(controller);
+void EngineGeneration::updateIncubationMode() {
+ // If we're in a situation with only hidden but tracked windows this might be wrong,
+ // but it seems to at least work.
+ this->incubationController.setIncubationMode(!this->trackedWindows.empty());
}
EngineGeneration* EngineGeneration::currentGeneration() {
diff --git a/src/core/generation.hpp b/src/core/generation.hpp
index fef8363..4543408 100644
--- a/src/core/generation.hpp
+++ b/src/core/generation.hpp
@@ -65,7 +65,7 @@ public:
QFileSystemWatcher* watcher = nullptr;
QVector deletedWatchedFiles;
QVector extraWatchedFiles;
- DelayedQmlIncubationController delayedIncubationController;
+ QsIncubationController incubationController;
bool reloadComplete = false;
QuickshellGlobal* qsgInstance = nullptr;
@@ -89,7 +89,7 @@ private slots:
private:
void postReload();
- void assignIncubationController();
+ void updateIncubationMode();
QVector trackedWindows;
bool incubationControllersLocked = false;
QHash extensions;
diff --git a/src/core/iconimageprovider.cpp b/src/core/iconimageprovider.cpp
index 43e00fd..1dbe3e7 100644
--- a/src/core/iconimageprovider.cpp
+++ b/src/core/iconimageprovider.cpp
@@ -19,8 +19,7 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
if (splitIdx != -1) {
iconName = id.sliced(0, splitIdx);
path = id.sliced(splitIdx + 6);
- qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for"
- << id;
+ path = QString("/%1/%2").arg(path, iconName.sliced(iconName.lastIndexOf('/') + 1));
} else {
splitIdx = id.indexOf("?fallback=");
if (splitIdx != -1) {
@@ -32,7 +31,8 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
}
auto icon = QIcon::fromTheme(iconName);
- if (icon.isNull()) icon = QIcon::fromTheme(fallbackName);
+ if (icon.isNull() && !fallbackName.isEmpty()) icon = QIcon::fromTheme(fallbackName);
+ if (icon.isNull() && !path.isEmpty()) icon = QPixmap(path);
auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100);
if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2);
diff --git a/src/core/iconprovider.cpp b/src/core/iconprovider.cpp
index 99b423e..383f7e1 100644
--- a/src/core/iconprovider.cpp
+++ b/src/core/iconprovider.cpp
@@ -22,8 +22,8 @@ class PixmapCacheIconEngine: public QIconEngine {
QIcon::Mode /*unused*/,
QIcon::State /*unused*/
) override {
- qFatal(
- ) << "Unexpected icon paint request bypassed pixmap method. Please report this as a bug.";
+ qFatal()
+ << "Unexpected icon paint request bypassed pixmap method. Please report this as a bug.";
}
QPixmap pixmap(const QSize& size, QIcon::Mode /*unused*/, QIcon::State /*unused*/) override {
diff --git a/src/core/incubator.cpp b/src/core/incubator.cpp
index c9d149a..f031b11 100644
--- a/src/core/incubator.cpp
+++ b/src/core/incubator.cpp
@@ -1,7 +1,16 @@
#include "incubator.hpp"
+#include
+#include
+#include
#include
+#include
+#include
+#include
+#include
+#include
#include
+#include
#include
#include "logcat.hpp"
@@ -15,3 +24,112 @@ void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) {
default: break;
}
}
+
+void QsIncubationController::initLoop() {
+ auto* app = static_cast(QGuiApplication::instance()); // NOLINT
+ this->renderLoop = QSGRenderLoop::instance();
+
+ QObject::connect(
+ app,
+ &QGuiApplication::screenAdded,
+ this,
+ &QsIncubationController::updateIncubationTime
+ );
+
+ QObject::connect(
+ app,
+ &QGuiApplication::screenRemoved,
+ this,
+ &QsIncubationController::updateIncubationTime
+ );
+
+ this->updateIncubationTime();
+
+ QObject::connect(
+ this->renderLoop,
+ &QSGRenderLoop::timeToIncubate,
+ this,
+ &QsIncubationController::incubate
+ );
+
+ QAnimationDriver* animationDriver = this->renderLoop->animationDriver();
+ if (animationDriver) {
+ QObject::connect(
+ animationDriver,
+ &QAnimationDriver::stopped,
+ this,
+ &QsIncubationController::animationStopped
+ );
+ } else {
+ qCInfo(logIncubator) << "Render loop does not have animation driver, animationStopped cannot "
+ "be used to trigger incubation.";
+ }
+}
+
+void QsIncubationController::setIncubationMode(bool render) {
+ if (render == this->followRenderloop) return;
+ this->followRenderloop = render;
+
+ if (render) {
+ qCDebug(logIncubator) << "Incubation mode changed: render loop driven";
+ } else {
+ qCDebug(logIncubator) << "Incubation mode changed: event loop driven";
+ }
+
+ if (!render && this->incubatingObjectCount()) this->incubateLater();
+}
+
+void QsIncubationController::timerEvent(QTimerEvent* /*event*/) {
+ this->killTimer(this->timerId);
+ this->timerId = 0;
+ this->incubate();
+}
+
+void QsIncubationController::incubateLater() {
+ if (this->followRenderloop) {
+ if (this->timerId != 0) {
+ this->killTimer(this->timerId);
+ this->timerId = 0;
+ }
+
+ // Incubate again at the end of the event processing queue
+ QMetaObject::invokeMethod(this, &QsIncubationController::incubate, Qt::QueuedConnection);
+ } else if (this->timerId == 0) {
+ // Wait for a while before processing the next batch. Using a
+ // timer to avoid starvation of system events.
+ this->timerId = this->startTimer(this->incubationTime);
+ }
+}
+
+void QsIncubationController::incubate() {
+ if ((!this->followRenderloop || this->renderLoop) && this->incubatingObjectCount()) {
+ if (!this->followRenderloop) {
+ this->incubateFor(10);
+ if (this->incubatingObjectCount()) this->incubateLater();
+ } else if (this->renderLoop->interleaveIncubation()) {
+ this->incubateFor(this->incubationTime);
+ } else {
+ this->incubateFor(this->incubationTime * 2);
+ if (this->incubatingObjectCount()) this->incubateLater();
+ }
+ }
+}
+
+void QsIncubationController::animationStopped() { this->incubate(); }
+
+void QsIncubationController::incubatingObjectCountChanged(int count) {
+ if (count
+ && (!this->followRenderloop
+ || (this->renderLoop && !this->renderLoop->interleaveIncubation())))
+ {
+ this->incubateLater();
+ }
+}
+
+void QsIncubationController::updateIncubationTime() {
+ auto* screen = QGuiApplication::primaryScreen();
+ if (!screen) return;
+
+ // 1/3 frame on primary screen
+ this->incubationTime = qMax(1, static_cast(1000 / screen->refreshRate() / 3));
+}
diff --git a/src/core/incubator.hpp b/src/core/incubator.hpp
index 5ebb9a0..15dc49a 100644
--- a/src/core/incubator.hpp
+++ b/src/core/incubator.hpp
@@ -1,6 +1,7 @@
#pragma once
#include
+#include
#include
#include
@@ -25,7 +26,37 @@ signals:
void failed();
};
-class DelayedQmlIncubationController: public QQmlIncubationController {
- // Do nothing.
- // This ensures lazy loaders don't start blocking before onReload creates windows.
+class QSGRenderLoop;
+
+class QsIncubationController
+ : public QObject
+ , public QQmlIncubationController {
+ Q_OBJECT
+
+public:
+ void initLoop();
+ void setIncubationMode(bool render);
+ void incubateLater();
+
+protected:
+ void timerEvent(QTimerEvent* event) override;
+
+public slots:
+ void incubate();
+ void animationStopped();
+ void updateIncubationTime();
+
+protected:
+ void incubatingObjectCountChanged(int count) override;
+
+private:
+// QPointer did not work with forward declarations prior to 6.7
+#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
+ QPointer renderLoop = nullptr;
+#else
+ QSGRenderLoop* renderLoop = nullptr;
+#endif
+ int incubationTime = 0;
+ int timerId = 0;
+ bool followRenderloop = false;
};
diff --git a/src/core/instanceinfo.cpp b/src/core/instanceinfo.cpp
index 7f0132b..b9b7b44 100644
--- a/src/core/instanceinfo.cpp
+++ b/src/core/instanceinfo.cpp
@@ -3,12 +3,14 @@
#include
QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) {
- stream << info.instanceId << info.configPath << info.shellId << info.launchTime << info.pid;
+ stream << info.instanceId << info.configPath << info.shellId << info.appId << info.launchTime
+ << info.pid << info.display;
return stream;
}
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) {
- stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime >> info.pid;
+ stream >> info.instanceId >> info.configPath >> info.shellId >> info.appId >> info.launchTime
+ >> info.pid >> info.display;
return stream;
}
diff --git a/src/core/instanceinfo.hpp b/src/core/instanceinfo.hpp
index 98ce614..a4a7e66 100644
--- a/src/core/instanceinfo.hpp
+++ b/src/core/instanceinfo.hpp
@@ -9,8 +9,10 @@ struct InstanceInfo {
QString instanceId;
QString configPath;
QString shellId;
+ QString appId;
QDateTime launchTime;
pid_t pid = -1;
+ QString display;
static InstanceInfo CURRENT; // NOLINT
};
@@ -34,6 +36,8 @@ namespace qs::crash {
struct CrashInfo {
int logFd = -1;
+ int traceFd = -1;
+ int infoFd = -1;
static CrashInfo INSTANCE; // NOLINT
};
diff --git a/src/core/lazyloader.hpp b/src/core/lazyloader.hpp
index dbaad4b..56cc964 100644
--- a/src/core/lazyloader.hpp
+++ b/src/core/lazyloader.hpp
@@ -82,9 +82,6 @@
/// > Notably, @@Variants does not corrently support asynchronous
/// > loading, meaning using it inside a LazyLoader will block similarly to not
/// > having a loader to start with.
-///
-/// > [!WARNING] LazyLoaders do not start loading before the first window is created,
-/// > meaning if you create all windows inside of lazy loaders, none of them will ever load.
class LazyLoader: public Reloadable {
Q_OBJECT;
/// The fully loaded item if the loader is @@loading or @@active, or `null`
diff --git a/src/core/logging.cpp b/src/core/logging.cpp
index 034a14d..1b19fab 100644
--- a/src/core/logging.cpp
+++ b/src/core/logging.cpp
@@ -14,6 +14,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -27,7 +28,13 @@
#include
#include
#include
+#ifdef __linux__
#include
+#include
+#endif
+#ifdef __FreeBSD__
+#include
+#endif
#include "instanceinfo.hpp"
#include "logcat.hpp"
@@ -43,6 +50,57 @@ using namespace qt_logging_registry;
QS_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg);
+namespace {
+bool copyFileData(int sourceFd, int destFd, qint64 size) {
+ auto usize = static_cast(size);
+
+#ifdef __linux__
+ off_t offset = 0;
+ auto remaining = usize;
+
+ while (remaining > 0) {
+ auto r = sendfile(destFd, sourceFd, &offset, remaining);
+ if (r == -1) {
+ if (errno == EINTR) continue;
+ return false;
+ }
+ if (r == 0) break;
+ remaining -= static_cast(r);
+ }
+
+ return true;
+#else
+ std::array buffer = {};
+ auto remaining = usize;
+
+ while (remaining > 0) {
+ auto chunk = std::min(remaining, buffer.size());
+ auto r = ::read(sourceFd, buffer.data(), chunk);
+ if (r == -1) {
+ if (errno == EINTR) continue;
+ return false;
+ }
+ if (r == 0) break;
+
+ auto readBytes = static_cast(r);
+ size_t written = 0;
+ while (written < readBytes) {
+ auto w = ::write(destFd, buffer.data() + written, readBytes - written);
+ if (w == -1) {
+ if (errno == EINTR) continue;
+ return false;
+ }
+ written += static_cast(w);
+ }
+
+ remaining -= readBytes;
+ }
+
+ return true;
+#endif
+}
+} // namespace
+
bool LogMessage::operator==(const LogMessage& other) const {
// note: not including time
return this->type == other.type && this->category == other.category && this->body == other.body;
@@ -163,6 +221,7 @@ void LogManager::messageHandler(
}
if (display) {
+ auto locker = QMutexLocker(&self->stdoutMutex);
LogMessage::formatMessage(
self->stdoutStream,
message,
@@ -251,10 +310,15 @@ void LogManager::init(
instance->rules->append(parser.rules());
}
- qInstallMessageHandler(&LogManager::messageHandler);
-
instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory);
+ if (instance->lastCategoryFilter == &LogManager::filterCategory) {
+ qCFatal(logLogging) << "Quickshell's log filter has been installed twice. This is a bug.";
+ instance->lastCategoryFilter = nullptr;
+ }
+
+ qInstallMessageHandler(&LogManager::messageHandler);
+
qCDebug(logLogging) << "Creating offthread logger...";
auto* thread = new QThread();
instance->threadProxy.moveToThread(thread);
@@ -361,7 +425,8 @@ void ThreadLogging::initFs() {
auto* runDir = QsPaths::instance()->instanceRunDir();
if (!runDir) {
- qCCritical(logLogging
+ qCCritical(
+ logLogging
) << "Could not start filesystem logging as the runtime directory could not be created.";
return;
}
@@ -372,7 +437,8 @@ void ThreadLogging::initFs() {
auto* detailedFile = new QFile(detailedPath);
if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
- qCCritical(logLogging
+ qCCritical(
+ logLogging
) << "Could not start filesystem logger as the log file could not be created:"
<< path;
delete file;
@@ -383,13 +449,14 @@ void ThreadLogging::initFs() {
// buffered by WriteBuffer
if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) {
- qCCritical(logLogging
+ qCCritical(
+ logLogging
) << "Could not start detailed filesystem logger as the log file could not be created:"
<< detailedPath;
delete detailedFile;
detailedFile = nullptr;
} else {
- auto lock = flock {
+ struct flock lock = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
@@ -411,7 +478,11 @@ void ThreadLogging::initFs() {
auto* oldFile = this->file;
if (oldFile) {
oldFile->seek(0);
- sendfile(file->handle(), oldFile->handle(), nullptr, oldFile->size());
+
+ if (!copyFileData(oldFile->handle(), file->handle(), oldFile->size())) {
+ qCritical(logLogging) << "Failed to copy log from memfd with error code " << errno
+ << qt_error_string(errno);
+ }
}
this->file = file;
@@ -423,7 +494,10 @@ void ThreadLogging::initFs() {
auto* oldFile = this->detailedFile;
if (oldFile) {
oldFile->seek(0);
- sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size());
+ if (!copyFileData(oldFile->handle(), detailedFile->handle(), oldFile->size())) {
+ qCritical(logLogging) << "Failed to copy detailed log from memfd with error code " << errno
+ << qt_error_string(errno);
+ }
}
crash::CrashInfo::INSTANCE.logFd = detailedFile->handle();
@@ -746,11 +820,11 @@ bool EncodedLogReader::readVarInt(quint32* slot) {
if (!this->reader.skip(1)) return false;
*slot = qFromLittleEndian(n);
} else if ((bytes[1] != 0xff || bytes[2] != 0xff) && readLength >= 3) {
- auto n = *reinterpret_cast(bytes.data() + 1);
+ auto n = *reinterpret_cast(bytes.data() + 1); // NOLINT
if (!this->reader.skip(3)) return false;
*slot = qFromLittleEndian(n);
} else if (readLength == 7) {
- auto n = *reinterpret_cast(bytes.data() + 3);
+ auto n = *reinterpret_cast(bytes.data() + 3); // NOLINT
if (!this->reader.skip(7)) return false;
*slot = qFromLittleEndian(n);
} else return false;
@@ -886,7 +960,7 @@ bool LogReader::continueReading() {
}
void LogFollower::FcntlWaitThread::run() {
- auto lock = flock {
+ struct flock lock = {
.l_type = F_RDLCK, // won't block other read locks when we take it
.l_whence = SEEK_SET,
.l_start = 0,
diff --git a/src/core/logging.hpp b/src/core/logging.hpp
index bf81133..7b6a758 100644
--- a/src/core/logging.hpp
+++ b/src/core/logging.hpp
@@ -10,6 +10,7 @@
#include
#include
#include
+#include
#include
#include
@@ -135,6 +136,7 @@ private:
QHash allFilters;
QTextStream stdoutStream;
+ QMutex stdoutMutex;
LoggingThreadProxy threadProxy;
friend void initLogCategoryLevel(const char* name, QtMsgType defaultLevel);
diff --git a/src/core/model.cpp b/src/core/model.cpp
index 165c606..47ef060 100644
--- a/src/core/model.cpp
+++ b/src/core/model.cpp
@@ -1,81 +1,14 @@
#include "model.hpp"
-#include
+#include
#include
#include
-#include
-#include
-#include
-#include
-#include
-
-qint32 UntypedObjectModel::rowCount(const QModelIndex& parent) const {
- if (parent != QModelIndex()) return 0;
- return static_cast(this->valuesList.length());
-}
-
-QVariant UntypedObjectModel::data(const QModelIndex& index, qint32 role) const {
- if (role != Qt::UserRole) return QVariant();
- return QVariant::fromValue(this->valuesList.at(index.row()));
-}
QHash UntypedObjectModel::roleNames() const {
return {{Qt::UserRole, "modelData"}};
}
-void UntypedObjectModel::insertObject(QObject* object, qsizetype index) {
- auto iindex = index == -1 ? this->valuesList.length() : index;
- emit this->objectInsertedPre(object, iindex);
-
- auto intIndex = static_cast(iindex);
- this->beginInsertRows(QModelIndex(), intIndex, intIndex);
- this->valuesList.insert(iindex, object);
- this->endInsertRows();
-
- emit this->valuesChanged();
- emit this->objectInsertedPost(object, iindex);
-}
-
-void UntypedObjectModel::removeAt(qsizetype index) {
- auto* object = this->valuesList.at(index);
- emit this->objectRemovedPre(object, index);
-
- auto intIndex = static_cast(index);
- this->beginRemoveRows(QModelIndex(), intIndex, intIndex);
- this->valuesList.removeAt(index);
- this->endRemoveRows();
-
- emit this->valuesChanged();
- emit this->objectRemovedPost(object, index);
-}
-
-bool UntypedObjectModel::removeObject(const QObject* object) {
- auto index = this->valuesList.indexOf(object);
- if (index == -1) return false;
-
- this->removeAt(index);
- return true;
-}
-
-void UntypedObjectModel::diffUpdate(const QVector& newValues) {
- for (qsizetype i = 0; i < this->valuesList.length();) {
- if (newValues.contains(this->valuesList.at(i))) i++;
- else this->removeAt(i);
- }
-
- qsizetype oi = 0;
- for (auto* object: newValues) {
- if (this->valuesList.length() == oi || this->valuesList.at(oi) != object) {
- this->insertObject(object, oi);
- }
-
- oi++;
- }
-}
-
-qsizetype UntypedObjectModel::indexOf(QObject* object) { return this->valuesList.indexOf(object); }
-
UntypedObjectModel* UntypedObjectModel::emptyInstance() {
- static auto* instance = new UntypedObjectModel(nullptr); // NOLINT
+ static auto* instance = new ObjectModel(nullptr);
return instance;
}
diff --git a/src/core/model.hpp b/src/core/model.hpp
index 3c5822a..0e88025 100644
--- a/src/core/model.hpp
+++ b/src/core/model.hpp
@@ -2,7 +2,7 @@
#include
-#include
+#include
#include
#include
#include
@@ -49,14 +49,11 @@ class UntypedObjectModel: public QAbstractListModel {
public:
explicit UntypedObjectModel(QObject* parent): QAbstractListModel(parent) {}
- [[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override;
- [[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override;
[[nodiscard]] QHash roleNames() const override;
- [[nodiscard]] QList values() const { return this->valuesList; }
- void removeAt(qsizetype index);
+ [[nodiscard]] virtual QList values() = 0;
- Q_INVOKABLE qsizetype indexOf(QObject* object);
+ Q_INVOKABLE virtual qsizetype indexOf(QObject* object) const = 0;
static UntypedObjectModel* emptyInstance();
@@ -71,15 +68,6 @@ signals:
/// Sent immediately after an object is removed from the list.
void objectRemovedPost(QObject* object, qsizetype index);
-protected:
- void insertObject(QObject* object, qsizetype index = -1);
- bool removeObject(const QObject* object);
-
- // Assumes only one instance of a specific value
- void diffUpdate(const QVector& newValues);
-
- QVector valuesList;
-
private:
static qsizetype valuesCount(QQmlListProperty* property);
static QObject* valueAt(QQmlListProperty* property, qsizetype index);
@@ -90,14 +78,20 @@ class ObjectModel: public UntypedObjectModel {
public:
explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {}
- [[nodiscard]] QVector& valueList() { return *std::bit_cast*>(&this->valuesList); }
-
- [[nodiscard]] const QVector& valueList() const {
- return *std::bit_cast*>(&this->valuesList);
- }
+ [[nodiscard]] const QList& valueList() const { return this->mValuesList; }
+ [[nodiscard]] QList& valueList() { return this->mValuesList; }
void insertObject(T* object, qsizetype index = -1) {
- this->UntypedObjectModel::insertObject(object, index);
+ auto iindex = index == -1 ? this->mValuesList.length() : index;
+ emit this->objectInsertedPre(object, iindex);
+
+ auto intIndex = static_cast(iindex);
+ this->beginInsertRows(QModelIndex(), intIndex, intIndex);
+ this->mValuesList.insert(iindex, object);
+ this->endInsertRows();
+
+ emit this->valuesChanged();
+ emit this->objectInsertedPost(object, iindex);
}
void insertObjectSorted(T* object, const std::function& compare) {
@@ -110,17 +104,71 @@ public:
}
auto idx = iter - list.begin();
- this->UntypedObjectModel::insertObject(object, idx);
+ this->insertObject(object, idx);
}
- void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); }
+ bool removeObject(const T* object) {
+ auto index = this->mValuesList.indexOf(object);
+ if (index == -1) return false;
+
+ this->removeAt(index);
+ return true;
+ }
+
+ void removeAt(qsizetype index) {
+ auto* object = this->mValuesList.at(index);
+ emit this->objectRemovedPre(object, index);
+
+ auto intIndex = static_cast(index);
+ this->beginRemoveRows(QModelIndex(), intIndex, intIndex);
+ this->mValuesList.removeAt(index);
+ this->endRemoveRows();
+
+ emit this->valuesChanged();
+ emit this->objectRemovedPost(object, index);
+ }
// Assumes only one instance of a specific value
- void diffUpdate(const QVector& newValues) {
- this->UntypedObjectModel::diffUpdate(*std::bit_cast*>(&newValues));
+ void diffUpdate(const QList& newValues) {
+ for (qsizetype i = 0; i < this->mValuesList.length();) {
+ if (newValues.contains(this->mValuesList.at(i))) i++;
+ else this->removeAt(i);
+ }
+
+ qsizetype oi = 0;
+ for (auto* object: newValues) {
+ if (this->mValuesList.length() == oi || this->mValuesList.at(oi) != object) {
+ this->insertObject(object, oi);
+ }
+
+ oi++;
+ }
}
static ObjectModel* emptyInstance() {
return static_cast*>(UntypedObjectModel::emptyInstance());
}
+
+ [[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override {
+ if (parent != QModelIndex()) return 0;
+ return static_cast(this->mValuesList.length());
+ }
+
+ [[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override {
+ if (role != Qt::UserRole) return QVariant();
+ // Values must be QObject derived, but we can't assert that here without breaking forward decls,
+ // so no static_cast.
+ return QVariant::fromValue(reinterpret_cast(this->mValuesList.at(index.row())));
+ }
+
+ qsizetype indexOf(QObject* object) const override {
+ return this->mValuesList.indexOf(reinterpret_cast(object));
+ }
+
+ [[nodiscard]] QList values() override {
+ return *reinterpret_cast*>(&this->mValuesList);
+ }
+
+private:
+ QList mValuesList;
};
diff --git a/src/core/module.md b/src/core/module.md
index b9404ea..41f065d 100644
--- a/src/core/module.md
+++ b/src/core/module.md
@@ -21,7 +21,6 @@ headers = [
"model.hpp",
"elapsedtimer.hpp",
"desktopentry.hpp",
- "objectrepeater.hpp",
"qsmenu.hpp",
"retainable.hpp",
"popupanchor.hpp",
diff --git a/src/core/objectrepeater.cpp b/src/core/objectrepeater.cpp
deleted file mode 100644
index 7971952..0000000
--- a/src/core/objectrepeater.cpp
+++ /dev/null
@@ -1,190 +0,0 @@
-#include "objectrepeater.hpp"
-#include
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-QVariant ObjectRepeater::model() const { return this->mModel; }
-
-void ObjectRepeater::setModel(QVariant model) {
- if (model == this->mModel) return;
-
- if (this->itemModel != nullptr) {
- QObject::disconnect(this->itemModel, nullptr, this, nullptr);
- }
-
- this->mModel = std::move(model);
- emit this->modelChanged();
- this->reloadElements();
-}
-
-void ObjectRepeater::onModelDestroyed() {
- this->mModel.clear();
- this->itemModel = nullptr;
- emit this->modelChanged();
- this->reloadElements();
-}
-
-QQmlComponent* ObjectRepeater::delegate() const { return this->mDelegate; }
-
-void ObjectRepeater::setDelegate(QQmlComponent* delegate) {
- if (delegate == this->mDelegate) return;
-
- if (this->mDelegate != nullptr) {
- QObject::disconnect(this->mDelegate, nullptr, this, nullptr);
- }
-
- this->mDelegate = delegate;
-
- if (delegate != nullptr) {
- QObject::connect(
- this->mDelegate,
- &QObject::destroyed,
- this,
- &ObjectRepeater::onDelegateDestroyed
- );
- }
-
- emit this->delegateChanged();
- this->reloadElements();
-}
-
-void ObjectRepeater::onDelegateDestroyed() {
- this->mDelegate = nullptr;
- emit this->delegateChanged();
- this->reloadElements();
-}
-
-void ObjectRepeater::reloadElements() {
- for (auto i = this->valuesList.length() - 1; i >= 0; i--) {
- this->removeComponent(i);
- }
-
- if (this->mDelegate == nullptr || !this->mModel.isValid()) return;
-
- if (this->mModel.canConvert()) {
- auto* model = this->mModel.value();
- this->itemModel = model;
-
- this->insertModelElements(model, 0, model->rowCount() - 1); // -1 is fine
-
- // clang-format off
- QObject::connect(model, &QObject::destroyed, this, &ObjectRepeater::onModelDestroyed);
- QObject::connect(model, &QAbstractItemModel::rowsInserted, this, &ObjectRepeater::onModelRowsInserted);
- QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &ObjectRepeater::onModelRowsRemoved);
- QObject::connect(model, &QAbstractItemModel::rowsMoved, this, &ObjectRepeater::onModelRowsMoved);
- QObject::connect(model, &QAbstractItemModel::modelAboutToBeReset, this, &ObjectRepeater::onModelAboutToBeReset);
- // clang-format on
- } else if (this->mModel.canConvert()) {
- auto values = this->mModel.value();
- auto len = values.count();
-
- for (auto i = 0; i != len; i++) {
- this->insertComponent(i, {{"modelData", QVariant::fromValue(values.at(i))}});
- }
- } else if (this->mModel.canConvert>()) {
- auto values = this->mModel.value>();
-
- for (auto& value: values) {
- this->insertComponent(this->valuesList.length(), {{"modelData", value}});
- }
- } else {
- qCritical() << this
- << "Cannot create components as the model is not compatible:" << this->mModel;
- }
-}
-
-void ObjectRepeater::insertModelElements(QAbstractItemModel* model, int first, int last) {
- auto roles = model->roleNames();
- auto roleDataVec = QVector();
- for (auto id: roles.keys()) {
- roleDataVec.push_back(QModelRoleData(id));
- }
-
- auto values = QModelRoleDataSpan(roleDataVec);
- auto props = QVariantMap();
-
- for (auto i = first; i != last + 1; i++) {
- auto index = model->index(i, 0);
- model->multiData(index, values);
-
- for (auto [id, name]: roles.asKeyValueRange()) {
- props.insert(name, *values.dataForRole(id));
- }
-
- this->insertComponent(i, props);
-
- props.clear();
- }
-}
-
-void ObjectRepeater::onModelRowsInserted(const QModelIndex& parent, int first, int last) {
- if (parent != QModelIndex()) return;
-
- this->insertModelElements(this->itemModel, first, last);
-}
-
-void ObjectRepeater::onModelRowsRemoved(const QModelIndex& parent, int first, int last) {
- if (parent != QModelIndex()) return;
-
- for (auto i = last; i != first - 1; i--) {
- this->removeComponent(i);
- }
-}
-
-void ObjectRepeater::onModelRowsMoved(
- const QModelIndex& sourceParent,
- int sourceStart,
- int sourceEnd,
- const QModelIndex& destParent,
- int destStart
-) {
- auto hasSource = sourceParent != QModelIndex();
- auto hasDest = destParent != QModelIndex();
-
- if (!hasSource && !hasDest) return;
-
- if (hasSource) {
- this->onModelRowsRemoved(sourceParent, sourceStart, sourceEnd);
- }
-
- if (hasDest) {
- this->onModelRowsInserted(destParent, destStart, destStart + (sourceEnd - sourceStart));
- }
-}
-
-void ObjectRepeater::onModelAboutToBeReset() {
- auto last = static_cast(this->valuesList.length() - 1);
- this->onModelRowsRemoved(QModelIndex(), 0, last); // -1 is fine
-}
-
-void ObjectRepeater::insertComponent(qsizetype index, const QVariantMap& properties) {
- auto* context = QQmlEngine::contextForObject(this);
- auto* instance = this->mDelegate->createWithInitialProperties(properties, context);
-
- if (instance == nullptr) {
- qWarning().noquote() << this->mDelegate->errorString();
- qWarning() << this << "failed to create object for model data" << properties;
- } else {
- QQmlEngine::setObjectOwnership(instance, QQmlEngine::CppOwnership);
- instance->setParent(this);
- }
-
- this->insertObject(instance, index);
-}
-
-void ObjectRepeater::removeComponent(qsizetype index) {
- auto* instance = this->valuesList.at(index);
- this->removeAt(index);
- delete instance;
-}
diff --git a/src/core/objectrepeater.hpp b/src/core/objectrepeater.hpp
deleted file mode 100644
index 409b12d..0000000
--- a/src/core/objectrepeater.hpp
+++ /dev/null
@@ -1,85 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "model.hpp"
-
-///! A Repeater / for loop / map for non Item derived objects.
-/// > [!ERROR] Removed in favor of @@QtQml.Models.Instantiator
-///
-/// The ObjectRepeater creates instances of the provided delegate for every entry in the
-/// given model, similarly to a @@QtQuick.Repeater but for non visual types.
-class ObjectRepeater: public ObjectModel {
- Q_OBJECT;
- /// The model providing data to the ObjectRepeater.
- ///
- /// Currently accepted model types are `list` lists, javascript arrays,
- /// and [QAbstractListModel] derived models, though only one column will be repeated
- /// from the latter.
- ///
- /// Note: @@ObjectModel is a [QAbstractListModel] with a single column.
- ///
- /// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html
- Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged);
- /// The delegate component to repeat.
- ///
- /// The delegate is given the same properties as in a Repeater, except `index` which
- /// is not currently implemented.
- ///
- /// If the model is a `list` or javascript array, a `modelData` property will be
- /// exposed containing the entry from the model. If the model is a [QAbstractListModel],
- /// the roles from the model will be exposed.
- ///
- /// Note: @@ObjectModel has a single role named `modelData` for compatibility with normal lists.
- ///
- /// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html
- Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged);
- Q_CLASSINFO("DefaultProperty", "delegate");
- QML_ELEMENT;
- QML_UNCREATABLE("ObjectRepeater has been removed in favor of QtQml.Models.Instantiator.");
-
-public:
- explicit ObjectRepeater(QObject* parent = nullptr): ObjectModel(parent) {}
-
- [[nodiscard]] QVariant model() const;
- void setModel(QVariant model);
-
- [[nodiscard]] QQmlComponent* delegate() const;
- void setDelegate(QQmlComponent* delegate);
-
-signals:
- void modelChanged();
- void delegateChanged();
-
-private slots:
- void onDelegateDestroyed();
- void onModelDestroyed();
- void onModelRowsInserted(const QModelIndex& parent, int first, int last);
- void onModelRowsRemoved(const QModelIndex& parent, int first, int last);
-
- void onModelRowsMoved(
- const QModelIndex& sourceParent,
- int sourceStart,
- int sourceEnd,
- const QModelIndex& destParent,
- int destStart
- );
-
- void onModelAboutToBeReset();
-
-private:
- void reloadElements();
- void insertModelElements(QAbstractItemModel* model, int first, int last);
- void insertComponent(qsizetype index, const QVariantMap& properties);
- void removeComponent(qsizetype index);
-
- QVariant mModel;
- QAbstractItemModel* itemModel = nullptr;
- QQmlComponent* mDelegate = nullptr;
-};
diff --git a/src/core/paths.cpp b/src/core/paths.cpp
index e17c3bc..d361e3d 100644
--- a/src/core/paths.cpp
+++ b/src/core/paths.cpp
@@ -27,12 +27,19 @@ QsPaths* QsPaths::instance() {
return instance;
}
-void QsPaths::init(QString shellId, QString pathId, QString dataOverride, QString stateOverride) {
+void QsPaths::init(
+ QString shellId,
+ QString pathId,
+ QString dataOverride,
+ QString stateOverride,
+ QString cacheOverride
+) {
auto* instance = QsPaths::instance();
instance->shellId = std::move(shellId);
instance->pathId = std::move(pathId);
instance->shellDataOverride = std::move(dataOverride);
instance->shellStateOverride = std::move(stateOverride);
+ instance->shellCacheOverride = std::move(cacheOverride);
}
QDir QsPaths::crashDir(const QString& id) {
@@ -57,7 +64,7 @@ QDir* QsPaths::baseRunDir() {
if (this->baseRunState == DirState::Unknown) {
auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR");
if (runtimeDir.isEmpty()) {
- runtimeDir = QString("/run/user/$1").arg(getuid());
+ runtimeDir = QString("/run/user/%1").arg(getuid());
qCInfo(logPaths) << "XDG_RUNTIME_DIR was not set, defaulting to" << runtimeDir;
}
@@ -168,7 +175,8 @@ void QsPaths::linkRunDir() {
auto* shellDir = this->shellRunDir();
if (!shellDir) {
- qCCritical(logPaths
+ qCCritical(
+ logPaths
) << "Could not create by-id symlink as the shell runtime path could not be created.";
} else {
auto shellPath = shellDir->filePath(runDir->dirName());
@@ -316,9 +324,16 @@ QDir QsPaths::shellStateDir() {
QDir QsPaths::shellCacheDir() {
if (this->shellCacheState == DirState::Unknown) {
- auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
- dir = QDir(dir.filePath("by-shell"));
- dir = QDir(dir.filePath(this->shellId));
+ QDir dir;
+ if (this->shellCacheOverride.isEmpty()) {
+ dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
+ dir = QDir(dir.filePath("by-shell"));
+ dir = QDir(dir.filePath(this->shellId));
+ } else {
+ auto basedir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
+ dir = QDir(this->shellCacheOverride.replace("$BASE", basedir));
+ }
+
this->mShellCacheDir = dir;
qCDebug(logPaths) << "Initialized cache path:" << dir.path();
@@ -346,7 +361,7 @@ void QsPaths::createLock() {
return;
}
- auto lock = flock {
+ struct flock lock = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
@@ -364,7 +379,8 @@ void QsPaths::createLock() {
qCDebug(logPaths) << "Created instance lock at" << path;
}
} else {
- qCCritical(logPaths
+ qCCritical(
+ logPaths
) << "Could not create instance lock, as the instance runtime directory could not be created.";
}
}
@@ -373,7 +389,7 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD
auto file = QFile(QDir(path).filePath("instance.lock"));
if (!file.open(QFile::ReadOnly)) return false;
- auto lock = flock {
+ struct flock lock = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
@@ -397,7 +413,7 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD
}
QPair, QVector>
-QsPaths::collectInstances(const QString& path) {
+QsPaths::collectInstances(const QString& path, const QString& display) {
qCDebug(logPaths) << "Collecting instances from" << path;
auto liveInstances = QVector();
auto deadInstances = QVector();
@@ -411,6 +427,11 @@ QsPaths::collectInstances(const QString& path) {
qCDebug(logPaths).nospace() << "Found instance " << info.instance.instanceId << " (pid "
<< info.pid << ") at " << path;
+ if (!display.isEmpty() && info.instance.display != display) {
+ qCDebug(logPaths) << "Skipped instance with mismatched display at" << path;
+ continue;
+ }
+
if (info.pid == -1) {
deadInstances.push_back(info);
} else {
diff --git a/src/core/paths.hpp b/src/core/paths.hpp
index 178bcda..c2500ed 100644
--- a/src/core/paths.hpp
+++ b/src/core/paths.hpp
@@ -17,14 +17,20 @@ QDataStream& operator>>(QDataStream& stream, InstanceLockInfo& info);
class QsPaths {
public:
static QsPaths* instance();
- static void init(QString shellId, QString pathId, QString dataOverride, QString stateOverride);
+ static void init(
+ QString shellId,
+ QString pathId,
+ QString dataOverride,
+ QString stateOverride,
+ QString cacheOverride
+ );
static QDir crashDir(const QString& id);
static QString basePath(const QString& id);
static QString ipcPath(const QString& id);
static bool
checkLock(const QString& path, InstanceLockInfo* info = nullptr, bool allowDead = false);
static QPair, QVector>
- collectInstances(const QString& path);
+ collectInstances(const QString& path, const QString& display);
QDir* baseRunDir();
QDir* shellRunDir();
@@ -65,4 +71,5 @@ private:
QString shellDataOverride;
QString shellStateOverride;
+ QString shellCacheOverride;
};
diff --git a/src/core/platformmenu.cpp b/src/core/platformmenu.cpp
index 427dde0..d8901e2 100644
--- a/src/core/platformmenu.cpp
+++ b/src/core/platformmenu.cpp
@@ -18,7 +18,6 @@
#include
#include "../window/proxywindow.hpp"
-#include "../window/windowinterface.hpp"
#include "iconprovider.hpp"
#include "model.hpp"
#include "platformmenu_p.hpp"
@@ -91,10 +90,8 @@ bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relati
} else if (parentWindow == nullptr) {
qCritical() << "Cannot display PlatformMenuEntry with null parent window.";
return false;
- } else if (auto* proxy = qobject_cast(parentWindow)) {
+ } else if (auto* proxy = ProxyWindowBase::forObject(parentWindow)) {
window = proxy->backingWindow();
- } else if (auto* interface = qobject_cast(parentWindow)) {
- window = interface->proxyWindow()->backingWindow();
} else {
qCritical() << "PlatformMenuEntry.display() must be called with a window.";
return false;
diff --git a/src/core/plugin.cpp b/src/core/plugin.cpp
index 0eb9a06..e6cd1bb 100644
--- a/src/core/plugin.cpp
+++ b/src/core/plugin.cpp
@@ -9,6 +9,18 @@ static QVector plugins; // NOLINT
void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); }
+void QsEnginePlugin::preinitPluginsOnly() {
+ plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); });
+
+ std::ranges::sort(plugins, [](QsEnginePlugin* a, QsEnginePlugin* b) {
+ return b->dependencies().contains(a->name());
+ });
+
+ for (QsEnginePlugin* plugin: plugins) {
+ plugin->preinit();
+ }
+}
+
void QsEnginePlugin::initPlugins() {
plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); });
@@ -16,6 +28,10 @@ void QsEnginePlugin::initPlugins() {
return b->dependencies().contains(a->name());
});
+ for (QsEnginePlugin* plugin: plugins) {
+ plugin->preinit();
+ }
+
for (QsEnginePlugin* plugin: plugins) {
plugin->init();
}
diff --git a/src/core/plugin.hpp b/src/core/plugin.hpp
index f0c14dc..f692e91 100644
--- a/src/core/plugin.hpp
+++ b/src/core/plugin.hpp
@@ -18,12 +18,14 @@ public:
virtual QString name() { return QString(); }
virtual QList dependencies() { return {}; }
virtual bool applies() { return true; }
+ virtual void preinit() {}
virtual void init() {}
virtual void registerTypes() {}
virtual void constructGeneration(EngineGeneration& /*unused*/) {} // NOLINT
virtual void onReload() {}
static void registerPlugin(QsEnginePlugin& plugin);
+ static void preinitPluginsOnly();
static void initPlugins();
static void runConstructGeneration(EngineGeneration& generation);
static void runOnReload();
diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp
index bbcc3a5..ca817c9 100644
--- a/src/core/popupanchor.cpp
+++ b/src/core/popupanchor.cpp
@@ -11,7 +11,6 @@
#include
#include "../window/proxywindow.hpp"
-#include "../window/windowinterface.hpp"
#include "types.hpp"
bool PopupAnchorState::operator==(const PopupAnchorState& other) const {
@@ -28,7 +27,7 @@ void PopupAnchor::markClean() { this->lastState = this->state; }
void PopupAnchor::markDirty() { this->lastState.reset(); }
QWindow* PopupAnchor::backingWindow() const {
- return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr;
+ return this->bProxyWindow ? this->bProxyWindow->backingWindow() : nullptr;
}
void PopupAnchor::setWindowInternal(QObject* window) {
@@ -36,14 +35,12 @@ void PopupAnchor::setWindowInternal(QObject* window) {
if (this->mWindow) {
QObject::disconnect(this->mWindow, nullptr, this, nullptr);
- QObject::disconnect(this->mProxyWindow, nullptr, this, nullptr);
+ QObject::disconnect(this->bProxyWindow, nullptr, this, nullptr);
}
if (window) {
- if (auto* proxy = qobject_cast(window)) {
- this->mProxyWindow = proxy;
- } else if (auto* interface = qobject_cast(window)) {
- this->mProxyWindow = interface->proxyWindow();
+ if (auto* proxy = ProxyWindowBase::forObject(window)) {
+ this->bProxyWindow = proxy;
} else {
qWarning() << "Tried to set popup anchor window to" << window
<< "which is not a quickshell window.";
@@ -55,7 +52,7 @@ void PopupAnchor::setWindowInternal(QObject* window) {
QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed);
QObject::connect(
- this->mProxyWindow,
+ this->bProxyWindow,
&ProxyWindowBase::backerVisibilityChanged,
this,
&PopupAnchor::backingWindowVisibilityChanged
@@ -70,7 +67,7 @@ void PopupAnchor::setWindowInternal(QObject* window) {
setnull:
if (this->mWindow) {
this->mWindow = nullptr;
- this->mProxyWindow = nullptr;
+ this->bProxyWindow = nullptr;
emit this->windowChanged();
emit this->backingWindowVisibilityChanged();
@@ -100,7 +97,7 @@ void PopupAnchor::setItem(QQuickItem* item) {
void PopupAnchor::onWindowDestroyed() {
this->mWindow = nullptr;
- this->mProxyWindow = nullptr;
+ this->bProxyWindow = nullptr;
emit this->windowChanged();
emit this->backingWindowVisibilityChanged();
}
@@ -186,11 +183,11 @@ void PopupAnchor::updatePlacement(const QPoint& anchorpoint, const QSize& size)
}
void PopupAnchor::updateAnchor() {
- if (this->mItem && this->mProxyWindow) {
+ if (this->mItem && this->bProxyWindow) {
auto baseRect =
this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect();
- auto rect = this->mProxyWindow->contentItem()->mapFromItem(
+ auto rect = this->bProxyWindow->contentItem()->mapFromItem(
this->mItem,
baseRect.marginsRemoved(this->mMargins.qmargins())
);
diff --git a/src/core/popupanchor.hpp b/src/core/popupanchor.hpp
index a9b121e..9f08512 100644
--- a/src/core/popupanchor.hpp
+++ b/src/core/popupanchor.hpp
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
#include
#include
#include