Compare commits

..

No commits in common. "master" and "v0.1.0" have entirely different histories.

385 changed files with 3591 additions and 20711 deletions

View file

@ -1,6 +1,6 @@
AlignArrayOfStructures: None AlignArrayOfStructures: None
AlignAfterOpenBracket: BlockIndent AlignAfterOpenBracket: BlockIndent
AllowShortBlocksOnASingleLine: Empty AllowShortBlocksOnASingleLine: Always
AllowShortCaseLabelsOnASingleLine: true AllowShortCaseLabelsOnASingleLine: true
AllowShortEnumsOnASingleLine: true AllowShortEnumsOnASingleLine: true
AllowShortFunctionsOnASingleLine: All AllowShortFunctionsOnASingleLine: All

View file

@ -20,8 +20,6 @@ Checks: >
-cppcoreguidelines-avoid-do-while, -cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-pro-type-reinterpret-cast, -cppcoreguidelines-pro-type-reinterpret-cast,
-cppcoreguidelines-pro-type-vararg, -cppcoreguidelines-pro-type-vararg,
-cppcoreguidelines-pro-type-union-access,
-cppcoreguidelines-use-enum-class,
google-global-names-in-headers, google-global-names-in-headers,
google-readability-casting, google-readability-casting,
google-runtime-int, google-runtime-int,
@ -65,8 +63,6 @@ CheckOptions:
readability-identifier-naming.ParameterCase: camelBack readability-identifier-naming.ParameterCase: camelBack
readability-identifier-naming.VariableCase: camelBack readability-identifier-naming.VariableCase: camelBack
misc-const-correctness.WarnPointersAsPointers: false
# does not appear to work # does not appear to work
readability-operators-representation.BinaryOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^=' readability-operators-representation.BinaryOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^='
readability-operators-representation.OverloadedOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^=' readability-operators-representation.OverloadedOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^='

View file

@ -1,17 +1,82 @@
name: Crash Report (v1) name: Crash Report
description: Quickshell has crashed (old) description: Quickshell has crashed
labels: ["unactionable"] labels: ["bug", "crash"]
body: body:
- type: markdown - type: textarea
id: crashinfo
attributes: attributes:
value: | label: General crash information
Thank you for taking the time to click the report button. description: |
At this point most of the worst issues in 0.2.1 and before have been fixed and we are Paste the contents of the `info.txt` file in your crash folder here.
preparing for a new release. Please do not submit this report. value: "<details> <summary>General information</summary>
- type: checkboxes
id: donotcheck
attributes: ```
label: Read the text above. Do not submit the report.
options: <Paste the contents of the file here inside of the triple backticks>
- label: Yes I want this report to be deleted.
```
</details>"
validations:
required: true required: true
- type: textarea
id: userinfo
attributes:
label: What caused the crash
description: |
Any information likely to help debug the crash. What were you doing when the crash occurred,
what changes did you make, can you get it to happen again?
- type: textarea
id: dump
attributes:
label: Minidump
description: |
Attach `minidump.dmp.log` here. If it is too big to upload, compress it.
You may skip this step if quickshell crashed while processing a password
or other sensitive information. If you skipped it write why instead.
validations:
required: true
- type: textarea
id: logs
attributes:
label: Log file
description: |
Attach `log.qslog.log` here. If it is too big to upload, compress it.
You can preview the log if you'd like using `quickshell read-log <path-to-log>`.
validations:
required: true
- type: textarea
id: config
attributes:
label: Configuration
description: |
Attach your configuration here, preferrably in full (not just one file).
Compress it into a zip, tar, etc.
This will help us reproduce the crash ourselves.
- type: textarea
id: bt
attributes:
label: Backtrace
description: |
If you have gdb installed and use systemd, or otherwise know how to get a backtrace,
we would appreciate one. (You may have gdb installed without knowing it)
1. Run `coredumpctl debug <pid>` where `pid` is the number shown after "Crashed process ID"
in the crash reporter.
2. Once it loads, type `bt -full` (then enter)
3. Copy the output and attach it as a file or in a spoiler.
- type: textarea
id: exe
attributes:
label: Executable
description: |
If the crash folder contains a executable.txt file, upload it here. If not you can ignore this field.
If it is too big to upload, compress it.
Note: executable.txt is the quickshell binary. It has a .txt extension due to github's limitations on
filetypes.

View file

@ -1,49 +0,0 @@
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 <path-to-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 <pid>` where `pid` is the number shown after "Crashed process ID"
in the crash reporter.
2. Once it loads, type `bt -full` (then enter)
3. Copy the output and attach it as a file or in a spoiler.

View file

@ -6,23 +6,17 @@ jobs:
name: Nix name: Nix
strategy: strategy:
matrix: matrix:
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] qtver: [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] compiler: [clang, gcc]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# Use cachix action over detsys for testing with act. # Use cachix action over detsys for testing with act.
# - uses: cachix/install-nix-action@v27 # - uses: cachix/install-nix-action@v27
- uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
with:
use-flakehub: false
- name: Download Dependencies - name: Download Dependencies
run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).unwrapped.inputDerivation' run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).inputDerivation'
- name: Build - name: Build
run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }' run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }'
@ -50,16 +44,13 @@ jobs:
wayland-protocols \ wayland-protocols \
wayland \ wayland \
libdrm \ libdrm \
vulkan-headers \
libxcb \ libxcb \
libpipewire \ libpipewire \
cli11 \ cli11 \
polkit \ jemalloc
jemalloc \
libunwind \
git # for cpptrace clone
- name: Build - name: Build
# breakpad is annoying to build in ci due to makepkg not running as root
run: | run: |
cmake -GNinja -B build -DVENDOR_CPPTRACE=ON cmake -GNinja -B build -DCRASH_REPORTER=OFF
cmake --build build cmake --build build

View file

@ -5,17 +5,11 @@ jobs:
lint: lint:
name: Lint name: Lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# Use cachix action over detsys for testing with act. # Use cachix action over detsys for testing with act.
# - uses: cachix/install-nix-action@v27 # - uses: cachix/install-nix-action@v27
- uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
with:
use-flakehub: false
- uses: nicknovitski/nix-develop@v1 - uses: nicknovitski/nix-develop@v1
- name: Check formatting - name: Check formatting

View file

@ -15,7 +15,15 @@ Please make this descriptive enough to identify your specific package, for examp
- `Nixpkgs` - `Nixpkgs`
- `Fedora COPR (errornointernet/quickshell)` - `Fedora COPR (errornointernet/quickshell)`
If you are forking quickshell, please change `CRASHREPORT_URL` to your own issue tracker. `-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).
### QML Module dir ### QML Module dir
Currently all QML modules are statically linked to quickshell, but this is where Currently all QML modules are statically linked to quickshell, but this is where
@ -33,22 +41,16 @@ Quickshell has a set of base dependencies you will always need, names vary by di
- `cmake` - `cmake`
- `qt6base` - `qt6base`
- `qt6declarative` - `qt6declarative`
- `libdrm` - `qtshadertools` (build-time only)
- `qtshadertools` (build-time) - `spirv-tools` (build-time only)
- `spirv-tools` (build-time) - `pkg-config` (build-time only)
- `pkg-config` (build-time) - `cli11`
- `cli11` (static library)
Build time dependencies and static libraries don't have to exist at runtime,
however build time dependencies must be compiled for the architecture of
the builder, while static libraries must be compiled for the architecture
of the target.
On some distros, private Qt headers are in separate packages which you may have to install. On some distros, private Qt headers are in separate packages which you may have to install.
We currently require private headers for the following libraries: We currently require private headers for the following libraries:
- `qt6declarative` - `qt6declarative`
- `qt6wayland` (for Qt versions prior to 6.10) - `qt6wayland`
We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and
svg icons will not work, including system ones. svg icons will not work, including system ones.
@ -57,24 +59,14 @@ At least Qt 6.6 is required.
All features are enabled by default and some have their own dependencies. All features are enabled by default and some have their own dependencies.
### Crash Handler ### Crash Reporter
The crash reporter catches crashes, restarts Quickshell when it crashes, The crash reporter catches crashes, restarts quickshell when it crashes,
and collects useful crash information in one place. Leaving this enabled will and collects useful crash information in one place. Leaving this enabled will
enable us to fix bugs far more easily. enable us to fix bugs far more easily.
To disable: `-DCRASH_HANDLER=OFF` To disable: `-DCRASH_REPORTER=OFF`
Dependencies: `cpptrace` Dependencies: `google-breakpad`
Note: `-DVENDOR_CPPTRACE=ON` can be set to vendor cpptrace using FetchContent.
When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the
package manager or fetched with FetchContent.
*Please ensure binaries have usable symbols.* We do not necessarily need full debuginfo, but
leaving symbols in the binary is extremely helpful. You can check if symbols are useful
by sending a SIGSEGV to the process and ensuring symbols for the quickshell binary are present
in the trace.
### Jemalloc ### Jemalloc
We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused
@ -107,13 +99,10 @@ Currently supported Qt versions: `6.6`, `6.7`.
To disable: `-DWAYLAND=OFF` To disable: `-DWAYLAND=OFF`
Dependencies: Dependencies:
- `qt6wayland` (for Qt versions prior to 6.10) - `qt6wayland`
- `wayland` (libwayland-client) - `wayland` (libwayland-client)
- `wayland-scanner` (build time) - `wayland-scanner` (may be part of your distro's wayland package)
- `wayland-protocols` (static library) - `wayland-protocols`
Note that one or both of `wayland-scanner` and `wayland-protocols` may be bundled
with you distro's wayland package.
#### Wlroots Layershell #### Wlroots Layershell
Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol, Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol,
@ -147,8 +136,8 @@ Enables streaming video from monitors and toplevel windows through various proto
To disable: `-DSCREENCOPY=OFF` To disable: `-DSCREENCOPY=OFF`
Dependencies: Dependencies:
- `libdrm`
- `libgbm` - `libgbm`
- `vulkan-headers` (build-time)
Specific protocols can also be disabled: Specific protocols can also be disabled:
- `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1] - `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1]
@ -195,13 +184,6 @@ To disable: `-DSERVICE_PAM=OFF`
Dependencies: `pam` Dependencies: `pam`
### Polkit
This feature enables creating Polkit agents that can prompt user for authentication.
To disable: `-DSERVICE_POLKIT=OFF`
Dependencies: `polkit`, `glib`
### Hyprland ### Hyprland
This feature enables hyprland specific integrations. It requires wayland support This feature enables hyprland specific integrations. It requires wayland support
but has no extra dependencies. but has no extra dependencies.
@ -238,11 +220,11 @@ To disable: `-DI3_IPC=OFF`
## Building ## Building
*For developers and prospective contributors: See [CONTRIBUTING.md](CONTRIBUTING.md).* *For developers and prospective contributors: See [CONTRIBUTING.md](CONTRIBUTING.md).*
Only `ninja` builds are tested, but makefiles may work. We highly recommend using `ninja` to run the build, but you can use makefiles if you must.
#### Configuring the build #### Configuring the build
```sh ```sh
$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release [additional disable flags from above here] $ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here]
``` ```
Note that features you do not supply dependencies for MUST be disabled with their associated flags Note that features you do not supply dependencies for MUST be disabled with their associated flags

View file

@ -1,11 +1,5 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
project(quickshell VERSION "0.2.1" LANGUAGES CXX C) project(quickshell VERSION "0.1.0" LANGUAGES CXX C)
set(UNRELEASED_FEATURES
"network.2"
"colorquant-imagerect"
"window-parent"
)
set(QT_MIN_VERSION "6.6.0") set(QT_MIN_VERSION "6.6.0")
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
@ -13,9 +7,6 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(QS_BUILD_OPTIONS "") set(QS_BUILD_OPTIONS "")
# should be changed for forks
set(CRASHREPORT_URL "https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml" CACHE STRING "Bugreport URL")
function(boption VAR NAME DEFAULT) function(boption VAR NAME DEFAULT)
cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "") cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "")
@ -47,17 +38,14 @@ string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}")
message(STATUS "Quickshell configuration") message(STATUS "Quickshell configuration")
message(STATUS " Distributor: ${DISTRIBUTOR}") message(STATUS " Distributor: ${DISTRIBUTOR}")
boption(DISTRIBUTOR_DEBUGINFO_AVAILABLE "Distributor provided debuginfo" NO)
boption(NO_PCH "Disable precompild headers (dev)" OFF) boption(NO_PCH "Disable precompild headers (dev)" OFF)
boption(BUILD_TESTING "Build tests (dev)" OFF) boption(BUILD_TESTING "Build tests (dev)" OFF)
boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang
boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN}) boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN})
if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") boption(CRASH_REPORTER "Crash Handling" ON)
boption(USE_JEMALLOC "Use jemalloc" OFF)
else()
boption(USE_JEMALLOC "Use jemalloc" ON) boption(USE_JEMALLOC "Use jemalloc" ON)
endif()
boption(CRASH_HANDLER "Crash Handling" ON)
boption(SOCKETS "Unix Sockets" ON) boption(SOCKETS "Unix Sockets" ON)
boption(WAYLAND "Wayland" ON) boption(WAYLAND "Wayland" ON)
boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND) boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND)
@ -79,21 +67,17 @@ boption(SERVICE_STATUS_NOTIFIER "System Tray" ON)
boption(SERVICE_PIPEWIRE "PipeWire" ON) boption(SERVICE_PIPEWIRE "PipeWire" ON)
boption(SERVICE_MPRIS "Mpris" ON) boption(SERVICE_MPRIS "Mpris" ON)
boption(SERVICE_PAM "Pam" ON) boption(SERVICE_PAM "Pam" ON)
boption(SERVICE_POLKIT "Polkit" ON)
boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_GREETD "Greetd" ON)
boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_UPOWER "UPower" ON)
boption(SERVICE_NOTIFICATIONS "Notifications" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON)
boption(BLUETOOTH "Bluetooth" ON)
boption(NETWORK "Network" ON)
include(cmake/install-qml-module.cmake) include(cmake/install-qml-module.cmake)
include(cmake/util.cmake) include(cmake/util.cmake)
add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension) add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension)
# pipewire defines these, breaking PCH # pipewire defines this, breaking PCH
add_compile_definitions(_REENTRANT) add_compile_definitions(_REENTRANT)
add_compile_options(-fno-strict-overflow)
if (FRAME_POINTERS) if (FRAME_POINTERS)
add_compile_options(-fno-omit-frame-pointer) add_compile_options(-fno-omit-frame-pointer)
@ -115,7 +99,6 @@ if (NOT CMAKE_BUILD_TYPE)
endif() endif()
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools) set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools)
set(QT_PRIVDEPS QuickPrivate)
include(cmake/pch.cmake) include(cmake/pch.cmake)
@ -131,10 +114,9 @@ endif()
if (WAYLAND) if (WAYLAND)
list(APPEND QT_FPDEPS WaylandClient) list(APPEND QT_FPDEPS WaylandClient)
list(APPEND QT_PRIVDEPS WaylandClientPrivate)
endif() endif()
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH OR NETWORK) if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS)
set(DBUS ON) set(DBUS ON)
endif() endif()
@ -144,13 +126,6 @@ endif()
find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS}) find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS})
# In Qt 6.10, private dependencies are required to be explicit,
# but they could not be explicitly depended on prior to 6.9.
if (Qt6_VERSION VERSION_GREATER_EQUAL "6.9.0")
set(QT_NO_PRIVATE_MODULE_WARNING ON)
find_package(Qt6 REQUIRED COMPONENTS ${QT_PRIVDEPS})
endif()
set(CMAKE_AUTOUIC OFF) set(CMAKE_AUTOUIC OFF)
qt_standard_project_setup(REQUIRES 6.6) qt_standard_project_setup(REQUIRES 6.6)
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules) set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules)
@ -170,14 +145,3 @@ install(CODE "
${CMAKE_INSTALL_FULL_BINDIR}/quickshell \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/qs ${CMAKE_INSTALL_FULL_BINDIR}/quickshell \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/qs
) )
") ")
install(
FILES ${CMAKE_SOURCE_DIR}/assets/org.quickshell.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications
)
install(
FILES ${CMAKE_SOURCE_DIR}/assets/quickshell.svg
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps
RENAME org.quickshell.svg
)

View file

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

View file

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

View file

@ -12,9 +12,6 @@ lint-ci:
lint-changed: lint-changed:
git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
lint-staged:
git diff --staged --name-only --diff-filter=d HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
configure target='debug' *FLAGS='': configure target='debug' *FLAGS='':
cmake -GNinja -B {{builddir}} \ cmake -GNinja -B {{builddir}} \
-DCMAKE_BUILD_TYPE={{ if target == "debug" { "Debug" } else { "RelWithDebInfo" } }} \ -DCMAKE_BUILD_TYPE={{ if target == "debug" { "Debug" } else { "RelWithDebInfo" } }} \

View file

@ -2,14 +2,8 @@
See the [website](https://quickshell.outfoxxed.me) for more information See the [website](https://quickshell.outfoxxed.me) for more information
and installation instructions. and installation instructions.
This repo is hosted at:
- https://git.outfoxxed.me/quickshell/quickshell
- https://github.com/quickshell-mirror/quickshell
# Contributing / Development # Contributing / Development
- [HACKING.md](HACKING.md) - Development instructions and policy. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution policy.
- [BUILD.md](BUILD.md) - Packaging and build instructions.
#### License #### License

View file

@ -1,7 +0,0 @@
[Desktop Entry]
Version=1.5
Type=Application
NoDisplay=true
Name=Quickshell
Icon=org.quickshell

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="724.635" height="724.635"><path fill="#359757" stroke="#359757" stroke-linecap="square" stroke-linejoin="round" stroke-width="74.755" d="m37.378 160.237 122.859-122.86h527.02v527.02l-122.86 122.86H37.378Z"/><path fill="#fff" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="12.201" d="M323.051 96.412a268.74 268.74 0 0 0-3.51.542c-4.052 14.481-7.815 29.941-14.904 42.692a230.02 230.02 0 0 0-59.406 24.679c-14.036-3.974-27.647-12.214-40.766-19.562a268.788 268.788 0 0 0-60.035 60.16c7.376 13.105 15.645 26.698 19.648 40.726a230.02 230.02 0 0 0-24.554 59.458c-12.735 7.115-28.186 10.913-42.66 14.994a268.789 268.789 0 0 0 .09 84.992c14.48 4.05 29.941 7.814 42.691 14.903a230.02 230.02 0 0 0 24.68 59.406c-3.974 14.037-12.215 27.647-19.563 40.766a268.788 268.788 0 0 0 60.161 60.035c13.104-7.376 26.696-15.645 40.725-19.648a230.02 230.02 0 0 0 59.457 24.555c7.116 12.735 10.913 28.186 14.995 42.659a268.788 268.788 0 0 0 84.99-.09c4.052-14.482 7.817-29.941 14.906-42.691a230.02 230.02 0 0 0 59.405-24.68c14.037 3.974 33.069 17.638 46.188 24.986a268.788 268.788 0 0 0 60.035-60.161c-7.376-13.105-21.068-32.12-25.071-46.149a230.02 230.02 0 0 0 24.555-59.457c12.735-7.116 28.186-10.912 42.659-14.993a268.788 268.788 0 0 0-.089-84.993c-14.482-4.051-29.942-7.814-42.692-14.904a230.02 230.02 0 0 0-24.68-59.405c3.974-14.037 12.216-27.647 19.565-40.767a268.788 268.788 0 0 0-60.161-60.035c-13.105 7.376-26.698 15.645-40.726 19.649a230.02 230.02 0 0 0-59.458-24.555c-7.115-12.735-10.913-28.187-14.994-42.66a268.788 268.788 0 0 0-81.481-.452zm15.778 106.85c58.282-8.328 116.455 15.865 151.643 63.065 35.19 47.2 41.766 109.86 17.144 163.337l-41.728-22.688s-38.558 31.44-57.344 63.012l23.893 36.326a160.78 160.78 0 0 1-46.633 15.058c-87.854 12.99-169.6-47.708-182.573-135.564-12.974-87.855 47.74-169.59 135.598-182.546Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,84 +0,0 @@
## 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.

View file

@ -1 +0,0 @@
Initial release

View file

@ -1,84 +0,0 @@
## Breaking Changes
- Files outside of the shell directory can no longer be referenced with relative paths, e.g. '../../foo.png'.
- PanelWindow's Automatic exclusion mode now adds an exclusion zone for panels with a single anchor.
- `QT_QUICK_CONTROLS_STYLE` and `QT_STYLE_OVERRIDE` are ignored unless `//@ pragma RespectSystemStyle` is set.
## New Features
### Root-Relative Imports
Quickshell 0.2 comes with a new method to import QML modules which is supported by QMLLS.
This replaces "root:/" imports for QML modules.
The new syntax is `import qs.path.to.module`, where `path/to/module` is the path to
a module/subdirectory relative to the config root (`qs`).
### Better LSP support
LSP support for Singletons and Root-Relative imports can be enabled by creating a file named
`.qmlls.ini` in the shell root directory. Quickshell will detect this file and automatically
populate it with an LSP configuration. This file should be gitignored in your configuration,
as it is system dependent.
The generated configuration also includes QML import paths available to Quickshell, meaning
QMLLS no longer requires the `-E` flag.
### Bluetooth Module
Quickshell can now manage your bluetooth devices through BlueZ. While authenticated pairing
has not landed in 0.2, support for connecting and disconnecting devices, basic device information,
and non-authenticated pairing are now supported.
### Other Features
- Added `HyprlandToplevel` and related toplevel/window management APIs in the Hyprland module.
- Added `Quickshell.execDetached()`, which spawns a detached process without a `Process` object.
- Added `Process.exec()` for easier reconfiguration of process commands when starting them.
- Added `FloatingWindow.title`, which allows changing the title of a floating window.
- Added `signal QsWindow.closed()`, fired when a window is closed externally.
- Added support for inline replies in notifications, when supported by applications.
- Added `DesktopEntry.startupWmClass` and `DesktopEntry.heuristicLookup()` to better identify toplevels.
- Added `DesktopEntry.command` which can be run as an alternative to `DesktopEntry.execute()`.
- Added `//@ pragma Internal`, which makes a QML component impossible to import outside of its module.
- Added dead instance selection for some subcommands, such as `qs log` and `qs list`.
## Other Changes
- `Quickshell.shellRoot` has been renamed to `Quickshell.shellDir`.
- PanelWindow margins opposite the window's anchorpoint are now added to exclusion zone.
- stdout/stderr or detached processes and executed desktop entries are now hidden by default.
- Various warnings caused by other applications Quickshell communicates with over D-BUS have been hidden in logs.
- Quickshell's new logo is now shown in any floating windows.
## Bug Fixes
- Fixed pipewire device volume and mute states not updating before the device has been used.
- Fixed a crash when changing the volume of any pipewire device on a sound card another removed device was using.
- Fixed a crash when accessing a removed previous default pipewire node from the default sink/source changed signals.
- Fixed session locks crashing if all monitors are disconnected.
- Fixed session locks crashing if unsupported by the compositor.
- Fixed a crash when creating a session lock and destroying it before acknowledged by the compositor.
- Fixed window input masks not updating after a reload.
- Fixed PanelWindows being unconfigurable unless `screen` was set under X11.
- Fixed a crash when anchoring a popup to a zero sized `Item`.
- Fixed `FileView` crashing if `watchChanges` was used.
- Fixed `SocketServer` sockets disappearing after a reload.
- Fixed `ScreencopyView` having incorrect rotation when displaying a rotated monitor.
- Fixed `MarginWrapperManager` breaking pixel alignment of child items when centering.
- Fixed `IpcHandler`, `NotificationServer` and `GlobalShortcut` not activating with certain QML structures.
- Fixed tracking of QML incubator destruction and deregistration, which occasionally caused crashes.
- Fixed FloatingWindows being constrained to the smallest window manager supported size unless max size was set.
- Fixed `MprisPlayer.lengthSupported` not updating reactively.
- Fixed normal tray icon being ignored when status is `NeedsAttention` and no attention icon is provided.
- Fixed `HyprlandWorkspace.activate()` sending invalid commands to Hyprland for named or special workspaces.
- Fixed file watcher occasionally breaking when using VSCode to edit QML files.
- Fixed crashes when screencopy buffer creation fails.
- Fixed a crash when wayland layer surfaces are recreated for the same window.
- Fixed the `QsWindow` attached object not working when using `WlrLayershell` directly.
- Fixed a crash when attempting to create a window without available VRAM.
- Fixed OOM crash when failing to write to detailed log file.
- Prevented distro logging configurations for Qt from interfering with Quickshell commands.
- Removed the "QProcess destroyed for running process" warning when destroying `Process` objects.
- Fixed `ColorQuantizer` printing a pointer to an error message instead of an error message.
- Fixed notification pixmap rowstride warning showing for correct rowstrides.

View file

@ -1,17 +0,0 @@
## New Features
- Changes to desktop entries are now tracked in real time.
## Other Changes
- Added support for Qt 6.10
## Bug Fixes
- Fixed volumes getting stuck on change for pipewire devices with few volume steps.
- Fixed a crash when running out of disk space to write log files.
- Fixed a rare crash when disconnecting a monitor.
- Fixed build issues preventing cross compilation from working.
- Fixed dekstop entries with lower priority than a hidden entry not being hidden.
- Fixed desktop entry keys with mismatched modifier or country not being discarded.
- Fixed greetd hanging when authenticating with a fingerprint.

View file

@ -2,10 +2,7 @@
qtver, qtver,
compiler, compiler,
}: let }: let
checkouts = import ./nix-checkouts.nix; nixpkgs = (import ./nix-checkouts.nix).${builtins.replaceStrings ["."] ["_"] qtver};
nixpkgs = checkouts.${builtins.replaceStrings ["."] ["_"] qtver};
compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler}; compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler};
pkg = (nixpkgs.callPackage ../default.nix {}).override (compilerOverride // { pkg = (nixpkgs.callPackage ../default.nix {}).override compilerOverride;
wayland-protocols = checkouts.latest.wayland-protocols;
});
in pkg in pkg

View file

@ -7,28 +7,9 @@ let
url = "https://github.com/nixos/nixpkgs/archive/${commit}.tar.gz"; url = "https://github.com/nixos/nixpkgs/archive/${commit}.tar.gz";
inherit sha256; inherit sha256;
}) {}; }) {};
in rec { in {
latest = qt6_10_0; # For old qt versions, grab the commit before the version bump that has all the patches
# instead of the bumped version.
qt6_10_1 = byCommit {
commit = "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38";
sha256 = "0fvbizl7j5rv2rf8j76yw0xb3d9l06hahkjys2a7k1yraznvnafm";
};
qt6_10_0 = byCommit {
commit = "c5ae371f1a6a7fd27823bc500d9390b38c05fa55";
sha256 = "18g0f8cb9m8mxnz9cf48sks0hib79b282iajl2nysyszph993yp0";
};
qt6_9_2 = byCommit {
commit = "e9f00bd893984bc8ce46c895c3bf7cac95331127";
sha256 = "0s2mhbrgzxlgkg2yxb0q0hpk8lby1a7w67dxvfmaz4gsmc0bnvfj";
};
qt6_9_1 = byCommit {
commit = "4c202d26483c5ccf3cb95e0053163facde9f047e";
sha256 = "06l2w4bcgfw7dfanpzpjcf25ydf84in240yplqsss82qx405y9di";
};
qt6_9_0 = byCommit { qt6_9_0 = byCommit {
commit = "546c545bd0594809a28ab7e869b5f80dd7243ef6"; commit = "546c545bd0594809a28ab7e869b5f80dd7243ef6";

View file

@ -2,6 +2,6 @@
clangStdenv, clangStdenv,
gccStdenv, gccStdenv,
}: { }: {
clang = { stdenv = clangStdenv; }; clang = { buildStdenv = clangStdenv; };
gcc = { stdenv = gccStdenv; }; gcc = { buildStdenv = gccStdenv; };
} }

View file

@ -2,31 +2,23 @@
lib, lib,
nix-gitignore, nix-gitignore,
pkgs, pkgs,
stdenv,
keepDebugInfo, keepDebugInfo,
buildStdenv ? pkgs.clangStdenv,
pkg-config,
cmake, cmake,
ninja, ninja,
spirv-tools,
qt6, qt6,
cpptrace ? null, spirv-tools,
libunwind,
libdwarf,
jemalloc,
cli11, cli11,
breakpad,
jemalloc,
wayland, wayland,
wayland-protocols, wayland-protocols,
wayland-scanner,
xorg,
libxcb ? xorg.libxcb,
libdrm, libdrm,
libgbm ? null, libgbm ? null,
vulkan-headers, xorg,
pipewire, pipewire,
pam, pam,
polkit,
glib,
gitRev ? (let gitRev ? (let
headExists = builtins.pathExists ./.git/HEAD; headExists = builtins.pathExists ./.git/HEAD;
@ -49,51 +41,35 @@
withPam ? true, withPam ? true,
withHyprland ? true, withHyprland ? true,
withI3 ? true, withI3 ? true,
withPolkit ? true, }: buildStdenv.mkDerivation {
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"}"; pname = "quickshell${lib.optionalString debug "-debug"}";
version = "0.2.1"; version = "0.1.0";
src = nix-gitignore.gitignoreSource "/default.nix\n" ./.; src = nix-gitignore.gitignoreSource "/docs\n/examples\n" ./.;
dontWrapQtApps = true; # see wrappers nativeBuildInputs = with pkgs; [
nativeBuildInputs = [
cmake cmake
ninja ninja
qt6.qtshadertools
spirv-tools spirv-tools
qt6.wrapQtAppsHook
pkg-config pkg-config
] ] ++ (lib.optionals withWayland [
++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland wayland-protocols
++ lib.optionals withWayland [
qt6.qtwayland # qtwaylandscanner required at build time
wayland-scanner wayland-scanner
]; ]);
buildInputs = [ buildInputs = [
qt6.qtbase qt6.qtbase
qt6.qtdeclarative qt6.qtdeclarative
libdrm
cli11 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 withJemalloc jemalloc
++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland ++ lib.optional withQtSvg qt6.qtsvg
++ lib.optionals withWayland [ wayland wayland-protocols ] ++ lib.optionals withWayland ([ qt6.qtwayland wayland ] ++ (if libgbm != null then [ libdrm libgbm ] else []))
++ lib.optionals (withWayland && libgbm != null) [ libgbm vulkan-headers ] ++ lib.optional withX11 xorg.libxcb
++ lib.optional withX11 libxcb
++ lib.optional withPam pam ++ lib.optional withPam pam
++ lib.optional withPipewire pipewire ++ lib.optional withPipewire pipewire;
++ lib.optionals withPolkit [ polkit glib ];
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
@ -102,14 +78,12 @@
(lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix) (lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
(lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true) (lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true)
(lib.cmakeFeature "GIT_REVISION" gitRev) (lib.cmakeFeature "GIT_REVISION" gitRev)
(lib.cmakeBool "CRASH_HANDLER" withCrashHandler) (lib.cmakeBool "CRASH_REPORTER" withCrashReporter)
(lib.cmakeBool "USE_JEMALLOC" withJemalloc) (lib.cmakeBool "USE_JEMALLOC" withJemalloc)
(lib.cmakeBool "WAYLAND" withWayland) (lib.cmakeBool "WAYLAND" withWayland)
(lib.cmakeBool "SCREENCOPY" (libgbm != null)) (lib.cmakeBool "SCREENCOPY" (libgbm != null))
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
(lib.cmakeBool "SERVICE_PAM" withPam) (lib.cmakeBool "SERVICE_PAM" withPam)
(lib.cmakeBool "SERVICE_NETWORKMANAGER" withNetworkManager)
(lib.cmakeBool "SERVICE_POLKIT" withPolkit)
(lib.cmakeBool "HYPRLAND" withHyprland) (lib.cmakeBool "HYPRLAND" withHyprland)
(lib.cmakeBool "I3" withI3) (lib.cmakeBool "I3" withI3)
]; ];
@ -122,34 +96,9 @@
dontStrip = debug; dontStrip = debug;
meta = with lib; { meta = with lib; {
homepage = "https://quickshell.org"; homepage = "https://git.outfoxxed.me/outfoxxed/quickshell";
description = "Flexbile QtQuick based desktop shell toolkit"; description = "Flexbile QtQuick based desktop shell toolkit";
license = licenses.lgpl3Only; license = licenses.lgpl3Only;
platforms = platforms.linux; platforms = platforms.linux;
mainProgram = "quickshell";
}; };
}; }
wrapper = unwrapped.stdenv.mkDerivation {
inherit (unwrapped) version meta buildInputs;
pname = "${unwrapped.pname}-wrapped";
nativeBuildInputs = unwrapped.nativeBuildInputs ++ [ qt6.wrapQtAppsHook ];
dontUnpack = true;
dontConfigure = true;
dontBuild = true;
installPhase = ''
mkdir -p $out
cp -r ${unwrapped}/* $out
'';
passthru = {
unwrapped = unwrapped;
withModules = modules: wrapper.overrideAttrs (prev: {
buildInputs = prev.buildInputs ++ modules;
});
};
};
in wrapper

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1768127708, "lastModified": 1736012469,
"narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=", "narHash": "sha256-/qlNWm/IEVVH7GfgAIyP6EsVZI6zjAx1cV5zNyrs+rI=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38", "rev": "8f3e1f807051e32d8c95cd12b9b421623850a34d",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -4,28 +4,22 @@
}; };
outputs = { self, nixpkgs }: let outputs = { self, nixpkgs }: let
overlayPkgs = p: p.appendOverlays [ self.overlays.default ]; forEachSystem = fn: nixpkgs.lib.genAttrs
[ "x86_64-linux" "aarch64-linux" ]
forEachSystem = fn: (system: fn system nixpkgs.legacyPackages.${system});
nixpkgs.lib.genAttrs
nixpkgs.lib.platforms.linux
(system: fn system (overlayPkgs nixpkgs.legacyPackages.${system}));
in { in {
overlays.default = import ./overlay.nix { packages = forEachSystem (system: pkgs: rec {
rev = self.rev or self.dirtyRev; quickshell = pkgs.callPackage ./default.nix {
gitRev = self.rev or self.dirtyRev;
}; };
packages = forEachSystem (system: pkgs: rec {
quickshell = pkgs.quickshell;
default = quickshell; default = quickshell;
}); });
devShells = forEachSystem (system: pkgs: rec { devShells = forEachSystem (system: pkgs: rec {
default = import ./shell.nix { default = import ./shell.nix {
inherit pkgs; inherit pkgs;
quickshell = self.packages.${system}.quickshell.override { inherit (self.packages.${system}) quickshell;
stdenv = pkgs.clangStdenv;
};
}; };
}); });
}; };

View file

@ -1,5 +0,0 @@
{ rev ? null }: (final: prev: {
quickshell = final.callPackage ./default.nix {
gitRev = rev;
};
})

View file

@ -35,14 +35,13 @@
pkg-config pkg-config
qtshadertools qtshadertools
spirv-tools spirv-tools
wayland-protocols wayland-protocols))
cli11)) (inputs (list cli11
(inputs (list jemalloc jemalloc
libdrm libdrm
libxcb libxcb
libxkbcommon libxkbcommon
linux-pam linux-pam
polkit
mesa mesa
pipewire pipewire
qtbase qtbase
@ -56,7 +55,8 @@
#~(list "-GNinja" #~(list "-GNinja"
"-DDISTRIBUTOR=\"In-tree Guix channel\"" "-DDISTRIBUTOR=\"In-tree Guix channel\""
"-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO" "-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO"
"-DCRASH_HANDLER=OFF") ;; Breakpad is not currently packaged for Guix.
"-DCRASH_REPORTER=OFF")
#:phases #:phases
#~(modify-phases %standard-phases #~(modify-phases %standard-phases
(replace 'build (lambda _ (invoke "cmake" "--build" "."))) (replace 'build (lambda _ (invoke "cmake" "--build" ".")))

View file

@ -1,15 +1,14 @@
{ {
pkgs ? import <nixpkgs> {}, pkgs ? import <nixpkgs> {},
stdenv ? pkgs.clangStdenv, # faster compiles than gcc quickshell ? pkgs.callPackage ./default.nix {},
quickshell ? pkgs.callPackage ./default.nix { inherit stdenv; },
... ...
}: let }: let
tidyfox = import (pkgs.fetchFromGitea { tidyfox = import (pkgs.fetchFromGitea {
domain = "git.outfoxxed.me"; domain = "git.outfoxxed.me";
owner = "outfoxxed"; owner = "outfoxxed";
repo = "tidyfox"; repo = "tidyfox";
rev = "9d85d7e7dea2602aa74ec3168955fee69967e92f"; rev = "1f062cc198d1112d13e5128fa1f2ee3dbffe613b";
hash = "sha256-77ERiweF6lumonp2c/124rAoVG6/o9J+Aajhttwtu0w="; sha256 = "kbt0Zc1qHE5fhqBkKz8iue+B+ZANjF1AR/RdgmX1r0I=";
}) { inherit pkgs; }; }) { inherit pkgs; };
in pkgs.mkShell.override { stdenv = quickshell.stdenv; } { in pkgs.mkShell.override { stdenv = quickshell.stdenv; } {
inputsFrom = [ quickshell ]; inputsFrom = [ quickshell ];

View file

@ -11,9 +11,8 @@ add_subdirectory(window)
add_subdirectory(io) add_subdirectory(io)
add_subdirectory(widgets) add_subdirectory(widgets)
add_subdirectory(ui) add_subdirectory(ui)
add_subdirectory(windowmanager)
if (CRASH_HANDLER) if (CRASH_REPORTER)
add_subdirectory(crash) add_subdirectory(crash)
endif() endif()
@ -30,11 +29,3 @@ if (X11)
endif() endif()
add_subdirectory(services) add_subdirectory(services)
if (BLUETOOTH)
add_subdirectory(bluetooth)
endif()
if (NETWORK)
add_subdirectory(network)
endif()

View file

@ -1,42 +0,0 @@
set_source_files_properties(org.bluez.Adapter.xml PROPERTIES
CLASSNAME DBusBluezAdapterInterface
)
set_source_files_properties(org.bluez.Device.xml PROPERTIES
CLASSNAME DBusBluezDeviceInterface
)
qt_add_dbus_interface(DBUS_INTERFACES
org.bluez.Adapter.xml
dbus_adapter
)
qt_add_dbus_interface(DBUS_INTERFACES
org.bluez.Device.xml
dbus_device
)
qt_add_library(quickshell-bluetooth STATIC
adapter.cpp
bluez.cpp
device.cpp
${DBUS_INTERFACES}
)
qt_add_qml_module(quickshell-bluetooth
URI Quickshell.Bluetooth
VERSION 0.1
DEPENDENCIES QtQml
)
install_qml_module(quickshell-bluetooth)
# dbus headers
target_include_directories(quickshell-bluetooth PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(quickshell-bluetooth PRIVATE Qt::Qml Qt::DBus)
qs_add_link_dependencies(quickshell-bluetooth quickshell-dbus)
qs_module_pch(quickshell-bluetooth SET dbus)
target_link_libraries(quickshell PRIVATE quickshell-bluetoothplugin)

View file

@ -1,223 +0,0 @@
#include "adapter.hpp"
#include <qcontainerfwd.h>
#include <qdbusconnection.h>
#include <qdbusextratypes.h>
#include <qdbuspendingcall.h>
#include <qdbuspendingreply.h>
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qstring.h>
#include <qtypes.h>
#include "../core/logcat.hpp"
#include "../dbus/properties.hpp"
#include "dbus_adapter.h"
namespace qs::bluetooth {
namespace {
QS_LOGGING_CATEGORY(logAdapter, "quickshell.bluetooth.adapter", QtWarningMsg);
}
QString BluetoothAdapterState::toString(BluetoothAdapterState::Enum state) {
switch (state) {
case BluetoothAdapterState::Disabled: return QStringLiteral("Disabled");
case BluetoothAdapterState::Enabled: return QStringLiteral("Enabled");
case BluetoothAdapterState::Enabling: return QStringLiteral("Enabling");
case BluetoothAdapterState::Disabling: return QStringLiteral("Disabling");
case BluetoothAdapterState::Blocked: return QStringLiteral("Blocked");
default: return QStringLiteral("Unknown");
}
}
BluetoothAdapter::BluetoothAdapter(const QString& path, QObject* parent): QObject(parent) {
this->mInterface =
new DBusBluezAdapterInterface("org.bluez", path, QDBusConnection::systemBus(), this);
if (!this->mInterface->isValid()) {
qCWarning(logAdapter) << "Could not create DBus interface for adapter at" << path;
this->mInterface = nullptr;
return;
}
this->properties.setInterface(this->mInterface);
}
QString BluetoothAdapter::adapterId() const {
auto path = this->path();
return path.sliced(path.lastIndexOf('/') + 1);
}
void BluetoothAdapter::setEnabled(bool enabled) {
if (enabled == this->bEnabled) return;
if (enabled && this->bState == BluetoothAdapterState::Blocked) {
qCCritical(logAdapter) << "Cannot enable adapter because it is blocked by rfkill.";
return;
}
this->bEnabled = enabled;
this->pEnabled.write();
}
void BluetoothAdapter::setDiscoverable(bool discoverable) {
if (discoverable == this->bDiscoverable) return;
this->bDiscoverable = discoverable;
this->pDiscoverable.write();
}
void BluetoothAdapter::setDiscovering(bool discovering) {
if (discovering) {
this->startDiscovery();
} else {
this->stopDiscovery();
}
}
void BluetoothAdapter::setDiscoverableTimeout(quint32 timeout) {
if (timeout == this->bDiscoverableTimeout) return;
this->bDiscoverableTimeout = timeout;
this->pDiscoverableTimeout.write();
}
void BluetoothAdapter::setPairable(bool pairable) {
if (pairable == this->bPairable) return;
this->bPairable = pairable;
this->pPairable.write();
}
void BluetoothAdapter::setPairableTimeout(quint32 timeout) {
if (timeout == this->bPairableTimeout) return;
this->bPairableTimeout = timeout;
this->pPairableTimeout.write();
}
void BluetoothAdapter::addInterface(const QString& interface, const QVariantMap& properties) {
if (interface == "org.bluez.Adapter1") {
this->properties.updatePropertySet(properties, false);
qCDebug(logAdapter) << "Updated Adapter properties for" << this;
}
}
void BluetoothAdapter::removeDevice(const QString& devicePath) {
qCDebug(logAdapter) << "Removing device" << devicePath << "from adapter" << this;
auto reply = this->mInterface->RemoveDevice(QDBusObjectPath(devicePath));
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this, devicePath](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logAdapter).nospace()
<< "Failed to remove device " << devicePath << " from adapter" << this << ": "
<< reply.error().message();
} else {
qCDebug(logAdapter) << "Successfully removed device" << devicePath << "from adapter"
<< this;
}
delete watcher;
}
);
}
void BluetoothAdapter::startDiscovery() {
if (this->bDiscovering) return;
qCDebug(logAdapter) << "Starting discovery for adapter" << this;
auto reply = this->mInterface->StartDiscovery();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logAdapter).nospace()
<< "Failed to start discovery on adapter" << this << ": " << reply.error().message();
} else {
qCDebug(logAdapter) << "Successfully started discovery on adapter" << this;
}
delete watcher;
}
);
}
void BluetoothAdapter::stopDiscovery() {
if (!this->bDiscovering) return;
qCDebug(logAdapter) << "Stopping discovery for adapter" << this;
auto reply = this->mInterface->StopDiscovery();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logAdapter).nospace()
<< "Failed to stop discovery on adapter " << this << ": " << reply.error().message();
} else {
qCDebug(logAdapter) << "Successfully stopped discovery on adapter" << this;
}
delete watcher;
}
);
}
} // namespace qs::bluetooth
namespace qs::dbus {
using namespace qs::bluetooth;
DBusResult<BluetoothAdapterState::Enum>
DBusDataTransform<BluetoothAdapterState::Enum>::fromWire(const Wire& wire) {
if (wire == QStringLiteral("off")) {
return BluetoothAdapterState::Disabled;
} else if (wire == QStringLiteral("on")) {
return BluetoothAdapterState::Enabled;
} else if (wire == QStringLiteral("off-enabling")) {
return BluetoothAdapterState::Enabling;
} else if (wire == QStringLiteral("on-disabling")) {
return BluetoothAdapterState::Disabling;
} else if (wire == QStringLiteral("off-blocked")) {
return BluetoothAdapterState::Blocked;
} else {
return QDBusError(
QDBusError::InvalidArgs,
QString("Invalid BluetoothAdapterState: %1").arg(wire)
);
}
}
} // namespace qs::dbus
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter) {
auto saver = QDebugStateSaver(debug);
if (adapter) {
debug.nospace() << "BluetoothAdapter(" << static_cast<const void*>(adapter)
<< ", path=" << adapter->path() << ")";
} else {
debug << "BluetoothAdapter(nullptr)";
}
return debug;
}

View file

@ -1,173 +0,0 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include "../core/doc.hpp"
#include "../core/model.hpp"
#include "../dbus/properties.hpp"
#include "dbus_adapter.h"
namespace qs::bluetooth {
///! Power state of a Bluetooth adapter.
class BluetoothAdapterState: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum : quint8 {
/// The adapter is powered off.
Disabled = 0,
/// The adapter is powered on.
Enabled = 1,
/// The adapter is transitioning from off to on.
Enabling = 2,
/// The adapter is transitioning from on to off.
Disabling = 3,
/// The adapter is blocked by rfkill.
Blocked = 4,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(BluetoothAdapterState::Enum state);
};
} // namespace qs::bluetooth
namespace qs::dbus {
template <>
struct DBusDataTransform<qs::bluetooth::BluetoothAdapterState::Enum> {
using Wire = QString;
using Data = qs::bluetooth::BluetoothAdapterState::Enum;
static DBusResult<Data> fromWire(const Wire& wire);
};
} // namespace qs::dbus
namespace qs::bluetooth {
class BluetoothAdapter;
class BluetoothDevice;
///! A Bluetooth adapter
class BluetoothAdapter: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_UNCREATABLE("");
// clang-format off
/// System provided name of the adapter. See @@adapterId for the internal identifier.
Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName);
/// True if the adapter is currently enabled. More detailed state is available from @@state.
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
/// Detailed power state of the adapter.
Q_PROPERTY(BluetoothAdapterState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
/// True if the adapter can be discovered by other bluetooth devices.
Q_PROPERTY(bool discoverable READ discoverable WRITE setDiscoverable NOTIFY discoverableChanged);
/// Timeout in seconds for how long the adapter stays discoverable after @@discoverable is set to true.
/// A value of 0 means the adapter stays discoverable forever.
Q_PROPERTY(quint32 discoverableTimeout READ discoverableTimeout WRITE setDiscoverableTimeout NOTIFY discoverableTimeoutChanged);
/// True if the adapter is scanning for new devices.
Q_PROPERTY(bool discovering READ discovering WRITE setDiscovering NOTIFY discoveringChanged);
/// True if the adapter is accepting incoming pairing requests.
///
/// This only affects incoming pairing requests and should typically only be changed
/// by system settings applications. Defaults to true.
Q_PROPERTY(bool pairable READ pairable WRITE setPairable NOTIFY pairableChanged);
/// Timeout in seconds for how long the adapter stays pairable after @@pairable is set to true.
/// A value of 0 means the adapter stays pairable forever. Defaults to 0.
Q_PROPERTY(quint32 pairableTimeout READ pairableTimeout WRITE setPairableTimeout NOTIFY pairableTimeoutChanged);
/// Bluetooth devices connected to this adapter.
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothDevice>*);
Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
/// The internal ID of the adapter (e.g., "hci0").
Q_PROPERTY(QString adapterId READ adapterId CONSTANT);
/// DBus path of the adapter under the `org.bluez` system service.
Q_PROPERTY(QString dbusPath READ path CONSTANT);
// clang-format on
public:
explicit BluetoothAdapter(const QString& path, QObject* parent = nullptr);
[[nodiscard]] bool isValid() const { return this->mInterface->isValid(); }
[[nodiscard]] QString path() const { return this->mInterface->path(); }
[[nodiscard]] QString adapterId() const;
[[nodiscard]] bool enabled() const { return this->bEnabled; }
void setEnabled(bool enabled);
[[nodiscard]] bool discoverable() const { return this->bDiscoverable; }
void setDiscoverable(bool discoverable);
[[nodiscard]] bool discovering() const { return this->bDiscovering; }
void setDiscovering(bool discovering);
[[nodiscard]] quint32 discoverableTimeout() const { return this->bDiscoverableTimeout; }
void setDiscoverableTimeout(quint32 timeout);
[[nodiscard]] bool pairable() const { return this->bPairable; }
void setPairable(bool pairable);
[[nodiscard]] quint32 pairableTimeout() const { return this->bPairableTimeout; }
void setPairableTimeout(quint32 timeout);
[[nodiscard]] QBindable<QString> bindableName() { return &this->bName; }
[[nodiscard]] QBindable<bool> bindableEnabled() { return &this->bEnabled; }
[[nodiscard]] QBindable<BluetoothAdapterState::Enum> bindableState() { return &this->bState; }
[[nodiscard]] QBindable<bool> bindableDiscoverable() { return &this->bDiscoverable; }
[[nodiscard]] QBindable<quint32> bindableDiscoverableTimeout() {
return &this->bDiscoverableTimeout;
}
[[nodiscard]] QBindable<bool> bindableDiscovering() { return &this->bDiscovering; }
[[nodiscard]] QBindable<bool> bindablePairable() { return &this->bPairable; }
[[nodiscard]] QBindable<quint32> bindablePairableTimeout() { return &this->bPairableTimeout; }
[[nodiscard]] ObjectModel<BluetoothDevice>* devices() { return &this->mDevices; }
void addInterface(const QString& interface, const QVariantMap& properties);
void removeDevice(const QString& devicePath);
void startDiscovery();
void stopDiscovery();
signals:
void nameChanged();
void enabledChanged();
void stateChanged();
void discoverableChanged();
void discoverableTimeoutChanged();
void discoveringChanged();
void pairableChanged();
void pairableTimeoutChanged();
private:
DBusBluezAdapterInterface* mInterface = nullptr;
ObjectModel<BluetoothDevice> mDevices {this};
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, QString, bName, &BluetoothAdapter::nameChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bEnabled, &BluetoothAdapter::enabledChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, BluetoothAdapterState::Enum, bState, &BluetoothAdapter::stateChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscoverable, &BluetoothAdapter::discoverableChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bDiscoverableTimeout, &BluetoothAdapter::discoverableTimeoutChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscovering, &BluetoothAdapter::discoveringChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bPairable, &BluetoothAdapter::pairableChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bPairableTimeout, &BluetoothAdapter::pairableTimeoutChanged);
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothAdapter, properties);
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pName, bName, properties, "Alias");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pEnabled, bEnabled, properties, "Powered");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pState, bState, properties, "PowerState");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverable, bDiscoverable, properties, "Discoverable");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverableTimeout, bDiscoverableTimeout, properties, "DiscoverableTimeout");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscovering, bDiscovering, properties, "Discovering");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairable, bPairable, properties, "Pairable");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairableTimeout, bPairableTimeout, properties, "PairableTimeout");
// clang-format on
};
} // namespace qs::bluetooth
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter);

View file

@ -1,168 +0,0 @@
#include "bluez.hpp"
#include <qcontainerfwd.h>
#include <qdbusconnection.h>
#include <qdbusextratypes.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include "../core/logcat.hpp"
#include "../dbus/dbus_objectmanager_types.hpp"
#include "../dbus/objectmanager.hpp"
#include "adapter.hpp"
#include "device.hpp"
namespace qs::bluetooth {
namespace {
QS_LOGGING_CATEGORY(logBluetooth, "quickshell.bluetooth", QtWarningMsg);
}
Bluez* Bluez::instance() {
static auto* instance = new Bluez();
return instance;
}
Bluez::Bluez() { this->init(); }
void Bluez::updateDefaultAdapter() {
const auto& adapters = this->mAdapters.valueList();
this->bDefaultAdapter = adapters.empty() ? nullptr : adapters.first();
}
void Bluez::init() {
qCDebug(logBluetooth) << "Connecting to BlueZ";
auto bus = QDBusConnection::systemBus();
if (!bus.isConnected()) {
qCWarning(logBluetooth) << "Could not connect to DBus. Bluetooth integration is not available.";
return;
}
this->objectManager = new qs::dbus::DBusObjectManager(this);
QObject::connect(
this->objectManager,
&qs::dbus::DBusObjectManager::interfacesAdded,
this,
&Bluez::onInterfacesAdded
);
QObject::connect(
this->objectManager,
&qs::dbus::DBusObjectManager::interfacesRemoved,
this,
&Bluez::onInterfacesRemoved
);
if (!this->objectManager->setInterface("org.bluez", "/", bus)) {
qCDebug(logBluetooth) << "BlueZ is not running. Bluetooth integration will not work.";
return;
}
}
void Bluez::onInterfacesAdded(
const QDBusObjectPath& path,
const DBusObjectManagerInterfaces& interfaces
) {
if (auto* adapter = this->mAdapterMap.value(path.path())) {
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
adapter->addInterface(interface, properties);
}
} else if (auto* device = this->mDeviceMap.value(path.path())) {
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
device->addInterface(interface, properties);
}
} else if (interfaces.contains("org.bluez.Adapter1")) {
auto* adapter = new BluetoothAdapter(path.path(), this);
if (!adapter->isValid()) {
qCWarning(logBluetooth) << "Adapter path is not valid, cannot track: " << device;
delete adapter;
return;
}
qCDebug(logBluetooth) << "Tracked new adapter" << adapter;
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
adapter->addInterface(interface, properties);
}
for (auto* device: this->mDevices.valueList()) {
if (device->adapterPath() == path) {
adapter->devices()->insertObject(device);
qCDebug(logBluetooth) << "Added tracked device" << device << "to new adapter" << adapter;
emit device->adapterChanged();
}
}
this->mAdapterMap.insert(path.path(), adapter);
this->mAdapters.insertObject(adapter);
this->updateDefaultAdapter();
} else if (interfaces.contains("org.bluez.Device1")) {
auto* device = new BluetoothDevice(path.path(), this);
if (!device->isValid()) {
qCWarning(logBluetooth) << "Device path is not valid, cannot track: " << device;
delete device;
return;
}
qCDebug(logBluetooth) << "Tracked new device" << device;
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
device->addInterface(interface, properties);
}
if (auto* adapter = device->adapter()) {
adapter->devices()->insertObject(device);
qCDebug(logBluetooth) << "Added device" << device << "to adapter" << adapter;
}
this->mDeviceMap.insert(path.path(), device);
this->mDevices.insertObject(device);
}
}
void Bluez::onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces) {
if (auto* adapter = this->mAdapterMap.value(path.path())) {
if (interfaces.contains("org.bluez.Adapter1")) {
qCDebug(logBluetooth) << "Adapter removed:" << adapter;
this->mAdapterMap.remove(path.path());
this->mAdapters.removeObject(adapter);
this->updateDefaultAdapter();
delete adapter;
}
} else if (auto* device = this->mDeviceMap.value(path.path())) {
if (interfaces.contains("org.bluez.Device1")) {
qCDebug(logBluetooth) << "Device removed:" << device;
if (auto* adapter = device->adapter()) {
adapter->devices()->removeObject(device);
}
this->mDeviceMap.remove(path.path());
this->mDevices.removeObject(device);
delete device;
} else {
for (const auto& interface: interfaces) {
device->removeInterface(interface);
}
}
}
}
BluezQml::BluezQml() {
QObject::connect(
Bluez::instance(),
&Bluez::defaultAdapterChanged,
this,
&BluezQml::defaultAdapterChanged
);
}
} // namespace qs::bluetooth

View file

@ -1,98 +0,0 @@
#pragma once
#include <qcontainerfwd.h>
#include <qhash.h>
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include "../core/doc.hpp"
#include "../core/model.hpp"
#include "../dbus/dbus_objectmanager_types.hpp"
#include "../dbus/objectmanager.hpp"
namespace qs::bluetooth {
class BluetoothAdapter;
class BluetoothDevice;
class Bluez: public QObject {
Q_OBJECT;
public:
[[nodiscard]] ObjectModel<BluetoothAdapter>* adapters() { return &this->mAdapters; }
[[nodiscard]] ObjectModel<BluetoothDevice>* devices() { return &this->mDevices; }
[[nodiscard]] BluetoothAdapter* adapter(const QString& path) {
return this->mAdapterMap.value(path);
}
static Bluez* instance();
signals:
void defaultAdapterChanged();
private slots:
void
onInterfacesAdded(const QDBusObjectPath& path, const DBusObjectManagerInterfaces& interfaces);
void onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces);
void updateDefaultAdapter();
private:
explicit Bluez();
void init();
qs::dbus::DBusObjectManager* objectManager = nullptr;
QHash<QString, BluetoothAdapter*> mAdapterMap;
QHash<QString, BluetoothDevice*> mDeviceMap;
ObjectModel<BluetoothAdapter> mAdapters {this};
ObjectModel<BluetoothDevice> mDevices {this};
public:
Q_OBJECT_BINDABLE_PROPERTY(
Bluez,
BluetoothAdapter*,
bDefaultAdapter,
&Bluez::defaultAdapterChanged
);
};
///! Bluetooth manager
/// Provides access to bluetooth devices and adapters.
class BluezQml: public QObject {
Q_OBJECT;
QML_NAMED_ELEMENT(Bluetooth);
QML_SINGLETON;
// clang-format off
/// The default bluetooth adapter. Usually there is only one.
Q_PROPERTY(BluetoothAdapter* defaultAdapter READ default NOTIFY defaultAdapterChanged BINDABLE bindableDefaultAdapter);
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothAdapter>*);
/// A list of all bluetooth adapters. See @@defaultAdapter for the default.
Q_PROPERTY(UntypedObjectModel* adapters READ adapters CONSTANT);
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothDevice>*);
/// A list of all connected bluetooth devices across all adapters.
/// See @@BluetoothAdapter.devices for the devices connected to a single adapter.
Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
// clang-format on
signals:
void defaultAdapterChanged();
public:
explicit BluezQml();
[[nodiscard]] static ObjectModel<BluetoothAdapter>* adapters() {
return Bluez::instance()->adapters();
}
[[nodiscard]] static ObjectModel<BluetoothDevice>* devices() {
return Bluez::instance()->devices();
}
[[nodiscard]] static QBindable<BluetoothAdapter*> bindableDefaultAdapter() {
return &Bluez::instance()->bDefaultAdapter;
}
};
} // namespace qs::bluetooth

View file

@ -1,318 +0,0 @@
#include "device.hpp"
#include <qcontainerfwd.h>
#include <qdbusconnection.h>
#include <qdbuspendingcall.h>
#include <qdbuspendingreply.h>
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../core/logcat.hpp"
#include "../dbus/properties.hpp"
#include "adapter.hpp"
#include "bluez.hpp"
#include "dbus_device.h"
namespace qs::bluetooth {
namespace {
QS_LOGGING_CATEGORY(logDevice, "quickshell.bluetooth.device", QtWarningMsg);
}
QString BluetoothDeviceState::toString(BluetoothDeviceState::Enum state) {
switch (state) {
case BluetoothDeviceState::Disconnected: return QStringLiteral("Disconnected");
case BluetoothDeviceState::Connected: return QStringLiteral("Connected");
case BluetoothDeviceState::Disconnecting: return QStringLiteral("Disconnecting");
case BluetoothDeviceState::Connecting: return QStringLiteral("Connecting");
default: return QStringLiteral("Unknown");
}
}
BluetoothDevice::BluetoothDevice(const QString& path, QObject* parent): QObject(parent) {
this->mInterface =
new DBusBluezDeviceInterface("org.bluez", path, QDBusConnection::systemBus(), this);
if (!this->mInterface->isValid()) {
qCWarning(logDevice) << "Could not create DBus interface for device at" << path;
delete this->mInterface;
this->mInterface = nullptr;
return;
}
this->properties.setInterface(this->mInterface);
}
BluetoothAdapter* BluetoothDevice::adapter() const {
return Bluez::instance()->adapter(this->bAdapterPath.value().path());
}
void BluetoothDevice::setConnected(bool connected) {
if (connected == this->bConnected) return;
if (connected) {
this->connect();
} else {
this->disconnect();
}
}
void BluetoothDevice::setTrusted(bool trusted) {
if (trusted == this->bTrusted) return;
this->bTrusted = trusted;
this->pTrusted.write();
}
void BluetoothDevice::setBlocked(bool blocked) {
if (blocked == this->bBlocked) return;
this->bBlocked = blocked;
this->pBlocked.write();
}
void BluetoothDevice::setName(const QString& name) {
if (name == this->bName) return;
this->bName = name;
this->pName.write();
}
void BluetoothDevice::setWakeAllowed(bool wakeAllowed) {
if (wakeAllowed == this->bWakeAllowed) return;
this->bWakeAllowed = wakeAllowed;
this->pWakeAllowed.write();
}
void BluetoothDevice::connect() {
if (this->bConnected) {
qCCritical(logDevice) << "Device" << this << "is already connected";
return;
}
if (this->bState == BluetoothDeviceState::Connecting) {
qCCritical(logDevice) << "Device" << this << "is already connecting";
return;
}
qCDebug(logDevice) << "Connecting to device" << this;
this->bState = BluetoothDeviceState::Connecting;
auto reply = this->mInterface->Connect();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logDevice).nospace()
<< "Failed to connect to device " << this << ": " << reply.error().message();
this->bState = this->bConnected ? BluetoothDeviceState::Connected
: BluetoothDeviceState::Disconnected;
} else {
qCDebug(logDevice) << "Successfully connected to to device" << this;
}
delete watcher;
}
);
}
void BluetoothDevice::disconnect() {
if (!this->bConnected) {
qCCritical(logDevice) << "Device" << this << "is already disconnected";
return;
}
if (this->bState == BluetoothDeviceState::Disconnecting) {
qCCritical(logDevice) << "Device" << this << "is already disconnecting";
return;
}
qCDebug(logDevice) << "Disconnecting from device" << this;
this->bState = BluetoothDeviceState::Disconnecting;
auto reply = this->mInterface->Disconnect();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logDevice).nospace()
<< "Failed to disconnect from device " << this << ": " << reply.error().message();
this->bState = this->bConnected ? BluetoothDeviceState::Connected
: BluetoothDeviceState::Disconnected;
} else {
qCDebug(logDevice) << "Successfully disconnected from from device" << this;
}
delete watcher;
}
);
}
void BluetoothDevice::pair() {
if (this->bPaired) {
qCCritical(logDevice) << "Device" << this << "is already paired";
return;
}
if (this->bPairing) {
qCCritical(logDevice) << "Device" << this << "is already pairing";
return;
}
qCDebug(logDevice) << "Pairing with device" << this;
this->bPairing = true;
auto reply = this->mInterface->Pair();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logDevice).nospace()
<< "Failed to pair with device " << this << ": " << reply.error().message();
} else {
qCDebug(logDevice) << "Successfully initiated pairing with device" << this;
}
this->bPairing = false;
delete watcher;
}
);
}
void BluetoothDevice::cancelPair() {
if (!this->bPairing) {
qCCritical(logDevice) << "Device" << this << "is not currently pairing";
return;
}
qCDebug(logDevice) << "Cancelling pairing with device" << this;
auto reply = this->mInterface->CancelPairing();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logDevice) << "Failed to cancel pairing with device" << this << ":"
<< reply.error().message();
} else {
qCDebug(logDevice) << "Successfully cancelled pairing with device" << this;
}
this->bPairing = false;
delete watcher;
}
);
}
void BluetoothDevice::forget() {
if (!this->mInterface || !this->mInterface->isValid()) {
qCCritical(logDevice) << "Cannot forget - device interface is invalid";
return;
}
if (auto* adapter = Bluez::instance()->adapter(this->bAdapterPath.value().path())) {
qCDebug(logDevice) << "Forgetting device" << this << "via adapter" << adapter;
adapter->removeDevice(this->path());
} else {
qCCritical(logDevice) << "Could not find adapter for path" << this->bAdapterPath.value().path()
<< "to forget from";
}
}
void BluetoothDevice::addInterface(const QString& interface, const QVariantMap& properties) {
if (interface == "org.bluez.Device1") {
this->properties.updatePropertySet(properties, false);
qCDebug(logDevice) << "Updated Device properties for" << this;
} else if (interface == "org.bluez.Battery1") {
if (!this->mBatteryInterface) {
this->mBatteryInterface = new QDBusInterface(
"org.bluez",
this->path(),
"org.bluez.Battery1",
QDBusConnection::systemBus(),
this
);
if (!this->mBatteryInterface->isValid()) {
qCWarning(logDevice) << "Could not create Battery interface for device at" << this;
delete this->mBatteryInterface;
this->mBatteryInterface = nullptr;
return;
}
}
this->batteryProperties.setInterface(this->mBatteryInterface);
this->batteryProperties.updatePropertySet(properties, false);
emit this->batteryAvailableChanged();
qCDebug(logDevice) << "Updated Battery properties for" << this;
}
}
void BluetoothDevice::removeInterface(const QString& interface) {
if (interface == "org.bluez.Battery1" && this->mBatteryInterface) {
this->batteryProperties.setInterface(nullptr);
delete this->mBatteryInterface;
this->mBatteryInterface = nullptr;
this->bBattery = 0;
emit this->batteryAvailableChanged();
qCDebug(logDevice) << "Battery interface removed from device" << this;
}
}
void BluetoothDevice::onConnectedChanged() {
this->bState =
this->bConnected ? BluetoothDeviceState::Connected : BluetoothDeviceState::Disconnected;
emit this->connectedChanged();
}
} // namespace qs::bluetooth
namespace qs::dbus {
using namespace qs::bluetooth;
DBusResult<qreal> DBusDataTransform<BatteryPercentage>::fromWire(quint8 percentage) {
return DBusResult(percentage * 0.01);
}
} // namespace qs::dbus
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device) {
auto saver = QDebugStateSaver(debug);
if (device) {
debug.nospace() << "BluetoothDevice(" << static_cast<const void*>(device)
<< ", path=" << device->path() << ")";
} else {
debug << "BluetoothDevice(nullptr)";
}
return debug;
}

View file

@ -1,225 +0,0 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../dbus/properties.hpp"
#include "dbus_device.h"
namespace qs::bluetooth {
///! Connection state of a Bluetooth device.
class BluetoothDeviceState: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum : quint8 {
/// The device is not connected.
Disconnected = 0,
/// The device is connected.
Connected = 1,
/// The device is disconnecting.
Disconnecting = 2,
/// The device is connecting.
Connecting = 3,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(BluetoothDeviceState::Enum state);
};
struct BatteryPercentage {};
} // namespace qs::bluetooth
namespace qs::dbus {
template <>
struct DBusDataTransform<qs::bluetooth::BatteryPercentage> {
using Wire = quint8;
using Data = qreal;
static DBusResult<Data> fromWire(Wire percentage);
};
} // namespace qs::dbus
namespace qs::bluetooth {
class BluetoothAdapter;
///! A tracked Bluetooth device.
class BluetoothDevice: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_UNCREATABLE("");
// clang-format off
/// MAC address of the device.
Q_PROPERTY(QString address READ default NOTIFY addressChanged BINDABLE bindableAddress);
/// The name of the Bluetooth device. This property may be written to create an alias, or set to
/// an empty string to fall back to the device provided name.
///
/// See @@deviceName for the name provided by the device.
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged);
/// The name of the Bluetooth device, ignoring user provided aliases. See also @@name
/// which returns a user provided alias if set.
Q_PROPERTY(QString deviceName READ default NOTIFY deviceNameChanged BINDABLE bindableDeviceName);
/// System icon representing the device type. Use @@Quickshell.Quickshell.iconPath() to display this in an image.
Q_PROPERTY(QString icon READ default NOTIFY iconChanged BINDABLE bindableIcon);
/// Connection state of the device.
Q_PROPERTY(BluetoothDeviceState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
/// True if the device is currently connected to the computer.
///
/// Setting this property is equivalent to calling @@connect() and @@disconnect().
///
/// > [!NOTE] @@state provides more detailed information if required.
Q_PROPERTY(bool connected READ connected WRITE setConnected NOTIFY connectedChanged);
/// True if the device is paired to the computer.
///
/// > [!NOTE] @@pair() can be used to pair a device, however you must @@forget() the device to unpair it.
Q_PROPERTY(bool paired READ default NOTIFY pairedChanged BINDABLE bindablePaired);
/// True if pairing information is stored for future connections.
Q_PROPERTY(bool bonded READ default NOTIFY bondedChanged BINDABLE bindableBonded);
/// True if the device is currently being paired.
///
/// > [!NOTE] @@cancelPair() can be used to cancel the pairing process.
Q_PROPERTY(bool pairing READ pairing NOTIFY pairingChanged);
/// True if the device is considered to be trusted by the system.
/// Trusted devices are allowed to reconnect themselves to the system without intervention.
Q_PROPERTY(bool trusted READ trusted WRITE setTrusted NOTIFY trustedChanged);
/// True if the device is blocked from connecting.
/// If a device is blocked, any connection attempts will be immediately rejected by the system.
Q_PROPERTY(bool blocked READ blocked WRITE setBlocked NOTIFY blockedChanged);
/// True if the device is allowed to wake up the host system from suspend.
Q_PROPERTY(bool wakeAllowed READ wakeAllowed WRITE setWakeAllowed NOTIFY wakeAllowedChanged);
/// True if the connected device reports its battery level. Battery level can be accessed via @@battery.
Q_PROPERTY(bool batteryAvailable READ batteryAvailable NOTIFY batteryAvailableChanged);
/// Battery level of the connected device, from `0.0` to `1.0`. Only valid if @@batteryAvailable is true.
Q_PROPERTY(qreal battery READ default NOTIFY batteryChanged BINDABLE bindableBattery);
/// The Bluetooth adapter this device belongs to.
Q_PROPERTY(BluetoothAdapter* adapter READ adapter NOTIFY adapterChanged);
/// DBus path of the device under the `org.bluez` system service.
Q_PROPERTY(QString dbusPath READ path CONSTANT);
// clang-format on
public:
explicit BluetoothDevice(const QString& path, QObject* parent = nullptr);
/// Attempt to connect to the device.
Q_INVOKABLE void connect();
/// Disconnect from the device.
Q_INVOKABLE void disconnect();
/// Attempt to pair the device.
///
/// > [!NOTE] @@paired and @@pairing return the current pairing status of the device.
Q_INVOKABLE void pair();
/// Cancel an active pairing attempt.
Q_INVOKABLE void cancelPair();
/// Forget the device.
Q_INVOKABLE void forget();
[[nodiscard]] bool isValid() const { return this->mInterface && this->mInterface->isValid(); }
[[nodiscard]] QString path() const {
return this->mInterface ? this->mInterface->path() : QString();
}
[[nodiscard]] bool batteryAvailable() const { return this->mBatteryInterface != nullptr; }
[[nodiscard]] BluetoothAdapter* adapter() const;
[[nodiscard]] QDBusObjectPath adapterPath() const { return this->bAdapterPath.value(); }
[[nodiscard]] bool connected() const { return this->bConnected; }
void setConnected(bool connected);
[[nodiscard]] bool trusted() const { return this->bTrusted; }
void setTrusted(bool trusted);
[[nodiscard]] bool blocked() const { return this->bBlocked; }
void setBlocked(bool blocked);
[[nodiscard]] QString name() const { return this->bName; }
void setName(const QString& name);
[[nodiscard]] bool wakeAllowed() const { return this->bWakeAllowed; }
void setWakeAllowed(bool wakeAllowed);
[[nodiscard]] bool pairing() const { return this->bPairing; }
[[nodiscard]] QBindable<QString> bindableAddress() { return &this->bAddress; }
[[nodiscard]] QBindable<QString> bindableDeviceName() { return &this->bDeviceName; }
[[nodiscard]] QBindable<QString> bindableName() { return &this->bName; }
[[nodiscard]] QBindable<bool> bindableConnected() { return &this->bConnected; }
[[nodiscard]] QBindable<bool> bindablePaired() { return &this->bPaired; }
[[nodiscard]] QBindable<bool> bindableBonded() { return &this->bBonded; }
[[nodiscard]] QBindable<bool> bindableTrusted() { return &this->bTrusted; }
[[nodiscard]] QBindable<bool> bindableBlocked() { return &this->bBlocked; }
[[nodiscard]] QBindable<bool> bindableWakeAllowed() { return &this->bWakeAllowed; }
[[nodiscard]] QBindable<QString> bindableIcon() { return &this->bIcon; }
[[nodiscard]] QBindable<qreal> bindableBattery() { return &this->bBattery; }
[[nodiscard]] QBindable<BluetoothDeviceState::Enum> bindableState() { return &this->bState; }
void addInterface(const QString& interface, const QVariantMap& properties);
void removeInterface(const QString& interface);
signals:
void addressChanged();
void deviceNameChanged();
void nameChanged();
void connectedChanged();
void stateChanged();
void pairedChanged();
void bondedChanged();
void pairingChanged();
void trustedChanged();
void blockedChanged();
void wakeAllowedChanged();
void iconChanged();
void batteryAvailableChanged();
void batteryChanged();
void adapterChanged();
private:
void onConnectedChanged();
DBusBluezDeviceInterface* mInterface = nullptr;
QDBusInterface* mBatteryInterface = nullptr;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bAddress, &BluetoothDevice::addressChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bDeviceName, &BluetoothDevice::deviceNameChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bName, &BluetoothDevice::nameChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bConnected, &BluetoothDevice::onConnectedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPaired, &BluetoothDevice::pairedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBonded, &BluetoothDevice::bondedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bTrusted, &BluetoothDevice::trustedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBlocked, &BluetoothDevice::blockedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bWakeAllowed, &BluetoothDevice::wakeAllowedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bIcon, &BluetoothDevice::iconChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QDBusObjectPath, bAdapterPath);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, qreal, bBattery, &BluetoothDevice::batteryChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, BluetoothDeviceState::Enum, bState, &BluetoothDevice::stateChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPairing, &BluetoothDevice::pairingChanged);
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, properties);
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAddress, bAddress, properties, "Address");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pDeviceName, bDeviceName, properties, "Name");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pName, bName, properties, "Alias");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pConnected, bConnected, properties, "Connected");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pPaired, bPaired, properties, "Paired");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBonded, bBonded, properties, "Bonded");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pTrusted, bTrusted, properties, "Trusted");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBlocked, bBlocked, properties, "Blocked");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pWakeAllowed, bWakeAllowed, properties, "WakeAllowed");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pIcon, bIcon, properties, "Icon");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAdapterPath, bAdapterPath, properties, "Adapter");
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, batteryProperties);
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, BatteryPercentage, pBattery, bBattery, batteryProperties, "Percentage", true);
// clang-format on
};
} // namespace qs::bluetooth
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device);

View file

@ -1,12 +0,0 @@
name = "Quickshell.Bluetooth"
description = "Bluetooth API"
headers = [
"bluez.hpp",
"adapter.hpp",
"device.hpp",
]
-----
This module exposes Bluetooth management APIs provided by the BlueZ DBus interface.
Both DBus and BlueZ must be running to use it.
See the @@Quickshell.Bluetooth.Bluetooth singleton.

View file

@ -1,9 +0,0 @@
<node>
<interface name="org.bluez.Adapter1">
<method name="StartDiscovery"/>
<method name="StopDiscovery"/>
<method name="RemoveDevice">
<arg name="device" type="o"/>
</method>
</interface>
</node>

View file

@ -1,8 +0,0 @@
<node>
<interface name="org.bluez.Device1">
<method name="Connect"/>
<method name="Disconnect"/>
<method name="Pair"/>
<method name="CancelPairing"/>
</interface>
</node>

View file

@ -1,200 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Bluetooth
FloatingWindow {
color: contentItem.palette.window
ListView {
anchors.fill: parent
anchors.margins: 5
model: Bluetooth.adapters
delegate: WrapperRectangle {
width: parent.width
color: "transparent"
border.color: palette.button
border.width: 1
margin: 5
ColumnLayout {
Label { text: `Adapter: ${modelData.name} (${modelData.adapterId})` }
RowLayout {
Layout.fillWidth: true
CheckBox {
text: "Enable"
checked: modelData.enabled
onToggled: modelData.enabled = checked
}
Label {
color: modelData.state === BluetoothAdapterState.Blocked ? palette.errorText : palette.placeholderText
text: BluetoothAdapterState.toString(modelData.state)
}
CheckBox {
text: "Discoverable"
checked: modelData.discoverable
onToggled: modelData.discoverable = checked
}
CheckBox {
text: "Discovering"
checked: modelData.discovering
onToggled: modelData.discovering = checked
}
CheckBox {
text: "Pairable"
checked: modelData.pairable
onToggled: modelData.pairable = checked
}
}
RowLayout {
Layout.fillWidth: true
Label { text: "Discoverable timeout:" }
SpinBox {
from: 0
to: 3600
value: modelData.discoverableTimeout
onValueModified: modelData.discoverableTimeout = value
textFromValue: time => time === 0 ? "∞" : time + "s"
}
Label { text: "Pairable timeout:" }
SpinBox {
from: 0
to: 3600
value: modelData.pairableTimeout
onValueModified: modelData.pairableTimeout = value
textFromValue: time => time === 0 ? "∞" : time + "s"
}
}
Repeater {
model: modelData.devices
WrapperRectangle {
Layout.fillWidth: true
color: palette.button
border.color: palette.mid
border.width: 1
margin: 5
RowLayout {
ColumnLayout {
Layout.fillWidth: true
RowLayout {
IconImage {
Layout.fillHeight: true
implicitWidth: height
source: Quickshell.iconPath(modelData.icon)
}
TextField {
text: modelData.name
font.bold: true
background: null
readOnly: false
selectByMouse: true
onEditingFinished: modelData.name = text
}
Label {
visible: modelData.name && modelData.name !== modelData.deviceName
text: `(${modelData.deviceName})`
color: palette.placeholderText
}
}
RowLayout {
Label {
text: modelData.address
color: palette.placeholderText
}
Label {
visible: modelData.batteryAvailable
text: `| Battery: ${Math.round(modelData.battery * 100)}%`
color: palette.placeholderText
}
}
RowLayout {
Label {
text: BluetoothDeviceState.toString(modelData.state)
color: modelData.connected ? palette.link : palette.placeholderText
}
Label {
text: modelData.pairing ? "Pairing" : (modelData.paired ? "Paired" : "Not Paired")
color: modelData.paired || modelData.pairing ? palette.link : palette.placeholderText
}
Label {
visible: modelData.bonded
text: "| Bonded"
color: palette.link
}
CheckBox {
text: "Trusted"
checked: modelData.trusted
onToggled: modelData.trusted = checked
}
CheckBox {
text: "Blocked"
checked: modelData.blocked
onToggled: modelData.blocked = checked
}
CheckBox {
text: "Wake Allowed"
checked: modelData.wakeAllowed
onToggled: modelData.wakeAllowed = checked
}
}
}
ColumnLayout {
Layout.alignment: Qt.AlignRight
Button {
Layout.alignment: Qt.AlignRight
text: modelData.connected ? "Disconnect" : "Connect"
onClicked: modelData.connected = !modelData.connected
}
Button {
Layout.alignment: Qt.AlignRight
text: modelData.pairing ? "Cancel" : (modelData.paired ? "Forget" : "Pair")
onClicked: {
if (modelData.pairing) {
modelData.cancelPair();
} else if (modelData.paired) {
modelData.forget();
} else {
modelData.pair();
}
}
}
}
}
}
}
}
}
}
}

View file

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

View file

@ -1,17 +1,12 @@
#pragma once #pragma once
// NOLINTBEGIN // 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 GIT_REVISION "@GIT_REVISION@"
#define DISTRIBUTOR "@DISTRIBUTOR@" #define DISTRIBUTOR "@DISTRIBUTOR@"
#define CRASH_HANDLER @CRASH_HANDLER_DEF@ #define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@
#define CRASH_REPORTER @CRASH_REPORTER_DEF@
#define BUILD_TYPE "@CMAKE_BUILD_TYPE@" #define BUILD_TYPE "@CMAKE_BUILD_TYPE@"
#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)" #define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)"
#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@" #define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@"
#define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@" #define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@"
#define CRASHREPORT_URL "@CRASHREPORT_URL@"
// NOLINTEND // NOLINTEND

View file

@ -1,4 +1,3 @@
pkg_check_modules(libdrm REQUIRED IMPORTED_TARGET libdrm)
qt_add_library(quickshell-core STATIC qt_add_library(quickshell-core STATIC
plugin.cpp plugin.cpp
shell.cpp shell.cpp
@ -13,7 +12,6 @@ qt_add_library(quickshell-core STATIC
singleton.cpp singleton.cpp
generation.cpp generation.cpp
scan.cpp scan.cpp
scanenv.cpp
qsintercept.cpp qsintercept.cpp
incubator.cpp incubator.cpp
lazyloader.cpp lazyloader.cpp
@ -25,7 +23,7 @@ qt_add_library(quickshell-core STATIC
model.cpp model.cpp
elapsedtimer.cpp elapsedtimer.cpp
desktopentry.cpp desktopentry.cpp
desktopentrymonitor.cpp objectrepeater.cpp
platformmenu.cpp platformmenu.cpp
qsmenu.cpp qsmenu.cpp
retainable.cpp retainable.cpp
@ -40,9 +38,6 @@ qt_add_library(quickshell-core STATIC
iconprovider.cpp iconprovider.cpp
scriptmodel.cpp scriptmodel.cpp
colorquantizer.cpp colorquantizer.cpp
toolsupport.cpp
streamreader.cpp
debuginfo.cpp
) )
qt_add_qml_module(quickshell-core qt_add_qml_module(quickshell-core
@ -55,7 +50,7 @@ qt_add_qml_module(quickshell-core
install_qml_module(quickshell-core) install_qml_module(quickshell-core)
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build PkgConfig::libdrm) target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets)
qs_module_pch(quickshell-core SET large) qs_module_pch(quickshell-core SET large)

View file

@ -13,55 +13,39 @@
#include <qnumeric.h> #include <qnumeric.h>
#include <qobject.h> #include <qobject.h>
#include <qqmllist.h> #include <qqmllist.h>
#include <qrect.h>
#include <qrgb.h> #include <qrgb.h>
#include <qthreadpool.h> #include <qthreadpool.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h> #include <qtypes.h>
#include "logcat.hpp"
namespace { namespace {
QS_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg); Q_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg);
} }
ColorQuantizerOperation::ColorQuantizerOperation( ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize)
QUrl* source,
qreal depth,
QRect imageRect,
qreal rescaleSize
)
: source(source) : source(source)
, maxDepth(depth) , maxDepth(depth)
, imageRect(imageRect)
, rescaleSize(rescaleSize) { , rescaleSize(rescaleSize) {
this->setAutoDelete(false); setAutoDelete(false);
} }
void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) { void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) {
if (shouldCancel.loadAcquire() || this->source->isEmpty()) return; if (shouldCancel.loadAcquire() || source->isEmpty()) return;
this->colors.clear(); colors.clear();
auto image = QImage(this->source->toLocalFile()); auto image = QImage(source->toLocalFile());
if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) {
if (this->imageRect.isValid()) {
image = image.copy(this->imageRect);
}
if ((image.width() > this->rescaleSize || image.height() > this->rescaleSize)
&& this->rescaleSize > 0)
{
image = image.scaled( image = image.scaled(
static_cast<int>(this->rescaleSize), static_cast<int>(rescaleSize),
static_cast<int>(this->rescaleSize), static_cast<int>(rescaleSize),
Qt::KeepAspectRatio, Qt::KeepAspectRatio,
Qt::SmoothTransformation Qt::SmoothTransformation
); );
} }
if (image.isNull()) { if (image.isNull()) {
qCWarning(logColorQuantizer) << "Failed to load image from" << this->source->toString(); qCWarning(logColorQuantizer) << "Failed to load image from" << source;
return; return;
} }
@ -77,7 +61,7 @@ void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCa
auto startTime = QDateTime::currentDateTime(); auto startTime = QDateTime::currentDateTime();
this->colors = this->quantization(pixels, 0); colors = quantization(pixels, 0);
auto endTime = QDateTime::currentDateTime(); auto endTime = QDateTime::currentDateTime();
auto milliseconds = startTime.msecsTo(endTime); auto milliseconds = startTime.msecsTo(endTime);
@ -91,7 +75,7 @@ QList<QColor> ColorQuantizerOperation::quantization(
) { ) {
if (shouldCancel.loadAcquire()) return QList<QColor>(); if (shouldCancel.loadAcquire()) return QList<QColor>();
if (depth >= this->maxDepth || rgbValues.isEmpty()) { if (depth >= maxDepth || rgbValues.isEmpty()) {
if (rgbValues.isEmpty()) return QList<QColor>(); if (rgbValues.isEmpty()) return QList<QColor>();
auto totalR = 0; auto totalR = 0;
@ -128,8 +112,8 @@ QList<QColor> ColorQuantizerOperation::quantization(
auto rightHalf = rgbValues.mid(mid); auto rightHalf = rgbValues.mid(mid);
QList<QColor> result; QList<QColor> result;
result.append(this->quantization(leftHalf, depth + 1)); result.append(quantization(leftHalf, depth + 1));
result.append(this->quantization(rightHalf, depth + 1)); result.append(quantization(rightHalf, depth + 1));
return result; return result;
} }
@ -173,7 +157,7 @@ void ColorQuantizerOperation::finishRun() {
} }
void ColorQuantizerOperation::finished() { void ColorQuantizerOperation::finished() {
emit this->done(this->colors); emit this->done(colors);
delete this; delete this;
} }
@ -192,50 +176,39 @@ void ColorQuantizerOperation::run() {
void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); } void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); }
void ColorQuantizer::componentComplete() { void ColorQuantizer::componentComplete() {
this->componentCompleted = true; componentCompleted = true;
if (!this->mSource.isEmpty()) this->quantizeAsync(); if (!mSource.isEmpty()) quantizeAsync();
} }
void ColorQuantizer::setSource(const QUrl& source) { void ColorQuantizer::setSource(const QUrl& source) {
if (this->mSource != source) { if (mSource != source) {
this->mSource = source; mSource = source;
emit this->sourceChanged(); emit this->sourceChanged();
if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync(); if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync();
} }
} }
void ColorQuantizer::setDepth(qreal depth) { void ColorQuantizer::setDepth(qreal depth) {
if (this->mDepth != depth) { if (mDepth != depth) {
this->mDepth = depth; mDepth = depth;
emit this->depthChanged(); emit this->depthChanged();
if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync(); if (this->componentCompleted) 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) { void ColorQuantizer::setRescaleSize(int rescaleSize) {
if (this->mRescaleSize != rescaleSize) { if (mRescaleSize != rescaleSize) {
this->mRescaleSize = rescaleSize; mRescaleSize = rescaleSize;
emit this->rescaleSizeChanged(); emit this->rescaleSizeChanged();
if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync(); if (this->componentCompleted) quantizeAsync();
} }
} }
void ColorQuantizer::operationFinished(const QList<QColor>& result) { void ColorQuantizer::operationFinished(const QList<QColor>& result) {
this->bColors = result; bColors = result;
this->liveOperation = nullptr; this->liveOperation = nullptr;
emit this->colorsChanged(); emit this->colorsChanged();
} }
@ -244,13 +217,7 @@ void ColorQuantizer::quantizeAsync() {
if (this->liveOperation) this->cancelAsync(); if (this->liveOperation) this->cancelAsync();
qCDebug(logColorQuantizer) << "Starting color quantization asynchronously"; qCDebug(logColorQuantizer) << "Starting color quantization asynchronously";
this->liveOperation = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize);
this->liveOperation = new ColorQuantizerOperation(
&this->mSource,
this->mDepth,
this->mImageRect,
this->mRescaleSize
);
QObject::connect( QObject::connect(
this->liveOperation, this->liveOperation,

View file

@ -5,7 +5,6 @@
#include <qproperty.h> #include <qproperty.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qqmlparserstatus.h> #include <qqmlparserstatus.h>
#include <qrect.h>
#include <qrunnable.h> #include <qrunnable.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h> #include <qtypes.h>
@ -17,7 +16,7 @@ class ColorQuantizerOperation
Q_OBJECT; Q_OBJECT;
public: public:
explicit ColorQuantizerOperation(QUrl* source, qreal depth, QRect imageRect, qreal rescaleSize); explicit ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize);
void run() override; void run() override;
void tryCancel(); void tryCancel();
@ -45,7 +44,6 @@ private:
QList<QColor> colors; QList<QColor> colors;
QUrl* source; QUrl* source;
qreal maxDepth; qreal maxDepth;
QRect imageRect;
qreal rescaleSize; qreal rescaleSize;
}; };
@ -80,13 +78,6 @@ class ColorQuantizer
/// binary split of the color space /// binary split of the color space
Q_PROPERTY(qreal depth READ depth WRITE setDepth NOTIFY depthChanged); 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. /// 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 /// > [!NOTE] Results from color quantization doesn't suffer much when rescaling, it's
/// > reccommended to rescale, otherwise the quantization process will take much longer. /// > reccommended to rescale, otherwise the quantization process will take much longer.
@ -100,24 +91,19 @@ public:
[[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; } [[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; }
[[nodiscard]] QUrl source() const { return this->mSource; } [[nodiscard]] QUrl source() const { return mSource; }
void setSource(const QUrl& source); void setSource(const QUrl& source);
[[nodiscard]] qreal depth() const { return this->mDepth; } [[nodiscard]] qreal depth() const { return mDepth; }
void setDepth(qreal depth); void setDepth(qreal depth);
[[nodiscard]] QRect imageRect() const { return this->mImageRect; } [[nodiscard]] qreal rescaleSize() const { return mRescaleSize; }
void setImageRect(QRect imageRect);
void resetImageRect();
[[nodiscard]] qreal rescaleSize() const { return this->mRescaleSize; }
void setRescaleSize(int rescaleSize); void setRescaleSize(int rescaleSize);
signals: signals:
void colorsChanged(); void colorsChanged();
void sourceChanged(); void sourceChanged();
void depthChanged(); void depthChanged();
void imageRectChanged();
void rescaleSizeChanged(); void rescaleSizeChanged();
public slots: public slots:
@ -131,7 +117,6 @@ private:
ColorQuantizerOperation* liveOperation = nullptr; ColorQuantizerOperation* liveOperation = nullptr;
QUrl mSource; QUrl mSource;
qreal mDepth = 0; qreal mDepth = 0;
QRect mImageRect;
qreal mRescaleSize = 0; qreal mRescaleSize = 0;
Q_OBJECT_BINDABLE_PROPERTY( Q_OBJECT_BINDABLE_PROPERTY(

View file

@ -1,9 +1,11 @@
#include "common.hpp" #include "common.hpp"
#include <qdatetime.h> #include <qdatetime.h>
#include <qprocess.h>
namespace qs { namespace qs {
const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime(); const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime();
QProcessEnvironment Common::INITIAL_ENVIRONMENT = {}; // NOLINT
} // namespace qs } // namespace qs

View file

@ -7,7 +7,7 @@ namespace qs {
struct Common { struct Common {
static const QDateTime LAUNCH_TIME; static const QDateTime LAUNCH_TIME;
static inline QProcessEnvironment INITIAL_ENVIRONMENT = {}; // NOLINT static QProcessEnvironment INITIAL_ENVIRONMENT; // NOLINT
}; };
} // namespace qs } // namespace qs

View file

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

View file

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

View file

@ -1,33 +1,26 @@
#include "desktopentry.hpp" #include "desktopentry.hpp"
#include <algorithm>
#include <utility>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qdebug.h> #include <qdebug.h>
#include <qdir.h> #include <qdir.h>
#include <qfile.h>
#include <qfileinfo.h> #include <qfileinfo.h>
#include <qhash.h>
#include <qlist.h>
#include <qlogging.h> #include <qlogging.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qnamespace.h> #include <qnamespace.h>
#include <qobject.h> #include <qobject.h>
#include <qobjectdefs.h>
#include <qpair.h> #include <qpair.h>
#include <qproperty.h> #include <qprocess.h>
#include <qscopeguard.h> #include <qstringview.h>
#include <qtenvironmentvariables.h> #include <qtenvironmentvariables.h>
#include <qthreadpool.h>
#include <qtmetamacros.h>
#include <ranges> #include <ranges>
#include "../io/processcore.hpp" #include "common.hpp"
#include "desktopentrymonitor.hpp"
#include "logcat.hpp"
#include "model.hpp" #include "model.hpp"
#include "qmlglobal.hpp"
namespace { namespace {
QS_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg); Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
} }
struct Locale { struct Locale {
@ -61,14 +54,12 @@ struct Locale {
[[nodiscard]] int matchScore(const Locale& other) const { [[nodiscard]] int matchScore(const Locale& other) const {
if (this->language != other.language) return 0; if (this->language != other.language) return 0;
auto territoryMatches = !this->territory.isEmpty() && this->territory == other.territory;
if (!other.modifier.isEmpty() && this->modifier != other.modifier) return 0; auto modifierMatches = !this->modifier.isEmpty() && this->modifier == other.modifier;
if (!other.territory.isEmpty() && this->territory != other.territory) return 0;
auto score = 1; auto score = 1;
if (territoryMatches) score += 2;
if (!other.territory.isEmpty()) score += 2; if (modifierMatches) score += 1;
if (!other.modifier.isEmpty()) score += 1;
return score; return score;
} }
@ -94,64 +85,51 @@ struct Locale {
QDebug operator<<(QDebug debug, const Locale& locale) { QDebug operator<<(QDebug debug, const Locale& locale) {
auto saver = QDebugStateSaver(debug); auto saver = QDebugStateSaver(debug);
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
<< ", modifier=" << locale.modifier << ')'; << ", modifier" << locale.modifier << ')';
return debug; return debug;
} }
ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& text) { void DesktopEntry::parseEntry(const QString& text) {
ParsedDesktopEntryData data;
data.id = id;
const auto& system = Locale::system(); const auto& system = Locale::system();
auto groupName = QString(); auto groupName = QString();
auto entries = QHash<QString, QPair<Locale, QString>>(); auto entries = QHash<QString, QPair<Locale, QString>>();
auto actionOrder = QStringList(); auto finishCategory = [this, &groupName, &entries]() {
auto pendingActions = QHash<QString, DesktopActionData>();
auto finishCategory = [&data, &groupName, &entries, &actionOrder, &pendingActions]() {
if (groupName == "Desktop Entry") { if (groupName == "Desktop Entry") {
if (entries.value("Type").second != "Application") return; if (entries["Type"].second != "Application") return;
if (entries.contains("Hidden") && entries["Hidden"].second == "true") return;
for (const auto& [key, pair]: entries.asKeyValueRange()) { for (const auto& [key, pair]: entries.asKeyValueRange()) {
auto& [_, value] = pair; auto& [_, value] = pair;
data.entries.insert(key, value); this->mEntries.insert(key, value);
if (key == "Name") data.name = value; if (key == "Name") this->mName = value;
else if (key == "GenericName") data.genericName = value; else if (key == "GenericName") this->mGenericName = value;
else if (key == "StartupWMClass") data.startupClass = value; else if (key == "NoDisplay") this->mNoDisplay = value == "true";
else if (key == "NoDisplay") data.noDisplay = value == "true"; else if (key == "Comment") this->mComment = value;
else if (key == "Hidden") data.hidden = value == "true"; else if (key == "Icon") this->mIcon = value;
else if (key == "Comment") data.comment = value; else if (key == "Exec") this->mExecString = value;
else if (key == "Icon") data.icon = value; else if (key == "Path") this->mWorkingDirectory = value;
else if (key == "Exec") { else if (key == "Terminal") this->mTerminal = value == "true";
data.execString = value; else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts);
data.command = DesktopEntry::parseExecString(value); else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts);
} else if (key == "Path") data.workingDirectory = value;
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 ")) { } else if (groupName.startsWith("Desktop Action ")) {
auto actionName = groupName.sliced(15); auto actionName = groupName.sliced(16);
DesktopActionData action; auto* action = new DesktopAction(actionName, this);
action.id = actionName;
for (const auto& [key, pair]: entries.asKeyValueRange()) { for (const auto& [key, pair]: entries.asKeyValueRange()) {
const auto& [_, value] = pair; const auto& [_, value] = pair;
action.entries.insert(key, value); action->mEntries.insert(key, value);
if (key == "Name") action.name = value; if (key == "Name") action->mName = value;
else if (key == "Icon") action.icon = value; else if (key == "Icon") action->mIcon = value;
else if (key == "Exec") { else if (key == "Exec") action->mExecString = value;
action.execString = value;
action.command = DesktopEntry::parseExecString(value);
}
} }
pendingActions.insert(actionName, action); this->mActions.insert(actionName, action);
} }
entries.clear(); entries.clear();
@ -197,73 +175,16 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString&
} }
finishCategory(); finishCategory();
for (const auto& actionId: actionOrder) {
if (pendingActions.contains(actionId)) {
data.actions.append(pendingActions.value(actionId));
}
}
return data;
}
void DesktopEntry::updateState(const ParsedDesktopEntryData& newState) {
Qt::beginPropertyUpdateGroup();
this->bName = newState.name;
this->bGenericName = newState.genericName;
this->bStartupClass = newState.startupClass;
this->bNoDisplay = newState.noDisplay;
this->bComment = newState.comment;
this->bIcon = newState.icon;
this->bExecString = newState.execString;
this->bCommand = newState.command;
this->bWorkingDirectory = newState.workingDirectory;
this->bRunInTerminal = newState.terminal;
this->bCategories = newState.categories;
this->bKeywords = newState.keywords;
Qt::endPropertyUpdateGroup();
this->state = newState;
this->updateActions(newState.actions);
}
void DesktopEntry::updateActions(const QVector<DesktopActionData>& newActions) {
auto old = this->mActions;
this->mActions.clear();
for (const auto& d: newActions) {
DesktopAction* act = nullptr;
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);
}
Qt::beginPropertyUpdateGroup();
act->bName = d.name;
act->bIcon = d.icon;
act->bExecString = d.execString;
act->bCommand = d.command;
Qt::endPropertyUpdateGroup();
act->mEntries = d.entries;
this->mActions.append(act);
}
for (auto* leftover: old) {
leftover->deleteLater();
}
} }
void DesktopEntry::execute() const { void DesktopEntry::execute() const {
DesktopEntry::doExec(this->bCommand.value(), this->bWorkingDirectory.value()); DesktopEntry::doExec(this->mExecString, this->mWorkingDirectory);
} }
bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); } bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); }
bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions; } QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); }
QVector<QString> DesktopEntry::parseExecString(const QString& execString) { QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
QVector<QString> arguments; QVector<QString> arguments;
@ -282,22 +203,16 @@ QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
currentArgument += '\\'; currentArgument += '\\';
escape = 0; escape = 0;
} }
} else if (escape == 2) {
currentArgument += c;
escape = 0;
} else if (escape != 0) { } else if (escape != 0) {
switch (c.unicode()) { if (escape != 2) {
case 's': currentArgument += u' '; break; // Technically this is an illegal state, but the spec has a terrible double escape
case 'n': currentArgument += u'\n'; break; // rule in strings for no discernable reason. Assuming someone might understandably
case 't': currentArgument += u'\t'; break; // misunderstand it, treat it as a normal escape and log it.
case 'r': currentArgument += u'\r'; break;
case '\\': currentArgument += u'\\'; break;
default:
qCWarning(logDesktopEntry).noquote() qCWarning(logDesktopEntry).noquote()
<< "Illegal escape sequence in desktop entry exec string:" << execString; << "Illegal escape sequence in desktop entry exec string:" << execString;
currentArgument += c;
break;
} }
currentArgument += c;
escape = 0; escape = 0;
} else if (c == u'"' || c == u'\'') { } else if (c == u'"' || c == u'\'') {
parsingString = false; parsingString = false;
@ -335,52 +250,75 @@ QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
return arguments; return arguments;
} }
void DesktopEntry::doExec(const QList<QString>& execString, const QString& workingDirectory) { void DesktopEntry::doExec(const QString& execString, const QString& workingDirectory) {
qs::io::process::ProcessContext ctx; auto args = DesktopEntry::parseExecString(execString);
ctx.setCommand(execString); if (args.isEmpty()) {
ctx.setWorkingDirectory(workingDirectory); qCWarning(logDesktopEntry) << "Tried to exec string" << execString << "which parsed as empty.";
QuickshellGlobal::execDetached(ctx); return;
}
auto process = QProcess();
process.setProgram(args.at(0));
process.setArguments(args.sliced(1));
if (!workingDirectory.isEmpty()) process.setWorkingDirectory(workingDirectory);
process.setProcessEnvironment(qs::Common::INITIAL_ENVIRONMENT);
process.startDetached();
} }
void DesktopAction::execute() const { void DesktopAction::execute() const {
DesktopEntry::doExec(this->bCommand.value(), this->entry->bWorkingDirectory.value()); DesktopEntry::doExec(this->mExecString, this->entry->mWorkingDirectory);
} }
DesktopEntryScanner::DesktopEntryScanner(DesktopEntryManager* manager): manager(manager) { DesktopEntryManager::DesktopEntryManager() {
this->setAutoDelete(true); this->scanDesktopEntries();
this->populateApplications();
} }
void DesktopEntryScanner::run() { void DesktopEntryManager::scanDesktopEntries() {
const auto& desktopPaths = DesktopEntryManager::desktopPaths(); QList<QString> dataPaths;
auto scanResults = QList<ParsedDesktopEntryData>();
for (const auto& path: desktopPaths | std::views::reverse) { if (qEnvironmentVariableIsSet("XDG_DATA_HOME")) {
auto file = QFileInfo(path); dataPaths.push_back(qEnvironmentVariable("XDG_DATA_HOME"));
if (!file.isDir()) continue; } else if (qEnvironmentVariableIsSet("HOME")) {
dataPaths.push_back(qEnvironmentVariable("HOME") + "/.local/share");
this->scanDirectory(QDir(path), QString(), scanResults);
} }
QMetaObject::invokeMethod( if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) {
this->manager, auto var = qEnvironmentVariable("XDG_DATA_DIRS");
"onScanCompleted", dataPaths += var.split(u':', Qt::SkipEmptyParts);
Qt::QueuedConnection, } else {
Q_ARG(QList<ParsedDesktopEntryData>, scanResults) dataPaths.push_back("/usr/local/share");
); dataPaths.push_back("/usr/share");
} }
void DesktopEntryScanner::scanDirectory( qCDebug(logDesktopEntry) << "Creating desktop entry scanners";
const QDir& dir,
const QString& idPrefix,
QList<ParsedDesktopEntryData>& entries
) {
auto dirEntries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
for (auto& entry: dirEntries) { for (auto& path: std::ranges::reverse_view(dataPaths)) {
if (entry.isDir()) { auto p = QDir(path).filePath("applications");
auto subdirPrefix = idPrefix.isEmpty() ? entry.fileName() : idPrefix + '-' + entry.fileName(); auto file = QFileInfo(p);
this->scanDirectory(QDir(entry.absoluteFilePath()), subdirPrefix, entries);
} else if (entry.isFile()) { if (!file.isDir()) {
qCDebug(logDesktopEntry) << "Not scanning path" << p << "as it is not a directory";
continue;
}
qCDebug(logDesktopEntry) << "Scanning path" << p;
this->scanPath(p);
}
}
void DesktopEntryManager::populateApplications() {
for (auto& entry: this->desktopEntries.values()) {
if (!entry->noDisplay()) this->mApplications.insertObject(entry);
}
}
void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
for (auto& entry: entries) {
if (entry.isDir()) this->scanPath(entry.absoluteFilePath(), prefix + dir.dirName() + "-");
else if (entry.isFile()) {
auto path = entry.filePath(); auto path = entry.filePath();
if (!path.endsWith(".desktop")) { if (!path.endsWith(".desktop")) {
qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension"; qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
@ -393,40 +331,44 @@ void DesktopEntryScanner::scanDirectory(
continue; continue;
} }
auto basename = QFileInfo(entry.fileName()).completeBaseName(); auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8);
auto id = idPrefix.isEmpty() ? basename : idPrefix + '-' + basename; auto lowerId = id.toLower();
auto content = QString::fromUtf8(file.readAll());
auto data = DesktopEntry::parseText(id, content); auto text = QString::fromUtf8(file.readAll());
entries.append(std::move(data)); auto* dentry = new DesktopEntry(id, this);
} dentry->parseEntry(text);
}
if (!dentry->isValid()) {
qCDebug(logDesktopEntry) << "Skipping desktop entry" << path;
delete dentry;
continue;
} }
DesktopEntryManager::DesktopEntryManager(): monitor(new DesktopEntryMonitor(this)) { qCDebug(logDesktopEntry) << "Found desktop entry" << id << "at" << path;
QObject::connect(
this->monitor,
&DesktopEntryMonitor::desktopEntriesChanged,
this,
&DesktopEntryManager::handleFileChanges
);
DesktopEntryScanner(this).run(); auto conflictingId = this->desktopEntries.contains(id);
if (conflictingId) {
qCDebug(logDesktopEntry) << "Replacing old entry for" << id;
delete this->desktopEntries.value(id);
this->desktopEntries.remove(id);
this->lowercaseDesktopEntries.remove(lowerId);
} }
void DesktopEntryManager::scanDesktopEntries() { this->desktopEntries.insert(id, dentry);
qCDebug(logDesktopEntry) << "Starting desktop entry scan";
if (this->scanInProgress) { if (this->lowercaseDesktopEntries.contains(lowerId)) {
qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan"; qCInfo(logDesktopEntry).nospace()
this->scanQueued = true; << "Multiple desktop entries have the same lowercased id " << lowerId
return; << ". This can cause ambiguity when byId requests are not made with the correct case "
"already.";
this->lowercaseDesktopEntries.remove(lowerId);
} }
this->scanInProgress = true; this->lowercaseDesktopEntries.insert(lowerId, dentry);
this->scanQueued = false; }
auto* scanner = new DesktopEntryScanner(this); }
QThreadPool::globalInstance()->start(scanner);
} }
DesktopEntryManager* DesktopEntryManager::instance() { DesktopEntryManager* DesktopEntryManager::instance() {
@ -444,167 +386,14 @@ DesktopEntry* DesktopEntryManager::byId(const QString& id) {
} }
} }
DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) {
if (auto* entry = this->byId(name)) return entry;
auto list = this->desktopEntries.values();
auto iter = std::ranges::find_if(list, [&](DesktopEntry* entry) {
return name == entry->bStartupClass.value();
});
if (iter != list.end()) return *iter;
iter = std::ranges::find_if(list, [&](DesktopEntry* entry) {
return name.toLower() == entry->bStartupClass.value().toLower();
});
if (iter != list.end()) return *iter;
return nullptr;
}
ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; } ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
void DesktopEntryManager::handleFileChanges() { DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); }
qCDebug(logDesktopEntry) << "Directory change detected, performing full rescan";
if (this->scanInProgress) {
qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan";
this->scanQueued = true;
return;
}
this->scanInProgress = true;
this->scanQueued = false;
auto* scanner = new DesktopEntryScanner(this);
QThreadPool::globalInstance()->start(scanner);
}
const QStringList& DesktopEntryManager::desktopPaths() {
static const auto paths = []() {
auto dataPaths = QStringList();
auto dataHome = qEnvironmentVariable("XDG_DATA_HOME");
if (dataHome.isEmpty() && qEnvironmentVariableIsSet("HOME"))
dataHome = qEnvironmentVariable("HOME") + "/.local/share";
if (!dataHome.isEmpty()) dataPaths.append(dataHome + "/applications");
auto dataDirs = qEnvironmentVariable("XDG_DATA_DIRS");
if (dataDirs.isEmpty()) dataDirs = "/usr/local/share:/usr/share";
for (const auto& dir: dataDirs.split(':', Qt::SkipEmptyParts)) {
dataPaths.append(dir + "/applications");
}
return dataPaths;
}();
return paths;
}
void DesktopEntryManager::onScanCompleted(const QList<ParsedDesktopEntryData>& scanResults) {
auto guard = qScopeGuard([this] {
this->scanInProgress = false;
if (this->scanQueued) {
this->scanQueued = false;
this->scanDesktopEntries();
}
});
auto oldEntries = this->desktopEntries;
auto newEntries = QHash<QString, DesktopEntry*>();
auto newLowercaseEntries = QHash<QString, DesktopEntry*>();
for (const auto& data: scanResults) {
auto lowerId = data.id.toLower();
if (data.hidden) {
if (auto* victim = newEntries.take(data.id)) victim->deleteLater();
newLowercaseEntries.remove(lowerId);
if (auto it = oldEntries.find(data.id); it != oldEntries.end()) {
it.value()->deleteLater();
oldEntries.erase(it);
}
qCDebug(logDesktopEntry) << "Masking hidden desktop entry" << data.id;
continue;
}
DesktopEntry* dentry = nullptr;
if (auto it = oldEntries.find(data.id); it != oldEntries.end()) {
dentry = it.value();
oldEntries.erase(it);
dentry->updateState(data);
} else {
dentry = new DesktopEntry(data.id, this);
dentry->updateState(data);
}
if (!dentry->isValid()) {
qCDebug(logDesktopEntry) << "Skipping desktop entry" << data.id;
if (!oldEntries.contains(data.id)) {
dentry->deleteLater();
}
continue;
}
qCDebug(logDesktopEntry) << "Found desktop entry" << data.id;
auto conflictingId = newEntries.contains(data.id);
if (conflictingId) {
qCDebug(logDesktopEntry) << "Replacing old entry for" << data.id;
if (auto* victim = newEntries.take(data.id)) victim->deleteLater();
newLowercaseEntries.remove(lowerId);
}
newEntries.insert(data.id, dentry);
if (newLowercaseEntries.contains(lowerId)) {
qCInfo(logDesktopEntry).nospace()
<< "Multiple desktop entries have the same lowercased id " << lowerId
<< ". This can cause ambiguity when byId requests are not made with the correct case "
"already.";
newLowercaseEntries.remove(lowerId);
}
newLowercaseEntries.insert(lowerId, dentry);
}
this->desktopEntries = newEntries;
this->lowercaseDesktopEntries = newLowercaseEntries;
auto newApplications = QVector<DesktopEntry*>();
for (auto* entry: this->desktopEntries.values())
if (!entry->bNoDisplay) newApplications.append(entry);
this->mApplications.diffUpdate(newApplications);
emit this->applicationsChanged();
for (auto* e: oldEntries) e->deleteLater();
}
DesktopEntries::DesktopEntries() {
QObject::connect(
DesktopEntryManager::instance(),
&DesktopEntryManager::applicationsChanged,
this,
&DesktopEntries::applicationsChanged
);
}
DesktopEntry* DesktopEntries::byId(const QString& id) { DesktopEntry* DesktopEntries::byId(const QString& id) {
return DesktopEntryManager::instance()->byId(id); return DesktopEntryManager::instance()->byId(id);
} }
DesktopEntry* DesktopEntries::heuristicLookup(const QString& name) {
return DesktopEntryManager::instance()->heuristicLookup(name);
}
ObjectModel<DesktopEntry>* DesktopEntries::applications() { ObjectModel<DesktopEntry>* DesktopEntries::applications() {
return DesktopEntryManager::instance()->applications(); return DesktopEntryManager::instance()->applications();
} }

View file

@ -6,84 +6,36 @@
#include <qdir.h> #include <qdir.h>
#include <qhash.h> #include <qhash.h>
#include <qobject.h> #include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qrunnable.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include "desktopentrymonitor.hpp"
#include "doc.hpp" #include "doc.hpp"
#include "model.hpp" #include "model.hpp"
class DesktopAction; class DesktopAction;
class DesktopEntryMonitor;
struct DesktopActionData {
QString id;
QString name;
QString icon;
QString execString;
QVector<QString> command;
QHash<QString, QString> entries;
};
struct ParsedDesktopEntryData {
QString id;
QString name;
QString genericName;
QString startupClass;
bool noDisplay = false;
bool hidden = false;
QString comment;
QString icon;
QString execString;
QVector<QString> command;
QString workingDirectory;
bool terminal = false;
QVector<QString> categories;
QVector<QString> keywords;
QHash<QString, QString> entries;
QVector<DesktopActionData> actions;
};
/// A desktop entry. See @@DesktopEntries for details. /// A desktop entry. See @@DesktopEntries for details.
class DesktopEntry: public QObject { class DesktopEntry: public QObject {
Q_OBJECT; Q_OBJECT;
Q_PROPERTY(QString id MEMBER mId CONSTANT); Q_PROPERTY(QString id MEMBER mId CONSTANT);
/// Name of the specific application, such as "Firefox". /// Name of the specific application, such as "Firefox".
// clang-format off Q_PROPERTY(QString name MEMBER mName CONSTANT);
Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName);
/// Short description of the application, such as "Web Browser". May be empty. /// Short description of the application, such as "Web Browser". May be empty.
Q_PROPERTY(QString genericName READ default WRITE default NOTIFY genericNameChanged BINDABLE bindableGenericName); Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT);
/// Initial class or app id the app intends to use. May be useful for matching running apps
/// to desktop entries.
Q_PROPERTY(QString startupClass READ default WRITE default NOTIFY startupClassChanged BINDABLE bindableStartupClass);
/// If true, this application should not be displayed in menus and launchers. /// If true, this application should not be displayed in menus and launchers.
Q_PROPERTY(bool noDisplay READ default WRITE default NOTIFY noDisplayChanged BINDABLE bindableNoDisplay); Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT);
/// Long description of the application, such as "View websites on the internet". May be empty. /// Long description of the application, such as "View websites on the internet". May be empty.
Q_PROPERTY(QString comment READ default WRITE default NOTIFY commentChanged BINDABLE bindableComment); Q_PROPERTY(QString comment MEMBER mComment CONSTANT);
/// Name of the icon associated with this application. May be empty. /// Name of the icon associated with this application. May be empty.
Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon); Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
/// The raw `Exec` string from the desktop entry. /// The raw `Exec` string from the desktop entry. You probably want @@execute().
/// Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString);
/// The parsed `Exec` command in the desktop entry.
///
/// The entry can be run with @@execute(), or by using this command in
/// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process.
/// If used in `execDetached` or a `Process`, @@workingDirectory should also be passed to
/// the invoked process. See @@execute() for details.
///
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
Q_PROPERTY(QVector<QString> command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand);
/// The working directory to execute from. /// The working directory to execute from.
Q_PROPERTY(QString workingDirectory READ default WRITE default NOTIFY workingDirectoryChanged BINDABLE bindableWorkingDirectory); Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT);
/// If the application should run in a terminal. /// If the application should run in a terminal.
Q_PROPERTY(bool runInTerminal READ default WRITE default NOTIFY runInTerminalChanged BINDABLE bindableRunInTerminal); Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT);
Q_PROPERTY(QVector<QString> categories READ default WRITE default NOTIFY categoriesChanged BINDABLE bindableCategories); Q_PROPERTY(QVector<QString> categories MEMBER mCategories CONSTANT);
Q_PROPERTY(QVector<QString> keywords READ default WRITE default NOTIFY keywordsChanged BINDABLE bindableKeywords); Q_PROPERTY(QVector<QString> keywords MEMBER mKeywords CONSTANT);
// clang-format on
Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT); Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT);
QML_ELEMENT; QML_ELEMENT;
QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries"); QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
@ -91,83 +43,35 @@ class DesktopEntry: public QObject {
public: public:
explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {} explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
static ParsedDesktopEntryData parseText(const QString& id, const QString& text); void parseEntry(const QString& text);
void updateState(const ParsedDesktopEntryData& newState);
/// Run the application. Currently ignores @@runInTerminal and field codes. /// Run the application. Currently ignores @@runInTerminal and field codes.
///
/// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command
/// and @@DesktopEntry.workingDirectory as shown below:
///
/// ```qml
/// Quickshell.execDetached({
/// command: desktopEntry.command,
/// workingDirectory: desktopEntry.workingDirectory,
/// });
/// ```
Q_INVOKABLE void execute() const; Q_INVOKABLE void execute() const;
[[nodiscard]] bool isValid() const; [[nodiscard]] bool isValid() const;
[[nodiscard]] bool noDisplay() const;
[[nodiscard]] QVector<DesktopAction*> actions() const; [[nodiscard]] QVector<DesktopAction*> actions() const;
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
[[nodiscard]] QBindable<QString> bindableGenericName() const { return &this->bGenericName; }
[[nodiscard]] QBindable<QString> bindableStartupClass() const { return &this->bStartupClass; }
[[nodiscard]] QBindable<bool> bindableNoDisplay() const { return &this->bNoDisplay; }
[[nodiscard]] QBindable<QString> bindableComment() const { return &this->bComment; }
[[nodiscard]] QBindable<QString> bindableIcon() const { return &this->bIcon; }
[[nodiscard]] QBindable<QString> bindableExecString() const { return &this->bExecString; }
[[nodiscard]] QBindable<QVector<QString>> bindableCommand() const { return &this->bCommand; }
[[nodiscard]] QBindable<QString> bindableWorkingDirectory() const {
return &this->bWorkingDirectory;
}
[[nodiscard]] QBindable<bool> bindableRunInTerminal() const { return &this->bRunInTerminal; }
[[nodiscard]] QBindable<QVector<QString>> bindableCategories() const {
return &this->bCategories;
}
[[nodiscard]] QBindable<QVector<QString>> bindableKeywords() const { return &this->bKeywords; }
// currently ignores all field codes. // currently ignores all field codes.
static QVector<QString> parseExecString(const QString& execString); static QVector<QString> parseExecString(const QString& execString);
static void doExec(const QList<QString>& execString, const QString& workingDirectory); static void doExec(const QString& execString, const QString& workingDirectory);
signals:
void nameChanged();
void genericNameChanged();
void startupClassChanged();
void noDisplayChanged();
void commentChanged();
void iconChanged();
void execStringChanged();
void commandChanged();
void workingDirectoryChanged();
void runInTerminalChanged();
void categoriesChanged();
void keywordsChanged();
public: public:
QString mId; QString mId;
QString mName;
// clang-format off QString mGenericName;
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bName, &DesktopEntry::nameChanged); bool mNoDisplay = false;
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bGenericName, &DesktopEntry::genericNameChanged); QString mComment;
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bStartupClass, &DesktopEntry::startupClassChanged); QString mIcon;
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bNoDisplay, &DesktopEntry::noDisplayChanged); QString mExecString;
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bComment, &DesktopEntry::commentChanged); QString mWorkingDirectory;
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bIcon, &DesktopEntry::iconChanged); bool mTerminal = false;
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bExecString, &DesktopEntry::execStringChanged); QVector<QString> mCategories;
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bCommand, &DesktopEntry::commandChanged); QVector<QString> mKeywords;
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bWorkingDirectory, &DesktopEntry::workingDirectoryChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bRunInTerminal, &DesktopEntry::runInTerminalChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bCategories, &DesktopEntry::categoriesChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bKeywords, &DesktopEntry::keywordsChanged);
// clang-format on
private: private:
void updateActions(const QVector<DesktopActionData>& newActions); QHash<QString, QString> mEntries;
QHash<QString, DesktopAction*> mActions;
ParsedDesktopEntryData state;
QVector<DesktopAction*> mActions;
friend class DesktopAction; friend class DesktopAction;
}; };
@ -176,23 +80,10 @@ private:
class DesktopAction: public QObject { class DesktopAction: public QObject {
Q_OBJECT; Q_OBJECT;
Q_PROPERTY(QString id MEMBER mId CONSTANT); Q_PROPERTY(QString id MEMBER mId CONSTANT);
// clang-format off Q_PROPERTY(QString name MEMBER mName CONSTANT);
Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName); Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon); /// The raw `Exec` string from the desktop entry. You probably want @@execute().
/// The raw `Exec` string from the action. Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
///
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString);
/// The parsed `Exec` command in the action.
///
/// The entry can be run with @@execute(), or by using this command in
/// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process.
/// If used in `execDetached` or a `Process`, @@DesktopEntry.workingDirectory should also be passed to
/// the invoked process.
///
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
Q_PROPERTY(QVector<QString> command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand);
// clang-format on
QML_ELEMENT; QML_ELEMENT;
QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry"); QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
@ -203,52 +94,19 @@ public:
, mId(std::move(id)) {} , mId(std::move(id)) {}
/// Run the application. Currently ignores @@DesktopEntry.runInTerminal and field codes. /// Run the application. Currently ignores @@DesktopEntry.runInTerminal and field codes.
///
/// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command
/// and @@DesktopEntry.workingDirectory.
Q_INVOKABLE void execute() const; Q_INVOKABLE void execute() const;
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
[[nodiscard]] QBindable<QString> bindableIcon() const { return &this->bIcon; }
[[nodiscard]] QBindable<QString> bindableExecString() const { return &this->bExecString; }
[[nodiscard]] QBindable<QVector<QString>> bindableCommand() const { return &this->bCommand; }
signals:
void nameChanged();
void iconChanged();
void execStringChanged();
void commandChanged();
private: private:
DesktopEntry* entry; DesktopEntry* entry;
QString mId; QString mId;
QString mName;
QString mIcon;
QString mExecString;
QHash<QString, QString> mEntries; QHash<QString, QString> mEntries;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bName, &DesktopAction::nameChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bIcon, &DesktopAction::iconChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bExecString, &DesktopAction::execStringChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QVector<QString>, bCommand, &DesktopAction::commandChanged);
// clang-format on
friend class DesktopEntry; friend class DesktopEntry;
}; };
class DesktopEntryManager;
class DesktopEntryScanner: public QRunnable {
public:
explicit DesktopEntryScanner(DesktopEntryManager* manager);
void run() override;
// clang-format off
void scanDirectory(const QDir& dir, const QString& idPrefix, QList<ParsedDesktopEntryData>& entries);
// clang-format on
private:
DesktopEntryManager* manager;
};
class DesktopEntryManager: public QObject { class DesktopEntryManager: public QObject {
Q_OBJECT; Q_OBJECT;
@ -256,32 +114,20 @@ public:
void scanDesktopEntries(); void scanDesktopEntries();
[[nodiscard]] DesktopEntry* byId(const QString& id); [[nodiscard]] DesktopEntry* byId(const QString& id);
[[nodiscard]] DesktopEntry* heuristicLookup(const QString& name);
[[nodiscard]] ObjectModel<DesktopEntry>* applications(); [[nodiscard]] ObjectModel<DesktopEntry>* applications();
static DesktopEntryManager* instance(); static DesktopEntryManager* instance();
static const QStringList& desktopPaths();
signals:
void applicationsChanged();
private slots:
void handleFileChanges();
void onScanCompleted(const QList<ParsedDesktopEntryData>& scanResults);
private: private:
explicit DesktopEntryManager(); explicit DesktopEntryManager();
void populateApplications();
void scanPath(const QDir& dir, const QString& prefix = QString());
QHash<QString, DesktopEntry*> desktopEntries; QHash<QString, DesktopEntry*> desktopEntries;
QHash<QString, DesktopEntry*> lowercaseDesktopEntries; QHash<QString, DesktopEntry*> lowercaseDesktopEntries;
ObjectModel<DesktopEntry> mApplications {this}; ObjectModel<DesktopEntry> mApplications {this};
DesktopEntryMonitor* monitor = nullptr;
bool scanInProgress = false;
bool scanQueued = false;
friend class DesktopEntryScanner;
}; };
///! Desktop entry index. ///! Desktop entry index.
@ -303,17 +149,7 @@ public:
explicit DesktopEntries(); explicit DesktopEntries();
/// Look up a desktop entry by name. Includes NoDisplay entries. May return null. /// Look up a desktop entry by name. Includes NoDisplay entries. May return null.
///
/// While this function requires an exact match, @@heuristicLookup() will correctly
/// find an entry more often and is generally more useful.
Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id); Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id);
/// Look up a desktop entry by name using heuristics. Unlike @@byId(),
/// if no exact matches are found this function will try to guess - potentially incorrectly.
/// May return null.
Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name);
[[nodiscard]] static ObjectModel<DesktopEntry>* applications(); [[nodiscard]] static ObjectModel<DesktopEntry>* applications();
signals:
void applicationsChanged();
}; };

View file

@ -1,68 +0,0 @@
#include "desktopentrymonitor.hpp"
#include <qdir.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qobject.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include "desktopentry.hpp"
namespace {
void addPathAndParents(QFileSystemWatcher& watcher, const QString& path) {
watcher.addPath(path);
auto p = QFileInfo(path).absolutePath();
while (!p.isEmpty()) {
watcher.addPath(p);
const auto parent = QFileInfo(p).dir().absolutePath();
if (parent == p) break;
p = parent;
}
}
} // namespace
DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) {
this->debounceTimer.setSingleShot(true);
this->debounceTimer.setInterval(100);
QObject::connect(
&this->watcher,
&QFileSystemWatcher::directoryChanged,
this,
&DesktopEntryMonitor::onDirectoryChanged
);
QObject::connect(
&this->debounceTimer,
&QTimer::timeout,
this,
&DesktopEntryMonitor::processChanges
);
this->startMonitoring();
}
void DesktopEntryMonitor::startMonitoring() {
for (const auto& path: DesktopEntryManager::desktopPaths()) {
if (!QDir(path).exists()) continue;
addPathAndParents(this->watcher, path);
this->scanAndWatch(path);
}
}
void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) {
auto dir = QDir(dirPath);
if (!dir.exists()) return;
this->watcher.addPath(dirPath);
auto subdirs = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
for (const auto& subdir: subdirs) this->watcher.addPath(subdir.absoluteFilePath());
}
void DesktopEntryMonitor::onDirectoryChanged(const QString& /*path*/) {
this->debounceTimer.start();
}
void DesktopEntryMonitor::processChanges() { emit this->desktopEntriesChanged(); }

View file

@ -1,32 +0,0 @@
#pragma once
#include <qfilesystemwatcher.h>
#include <qobject.h>
#include <qstringlist.h>
#include <qtimer.h>
class DesktopEntryMonitor: public QObject {
Q_OBJECT
public:
explicit DesktopEntryMonitor(QObject* parent = nullptr);
~DesktopEntryMonitor() override = default;
DesktopEntryMonitor(const DesktopEntryMonitor&) = delete;
DesktopEntryMonitor& operator=(const DesktopEntryMonitor&) = delete;
DesktopEntryMonitor(DesktopEntryMonitor&&) = delete;
DesktopEntryMonitor& operator=(DesktopEntryMonitor&&) = delete;
signals:
void desktopEntriesChanged();
private slots:
void onDirectoryChanged(const QString& path);
void processChanges();
private:
void startMonitoring();
void scanAndWatch(const QString& dirPath);
QFileSystemWatcher watcher;
QTimer debounceTimer;
};

View file

@ -16,20 +16,18 @@
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qqmlerror.h> #include <qqmlerror.h>
#include <qqmlincubator.h> #include <qqmlincubator.h>
#include <qquickwindow.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include "iconimageprovider.hpp" #include "iconimageprovider.hpp"
#include "imageprovider.hpp" #include "imageprovider.hpp"
#include "incubator.hpp" #include "incubator.hpp"
#include "logcat.hpp"
#include "plugin.hpp" #include "plugin.hpp"
#include "qsintercept.hpp" #include "qsintercept.hpp"
#include "reload.hpp" #include "reload.hpp"
#include "scan.hpp" #include "scan.hpp"
namespace { namespace {
QS_LOGGING_CATEGORY(logScene, "scene"); Q_LOGGING_CATEGORY(logScene, "scene");
} }
static QHash<const QQmlEngine*, EngineGeneration*> g_generations; // NOLINT static QHash<const QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
@ -38,7 +36,7 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
: rootPath(rootPath) : rootPath(rootPath)
, scanner(std::move(scanner)) , scanner(std::move(scanner))
, urlInterceptor(this->rootPath) , urlInterceptor(this->rootPath)
, interceptNetFactory(this->rootPath, this->scanner.fileIntercepts) , interceptNetFactory(this->scanner.fileIntercepts)
, engine(new QQmlEngine()) { , engine(new QQmlEngine()) {
g_generations.insert(this->engine, this); g_generations.insert(this->engine, this);
@ -46,11 +44,8 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
QObject::connect(this->engine, &QQmlEngine::warnings, this, &EngineGeneration::onEngineWarnings); QObject::connect(this->engine, &QQmlEngine::warnings, this, &EngineGeneration::onEngineWarnings);
this->engine->addUrlInterceptor(&this->urlInterceptor); this->engine->addUrlInterceptor(&this->urlInterceptor);
this->engine->addImportPath("qs:@/");
this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory); this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory);
this->incubationController.initLoop(); this->engine->setIncubationController(&this->delayedIncubationController);
this->engine->setIncubationController(&this->incubationController);
this->engine->addImageProvider("icon", new IconImageProvider()); this->engine->addImageProvider("icon", new IconImageProvider());
this->engine->addImageProvider("qsimage", new QsImageProvider()); this->engine->addImageProvider("qsimage", new QsImageProvider());
@ -135,7 +130,7 @@ void EngineGeneration::onReload(EngineGeneration* old) {
// new generation acquires it then incubators will hang intermittently // new generation acquires it then incubators will hang intermittently
qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old; qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old;
old->incubationControllersLocked = true; old->incubationControllersLocked = true;
old->updateIncubationMode(); old->assignIncubationController();
} }
QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit); QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit);
@ -162,9 +157,8 @@ void EngineGeneration::postReload() {
if (this->engine == nullptr || this->root == nullptr) return; if (this->engine == nullptr || this->root == nullptr) return;
QsEnginePlugin::runOnReload(); QsEnginePlugin::runOnReload();
PostReloadHook::postReloadTree(this->root);
emit this->firePostReload(); this->singletonRegistry.onPostReload();
QObject::disconnect(this, &EngineGeneration::firePostReload, nullptr, nullptr);
} }
void EngineGeneration::setWatchingFiles(bool watching) { void EngineGeneration::setWatchingFiles(bool watching) {
@ -209,8 +203,6 @@ bool EngineGeneration::setExtraWatchedFiles(const QVector<QString>& files) {
for (const auto& file: files) { for (const auto& file: files) {
if (!this->scanner.scannedFiles.contains(file)) { if (!this->scanner.scannedFiles.contains(file)) {
this->extraWatchedFiles.append(file); this->extraWatchedFiles.append(file);
QByteArray data;
this->scanner.readAndHashFile(file, data);
} }
} }
@ -226,16 +218,6 @@ void EngineGeneration::onFileChanged(const QString& name) {
if (!this->watcher->files().contains(name)) { if (!this->watcher->files().contains(name)) {
this->deletedWatchedFiles.push_back(name); this->deletedWatchedFiles.push_back(name);
} else { } else {
// some editors (e.g vscode) perform file saving in two steps: truncate + write
// ignore the first event (truncate) with size 0 to prevent incorrect live reloading
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(); emit this->filesChanged();
} }
} }
@ -244,22 +226,104 @@ void EngineGeneration::onDirectoryChanged() {
// try to find any files that were just deleted from a replace operation // try to find any files that were just deleted from a replace operation
for (auto& file: this->deletedWatchedFiles) { for (auto& file: this->deletedWatchedFiles) {
if (QFileInfo(file).exists()) { if (QFileInfo(file).exists()) {
if (!this->scanner.hasFileContentChanged(file)) {
qCDebug(logQmlScanner) << "Ignoring restored file with unchanged content:" << file;
continue;
}
emit this->filesChanged(); emit this->filesChanged();
break; break;
} }
} }
} }
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) { void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) {
// We only want controllers that we can swap out if destroyed.
// This happens if the window owning the active controller dies.
if (auto* obj = dynamic_cast<QObject*>(controller)) {
QObject::connect(
obj,
&QObject::destroyed,
this,
&EngineGeneration::incubationControllerDestroyed
);
} else {
qCWarning(logIncubator) << "Could not register incubation controller as it is not a QObject"
<< controller;
return;
}
this->incubationControllers.push_back(controller);
qCDebug(logIncubator) << "Registered incubation controller" << controller << "to generation"
<< this;
// This function can run during destruction.
if (this->engine == nullptr) return;
if (this->engine->incubationController() == &this->delayedIncubationController) {
this->assignIncubationController();
}
}
void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) {
if (auto* obj = dynamic_cast<QObject*>(controller)) {
QObject::disconnect(obj, nullptr, this, nullptr);
} else {
qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, "
"however only QObject controllers should be registered.";
}
if (!this->incubationControllers.removeOne(controller)) {
qCCritical(logIncubator) << "Failed to deregister incubation controller" << controller << "from"
<< this << "as it was not registered to begin with";
qCCritical(logIncubator) << "Current registered incuabation controllers"
<< this->incubationControllers;
} else {
qCDebug(logIncubator) << "Deregistered incubation controller" << controller << "from" << this;
}
// This function can run during destruction.
if (this->engine == nullptr) return;
if (this->engine->incubationController() == controller) {
qCDebug(logIncubator
) << "Destroyed incubation controller was currently active, reassigning from pool";
this->assignIncubationController();
}
}
void EngineGeneration::incubationControllerDestroyed() {
auto* sender = this->sender();
auto* controller = dynamic_cast<QQmlIncubationController*>(sender);
if (controller == nullptr) {
qCCritical(logIncubator) << "Destroyed incubation controller" << sender << "is not known to"
<< this << ", this may cause memory corruption";
qCCritical(logIncubator) << "Current registered incuabation controllers"
<< this->incubationControllers;
return;
}
if (this->incubationControllers.removeOne(controller)) {
qCDebug(logIncubator) << "Destroyed incubation controller" << controller << "deregistered from"
<< this;
} else {
qCCritical(logIncubator) << "Destroyed incubation controller" << controller
<< "was not registered, but its destruction was observed by" << this;
return;
}
// This function can run during destruction.
if (this->engine == nullptr) return;
if (this->engine->incubationController() == controller) {
qCDebug(logIncubator
) << "Destroyed incubation controller was currently active, reassigning from pool";
this->assignIncubationController();
}
}
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) const {
for (const auto& error: warnings) { for (const auto& error: warnings) {
const auto& url = error.url(); auto rel = "**/" % this->rootPath.relativeFilePath(error.url().path());
auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5)
: url.toString();
QString objectName; QString objectName;
auto desc = error.description(); auto desc = error.description();
@ -296,23 +360,20 @@ void EngineGeneration::exit(int code) {
this->destroy(); this->destroy();
} }
void EngineGeneration::trackWindowIncubationController(QQuickWindow* window) { void EngineGeneration::assignIncubationController() {
if (this->trackedWindows.contains(window)) return; QQmlIncubationController* controller = nullptr;
QObject::connect(window, &QObject::destroyed, this, &EngineGeneration::onTrackedWindowDestroyed); if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) {
this->trackedWindows.append(window); controller = &this->delayedIncubationController;
this->updateIncubationMode(); } else {
controller = this->incubationControllers.first();
} }
void EngineGeneration::onTrackedWindowDestroyed(QObject* object) { qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"
this->trackedWindows.removeAll(static_cast<QQuickWindow*>(object)); // NOLINT << this
this->updateIncubationMode(); << "fallback:" << (controller == &this->delayedIncubationController);
}
void EngineGeneration::updateIncubationMode() { this->engine->setIncubationController(controller);
// 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() { EngineGeneration* EngineGeneration::currentGeneration() {

View file

@ -9,7 +9,6 @@
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qqmlerror.h> #include <qqmlerror.h>
#include <qqmlincubator.h> #include <qqmlincubator.h>
#include <qquickwindow.h>
#include <qtclasshelpermacros.h> #include <qtclasshelpermacros.h>
#include "incubator.hpp" #include "incubator.hpp"
@ -41,7 +40,8 @@ public:
void setWatchingFiles(bool watching); void setWatchingFiles(bool watching);
bool setExtraWatchedFiles(const QVector<QString>& files); bool setExtraWatchedFiles(const QVector<QString>& files);
void trackWindowIncubationController(QQuickWindow* window); void registerIncubationController(QQmlIncubationController* controller);
void deregisterIncubationController(QQmlIncubationController* controller);
// takes ownership // takes ownership
void registerExtension(const void* key, EngineGenerationExt* extension); void registerExtension(const void* key, EngineGenerationExt* extension);
@ -65,7 +65,7 @@ public:
QFileSystemWatcher* watcher = nullptr; QFileSystemWatcher* watcher = nullptr;
QVector<QString> deletedWatchedFiles; QVector<QString> deletedWatchedFiles;
QVector<QString> extraWatchedFiles; QVector<QString> extraWatchedFiles;
QsIncubationController incubationController; DelayedQmlIncubationController delayedIncubationController;
bool reloadComplete = false; bool reloadComplete = false;
QuickshellGlobal* qsgInstance = nullptr; QuickshellGlobal* qsgInstance = nullptr;
@ -75,7 +75,6 @@ public:
signals: signals:
void filesChanged(); void filesChanged();
void reloadFinished(); void reloadFinished();
void firePostReload();
public slots: public slots:
void quit(); void quit();
@ -84,13 +83,13 @@ public slots:
private slots: private slots:
void onFileChanged(const QString& name); void onFileChanged(const QString& name);
void onDirectoryChanged(); void onDirectoryChanged();
void onTrackedWindowDestroyed(QObject* object); void incubationControllerDestroyed();
static void onEngineWarnings(const QList<QQmlError>& warnings); void onEngineWarnings(const QList<QQmlError>& warnings) const;
private: private:
void postReload(); void postReload();
void updateIncubationMode(); void assignIncubationController();
QVector<QQuickWindow*> trackedWindows; QVector<QQmlIncubationController*> incubationControllers;
bool incubationControllersLocked = false; bool incubationControllersLocked = false;
QHash<const void*, EngineGenerationExt*> extensions; QHash<const void*, EngineGenerationExt*> extensions;

View file

@ -19,7 +19,8 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
if (splitIdx != -1) { if (splitIdx != -1) {
iconName = id.sliced(0, splitIdx); iconName = id.sliced(0, splitIdx);
path = id.sliced(splitIdx + 6); path = id.sliced(splitIdx + 6);
path = QString("/%1/%2").arg(path, iconName.sliced(iconName.lastIndexOf('/') + 1)); qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for"
<< id;
} else { } else {
splitIdx = id.indexOf("?fallback="); splitIdx = id.indexOf("?fallback=");
if (splitIdx != -1) { if (splitIdx != -1) {
@ -31,8 +32,7 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
} }
auto icon = QIcon::fromTheme(iconName); auto icon = QIcon::fromTheme(iconName);
if (icon.isNull() && !fallbackName.isEmpty()) icon = QIcon::fromTheme(fallbackName); if (icon.isNull()) icon = QIcon::fromTheme(fallbackName);
if (icon.isNull() && !path.isEmpty()) icon = QPixmap(path);
auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100); auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100);
if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2); if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2);

View file

@ -22,8 +22,8 @@ class PixmapCacheIconEngine: public QIconEngine {
QIcon::Mode /*unused*/, QIcon::Mode /*unused*/,
QIcon::State /*unused*/ QIcon::State /*unused*/
) override { ) override {
qFatal() qFatal(
<< "Unexpected icon paint request bypassed pixmap method. Please report this as a bug."; ) << "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 { QPixmap pixmap(const QSize& size, QIcon::Mode /*unused*/, QIcon::State /*unused*/) override {

View file

@ -1,21 +1,11 @@
#include "incubator.hpp" #include "incubator.hpp"
#include <private/qsgrenderloop_p.h>
#include <qabstractanimation.h>
#include <qguiapplication.h>
#include <qlogging.h> #include <qlogging.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qminmax.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qqmlincubator.h> #include <qqmlincubator.h>
#include <qscreen.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include "logcat.hpp" Q_LOGGING_CATEGORY(logIncubator, "quickshell.incubator", QtWarningMsg);
QS_LOGGING_CATEGORY(logIncubator, "quickshell.incubator", QtWarningMsg);
void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) { void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) {
switch (status) { switch (status) {
@ -24,112 +14,3 @@ void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) {
default: break; default: break;
} }
} }
void QsIncubationController::initLoop() {
auto* app = static_cast<QGuiApplication*>(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<int>(1000 / screen->refreshRate() / 3));
}

View file

@ -1,13 +1,11 @@
#pragma once #pragma once
#include <qloggingcategory.h>
#include <qobject.h> #include <qobject.h>
#include <qpointer.h>
#include <qqmlincubator.h> #include <qqmlincubator.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include "logcat.hpp" Q_DECLARE_LOGGING_CATEGORY(logIncubator);
QS_DECLARE_LOGGING_CATEGORY(logIncubator);
class QsQmlIncubator class QsQmlIncubator
: public QObject : public QObject
@ -26,37 +24,7 @@ signals:
void failed(); void failed();
}; };
class QSGRenderLoop; class DelayedQmlIncubationController: public QQmlIncubationController {
// Do nothing.
class QsIncubationController // This ensures lazy loaders don't start blocking before onReload creates windows.
: 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<QSGRenderLoop> renderLoop = nullptr;
#else
QSGRenderLoop* renderLoop = nullptr;
#endif
int incubationTime = 0;
int timerId = 0;
bool followRenderloop = false;
}; };

View file

@ -3,14 +3,12 @@
#include <qdatastream.h> #include <qdatastream.h>
QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) { QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) {
stream << info.instanceId << info.configPath << info.shellId << info.appId << info.launchTime stream << info.instanceId << info.configPath << info.shellId << info.launchTime;
<< info.pid << info.display;
return stream; return stream;
} }
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) { QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) {
stream >> info.instanceId >> info.configPath >> info.shellId >> info.appId >> info.launchTime stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime;
>> info.pid >> info.display;
return stream; return stream;
} }

View file

@ -3,16 +3,12 @@
#include <qdatetime.h> #include <qdatetime.h>
#include <qlogging.h> #include <qlogging.h>
#include <qstring.h> #include <qstring.h>
#include <sys/types.h>
struct InstanceInfo { struct InstanceInfo {
QString instanceId; QString instanceId;
QString configPath; QString configPath;
QString shellId; QString shellId;
QString appId;
QDateTime launchTime; QDateTime launchTime;
pid_t pid = -1;
QString display;
static InstanceInfo CURRENT; // NOLINT static InstanceInfo CURRENT; // NOLINT
}; };
@ -36,8 +32,6 @@ namespace qs::crash {
struct CrashInfo { struct CrashInfo {
int logFd = -1; int logFd = -1;
int traceFd = -1;
int infoFd = -1;
static CrashInfo INSTANCE; // NOLINT static CrashInfo INSTANCE; // NOLINT
}; };

View file

@ -82,6 +82,9 @@
/// > Notably, @@Variants does not corrently support asynchronous /// > Notably, @@Variants does not corrently support asynchronous
/// > loading, meaning using it inside a LazyLoader will block similarly to not /// > loading, meaning using it inside a LazyLoader will block similarly to not
/// > having a loader to start with. /// > 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 { class LazyLoader: public Reloadable {
Q_OBJECT; Q_OBJECT;
/// The fully loaded item if the loader is @@loading or @@active, or `null` /// The fully loaded item if the loader is @@loading or @@active, or `null`

View file

@ -1,28 +0,0 @@
#pragma once
#include <qlogging.h>
#include <qloggingcategory.h>
namespace qs::log {
void initLogCategoryLevel(const char* name, QtMsgType defaultLevel = QtDebugMsg);
}
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define QS_DECLARE_LOGGING_CATEGORY(name) \
namespace qslogcat { \
Q_DECLARE_LOGGING_CATEGORY(name); \
} \
const QLoggingCategory& name()
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define QS_LOGGING_CATEGORY(name, category, ...) \
namespace qslogcat { \
Q_LOGGING_CATEGORY(name, category __VA_OPT__(, __VA_ARGS__)); \
} \
const QLoggingCategory& name() { \
static auto* init = []() { \
qs::log::initLogCategoryLevel(category __VA_OPT__(, __VA_ARGS__)); \
return &qslogcat::name; \
}(); \
return (init) (); \
}

View file

@ -14,7 +14,6 @@
#include <qlist.h> #include <qlist.h>
#include <qlogging.h> #include <qlogging.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qmutex.h>
#include <qnamespace.h> #include <qnamespace.h>
#include <qobject.h> #include <qobject.h>
#include <qobjectdefs.h> #include <qobjectdefs.h>
@ -28,78 +27,20 @@
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h> #include <qtypes.h>
#include <sys/mman.h> #include <sys/mman.h>
#ifdef __linux__
#include <sys/sendfile.h> #include <sys/sendfile.h>
#include <sys/types.h>
#endif
#ifdef __FreeBSD__
#include <unistd.h>
#endif
#include "instanceinfo.hpp" #include "instanceinfo.hpp"
#include "logcat.hpp"
#include "logging_p.hpp" #include "logging_p.hpp"
#include "logging_qtprivate.cpp" // NOLINT #include "logging_qtprivate.cpp" // NOLINT
#include "paths.hpp" #include "paths.hpp"
#include "ringbuf.hpp" #include "ringbuf.hpp"
QS_LOGGING_CATEGORY(logBare, "quickshell.bare"); Q_LOGGING_CATEGORY(logBare, "quickshell.bare");
namespace qs::log { namespace qs::log {
using namespace qt_logging_registry; using namespace qt_logging_registry;
QS_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg); Q_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg);
namespace {
bool copyFileData(int sourceFd, int destFd, qint64 size) {
auto usize = static_cast<size_t>(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<size_t>(r);
}
return true;
#else
std::array<char, 64 * 1024> 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<size_t>(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<size_t>(w);
}
remaining -= readBytes;
}
return true;
#endif
}
} // namespace
bool LogMessage::operator==(const LogMessage& other) const { bool LogMessage::operator==(const LogMessage& other) const {
// note: not including time // note: not including time
@ -221,7 +162,6 @@ void LogManager::messageHandler(
} }
if (display) { if (display) {
auto locker = QMutexLocker(&self->stdoutMutex);
LogMessage::formatMessage( LogMessage::formatMessage(
self->stdoutStream, self->stdoutStream,
message, message,
@ -242,24 +182,17 @@ void LogManager::filterCategory(QLoggingCategory* category) {
auto categoryName = QLatin1StringView(category->categoryName()); auto categoryName = QLatin1StringView(category->categoryName());
auto isQs = categoryName.startsWith(QLatin1StringView("quickshell.")); auto isQs = categoryName.startsWith(QLatin1StringView("quickshell."));
CategoryFilter filter; if (instance->lastCategoryFilter) {
// We don't respect log filters for qs logs because some distros like to ship
// default configs that hide everything. QT_LOGGING_RULES is considered via the filter list.
if (isQs) {
// QtDebugMsg == 0, so default
auto defaultLevel = instance->defaultLevels.value(categoryName);
filter = CategoryFilter();
// clang-format off
filter.debug = instance->mDefaultLevel == QtDebugMsg || defaultLevel == QtDebugMsg;
filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg || defaultLevel == QtInfoMsg;
filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg || defaultLevel == QtWarningMsg;
filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg || defaultLevel == QtCriticalMsg;
// clang-format on
} else if (instance->lastCategoryFilter) {
instance->lastCategoryFilter(category); instance->lastCategoryFilter(category);
filter = CategoryFilter(category); }
auto filter = CategoryFilter(category);
if (isQs) {
filter.debug = filter.debug || instance->mDefaultLevel == QtDebugMsg;
filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg;
filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg;
filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg;
} }
for (const auto& rule: *instance->rules) { for (const auto& rule: *instance->rules) {
@ -302,23 +235,14 @@ void LogManager::init(
{ {
QLoggingSettingsParser parser; QLoggingSettingsParser parser;
// Load QT_LOGGING_RULES because we ignore the last category filter for QS messages
// due to disk config files.
parser.setContent(qEnvironmentVariable("QT_LOGGING_RULES"));
instance->rules = new QList(parser.rules());
parser.setContent(rules); parser.setContent(rules);
instance->rules->append(parser.rules()); instance->rules = new QList(parser.rules());
}
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); qInstallMessageHandler(&LogManager::messageHandler);
instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory);
qCDebug(logLogging) << "Creating offthread logger..."; qCDebug(logLogging) << "Creating offthread logger...";
auto* thread = new QThread(); auto* thread = new QThread();
instance->threadProxy.moveToThread(thread); instance->threadProxy.moveToThread(thread);
@ -333,10 +257,6 @@ void LogManager::init(
qCDebug(logLogging) << "Logger initialized."; qCDebug(logLogging) << "Logger initialized.";
} }
void initLogCategoryLevel(const char* name, QtMsgType defaultLevel) {
LogManager::instance()->defaultLevels.insert(QLatin1StringView(name), defaultLevel);
}
void LogManager::initFs() { void LogManager::initFs() {
QMetaObject::invokeMethod( QMetaObject::invokeMethod(
&LogManager::instance()->threadProxy, &LogManager::instance()->threadProxy,
@ -377,12 +297,8 @@ void ThreadLogging::init() {
if (logMfd != -1) { if (logMfd != -1) {
this->file = new QFile(); this->file = new QFile();
this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle);
if (this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle)) {
this->fileStream.setDevice(this->file); this->fileStream.setDevice(this->file);
} else {
qCCritical(logLogging) << "Failed to open early logging memfd.";
}
} }
if (dlogMfd != -1) { if (dlogMfd != -1) {
@ -390,9 +306,7 @@ void ThreadLogging::init() {
this->detailedFile = new QFile(); this->detailedFile = new QFile();
// buffered by WriteBuffer // buffered by WriteBuffer
if (this->detailedFile this->detailedFile->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle);
->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle))
{
this->detailedWriter.setDevice(this->detailedFile); this->detailedWriter.setDevice(this->detailedFile);
if (!this->detailedWriter.writeHeader()) { if (!this->detailedWriter.writeHeader()) {
@ -401,9 +315,6 @@ void ThreadLogging::init() {
delete this->detailedFile; delete this->detailedFile;
this->detailedFile = nullptr; this->detailedFile = nullptr;
} }
} else {
qCCritical(logLogging) << "Failed to open early detailed logging memfd.";
}
} }
// This connection is direct so it works while the event loop is destroyed between // This connection is direct so it works while the event loop is destroyed between
@ -425,8 +336,7 @@ void ThreadLogging::initFs() {
auto* runDir = QsPaths::instance()->instanceRunDir(); auto* runDir = QsPaths::instance()->instanceRunDir();
if (!runDir) { if (!runDir) {
qCCritical( qCCritical(logLogging
logLogging
) << "Could not start filesystem logging as the runtime directory could not be created."; ) << "Could not start filesystem logging as the runtime directory could not be created.";
return; return;
} }
@ -437,8 +347,7 @@ void ThreadLogging::initFs() {
auto* detailedFile = new QFile(detailedPath); auto* detailedFile = new QFile(detailedPath);
if (!file->open(QFile::ReadWrite | QFile::Truncate)) { if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
qCCritical( qCCritical(logLogging
logLogging
) << "Could not start filesystem logger as the log file could not be created:" ) << "Could not start filesystem logger as the log file could not be created:"
<< path; << path;
delete file; delete file;
@ -449,14 +358,13 @@ void ThreadLogging::initFs() {
// buffered by WriteBuffer // buffered by WriteBuffer
if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) { if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) {
qCCritical( qCCritical(logLogging
logLogging
) << "Could not start detailed filesystem logger as the log file could not be created:" ) << "Could not start detailed filesystem logger as the log file could not be created:"
<< detailedPath; << detailedPath;
delete detailedFile; delete detailedFile;
detailedFile = nullptr; detailedFile = nullptr;
} else { } else {
struct flock lock = { auto lock = flock {
.l_type = F_WRLCK, .l_type = F_WRLCK,
.l_whence = SEEK_SET, .l_whence = SEEK_SET,
.l_start = 0, .l_start = 0,
@ -478,11 +386,7 @@ void ThreadLogging::initFs() {
auto* oldFile = this->file; auto* oldFile = this->file;
if (oldFile) { if (oldFile) {
oldFile->seek(0); 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; this->file = file;
@ -494,10 +398,7 @@ void ThreadLogging::initFs() {
auto* oldFile = this->detailedFile; auto* oldFile = this->detailedFile;
if (oldFile) { if (oldFile) {
oldFile->seek(0); oldFile->seek(0);
if (!copyFileData(oldFile->handle(), detailedFile->handle(), oldFile->size())) { sendfile(detailedFile->handle(), oldFile->handle(), nullptr, 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(); crash::CrashInfo::INSTANCE.logFd = detailedFile->handle();
@ -540,16 +441,12 @@ void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) {
this->fileStream << Qt::endl; this->fileStream << Qt::endl;
} }
if (!this->detailedWriter.write(msg) || (this->detailedFile && !this->detailedFile->flush())) { if (this->detailedWriter.write(msg)) {
this->detailedWriter.setDevice(nullptr); this->detailedFile->flush();
} else if (this->detailedFile != nullptr) {
if (this->detailedFile) {
this->detailedFile->close();
this->detailedFile = nullptr;
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs."; qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
} }
} }
}
CompressedLogType compressedTypeOf(QtMsgType type) { CompressedLogType compressedTypeOf(QtMsgType type) {
switch (type) { switch (type) {
@ -820,11 +717,11 @@ bool EncodedLogReader::readVarInt(quint32* slot) {
if (!this->reader.skip(1)) return false; if (!this->reader.skip(1)) return false;
*slot = qFromLittleEndian(n); *slot = qFromLittleEndian(n);
} else if ((bytes[1] != 0xff || bytes[2] != 0xff) && readLength >= 3) { } else if ((bytes[1] != 0xff || bytes[2] != 0xff) && readLength >= 3) {
auto n = *reinterpret_cast<quint16*>(bytes.data() + 1); // NOLINT auto n = *reinterpret_cast<quint16*>(bytes.data() + 1);
if (!this->reader.skip(3)) return false; if (!this->reader.skip(3)) return false;
*slot = qFromLittleEndian(n); *slot = qFromLittleEndian(n);
} else if (readLength == 7) { } else if (readLength == 7) {
auto n = *reinterpret_cast<quint32*>(bytes.data() + 3); // NOLINT auto n = *reinterpret_cast<quint32*>(bytes.data() + 3);
if (!this->reader.skip(7)) return false; if (!this->reader.skip(7)) return false;
*slot = qFromLittleEndian(n); *slot = qFromLittleEndian(n);
} else return false; } else return false;
@ -960,7 +857,7 @@ bool LogReader::continueReading() {
} }
void LogFollower::FcntlWaitThread::run() { void LogFollower::FcntlWaitThread::run() {
struct flock lock = { auto lock = flock {
.l_type = F_RDLCK, // won't block other read locks when we take it .l_type = F_RDLCK, // won't block other read locks when we take it
.l_whence = SEEK_SET, .l_whence = SEEK_SET,
.l_start = 0, .l_start = 0,

View file

@ -2,7 +2,6 @@
#include <utility> #include <utility>
#include <qbytearrayview.h>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qdatetime.h> #include <qdatetime.h>
#include <qfile.h> #include <qfile.h>
@ -10,13 +9,10 @@
#include <qlatin1stringview.h> #include <qlatin1stringview.h>
#include <qlogging.h> #include <qlogging.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qmutex.h>
#include <qobject.h> #include <qobject.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include "logcat.hpp" Q_DECLARE_LOGGING_CATEGORY(logBare);
QS_DECLARE_LOGGING_CATEGORY(logBare);
namespace qs::log { namespace qs::log {
@ -131,15 +127,11 @@ private:
QString mRulesString; QString mRulesString;
QList<qt_logging_registry::QLoggingRule>* rules = nullptr; QList<qt_logging_registry::QLoggingRule>* rules = nullptr;
QtMsgType mDefaultLevel = QtWarningMsg; QtMsgType mDefaultLevel = QtWarningMsg;
QHash<QLatin1StringView, QtMsgType> defaultLevels;
QHash<const void*, CategoryFilter> sparseFilters; QHash<const void*, CategoryFilter> sparseFilters;
QHash<QLatin1StringView, CategoryFilter> allFilters; QHash<QLatin1StringView, CategoryFilter> allFilters;
QTextStream stdoutStream; QTextStream stdoutStream;
QMutex stdoutMutex;
LoggingThreadProxy threadProxy; LoggingThreadProxy threadProxy;
friend void initLogCategoryLevel(const char* name, QtMsgType defaultLevel);
}; };
bool readEncodedLogs( bool readEncodedLogs(

View file

@ -16,11 +16,10 @@
#include <qstringview.h> #include <qstringview.h>
#include <qtypes.h> #include <qtypes.h>
#include "logcat.hpp"
#include "logging_qtprivate.hpp" #include "logging_qtprivate.hpp"
namespace qs::log { namespace qs::log {
QS_DECLARE_LOGGING_CATEGORY(logLogging); Q_DECLARE_LOGGING_CATEGORY(logLogging);
namespace qt_logging_registry { namespace qt_logging_registry {

View file

@ -12,10 +12,8 @@
#include <qstringview.h> #include <qstringview.h>
#include <qtypes.h> #include <qtypes.h>
#include "logcat.hpp"
namespace qs::log { namespace qs::log {
QS_DECLARE_LOGGING_CATEGORY(logLogging); Q_DECLARE_LOGGING_CATEGORY(logLogging);
namespace qt_logging_registry { namespace qt_logging_registry {

View file

@ -1,14 +1,81 @@
#include "model.hpp" #include "model.hpp"
#include <qbytearray.h> #include <qabstractitemmodel.h>
#include <qhash.h> #include <qhash.h>
#include <qnamespace.h> #include <qnamespace.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
qint32 UntypedObjectModel::rowCount(const QModelIndex& parent) const {
if (parent != QModelIndex()) return 0;
return static_cast<qint32>(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<int, QByteArray> UntypedObjectModel::roleNames() const { QHash<int, QByteArray> UntypedObjectModel::roleNames() const {
return {{Qt::UserRole, "modelData"}}; 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<qint32>(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<qint32>(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<QObject*>& 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() { UntypedObjectModel* UntypedObjectModel::emptyInstance() {
static auto* instance = new ObjectModel<void>(nullptr); static auto* instance = new UntypedObjectModel(nullptr); // NOLINT
return instance; return instance;
} }

View file

@ -2,7 +2,7 @@
#include <functional> #include <functional>
#include <QtCore/qtmetamacros.h> #include <bit>
#include <qabstractitemmodel.h> #include <qabstractitemmodel.h>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qobject.h> #include <qobject.h>
@ -49,11 +49,14 @@ class UntypedObjectModel: public QAbstractListModel {
public: public:
explicit UntypedObjectModel(QObject* parent): QAbstractListModel(parent) {} 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<int, QByteArray> roleNames() const override; [[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] virtual QList<QObject*> values() = 0; [[nodiscard]] QList<QObject*> values() const { return this->valuesList; };
void removeAt(qsizetype index);
Q_INVOKABLE virtual qsizetype indexOf(QObject* object) const = 0; Q_INVOKABLE qsizetype indexOf(QObject* object);
static UntypedObjectModel* emptyInstance(); static UntypedObjectModel* emptyInstance();
@ -68,6 +71,15 @@ signals:
/// Sent immediately after an object is removed from the list. /// Sent immediately after an object is removed from the list.
void objectRemovedPost(QObject* object, qsizetype index); 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<QObject*>& newValues);
QVector<QObject*> valuesList;
private: private:
static qsizetype valuesCount(QQmlListProperty<QObject>* property); static qsizetype valuesCount(QQmlListProperty<QObject>* property);
static QObject* valueAt(QQmlListProperty<QObject>* property, qsizetype index); static QObject* valueAt(QQmlListProperty<QObject>* property, qsizetype index);
@ -78,20 +90,14 @@ class ObjectModel: public UntypedObjectModel {
public: public:
explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {} explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {}
[[nodiscard]] const QList<T*>& valueList() const { return this->mValuesList; } [[nodiscard]] QVector<T*>& valueList() { return *std::bit_cast<QVector<T*>*>(&this->valuesList); }
[[nodiscard]] QList<T*>& valueList() { return this->mValuesList; }
[[nodiscard]] const QVector<T*>& valueList() const {
return *std::bit_cast<const QVector<T*>*>(&this->valuesList);
}
void insertObject(T* object, qsizetype index = -1) { void insertObject(T* object, qsizetype index = -1) {
auto iindex = index == -1 ? this->mValuesList.length() : index; this->UntypedObjectModel::insertObject(object, index);
emit this->objectInsertedPre(object, iindex);
auto intIndex = static_cast<qint32>(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<bool(T*, T*)>& compare) { void insertObjectSorted(T* object, const std::function<bool(T*, T*)>& compare) {
@ -104,71 +110,17 @@ public:
} }
auto idx = iter - list.begin(); auto idx = iter - list.begin();
this->insertObject(object, idx); this->UntypedObjectModel::insertObject(object, idx);
} }
bool removeObject(const T* object) { void removeObject(const T* object) { this->UntypedObjectModel::removeObject(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<qint32>(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 // Assumes only one instance of a specific value
void diffUpdate(const QList<T*>& newValues) { void diffUpdate(const QVector<T*>& newValues) {
for (qsizetype i = 0; i < this->mValuesList.length();) { this->UntypedObjectModel::diffUpdate(*std::bit_cast<const QVector<QObject*>*>(&newValues));
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<T>* emptyInstance() { static ObjectModel<T>* emptyInstance() {
return static_cast<ObjectModel<T>*>(UntypedObjectModel::emptyInstance()); return static_cast<ObjectModel<T>*>(UntypedObjectModel::emptyInstance());
} }
[[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override {
if (parent != QModelIndex()) return 0;
return static_cast<qint32>(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<QObject*>(this->mValuesList.at(index.row())));
}
qsizetype indexOf(QObject* object) const override {
return this->mValuesList.indexOf(reinterpret_cast<T*>(object));
}
[[nodiscard]] QList<QObject*> values() override {
return *reinterpret_cast<QList<QObject*>*>(&this->mValuesList);
}
private:
QList<T*> mValuesList;
}; };

View file

@ -21,6 +21,7 @@ headers = [
"model.hpp", "model.hpp",
"elapsedtimer.hpp", "elapsedtimer.hpp",
"desktopentry.hpp", "desktopentry.hpp",
"objectrepeater.hpp",
"qsmenu.hpp", "qsmenu.hpp",
"retainable.hpp", "retainable.hpp",
"popupanchor.hpp", "popupanchor.hpp",

190
src/core/objectrepeater.cpp Normal file
View file

@ -0,0 +1,190 @@
#include "objectrepeater.hpp"
#include <utility>
#include <qabstractitemmodel.h>
#include <qcontainerfwd.h>
#include <qhash.h>
#include <qlogging.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
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<QAbstractItemModel*>()) {
auto* model = this->mModel.value<QAbstractItemModel*>();
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<QQmlListReference>()) {
auto values = this->mModel.value<QQmlListReference>();
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<QVector<QVariant>>()) {
auto values = this->mModel.value<QVector<QVariant>>();
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<QModelRoleData>();
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<int>(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;
}

View file

@ -0,0 +1,85 @@
#pragma once
#include <qabstractitemmodel.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
#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<QObject> {
Q_OBJECT;
/// The model providing data to the ObjectRepeater.
///
/// Currently accepted model types are `list<T>` 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<T>` 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;
};

View file

@ -9,17 +9,15 @@
#include <qdir.h> #include <qdir.h>
#include <qlogging.h> #include <qlogging.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qpair.h>
#include <qstandardpaths.h> #include <qstandardpaths.h>
#include <qtenvironmentvariables.h> #include <qtenvironmentvariables.h>
#include <qtversionchecks.h> #include <qtversionchecks.h>
#include <unistd.h> #include <unistd.h>
#include "instanceinfo.hpp" #include "instanceinfo.hpp"
#include "logcat.hpp"
namespace { namespace {
QS_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg); Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
} }
QsPaths* QsPaths::instance() { QsPaths* QsPaths::instance() {
@ -27,19 +25,12 @@ QsPaths* QsPaths::instance() {
return instance; return instance;
} }
void QsPaths::init( void QsPaths::init(QString shellId, QString pathId, QString dataOverride, QString stateOverride) {
QString shellId,
QString pathId,
QString dataOverride,
QString stateOverride,
QString cacheOverride
) {
auto* instance = QsPaths::instance(); auto* instance = QsPaths::instance();
instance->shellId = std::move(shellId); instance->shellId = std::move(shellId);
instance->pathId = std::move(pathId); instance->pathId = std::move(pathId);
instance->shellDataOverride = std::move(dataOverride); instance->shellDataOverride = std::move(dataOverride);
instance->shellStateOverride = std::move(stateOverride); instance->shellStateOverride = std::move(stateOverride);
instance->shellCacheOverride = std::move(cacheOverride);
} }
QDir QsPaths::crashDir(const QString& id) { QDir QsPaths::crashDir(const QString& id) {
@ -64,7 +55,7 @@ QDir* QsPaths::baseRunDir() {
if (this->baseRunState == DirState::Unknown) { if (this->baseRunState == DirState::Unknown) {
auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR"); auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR");
if (runtimeDir.isEmpty()) { 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; qCInfo(logPaths) << "XDG_RUNTIME_DIR was not set, defaulting to" << runtimeDir;
} }
@ -142,41 +133,13 @@ QDir* QsPaths::instanceRunDir() {
else return &this->mInstanceRunDir; else return &this->mInstanceRunDir;
} }
QDir* QsPaths::shellVfsDir() {
if (this->shellVfsState == DirState::Unknown) {
if (auto* baseRunDir = this->baseRunDir()) {
this->mShellVfsDir = QDir(baseRunDir->filePath("vfs"));
this->mShellVfsDir = QDir(this->mShellVfsDir.filePath(this->shellId));
qCDebug(logPaths) << "Initialized runtime vfs path:" << this->mShellVfsDir.path();
if (!this->mShellVfsDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create runtime vfs directory at"
<< this->mShellVfsDir.path();
this->shellVfsState = DirState::Failed;
} else {
this->shellVfsState = DirState::Ready;
}
} else {
qCCritical(logPaths) << "Could not create shell runtime vfs path as it was not possible to "
"create the base runtime path.";
this->shellVfsState = DirState::Failed;
}
}
if (this->shellVfsState == DirState::Failed) return nullptr;
else return &this->mShellVfsDir;
}
void QsPaths::linkRunDir() { void QsPaths::linkRunDir() {
if (auto* runDir = this->instanceRunDir()) { if (auto* runDir = this->instanceRunDir()) {
auto pidDir = QDir(this->baseRunDir()->filePath("by-pid")); auto pidDir = QDir(this->baseRunDir()->filePath("by-pid"));
auto* shellDir = this->shellRunDir(); auto* shellDir = this->shellRunDir();
if (!shellDir) { if (!shellDir) {
qCCritical( qCCritical(logPaths
logPaths
) << "Could not create by-id symlink as the shell runtime path could not be created."; ) << "Could not create by-id symlink as the shell runtime path could not be created.";
} else { } else {
auto shellPath = shellDir->filePath(runDir->dirName()); auto shellPath = shellDir->filePath(runDir->dirName());
@ -324,16 +287,9 @@ QDir QsPaths::shellStateDir() {
QDir QsPaths::shellCacheDir() { QDir QsPaths::shellCacheDir() {
if (this->shellCacheState == DirState::Unknown) { if (this->shellCacheState == DirState::Unknown) {
QDir dir; auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
if (this->shellCacheOverride.isEmpty()) {
dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
dir = QDir(dir.filePath("by-shell")); dir = QDir(dir.filePath("by-shell"));
dir = QDir(dir.filePath(this->shellId)); dir = QDir(dir.filePath(this->shellId));
} else {
auto basedir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
dir = QDir(this->shellCacheOverride.replace("$BASE", basedir));
}
this->mShellCacheDir = dir; this->mShellCacheDir = dir;
qCDebug(logPaths) << "Initialized cache path:" << dir.path(); qCDebug(logPaths) << "Initialized cache path:" << dir.path();
@ -361,7 +317,7 @@ void QsPaths::createLock() {
return; return;
} }
struct flock lock = { auto lock = flock {
.l_type = F_WRLCK, .l_type = F_WRLCK,
.l_whence = SEEK_SET, .l_whence = SEEK_SET,
.l_start = 0, .l_start = 0,
@ -379,8 +335,7 @@ void QsPaths::createLock() {
qCDebug(logPaths) << "Created instance lock at" << path; qCDebug(logPaths) << "Created instance lock at" << path;
} }
} else { } else {
qCCritical( qCCritical(logPaths
logPaths
) << "Could not create instance lock, as the instance runtime directory could not be created."; ) << "Could not create instance lock, as the instance runtime directory could not be created.";
} }
} }
@ -389,7 +344,7 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD
auto file = QFile(QDir(path).filePath("instance.lock")); auto file = QFile(QDir(path).filePath("instance.lock"));
if (!file.open(QFile::ReadOnly)) return false; if (!file.open(QFile::ReadOnly)) return false;
struct flock lock = { auto lock = flock {
.l_type = F_WRLCK, .l_type = F_WRLCK,
.l_whence = SEEK_SET, .l_whence = SEEK_SET,
.l_start = 0, .l_start = 0,
@ -412,35 +367,29 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD
return true; return true;
} }
QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>> QVector<InstanceLockInfo> QsPaths::collectInstances(const QString& path, bool fallbackDead) {
QsPaths::collectInstances(const QString& path, const QString& display) {
qCDebug(logPaths) << "Collecting instances from" << path; qCDebug(logPaths) << "Collecting instances from" << path;
auto liveInstances = QVector<InstanceLockInfo>(); auto instances = QVector<InstanceLockInfo>();
auto deadInstances = QVector<InstanceLockInfo>();
auto dir = QDir(path); auto dir = QDir(path);
InstanceLockInfo info; InstanceLockInfo info;
for (auto& entry: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { for (auto& entry: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
auto path = dir.filePath(entry); auto path = dir.filePath(entry);
if (QsPaths::checkLock(path, &info, true)) { if (QsPaths::checkLock(path, &info, fallbackDead)) {
if (fallbackDead && info.pid != -1) {
fallbackDead = false;
instances.clear();
}
qCDebug(logPaths).nospace() << "Found instance " << info.instance.instanceId << " (pid " qCDebug(logPaths).nospace() << "Found instance " << info.instance.instanceId << " (pid "
<< info.pid << ") at " << path; << info.pid << ") at " << path;
if (!display.isEmpty() && info.instance.display != display) { instances.push_back(info);
qCDebug(logPaths) << "Skipped instance with mismatched display at" << path;
continue;
}
if (info.pid == -1) {
deadInstances.push_back(info);
} else {
liveInstances.push_back(info);
}
} else { } else {
qCDebug(logPaths) << "Skipped potential instance at" << path; qCDebug(logPaths) << "Skipped potential instance at" << path;
} }
} }
return qMakePair(liveInstances, deadInstances); return instances;
} }

View file

@ -1,7 +1,6 @@
#pragma once #pragma once
#include <qdatetime.h> #include <qdatetime.h>
#include <qdir.h> #include <qdir.h>
#include <qpair.h>
#include <qtypes.h> #include <qtypes.h>
#include "instanceinfo.hpp" #include "instanceinfo.hpp"
@ -17,24 +16,16 @@ QDataStream& operator>>(QDataStream& stream, InstanceLockInfo& info);
class QsPaths { class QsPaths {
public: public:
static QsPaths* instance(); static QsPaths* instance();
static void init( static void init(QString shellId, QString pathId, QString dataOverride, QString stateOverride);
QString shellId,
QString pathId,
QString dataOverride,
QString stateOverride,
QString cacheOverride
);
static QDir crashDir(const QString& id); static QDir crashDir(const QString& id);
static QString basePath(const QString& id); static QString basePath(const QString& id);
static QString ipcPath(const QString& id); static QString ipcPath(const QString& id);
static bool static bool
checkLock(const QString& path, InstanceLockInfo* info = nullptr, bool allowDead = false); checkLock(const QString& path, InstanceLockInfo* info = nullptr, bool allowDead = false);
static QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>> static QVector<InstanceLockInfo> collectInstances(const QString& path, bool fallbackDead = false);
collectInstances(const QString& path, const QString& display);
QDir* baseRunDir(); QDir* baseRunDir();
QDir* shellRunDir(); QDir* shellRunDir();
QDir* shellVfsDir();
QDir* instanceRunDir(); QDir* instanceRunDir();
void linkRunDir(); void linkRunDir();
void linkPathDir(); void linkPathDir();
@ -55,11 +46,9 @@ private:
QString pathId; QString pathId;
QDir mBaseRunDir; QDir mBaseRunDir;
QDir mShellRunDir; QDir mShellRunDir;
QDir mShellVfsDir;
QDir mInstanceRunDir; QDir mInstanceRunDir;
DirState baseRunState = DirState::Unknown; DirState baseRunState = DirState::Unknown;
DirState shellRunState = DirState::Unknown; DirState shellRunState = DirState::Unknown;
DirState shellVfsState = DirState::Unknown;
DirState instanceRunState = DirState::Unknown; DirState instanceRunState = DirState::Unknown;
QDir mShellDataDir; QDir mShellDataDir;
@ -71,5 +60,4 @@ private:
QString shellDataOverride; QString shellDataOverride;
QString shellStateOverride; QString shellStateOverride;
QString shellCacheOverride;
}; };

View file

@ -18,6 +18,7 @@
#include <qwindow.h> #include <qwindow.h>
#include "../window/proxywindow.hpp" #include "../window/proxywindow.hpp"
#include "../window/windowinterface.hpp"
#include "iconprovider.hpp" #include "iconprovider.hpp"
#include "model.hpp" #include "model.hpp"
#include "platformmenu_p.hpp" #include "platformmenu_p.hpp"
@ -90,8 +91,10 @@ bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relati
} else if (parentWindow == nullptr) { } else if (parentWindow == nullptr) {
qCritical() << "Cannot display PlatformMenuEntry with null parent window."; qCritical() << "Cannot display PlatformMenuEntry with null parent window.";
return false; return false;
} else if (auto* proxy = ProxyWindowBase::forObject(parentWindow)) { } else if (auto* proxy = qobject_cast<ProxyWindowBase*>(parentWindow)) {
window = proxy->backingWindow(); window = proxy->backingWindow();
} else if (auto* interface = qobject_cast<WindowInterface*>(parentWindow)) {
window = interface->proxyWindow()->backingWindow();
} else { } else {
qCritical() << "PlatformMenuEntry.display() must be called with a window."; qCritical() << "PlatformMenuEntry.display() must be called with a window.";
return false; return false;

View file

@ -9,18 +9,6 @@ static QVector<QsEnginePlugin*> plugins; // NOLINT
void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); } 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() { void QsEnginePlugin::initPlugins() {
plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); }); plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); });
@ -28,10 +16,6 @@ void QsEnginePlugin::initPlugins() {
return b->dependencies().contains(a->name()); return b->dependencies().contains(a->name());
}); });
for (QsEnginePlugin* plugin: plugins) {
plugin->preinit();
}
for (QsEnginePlugin* plugin: plugins) { for (QsEnginePlugin* plugin: plugins) {
plugin->init(); plugin->init();
} }

View file

@ -18,14 +18,12 @@ public:
virtual QString name() { return QString(); } virtual QString name() { return QString(); }
virtual QList<QString> dependencies() { return {}; } virtual QList<QString> dependencies() { return {}; }
virtual bool applies() { return true; } virtual bool applies() { return true; }
virtual void preinit() {}
virtual void init() {} virtual void init() {}
virtual void registerTypes() {} virtual void registerTypes() {}
virtual void constructGeneration(EngineGeneration& /*unused*/) {} // NOLINT virtual void constructGeneration(EngineGeneration& /*unused*/) {} // NOLINT
virtual void onReload() {} virtual void onReload() {}
static void registerPlugin(QsEnginePlugin& plugin); static void registerPlugin(QsEnginePlugin& plugin);
static void preinitPluginsOnly();
static void initPlugins(); static void initPlugins();
static void runConstructGeneration(EngineGeneration& generation); static void runConstructGeneration(EngineGeneration& generation);
static void runOnReload(); static void runOnReload();

View file

@ -11,6 +11,7 @@
#include <qwindow.h> #include <qwindow.h>
#include "../window/proxywindow.hpp" #include "../window/proxywindow.hpp"
#include "../window/windowinterface.hpp"
#include "types.hpp" #include "types.hpp"
bool PopupAnchorState::operator==(const PopupAnchorState& other) const { bool PopupAnchorState::operator==(const PopupAnchorState& other) const {
@ -27,7 +28,7 @@ void PopupAnchor::markClean() { this->lastState = this->state; }
void PopupAnchor::markDirty() { this->lastState.reset(); } void PopupAnchor::markDirty() { this->lastState.reset(); }
QWindow* PopupAnchor::backingWindow() const { QWindow* PopupAnchor::backingWindow() const {
return this->bProxyWindow ? this->bProxyWindow->backingWindow() : nullptr; return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr;
} }
void PopupAnchor::setWindowInternal(QObject* window) { void PopupAnchor::setWindowInternal(QObject* window) {
@ -35,12 +36,14 @@ void PopupAnchor::setWindowInternal(QObject* window) {
if (this->mWindow) { if (this->mWindow) {
QObject::disconnect(this->mWindow, nullptr, this, nullptr); QObject::disconnect(this->mWindow, nullptr, this, nullptr);
QObject::disconnect(this->bProxyWindow, nullptr, this, nullptr); QObject::disconnect(this->mProxyWindow, nullptr, this, nullptr);
} }
if (window) { if (window) {
if (auto* proxy = ProxyWindowBase::forObject(window)) { if (auto* proxy = qobject_cast<ProxyWindowBase*>(window)) {
this->bProxyWindow = proxy; this->mProxyWindow = proxy;
} else if (auto* interface = qobject_cast<WindowInterface*>(window)) {
this->mProxyWindow = interface->proxyWindow();
} else { } else {
qWarning() << "Tried to set popup anchor window to" << window qWarning() << "Tried to set popup anchor window to" << window
<< "which is not a quickshell window."; << "which is not a quickshell window.";
@ -52,7 +55,7 @@ void PopupAnchor::setWindowInternal(QObject* window) {
QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed); QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed);
QObject::connect( QObject::connect(
this->bProxyWindow, this->mProxyWindow,
&ProxyWindowBase::backerVisibilityChanged, &ProxyWindowBase::backerVisibilityChanged,
this, this,
&PopupAnchor::backingWindowVisibilityChanged &PopupAnchor::backingWindowVisibilityChanged
@ -67,7 +70,7 @@ void PopupAnchor::setWindowInternal(QObject* window) {
setnull: setnull:
if (this->mWindow) { if (this->mWindow) {
this->mWindow = nullptr; this->mWindow = nullptr;
this->bProxyWindow = nullptr; this->mProxyWindow = nullptr;
emit this->windowChanged(); emit this->windowChanged();
emit this->backingWindowVisibilityChanged(); emit this->backingWindowVisibilityChanged();
@ -97,7 +100,7 @@ void PopupAnchor::setItem(QQuickItem* item) {
void PopupAnchor::onWindowDestroyed() { void PopupAnchor::onWindowDestroyed() {
this->mWindow = nullptr; this->mWindow = nullptr;
this->bProxyWindow = nullptr; this->mProxyWindow = nullptr;
emit this->windowChanged(); emit this->windowChanged();
emit this->backingWindowVisibilityChanged(); emit this->backingWindowVisibilityChanged();
} }
@ -183,18 +186,14 @@ void PopupAnchor::updatePlacement(const QPoint& anchorpoint, const QSize& size)
} }
void PopupAnchor::updateAnchor() { void PopupAnchor::updateAnchor() {
if (this->mItem && this->bProxyWindow) { if (this->mItem && this->mProxyWindow) {
auto baseRect = auto baseRect =
this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect(); this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect();
auto rect = this->mProxyWindow->contentItem()->mapFromItem(
auto rect = this->bProxyWindow->contentItem()->mapFromItem(
this->mItem, this->mItem,
baseRect.marginsRemoved(this->mMargins.qmargins()) baseRect.marginsRemoved(this->mMargins.qmargins())
); );
if (rect.width() < 1) rect.setWidth(1);
if (rect.height() < 1) rect.setHeight(1);
this->setWindowRect(rect.toRect()); this->setWindowRect(rect.toRect());
} }

View file

@ -6,7 +6,6 @@
#include <qnamespace.h> #include <qnamespace.h>
#include <qobject.h> #include <qobject.h>
#include <qpoint.h> #include <qpoint.h>
#include <qproperty.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qquickitem.h> #include <qquickitem.h>
#include <qsize.h> #include <qsize.h>
@ -140,9 +139,7 @@ public:
void markDirty(); void markDirty();
[[nodiscard]] QObject* window() const { return this->mWindow; } [[nodiscard]] QObject* window() const { return this->mWindow; }
[[nodiscard]] QBindable<ProxyWindowBase*> bindableProxyWindow() const { [[nodiscard]] ProxyWindowBase* proxyWindow() const { return this->mProxyWindow; }
return &this->bProxyWindow;
}
[[nodiscard]] QWindow* backingWindow() const; [[nodiscard]] QWindow* backingWindow() const;
void setWindowInternal(QObject* window); void setWindowInternal(QObject* window);
void setWindow(QObject* window); void setWindow(QObject* window);
@ -196,12 +193,11 @@ private slots:
private: private:
QObject* mWindow = nullptr; QObject* mWindow = nullptr;
QQuickItem* mItem = nullptr; QQuickItem* mItem = nullptr;
ProxyWindowBase* mProxyWindow = nullptr;
PopupAnchorState state; PopupAnchorState state;
Box mUserRect; Box mUserRect;
Margins mMargins; Margins mMargins;
std::optional<PopupAnchorState> lastState; std::optional<PopupAnchorState> lastState;
Q_OBJECT_BINDABLE_PROPERTY(PopupAnchor, ProxyWindowBase*, bProxyWindow);
}; };
class PopupPositioner { class PopupPositioner {

View file

@ -8,10 +8,8 @@
#include <qguiapplication.h> #include <qguiapplication.h>
#include <qicon.h> #include <qicon.h>
#include <qjsengine.h> #include <qjsengine.h>
#include <qlist.h>
#include <qlogging.h> #include <qlogging.h>
#include <qobject.h> #include <qobject.h>
#include <qprocess.h>
#include <qqmlcontext.h> #include <qqmlcontext.h>
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qqmllist.h> #include <qqmllist.h>
@ -23,14 +21,11 @@
#include <qwindowdefs.h> #include <qwindowdefs.h>
#include <unistd.h> #include <unistd.h>
#include "../io/processcore.hpp"
#include "generation.hpp" #include "generation.hpp"
#include "iconimageprovider.hpp" #include "iconimageprovider.hpp"
#include "instanceinfo.hpp"
#include "paths.hpp" #include "paths.hpp"
#include "qmlscreen.hpp" #include "qmlscreen.hpp"
#include "rootwrapper.hpp" #include "rootwrapper.hpp"
#include "scanenv.hpp"
QuickshellSettings::QuickshellSettings() { QuickshellSettings::QuickshellSettings() {
QObject::connect( QObject::connect(
@ -61,9 +56,7 @@ void QuickshellSettings::setWorkingDirectory(QString workingDirectory) { // NOLI
emit this->workingDirectoryChanged(); emit this->workingDirectoryChanged();
} }
bool QuickshellSettings::watchFiles() const { bool QuickshellSettings::watchFiles() const { return this->mWatchFiles; }
return this->mWatchFiles && qEnvironmentVariableIsEmpty("QS_DISABLE_FILE_WATCHER");
}
void QuickshellSettings::setWatchFiles(bool watchFiles) { void QuickshellSettings::setWatchFiles(bool watchFiles) {
if (watchFiles == this->mWatchFiles) return; if (watchFiles == this->mWatchFiles) return;
@ -154,22 +147,6 @@ qint32 QuickshellGlobal::processId() const { // NOLINT
return getpid(); return getpid();
} }
QString QuickshellGlobal::instanceId() const { // NOLINT
return InstanceInfo::CURRENT.instanceId;
}
QString QuickshellGlobal::shellId() const { // NOLINT
return InstanceInfo::CURRENT.shellId;
}
QString QuickshellGlobal::appId() const { // NOLINT
return InstanceInfo::CURRENT.appId;
}
QDateTime QuickshellGlobal::launchTime() const { // NOLINT
return InstanceInfo::CURRENT.launchTime;
}
qsizetype QuickshellGlobal::screensCount(QQmlListProperty<QuickshellScreenInfo>* /*unused*/) { qsizetype QuickshellGlobal::screensCount(QQmlListProperty<QuickshellScreenInfo>* /*unused*/) {
return QuickshellTracked::instance()->screens.size(); return QuickshellTracked::instance()->screens.size();
} }
@ -200,6 +177,12 @@ void QuickshellGlobal::reload(bool hard) {
root->reloadGraph(hard); root->reloadGraph(hard);
} }
QString QuickshellGlobal::shellRoot() const {
auto* generation = EngineGeneration::findObjectGeneration(this);
// already canonical
return generation->rootPath.path();
}
QString QuickshellGlobal::workingDirectory() const { // NOLINT QString QuickshellGlobal::workingDirectory() const { // NOLINT
return QuickshellSettings::instance()->workingDirectory(); return QuickshellSettings::instance()->workingDirectory();
} }
@ -230,22 +213,6 @@ void QuickshellGlobal::onClipboardChanged(QClipboard::Mode mode) {
if (mode == QClipboard::Clipboard) emit this->clipboardTextChanged(); if (mode == QClipboard::Clipboard) emit this->clipboardTextChanged();
} }
QString QuickshellGlobal::shellDir() const {
return EngineGeneration::findObjectGeneration(this)->rootPath.path();
}
QString QuickshellGlobal::configDir() const {
qWarning() << "Quickshell.configDir is deprecated and may be removed in a future release. Use "
"Quickshell.shellDir.";
return this->shellDir();
}
QString QuickshellGlobal::shellRoot() const {
qWarning() << "Quickshell.shellRoot is deprecated and may be removed in a future release. Use "
"Quickshell.shellDir.";
return this->shellDir();
}
QString QuickshellGlobal::dataDir() const { // NOLINT QString QuickshellGlobal::dataDir() const { // NOLINT
return QsPaths::instance()->shellDataDir().path(); return QsPaths::instance()->shellDataDir().path();
} }
@ -258,16 +225,6 @@ QString QuickshellGlobal::cacheDir() const { // NOLINT
return QsPaths::instance()->shellCacheDir().path(); return QsPaths::instance()->shellCacheDir().path();
} }
QString QuickshellGlobal::shellPath(const QString& path) const {
return this->shellDir() % '/' % path;
}
QString QuickshellGlobal::configPath(const QString& path) const {
qWarning() << "Quickshell.configPath() is deprecated and may be removed in a future release. Use "
"Quickshell.shellPath().";
return this->shellPath(path);
}
QString QuickshellGlobal::dataPath(const QString& path) const { QString QuickshellGlobal::dataPath(const QString& path) const {
return this->dataDir() % '/' % path; return this->dataDir() % '/' % path;
} }
@ -287,39 +244,6 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT
return qEnvironmentVariable(vstr.data()); return qEnvironmentVariable(vstr.data());
} }
void QuickshellGlobal::execDetached(QList<QString> command) {
QuickshellGlobal::execDetached(qs::io::process::ProcessContext(std::move(command)));
}
void QuickshellGlobal::execDetached(const qs::io::process::ProcessContext& context) {
if (context.command.isEmpty()) {
qWarning() << "Cannot start process as command is empty.";
return;
}
const auto& cmd = context.command.first();
auto args = context.command.sliced(1);
QProcess process;
qs::io::process::setupProcessEnvironment(&process, context.clearEnvironment, context.environment);
if (!context.workingDirectory.isEmpty()) {
process.setWorkingDirectory(context.workingDirectory);
}
process.setProgram(cmd);
process.setArguments(args);
process.setStandardInputFile(QProcess::nullDevice());
if (context.unbindStdout) {
process.setStandardOutputFile(QProcess::nullDevice());
process.setStandardErrorFile(QProcess::nullDevice());
}
process.startDetached();
}
QString QuickshellGlobal::iconPath(const QString& icon) { QString QuickshellGlobal::iconPath(const QString& icon) {
return IconImageProvider::requestString(icon); return IconImageProvider::requestString(icon);
} }
@ -333,16 +257,6 @@ QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback)
return IconImageProvider::requestString(icon, "", fallback); return IconImageProvider::requestString(icon, "", fallback);
} }
bool QuickshellGlobal::hasThemeIcon(const QString& icon) { return QIcon::hasThemeIcon(icon); }
bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor, const QStringList& features) {
return qs::scan::env::PreprocEnv::hasVersion(major, minor, features);
}
bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor) {
return QuickshellGlobal::hasVersion(major, minor, QStringList());
}
QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) { QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) {
auto* qsg = new QuickshellGlobal(); auto* qsg = new QuickshellGlobal();
auto* generation = EngineGeneration::findEngineGeneration(engine); auto* generation = EngineGeneration::findEngineGeneration(engine);

View file

@ -2,9 +2,7 @@
#include <qclipboard.h> #include <qclipboard.h>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qhash.h>
#include <qjsengine.h> #include <qjsengine.h>
#include <qlist.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
@ -15,9 +13,6 @@
#include <qvariant.h> #include <qvariant.h>
#include <qwindowdefs.h> #include <qwindowdefs.h>
#include "../io/processcore.hpp"
#include "doc.hpp"
#include "instanceinfo.hpp"
#include "qmlscreen.hpp" #include "qmlscreen.hpp"
///! Accessor for some options under the Quickshell type. ///! Accessor for some options under the Quickshell type.
@ -84,21 +79,6 @@ class QuickshellGlobal: public QObject {
// clang-format off // clang-format off
/// Quickshell's process id. /// Quickshell's process id.
Q_PROPERTY(qint32 processId READ processId CONSTANT); Q_PROPERTY(qint32 processId READ processId CONSTANT);
/// A unique identifier for this Quickshell instance
Q_PROPERTY(QString instanceId READ instanceId CONSTANT)
/// The shell ID, used to differentiate between different shell configurations.
///
/// Defaults to a stable value derived from the config path.
/// Can be overridden with `//@ pragma ShellId <id>` in the root qml file.
Q_PROPERTY(QString shellId READ shellId CONSTANT)
/// The desktop application ID.
///
/// Defaults to `org.quickshell`.
/// Can be overridden with `//@ pragma AppId <id>` in the root qml file
/// or the `QS_APP_ID` environment variable.
Q_PROPERTY(QString appId READ appId CONSTANT)
/// The time at which this Quickshell instance was launched.
Q_PROPERTY(QDateTime launchTime READ launchTime CONSTANT)
/// All currently connected screens. /// All currently connected screens.
/// ///
/// This property updates as connected screens change. /// This property updates as connected screens change.
@ -124,10 +104,6 @@ class QuickshellGlobal: public QObject {
/// ///
/// The root directory is the folder containing the entrypoint to your shell, often referred /// The root directory is the folder containing the entrypoint to your shell, often referred
/// to as `shell.qml`. /// to as `shell.qml`.
Q_PROPERTY(QString shellDir READ shellDir CONSTANT);
/// > [!WARNING] Deprecated: Renamed to @@shellDir for clarity.
Q_PROPERTY(QString configDir READ configDir CONSTANT);
/// > [!WARNING] Deprecated: Renamed to @@shellDir for consistency.
Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT); Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT);
/// Quickshell's working directory. Defaults to whereever quickshell was launched from. /// Quickshell's working directory. Defaults to whereever quickshell was launched from.
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged); Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged);
@ -143,21 +119,18 @@ class QuickshellGlobal: public QObject {
/// Usually `~/.local/share/quickshell/by-shell/<shell-id>` /// Usually `~/.local/share/quickshell/by-shell/<shell-id>`
/// ///
/// Can be overridden using `//@ pragma DataDir $BASE/path` in the root qml file, where `$BASE` /// Can be overridden using `//@ pragma DataDir $BASE/path` in the root qml file, where `$BASE`
/// corresponds to `$XDG_DATA_HOME` (usually `~/.local/share`). /// corrosponds to `$XDG_DATA_HOME` (usually `~/.local/share`).
Q_PROPERTY(QString dataDir READ dataDir CONSTANT); Q_PROPERTY(QString dataDir READ dataDir CONSTANT);
/// The per-shell state directory. /// The per-shell state directory.
/// ///
/// Usually `~/.local/state/quickshell/by-shell/<shell-id>` /// Usually `~/.local/state/quickshell/by-shell/<shell-id>`
/// ///
/// Can be overridden using `//@ pragma StateDir $BASE/path` in the root qml file, where `$BASE` /// Can be overridden using `//@ pragma StateDir $BASE/path` in the root qml file, where `$BASE`
/// corresponds to `$XDG_STATE_HOME` (usually `~/.local/state`). /// corrosponds to `$XDG_STATE_HOME` (usually `~/.local/state`).
Q_PROPERTY(QString stateDir READ stateDir CONSTANT); Q_PROPERTY(QString stateDir READ stateDir CONSTANT);
/// The per-shell cache directory. /// The per-shell cache directory.
/// ///
/// Usually `~/.cache/quickshell/by-shell/<shell-id>` /// Usually `~/.cache/quickshell/by-shell/<shell-id>`
///
/// Can be overridden using `//@ pragma CacheDir $BASE/path` in the root qml file, where `$BASE`
/// corresponds to `$XDG_CACHE_HOME` (usually `~/.cache`).
Q_PROPERTY(QString cacheDir READ cacheDir CONSTANT); Q_PROPERTY(QString cacheDir READ cacheDir CONSTANT);
// clang-format on // clang-format on
QML_SINGLETON; QML_SINGLETON;
@ -165,10 +138,6 @@ class QuickshellGlobal: public QObject {
public: public:
[[nodiscard]] qint32 processId() const; [[nodiscard]] qint32 processId() const;
[[nodiscard]] QString instanceId() const;
[[nodiscard]] QString shellId() const;
[[nodiscard]] QString appId() const;
[[nodiscard]] QDateTime launchTime() const;
QQmlListProperty<QuickshellScreenInfo> screens(); QQmlListProperty<QuickshellScreenInfo> screens();
@ -183,30 +152,6 @@ public:
/// Returns the string value of an environment variable or null if it is not set. /// Returns the string value of an environment variable or null if it is not set.
Q_INVOKABLE QVariant env(const QString& variable); Q_INVOKABLE QVariant env(const QString& variable);
// MUST be before execDetached(ctx) or the other will be called with a default constructed obj.
QSDOC_HIDE Q_INVOKABLE static void execDetached(QList<QString> command);
/// Launch a process detached from Quickshell.
///
/// The context parameter can either be a list of command arguments or a JS object with the following fields:
/// - `command`: A list containing the command and all its arguments. See @@Quickshell.Io.Process.command.
/// - `environment`: Changes to make to the process environment. See @@Quickshell.Io.Process.environment.
/// - `clearEnvironment`: Removes all variables from the environment if true.
/// - `workingDirectory`: The working directory the command should run in.
///
/// > [!WARNING] This does not run command in a shell. All arguments to the command
/// > must be in separate values in the list, e.g. `["echo", "hello"]`
/// > and not `["echo hello"]`.
/// >
/// > Additionally, shell scripts must be run by your shell,
/// > e.g. `["sh", "script.sh"]` instead of `["script.sh"]` unless the script
/// > has a shebang.
///
/// > [!INFO] You can use `["sh", "-c", <your command>]` to execute your command with
/// > the system shell.
///
/// This function is equivalent to @@Quickshell.Io.Process.startDetached().
Q_INVOKABLE static void execDetached(const qs::io::process::ProcessContext& context);
/// Returns a string usable for a @@QtQuick.Image.source for a given system icon. /// Returns a string usable for a @@QtQuick.Image.source for a given system icon.
/// ///
/// > [!INFO] By default, icons are loaded from the theme selected by the qt platform theme, /// > [!INFO] By default, icons are loaded from the theme selected by the qt platform theme,
@ -222,12 +167,6 @@ public:
/// Setting the `fallback` parameter of `iconPath` will attempt to load the fallback /// Setting the `fallback` parameter of `iconPath` will attempt to load the fallback
/// icon if the requested one could not be loaded. /// icon if the requested one could not be loaded.
Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback); Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback);
/// Check if specified icon has an available icon in your icon theme
Q_INVOKABLE static bool hasThemeIcon(const QString& icon);
/// Equivalent to `${Quickshell.configDir}/${path}`
Q_INVOKABLE [[nodiscard]] QString shellPath(const QString& path) const;
/// > [!WARNING] Deprecated: Renamed to @@shellPath() for clarity.
Q_INVOKABLE [[nodiscard]] QString configPath(const QString& path) const;
/// Equivalent to `${Quickshell.dataDir}/${path}` /// Equivalent to `${Quickshell.dataDir}/${path}`
Q_INVOKABLE [[nodiscard]] QString dataPath(const QString& path) const; Q_INVOKABLE [[nodiscard]] QString dataPath(const QString& path) const;
/// Equivalent to `${Quickshell.stateDir}/${path}` /// Equivalent to `${Quickshell.stateDir}/${path}`
@ -239,27 +178,10 @@ public:
/// ///
/// The popup can also be blocked by setting `QS_NO_RELOAD_POPUP=1`. /// The popup can also be blocked by setting `QS_NO_RELOAD_POPUP=1`.
Q_INVOKABLE void inhibitReloadPopup() { this->mInhibitReloadPopup = true; } Q_INVOKABLE void inhibitReloadPopup() { this->mInhibitReloadPopup = true; }
/// Check if Quickshell's version is at least `major.minor` and the listed
/// unreleased features are available. If Quickshell is newer than the given version
/// it is assumed that all unreleased features are present. The unreleased feature list
/// may be omitted.
///
/// > [!NOTE] You can feature gate code blocks using Quickshell's preprocessor which
/// > has the same function available.
/// >
/// > ```qml
/// > //@ if hasVersion(0, 3, ["feature"])
/// > ...
/// > //@ endif
/// > ```
Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor, const QStringList& features);
Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor);
void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; } void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; }
[[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; } [[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; }
[[nodiscard]] QString shellDir() const;
[[nodiscard]] QString configDir() const;
[[nodiscard]] QString shellRoot() const; [[nodiscard]] QString shellRoot() const;
[[nodiscard]] QString workingDirectory() const; [[nodiscard]] QString workingDirectory() const;

View file

@ -1,7 +1,6 @@
#include "qsintercept.hpp" #include "qsintercept.hpp"
#include <cstring> #include <cstring>
#include <qdir.h>
#include <qhash.h> #include <qhash.h>
#include <qiodevice.h> #include <qiodevice.h>
#include <qlogging.h> #include <qlogging.h>
@ -15,9 +14,7 @@
#include <qtypes.h> #include <qtypes.h>
#include <qurl.h> #include <qurl.h>
#include "logcat.hpp" Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg);
QS_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg);
QUrl QsUrlInterceptor::intercept( QUrl QsUrlInterceptor::intercept(
const QUrl& originalUrl, const QUrl& originalUrl,
@ -26,45 +23,28 @@ QUrl QsUrlInterceptor::intercept(
auto url = originalUrl; auto url = originalUrl;
if (url.scheme() == "root") { if (url.scheme() == "root") {
url.setScheme("qs"); url.setScheme("qsintercept");
auto path = url.path(); auto path = url.path();
if (path.startsWith('/')) path = path.sliced(1); if (path.startsWith('/')) path = path.sliced(1);
url.setPath("@/qs/" % path); url.setPath(this->configRoot.filePath(path));
qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url; qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url;
} }
if (url.scheme() == "qs") {
auto path = url.path();
// Our import path is on "qs:@/".
// We want to blackhole any import resolution outside of the config folder as it breaks Qt
// but NOT file lookups that might be on "qs:/" due to a missing "file:/" prefix.
if (path.startsWith("@/qs/")) {
path = this->configRoot.filePath(path.sliced(5));
} else if (!path.startsWith("/")) {
qCDebug(logQsIntercept) << "Blackholed import URL" << url;
return QUrl("qrc:/qs-blackhole");
}
// Some types such as Image take into account where they are loading from, and force // Some types such as Image take into account where they are loading from, and force
// asynchronous loading over a network. qs: is considered to be over a network. // asynchronous loading over a network. qsintercept is considered to be over a network.
// In those cases we want to return a file:// url so asynchronous loading is not forced. if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") {
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString) {
// Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which // Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which
// case we want to keep the intercept, otherwise objects created from those paths // case we want to keep the intercept, otherwise objects created from those paths
// will not be able to use singletons. // will not be able to use singletons.
if (path.endsWith(".qml")) return url; if (url.path().endsWith(".qml")) return url;
auto newUrl = url; auto newUrl = url;
newUrl.setScheme("file"); newUrl.setScheme("file");
// above check asserts path starts with /qs/
newUrl.setPath(path);
qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl; qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl;
return newUrl; return newUrl;
} }
}
return url; return url;
} }
@ -85,12 +65,10 @@ qint64 QsInterceptDataReply::readData(char* data, qint64 maxSize) {
} }
QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager( QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager(
const QDir& configRoot,
const QHash<QString, QString>& fileIntercepts, const QHash<QString, QString>& fileIntercepts,
QObject* parent QObject* parent
) )
: QNetworkAccessManager(parent) : QNetworkAccessManager(parent)
, configRoot(configRoot)
, fileIntercepts(fileIntercepts) {} , fileIntercepts(fileIntercepts) {}
QNetworkReply* QsInterceptNetworkAccessManager::createRequest( QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
@ -99,26 +77,19 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
QIODevice* outgoingData QIODevice* outgoingData
) { ) {
auto url = req.url(); auto url = req.url();
if (url.scheme() == "qsintercept") {
if (url.scheme() == "qs") {
auto path = url.path(); auto path = url.path();
if (path.startsWith("@/qs/")) path = this->configRoot.filePath(path.sliced(5));
// otherwise pass through to fs
qCDebug(logQsIntercept) << "Got intercept for" << path << "contains" qCDebug(logQsIntercept) << "Got intercept for" << path << "contains"
<< this->fileIntercepts.value(path); << this->fileIntercepts.value(path);
auto data = this->fileIntercepts.value(path);
if (auto data = this->fileIntercepts.value(path); !data.isEmpty()) { if (data != nullptr) {
return new QsInterceptDataReply(data, this); return new QsInterceptDataReply(data, this);
} }
auto fileReq = req; auto fileReq = req;
auto fileUrl = req.url(); auto fileUrl = req.url();
fileUrl.setScheme("file"); fileUrl.setScheme("file");
fileUrl.setPath(path);
qCDebug(logQsIntercept) << "Passing through intercept" << url << "to" << fileUrl; qCDebug(logQsIntercept) << "Passing through intercept" << url << "to" << fileUrl;
fileReq.setUrl(fileUrl); fileReq.setUrl(fileUrl);
return this->QNetworkAccessManager::createRequest(op, fileReq, outgoingData); return this->QNetworkAccessManager::createRequest(op, fileReq, outgoingData);
} }
@ -127,5 +98,5 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
} }
QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) { QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) {
return new QsInterceptNetworkAccessManager(this->configRoot, this->fileIntercepts, parent); return new QsInterceptNetworkAccessManager(this->fileIntercepts, parent);
} }

View file

@ -10,9 +10,7 @@
#include <qqmlnetworkaccessmanagerfactory.h> #include <qqmlnetworkaccessmanagerfactory.h>
#include <qurl.h> #include <qurl.h>
#include "logcat.hpp" Q_DECLARE_LOGGING_CATEGORY(logQsIntercept);
QS_DECLARE_LOGGING_CATEGORY(logQsIntercept);
class QsUrlInterceptor: public QQmlAbstractUrlInterceptor { class QsUrlInterceptor: public QQmlAbstractUrlInterceptor {
public: public:
@ -45,7 +43,6 @@ class QsInterceptNetworkAccessManager: public QNetworkAccessManager {
public: public:
QsInterceptNetworkAccessManager( QsInterceptNetworkAccessManager(
const QDir& configRoot,
const QHash<QString, QString>& fileIntercepts, const QHash<QString, QString>& fileIntercepts,
QObject* parent = nullptr QObject* parent = nullptr
); );
@ -58,21 +55,15 @@ protected:
) override; ) override;
private: private:
QDir configRoot;
const QHash<QString, QString>& fileIntercepts; const QHash<QString, QString>& fileIntercepts;
}; };
class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory { class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory {
public: public:
QsInterceptNetworkAccessManagerFactory( QsInterceptNetworkAccessManagerFactory(const QHash<QString, QString>& fileIntercepts)
const QDir& configRoot, : fileIntercepts(fileIntercepts) {}
const QHash<QString, QString>& fileIntercepts
)
: configRoot(configRoot)
, fileIntercepts(fileIntercepts) {}
QNetworkAccessManager* create(QObject* parent) override; QNetworkAccessManager* create(QObject* parent) override;
private: private:
QDir configRoot;
const QHash<QString, QString>& fileIntercepts; const QHash<QString, QString>& fileIntercepts;
}; };

View file

@ -46,8 +46,8 @@ class QsMenuHandle: public QObject {
public: public:
explicit QsMenuHandle(QObject* parent): QObject(parent) {} explicit QsMenuHandle(QObject* parent): QObject(parent) {}
virtual void refHandle() {} virtual void refHandle() {};
virtual void unrefHandle() {} virtual void unrefHandle() {};
[[nodiscard]] virtual QsMenuEntry* menu() = 0; [[nodiscard]] virtual QsMenuEntry* menu() = 0;

View file

@ -1,5 +1,4 @@
#include "region.hpp" #include "region.hpp"
#include <algorithm>
#include <cmath> #include <cmath>
#include <qobject.h> #include <qobject.h>
@ -19,17 +18,10 @@ PendingRegion::PendingRegion(QObject* parent): QObject(parent) {
QObject::connect(this, &PendingRegion::yChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::yChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::widthChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::widthChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::heightChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::heightChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::radiusChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::topLeftRadiusChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::topRightRadiusChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::bottomLeftRadiusChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::bottomRightRadiusChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::childrenChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::childrenChanged, this, &PendingRegion::changed);
} }
void PendingRegion::setItem(QQuickItem* item) { void PendingRegion::setItem(QQuickItem* item) {
if (item == this->mItem) return;
if (this->mItem != nullptr) { if (this->mItem != nullptr) {
QObject::disconnect(this->mItem, nullptr, this, nullptr); QObject::disconnect(this->mItem, nullptr, this, nullptr);
} }
@ -51,79 +43,6 @@ void PendingRegion::onItemDestroyed() { this->mItem = nullptr; }
void PendingRegion::onChildDestroyed() { this->mRegions.removeAll(this->sender()); } void PendingRegion::onChildDestroyed() { this->mRegions.removeAll(this->sender()); }
qint32 PendingRegion::radius() const { return this->mRadius; }
void PendingRegion::setRadius(qint32 radius) {
if (radius == this->mRadius) return;
this->mRadius = radius;
emit this->radiusChanged();
if (!(this->mCornerOverrides & TopLeft)) emit this->topLeftRadiusChanged();
if (!(this->mCornerOverrides & TopRight)) emit this->topRightRadiusChanged();
if (!(this->mCornerOverrides & BottomLeft)) emit this->bottomLeftRadiusChanged();
if (!(this->mCornerOverrides & BottomRight)) emit this->bottomRightRadiusChanged();
}
qint32 PendingRegion::topLeftRadius() const {
return (this->mCornerOverrides & TopLeft) ? this->mTopLeftRadius : this->mRadius;
}
void PendingRegion::setTopLeftRadius(qint32 radius) {
this->mTopLeftRadius = radius;
this->mCornerOverrides |= TopLeft;
emit this->topLeftRadiusChanged();
}
void PendingRegion::resetTopLeftRadius() {
this->mCornerOverrides &= ~TopLeft;
emit this->topLeftRadiusChanged();
}
qint32 PendingRegion::topRightRadius() const {
return (this->mCornerOverrides & TopRight) ? this->mTopRightRadius : this->mRadius;
}
void PendingRegion::setTopRightRadius(qint32 radius) {
this->mTopRightRadius = radius;
this->mCornerOverrides |= TopRight;
emit this->topRightRadiusChanged();
}
void PendingRegion::resetTopRightRadius() {
this->mCornerOverrides &= ~TopRight;
emit this->topRightRadiusChanged();
}
qint32 PendingRegion::bottomLeftRadius() const {
return (this->mCornerOverrides & BottomLeft) ? this->mBottomLeftRadius : this->mRadius;
}
void PendingRegion::setBottomLeftRadius(qint32 radius) {
this->mBottomLeftRadius = radius;
this->mCornerOverrides |= BottomLeft;
emit this->bottomLeftRadiusChanged();
}
void PendingRegion::resetBottomLeftRadius() {
this->mCornerOverrides &= ~BottomLeft;
emit this->bottomLeftRadiusChanged();
}
qint32 PendingRegion::bottomRightRadius() const {
return (this->mCornerOverrides & BottomRight) ? this->mBottomRightRadius : this->mRadius;
}
void PendingRegion::setBottomRightRadius(qint32 radius) {
this->mBottomRightRadius = radius;
this->mCornerOverrides |= BottomRight;
emit this->bottomRightRadiusChanged();
}
void PendingRegion::resetBottomRightRadius() {
this->mCornerOverrides &= ~BottomRight;
emit this->bottomRightRadiusChanged();
}
QQmlListProperty<PendingRegion> PendingRegion::regions() { QQmlListProperty<PendingRegion> PendingRegion::regions() {
return QQmlListProperty<PendingRegion>( return QQmlListProperty<PendingRegion>(
this, this,
@ -169,60 +88,6 @@ QRegion PendingRegion::build() const {
region = QRegion(this->mX, this->mY, this->mWidth, this->mHeight, type); region = QRegion(this->mX, this->mY, this->mWidth, this->mHeight, type);
} }
if (this->mShape == RegionShape::Rect && !region.isEmpty()) {
auto tl = std::max(this->topLeftRadius(), 0);
auto tr = std::max(this->topRightRadius(), 0);
auto bl = std::max(this->bottomLeftRadius(), 0);
auto br = std::max(this->bottomRightRadius(), 0);
if (tl > 0 || tr > 0 || bl > 0 || br > 0) {
auto rect = region.boundingRect();
auto x = rect.x();
auto y = rect.y();
auto w = rect.width();
auto h = rect.height();
// Normalize so adjacent corners don't exceed their shared edge.
// Each corner is scaled by the tightest constraint of its two edges.
auto topScale = tl + tr > w ? static_cast<double>(w) / (tl + tr) : 1.0;
auto bottomScale = bl + br > w ? static_cast<double>(w) / (bl + br) : 1.0;
auto leftScale = tl + bl > h ? static_cast<double>(h) / (tl + bl) : 1.0;
auto rightScale = tr + br > h ? static_cast<double>(h) / (tr + br) : 1.0;
tl = static_cast<qint32>(tl * std::min(topScale, leftScale));
tr = static_cast<qint32>(tr * std::min(topScale, rightScale));
bl = static_cast<qint32>(bl * std::min(bottomScale, leftScale));
br = static_cast<qint32>(br * std::min(bottomScale, rightScale));
// Unlock each corner: subtract (cornerBox - quarterEllipse) from the
// full rect. Each corner only modifies pixels inside its own box,
// so no diagonal overlap is possible.
if (tl > 0) {
auto box = QRegion(x, y, tl, tl);
auto ellipse = QRegion(x, y, tl * 2, tl * 2, QRegion::Ellipse);
region -= box - (ellipse & box);
}
if (tr > 0) {
auto box = QRegion(x + w - tr, y, tr, tr);
auto ellipse = QRegion(x + w - tr * 2, y, tr * 2, tr * 2, QRegion::Ellipse);
region -= box - (ellipse & box);
}
if (bl > 0) {
auto box = QRegion(x, y + h - bl, bl, bl);
auto ellipse = QRegion(x, y + h - bl * 2, bl * 2, bl * 2, QRegion::Ellipse);
region -= box - (ellipse & box);
}
if (br > 0) {
auto box = QRegion(x + w - br, y + h - br, br, br);
auto ellipse = QRegion(x + w - br * 2, y + h - br * 2, br * 2, br * 2, QRegion::Ellipse);
region -= box - (ellipse & box);
}
}
}
for (const auto& childRegion: this->mRegions) { for (const auto& childRegion: this->mRegions) {
region = childRegion->applyTo(region); region = childRegion->applyTo(region);
} }

View file

@ -66,29 +66,6 @@ class PendingRegion: public QObject {
Q_PROPERTY(qint32 width MEMBER mWidth NOTIFY widthChanged); Q_PROPERTY(qint32 width MEMBER mWidth NOTIFY widthChanged);
/// Defaults to 0. Does nothing if @@item is set. /// Defaults to 0. Does nothing if @@item is set.
Q_PROPERTY(qint32 height MEMBER mHeight NOTIFY heightChanged); Q_PROPERTY(qint32 height MEMBER mHeight NOTIFY heightChanged);
// clang-format off
/// Corner radius for rounded rectangles. Only applies when @@shape is `Rect`. Defaults to 0.
///
/// Acts as the default for @@topLeftRadius, @@topRightRadius, @@bottomLeftRadius,
/// and @@bottomRightRadius.
Q_PROPERTY(qint32 radius READ radius WRITE setRadius NOTIFY radiusChanged);
/// Top-left corner radius. Only applies when @@shape is `Rect`.
///
/// Defaults to @@radius, and may be reset by assigning `undefined`.
Q_PROPERTY(qint32 topLeftRadius READ topLeftRadius WRITE setTopLeftRadius RESET resetTopLeftRadius NOTIFY topLeftRadiusChanged);
/// Top-right corner radius. Only applies when @@shape is `Rect`.
///
/// Defaults to @@radius, and may be reset by assigning `undefined`.
Q_PROPERTY(qint32 topRightRadius READ topRightRadius WRITE setTopRightRadius RESET resetTopRightRadius NOTIFY topRightRadiusChanged);
/// Bottom-left corner radius. Only applies when @@shape is `Rect`.
///
/// Defaults to @@radius, and may be reset by assigning `undefined`.
Q_PROPERTY(qint32 bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius RESET resetBottomLeftRadius NOTIFY bottomLeftRadiusChanged);
/// Bottom-right corner radius. Only applies when @@shape is `Rect`.
///
/// Defaults to @@radius, and may be reset by assigning `undefined`.
Q_PROPERTY(qint32 bottomRightRadius READ bottomRightRadius WRITE setBottomRightRadius RESET resetBottomRightRadius NOTIFY bottomRightRadiusChanged);
// clang-format on
/// Regions to apply on top of this region. /// Regions to apply on top of this region.
/// ///
@ -114,25 +91,6 @@ public:
void setItem(QQuickItem* item); void setItem(QQuickItem* item);
[[nodiscard]] qint32 radius() const;
void setRadius(qint32 radius);
[[nodiscard]] qint32 topLeftRadius() const;
void setTopLeftRadius(qint32 radius);
void resetTopLeftRadius();
[[nodiscard]] qint32 topRightRadius() const;
void setTopRightRadius(qint32 radius);
void resetTopRightRadius();
[[nodiscard]] qint32 bottomLeftRadius() const;
void setBottomLeftRadius(qint32 radius);
void resetBottomLeftRadius();
[[nodiscard]] qint32 bottomRightRadius() const;
void setBottomRightRadius(qint32 radius);
void resetBottomRightRadius();
QQmlListProperty<PendingRegion> regions(); QQmlListProperty<PendingRegion> regions();
[[nodiscard]] bool empty() const; [[nodiscard]] bool empty() const;
@ -151,11 +109,6 @@ signals:
void yChanged(); void yChanged();
void widthChanged(); void widthChanged();
void heightChanged(); void heightChanged();
void radiusChanged();
void topLeftRadiusChanged();
void topRightRadiusChanged();
void bottomLeftRadiusChanged();
void bottomRightRadiusChanged();
void childrenChanged(); void childrenChanged();
/// Triggered when the region's geometry changes. /// Triggered when the region's geometry changes.
@ -177,25 +130,12 @@ private:
static void static void
regionsReplace(QQmlListProperty<PendingRegion>* prop, qsizetype i, PendingRegion* region); regionsReplace(QQmlListProperty<PendingRegion>* prop, qsizetype i, PendingRegion* region);
enum CornerOverride : quint8 {
TopLeft = 0b1,
TopRight = 0b10,
BottomLeft = 0b100,
BottomRight = 0b1000,
};
QQuickItem* mItem = nullptr; QQuickItem* mItem = nullptr;
qint32 mX = 0; qint32 mX = 0;
qint32 mY = 0; qint32 mY = 0;
qint32 mWidth = 0; qint32 mWidth = 0;
qint32 mHeight = 0; qint32 mHeight = 0;
qint32 mRadius = 0;
qint32 mTopLeftRadius = 0;
qint32 mTopRightRadius = 0;
qint32 mBottomLeftRadius = 0;
qint32 mBottomRightRadius = 0;
quint8 mCornerOverrides = 0;
QList<PendingRegion*> mRegions; QList<PendingRegion*> mRegions;
}; };

View file

@ -126,21 +126,12 @@ QObject* Reloadable::getChildByReloadId(QObject* parent, const QString& reloadId
return nullptr; return nullptr;
} }
void PostReloadHook::componentComplete() { void PostReloadHook::postReloadTree(QObject* root) {
auto* engineGeneration = EngineGeneration::findObjectGeneration(this); for (auto* child: root->children()) {
if (!engineGeneration || engineGeneration->reloadComplete) this->postReload(); PostReloadHook::postReloadTree(child);
else {
// disconnected by EngineGeneration::postReload
QObject::connect(
engineGeneration,
&EngineGeneration::firePostReload,
this,
&PostReloadHook::postReload
);
}
} }
void PostReloadHook::postReload() { if (auto* self = dynamic_cast<PostReloadHook*>(root)) {
this->isPostReload = true; self->onPostReload();
this->onPostReload(); }
} }

View file

@ -57,7 +57,7 @@ public:
void reload(QObject* oldInstance = nullptr); void reload(QObject* oldInstance = nullptr);
void classBegin() override {} void classBegin() override {};
void componentComplete() override; void componentComplete() override;
// Reload objects in the parent->child graph recursively. // Reload objects in the parent->child graph recursively.
@ -119,23 +119,16 @@ private:
}; };
/// Hook that runs after the old widget tree is dropped during a reload. /// Hook that runs after the old widget tree is dropped during a reload.
class PostReloadHook class PostReloadHook {
: public QObject
, public QQmlParserStatus {
Q_OBJECT;
QML_ANONYMOUS;
Q_INTERFACES(QQmlParserStatus);
public: public:
PostReloadHook(QObject* parent = nullptr): QObject(parent) {} PostReloadHook() = default;
void classBegin() override {} virtual ~PostReloadHook() = default;
void componentComplete() override; PostReloadHook(PostReloadHook&&) = default;
PostReloadHook(const PostReloadHook&) = default;
PostReloadHook& operator=(PostReloadHook&&) = default;
PostReloadHook& operator=(const PostReloadHook&) = default;
virtual void onPostReload() = 0; virtual void onPostReload() = 0;
public slots: static void postReloadTree(QObject* root);
void postReload();
protected:
bool isPostReload = false;
}; };

View file

@ -4,7 +4,6 @@
#include <qdir.h> #include <qdir.h>
#include <qfileinfo.h> #include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qlogging.h> #include <qlogging.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlcomponent.h> #include <qqmlcomponent.h>
@ -19,26 +18,15 @@
#include "instanceinfo.hpp" #include "instanceinfo.hpp"
#include "qmlglobal.hpp" #include "qmlglobal.hpp"
#include "scan.hpp" #include "scan.hpp"
#include "toolsupport.hpp"
RootWrapper::RootWrapper(QString rootPath, QString shellId) RootWrapper::RootWrapper(QString rootPath, QString shellId)
: QObject(nullptr) : QObject(nullptr)
, rootPath(std::move(rootPath)) , rootPath(std::move(rootPath))
, shellId(std::move(shellId)) , shellId(std::move(shellId))
, originalWorkingDirectory(QDir::current().absolutePath()) { , originalWorkingDirectory(QDir::current().absolutePath()) {
QObject::connect( // clang-format off
QuickshellSettings::instance(), QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::watchFilesChanged, this, &RootWrapper::onWatchFilesChanged);
&QuickshellSettings::watchFilesChanged, // clang-format on
this,
&RootWrapper::onWatchFilesChanged
);
QObject::connect(
&this->configDirWatcher,
&QFileSystemWatcher::directoryChanged,
this,
&RootWrapper::updateTooling
);
this->reloadGraph(true); this->reloadGraph(true);
@ -55,13 +43,12 @@ RootWrapper::~RootWrapper() {
} }
void RootWrapper::reloadGraph(bool hard) { void RootWrapper::reloadGraph(bool hard) {
auto rootFile = QFileInfo(this->rootPath); auto rootPath = QFileInfo(this->rootPath).dir();
auto rootPath = rootFile.dir();
auto scanner = QmlScanner(rootPath); auto scanner = QmlScanner(rootPath);
scanner.scanQmlRoot(this->rootPath); scanner.scanQmlFile(this->rootPath);
qs::core::QmlToolingSupport::updateTooling(rootPath, scanner); auto* generation = new EngineGeneration(rootPath, std::move(scanner));
this->configDirWatcher.addPath(rootPath.path()); generation->wrapper = this;
// todo: move into EngineGeneration // todo: move into EngineGeneration
if (this->generation != nullptr) { if (this->generation != nullptr) {
@ -71,36 +58,9 @@ void RootWrapper::reloadGraph(bool hard) {
QDir::setCurrent(this->originalWorkingDirectory); QDir::setCurrent(this->originalWorkingDirectory);
if (!scanner.scanErrors.isEmpty()) { auto url = QUrl::fromLocalFile(this->rootPath);
qCritical() << "Failed to load configuration"; // unless the original file comes from the qsintercept scheme
QString errorString = "Failed to load configuration"; url.setScheme("qsintercept");
for (auto& error: scanner.scanErrors) {
const auto& file = error.file;
QString rel;
if (file.startsWith(rootPath.path() % '/')) {
rel = '@' % file.sliced(rootPath.path().length() + 1);
} else {
rel = file;
}
auto msg = " error in " % rel % '[' % QString::number(error.line) % ":0]: " % error.message;
errorString += '\n' % msg;
qCritical().noquote() << msg;
}
if (this->generation != nullptr && this->generation->qsgInstance != nullptr) {
emit this->generation->qsgInstance->reloadFailed(errorString);
}
return;
}
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
generation->wrapper = this;
QUrl url;
url.setScheme("qs");
url.setPath("@/qs/" % rootFile.fileName());
auto component = QQmlComponent(generation->engine, url); auto component = QQmlComponent(generation->engine, url);
if (!component.isReady()) { if (!component.isReady()) {
@ -109,9 +69,7 @@ void RootWrapper::reloadGraph(bool hard) {
auto errors = component.errors(); auto errors = component.errors();
for (auto& error: errors) { for (auto& error: errors) {
const auto& url = error.url(); auto rel = "**/" % rootPath.relativeFilePath(error.url().path());
auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5)
: url.toString();
auto msg = " caused by " % rel % '[' % QString::number(error.line()) % ':' auto msg = " caused by " % rel % '[' % QString::number(error.line()) % ':'
% QString::number(error.column()) % "]: " % error.description(); % QString::number(error.column()) % "]: " % error.description();
errorString += '\n' % msg; errorString += '\n' % msg;
@ -207,9 +165,3 @@ void RootWrapper::onWatchFilesChanged() {
} }
void RootWrapper::onWatchedFilesChanged() { this->reloadGraph(false); } void RootWrapper::onWatchedFilesChanged() { this->reloadGraph(false); }
void RootWrapper::updateTooling() {
if (!this->generation) return;
auto configDir = QFileInfo(this->rootPath).dir();
qs::core::QmlToolingSupport::updateTooling(configDir, this->generation->scanner);
}

View file

@ -1,6 +1,5 @@
#pragma once #pragma once
#include <qfilesystemwatcher.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qtclasshelpermacros.h> #include <qtclasshelpermacros.h>
@ -23,12 +22,10 @@ private slots:
void generationDestroyed(); void generationDestroyed();
void onWatchFilesChanged(); void onWatchFilesChanged();
void onWatchedFilesChanged(); void onWatchedFilesChanged();
void updateTooling();
private: private:
QString rootPath; QString rootPath;
QString shellId; QString shellId;
EngineGeneration* generation = nullptr; EngineGeneration* generation = nullptr;
QString originalWorkingDirectory; QString originalWorkingDirectory;
QFileSystemWatcher configDirWatcher;
}; };

View file

@ -1,12 +1,9 @@
#include "scan.hpp" #include "scan.hpp"
#include <cmath> #include <cmath>
#include <utility>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qcryptographichash.h>
#include <qdir.h> #include <qdir.h>
#include <qfileinfo.h> #include <qfileinfo.h>
#include <qjsengine.h>
#include <qjsonarray.h> #include <qjsonarray.h>
#include <qjsondocument.h> #include <qjsondocument.h>
#include <qjsonobject.h> #include <qjsonobject.h>
@ -15,107 +12,54 @@
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qpair.h> #include <qpair.h>
#include <qstring.h> #include <qstring.h>
#include <qstringliteral.h>
#include <qtextstream.h> #include <qtextstream.h>
#include "logcat.hpp" Q_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg);
#include "scanenv.hpp"
QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg); void QmlScanner::scanDir(const QString& path) {
if (this->scannedDirs.contains(path)) return;
bool QmlScanner::readAndHashFile(const QString& path, QByteArray& data) { this->scannedDirs.push_back(path);
auto file = QFile(path);
if (!file.open(QFile::ReadOnly)) return false;
data = file.readAll();
this->fileHashes.insert(path, QCryptographicHash::hash(data, QCryptographicHash::Md5));
return true;
}
bool QmlScanner::hasFileContentChanged(const QString& path) const {
auto it = this->fileHashes.constFind(path);
if (it == this->fileHashes.constEnd()) return true;
auto file = QFile(path);
if (!file.open(QFile::ReadOnly)) return true;
auto newHash = QCryptographicHash::hash(file.readAll(), QCryptographicHash::Md5);
return newHash != it.value();
}
void QmlScanner::scanDir(const QDir& dir) {
if (this->scannedDirs.contains(dir)) return;
this->scannedDirs.push_back(dir);
const auto& path = dir.path();
qCDebug(logQmlScanner) << "Scanning directory" << path; qCDebug(logQmlScanner) << "Scanning directory" << path;
auto dir = QDir(path);
struct Entry {
QString name;
bool singleton = false;
bool internal = false;
};
bool seenQmldir = false; bool seenQmldir = false;
auto entries = QVector<Entry>(); auto singletons = QVector<QString>();
auto entries = QVector<QString>();
for (auto& name: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) { for (auto& entry: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) {
if (name == "qmldir") { if (entry == "qmldir") {
qCDebug( qCDebug(logQmlScanner
logQmlScanner
) << "Found qmldir file, qmldir synthesization will be disabled for directory" ) << "Found qmldir file, qmldir synthesization will be disabled for directory"
<< path; << path;
seenQmldir = true; seenQmldir = true;
} else if (name.at(0).isUpper() && name.endsWith(".qml")) { } else if (entry.at(0).isUpper() && entry.endsWith(".qml")) {
auto& entry = entries.emplaceBack(); if (this->scanQmlFile(dir.filePath(entry))) {
singletons.push_back(entry);
if (this->scanQmlFile(dir.filePath(name), entry.singleton, entry.internal)) {
entry.name = name;
} else { } else {
entries.pop_back(); entries.push_back(entry);
}
} else if (name.at(0).isUpper() && name.endsWith(".qml.json")) {
if (this->scanQmlJson(dir.filePath(name))) {
entries.push_back({
.name = name.first(name.length() - 5),
.singleton = true,
});
} }
} else if (entry.at(0).isUpper() && entry.endsWith(".qml.json")) {
this->scanQmlJson(dir.filePath(entry));
singletons.push_back(entry.first(entry.length() - 5));
} }
} }
// Due to the qsintercept:// protocol a qmldir is always required, even without singletons.
if (!seenQmldir) { if (!seenQmldir) {
qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path; qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path << "singletons"
<< singletons;
QString qmldir; QString qmldir;
auto stream = QTextStream(&qmldir); auto stream = QTextStream(&qmldir);
// cant derive a module name if not in shell path for (auto& singleton: singletons) {
if (path.startsWith(this->rootPath.path())) { stream << "singleton " << singleton.sliced(0, singleton.length() - 4) << " 1.0 " << singleton
auto end = path.sliced(this->rootPath.path().length()); << "\n";
// verify we have a valid module name.
for (auto& c: end) {
if (c == '/') c = '.';
else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|| c == '_')
{
} else {
qCWarning(logQmlScanner) << "Module path contains invalid characters for a module name: "
<< path.sliced(this->rootPath.path().length());
goto skipadd;
}
} }
stream << "module qs" << end << '\n'; for (auto& entry: entries) {
skipadd:; stream << entry.sliced(0, entry.length() - 4) << " 1.0 " << entry << "\n";
} else {
qCWarning(logQmlScanner) << "Module path" << path << "is outside of the config folder.";
}
for (const auto& entry: entries) {
if (entry.internal) stream << "internal ";
if (entry.singleton) stream << "singleton ";
stream << entry.name.sliced(0, entry.name.length() - 4) << " 1.0 " << entry.name << '\n';
} }
qCDebug(logQmlScanner) << "Synthesized qmldir for" << path << qPrintable("\n" + qmldir); qCDebug(logQmlScanner) << "Synthesized qmldir for" << path << qPrintable("\n" + qmldir);
@ -123,131 +67,50 @@ void QmlScanner::scanDir(const QDir& dir) {
} }
} }
bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& internal) { bool QmlScanner::scanQmlFile(const QString& path) {
if (this->scannedFiles.contains(path)) return false; if (this->scannedFiles.contains(path)) return false;
this->scannedFiles.push_back(path); this->scannedFiles.push_back(path);
qCDebug(logQmlScanner) << "Scanning qml file" << path; qCDebug(logQmlScanner) << "Scanning qml file" << path;
QByteArray fileData; auto file = QFile(path);
if (!this->readAndHashFile(path, fileData)) { if (!file.open(QFile::ReadOnly | QFile::Text)) {
qCWarning(logQmlScanner) << "Failed to open file" << path; qCWarning(logQmlScanner) << "Failed to open file" << path;
return false; return false;
} }
auto stream = QTextStream(&fileData); auto stream = QTextStream(&file);
auto imports = QVector<QString>(); auto imports = QVector<QString>();
bool inHeader = true; bool singleton = false;
auto ifScopes = QVector<bool>();
bool sourceMasked = false;
int lineNum = 0;
QString overrideText;
bool isOverridden = false;
auto& pragmaEngine = *QmlScanner::preprocEngine();
auto postError = [&, this](QString error) {
this->scanErrors.append({.file = path, .message = std::move(error), .line = lineNum});
};
while (!stream.atEnd()) { while (!stream.atEnd()) {
++lineNum; auto line = stream.readLine().trimmed();
bool hideMask = false;
auto rawLine = stream.readLine();
auto line = rawLine.trimmed();
if (!sourceMasked && inHeader) {
if (!singleton && line == "pragma Singleton") { if (!singleton && line == "pragma Singleton") {
qCDebug(logQmlScanner) << "Discovered singleton" << path;
singleton = true; singleton = true;
} else if (line.startsWith("import")) { } else if (line.startsWith("import")) {
// we dont care about "import qs" as we always load the root folder
if (auto importCursor = line.indexOf(" qs."); importCursor != -1) {
importCursor += 4;
QString path;
while (importCursor != line.length()) { auto startQuot = line.indexOf('"');
auto c = line.at(importCursor); if (startQuot == -1 || line.length() < startQuot + 3) continue;
if (c == '.') c = '/';
else if (c == ' ') break;
else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|| c == '_')
{
} else {
qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line;
goto next;
}
path.append(c);
importCursor += 1;
}
imports.append(this->rootPath.filePath(path));
} else if (auto startQuot = line.indexOf('"');
startQuot != -1 && line.length() >= startQuot + 3)
{
auto endQuot = line.indexOf('"', startQuot + 1); auto endQuot = line.indexOf('"', startQuot + 1);
if (endQuot == -1) continue; if (endQuot == -1) continue;
auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1); auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1);
imports.push_back(name); imports.push_back(name);
} } else if (line.contains('{')) break;
} else if (!internal && line == "//@ pragma Internal") {
internal = true;
} else if (line.contains('{')) {
inHeader = false;
}
} }
if (line.startsWith("//@ if ")) { file.close();
auto code = line.sliced(7);
auto value = pragmaEngine.evaluate(code, path, 1234);
bool mask = true;
if (value.isError()) {
postError(QString("Evaluating if: %0").arg(value.toString()));
} else if (!value.isBool()) {
postError(QString("If expression \"%0\" is not a boolean").arg(value.toString()));
} else if (value.toBool()) {
mask = false;
}
if (!sourceMasked && mask) hideMask = true;
mask = sourceMasked || mask; // cant unmask if a nested if passes
ifScopes.append(mask);
if (mask) isOverridden = true;
sourceMasked = mask;
} else if (line.startsWith("//@ endif")) {
if (ifScopes.isEmpty()) {
postError("endif without matching if");
} else {
ifScopes.pop_back();
if (ifScopes.isEmpty()) sourceMasked = false;
else sourceMasked = ifScopes.last();
}
}
if (!hideMask && sourceMasked) overrideText.append("// MASKED: " % rawLine % '\n');
else overrideText.append(rawLine % '\n');
next:;
}
if (!ifScopes.isEmpty()) {
postError("unclosed preprocessor if block");
}
if (isOverridden) {
this->fileIntercepts.insert(path, overrideText);
}
if (logQmlScanner().isDebugEnabled() && !imports.isEmpty()) { if (logQmlScanner().isDebugEnabled() && !imports.isEmpty()) {
qCDebug(logQmlScanner) << "Found imports" << imports; qCDebug(logQmlScanner) << "Found imports" << imports;
} }
auto currentdir = QDir(QFileInfo(path).absolutePath()); auto currentdir = QDir(QFileInfo(path).canonicalPath());
// the root can never be a singleton so it dosent matter if we skip it // the root can never be a singleton so it dosent matter if we skip it
this->scanDir(currentdir); this->scanDir(currentdir.path());
for (auto& import: imports) { for (auto& import: imports) {
QString ipath; QString ipath;
@ -259,44 +122,31 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna
ipath = currentdir.filePath(import); ipath = currentdir.filePath(import);
} }
auto pathInfo = QFileInfo(ipath); auto cpath = QFileInfo(ipath).canonicalFilePath();
auto cpath = pathInfo.absoluteFilePath();
if (!pathInfo.exists()) { if (cpath.isEmpty()) {
qCWarning(logQmlScanner) << "Ignoring unresolvable import" << ipath << "from" << path; qCWarning(logQmlScanner) << "Ignoring unresolvable import" << ipath << "from" << path;
continue; continue;
} }
if (!pathInfo.isDir()) { if (import.endsWith(".js")) this->scannedFiles.push_back(cpath);
qCDebug(logQmlScanner) << "Ignoring non-directory import" << ipath << "from" << path; else this->scanDir(cpath);
continue;
} }
if (import.endsWith(".js")) { return singleton;
this->scannedFiles.push_back(cpath);
QByteArray jsData;
this->readAndHashFile(cpath, jsData);
} else this->scanDir(cpath);
} }
return true; void QmlScanner::scanQmlJson(const QString& path) {
}
void QmlScanner::scanQmlRoot(const QString& path) {
bool singleton = false;
bool internal = false;
this->scanQmlFile(path, singleton, internal);
}
bool QmlScanner::scanQmlJson(const QString& path) {
qCDebug(logQmlScanner) << "Scanning qml.json file" << path; qCDebug(logQmlScanner) << "Scanning qml.json file" << path;
QByteArray data; auto file = QFile(path);
if (!this->readAndHashFile(path, data)) { if (!file.open(QFile::ReadOnly | QFile::Text)) {
qCWarning(logQmlScanner) << "Failed to open file" << path; qCWarning(logQmlScanner) << "Failed to open file" << path;
return false; return;
} }
auto data = file.readAll();
// Importing this makes CI builds fail for some reason. // Importing this makes CI builds fail for some reason.
QJsonParseError error; // NOLINT (misc-include-cleaner) QJsonParseError error; // NOLINT (misc-include-cleaner)
auto json = QJsonDocument::fromJson(data, &error); auto json = QJsonDocument::fromJson(data, &error);
@ -304,7 +154,7 @@ bool QmlScanner::scanQmlJson(const QString& path) {
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
qCCritical(logQmlScanner).nospace() qCCritical(logQmlScanner).nospace()
<< "Failed to parse qml.json file at " << path << ": " << error.errorString(); << "Failed to parse qml.json file at " << path << ": " << error.errorString();
return false; return;
} }
const QString body = const QString body =
@ -314,7 +164,6 @@ bool QmlScanner::scanQmlJson(const QString& path) {
this->fileIntercepts.insert(path.first(path.length() - 5), body); this->fileIntercepts.insert(path.first(path.length() - 5), body);
this->scannedFiles.push_back(path); this->scannedFiles.push_back(path);
return true;
} }
QPair<QString, QString> QmlScanner::jsonToQml(const QJsonValue& value, int indent) { QPair<QString, QString> QmlScanner::jsonToQml(const QJsonValue& value, int indent) {
@ -367,13 +216,3 @@ QPair<QString, QString> QmlScanner::jsonToQml(const QJsonValue& value, int inden
return qMakePair(QStringLiteral("var"), "null"); return qMakePair(QStringLiteral("var"), "null");
} }
} }
QJSEngine* QmlScanner::preprocEngine() {
static auto* engine = [] {
auto* engine = new QJSEngine();
engine->globalObject().setPrototype(engine->newQObject(new qs::scan::env::PreprocEnv()));
return engine;
}();
return engine;
}

View file

@ -1,16 +1,12 @@
#pragma once #pragma once
#include <qbytearray.h>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qdir.h> #include <qdir.h>
#include <qhash.h> #include <qhash.h>
#include <qjsengine.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qvector.h> #include <qvector.h>
#include "logcat.hpp" Q_DECLARE_LOGGING_CATEGORY(logQmlScanner);
QS_DECLARE_LOGGING_CATEGORY(logQmlScanner);
// expects canonical paths // expects canonical paths
class QmlScanner { class QmlScanner {
@ -18,31 +14,17 @@ public:
QmlScanner() = default; QmlScanner() = default;
QmlScanner(const QDir& rootPath): rootPath(rootPath) {} QmlScanner(const QDir& rootPath): rootPath(rootPath) {}
void scanDir(const QDir& dir); void scanDir(const QString& path);
void scanQmlRoot(const QString& path); // returns if the file has a singleton
bool scanQmlFile(const QString& path);
QVector<QDir> scannedDirs; QVector<QString> scannedDirs;
QVector<QString> scannedFiles; QVector<QString> scannedFiles;
QHash<QString, QByteArray> fileHashes;
QHash<QString, QString> fileIntercepts; QHash<QString, QString> fileIntercepts;
struct ScanError {
QString file;
QString message;
int line;
};
QVector<ScanError> scanErrors;
bool readAndHashFile(const QString& path, QByteArray& data);
[[nodiscard]] bool hasFileContentChanged(const QString& path) const;
private: private:
QDir rootPath; QDir rootPath;
bool scanQmlFile(const QString& path, bool& singleton, bool& internal); void scanQmlJson(const QString& path);
bool scanQmlJson(const QString& path);
[[nodiscard]] static QPair<QString, QString> jsonToQml(const QJsonValue& value, int indent = 0); [[nodiscard]] static QPair<QString, QString> jsonToQml(const QJsonValue& value, int indent = 0);
static QJSEngine* preprocEngine();
}; };

View file

@ -1,31 +0,0 @@
#include "scanenv.hpp"
#include <qcontainerfwd.h>
#include <qtenvironmentvariables.h>
#include "build.hpp"
namespace qs::scan::env {
bool PreprocEnv::hasVersion(int major, int minor, const QStringList& features) {
if (QS_VERSION_MAJOR > major) return true;
if (QS_VERSION_MAJOR == major && QS_VERSION_MINOR > minor) return true;
auto availFeatures = QString(QS_UNRELEASED_FEATURES).split(';');
for (const auto& feature: features) {
if (!availFeatures.contains(feature)) return false;
}
return QS_VERSION_MAJOR == major && QS_VERSION_MINOR == minor;
}
QString PreprocEnv::env(const QString& variable) {
return qEnvironmentVariable(variable.toStdString().c_str());
}
bool PreprocEnv::isEnvSet(const QString& variable) {
return qEnvironmentVariableIsSet(variable.toStdString().c_str());
}
} // namespace qs::scan::env

View file

@ -1,20 +0,0 @@
#pragma once
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qtmetamacros.h>
namespace qs::scan::env {
class PreprocEnv: public QObject {
Q_OBJECT;
public:
Q_INVOKABLE static bool
hasVersion(int major, int minor, const QStringList& features = QStringList());
Q_INVOKABLE static QString env(const QString& variable);
Q_INVOKABLE static bool isEnvSet(const QString& variable);
};
} // namespace qs::scan::env

View file

@ -19,7 +19,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
auto newIter = newValues.begin(); auto newIter = newValues.begin();
// TODO: cache this // TODO: cache this
auto getCmpKey = [this](const QVariant& v) { auto getCmpKey = [&](const QVariant& v) {
if (v.canConvert<QVariantMap>()) { if (v.canConvert<QVariantMap>()) {
auto vMap = v.value<QVariantMap>(); auto vMap = v.value<QVariantMap>();
if (vMap.contains(this->cmpKey)) { if (vMap.contains(this->cmpKey)) {
@ -30,7 +30,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
return v; return v;
}; };
auto variantCmp = [&, this](const QVariant& a, const QVariant& b) { auto variantCmp = [&](const QVariant& a, const QVariant& b) {
if (!this->cmpKey.isEmpty()) return getCmpKey(a) == getCmpKey(b); if (!this->cmpKey.isEmpty()) return getCmpKey(a) == getCmpKey(b);
else return a == b; else return a == b;
}; };
@ -72,8 +72,8 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
do { do {
++iter; ++iter;
} while (iter != this->mValues.end() } while (iter != this->mValues.end()
&& std::find_if(newIter, newValues.end(), eqPredicate(*iter)) && std::find_if(newIter, newValues.end(), eqPredicate(*iter)) == newValues.end()
== newValues.end()); );
auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter)); auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
auto startIndex = static_cast<qint32>(std::distance(this->mValues.begin(), startIter)); auto startIndex = static_cast<qint32>(std::distance(this->mValues.begin(), startIter));

View file

@ -51,3 +51,9 @@ void SingletonRegistry::onReload(SingletonRegistry* old) {
singleton->reload(old == nullptr ? nullptr : old->registry.value(url)); singleton->reload(old == nullptr ? nullptr : old->registry.value(url));
} }
} }
void SingletonRegistry::onPostReload() {
for (auto* singleton: this->registry.values()) {
PostReloadHook::postReloadTree(singleton);
}
}

View file

@ -26,6 +26,7 @@ public:
void registerSingleton(const QUrl& url, Singleton* singleton); void registerSingleton(const QUrl& url, Singleton* singleton);
void onReload(SingletonRegistry* old); void onReload(SingletonRegistry* old);
void onPostReload();
private: private:
QHash<QUrl, Singleton*> registry; QHash<QUrl, Singleton*> registry;

View file

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

View file

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

View file

@ -1,6 +1,6 @@
function (qs_test name) function (qs_test name)
add_executable(${name} ${ARGN}) add_executable(${name} ${ARGN})
target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window quickshell-ui quickshell-io) target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window quickshell-ui)
add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $<TARGET_FILE:${name}>) add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $<TARGET_FILE:${name}>)
endfunction() endfunction()

View file

@ -115,7 +115,7 @@ void TestScriptModel::unique_data() {
void TestScriptModel::unique() { void TestScriptModel::unique() {
QFETCH(const QString, oldstr); QFETCH(const QString, oldstr);
QFETCH(const QString, newstr); QFETCH(const QString, newstr);
QFETCH(const OpList, operations); QFETCH(OpList, operations);
auto strToVariantList = [](const QString& str) -> QVariantList { auto strToVariantList = [](const QString& str) -> QVariantList {
QVariantList list; QVariantList list;

Some files were not shown because too many files have changed in this diff Show more