diff --git a/.clang-format b/.clang-format
index 610ee65..8ec602a 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,6 +1,6 @@
AlignArrayOfStructures: None
AlignAfterOpenBracket: BlockIndent
-AllowShortBlocksOnASingleLine: Always
+AllowShortBlocksOnASingleLine: Empty
AllowShortCaseLabelsOnASingleLine: true
AllowShortEnumsOnASingleLine: true
AllowShortFunctionsOnASingleLine: All
diff --git a/.clang-tidy b/.clang-tidy
index 002c444..da14682 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -20,6 +20,8 @@ Checks: >
-cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-pro-type-reinterpret-cast,
-cppcoreguidelines-pro-type-vararg,
+ -cppcoreguidelines-pro-type-union-access,
+ -cppcoreguidelines-use-enum-class,
google-global-names-in-headers,
google-readability-casting,
google-runtime-int,
@@ -63,6 +65,8 @@ CheckOptions:
readability-identifier-naming.ParameterCase: camelBack
readability-identifier-naming.VariableCase: camelBack
+ misc-const-correctness.WarnPointersAsPointers: false
+
# does not appear to work
readability-operators-representation.BinaryOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^='
readability-operators-representation.OverloadedOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^='
diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml
index c8b4804..958c884 100644
--- a/.github/ISSUE_TEMPLATE/crash.yml
+++ b/.github/ISSUE_TEMPLATE/crash.yml
@@ -1,82 +1,17 @@
-name: Crash Report
-description: Quickshell has crashed
-labels: ["bug", "crash"]
+name: Crash Report (v1)
+description: Quickshell has crashed (old)
+labels: ["unactionable"]
body:
- - type: textarea
- id: crashinfo
+ - type: markdown
attributes:
- label: General crash information
- description: |
- Paste the contents of the `info.txt` file in your crash folder here.
- value: " General information
-
-
- ```
-
-
-
- ```
-
-
- "
- validations:
- required: true
- - type: textarea
- id: userinfo
+ value: |
+ Thank you for taking the time to click the report button.
+ At this point most of the worst issues in 0.2.1 and before have been fixed and we are
+ preparing for a new release. Please do not submit this report.
+ - type: checkboxes
+ id: donotcheck
attributes:
- label: What caused the crash
- description: |
- Any information likely to help debug the crash. What were you doing when the crash occurred,
- what changes did you make, can you get it to happen again?
- - type: textarea
- id: dump
- attributes:
- label: Minidump
- description: |
- Attach `minidump.dmp.log` here. If it is too big to upload, compress it.
-
- You may skip this step if quickshell crashed while processing a password
- or other sensitive information. If you skipped it write why instead.
- validations:
- required: true
- - type: textarea
- id: logs
- attributes:
- label: Log file
- description: |
- Attach `log.qslog.log` here. If it is too big to upload, compress it.
-
- You can preview the log if you'd like using `quickshell read-log `.
- validations:
- required: true
- - type: textarea
- id: config
- attributes:
- label: Configuration
- description: |
- Attach your configuration here, preferrably in full (not just one file).
- Compress it into a zip, tar, etc.
-
- This will help us reproduce the crash ourselves.
- - type: textarea
- id: bt
- attributes:
- label: Backtrace
- description: |
- If you have gdb installed and use systemd, or otherwise know how to get a backtrace,
- we would appreciate one. (You may have gdb installed without knowing it)
-
- 1. Run `coredumpctl debug ` where `pid` is the number shown after "Crashed process ID"
- in the crash reporter.
- 2. Once it loads, type `bt -full` (then enter)
- 3. Copy the output and attach it as a file or in a spoiler.
- - type: textarea
- id: exe
- attributes:
- label: Executable
- description: |
- If the crash folder contains a executable.txt file, upload it here. If not you can ignore this field.
- If it is too big to upload, compress it.
-
- Note: executable.txt is the quickshell binary. It has a .txt extension due to github's limitations on
- filetypes.
+ label: Read the text above. Do not submit the report.
+ options:
+ - label: Yes I want this report to be deleted.
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/crash2.yml b/.github/ISSUE_TEMPLATE/crash2.yml
new file mode 100644
index 0000000..86f490c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/crash2.yml
@@ -0,0 +1,49 @@
+name: Crash Report (v2)
+description: Quickshell has crashed
+labels: ["bug", "crash"]
+body:
+ - type: textarea
+ id: userinfo
+ attributes:
+ label: What caused the crash
+ description: |
+ Any information likely to help debug the crash. What were you doing when the crash occurred,
+ what changes did you make, can you get it to happen again?
+ - type: upload
+ id: report
+ attributes:
+ label: Report file
+ description: Attach `report.txt` here.
+ validations:
+ required: true
+ - type: upload
+ id: logs
+ attributes:
+ label: Log file
+ description: |
+ Attach `log.qslog.log` here. If it is too big to upload, compress it.
+
+ You can preview the log if you'd like using `qs log -r '*=true'`.
+ validations:
+ required: true
+ - type: textarea
+ id: config
+ attributes:
+ label: Configuration
+ description: |
+ Attach or link your configuration here, preferrably in full (not just one file).
+ Compress it into a zip, tar, etc.
+
+ This will help us reproduce the crash ourselves.
+ - type: textarea
+ id: bt
+ attributes:
+ label: Backtrace
+ description: |
+ GDB usually produces better stacktraces than quickshell can. Consider attaching a gdb backtrace
+ following the instructions below.
+
+ 1. Run `coredumpctl debug ` where `pid` is the number shown after "Crashed process ID"
+ in the crash reporter.
+ 2. Once it loads, type `bt -full` (then enter)
+ 3. Copy the output and attach it as a file or in a spoiler.
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 93b8458..7b8cbce 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,17 +6,23 @@ jobs:
name: Nix
strategy:
matrix:
- 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]
+ qtver: [qt6.10.1, qt6.10.0, qt6.9.2, qt6.9.1, qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0]
compiler: [clang, gcc]
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ id-token: write
steps:
- uses: actions/checkout@v4
# Use cachix action over detsys for testing with act.
# - uses: cachix/install-nix-action@v27
- uses: DeterminateSystems/nix-installer-action@main
+ - uses: DeterminateSystems/magic-nix-cache-action@main
+ with:
+ use-flakehub: false
- name: Download Dependencies
- run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).inputDerivation'
+ run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).unwrapped.inputDerivation'
- name: Build
run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }'
@@ -44,13 +50,16 @@ jobs:
wayland-protocols \
wayland \
libdrm \
+ vulkan-headers \
libxcb \
libpipewire \
cli11 \
- jemalloc
+ polkit \
+ jemalloc \
+ libunwind \
+ git # for cpptrace clone
- name: Build
- # breakpad is annoying to build in ci due to makepkg not running as root
run: |
- cmake -GNinja -B build -DCRASH_REPORTER=OFF
+ cmake -GNinja -B build -DVENDOR_CPPTRACE=ON
cmake --build build
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index da329cc..de0c304 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -5,11 +5,17 @@ jobs:
lint:
name: Lint
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ id-token: write
steps:
- uses: actions/checkout@v4
# Use cachix action over detsys for testing with act.
# - uses: cachix/install-nix-action@v27
- uses: DeterminateSystems/nix-installer-action@main
+ - uses: DeterminateSystems/magic-nix-cache-action@main
+ with:
+ use-flakehub: false
- uses: nicknovitski/nix-develop@v1
- name: Check formatting
diff --git a/BUILD.md b/BUILD.md
index 3172dbe..d624a06 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -15,15 +15,7 @@ Please make this descriptive enough to identify your specific package, for examp
- `Nixpkgs`
- `Fedora COPR (errornointernet/quickshell)`
-`-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=YES/NO`
-
-If we can retrieve binaries and debug information for the package without actually running your
-distribution (e.g. from an website), and you would like to strip the binary, please set this to `YES`.
-
-If we cannot retrieve debug information, please set this to `NO` and
-**ensure you aren't distributing stripped (non debuggable) binaries**.
-
-In both cases you should build with `-DCMAKE_BUILD_TYPE=RelWithDebInfo` (then split or keep the debuginfo).
+If you are forking quickshell, please change `CRASHREPORT_URL` to your own issue tracker.
### QML Module dir
Currently all QML modules are statically linked to quickshell, but this is where
@@ -41,16 +33,22 @@ Quickshell has a set of base dependencies you will always need, names vary by di
- `cmake`
- `qt6base`
- `qt6declarative`
-- `qtshadertools` (build-time only)
-- `spirv-tools` (build-time only)
-- `pkg-config` (build-time only)
-- `cli11`
+- `libdrm`
+- `qtshadertools` (build-time)
+- `spirv-tools` (build-time)
+- `pkg-config` (build-time)
+- `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.
We currently require private headers for the following libraries:
- `qt6declarative`
-- `qt6wayland`
+- `qt6wayland` (for Qt versions prior to 6.10)
We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and
svg icons will not work, including system ones.
@@ -59,14 +57,24 @@ At least Qt 6.6 is required.
All features are enabled by default and some have their own dependencies.
-### Crash Reporter
-The crash reporter catches crashes, restarts quickshell when it crashes,
+### Crash Handler
+The crash reporter catches crashes, restarts Quickshell when it crashes,
and collects useful crash information in one place. Leaving this enabled will
enable us to fix bugs far more easily.
-To disable: `-DCRASH_REPORTER=OFF`
+To disable: `-DCRASH_HANDLER=OFF`
-Dependencies: `google-breakpad`
+Dependencies: `cpptrace`
+
+Note: `-DVENDOR_CPPTRACE=ON` can be set to vendor cpptrace using FetchContent.
+
+When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the
+package manager or fetched with FetchContent.
+
+*Please ensure binaries have usable symbols.* We do not necessarily need full debuginfo, but
+leaving symbols in the binary is extremely helpful. You can check if symbols are useful
+by sending a SIGSEGV to the process and ensuring symbols for the quickshell binary are present
+in the trace.
### Jemalloc
We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused
@@ -99,10 +107,13 @@ Currently supported Qt versions: `6.6`, `6.7`.
To disable: `-DWAYLAND=OFF`
Dependencies:
- - `qt6wayland`
+ - `qt6wayland` (for Qt versions prior to 6.10)
- `wayland` (libwayland-client)
- - `wayland-scanner` (may be part of your distro's wayland package)
- - `wayland-protocols`
+ - `wayland-scanner` (build time)
+ - `wayland-protocols` (static library)
+
+Note that one or both of `wayland-scanner` and `wayland-protocols` may be bundled
+with you distro's wayland package.
#### Wlroots Layershell
Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol,
@@ -136,8 +147,8 @@ Enables streaming video from monitors and toplevel windows through various proto
To disable: `-DSCREENCOPY=OFF`
Dependencies:
-- `libdrm`
- `libgbm`
+- `vulkan-headers` (build-time)
Specific protocols can also be disabled:
- `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1]
@@ -184,6 +195,13 @@ To disable: `-DSERVICE_PAM=OFF`
Dependencies: `pam`
+### Polkit
+This feature enables creating Polkit agents that can prompt user for authentication.
+
+To disable: `-DSERVICE_POLKIT=OFF`
+
+Dependencies: `polkit`, `glib`
+
### Hyprland
This feature enables hyprland specific integrations. It requires wayland support
but has no extra dependencies.
@@ -220,11 +238,11 @@ To disable: `-DI3_IPC=OFF`
## Building
*For developers and prospective contributors: See [CONTRIBUTING.md](CONTRIBUTING.md).*
-We highly recommend using `ninja` to run the build, but you can use makefiles if you must.
+Only `ninja` builds are tested, but makefiles may work.
#### Configuring the build
```sh
-$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here]
+$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release [additional disable flags from above here]
```
Note that features you do not supply dependencies for MUST be disabled with their associated flags
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 846a280..8293f23 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,5 +1,11 @@
cmake_minimum_required(VERSION 3.20)
-project(quickshell VERSION "0.1.0" LANGUAGES CXX C)
+project(quickshell VERSION "0.2.1" LANGUAGES CXX C)
+
+set(UNRELEASED_FEATURES
+ "network.2"
+ "colorquant-imagerect"
+ "window-parent"
+)
set(QT_MIN_VERSION "6.6.0")
set(CMAKE_CXX_STANDARD 20)
@@ -7,6 +13,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(QS_BUILD_OPTIONS "")
+# should be changed for forks
+set(CRASHREPORT_URL "https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml" CACHE STRING "Bugreport URL")
+
function(boption VAR NAME DEFAULT)
cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "")
@@ -38,14 +47,17 @@ string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}")
message(STATUS "Quickshell configuration")
message(STATUS " Distributor: ${DISTRIBUTOR}")
-boption(DISTRIBUTOR_DEBUGINFO_AVAILABLE "Distributor provided debuginfo" NO)
boption(NO_PCH "Disable precompild headers (dev)" OFF)
boption(BUILD_TESTING "Build tests (dev)" OFF)
boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang
boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN})
-boption(CRASH_REPORTER "Crash Handling" ON)
-boption(USE_JEMALLOC "Use jemalloc" ON)
+if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
+ boption(USE_JEMALLOC "Use jemalloc" OFF)
+else()
+ boption(USE_JEMALLOC "Use jemalloc" ON)
+endif()
+boption(CRASH_HANDLER "Crash Handling" ON)
boption(SOCKETS "Unix Sockets" ON)
boption(WAYLAND "Wayland" ON)
boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND)
@@ -67,17 +79,21 @@ boption(SERVICE_STATUS_NOTIFIER "System Tray" ON)
boption(SERVICE_PIPEWIRE "PipeWire" ON)
boption(SERVICE_MPRIS "Mpris" ON)
boption(SERVICE_PAM "Pam" ON)
+boption(SERVICE_POLKIT "Polkit" ON)
boption(SERVICE_GREETD "Greetd" ON)
boption(SERVICE_UPOWER "UPower" ON)
boption(SERVICE_NOTIFICATIONS "Notifications" ON)
+boption(BLUETOOTH "Bluetooth" ON)
+boption(NETWORK "Network" ON)
include(cmake/install-qml-module.cmake)
include(cmake/util.cmake)
add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension)
-# pipewire defines this, breaking PCH
+# pipewire defines these, breaking PCH
add_compile_definitions(_REENTRANT)
+add_compile_options(-fno-strict-overflow)
if (FRAME_POINTERS)
add_compile_options(-fno-omit-frame-pointer)
@@ -99,6 +115,7 @@ if (NOT CMAKE_BUILD_TYPE)
endif()
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools)
+set(QT_PRIVDEPS QuickPrivate)
include(cmake/pch.cmake)
@@ -114,9 +131,10 @@ endif()
if (WAYLAND)
list(APPEND QT_FPDEPS WaylandClient)
+ list(APPEND QT_PRIVDEPS WaylandClientPrivate)
endif()
-if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS)
+if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH OR NETWORK)
set(DBUS ON)
endif()
@@ -126,6 +144,13 @@ endif()
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)
qt_standard_project_setup(REQUIRES 6.6)
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules)
@@ -145,3 +170,14 @@ install(CODE "
${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
+)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 39fab13..73e7931 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,235 +1,40 @@
-# Contributing / Development
-Instructions for development setup and upstreaming patches.
+# Contributing
-If you just want to build or package quickshell see [BUILD.md](BUILD.md).
+Thank you for taking the time to contribute.
+To ensure nobody's time is wasted, please follow the rules below.
-## Development
+## Acceptable Code Contributions
-Install the dependencies listed in [BUILD.md](BUILD.md).
-You probably want all of them even if you don't use all of them
-to ensure tests work correctly and avoid passing a bunch of configure
-flags when you need to wipe the build directory.
+- All changes submitted MUST be **fully understood by the submitter**. If you do not know why or how
+ your change works, do not submit it to be merged. You must be able to explain your reasoning
+ for every change.
-Quickshell also uses `just` for common development command aliases.
+- Changes MUST be submitted by a human who will be responsible for them. Changes submitted without
+ a human in the loop such as automated tooling and AI Agents are **strictly disallowed**. Accounts
+ responsible for such contribution attempts **will be banned**.
-The dependencies are also available as a nix shell or nix flake which we recommend
-using with nix-direnv.
+- Changes MUST respect Quickshell's license and the license of any source works. Changes including
+ code from any other works must disclose the source of the code, explain why it was used, and
+ ensure the license is compatible.
-Common aliases:
-- `just configure [ [extra cmake args]]` (note that you must specify debug/release to specify extra args)
-- `just build` - runs the build, configuring if not configured already.
-- `just run [args]` - runs quickshell with the given arguments
-- `just clean` - clean up build artifacts. `just clean build` is somewhat common.
+- Changes must follow the guidelines outlined in [HACKING.md](HACKING.md) for style and substance.
-### Formatting
-All contributions should be formatted similarly to what already exists.
-Group related functionality together.
+- Changes must stand on their own as a unit. Do not make multiple unrelated changes in one PR.
+ Changes depending on prior merges should be marked as a draft.
-Run the formatter using `just fmt`.
-If the results look stupid, fix the clang-format file if possible,
-or disable clang-format in the affected area
-using `// clang-format off` and `// clang-format on`.
+## Acceptable Non-code Contributions
-#### Style preferences not caught by clang-format
-These are flexible. You can ignore them if it looks or works better to
-for one reason or another.
+- Bug and crash reports. You must follow the instructions in the issue templates and provide the
+ information requested.
-Use `auto` if the type of a variable can be deduced automatically, instead of
-redeclaring the returned value's type. Additionally, auto should be used when a
-constructor takes arguments.
+- Feature requests can be made via Issues. Please check to ensure nobody else has requested the same feature.
-```cpp
-auto x = ; // ok
-auto x = QString::number(3); // ok
-QString x; // ok
-QString x = "foo"; // ok
-auto x = QString("foo"); // ok
+- Do not make insubstantial or pointless changes.
-auto x = QString(); // avoid
-QString x(); // avoid
-QString x("foo"); // avoid
-```
+- Changes to project rules / policy / governance will not be entertained, except from significant
+ long-term contributors. These changes should not be addressed through contribution channels.
-Put newlines around logical units of code, and after closing braces. If the
-most reasonable logical unit of code takes only a single line, it should be
-merged into the next single line logical unit if applicable.
-```cpp
-// multiple units
-auto x = ; // unit 1
-auto y = ; // unit 2
+## Merge timelines
-auto x = ; // unit 1
-emit this->y(); // unit 2
-
-auto x1 = ; // unit 1
-auto x2 = ; // unit 1
-auto x3 = ; // unit 1
-
-auto y1 = ; // unit 2
-auto y2 = ; // unit 2
-auto y3 = ; // unit 2
-
-// one unit
-auto x = ;
-if (x...) {
- // ...
-}
-
-// if more than one variable needs to be used then add a newline
-auto x = ;
-auto y = ;
-
-if (x && y) {
- // ...
-}
-```
-
-Class formatting:
-```cpp
-//! Doc comment summary
-/// Doc comment body
-class Foo: public QObject {
- // The Q_OBJECT macro comes first. Macros are ; terminated.
- Q_OBJECT;
- QML_ELEMENT;
- QML_CLASSINFO(...);
- // Properties must stay on a single line or the doc generator won't be able to pick them up
- Q_PROPERTY(...);
- /// Doc comment
- Q_PROPERTY(...);
- /// Doc comment
- Q_PROPERTY(...);
-
-public:
- // Classes should have explicit constructors if they aren't intended to
- // implicitly cast. The constructor can be inline in the header if it has no body.
- explicit Foo(QObject* parent = nullptr): QObject(parent) {}
-
- // Instance functions if applicable.
- static Foo* instance();
-
- // Member functions unrelated to properties come next
- void function();
- void function();
- void function();
-
- // Then Q_INVOKABLEs
- Q_INVOKABLE function();
- /// Doc comment
- Q_INVOKABLE function();
- /// Doc comment
- Q_INVOKABLE function();
-
- // Then property related functions, in the order (bindable, getter, setter).
- // Related functions may be included here as well. Function bodies may be inline
- // if they are a single expression. There should be a newline between each
- // property's methods.
- [[nodiscard]] QBindable bindableFoo() { return &this->bFoo; }
- [[nodiscard]] T foo() const { return this->foo; }
- void setFoo();
-
- [[nodiscard]] T bar() const { return this->foo; }
- void setBar();
-
-signals:
- // Signals that are not property change related go first.
- // Property change signals go in property definition order.
- void asd();
- void asd2();
- void fooChanged();
- void barChanged();
-
-public slots:
- // generally Q_INVOKABLEs are preferred to public slots.
- void slot();
-
-private slots:
- // ...
-
-private:
- // statics, then functions, then fields
- static const foo BAR;
- static void foo();
-
- void foo();
- void bar();
-
- // property related members are prefixed with `m`.
- QString mFoo;
- QString bar;
-
- // Bindables go last and should be prefixed with `b`.
- Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged);
-};
-```
-
-### Linter
-All contributions should pass the linter.
-
-Note that running the linter requires disabling precompiled
-headers and including the test codepaths:
-```sh
-$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
-$ just lint-changed
-```
-
-If the linter is complaining about something that you think it should not,
-please disable the lint in your MR and explain your reasoning if it isn't obvious.
-
-### Tests
-If you feel like the feature you are working on is very complex or likely to break,
-please write some tests. We will ask you to directly if you send in an MR for an
-overly complex or breakable feature.
-
-At least all tests that passed before your changes should still be passing
-by the time your contribution is ready.
-
-You can run the tests using `just test` but you must enable them first
-using `-DBUILD_TESTING=ON`.
-
-### Documentation
-Most of quickshell's documentation is automatically generated from the source code.
-You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser
-cannot handle random line breaks and will usually require you to disable clang-format if the
-lines are too long.
-
-Before submitting an MR, if adding new features please make sure the documentation is generated
-reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo.
-
-Doc comments take the form `///` or `///!` (summary) and work with markdown.
-You can reference other types using the `@@[Module.][Type.][member]` shorthand
-where all parts are optional. If module or type are not specified they will
-be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`.
-Look at existing code for how it works.
-
-Quickshell modules additionally have a `module.md` file which contains a summary, description,
-and list of headers to scan for documentation.
-
-## Contributing
-
-### Commits
-Please structure your commit messages as `scope[!]: commit` where
-the scope is something like `core` or `service/mpris`. (pick what has been
-used historically or what makes sense if new). Add `!` for changes that break
-existing APIs or functionality.
-
-Commit descriptions should contain a summary of the changes if they are not
-sufficiently addressed in the commit message.
-
-Please squash/rebase additions or edits to previous changes and follow the
-commit style to keep the history easily searchable at a glance.
-Depending on the change, it is often reasonable to squash it into just
-a single commit. (If you do not follow this we will squash your changes
-for you.)
-
-### Sending patches
-You may contribute by submitting a pull request on github, asking for
-an account on our git server, or emailing patches / git bundles
-directly to `outfoxxed@outfoxxed.me`.
-
-### Getting help
-If you're getting stuck, you can come talk to us in the
-[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me)
-for help on implementation, conventions, etc.
-Feel free to ask for advice early in your implementation if you are
-unsure.
+We handle work for the most part on a push basis. If your PR has been ignored for a while
+and is still relevant please bump it.
diff --git a/HACKING.md b/HACKING.md
new file mode 100644
index 0000000..69357f1
--- /dev/null
+++ b/HACKING.md
@@ -0,0 +1,226 @@
+## Development
+
+Install the dependencies listed in [BUILD.md](BUILD.md).
+You probably want all of them even if you don't use all of them
+to ensure tests work correctly and avoid passing a bunch of configure
+flags when you need to wipe the build directory.
+
+The dependencies are also available as a nix shell or nix flake which we recommend
+using with nix-direnv.
+
+Quickshell uses `just` for common development command aliases.
+
+Common aliases:
+- `just configure [ [extra cmake args]]` (note that you must specify debug/release to specify extra args)
+- `just build` - runs the build, configuring if not configured already.
+- `just run [args]` - runs quickshell with the given arguments
+- `just clean` - clean up build artifacts. `just clean build` is somewhat common.
+
+### Formatting
+All contributions should be formatted similarly to what already exists.
+Group related functionality together.
+
+Run the formatter using `just fmt`.
+If the results look stupid, fix the clang-format file if possible,
+or disable clang-format in the affected area
+using `// clang-format off` and `// clang-format on`.
+
+#### Style preferences not caught by clang-format
+These are flexible. You can ignore them if it looks or works better to
+for one reason or another.
+
+Use `auto` if the type of a variable can be deduced automatically, instead of
+redeclaring the returned value's type. Additionally, auto should be used when a
+constructor takes arguments.
+
+```cpp
+auto x = ; // ok
+auto x = QString::number(3); // ok
+QString x; // ok
+QString x = "foo"; // ok
+auto x = QString("foo"); // ok
+
+auto x = QString(); // avoid
+QString x(); // avoid
+QString x("foo"); // avoid
+```
+
+Put newlines around logical units of code, and after closing braces. If the
+most reasonable logical unit of code takes only a single line, it should be
+merged into the next single line logical unit if applicable.
+```cpp
+// multiple units
+auto x = ; // unit 1
+auto y = ; // unit 2
+
+auto x = ; // unit 1
+emit this->y(); // unit 2
+
+auto x1 = ; // unit 1
+auto x2 = ; // unit 1
+auto x3 = ; // unit 1
+
+auto y1 = ; // unit 2
+auto y2 = ; // unit 2
+auto y3 = ; // unit 2
+
+// one unit
+auto x = ;
+if (x...) {
+ // ...
+}
+
+// if more than one variable needs to be used then add a newline
+auto x = ;
+auto y = ;
+
+if (x && y) {
+ // ...
+}
+```
+
+Class formatting:
+```cpp
+//! Doc comment summary
+/// Doc comment body
+class Foo: public QObject {
+ // The Q_OBJECT macro comes first. Macros are ; terminated.
+ Q_OBJECT;
+ QML_ELEMENT;
+ QML_CLASSINFO(...);
+ // Properties must stay on a single line or the doc generator won't be able to pick them up
+ Q_PROPERTY(...);
+ /// Doc comment
+ Q_PROPERTY(...);
+ /// Doc comment
+ Q_PROPERTY(...);
+
+public:
+ // Classes should have explicit constructors if they aren't intended to
+ // implicitly cast. The constructor can be inline in the header if it has no body.
+ explicit Foo(QObject* parent = nullptr): QObject(parent) {}
+
+ // Instance functions if applicable.
+ static Foo* instance();
+
+ // Member functions unrelated to properties come next
+ void function();
+ void function();
+ void function();
+
+ // Then Q_INVOKABLEs
+ Q_INVOKABLE function();
+ /// Doc comment
+ Q_INVOKABLE function();
+ /// Doc comment
+ Q_INVOKABLE function();
+
+ // Then property related functions, in the order (bindable, getter, setter).
+ // Related functions may be included here as well. Function bodies may be inline
+ // if they are a single expression. There should be a newline between each
+ // property's methods.
+ [[nodiscard]] QBindable bindableFoo() { return &this->bFoo; }
+ [[nodiscard]] T foo() const { return this->foo; }
+ void setFoo();
+
+ [[nodiscard]] T bar() const { return this->foo; }
+ void setBar();
+
+signals:
+ // Signals that are not property change related go first.
+ // Property change signals go in property definition order.
+ void asd();
+ void asd2();
+ void fooChanged();
+ void barChanged();
+
+public slots:
+ // generally Q_INVOKABLEs are preferred to public slots.
+ void slot();
+
+private slots:
+ // ...
+
+private:
+ // statics, then functions, then fields
+ static const foo BAR;
+ static void foo();
+
+ void foo();
+ void bar();
+
+ // property related members are prefixed with `m`.
+ QString mFoo;
+ QString bar;
+
+ // Bindables go last and should be prefixed with `b`.
+ Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged);
+};
+```
+
+Use lowercase .h suffixed Qt headers, e.g. `` over ``.
+
+### Linter
+All contributions should pass the linter.
+
+Note that running the linter requires disabling precompiled
+headers and including the test codepaths:
+```sh
+$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
+$ just lint-changed
+```
+
+If the linter is complaining about something that you think it should not,
+please disable the lint in your MR and explain your reasoning if it isn't obvious.
+
+### Tests
+If you feel like the feature you are working on is very complex or likely to break,
+please write some tests. We will ask you to directly if you send in an MR for an
+overly complex or breakable feature.
+
+At least all tests that passed before your changes should still be passing
+by the time your contribution is ready.
+
+You can run the tests using `just test` but you must enable them first
+using `-DBUILD_TESTING=ON`.
+
+### Documentation
+Most of quickshell's documentation is automatically generated from the source code.
+You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser
+cannot handle random line breaks and will usually require you to disable clang-format if the
+lines are too long.
+
+Make sure new files containing doc comments are added to a `module.md` file.
+See existing module files for reference.
+
+Doc comments take the form `///` or `///!` (summary) and work with markdown.
+You can reference other types using the `@@[Module.][Type.][member]` shorthand
+where all parts are optional. If module or type are not specified they will
+be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`.
+Look at existing code for how it works.
+
+If you have made a user visible change since the last tagged release, describe it in
+[changelog/next.md](changelog/next.md).
+
+## Contributing
+
+### Commits
+Please structure your commit messages as `scope: commit` where
+the scope is something like `core` or `service/mpris`. (pick what has been
+used historically or what makes sense if new).
+
+Commit descriptions should contain a summary of the changes if they are not
+sufficiently addressed in the commit message.
+
+Please squash/rebase additions or edits to previous changes and follow the
+commit style to keep the history easily searchable at a glance.
+Depending on the change, it is often reasonable to squash it into just
+a single commit. (If you do not follow this we will squash your changes
+for you.)
+
+### Getting help
+If you're getting stuck, you can come talk to us in the
+[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me)
+for help on implementation, conventions, etc. There is also a bridged [discord server](https://discord.gg/UtZeT3xNyT).
+Feel free to ask for advice early in your implementation if you are
+unsure.
diff --git a/Justfile b/Justfile
index f60771a..801eb2a 100644
--- a/Justfile
+++ b/Justfile
@@ -12,6 +12,9 @@ lint-ci:
lint-changed:
git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
+lint-staged:
+ git diff --staged --name-only --diff-filter=d HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
+
configure target='debug' *FLAGS='':
cmake -GNinja -B {{builddir}} \
-DCMAKE_BUILD_TYPE={{ if target == "debug" { "Debug" } else { "RelWithDebInfo" } }} \
diff --git a/README.md b/README.md
index adefcb8..365bdb5 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,14 @@
See the [website](https://quickshell.outfoxxed.me) for more information
and installation instructions.
+This repo is hosted at:
+- https://git.outfoxxed.me/quickshell/quickshell
+- https://github.com/quickshell-mirror/quickshell
+
# Contributing / Development
-See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
+- [HACKING.md](HACKING.md) - Development instructions and policy.
+- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution policy.
+- [BUILD.md](BUILD.md) - Packaging and build instructions.
#### License
diff --git a/assets/org.quickshell.desktop b/assets/org.quickshell.desktop
new file mode 100644
index 0000000..63f65fd
--- /dev/null
+++ b/assets/org.quickshell.desktop
@@ -0,0 +1,7 @@
+[Desktop Entry]
+Version=1.5
+Type=Application
+NoDisplay=true
+
+Name=Quickshell
+Icon=org.quickshell
diff --git a/assets/quickshell.svg b/assets/quickshell.svg
new file mode 100644
index 0000000..7d0f948
--- /dev/null
+++ b/assets/quickshell.svg
@@ -0,0 +1 @@
+
diff --git a/changelog/next.md b/changelog/next.md
new file mode 100644
index 0000000..761161d
--- /dev/null
+++ b/changelog/next.md
@@ -0,0 +1,84 @@
+## Breaking Changes
+
+### Config paths are no longer canonicalized
+
+This fixes nix configs changing shell-ids on rebuild as the shell id is now derived from
+the symlink path. Configs with a symlink in their path will have a different shell id.
+
+Shell ids are used to derive the default config / state / cache folders, so those files
+will need to be manually moved if using a config behind a symlinked path without an explicitly
+set shell id.
+
+## New Features
+
+- Added support for creating Polkit agents.
+- Added support for creating wayland idle inhibitors.
+- Added support for wayland idle timeouts.
+- Added support for inhibiting wayland compositor shortcuts for focused windows.
+- Added the ability to override Quickshell.cacheDir with a custom path.
+- Added minimized, maximized, and fullscreen properties to FloatingWindow.
+- Added the ability to handle move and resize events to FloatingWindow.
+- Pipewire service now reconnects if pipewire dies or a protocol error occurs.
+- Added pipewire audio peak detection.
+- Added network management support.
+- Added support for grabbing focus from popup windows.
+- Added support for IPC signal listeners.
+- Added Quickshell version checking and version gated preprocessing.
+- Added a way to detect if an icon is from the system icon theme or not.
+- Added vulkan support to screencopy.
+- Added generic WindowManager interface implementing ext-workspace.
+- Added ext-background-effect window blur support.
+- Added per-corner radius support to Region.
+- Added ColorQuantizer region selection.
+- Added dialog window support to FloatingWindow.
+
+## Other Changes
+
+- FreeBSD is now partially supported.
+- IPC operations filter available instances to the current display connection by default.
+- PwNodeLinkTracker ignores sound level monitoring programs.
+- Replaced breakpad with cpptrace.
+- Reloads are prevented if no file content has changed.
+- Added `QS_DISABLE_FILE_WATCHER` environment variable to disable file watching.
+- Added `QS_DISABLE_CRASH_HANDLER` environment variable to disable crash handling.
+- Added `QS_CRASHREPORT_URL` environment variable to allow overriding the crash reporter link.
+- Added `AppId` pragma and `QS_APP_ID` environment variable to allow overriding the desktop application ID.
+- Added `DropExpensiveFonts` pragma and `QS_DROP_EXPENSIVE_FONTS` environment variable which avoids loading fonts which may cause lag and excessive memory usage if many variants are used.
+- Unrecognized pragmas are no longer a hard error for future backward compatibility.
+
+## Bug Fixes
+
+- Fixed volume control breaking with pipewire pro audio mode.
+- Fixed volume control breaking with bluez streams and potentially others.
+- Fixed volume control breaking for devices without route definitions.
+- Fixed escape sequence handling in desktop entries.
+- Fixed volumes not initializing if a pipewire device was already loaded before its node.
+- Fixed hyprland active toplevel not resetting after window closes.
+- Fixed hyprland ipc window names and titles being reversed.
+- Fixed a hyprland ipc crash when refreshing toplevels before workspaces.
+- Fixed missing signals for system tray item title and description updates.
+- Fixed asynchronous loaders not working after reload.
+- Fixed asynchronous loaders not working before window creation.
+- Fixed memory leak in IPC handlers.
+- Fixed ClippingRectangle related crashes.
+- Fixed crashes when monitors are unplugged.
+- Fixed crashes when default pipewire devices are lost.
+- Fixed ToplevelManager not clearing activeToplevel on deactivation.
+- Desktop action order is now preserved.
+- Fixed partial socket reads in greetd and hyprland on slow machines.
+- Worked around Qt bug causing crashes when plugging and unplugging monitors.
+- Fixed HyprlandFocusGrab crashing if windows were destroyed after being passed to it.
+- Fixed ScreencopyView pixelation when scaled.
+- Fixed JsonAdapter crashing and providing bad data on read when using JsonObject.
+- Fixed JsonAdapter sending unnecessary property changes for primitive values.
+- Fixed JsonAdapter serialization for lists.
+- Fixed pipewire crashes after hotplugging devices and changing default outputs.
+- Fixed launches failing for `--daemonize` on some systems.
+
+## Packaging Changes
+
+- `glib` and `polkit` have been added as dependencies when compiling with polkit agent support.
+- `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support).
+- `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore.
+- `DISTRIBUTOR_DEBUGINFO_AVAILABLE` was removed as it is no longer important without breakpad.
+- `libdrm` is now unconditionally required as a direct dependency.
diff --git a/changelog/v0.1.0.md b/changelog/v0.1.0.md
new file mode 100644
index 0000000..f8a032f
--- /dev/null
+++ b/changelog/v0.1.0.md
@@ -0,0 +1 @@
+Initial release
diff --git a/changelog/v0.2.0.md b/changelog/v0.2.0.md
new file mode 100644
index 0000000..2fbf74d
--- /dev/null
+++ b/changelog/v0.2.0.md
@@ -0,0 +1,84 @@
+## 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.
diff --git a/changelog/v0.2.1.md b/changelog/v0.2.1.md
new file mode 100644
index 0000000..596b82f
--- /dev/null
+++ b/changelog/v0.2.1.md
@@ -0,0 +1,17 @@
+## 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.
diff --git a/ci/matrix.nix b/ci/matrix.nix
index be2da61..dd20fa5 100644
--- a/ci/matrix.nix
+++ b/ci/matrix.nix
@@ -2,7 +2,10 @@
qtver,
compiler,
}: let
- nixpkgs = (import ./nix-checkouts.nix).${builtins.replaceStrings ["."] ["_"] qtver};
+ checkouts = import ./nix-checkouts.nix;
+ nixpkgs = checkouts.${builtins.replaceStrings ["."] ["_"] qtver};
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
diff --git a/ci/nix-checkouts.nix b/ci/nix-checkouts.nix
index 73c2415..945973c 100644
--- a/ci/nix-checkouts.nix
+++ b/ci/nix-checkouts.nix
@@ -7,9 +7,28 @@ let
url = "https://github.com/nixos/nixpkgs/archive/${commit}.tar.gz";
inherit sha256;
}) {};
-in {
- # For old qt versions, grab the commit before the version bump that has all the patches
- # instead of the bumped version.
+in rec {
+ latest = qt6_10_0;
+
+ qt6_10_1 = byCommit {
+ commit = "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38";
+ sha256 = "0fvbizl7j5rv2rf8j76yw0xb3d9l06hahkjys2a7k1yraznvnafm";
+ };
+
+ qt6_10_0 = byCommit {
+ commit = "c5ae371f1a6a7fd27823bc500d9390b38c05fa55";
+ sha256 = "18g0f8cb9m8mxnz9cf48sks0hib79b282iajl2nysyszph993yp0";
+ };
+
+ qt6_9_2 = byCommit {
+ commit = "e9f00bd893984bc8ce46c895c3bf7cac95331127";
+ sha256 = "0s2mhbrgzxlgkg2yxb0q0hpk8lby1a7w67dxvfmaz4gsmc0bnvfj";
+ };
+
+ qt6_9_1 = byCommit {
+ commit = "4c202d26483c5ccf3cb95e0053163facde9f047e";
+ sha256 = "06l2w4bcgfw7dfanpzpjcf25ydf84in240yplqsss82qx405y9di";
+ };
qt6_9_0 = byCommit {
commit = "546c545bd0594809a28ab7e869b5f80dd7243ef6";
diff --git a/ci/variations.nix b/ci/variations.nix
index b0889be..b1d2947 100644
--- a/ci/variations.nix
+++ b/ci/variations.nix
@@ -2,6 +2,6 @@
clangStdenv,
gccStdenv,
}: {
- clang = { buildStdenv = clangStdenv; };
- gcc = { buildStdenv = gccStdenv; };
+ clang = { stdenv = clangStdenv; };
+ gcc = { stdenv = gccStdenv; };
}
diff --git a/default.nix b/default.nix
index 79c9b7a..749ef49 100644
--- a/default.nix
+++ b/default.nix
@@ -2,23 +2,31 @@
lib,
nix-gitignore,
pkgs,
+ stdenv,
keepDebugInfo,
- buildStdenv ? pkgs.clangStdenv,
+ pkg-config,
cmake,
ninja,
- qt6,
spirv-tools,
- cli11,
- breakpad,
+ qt6,
+ cpptrace ? null,
+ libunwind,
+ libdwarf,
jemalloc,
+ cli11,
wayland,
wayland-protocols,
+ wayland-scanner,
+ xorg,
+ libxcb ? xorg.libxcb,
libdrm,
libgbm ? null,
- xorg,
+ vulkan-headers,
pipewire,
pam,
+ polkit,
+ glib,
gitRev ? (let
headExists = builtins.pathExists ./.git/HEAD;
@@ -41,64 +49,107 @@
withPam ? true,
withHyprland ? true,
withI3 ? true,
-}: buildStdenv.mkDerivation {
- pname = "quickshell${lib.optionalString debug "-debug"}";
- version = "0.1.0";
- src = nix-gitignore.gitignoreSource "/docs\n/examples\n" ./.;
+ withPolkit ? true,
+ withNetworkManager ? true,
+}: let
+ withCrashHandler = withCrashReporter && cpptrace != null && lib.strings.compareVersions cpptrace.version "0.7.2" >= 0;
- nativeBuildInputs = with pkgs; [
- cmake
- ninja
- qt6.qtshadertools
- spirv-tools
- qt6.wrapQtAppsHook
- pkg-config
- ] ++ (lib.optionals withWayland [
- wayland-protocols
- wayland-scanner
- ]);
+ unwrapped = stdenv.mkDerivation {
+ pname = "quickshell${lib.optionalString debug "-debug"}";
+ version = "0.2.1";
+ src = nix-gitignore.gitignoreSource "/default.nix\n" ./.;
- buildInputs = [
- qt6.qtbase
- qt6.qtdeclarative
- cli11
- ]
- ++ lib.optional withCrashReporter breakpad
- ++ lib.optional withJemalloc jemalloc
- ++ lib.optional withQtSvg qt6.qtsvg
- ++ lib.optionals withWayland ([ qt6.qtwayland wayland ] ++ (if libgbm != null then [ libdrm libgbm ] else []))
- ++ lib.optional withX11 xorg.libxcb
- ++ lib.optional withPam pam
- ++ lib.optional withPipewire pipewire;
+ dontWrapQtApps = true; # see wrappers
- cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
+ nativeBuildInputs = [
+ cmake
+ ninja
+ spirv-tools
+ pkg-config
+ ]
+ ++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
+ ++ lib.optionals withWayland [
+ qt6.qtwayland # qtwaylandscanner required at build time
+ wayland-scanner
+ ];
- cmakeFlags = [
- (lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake")
- (lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
- (lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true)
- (lib.cmakeFeature "GIT_REVISION" gitRev)
- (lib.cmakeBool "CRASH_REPORTER" withCrashReporter)
- (lib.cmakeBool "USE_JEMALLOC" withJemalloc)
- (lib.cmakeBool "WAYLAND" withWayland)
- (lib.cmakeBool "SCREENCOPY" (libgbm != null))
- (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
- (lib.cmakeBool "SERVICE_PAM" withPam)
- (lib.cmakeBool "HYPRLAND" withHyprland)
- (lib.cmakeBool "I3" withI3)
- ];
+ buildInputs = [
+ qt6.qtbase
+ qt6.qtdeclarative
+ libdrm
+ cli11
+ ]
+ ++ lib.optional withQtSvg qt6.qtsvg
+ ++ lib.optional withCrashHandler (cpptrace.overrideAttrs (prev: {
+ cmakeFlags = prev.cmakeFlags ++ [
+ "-DCPPTRACE_UNWIND_WITH_LIBUNWIND=TRUE"
+ ];
+ buildInputs = prev.buildInputs ++ [ libunwind ];
+ }))
+ ++ lib.optional withJemalloc jemalloc
+ ++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
+ ++ lib.optionals withWayland [ wayland wayland-protocols ]
+ ++ lib.optionals (withWayland && libgbm != null) [ libgbm vulkan-headers ]
+ ++ lib.optional withX11 libxcb
+ ++ lib.optional withPam pam
+ ++ lib.optional withPipewire pipewire
+ ++ lib.optionals withPolkit [ polkit glib ];
- # How to get debuginfo in gdb from a release build:
- # 1. build `quickshell.debug`
- # 2. set NIX_DEBUG_INFO_DIRS="/lib/debug"
- # 3. launch gdb / coredumpctl and debuginfo will work
- separateDebugInfo = !debug;
- dontStrip = debug;
+ cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
- meta = with lib; {
- homepage = "https://git.outfoxxed.me/outfoxxed/quickshell";
- description = "Flexbile QtQuick based desktop shell toolkit";
- license = licenses.lgpl3Only;
- platforms = platforms.linux;
+ cmakeFlags = [
+ (lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake")
+ (lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
+ (lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true)
+ (lib.cmakeFeature "GIT_REVISION" gitRev)
+ (lib.cmakeBool "CRASH_HANDLER" withCrashHandler)
+ (lib.cmakeBool "USE_JEMALLOC" withJemalloc)
+ (lib.cmakeBool "WAYLAND" withWayland)
+ (lib.cmakeBool "SCREENCOPY" (libgbm != null))
+ (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
+ (lib.cmakeBool "SERVICE_PAM" withPam)
+ (lib.cmakeBool "SERVICE_NETWORKMANAGER" withNetworkManager)
+ (lib.cmakeBool "SERVICE_POLKIT" withPolkit)
+ (lib.cmakeBool "HYPRLAND" withHyprland)
+ (lib.cmakeBool "I3" withI3)
+ ];
+
+ # How to get debuginfo in gdb from a release build:
+ # 1. build `quickshell.debug`
+ # 2. set NIX_DEBUG_INFO_DIRS="/lib/debug"
+ # 3. launch gdb / coredumpctl and debuginfo will work
+ separateDebugInfo = !debug;
+ dontStrip = debug;
+
+ meta = with lib; {
+ homepage = "https://quickshell.org";
+ description = "Flexbile QtQuick based desktop shell toolkit";
+ license = licenses.lgpl3Only;
+ 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
diff --git a/flake.lock b/flake.lock
index df5aa3f..2f95a44 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1736012469,
- "narHash": "sha256-/qlNWm/IEVVH7GfgAIyP6EsVZI6zjAx1cV5zNyrs+rI=",
+ "lastModified": 1768127708,
+ "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "8f3e1f807051e32d8c95cd12b9b421623850a34d",
+ "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index a0bc18d..8edda2c 100644
--- a/flake.nix
+++ b/flake.nix
@@ -4,22 +4,28 @@
};
outputs = { self, nixpkgs }: let
- forEachSystem = fn: nixpkgs.lib.genAttrs
- [ "x86_64-linux" "aarch64-linux" ]
- (system: fn system nixpkgs.legacyPackages.${system});
- in {
- packages = forEachSystem (system: pkgs: rec {
- quickshell = pkgs.callPackage ./default.nix {
- gitRev = self.rev or self.dirtyRev;
- };
+ overlayPkgs = p: p.appendOverlays [ self.overlays.default ];
+ forEachSystem = fn:
+ nixpkgs.lib.genAttrs
+ nixpkgs.lib.platforms.linux
+ (system: fn system (overlayPkgs nixpkgs.legacyPackages.${system}));
+ in {
+ overlays.default = import ./overlay.nix {
+ rev = self.rev or self.dirtyRev;
+ };
+
+ packages = forEachSystem (system: pkgs: rec {
+ quickshell = pkgs.quickshell;
default = quickshell;
});
devShells = forEachSystem (system: pkgs: rec {
default = import ./shell.nix {
inherit pkgs;
- inherit (self.packages.${system}) quickshell;
+ quickshell = self.packages.${system}.quickshell.override {
+ stdenv = pkgs.clangStdenv;
+ };
};
});
};
diff --git a/overlay.nix b/overlay.nix
new file mode 100644
index 0000000..d8ea137
--- /dev/null
+++ b/overlay.nix
@@ -0,0 +1,5 @@
+{ rev ? null }: (final: prev: {
+ quickshell = final.callPackage ./default.nix {
+ gitRev = rev;
+ };
+})
diff --git a/quickshell.scm b/quickshell.scm
index 05bf269..780bb96 100644
--- a/quickshell.scm
+++ b/quickshell.scm
@@ -35,13 +35,14 @@
pkg-config
qtshadertools
spirv-tools
- wayland-protocols))
- (inputs (list cli11
- jemalloc
+ wayland-protocols
+ cli11))
+ (inputs (list jemalloc
libdrm
libxcb
libxkbcommon
linux-pam
+ polkit
mesa
pipewire
qtbase
@@ -55,8 +56,7 @@
#~(list "-GNinja"
"-DDISTRIBUTOR=\"In-tree Guix channel\""
"-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO"
- ;; Breakpad is not currently packaged for Guix.
- "-DCRASH_REPORTER=OFF")
+ "-DCRASH_HANDLER=OFF")
#:phases
#~(modify-phases %standard-phases
(replace 'build (lambda _ (invoke "cmake" "--build" ".")))
diff --git a/shell.nix b/shell.nix
index 82382f9..03a446d 100644
--- a/shell.nix
+++ b/shell.nix
@@ -1,14 +1,15 @@
{
pkgs ? import {},
- quickshell ? pkgs.callPackage ./default.nix {},
+ stdenv ? pkgs.clangStdenv, # faster compiles than gcc
+ quickshell ? pkgs.callPackage ./default.nix { inherit stdenv; },
...
}: let
tidyfox = import (pkgs.fetchFromGitea {
domain = "git.outfoxxed.me";
owner = "outfoxxed";
repo = "tidyfox";
- rev = "1f062cc198d1112d13e5128fa1f2ee3dbffe613b";
- sha256 = "kbt0Zc1qHE5fhqBkKz8iue+B+ZANjF1AR/RdgmX1r0I=";
+ rev = "9d85d7e7dea2602aa74ec3168955fee69967e92f";
+ hash = "sha256-77ERiweF6lumonp2c/124rAoVG6/o9J+Aajhttwtu0w=";
}) { inherit pkgs; };
in pkgs.mkShell.override { stdenv = quickshell.stdenv; } {
inputsFrom = [ quickshell ];
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index d3070b6..0c05419 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -11,8 +11,9 @@ add_subdirectory(window)
add_subdirectory(io)
add_subdirectory(widgets)
add_subdirectory(ui)
+add_subdirectory(windowmanager)
-if (CRASH_REPORTER)
+if (CRASH_HANDLER)
add_subdirectory(crash)
endif()
@@ -29,3 +30,11 @@ if (X11)
endif()
add_subdirectory(services)
+
+if (BLUETOOTH)
+ add_subdirectory(bluetooth)
+endif()
+
+if (NETWORK)
+ add_subdirectory(network)
+endif()
diff --git a/src/bluetooth/CMakeLists.txt b/src/bluetooth/CMakeLists.txt
new file mode 100644
index 0000000..806ff04
--- /dev/null
+++ b/src/bluetooth/CMakeLists.txt
@@ -0,0 +1,42 @@
+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)
diff --git a/src/bluetooth/adapter.cpp b/src/bluetooth/adapter.cpp
new file mode 100644
index 0000000..7f70a27
--- /dev/null
+++ b/src/bluetooth/adapter.cpp
@@ -0,0 +1,223 @@
+#include "adapter.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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
+DBusDataTransform::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(adapter)
+ << ", path=" << adapter->path() << ")";
+ } else {
+ debug << "BluetoothAdapter(nullptr)";
+ }
+
+ return debug;
+}
diff --git a/src/bluetooth/adapter.hpp b/src/bluetooth/adapter.hpp
new file mode 100644
index 0000000..d7f21d7
--- /dev/null
+++ b/src/bluetooth/adapter.hpp
@@ -0,0 +1,173 @@
+#pragma once
+
+#include
+#include
+#include
+
+#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 {
+ using Wire = QString;
+ using Data = qs::bluetooth::BluetoothAdapterState::Enum;
+ static DBusResult 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*);
+ 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 bindableName() { return &this->bName; }
+ [[nodiscard]] QBindable bindableEnabled() { return &this->bEnabled; }
+ [[nodiscard]] QBindable bindableState() { return &this->bState; }
+ [[nodiscard]] QBindable bindableDiscoverable() { return &this->bDiscoverable; }
+ [[nodiscard]] QBindable bindableDiscoverableTimeout() {
+ return &this->bDiscoverableTimeout;
+ }
+ [[nodiscard]] QBindable bindableDiscovering() { return &this->bDiscovering; }
+ [[nodiscard]] QBindable bindablePairable() { return &this->bPairable; }
+ [[nodiscard]] QBindable bindablePairableTimeout() { return &this->bPairableTimeout; }
+ [[nodiscard]] ObjectModel* 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 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);
diff --git a/src/bluetooth/bluez.cpp b/src/bluetooth/bluez.cpp
new file mode 100644
index 0000000..f2c4300
--- /dev/null
+++ b/src/bluetooth/bluez.cpp
@@ -0,0 +1,168 @@
+#include "bluez.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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
diff --git a/src/bluetooth/bluez.hpp b/src/bluetooth/bluez.hpp
new file mode 100644
index 0000000..9d7c93c
--- /dev/null
+++ b/src/bluetooth/bluez.hpp
@@ -0,0 +1,98 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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* adapters() { return &this->mAdapters; }
+ [[nodiscard]] ObjectModel* 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 mAdapterMap;
+ QHash mDeviceMap;
+ ObjectModel mAdapters {this};
+ ObjectModel 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*);
+ /// A list of all bluetooth adapters. See @@defaultAdapter for the default.
+ Q_PROPERTY(UntypedObjectModel* adapters READ adapters CONSTANT);
+ QSDOC_TYPE_OVERRIDE(ObjectModel*);
+ /// 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* adapters() {
+ return Bluez::instance()->adapters();
+ }
+
+ [[nodiscard]] static ObjectModel* devices() {
+ return Bluez::instance()->devices();
+ }
+
+ [[nodiscard]] static QBindable bindableDefaultAdapter() {
+ return &Bluez::instance()->bDefaultAdapter;
+ }
+};
+
+} // namespace qs::bluetooth
diff --git a/src/bluetooth/device.cpp b/src/bluetooth/device.cpp
new file mode 100644
index 0000000..b140aa0
--- /dev/null
+++ b/src/bluetooth/device.cpp
@@ -0,0 +1,318 @@
+#include "device.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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 DBusDataTransform::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(device)
+ << ", path=" << device->path() << ")";
+ } else {
+ debug << "BluetoothDevice(nullptr)";
+ }
+
+ return debug;
+}
diff --git a/src/bluetooth/device.hpp b/src/bluetooth/device.hpp
new file mode 100644
index 0000000..23f230f
--- /dev/null
+++ b/src/bluetooth/device.hpp
@@ -0,0 +1,225 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#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 {
+ using Wire = quint8;
+ using Data = qreal;
+ static DBusResult 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 bindableAddress() { return &this->bAddress; }
+ [[nodiscard]] QBindable bindableDeviceName() { return &this->bDeviceName; }
+ [[nodiscard]] QBindable bindableName() { return &this->bName; }
+ [[nodiscard]] QBindable bindableConnected() { return &this->bConnected; }
+ [[nodiscard]] QBindable bindablePaired() { return &this->bPaired; }
+ [[nodiscard]] QBindable bindableBonded() { return &this->bBonded; }
+ [[nodiscard]] QBindable bindableTrusted() { return &this->bTrusted; }
+ [[nodiscard]] QBindable bindableBlocked() { return &this->bBlocked; }
+ [[nodiscard]] QBindable bindableWakeAllowed() { return &this->bWakeAllowed; }
+ [[nodiscard]] QBindable bindableIcon() { return &this->bIcon; }
+ [[nodiscard]] QBindable bindableBattery() { return &this->bBattery; }
+ [[nodiscard]] QBindable 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);
diff --git a/src/bluetooth/module.md b/src/bluetooth/module.md
new file mode 100644
index 0000000..eb797d9
--- /dev/null
+++ b/src/bluetooth/module.md
@@ -0,0 +1,12 @@
+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.
diff --git a/src/bluetooth/org.bluez.Adapter.xml b/src/bluetooth/org.bluez.Adapter.xml
new file mode 100644
index 0000000..286991e
--- /dev/null
+++ b/src/bluetooth/org.bluez.Adapter.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/bluetooth/org.bluez.Device.xml b/src/bluetooth/org.bluez.Device.xml
new file mode 100644
index 0000000..274e9fd
--- /dev/null
+++ b/src/bluetooth/org.bluez.Device.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/src/bluetooth/test/manual/test.qml b/src/bluetooth/test/manual/test.qml
new file mode 100644
index 0000000..21c53b1
--- /dev/null
+++ b/src/bluetooth/test/manual/test.qml
@@ -0,0 +1,200 @@
+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();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/build/CMakeLists.txt b/src/build/CMakeLists.txt
index bb35da9..c1ffa59 100644
--- a/src/build/CMakeLists.txt
+++ b/src/build/CMakeLists.txt
@@ -9,16 +9,10 @@ if (NOT DEFINED GIT_REVISION)
)
endif()
-if (CRASH_REPORTER)
- set(CRASH_REPORTER_DEF 1)
+if (CRASH_HANDLER)
+ set(CRASH_HANDLER_DEF 1)
else()
- set(CRASH_REPORTER_DEF 0)
-endif()
-
-if (DISTRIBUTOR_DEBUGINFO_AVAILABLE)
- set(DEBUGINFO_AVAILABLE 1)
-else()
- set(DEBUGINFO_AVAILABLE 0)
+ set(CRASH_HANDLER_DEF 0)
endif()
configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES)
diff --git a/src/build/build.hpp.in b/src/build/build.hpp.in
index 075abd1..acc3c58 100644
--- a/src/build/build.hpp.in
+++ b/src/build/build.hpp.in
@@ -1,12 +1,17 @@
#pragma once
// NOLINTBEGIN
+#define QS_VERSION "@quickshell_VERSION@"
+#define QS_VERSION_MAJOR @quickshell_VERSION_MAJOR@
+#define QS_VERSION_MINOR @quickshell_VERSION_MINOR@
+#define QS_VERSION_PATCH @quickshell_VERSION_PATCH@
+#define QS_UNRELEASED_FEATURES "@UNRELEASED_FEATURES@"
#define GIT_REVISION "@GIT_REVISION@"
#define DISTRIBUTOR "@DISTRIBUTOR@"
-#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@
-#define CRASH_REPORTER @CRASH_REPORTER_DEF@
+#define CRASH_HANDLER @CRASH_HANDLER_DEF@
#define BUILD_TYPE "@CMAKE_BUILD_TYPE@"
#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)"
#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@"
#define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@"
+#define CRASHREPORT_URL "@CRASHREPORT_URL@"
// NOLINTEND
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index eca7270..4824965 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -1,3 +1,4 @@
+pkg_check_modules(libdrm REQUIRED IMPORTED_TARGET libdrm)
qt_add_library(quickshell-core STATIC
plugin.cpp
shell.cpp
@@ -12,6 +13,7 @@ qt_add_library(quickshell-core STATIC
singleton.cpp
generation.cpp
scan.cpp
+ scanenv.cpp
qsintercept.cpp
incubator.cpp
lazyloader.cpp
@@ -23,7 +25,7 @@ qt_add_library(quickshell-core STATIC
model.cpp
elapsedtimer.cpp
desktopentry.cpp
- objectrepeater.cpp
+ desktopentrymonitor.cpp
platformmenu.cpp
qsmenu.cpp
retainable.cpp
@@ -38,6 +40,9 @@ qt_add_library(quickshell-core STATIC
iconprovider.cpp
scriptmodel.cpp
colorquantizer.cpp
+ toolsupport.cpp
+ streamreader.cpp
+ debuginfo.cpp
)
qt_add_qml_module(quickshell-core
@@ -50,7 +55,7 @@ qt_add_qml_module(quickshell-core
install_qml_module(quickshell-core)
-target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets)
+target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build PkgConfig::libdrm)
qs_module_pch(quickshell-core SET large)
diff --git a/src/core/colorquantizer.cpp b/src/core/colorquantizer.cpp
index 667942c..d983f76 100644
--- a/src/core/colorquantizer.cpp
+++ b/src/core/colorquantizer.cpp
@@ -13,39 +13,55 @@
#include
#include
#include
+#include
#include
#include
#include
#include
+#include "logcat.hpp"
+
namespace {
-Q_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg);
+QS_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg);
}
-ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize)
+ColorQuantizerOperation::ColorQuantizerOperation(
+ QUrl* source,
+ qreal depth,
+ QRect imageRect,
+ qreal rescaleSize
+)
: source(source)
, maxDepth(depth)
+ , imageRect(imageRect)
, rescaleSize(rescaleSize) {
- setAutoDelete(false);
+ this->setAutoDelete(false);
}
void ColorQuantizerOperation::quantizeImage(const QAtomicInteger& shouldCancel) {
- if (shouldCancel.loadAcquire() || source->isEmpty()) return;
+ if (shouldCancel.loadAcquire() || this->source->isEmpty()) return;
- colors.clear();
+ this->colors.clear();
- auto image = QImage(source->toLocalFile());
- if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) {
+ auto image = QImage(this->source->toLocalFile());
+
+ if (this->imageRect.isValid()) {
+ image = image.copy(this->imageRect);
+ }
+
+ if ((image.width() > this->rescaleSize || image.height() > this->rescaleSize)
+ && this->rescaleSize > 0)
+ {
image = image.scaled(
- static_cast(rescaleSize),
- static_cast(rescaleSize),
+ static_cast(this->rescaleSize),
+ static_cast(this->rescaleSize),
Qt::KeepAspectRatio,
Qt::SmoothTransformation
);
}
if (image.isNull()) {
- qCWarning(logColorQuantizer) << "Failed to load image from" << source;
+ qCWarning(logColorQuantizer) << "Failed to load image from" << this->source->toString();
return;
}
@@ -61,7 +77,7 @@ void ColorQuantizerOperation::quantizeImage(const QAtomicInteger& shouldCa
auto startTime = QDateTime::currentDateTime();
- colors = quantization(pixels, 0);
+ this->colors = this->quantization(pixels, 0);
auto endTime = QDateTime::currentDateTime();
auto milliseconds = startTime.msecsTo(endTime);
@@ -75,7 +91,7 @@ QList ColorQuantizerOperation::quantization(
) {
if (shouldCancel.loadAcquire()) return QList();
- if (depth >= maxDepth || rgbValues.isEmpty()) {
+ if (depth >= this->maxDepth || rgbValues.isEmpty()) {
if (rgbValues.isEmpty()) return QList();
auto totalR = 0;
@@ -112,8 +128,8 @@ QList ColorQuantizerOperation::quantization(
auto rightHalf = rgbValues.mid(mid);
QList result;
- result.append(quantization(leftHalf, depth + 1));
- result.append(quantization(rightHalf, depth + 1));
+ result.append(this->quantization(leftHalf, depth + 1));
+ result.append(this->quantization(rightHalf, depth + 1));
return result;
}
@@ -157,7 +173,7 @@ void ColorQuantizerOperation::finishRun() {
}
void ColorQuantizerOperation::finished() {
- emit this->done(colors);
+ emit this->done(this->colors);
delete this;
}
@@ -176,39 +192,50 @@ void ColorQuantizerOperation::run() {
void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); }
void ColorQuantizer::componentComplete() {
- componentCompleted = true;
- if (!mSource.isEmpty()) quantizeAsync();
+ this->componentCompleted = true;
+ if (!this->mSource.isEmpty()) this->quantizeAsync();
}
void ColorQuantizer::setSource(const QUrl& source) {
- if (mSource != source) {
- mSource = source;
+ if (this->mSource != source) {
+ this->mSource = source;
emit this->sourceChanged();
- if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync();
+ if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync();
}
}
void ColorQuantizer::setDepth(qreal depth) {
- if (mDepth != depth) {
- mDepth = depth;
+ if (this->mDepth != depth) {
+ this->mDepth = depth;
emit this->depthChanged();
- if (this->componentCompleted) quantizeAsync();
+ if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync();
}
}
+void ColorQuantizer::setImageRect(QRect imageRect) {
+ if (this->mImageRect != imageRect) {
+ this->mImageRect = imageRect;
+ emit this->imageRectChanged();
+
+ if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync();
+ }
+}
+
+void ColorQuantizer::resetImageRect() { this->setImageRect(QRect()); }
+
void ColorQuantizer::setRescaleSize(int rescaleSize) {
- if (mRescaleSize != rescaleSize) {
- mRescaleSize = rescaleSize;
+ if (this->mRescaleSize != rescaleSize) {
+ this->mRescaleSize = rescaleSize;
emit this->rescaleSizeChanged();
- if (this->componentCompleted) quantizeAsync();
+ if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync();
}
}
void ColorQuantizer::operationFinished(const QList& result) {
- bColors = result;
+ this->bColors = result;
this->liveOperation = nullptr;
emit this->colorsChanged();
}
@@ -217,7 +244,13 @@ void ColorQuantizer::quantizeAsync() {
if (this->liveOperation) this->cancelAsync();
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(
this->liveOperation,
diff --git a/src/core/colorquantizer.hpp b/src/core/colorquantizer.hpp
index d35a15a..0159181 100644
--- a/src/core/colorquantizer.hpp
+++ b/src/core/colorquantizer.hpp
@@ -5,6 +5,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -16,7 +17,7 @@ class ColorQuantizerOperation
Q_OBJECT;
public:
- explicit ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize);
+ explicit ColorQuantizerOperation(QUrl* source, qreal depth, QRect imageRect, qreal rescaleSize);
void run() override;
void tryCancel();
@@ -44,6 +45,7 @@ private:
QList colors;
QUrl* source;
qreal maxDepth;
+ QRect imageRect;
qreal rescaleSize;
};
@@ -78,6 +80,13 @@ class ColorQuantizer
/// binary split of the color space
Q_PROPERTY(qreal depth READ depth WRITE setDepth NOTIFY depthChanged);
+ // clang-format off
+ /// Rectangle that the source image is cropped to.
+ ///
+ /// Can be set to `undefined` to reset.
+ Q_PROPERTY(QRect imageRect READ imageRect WRITE setImageRect RESET resetImageRect NOTIFY imageRectChanged);
+ // clang-format on
+
/// The size to rescale the image to, when rescaleSize is 0 then no scaling will be done.
/// > [!NOTE] Results from color quantization doesn't suffer much when rescaling, it's
/// > reccommended to rescale, otherwise the quantization process will take much longer.
@@ -91,19 +100,24 @@ public:
[[nodiscard]] QBindable> bindableColors() { return &this->bColors; }
- [[nodiscard]] QUrl source() const { return mSource; }
+ [[nodiscard]] QUrl source() const { return this->mSource; }
void setSource(const QUrl& source);
- [[nodiscard]] qreal depth() const { return mDepth; }
+ [[nodiscard]] qreal depth() const { return this->mDepth; }
void setDepth(qreal depth);
- [[nodiscard]] qreal rescaleSize() const { return mRescaleSize; }
+ [[nodiscard]] QRect imageRect() const { return this->mImageRect; }
+ void setImageRect(QRect imageRect);
+ void resetImageRect();
+
+ [[nodiscard]] qreal rescaleSize() const { return this->mRescaleSize; }
void setRescaleSize(int rescaleSize);
signals:
void colorsChanged();
void sourceChanged();
void depthChanged();
+ void imageRectChanged();
void rescaleSizeChanged();
public slots:
@@ -117,6 +131,7 @@ private:
ColorQuantizerOperation* liveOperation = nullptr;
QUrl mSource;
qreal mDepth = 0;
+ QRect mImageRect;
qreal mRescaleSize = 0;
Q_OBJECT_BINDABLE_PROPERTY(
diff --git a/src/core/common.cpp b/src/core/common.cpp
index 5928e47..080019a 100644
--- a/src/core/common.cpp
+++ b/src/core/common.cpp
@@ -1,11 +1,9 @@
#include "common.hpp"
#include
-#include
namespace qs {
const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime();
-QProcessEnvironment Common::INITIAL_ENVIRONMENT = {}; // NOLINT
} // namespace qs
diff --git a/src/core/common.hpp b/src/core/common.hpp
index f2a01bc..ab8edb8 100644
--- a/src/core/common.hpp
+++ b/src/core/common.hpp
@@ -7,7 +7,7 @@ namespace qs {
struct Common {
static const QDateTime LAUNCH_TIME;
- static QProcessEnvironment INITIAL_ENVIRONMENT; // NOLINT
+ static inline QProcessEnvironment INITIAL_ENVIRONMENT = {}; // NOLINT
};
} // namespace qs
diff --git a/src/core/debuginfo.cpp b/src/core/debuginfo.cpp
new file mode 100644
index 0000000..abc467d
--- /dev/null
+++ b/src/core/debuginfo.cpp
@@ -0,0 +1,176 @@
+#include "debuginfo.hpp"
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "build.hpp"
+
+extern char** environ; // NOLINT
+
+namespace qs::debuginfo {
+
+QString qsVersion() {
+ return QS_VERSION " (revision " GIT_REVISION ", distributed by " DISTRIBUTOR ")";
+}
+
+QString qtVersion() { return qVersion() % QStringLiteral(" (built against " QT_VERSION_STR ")"); }
+
+QString gpuInfo() {
+ auto deviceCount = drmGetDevices2(0, nullptr, 0);
+ if (deviceCount < 0) return "Failed to get DRM device count: " % QString::number(deviceCount);
+ auto* devices = new drmDevicePtr[deviceCount];
+ auto devicesArrayGuard = qScopeGuard([&] { delete[] devices; });
+ auto r = drmGetDevices2(0, devices, deviceCount);
+ if (deviceCount < 0) return "Failed to get DRM devices: " % QString::number(r);
+ auto devicesGuard = qScopeGuard([&] {
+ for (auto i = 0; i != deviceCount; ++i) drmFreeDevice(&devices[i]); // NOLINT
+ });
+
+ QString info;
+ auto stream = QTextStream(&info);
+
+ for (auto i = 0; i != deviceCount; ++i) {
+ auto* device = devices[i]; // NOLINT
+
+ int deviceNodeType = -1;
+ if (device->available_nodes & (1 << DRM_NODE_RENDER)) deviceNodeType = DRM_NODE_RENDER;
+ else if (device->available_nodes & (1 << DRM_NODE_PRIMARY)) deviceNodeType = DRM_NODE_PRIMARY;
+
+ if (deviceNodeType == -1) continue;
+
+ auto* deviceNode = device->nodes[DRM_NODE_RENDER]; // NOLINT
+
+ auto driver = [&]() -> QString {
+ auto fd = open(deviceNode, O_RDWR | O_CLOEXEC);
+ if (fd == -1) return "";
+ auto fdGuard = qScopeGuard([&] { close(fd); });
+ auto* ver = drmGetVersion(fd);
+ if (!ver) return "";
+ auto verGuard = qScopeGuard([&] { drmFreeVersion(ver); });
+
+ // clang-format off
+ return QString(ver->name)
+ % ' ' % QString::number(ver->version_major)
+ % '.' % QString::number(ver->version_minor)
+ % '.' % QString::number(ver->version_patchlevel)
+ % " (" % ver->desc % ')';
+ // clang-format on
+ }();
+
+ QString product = "unknown";
+ QString address = "unknown";
+
+ auto hex = [](int num, int pad) { return QString::number(num, 16).rightJustified(pad, '0'); };
+
+ switch (device->bustype) {
+ case DRM_BUS_PCI: {
+ auto* b = device->businfo.pci;
+ auto* d = device->deviceinfo.pci;
+ address = "PCI " % hex(b->bus, 2) % ':' % hex(b->dev, 2) % '.' % hex(b->func, 1);
+ product = hex(d->vendor_id, 4) % ':' % hex(d->device_id, 4);
+ } break;
+ case DRM_BUS_USB: {
+ auto* b = device->businfo.usb;
+ auto* d = device->deviceinfo.usb;
+ address = "USB " % QString::number(b->bus) % ':' % QString::number(b->dev);
+ product = hex(d->vendor, 4) % ':' % hex(d->product, 4);
+ } break;
+ default: break;
+ }
+
+ stream << "GPU " << deviceNode << "\n Driver: " << driver << "\n Model: " << product
+ << "\n Address: " << address << '\n';
+ }
+
+ return info;
+}
+
+QString systemInfo() {
+ QString info;
+ auto stream = QTextStream(&info);
+
+ stream << gpuInfo() << '\n';
+
+ stream << "/etc/os-release:";
+ auto osReleaseFile = QFile("/etc/os-release");
+ if (osReleaseFile.open(QFile::ReadOnly)) {
+ stream << '\n' << osReleaseFile.readAll() << '\n';
+ osReleaseFile.close();
+ } else {
+ stream << "FAILED TO OPEN\n";
+ }
+
+ stream << "/etc/lsb-release:";
+ auto lsbReleaseFile = QFile("/etc/lsb-release");
+ if (lsbReleaseFile.open(QFile::ReadOnly)) {
+ stream << '\n' << lsbReleaseFile.readAll();
+ lsbReleaseFile.close();
+ } else {
+ stream << "FAILED TO OPEN\n";
+ }
+
+ return info;
+}
+
+QString envInfo() {
+ QString info;
+ auto stream = QTextStream(&info);
+
+ for (auto** envp = environ; *envp != nullptr; ++envp) { // NOLINT
+ auto prefixes = std::array {
+ "QS_",
+ "QT_",
+ "QML_",
+ "QML2_",
+ "QSG_",
+ "XDG_CURRENT_DESKTOP=",
+ };
+
+ for (const auto& prefix: prefixes) {
+ if (strncmp(prefix.data(), *envp, prefix.length()) == 0) goto print;
+ }
+ continue;
+
+ print:
+ stream << *envp << '\n';
+ }
+
+ return info;
+}
+
+QString combinedInfo() {
+ QString info;
+ auto stream = QTextStream(&info);
+
+ stream << "===== Version Information =====\n";
+ stream << "Quickshell: " << qsVersion() << '\n';
+ stream << "Qt: " << qtVersion() << '\n';
+
+ stream << "\n===== Build Information =====\n";
+ stream << "Build Type: " << BUILD_TYPE << '\n';
+ stream << "Compiler: " << COMPILER << '\n';
+ stream << "Compile Flags: " << COMPILE_FLAGS << '\n';
+ stream << "Configuration:\n" << BUILD_CONFIGURATION << '\n';
+
+ stream << "\n===== System Information =====\n";
+ stream << systemInfo();
+
+ stream << "\n===== Environment (trimmed) =====\n";
+ stream << envInfo();
+
+ return info;
+}
+
+} // namespace qs::debuginfo
diff --git a/src/core/debuginfo.hpp b/src/core/debuginfo.hpp
new file mode 100644
index 0000000..fc766fc
--- /dev/null
+++ b/src/core/debuginfo.hpp
@@ -0,0 +1,14 @@
+#pragma once
+
+#include
+
+namespace qs::debuginfo {
+
+QString qsVersion();
+QString qtVersion();
+QString gpuInfo();
+QString systemInfo();
+QString envInfo();
+QString combinedInfo();
+
+} // namespace qs::debuginfo
diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp
index 541207e..637f758 100644
--- a/src/core/desktopentry.cpp
+++ b/src/core/desktopentry.cpp
@@ -1,26 +1,33 @@
#include "desktopentry.hpp"
+#include
+#include
#include
#include
#include
+#include
#include
-#include
-#include
#include
#include
#include
#include
+#include
#include
-#include
-#include
+#include
+#include
#include
+#include
+#include
#include
-#include "common.hpp"
+#include "../io/processcore.hpp"
+#include "desktopentrymonitor.hpp"
+#include "logcat.hpp"
#include "model.hpp"
+#include "qmlglobal.hpp"
namespace {
-Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
+QS_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
}
struct Locale {
@@ -54,12 +61,14 @@ struct Locale {
[[nodiscard]] int matchScore(const Locale& other) const {
if (this->language != other.language) return 0;
- auto territoryMatches = !this->territory.isEmpty() && this->territory == other.territory;
- auto modifierMatches = !this->modifier.isEmpty() && this->modifier == other.modifier;
+
+ if (!other.modifier.isEmpty() && this->modifier != other.modifier) return 0;
+ if (!other.territory.isEmpty() && this->territory != other.territory) return 0;
auto score = 1;
- if (territoryMatches) score += 2;
- if (modifierMatches) score += 1;
+
+ if (!other.territory.isEmpty()) score += 2;
+ if (!other.modifier.isEmpty()) score += 1;
return score;
}
@@ -85,51 +94,64 @@ struct Locale {
QDebug operator<<(QDebug debug, const Locale& locale) {
auto saver = QDebugStateSaver(debug);
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
- << ", modifier" << locale.modifier << ')';
+ << ", modifier=" << locale.modifier << ')';
return debug;
}
-void DesktopEntry::parseEntry(const QString& text) {
+ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& text) {
+ ParsedDesktopEntryData data;
+ data.id = id;
const auto& system = Locale::system();
auto groupName = QString();
auto entries = QHash>();
- auto finishCategory = [this, &groupName, &entries]() {
+ auto actionOrder = QStringList();
+ auto pendingActions = QHash();
+
+ auto finishCategory = [&data, &groupName, &entries, &actionOrder, &pendingActions]() {
if (groupName == "Desktop Entry") {
- if (entries["Type"].second != "Application") return;
- if (entries.contains("Hidden") && entries["Hidden"].second == "true") return;
+ if (entries.value("Type").second != "Application") return;
for (const auto& [key, pair]: entries.asKeyValueRange()) {
auto& [_, value] = pair;
- this->mEntries.insert(key, value);
+ data.entries.insert(key, value);
- if (key == "Name") this->mName = value;
- else if (key == "GenericName") this->mGenericName = value;
- else if (key == "NoDisplay") this->mNoDisplay = value == "true";
- else if (key == "Comment") this->mComment = value;
- else if (key == "Icon") this->mIcon = value;
- else if (key == "Exec") this->mExecString = value;
- else if (key == "Path") this->mWorkingDirectory = value;
- else if (key == "Terminal") this->mTerminal = value == "true";
- else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts);
- else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts);
+ if (key == "Name") data.name = value;
+ else if (key == "GenericName") data.genericName = value;
+ else if (key == "StartupWMClass") data.startupClass = value;
+ else if (key == "NoDisplay") data.noDisplay = value == "true";
+ else if (key == "Hidden") data.hidden = value == "true";
+ else if (key == "Comment") data.comment = value;
+ else if (key == "Icon") data.icon = value;
+ else if (key == "Exec") {
+ data.execString = value;
+ data.command = DesktopEntry::parseExecString(value);
+ } 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 ")) {
- auto actionName = groupName.sliced(16);
- auto* action = new DesktopAction(actionName, this);
+ auto actionName = groupName.sliced(15);
+ DesktopActionData action;
+ action.id = actionName;
for (const auto& [key, pair]: entries.asKeyValueRange()) {
const auto& [_, value] = pair;
- action->mEntries.insert(key, value);
+ action.entries.insert(key, value);
- if (key == "Name") action->mName = value;
- else if (key == "Icon") action->mIcon = value;
- else if (key == "Exec") action->mExecString = value;
+ if (key == "Name") action.name = value;
+ else if (key == "Icon") action.icon = value;
+ else if (key == "Exec") {
+ action.execString = value;
+ action.command = DesktopEntry::parseExecString(value);
+ }
}
- this->mActions.insert(actionName, action);
+ pendingActions.insert(actionName, action);
}
entries.clear();
@@ -175,16 +197,73 @@ void DesktopEntry::parseEntry(const QString& text) {
}
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& 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 {
- DesktopEntry::doExec(this->mExecString, this->mWorkingDirectory);
+ DesktopEntry::doExec(this->bCommand.value(), this->bWorkingDirectory.value());
}
-bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); }
-bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
+bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); }
-QVector DesktopEntry::actions() const { return this->mActions.values(); }
+QVector DesktopEntry::actions() const { return this->mActions; }
QVector DesktopEntry::parseExecString(const QString& execString) {
QVector arguments;
@@ -203,16 +282,22 @@ QVector DesktopEntry::parseExecString(const QString& execString) {
currentArgument += '\\';
escape = 0;
}
+ } else if (escape == 2) {
+ currentArgument += c;
+ escape = 0;
} else if (escape != 0) {
- if (escape != 2) {
- // Technically this is an illegal state, but the spec has a terrible double escape
- // rule in strings for no discernable reason. Assuming someone might understandably
- // misunderstand it, treat it as a normal escape and log it.
+ switch (c.unicode()) {
+ case 's': currentArgument += u' '; break;
+ case 'n': currentArgument += u'\n'; break;
+ case 't': currentArgument += u'\t'; break;
+ case 'r': currentArgument += u'\r'; break;
+ case '\\': currentArgument += u'\\'; break;
+ default:
qCWarning(logDesktopEntry).noquote()
<< "Illegal escape sequence in desktop entry exec string:" << execString;
+ currentArgument += c;
+ break;
}
-
- currentArgument += c;
escape = 0;
} else if (c == u'"' || c == u'\'') {
parsingString = false;
@@ -250,75 +335,52 @@ QVector DesktopEntry::parseExecString(const QString& execString) {
return arguments;
}
-void DesktopEntry::doExec(const QString& execString, const QString& workingDirectory) {
- auto args = DesktopEntry::parseExecString(execString);
- if (args.isEmpty()) {
- qCWarning(logDesktopEntry) << "Tried to exec string" << execString << "which parsed as empty.";
- 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 DesktopEntry::doExec(const QList& execString, const QString& workingDirectory) {
+ qs::io::process::ProcessContext ctx;
+ ctx.setCommand(execString);
+ ctx.setWorkingDirectory(workingDirectory);
+ QuickshellGlobal::execDetached(ctx);
}
void DesktopAction::execute() const {
- DesktopEntry::doExec(this->mExecString, this->entry->mWorkingDirectory);
+ DesktopEntry::doExec(this->bCommand.value(), this->entry->bWorkingDirectory.value());
}
-DesktopEntryManager::DesktopEntryManager() {
- this->scanDesktopEntries();
- this->populateApplications();
+DesktopEntryScanner::DesktopEntryScanner(DesktopEntryManager* manager): manager(manager) {
+ this->setAutoDelete(true);
}
-void DesktopEntryManager::scanDesktopEntries() {
- QList dataPaths;
+void DesktopEntryScanner::run() {
+ const auto& desktopPaths = DesktopEntryManager::desktopPaths();
+ auto scanResults = QList();
- if (qEnvironmentVariableIsSet("XDG_DATA_HOME")) {
- dataPaths.push_back(qEnvironmentVariable("XDG_DATA_HOME"));
- } else if (qEnvironmentVariableIsSet("HOME")) {
- dataPaths.push_back(qEnvironmentVariable("HOME") + "/.local/share");
+ for (const auto& path: desktopPaths | std::views::reverse) {
+ auto file = QFileInfo(path);
+ if (!file.isDir()) continue;
+
+ this->scanDirectory(QDir(path), QString(), scanResults);
}
- if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) {
- auto var = qEnvironmentVariable("XDG_DATA_DIRS");
- dataPaths += var.split(u':', Qt::SkipEmptyParts);
- } else {
- dataPaths.push_back("/usr/local/share");
- dataPaths.push_back("/usr/share");
- }
-
- qCDebug(logDesktopEntry) << "Creating desktop entry scanners";
-
- for (auto& path: std::ranges::reverse_view(dataPaths)) {
- auto p = QDir(path).filePath("applications");
- auto file = QFileInfo(p);
-
- if (!file.isDir()) {
- qCDebug(logDesktopEntry) << "Not scanning path" << p << "as it is not a directory";
- continue;
- }
-
- qCDebug(logDesktopEntry) << "Scanning path" << p;
- this->scanPath(p);
- }
+ QMetaObject::invokeMethod(
+ this->manager,
+ "onScanCompleted",
+ Qt::QueuedConnection,
+ Q_ARG(QList, scanResults)
+ );
}
-void DesktopEntryManager::populateApplications() {
- for (auto& entry: this->desktopEntries.values()) {
- if (!entry->noDisplay()) this->mApplications.insertObject(entry);
- }
-}
+void DesktopEntryScanner::scanDirectory(
+ const QDir& dir,
+ const QString& idPrefix,
+ QList& entries
+) {
+ auto dirEntries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
-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()) {
+ for (auto& entry: dirEntries) {
+ if (entry.isDir()) {
+ auto subdirPrefix = idPrefix.isEmpty() ? entry.fileName() : idPrefix + '-' + entry.fileName();
+ this->scanDirectory(QDir(entry.absoluteFilePath()), subdirPrefix, entries);
+ } else if (entry.isFile()) {
auto path = entry.filePath();
if (!path.endsWith(".desktop")) {
qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
@@ -331,46 +393,42 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
continue;
}
- auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8);
- auto lowerId = id.toLower();
+ auto basename = QFileInfo(entry.fileName()).completeBaseName();
+ auto id = idPrefix.isEmpty() ? basename : idPrefix + '-' + basename;
+ auto content = QString::fromUtf8(file.readAll());
- auto text = QString::fromUtf8(file.readAll());
- auto* dentry = new DesktopEntry(id, this);
- dentry->parseEntry(text);
-
- if (!dentry->isValid()) {
- qCDebug(logDesktopEntry) << "Skipping desktop entry" << path;
- delete dentry;
- continue;
- }
-
- qCDebug(logDesktopEntry) << "Found desktop entry" << id << "at" << path;
-
- 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);
- }
-
- this->desktopEntries.insert(id, dentry);
-
- if (this->lowercaseDesktopEntries.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.";
-
- this->lowercaseDesktopEntries.remove(lowerId);
- }
-
- this->lowercaseDesktopEntries.insert(lowerId, dentry);
+ auto data = DesktopEntry::parseText(id, content);
+ entries.append(std::move(data));
}
}
}
+DesktopEntryManager::DesktopEntryManager(): monitor(new DesktopEntryMonitor(this)) {
+ QObject::connect(
+ this->monitor,
+ &DesktopEntryMonitor::desktopEntriesChanged,
+ this,
+ &DesktopEntryManager::handleFileChanges
+ );
+
+ DesktopEntryScanner(this).run();
+}
+
+void DesktopEntryManager::scanDesktopEntries() {
+ qCDebug(logDesktopEntry) << "Starting desktop entry scan";
+
+ 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);
+}
+
DesktopEntryManager* DesktopEntryManager::instance() {
static auto* instance = new DesktopEntryManager(); // NOLINT
return instance;
@@ -386,14 +444,167 @@ 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* DesktopEntryManager::applications() { return &this->mApplications; }
-DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); }
+void DesktopEntryManager::handleFileChanges() {
+ 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& scanResults) {
+ auto guard = qScopeGuard([this] {
+ this->scanInProgress = false;
+ if (this->scanQueued) {
+ this->scanQueued = false;
+ this->scanDesktopEntries();
+ }
+ });
+
+ auto oldEntries = this->desktopEntries;
+ auto newEntries = QHash();
+ auto newLowercaseEntries = QHash();
+
+ 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();
+ 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) {
return DesktopEntryManager::instance()->byId(id);
}
+DesktopEntry* DesktopEntries::heuristicLookup(const QString& name) {
+ return DesktopEntryManager::instance()->heuristicLookup(name);
+}
+
ObjectModel* DesktopEntries::applications() {
return DesktopEntryManager::instance()->applications();
}
diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp
index 3871181..0d1eff2 100644
--- a/src/core/desktopentry.hpp
+++ b/src/core/desktopentry.hpp
@@ -6,36 +6,84 @@
#include
#include
#include
+#include
#include
+#include
#include
+#include "desktopentrymonitor.hpp"
#include "doc.hpp"
#include "model.hpp"
class DesktopAction;
+class DesktopEntryMonitor;
+
+struct DesktopActionData {
+ QString id;
+ QString name;
+ QString icon;
+ QString execString;
+ QVector command;
+ QHash entries;
+};
+
+struct ParsedDesktopEntryData {
+ QString id;
+ QString name;
+ QString genericName;
+ QString startupClass;
+ bool noDisplay = false;
+ bool hidden = false;
+ QString comment;
+ QString icon;
+ QString execString;
+ QVector command;
+ QString workingDirectory;
+ bool terminal = false;
+ QVector categories;
+ QVector keywords;
+ QHash entries;
+ QVector actions;
+};
/// A desktop entry. See @@DesktopEntries for details.
class DesktopEntry: public QObject {
Q_OBJECT;
Q_PROPERTY(QString id MEMBER mId CONSTANT);
/// Name of the specific application, such as "Firefox".
- Q_PROPERTY(QString name MEMBER mName CONSTANT);
+ // clang-format off
+ Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName);
/// Short description of the application, such as "Web Browser". May be empty.
- Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT);
+ Q_PROPERTY(QString genericName READ default WRITE default NOTIFY genericNameChanged BINDABLE bindableGenericName);
+ /// 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.
- Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT);
+ Q_PROPERTY(bool noDisplay READ default WRITE default NOTIFY noDisplayChanged BINDABLE bindableNoDisplay);
/// Long description of the application, such as "View websites on the internet". May be empty.
- Q_PROPERTY(QString comment MEMBER mComment CONSTANT);
+ Q_PROPERTY(QString comment READ default WRITE default NOTIFY commentChanged BINDABLE bindableComment);
/// Name of the icon associated with this application. May be empty.
- Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
- /// The raw `Exec` string from the desktop entry. You probably want @@execute().
- Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
+ Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon);
+ /// The raw `Exec` string from the desktop entry.
+ ///
+ /// > [!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 command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand);
/// The working directory to execute from.
- Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT);
+ Q_PROPERTY(QString workingDirectory READ default WRITE default NOTIFY workingDirectoryChanged BINDABLE bindableWorkingDirectory);
/// If the application should run in a terminal.
- Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT);
- Q_PROPERTY(QVector categories MEMBER mCategories CONSTANT);
- Q_PROPERTY(QVector keywords MEMBER mKeywords CONSTANT);
+ Q_PROPERTY(bool runInTerminal READ default WRITE default NOTIFY runInTerminalChanged BINDABLE bindableRunInTerminal);
+ Q_PROPERTY(QVector categories READ default WRITE default NOTIFY categoriesChanged BINDABLE bindableCategories);
+ Q_PROPERTY(QVector keywords READ default WRITE default NOTIFY keywordsChanged BINDABLE bindableKeywords);
+ // clang-format on
Q_PROPERTY(QVector actions READ actions CONSTANT);
QML_ELEMENT;
QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
@@ -43,35 +91,83 @@ class DesktopEntry: public QObject {
public:
explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
- void parseEntry(const QString& text);
+ static ParsedDesktopEntryData parseText(const QString& id, const QString& text);
+ void updateState(const ParsedDesktopEntryData& newState);
/// 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;
[[nodiscard]] bool isValid() const;
- [[nodiscard]] bool noDisplay() const;
[[nodiscard]] QVector actions() const;
+ [[nodiscard]] QBindable bindableName() const { return &this->bName; }
+ [[nodiscard]] QBindable bindableGenericName() const { return &this->bGenericName; }
+ [[nodiscard]] QBindable bindableStartupClass() const { return &this->bStartupClass; }
+ [[nodiscard]] QBindable bindableNoDisplay() const { return &this->bNoDisplay; }
+ [[nodiscard]] QBindable bindableComment() const { return &this->bComment; }
+ [[nodiscard]] QBindable bindableIcon() const { return &this->bIcon; }
+ [[nodiscard]] QBindable bindableExecString() const { return &this->bExecString; }
+ [[nodiscard]] QBindable> bindableCommand() const { return &this->bCommand; }
+ [[nodiscard]] QBindable bindableWorkingDirectory() const {
+ return &this->bWorkingDirectory;
+ }
+ [[nodiscard]] QBindable bindableRunInTerminal() const { return &this->bRunInTerminal; }
+ [[nodiscard]] QBindable> bindableCategories() const {
+ return &this->bCategories;
+ }
+ [[nodiscard]] QBindable> bindableKeywords() const { return &this->bKeywords; }
+
// currently ignores all field codes.
static QVector parseExecString(const QString& execString);
- static void doExec(const QString& execString, const QString& workingDirectory);
+ static void doExec(const QList& 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:
QString mId;
- QString mName;
- QString mGenericName;
- bool mNoDisplay = false;
- QString mComment;
- QString mIcon;
- QString mExecString;
- QString mWorkingDirectory;
- bool mTerminal = false;
- QVector mCategories;
- QVector mKeywords;
+
+ // clang-format off
+ Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bName, &DesktopEntry::nameChanged);
+ Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bGenericName, &DesktopEntry::genericNameChanged);
+ Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bStartupClass, &DesktopEntry::startupClassChanged);
+ Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bNoDisplay, &DesktopEntry::noDisplayChanged);
+ Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bComment, &DesktopEntry::commentChanged);
+ Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bIcon, &DesktopEntry::iconChanged);
+ Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bExecString, &DesktopEntry::execStringChanged);
+ Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector, bCommand, &DesktopEntry::commandChanged);
+ Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bWorkingDirectory, &DesktopEntry::workingDirectoryChanged);
+ Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bRunInTerminal, &DesktopEntry::runInTerminalChanged);
+ Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector, bCategories, &DesktopEntry::categoriesChanged);
+ Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector