mirror of
https://github.com/quickshell-mirror/quickshell.git
synced 2026-10-28 02:55:05 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1a150fab0 | ||
|
|
40bc09b800 | ||
|
|
a94418f45d | ||
|
|
8d2a2d3dd2 | ||
|
|
e10747addd | ||
|
|
b254f6dabc | ||
|
|
7fc6635ca8 | ||
|
|
522d126d1b | ||
|
|
f5ca8453c0 | ||
|
|
e81e6da08f | ||
|
|
f73729754d | ||
|
|
344ca340ba | ||
|
|
6cc1b6f36a | ||
|
|
354afeaa35 | ||
|
|
62df8d917f | ||
|
|
2f0f2d1201 | ||
|
|
4b825e7051 | ||
|
|
d4b19e4a30 | ||
|
|
b9cce25061 | ||
|
|
996efc93b7 | ||
|
|
2115f31416 | ||
|
|
e4d33fa52f | ||
|
|
83f5af522d | ||
|
|
a5431dd02d | ||
|
|
f0d5f48a82 | ||
|
|
1c026545e9 | ||
|
|
0416032a7c | ||
|
|
1644ed5e19 | ||
|
|
91c9db581e | ||
|
|
ab096b7e78 | ||
|
|
448623de5a | ||
|
|
dfededc901 | ||
|
|
4dad447570 | ||
|
|
3bbf39c67e | ||
|
|
f90bef2d99 | ||
|
|
db77c71c21 | ||
|
|
fcffbbced8 | ||
|
|
759bd721df | ||
|
|
63a6d27213 | ||
|
|
77de23bb71 | ||
|
|
7b417bb808 | ||
|
|
e55d519c28 | ||
|
|
ecc4a1249d | ||
|
|
6572a7f61d | ||
|
|
e885f4aec1 | ||
|
|
115d6717a8 | ||
|
|
91dcb41d22 | ||
|
|
201c559dcd | ||
|
|
78e3874ac6 | ||
|
|
986749cdb9 | ||
|
|
4d8055f1cd | ||
|
|
a45fc03c7d | ||
|
|
c40074dd56 | ||
|
|
3dfb7d8827 | ||
|
|
a2146f6394 | ||
|
|
5706c09e6f | ||
|
|
5ac9096c1c | ||
|
|
05fbead660 | ||
|
|
478aa2bda1 | ||
|
|
cee1f5837e | ||
|
|
71334bfcaf | ||
|
|
de25787451 | ||
|
|
b011cd9d33 | ||
|
|
1e1ba93713 | ||
|
|
59d29bb254 | ||
|
|
9604302415 | ||
|
|
479ff58f84 | ||
|
|
3b4ebc5f16 | ||
|
|
bb206e3a19 | ||
|
|
0c9c5be8dd | ||
|
|
49a3752b9d | ||
|
|
026aac3756 | ||
|
|
d7079b7524 | ||
|
|
6f774af11e | ||
|
|
5703fbae21 | ||
|
|
07ea4de248 | ||
|
|
2629e211fa | ||
|
|
b4c62b8ff9 | ||
|
|
1af08c0c52 | ||
|
|
4b35d7b51b | ||
|
|
3d594e16dd | ||
|
|
5d7e07508a | ||
|
|
87d99b866f | ||
|
|
7eff415b25 | ||
|
|
3cc7ced3a0 | ||
|
|
fb37be7611 | ||
|
|
9708d8212a | ||
|
|
0e6518a706 | ||
|
|
86591f122d | ||
|
|
f681e2016f | ||
|
|
1d02292fbf | ||
|
|
f842b84a5a | ||
|
|
d949f91347 | ||
|
|
27f97c3283 | ||
|
|
20c3da01f1 | ||
|
|
8be18c05ed | ||
|
|
98d09b5a36 | ||
|
|
8fc3e1cb6e | ||
|
|
c17ea54371 | ||
|
|
362c8e1b69 | ||
|
|
c115df8d34 | ||
|
|
02362c3e94 | ||
|
|
3d3b7f1c05 | ||
|
|
79b2204af8 | ||
|
|
95d0af8113 | ||
|
|
579d589290 | ||
|
|
9a30333405 | ||
|
|
d9164578a2 | ||
|
|
20322484b9 | ||
|
|
0499518143 | ||
|
|
0140356d99 | ||
|
|
71fe3d9165 | ||
|
|
517143adf9 | ||
|
|
05b5eccf2e |
No files matched your search
@@ -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.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 }}"; }'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -41,16 +41,21 @@ 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`
|
||||
- `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.
|
||||
@@ -66,7 +71,7 @@ enable us to fix bugs far more easily.
|
||||
|
||||
To disable: `-DCRASH_REPORTER=OFF`
|
||||
|
||||
Dependencies: `google-breakpad`
|
||||
Dependencies: `google-breakpad` (static library)
|
||||
|
||||
### Jemalloc
|
||||
We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused
|
||||
@@ -99,10 +104,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,
|
||||
@@ -220,7 +228,7 @@ 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
|
||||
|
||||
+23
-2
@@ -1,5 +1,5 @@
|
||||
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(QT_MIN_VERSION "6.6.0")
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
@@ -70,6 +70,7 @@ boption(SERVICE_PAM "Pam" ON)
|
||||
boption(SERVICE_GREETD "Greetd" ON)
|
||||
boption(SERVICE_UPOWER "UPower" ON)
|
||||
boption(SERVICE_NOTIFICATIONS "Notifications" ON)
|
||||
boption(BLUETOOTH "Bluetooth" ON)
|
||||
|
||||
include(cmake/install-qml-module.cmake)
|
||||
include(cmake/util.cmake)
|
||||
@@ -99,6 +100,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 +116,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)
|
||||
set(DBUS ON)
|
||||
endif()
|
||||
|
||||
@@ -126,6 +129,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 +155,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
|
||||
)
|
||||
@@ -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 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" } }} \
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
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.
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Version=1.5
|
||||
Type=Application
|
||||
NoDisplay=true
|
||||
|
||||
Name=Quickshell
|
||||
Icon=org.quickshell
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="724.635" height="724.635"><path fill="#359757" stroke="#359757" stroke-linecap="square" stroke-linejoin="round" stroke-width="74.755" d="m37.378 160.237 122.859-122.86h527.02v527.02l-122.86 122.86H37.378Z"/><path fill="#fff" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="12.201" d="M323.051 96.412a268.74 268.74 0 0 0-3.51.542c-4.052 14.481-7.815 29.941-14.904 42.692a230.02 230.02 0 0 0-59.406 24.679c-14.036-3.974-27.647-12.214-40.766-19.562a268.788 268.788 0 0 0-60.035 60.16c7.376 13.105 15.645 26.698 19.648 40.726a230.02 230.02 0 0 0-24.554 59.458c-12.735 7.115-28.186 10.913-42.66 14.994a268.789 268.789 0 0 0 .09 84.992c14.48 4.05 29.941 7.814 42.691 14.903a230.02 230.02 0 0 0 24.68 59.406c-3.974 14.037-12.215 27.647-19.563 40.766a268.788 268.788 0 0 0 60.161 60.035c13.104-7.376 26.696-15.645 40.725-19.648a230.02 230.02 0 0 0 59.457 24.555c7.116 12.735 10.913 28.186 14.995 42.659a268.788 268.788 0 0 0 84.99-.09c4.052-14.482 7.817-29.941 14.906-42.691a230.02 230.02 0 0 0 59.405-24.68c14.037 3.974 33.069 17.638 46.188 24.986a268.788 268.788 0 0 0 60.035-60.161c-7.376-13.105-21.068-32.12-25.071-46.149a230.02 230.02 0 0 0 24.555-59.457c12.735-7.116 28.186-10.912 42.659-14.993a268.788 268.788 0 0 0-.089-84.993c-14.482-4.051-29.942-7.814-42.692-14.904a230.02 230.02 0 0 0-24.68-59.405c3.974-14.037 12.216-27.647 19.565-40.767a268.788 268.788 0 0 0-60.161-60.035c-13.105 7.376-26.698 15.645-40.726 19.649a230.02 230.02 0 0 0-59.458-24.555c-7.115-12.735-10.913-28.187-14.994-42.66a268.788 268.788 0 0 0-81.481-.452zm15.778 106.85c58.282-8.328 116.455 15.865 151.643 63.065 35.19 47.2 41.766 109.86 17.144 163.337l-41.728-22.688s-38.558 31.44-57.344 63.012l23.893 36.326a160.78 160.78 0 0 1-46.633 15.058c-87.854 12.99-169.6-47.708-182.573-135.564-12.974-87.855 47.74-169.59 135.598-182.546Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1 @@
|
||||
Initial release
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
+5
-2
@@ -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
|
||||
+12
-3
@@ -7,9 +7,18 @@ 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_9_0;
|
||||
|
||||
qt6_9_2 = byCommit {
|
||||
commit = "e9f00bd893984bc8ce46c895c3bf7cac95331127";
|
||||
sha256 = "0s2mhbrgzxlgkg2yxb0q0hpk8lby1a7w67dxvfmaz4gsmc0bnvfj";
|
||||
};
|
||||
|
||||
qt6_9_1 = byCommit {
|
||||
commit = "4c202d26483c5ccf3cb95e0053163facde9f047e";
|
||||
sha256 = "06l2w4bcgfw7dfanpzpjcf25ydf84in240yplqsss82qx405y9di";
|
||||
};
|
||||
|
||||
qt6_9_0 = byCommit {
|
||||
commit = "546c545bd0594809a28ab7e869b5f80dd7243ef6";
|
||||
|
||||
+2
-2
@@ -2,6 +2,6 @@
|
||||
clangStdenv,
|
||||
gccStdenv,
|
||||
}: {
|
||||
clang = { buildStdenv = clangStdenv; };
|
||||
gcc = { buildStdenv = gccStdenv; };
|
||||
clang = { stdenv = clangStdenv; };
|
||||
gcc = { stdenv = gccStdenv; };
|
||||
}
|
||||
+90
-58
@@ -2,21 +2,23 @@
|
||||
lib,
|
||||
nix-gitignore,
|
||||
pkgs,
|
||||
stdenv,
|
||||
keepDebugInfo,
|
||||
buildStdenv ? pkgs.clangStdenv,
|
||||
|
||||
pkg-config,
|
||||
cmake,
|
||||
ninja,
|
||||
qt6,
|
||||
spirv-tools,
|
||||
cli11,
|
||||
qt6,
|
||||
breakpad,
|
||||
jemalloc,
|
||||
cli11,
|
||||
wayland,
|
||||
wayland-protocols,
|
||||
wayland-scanner,
|
||||
xorg,
|
||||
libdrm,
|
||||
libgbm ? null,
|
||||
xorg,
|
||||
pipewire,
|
||||
pam,
|
||||
|
||||
@@ -41,64 +43,94 @@
|
||||
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" ./.;
|
||||
}: let
|
||||
unwrapped = stdenv.mkDerivation {
|
||||
pname = "quickshell${lib.optionalString debug "-debug"}";
|
||||
version = "0.2.0";
|
||||
src = nix-gitignore.gitignoreSource "/default.nix\n" ./.;
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
cmake
|
||||
ninja
|
||||
qt6.qtshadertools
|
||||
spirv-tools
|
||||
qt6.wrapQtAppsHook
|
||||
pkg-config
|
||||
] ++ (lib.optionals withWayland [
|
||||
wayland-protocols
|
||||
wayland-scanner
|
||||
]);
|
||||
dontWrapQtApps = true; # see wrappers
|
||||
|
||||
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;
|
||||
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
|
||||
];
|
||||
|
||||
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
|
||||
buildInputs = [
|
||||
qt6.qtbase
|
||||
qt6.qtdeclarative
|
||||
cli11
|
||||
]
|
||||
++ lib.optional withQtSvg qt6.qtsvg
|
||||
++ lib.optional withCrashReporter breakpad
|
||||
++ lib.optional withJemalloc jemalloc
|
||||
++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
|
||||
++ lib.optionals withWayland [ wayland wayland-protocols ]
|
||||
++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ]
|
||||
++ lib.optional withX11 xorg.libxcb
|
||||
++ lib.optional withPam pam
|
||||
++ lib.optional withPipewire pipewire;
|
||||
|
||||
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)
|
||||
];
|
||||
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
|
||||
|
||||
# How to get debuginfo in gdb from a release build:
|
||||
# 1. build `quickshell.debug`
|
||||
# 2. set NIX_DEBUG_INFO_DIRS="<quickshell.debug store path>/lib/debug"
|
||||
# 3. launch gdb / coredumpctl and debuginfo will work
|
||||
separateDebugInfo = !debug;
|
||||
dontStrip = debug;
|
||||
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)
|
||||
];
|
||||
|
||||
meta = with lib; {
|
||||
homepage = "https://git.outfoxxed.me/outfoxxed/quickshell";
|
||||
description = "Flexbile QtQuick based desktop shell toolkit";
|
||||
license = licenses.lgpl3Only;
|
||||
platforms = platforms.linux;
|
||||
# How to get debuginfo in gdb from a release build:
|
||||
# 1. build `quickshell.debug`
|
||||
# 2. set NIX_DEBUG_INFO_DIRS="<quickshell.debug store path>/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
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1736012469,
|
||||
"narHash": "sha256-/qlNWm/IEVVH7GfgAIyP6EsVZI6zjAx1cV5zNyrs+rI=",
|
||||
"lastModified": 1758690382,
|
||||
"narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8f3e1f807051e32d8c95cd12b9b421623850a34d",
|
||||
"rev": "e643668fd71b949c53f8626614b21ff71a07379d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{ rev ? null }: (final: prev: {
|
||||
quickshell = final.callPackage ./default.nix {
|
||||
gitRev = rev;
|
||||
};
|
||||
})
|
||||
+3
-3
@@ -35,9 +35,9 @@
|
||||
pkg-config
|
||||
qtshadertools
|
||||
spirv-tools
|
||||
wayland-protocols))
|
||||
(inputs (list cli11
|
||||
jemalloc
|
||||
wayland-protocols
|
||||
cli11))
|
||||
(inputs (list jemalloc
|
||||
libdrm
|
||||
libxcb
|
||||
libxkbcommon
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
{
|
||||
pkgs ? import <nixpkgs> {},
|
||||
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 ];
|
||||
|
||||
@@ -29,3 +29,7 @@ if (X11)
|
||||
endif()
|
||||
|
||||
add_subdirectory(services)
|
||||
|
||||
if (BLUETOOTH)
|
||||
add_subdirectory(bluetooth)
|
||||
endif()
|
||||
@@ -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)
|
||||
@@ -0,0 +1,224 @@
|
||||
#include "adapter.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdbusconnection.h>
|
||||
#include <qdbusextratypes.h>
|
||||
#include <qdbuspendingcall.h>
|
||||
#include <qdbuspendingreply.h>
|
||||
#include <qdebug.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qstring.h>
|
||||
#include <qstringliteral.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "../dbus/properties.hpp"
|
||||
#include "dbus_adapter.h"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logAdapter, "quickshell.bluetooth.adapter", QtWarningMsg);
|
||||
}
|
||||
|
||||
QString BluetoothAdapterState::toString(BluetoothAdapterState::Enum state) {
|
||||
switch (state) {
|
||||
case BluetoothAdapterState::Disabled: return QStringLiteral("Disabled");
|
||||
case BluetoothAdapterState::Enabled: return QStringLiteral("Enabled");
|
||||
case BluetoothAdapterState::Enabling: return QStringLiteral("Enabling");
|
||||
case BluetoothAdapterState::Disabling: return QStringLiteral("Disabling");
|
||||
case BluetoothAdapterState::Blocked: return QStringLiteral("Blocked");
|
||||
default: return QStringLiteral("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
BluetoothAdapter::BluetoothAdapter(const QString& path, QObject* parent): QObject(parent) {
|
||||
this->mInterface =
|
||||
new DBusBluezAdapterInterface("org.bluez", path, QDBusConnection::systemBus(), this);
|
||||
|
||||
if (!this->mInterface->isValid()) {
|
||||
qCWarning(logAdapter) << "Could not create DBus interface for adapter at" << path;
|
||||
this->mInterface = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
this->properties.setInterface(this->mInterface);
|
||||
}
|
||||
|
||||
QString BluetoothAdapter::adapterId() const {
|
||||
auto path = this->path();
|
||||
return path.sliced(path.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setEnabled(bool enabled) {
|
||||
if (enabled == this->bEnabled) return;
|
||||
|
||||
if (enabled && this->bState == BluetoothAdapterState::Blocked) {
|
||||
qCCritical(logAdapter) << "Cannot enable adapter because it is blocked by rfkill.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->bEnabled = enabled;
|
||||
this->pEnabled.write();
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setDiscoverable(bool discoverable) {
|
||||
if (discoverable == this->bDiscoverable) return;
|
||||
this->bDiscoverable = discoverable;
|
||||
this->pDiscoverable.write();
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setDiscovering(bool discovering) {
|
||||
if (discovering) {
|
||||
this->startDiscovery();
|
||||
} else {
|
||||
this->stopDiscovery();
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setDiscoverableTimeout(quint32 timeout) {
|
||||
if (timeout == this->bDiscoverableTimeout) return;
|
||||
this->bDiscoverableTimeout = timeout;
|
||||
this->pDiscoverableTimeout.write();
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setPairable(bool pairable) {
|
||||
if (pairable == this->bPairable) return;
|
||||
this->bPairable = pairable;
|
||||
this->pPairable.write();
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setPairableTimeout(quint32 timeout) {
|
||||
if (timeout == this->bPairableTimeout) return;
|
||||
this->bPairableTimeout = timeout;
|
||||
this->pPairableTimeout.write();
|
||||
}
|
||||
|
||||
void BluetoothAdapter::addInterface(const QString& interface, const QVariantMap& properties) {
|
||||
if (interface == "org.bluez.Adapter1") {
|
||||
this->properties.updatePropertySet(properties, false);
|
||||
qCDebug(logAdapter) << "Updated Adapter properties for" << this;
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothAdapter::removeDevice(const QString& devicePath) {
|
||||
qCDebug(logAdapter) << "Removing device" << devicePath << "from adapter" << this;
|
||||
|
||||
auto reply = this->mInterface->RemoveDevice(QDBusObjectPath(devicePath));
|
||||
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this, devicePath](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logAdapter).nospace()
|
||||
<< "Failed to remove device " << devicePath << " from adapter" << this << ": "
|
||||
<< reply.error().message();
|
||||
} else {
|
||||
qCDebug(logAdapter) << "Successfully removed device" << devicePath << "from adapter"
|
||||
<< this;
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothAdapter::startDiscovery() {
|
||||
if (this->bDiscovering) return;
|
||||
qCDebug(logAdapter) << "Starting discovery for adapter" << this;
|
||||
|
||||
auto reply = this->mInterface->StartDiscovery();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logAdapter).nospace()
|
||||
<< "Failed to start discovery on adapter" << this << ": " << reply.error().message();
|
||||
} else {
|
||||
qCDebug(logAdapter) << "Successfully started discovery on adapter" << this;
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothAdapter::stopDiscovery() {
|
||||
if (!this->bDiscovering) return;
|
||||
qCDebug(logAdapter) << "Stopping discovery for adapter" << this;
|
||||
|
||||
auto reply = this->mInterface->StopDiscovery();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logAdapter).nospace()
|
||||
<< "Failed to stop discovery on adapter " << this << ": " << reply.error().message();
|
||||
} else {
|
||||
qCDebug(logAdapter) << "Successfully stopped discovery on adapter" << this;
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
using namespace qs::bluetooth;
|
||||
|
||||
DBusResult<BluetoothAdapterState::Enum>
|
||||
DBusDataTransform<BluetoothAdapterState::Enum>::fromWire(const Wire& wire) {
|
||||
if (wire == QStringLiteral("off")) {
|
||||
return BluetoothAdapterState::Disabled;
|
||||
} else if (wire == QStringLiteral("on")) {
|
||||
return BluetoothAdapterState::Enabled;
|
||||
} else if (wire == QStringLiteral("off-enabling")) {
|
||||
return BluetoothAdapterState::Enabling;
|
||||
} else if (wire == QStringLiteral("on-disabling")) {
|
||||
return BluetoothAdapterState::Disabling;
|
||||
} else if (wire == QStringLiteral("off-blocked")) {
|
||||
return BluetoothAdapterState::Blocked;
|
||||
} else {
|
||||
return QDBusError(
|
||||
QDBusError::InvalidArgs,
|
||||
QString("Invalid BluetoothAdapterState: %1").arg(wire)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter) {
|
||||
auto saver = QDebugStateSaver(debug);
|
||||
|
||||
if (adapter) {
|
||||
debug.nospace() << "BluetoothAdapter(" << static_cast<const void*>(adapter)
|
||||
<< ", path=" << adapter->path() << ")";
|
||||
} else {
|
||||
debug << "BluetoothAdapter(nullptr)";
|
||||
}
|
||||
|
||||
return debug;
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../core/doc.hpp"
|
||||
#include "../core/model.hpp"
|
||||
#include "../dbus/properties.hpp"
|
||||
#include "dbus_adapter.h"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
///! Power state of a Bluetooth adapter.
|
||||
class BluetoothAdapterState: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
||||
public:
|
||||
enum Enum : quint8 {
|
||||
/// The adapter is powered off.
|
||||
Disabled = 0,
|
||||
/// The adapter is powered on.
|
||||
Enabled = 1,
|
||||
/// The adapter is transitioning from off to on.
|
||||
Enabling = 2,
|
||||
/// The adapter is transitioning from on to off.
|
||||
Disabling = 3,
|
||||
/// The adapter is blocked by rfkill.
|
||||
Blocked = 4,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
Q_INVOKABLE static QString toString(BluetoothAdapterState::Enum state);
|
||||
};
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
template <>
|
||||
struct DBusDataTransform<qs::bluetooth::BluetoothAdapterState::Enum> {
|
||||
using Wire = QString;
|
||||
using Data = qs::bluetooth::BluetoothAdapterState::Enum;
|
||||
static DBusResult<Data> fromWire(const Wire& wire);
|
||||
};
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
class BluetoothAdapter;
|
||||
class BluetoothDevice;
|
||||
|
||||
///! A Bluetooth adapter
|
||||
class BluetoothAdapter: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("");
|
||||
// clang-format off
|
||||
/// System provided name of the adapter. See @@adapterId for the internal identifier.
|
||||
Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName);
|
||||
/// True if the adapter is currently enabled. More detailed state is available from @@state.
|
||||
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
|
||||
/// Detailed power state of the adapter.
|
||||
Q_PROPERTY(BluetoothAdapterState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
|
||||
/// True if the adapter can be discovered by other bluetooth devices.
|
||||
Q_PROPERTY(bool discoverable READ discoverable WRITE setDiscoverable NOTIFY discoverableChanged);
|
||||
/// Timeout in seconds for how long the adapter stays discoverable after @@discoverable is set to true.
|
||||
/// A value of 0 means the adapter stays discoverable forever.
|
||||
Q_PROPERTY(quint32 discoverableTimeout READ discoverableTimeout WRITE setDiscoverableTimeout NOTIFY discoverableTimeoutChanged);
|
||||
/// True if the adapter is scanning for new devices.
|
||||
Q_PROPERTY(bool discovering READ discovering WRITE setDiscovering NOTIFY discoveringChanged);
|
||||
/// True if the adapter is accepting incoming pairing requests.
|
||||
///
|
||||
/// This only affects incoming pairing requests and should typically only be changed
|
||||
/// by system settings applications. Defaults to true.
|
||||
Q_PROPERTY(bool pairable READ pairable WRITE setPairable NOTIFY pairableChanged);
|
||||
/// Timeout in seconds for how long the adapter stays pairable after @@pairable is set to true.
|
||||
/// A value of 0 means the adapter stays pairable forever. Defaults to 0.
|
||||
Q_PROPERTY(quint32 pairableTimeout READ pairableTimeout WRITE setPairableTimeout NOTIFY pairableTimeoutChanged);
|
||||
/// Bluetooth devices connected to this adapter.
|
||||
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothDevice>*);
|
||||
Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
|
||||
/// The internal ID of the adapter (e.g., "hci0").
|
||||
Q_PROPERTY(QString adapterId READ adapterId CONSTANT);
|
||||
/// DBus path of the adapter under the `org.bluez` system service.
|
||||
Q_PROPERTY(QString dbusPath READ path CONSTANT);
|
||||
// clang-format on
|
||||
|
||||
public:
|
||||
explicit BluetoothAdapter(const QString& path, QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] bool isValid() const { return this->mInterface->isValid(); }
|
||||
[[nodiscard]] QString path() const { return this->mInterface->path(); }
|
||||
[[nodiscard]] QString adapterId() const;
|
||||
|
||||
[[nodiscard]] bool enabled() const { return this->bEnabled; }
|
||||
void setEnabled(bool enabled);
|
||||
|
||||
[[nodiscard]] bool discoverable() const { return this->bDiscoverable; }
|
||||
void setDiscoverable(bool discoverable);
|
||||
|
||||
[[nodiscard]] bool discovering() const { return this->bDiscovering; }
|
||||
void setDiscovering(bool discovering);
|
||||
|
||||
[[nodiscard]] quint32 discoverableTimeout() const { return this->bDiscoverableTimeout; }
|
||||
void setDiscoverableTimeout(quint32 timeout);
|
||||
|
||||
[[nodiscard]] bool pairable() const { return this->bPairable; }
|
||||
void setPairable(bool pairable);
|
||||
|
||||
[[nodiscard]] quint32 pairableTimeout() const { return this->bPairableTimeout; }
|
||||
void setPairableTimeout(quint32 timeout);
|
||||
|
||||
[[nodiscard]] QBindable<QString> bindableName() { return &this->bName; }
|
||||
[[nodiscard]] QBindable<bool> bindableEnabled() { return &this->bEnabled; }
|
||||
[[nodiscard]] QBindable<BluetoothAdapterState::Enum> bindableState() { return &this->bState; }
|
||||
[[nodiscard]] QBindable<bool> bindableDiscoverable() { return &this->bDiscoverable; }
|
||||
[[nodiscard]] QBindable<quint32> bindableDiscoverableTimeout() {
|
||||
return &this->bDiscoverableTimeout;
|
||||
}
|
||||
[[nodiscard]] QBindable<bool> bindableDiscovering() { return &this->bDiscovering; }
|
||||
[[nodiscard]] QBindable<bool> bindablePairable() { return &this->bPairable; }
|
||||
[[nodiscard]] QBindable<quint32> bindablePairableTimeout() { return &this->bPairableTimeout; }
|
||||
[[nodiscard]] ObjectModel<BluetoothDevice>* devices() { return &this->mDevices; }
|
||||
|
||||
void addInterface(const QString& interface, const QVariantMap& properties);
|
||||
void removeDevice(const QString& devicePath);
|
||||
|
||||
void startDiscovery();
|
||||
void stopDiscovery();
|
||||
|
||||
signals:
|
||||
void nameChanged();
|
||||
void enabledChanged();
|
||||
void stateChanged();
|
||||
void discoverableChanged();
|
||||
void discoverableTimeoutChanged();
|
||||
void discoveringChanged();
|
||||
void pairableChanged();
|
||||
void pairableTimeoutChanged();
|
||||
|
||||
private:
|
||||
DBusBluezAdapterInterface* mInterface = nullptr;
|
||||
ObjectModel<BluetoothDevice> mDevices {this};
|
||||
|
||||
// clang-format off
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, QString, bName, &BluetoothAdapter::nameChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bEnabled, &BluetoothAdapter::enabledChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, BluetoothAdapterState::Enum, bState, &BluetoothAdapter::stateChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscoverable, &BluetoothAdapter::discoverableChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bDiscoverableTimeout, &BluetoothAdapter::discoverableTimeoutChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscovering, &BluetoothAdapter::discoveringChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bPairable, &BluetoothAdapter::pairableChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bPairableTimeout, &BluetoothAdapter::pairableTimeoutChanged);
|
||||
|
||||
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothAdapter, properties);
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pName, bName, properties, "Alias");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pEnabled, bEnabled, properties, "Powered");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pState, bState, properties, "PowerState");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverable, bDiscoverable, properties, "Discoverable");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverableTimeout, bDiscoverableTimeout, properties, "DiscoverableTimeout");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscovering, bDiscovering, properties, "Discovering");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairable, bPairable, properties, "Pairable");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairableTimeout, bPairableTimeout, properties, "PairableTimeout");
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter);
|
||||
@@ -0,0 +1,168 @@
|
||||
#include "bluez.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdbusconnection.h>
|
||||
#include <qdbusextratypes.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "../dbus/dbus_objectmanager_types.hpp"
|
||||
#include "../dbus/objectmanager.hpp"
|
||||
#include "adapter.hpp"
|
||||
#include "device.hpp"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logBluetooth, "quickshell.bluetooth", QtWarningMsg);
|
||||
}
|
||||
|
||||
Bluez* Bluez::instance() {
|
||||
static auto* instance = new Bluez();
|
||||
return instance;
|
||||
}
|
||||
|
||||
Bluez::Bluez() { this->init(); }
|
||||
|
||||
void Bluez::updateDefaultAdapter() {
|
||||
const auto& adapters = this->mAdapters.valueList();
|
||||
this->bDefaultAdapter = adapters.empty() ? nullptr : adapters.first();
|
||||
}
|
||||
|
||||
void Bluez::init() {
|
||||
qCDebug(logBluetooth) << "Connecting to BlueZ";
|
||||
|
||||
auto bus = QDBusConnection::systemBus();
|
||||
|
||||
if (!bus.isConnected()) {
|
||||
qCWarning(logBluetooth) << "Could not connect to DBus. Bluetooth integration is not available.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->objectManager = new qs::dbus::DBusObjectManager(this);
|
||||
|
||||
QObject::connect(
|
||||
this->objectManager,
|
||||
&qs::dbus::DBusObjectManager::interfacesAdded,
|
||||
this,
|
||||
&Bluez::onInterfacesAdded
|
||||
);
|
||||
|
||||
QObject::connect(
|
||||
this->objectManager,
|
||||
&qs::dbus::DBusObjectManager::interfacesRemoved,
|
||||
this,
|
||||
&Bluez::onInterfacesRemoved
|
||||
);
|
||||
|
||||
if (!this->objectManager->setInterface("org.bluez", "/", bus)) {
|
||||
qCDebug(logBluetooth) << "BlueZ is not running. Bluetooth integration will not work.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Bluez::onInterfacesAdded(
|
||||
const QDBusObjectPath& path,
|
||||
const DBusObjectManagerInterfaces& interfaces
|
||||
) {
|
||||
if (auto* adapter = this->mAdapterMap.value(path.path())) {
|
||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
||||
adapter->addInterface(interface, properties);
|
||||
}
|
||||
} else if (auto* device = this->mDeviceMap.value(path.path())) {
|
||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
||||
device->addInterface(interface, properties);
|
||||
}
|
||||
} else if (interfaces.contains("org.bluez.Adapter1")) {
|
||||
auto* adapter = new BluetoothAdapter(path.path(), this);
|
||||
|
||||
if (!adapter->isValid()) {
|
||||
qCWarning(logBluetooth) << "Adapter path is not valid, cannot track: " << device;
|
||||
delete adapter;
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logBluetooth) << "Tracked new adapter" << adapter;
|
||||
|
||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
||||
adapter->addInterface(interface, properties);
|
||||
}
|
||||
|
||||
for (auto* device: this->mDevices.valueList()) {
|
||||
if (device->adapterPath() == path) {
|
||||
adapter->devices()->insertObject(device);
|
||||
qCDebug(logBluetooth) << "Added tracked device" << device << "to new adapter" << adapter;
|
||||
emit device->adapterChanged();
|
||||
}
|
||||
}
|
||||
|
||||
this->mAdapterMap.insert(path.path(), adapter);
|
||||
this->mAdapters.insertObject(adapter);
|
||||
this->updateDefaultAdapter();
|
||||
} else if (interfaces.contains("org.bluez.Device1")) {
|
||||
auto* device = new BluetoothDevice(path.path(), this);
|
||||
|
||||
if (!device->isValid()) {
|
||||
qCWarning(logBluetooth) << "Device path is not valid, cannot track: " << device;
|
||||
delete device;
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logBluetooth) << "Tracked new device" << device;
|
||||
|
||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
||||
device->addInterface(interface, properties);
|
||||
}
|
||||
|
||||
if (auto* adapter = device->adapter()) {
|
||||
adapter->devices()->insertObject(device);
|
||||
qCDebug(logBluetooth) << "Added device" << device << "to adapter" << adapter;
|
||||
}
|
||||
|
||||
this->mDeviceMap.insert(path.path(), device);
|
||||
this->mDevices.insertObject(device);
|
||||
}
|
||||
}
|
||||
|
||||
void Bluez::onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces) {
|
||||
if (auto* adapter = this->mAdapterMap.value(path.path())) {
|
||||
if (interfaces.contains("org.bluez.Adapter1")) {
|
||||
qCDebug(logBluetooth) << "Adapter removed:" << adapter;
|
||||
|
||||
this->mAdapterMap.remove(path.path());
|
||||
this->mAdapters.removeObject(adapter);
|
||||
this->updateDefaultAdapter();
|
||||
delete adapter;
|
||||
}
|
||||
} else if (auto* device = this->mDeviceMap.value(path.path())) {
|
||||
if (interfaces.contains("org.bluez.Device1")) {
|
||||
qCDebug(logBluetooth) << "Device removed:" << device;
|
||||
|
||||
if (auto* adapter = device->adapter()) {
|
||||
adapter->devices()->removeObject(device);
|
||||
}
|
||||
|
||||
this->mDeviceMap.remove(path.path());
|
||||
this->mDevices.removeObject(device);
|
||||
delete device;
|
||||
} else {
|
||||
for (const auto& interface: interfaces) {
|
||||
device->removeInterface(interface);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BluezQml::BluezQml() {
|
||||
QObject::connect(
|
||||
Bluez::instance(),
|
||||
&Bluez::defaultAdapterChanged,
|
||||
this,
|
||||
&BluezQml::defaultAdapterChanged
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qhash.h>
|
||||
#include <qobject.h>
|
||||
#include <qproperty.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../core/doc.hpp"
|
||||
#include "../core/model.hpp"
|
||||
#include "../dbus/dbus_objectmanager_types.hpp"
|
||||
#include "../dbus/objectmanager.hpp"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
class BluetoothAdapter;
|
||||
class BluetoothDevice;
|
||||
|
||||
class Bluez: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
[[nodiscard]] ObjectModel<BluetoothAdapter>* adapters() { return &this->mAdapters; }
|
||||
[[nodiscard]] ObjectModel<BluetoothDevice>* devices() { return &this->mDevices; }
|
||||
|
||||
[[nodiscard]] BluetoothAdapter* adapter(const QString& path) {
|
||||
return this->mAdapterMap.value(path);
|
||||
}
|
||||
|
||||
static Bluez* instance();
|
||||
|
||||
signals:
|
||||
void defaultAdapterChanged();
|
||||
|
||||
private slots:
|
||||
void
|
||||
onInterfacesAdded(const QDBusObjectPath& path, const DBusObjectManagerInterfaces& interfaces);
|
||||
void onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces);
|
||||
void updateDefaultAdapter();
|
||||
|
||||
private:
|
||||
explicit Bluez();
|
||||
void init();
|
||||
|
||||
qs::dbus::DBusObjectManager* objectManager = nullptr;
|
||||
QHash<QString, BluetoothAdapter*> mAdapterMap;
|
||||
QHash<QString, BluetoothDevice*> mDeviceMap;
|
||||
ObjectModel<BluetoothAdapter> mAdapters {this};
|
||||
ObjectModel<BluetoothDevice> mDevices {this};
|
||||
|
||||
public:
|
||||
Q_OBJECT_BINDABLE_PROPERTY(
|
||||
Bluez,
|
||||
BluetoothAdapter*,
|
||||
bDefaultAdapter,
|
||||
&Bluez::defaultAdapterChanged
|
||||
);
|
||||
};
|
||||
|
||||
///! Bluetooth manager
|
||||
/// Provides access to bluetooth devices and adapters.
|
||||
class BluezQml: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_NAMED_ELEMENT(Bluetooth);
|
||||
QML_SINGLETON;
|
||||
// clang-format off
|
||||
/// The default bluetooth adapter. Usually there is only one.
|
||||
Q_PROPERTY(BluetoothAdapter* defaultAdapter READ default NOTIFY defaultAdapterChanged BINDABLE bindableDefaultAdapter);
|
||||
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothAdapter>*);
|
||||
/// A list of all bluetooth adapters. See @@defaultAdapter for the default.
|
||||
Q_PROPERTY(UntypedObjectModel* adapters READ adapters CONSTANT);
|
||||
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothDevice>*);
|
||||
/// A list of all connected bluetooth devices across all adapters.
|
||||
/// See @@BluetoothAdapter.devices for the devices connected to a single adapter.
|
||||
Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
|
||||
// clang-format on
|
||||
|
||||
signals:
|
||||
void defaultAdapterChanged();
|
||||
|
||||
public:
|
||||
explicit BluezQml();
|
||||
|
||||
[[nodiscard]] static ObjectModel<BluetoothAdapter>* adapters() {
|
||||
return Bluez::instance()->adapters();
|
||||
}
|
||||
|
||||
[[nodiscard]] static ObjectModel<BluetoothDevice>* devices() {
|
||||
return Bluez::instance()->devices();
|
||||
}
|
||||
|
||||
[[nodiscard]] static QBindable<BluetoothAdapter*> bindableDefaultAdapter() {
|
||||
return &Bluez::instance()->bDefaultAdapter;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
@@ -0,0 +1,319 @@
|
||||
#include "device.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdbusconnection.h>
|
||||
#include <qdbuspendingcall.h>
|
||||
#include <qdbuspendingreply.h>
|
||||
#include <qdebug.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qstring.h>
|
||||
#include <qstringliteral.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "../dbus/properties.hpp"
|
||||
#include "adapter.hpp"
|
||||
#include "bluez.hpp"
|
||||
#include "dbus_device.h"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logDevice, "quickshell.bluetooth.device", QtWarningMsg);
|
||||
}
|
||||
|
||||
QString BluetoothDeviceState::toString(BluetoothDeviceState::Enum state) {
|
||||
switch (state) {
|
||||
case BluetoothDeviceState::Disconnected: return QStringLiteral("Disconnected");
|
||||
case BluetoothDeviceState::Connected: return QStringLiteral("Connected");
|
||||
case BluetoothDeviceState::Disconnecting: return QStringLiteral("Disconnecting");
|
||||
case BluetoothDeviceState::Connecting: return QStringLiteral("Connecting");
|
||||
default: return QStringLiteral("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
BluetoothDevice::BluetoothDevice(const QString& path, QObject* parent): QObject(parent) {
|
||||
this->mInterface =
|
||||
new DBusBluezDeviceInterface("org.bluez", path, QDBusConnection::systemBus(), this);
|
||||
|
||||
if (!this->mInterface->isValid()) {
|
||||
qCWarning(logDevice) << "Could not create DBus interface for device at" << path;
|
||||
delete this->mInterface;
|
||||
this->mInterface = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
this->properties.setInterface(this->mInterface);
|
||||
}
|
||||
|
||||
BluetoothAdapter* BluetoothDevice::adapter() const {
|
||||
return Bluez::instance()->adapter(this->bAdapterPath.value().path());
|
||||
}
|
||||
|
||||
void BluetoothDevice::setConnected(bool connected) {
|
||||
if (connected == this->bConnected) return;
|
||||
|
||||
if (connected) {
|
||||
this->connect();
|
||||
} else {
|
||||
this->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothDevice::setTrusted(bool trusted) {
|
||||
if (trusted == this->bTrusted) return;
|
||||
this->bTrusted = trusted;
|
||||
this->pTrusted.write();
|
||||
}
|
||||
|
||||
void BluetoothDevice::setBlocked(bool blocked) {
|
||||
if (blocked == this->bBlocked) return;
|
||||
this->bBlocked = blocked;
|
||||
this->pBlocked.write();
|
||||
}
|
||||
|
||||
void BluetoothDevice::setName(const QString& name) {
|
||||
if (name == this->bName) return;
|
||||
this->bName = name;
|
||||
this->pName.write();
|
||||
}
|
||||
|
||||
void BluetoothDevice::setWakeAllowed(bool wakeAllowed) {
|
||||
if (wakeAllowed == this->bWakeAllowed) return;
|
||||
this->bWakeAllowed = wakeAllowed;
|
||||
this->pWakeAllowed.write();
|
||||
}
|
||||
|
||||
void BluetoothDevice::connect() {
|
||||
if (this->bConnected) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already connected";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->bState == BluetoothDeviceState::Connecting) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already connecting";
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logDevice) << "Connecting to device" << this;
|
||||
this->bState = BluetoothDeviceState::Connecting;
|
||||
|
||||
auto reply = this->mInterface->Connect();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDevice).nospace()
|
||||
<< "Failed to connect to device " << this << ": " << reply.error().message();
|
||||
|
||||
this->bState = this->bConnected ? BluetoothDeviceState::Connected
|
||||
: BluetoothDeviceState::Disconnected;
|
||||
} else {
|
||||
qCDebug(logDevice) << "Successfully connected to to device" << this;
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothDevice::disconnect() {
|
||||
if (!this->bConnected) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already disconnected";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->bState == BluetoothDeviceState::Disconnecting) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already disconnecting";
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logDevice) << "Disconnecting from device" << this;
|
||||
this->bState = BluetoothDeviceState::Disconnecting;
|
||||
|
||||
auto reply = this->mInterface->Disconnect();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDevice).nospace()
|
||||
<< "Failed to disconnect from device " << this << ": " << reply.error().message();
|
||||
|
||||
this->bState = this->bConnected ? BluetoothDeviceState::Connected
|
||||
: BluetoothDeviceState::Disconnected;
|
||||
} else {
|
||||
qCDebug(logDevice) << "Successfully disconnected from from device" << this;
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothDevice::pair() {
|
||||
if (this->bPaired) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already paired";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->bPairing) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already pairing";
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logDevice) << "Pairing with device" << this;
|
||||
this->bPairing = true;
|
||||
|
||||
auto reply = this->mInterface->Pair();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDevice).nospace()
|
||||
<< "Failed to pair with device " << this << ": " << reply.error().message();
|
||||
} else {
|
||||
qCDebug(logDevice) << "Successfully initiated pairing with device" << this;
|
||||
}
|
||||
|
||||
this->bPairing = false;
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothDevice::cancelPair() {
|
||||
if (!this->bPairing) {
|
||||
qCCritical(logDevice) << "Device" << this << "is not currently pairing";
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logDevice) << "Cancelling pairing with device" << this;
|
||||
|
||||
auto reply = this->mInterface->CancelPairing();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDevice) << "Failed to cancel pairing with device" << this << ":"
|
||||
<< reply.error().message();
|
||||
} else {
|
||||
qCDebug(logDevice) << "Successfully cancelled pairing with device" << this;
|
||||
}
|
||||
|
||||
this->bPairing = false;
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothDevice::forget() {
|
||||
if (!this->mInterface || !this->mInterface->isValid()) {
|
||||
qCCritical(logDevice) << "Cannot forget - device interface is invalid";
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto* adapter = Bluez::instance()->adapter(this->bAdapterPath.value().path())) {
|
||||
qCDebug(logDevice) << "Forgetting device" << this << "via adapter" << adapter;
|
||||
adapter->removeDevice(this->path());
|
||||
} else {
|
||||
qCCritical(logDevice) << "Could not find adapter for path" << this->bAdapterPath.value().path()
|
||||
<< "to forget from";
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothDevice::addInterface(const QString& interface, const QVariantMap& properties) {
|
||||
if (interface == "org.bluez.Device1") {
|
||||
this->properties.updatePropertySet(properties, false);
|
||||
qCDebug(logDevice) << "Updated Device properties for" << this;
|
||||
} else if (interface == "org.bluez.Battery1") {
|
||||
if (!this->mBatteryInterface) {
|
||||
this->mBatteryInterface = new QDBusInterface(
|
||||
"org.bluez",
|
||||
this->path(),
|
||||
"org.bluez.Battery1",
|
||||
QDBusConnection::systemBus(),
|
||||
this
|
||||
);
|
||||
|
||||
if (!this->mBatteryInterface->isValid()) {
|
||||
qCWarning(logDevice) << "Could not create Battery interface for device at" << this;
|
||||
delete this->mBatteryInterface;
|
||||
this->mBatteryInterface = nullptr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->batteryProperties.setInterface(this->mBatteryInterface);
|
||||
this->batteryProperties.updatePropertySet(properties, false);
|
||||
|
||||
emit this->batteryAvailableChanged();
|
||||
qCDebug(logDevice) << "Updated Battery properties for" << this;
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothDevice::removeInterface(const QString& interface) {
|
||||
if (interface == "org.bluez.Battery1" && this->mBatteryInterface) {
|
||||
this->batteryProperties.setInterface(nullptr);
|
||||
delete this->mBatteryInterface;
|
||||
this->mBatteryInterface = nullptr;
|
||||
this->bBattery = 0;
|
||||
|
||||
emit this->batteryAvailableChanged();
|
||||
qCDebug(logDevice) << "Battery interface removed from device" << this;
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothDevice::onConnectedChanged() {
|
||||
this->bState =
|
||||
this->bConnected ? BluetoothDeviceState::Connected : BluetoothDeviceState::Disconnected;
|
||||
emit this->connectedChanged();
|
||||
}
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
using namespace qs::bluetooth;
|
||||
|
||||
DBusResult<qreal> DBusDataTransform<BatteryPercentage>::fromWire(quint8 percentage) {
|
||||
return DBusResult(percentage * 0.01);
|
||||
}
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device) {
|
||||
auto saver = QDebugStateSaver(debug);
|
||||
|
||||
if (device) {
|
||||
debug.nospace() << "BluetoothDevice(" << static_cast<const void*>(device)
|
||||
<< ", path=" << device->path() << ")";
|
||||
} else {
|
||||
debug << "BluetoothDevice(nullptr)";
|
||||
}
|
||||
|
||||
return debug;
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../dbus/properties.hpp"
|
||||
#include "dbus_device.h"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
///! Connection state of a Bluetooth device.
|
||||
class BluetoothDeviceState: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
||||
public:
|
||||
enum Enum : quint8 {
|
||||
/// The device is not connected.
|
||||
Disconnected = 0,
|
||||
/// The device is connected.
|
||||
Connected = 1,
|
||||
/// The device is disconnecting.
|
||||
Disconnecting = 2,
|
||||
/// The device is connecting.
|
||||
Connecting = 3,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
Q_INVOKABLE static QString toString(BluetoothDeviceState::Enum state);
|
||||
};
|
||||
|
||||
struct BatteryPercentage {};
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
template <>
|
||||
struct DBusDataTransform<qs::bluetooth::BatteryPercentage> {
|
||||
using Wire = quint8;
|
||||
using Data = qreal;
|
||||
static DBusResult<Data> fromWire(Wire percentage);
|
||||
};
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
class BluetoothAdapter;
|
||||
|
||||
///! A tracked Bluetooth device.
|
||||
class BluetoothDevice: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("");
|
||||
// clang-format off
|
||||
/// MAC address of the device.
|
||||
Q_PROPERTY(QString address READ default NOTIFY addressChanged BINDABLE bindableAddress);
|
||||
/// The name of the Bluetooth device. This property may be written to create an alias, or set to
|
||||
/// an empty string to fall back to the device provided name.
|
||||
///
|
||||
/// See @@deviceName for the name provided by the device.
|
||||
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged);
|
||||
/// The name of the Bluetooth device, ignoring user provided aliases. See also @@name
|
||||
/// which returns a user provided alias if set.
|
||||
Q_PROPERTY(QString deviceName READ default NOTIFY deviceNameChanged BINDABLE bindableDeviceName);
|
||||
/// System icon representing the device type. Use @@Quickshell.Quickshell.iconPath() to display this in an image.
|
||||
Q_PROPERTY(QString icon READ default NOTIFY iconChanged BINDABLE bindableIcon);
|
||||
/// Connection state of the device.
|
||||
Q_PROPERTY(BluetoothDeviceState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
|
||||
/// True if the device is currently connected to the computer.
|
||||
///
|
||||
/// Setting this property is equivalent to calling @@connect() and @@disconnect().
|
||||
///
|
||||
/// > [!NOTE] @@state provides more detailed information if required.
|
||||
Q_PROPERTY(bool connected READ connected WRITE setConnected NOTIFY connectedChanged);
|
||||
/// True if the device is paired to the computer.
|
||||
///
|
||||
/// > [!NOTE] @@pair() can be used to pair a device, however you must @@forget() the device to unpair it.
|
||||
Q_PROPERTY(bool paired READ default NOTIFY pairedChanged BINDABLE bindablePaired);
|
||||
/// True if pairing information is stored for future connections.
|
||||
Q_PROPERTY(bool bonded READ default NOTIFY bondedChanged BINDABLE bindableBonded);
|
||||
/// True if the device is currently being paired.
|
||||
///
|
||||
/// > [!NOTE] @@cancelPair() can be used to cancel the pairing process.
|
||||
Q_PROPERTY(bool pairing READ pairing NOTIFY pairingChanged);
|
||||
/// True if the device is considered to be trusted by the system.
|
||||
/// Trusted devices are allowed to reconnect themselves to the system without intervention.
|
||||
Q_PROPERTY(bool trusted READ trusted WRITE setTrusted NOTIFY trustedChanged);
|
||||
/// True if the device is blocked from connecting.
|
||||
/// If a device is blocked, any connection attempts will be immediately rejected by the system.
|
||||
Q_PROPERTY(bool blocked READ blocked WRITE setBlocked NOTIFY blockedChanged);
|
||||
/// True if the device is allowed to wake up the host system from suspend.
|
||||
Q_PROPERTY(bool wakeAllowed READ wakeAllowed WRITE setWakeAllowed NOTIFY wakeAllowedChanged);
|
||||
/// True if the connected device reports its battery level. Battery level can be accessed via @@battery.
|
||||
Q_PROPERTY(bool batteryAvailable READ batteryAvailable NOTIFY batteryAvailableChanged);
|
||||
/// Battery level of the connected device, from `0.0` to `1.0`. Only valid if @@batteryAvailable is true.
|
||||
Q_PROPERTY(qreal battery READ default NOTIFY batteryChanged BINDABLE bindableBattery);
|
||||
/// The Bluetooth adapter this device belongs to.
|
||||
Q_PROPERTY(BluetoothAdapter* adapter READ adapter NOTIFY adapterChanged);
|
||||
/// DBus path of the device under the `org.bluez` system service.
|
||||
Q_PROPERTY(QString dbusPath READ path CONSTANT);
|
||||
// clang-format on
|
||||
|
||||
public:
|
||||
explicit BluetoothDevice(const QString& path, QObject* parent = nullptr);
|
||||
|
||||
/// Attempt to connect to the device.
|
||||
Q_INVOKABLE void connect();
|
||||
/// Disconnect from the device.
|
||||
Q_INVOKABLE void disconnect();
|
||||
/// Attempt to pair the device.
|
||||
///
|
||||
/// > [!NOTE] @@paired and @@pairing return the current pairing status of the device.
|
||||
Q_INVOKABLE void pair();
|
||||
/// Cancel an active pairing attempt.
|
||||
Q_INVOKABLE void cancelPair();
|
||||
/// Forget the device.
|
||||
Q_INVOKABLE void forget();
|
||||
|
||||
[[nodiscard]] bool isValid() const { return this->mInterface && this->mInterface->isValid(); }
|
||||
[[nodiscard]] QString path() const {
|
||||
return this->mInterface ? this->mInterface->path() : QString();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool batteryAvailable() const { return this->mBatteryInterface != nullptr; }
|
||||
[[nodiscard]] BluetoothAdapter* adapter() const;
|
||||
[[nodiscard]] QDBusObjectPath adapterPath() const { return this->bAdapterPath.value(); }
|
||||
|
||||
[[nodiscard]] bool connected() const { return this->bConnected; }
|
||||
void setConnected(bool connected);
|
||||
|
||||
[[nodiscard]] bool trusted() const { return this->bTrusted; }
|
||||
void setTrusted(bool trusted);
|
||||
|
||||
[[nodiscard]] bool blocked() const { return this->bBlocked; }
|
||||
void setBlocked(bool blocked);
|
||||
|
||||
[[nodiscard]] QString name() const { return this->bName; }
|
||||
void setName(const QString& name);
|
||||
|
||||
[[nodiscard]] bool wakeAllowed() const { return this->bWakeAllowed; }
|
||||
void setWakeAllowed(bool wakeAllowed);
|
||||
|
||||
[[nodiscard]] bool pairing() const { return this->bPairing; }
|
||||
|
||||
[[nodiscard]] QBindable<QString> bindableAddress() { return &this->bAddress; }
|
||||
[[nodiscard]] QBindable<QString> bindableDeviceName() { return &this->bDeviceName; }
|
||||
[[nodiscard]] QBindable<QString> bindableName() { return &this->bName; }
|
||||
[[nodiscard]] QBindable<bool> bindableConnected() { return &this->bConnected; }
|
||||
[[nodiscard]] QBindable<bool> bindablePaired() { return &this->bPaired; }
|
||||
[[nodiscard]] QBindable<bool> bindableBonded() { return &this->bBonded; }
|
||||
[[nodiscard]] QBindable<bool> bindableTrusted() { return &this->bTrusted; }
|
||||
[[nodiscard]] QBindable<bool> bindableBlocked() { return &this->bBlocked; }
|
||||
[[nodiscard]] QBindable<bool> bindableWakeAllowed() { return &this->bWakeAllowed; }
|
||||
[[nodiscard]] QBindable<QString> bindableIcon() { return &this->bIcon; }
|
||||
[[nodiscard]] QBindable<qreal> bindableBattery() { return &this->bBattery; }
|
||||
[[nodiscard]] QBindable<BluetoothDeviceState::Enum> bindableState() { return &this->bState; }
|
||||
|
||||
void addInterface(const QString& interface, const QVariantMap& properties);
|
||||
void removeInterface(const QString& interface);
|
||||
|
||||
signals:
|
||||
void addressChanged();
|
||||
void deviceNameChanged();
|
||||
void nameChanged();
|
||||
void connectedChanged();
|
||||
void stateChanged();
|
||||
void pairedChanged();
|
||||
void bondedChanged();
|
||||
void pairingChanged();
|
||||
void trustedChanged();
|
||||
void blockedChanged();
|
||||
void wakeAllowedChanged();
|
||||
void iconChanged();
|
||||
void batteryAvailableChanged();
|
||||
void batteryChanged();
|
||||
void adapterChanged();
|
||||
|
||||
private:
|
||||
void onConnectedChanged();
|
||||
|
||||
DBusBluezDeviceInterface* mInterface = nullptr;
|
||||
QDBusInterface* mBatteryInterface = nullptr;
|
||||
|
||||
// clang-format off
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bAddress, &BluetoothDevice::addressChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bDeviceName, &BluetoothDevice::deviceNameChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bName, &BluetoothDevice::nameChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bConnected, &BluetoothDevice::onConnectedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPaired, &BluetoothDevice::pairedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBonded, &BluetoothDevice::bondedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bTrusted, &BluetoothDevice::trustedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBlocked, &BluetoothDevice::blockedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bWakeAllowed, &BluetoothDevice::wakeAllowedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bIcon, &BluetoothDevice::iconChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QDBusObjectPath, bAdapterPath);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, qreal, bBattery, &BluetoothDevice::batteryChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, BluetoothDeviceState::Enum, bState, &BluetoothDevice::stateChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPairing, &BluetoothDevice::pairingChanged);
|
||||
|
||||
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, properties);
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAddress, bAddress, properties, "Address");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pDeviceName, bDeviceName, properties, "Name");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pName, bName, properties, "Alias");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pConnected, bConnected, properties, "Connected");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pPaired, bPaired, properties, "Paired");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBonded, bBonded, properties, "Bonded");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pTrusted, bTrusted, properties, "Trusted");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBlocked, bBlocked, properties, "Blocked");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pWakeAllowed, bWakeAllowed, properties, "WakeAllowed");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pIcon, bIcon, properties, "Icon");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAdapterPath, bAdapterPath, properties, "Adapter");
|
||||
|
||||
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, batteryProperties);
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, BatteryPercentage, pBattery, bBattery, batteryProperties, "Percentage", true);
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device);
|
||||
@@ -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.
|
||||
@@ -0,0 +1,9 @@
|
||||
<node>
|
||||
<interface name="org.bluez.Adapter1">
|
||||
<method name="StartDiscovery"/>
|
||||
<method name="StopDiscovery"/>
|
||||
<method name="RemoveDevice">
|
||||
<arg name="device" type="o"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>
|
||||
@@ -0,0 +1,8 @@
|
||||
<node>
|
||||
<interface name="org.bluez.Device1">
|
||||
<method name="Connect"/>
|
||||
<method name="Disconnect"/>
|
||||
<method name="Pair"/>
|
||||
<method name="CancelPairing"/>
|
||||
</interface>
|
||||
</node>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ qt_add_library(quickshell-core STATIC
|
||||
model.cpp
|
||||
elapsedtimer.cpp
|
||||
desktopentry.cpp
|
||||
desktopentrymonitor.cpp
|
||||
objectrepeater.cpp
|
||||
platformmenu.cpp
|
||||
qsmenu.cpp
|
||||
@@ -38,6 +39,7 @@ qt_add_library(quickshell-core STATIC
|
||||
iconprovider.cpp
|
||||
scriptmodel.cpp
|
||||
colorquantizer.cpp
|
||||
toolsupport.cpp
|
||||
)
|
||||
|
||||
qt_add_qml_module(quickshell-core
|
||||
|
||||
+32
-27
@@ -18,34 +18,38 @@
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#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)
|
||||
: source(source)
|
||||
, maxDepth(depth)
|
||||
, rescaleSize(rescaleSize) {
|
||||
setAutoDelete(false);
|
||||
this->setAutoDelete(false);
|
||||
}
|
||||
|
||||
void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& 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 ((image.width() > this->rescaleSize || image.height() > this->rescaleSize)
|
||||
&& this->rescaleSize > 0)
|
||||
{
|
||||
image = image.scaled(
|
||||
static_cast<int>(rescaleSize),
|
||||
static_cast<int>(rescaleSize),
|
||||
static_cast<int>(this->rescaleSize),
|
||||
static_cast<int>(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 +65,7 @@ void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& 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 +79,7 @@ QList<QColor> ColorQuantizerOperation::quantization(
|
||||
) {
|
||||
if (shouldCancel.loadAcquire()) return QList<QColor>();
|
||||
|
||||
if (depth >= maxDepth || rgbValues.isEmpty()) {
|
||||
if (depth >= this->maxDepth || rgbValues.isEmpty()) {
|
||||
if (rgbValues.isEmpty()) return QList<QColor>();
|
||||
|
||||
auto totalR = 0;
|
||||
@@ -112,8 +116,8 @@ QList<QColor> ColorQuantizerOperation::quantization(
|
||||
auto rightHalf = rgbValues.mid(mid);
|
||||
|
||||
QList<QColor> 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 +161,7 @@ void ColorQuantizerOperation::finishRun() {
|
||||
}
|
||||
|
||||
void ColorQuantizerOperation::finished() {
|
||||
emit this->done(colors);
|
||||
emit this->done(this->colors);
|
||||
delete this;
|
||||
}
|
||||
|
||||
@@ -176,39 +180,39 @@ 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->quantizeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
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->quantizeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
void ColorQuantizer::operationFinished(const QList<QColor>& result) {
|
||||
bColors = result;
|
||||
this->bColors = result;
|
||||
this->liveOperation = nullptr;
|
||||
emit this->colorsChanged();
|
||||
}
|
||||
@@ -217,7 +221,8 @@ 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->mRescaleSize);
|
||||
|
||||
QObject::connect(
|
||||
this->liveOperation,
|
||||
|
||||
@@ -91,13 +91,13 @@ public:
|
||||
|
||||
[[nodiscard]] QBindable<QList<QColor>> 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]] qreal rescaleSize() const { return this->mRescaleSize; }
|
||||
void setRescaleSize(int rescaleSize);
|
||||
|
||||
signals:
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#include "common.hpp"
|
||||
|
||||
#include <qdatetime.h>
|
||||
#include <qprocess.h>
|
||||
|
||||
namespace qs {
|
||||
|
||||
const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime();
|
||||
QProcessEnvironment Common::INITIAL_ENVIRONMENT = {}; // NOLINT
|
||||
|
||||
} // namespace qs
|
||||
+1
-1
@@ -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
|
||||
+319
-127
@@ -1,26 +1,33 @@
|
||||
#include "desktopentry.hpp"
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdebug.h>
|
||||
#include <qdir.h>
|
||||
#include <qfile.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qhash.h>
|
||||
#include <qlist.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qnamespace.h>
|
||||
#include <qobject.h>
|
||||
#include <qobjectdefs.h>
|
||||
#include <qpair.h>
|
||||
#include <qprocess.h>
|
||||
#include <qstringview.h>
|
||||
#include <qproperty.h>
|
||||
#include <qscopeguard.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
#include <qthreadpool.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <ranges>
|
||||
|
||||
#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,60 @@ 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<QString, QPair<Locale, QString>>();
|
||||
|
||||
auto finishCategory = [this, &groupName, &entries]() {
|
||||
auto finishCategory = [&data, &groupName, &entries]() {
|
||||
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 (groupName.startsWith("Desktop Action ")) {
|
||||
auto actionName = groupName.sliced(16);
|
||||
auto* action = new DesktopAction(actionName, this);
|
||||
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);
|
||||
data.actions.insert(actionName, action);
|
||||
}
|
||||
|
||||
entries.clear();
|
||||
@@ -175,14 +193,62 @@ void DesktopEntry::parseEntry(const QString& text) {
|
||||
}
|
||||
|
||||
finishCategory();
|
||||
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 QHash<QString, DesktopActionData>& newActions) {
|
||||
auto old = this->mActions;
|
||||
|
||||
for (const auto& [key, d]: newActions.asKeyValueRange()) {
|
||||
DesktopAction* act = nullptr;
|
||||
if (auto found = old.find(key); found != old.end()) {
|
||||
act = found.value();
|
||||
old.erase(found);
|
||||
} else {
|
||||
act = new DesktopAction(d.id, this);
|
||||
this->mActions.insert(key, act);
|
||||
}
|
||||
|
||||
Qt::beginPropertyUpdateGroup();
|
||||
act->bName = d.name;
|
||||
act->bIcon = d.icon;
|
||||
act->bExecString = d.execString;
|
||||
act->bCommand = d.command;
|
||||
Qt::endPropertyUpdateGroup();
|
||||
|
||||
act->mEntries = d.entries;
|
||||
}
|
||||
|
||||
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<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); }
|
||||
|
||||
@@ -250,75 +316,52 @@ QVector<QString> 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<QString>& 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<QString> dataPaths;
|
||||
void DesktopEntryScanner::run() {
|
||||
const auto& desktopPaths = DesktopEntryManager::desktopPaths();
|
||||
auto scanResults = QList<ParsedDesktopEntryData>();
|
||||
|
||||
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<ParsedDesktopEntryData>, 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<ParsedDesktopEntryData>& 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 +374,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 +425,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<DesktopEntry>* 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<ParsedDesktopEntryData>& scanResults) {
|
||||
auto guard = qScopeGuard([this] {
|
||||
this->scanInProgress = false;
|
||||
if (this->scanQueued) {
|
||||
this->scanQueued = false;
|
||||
this->scanDesktopEntries();
|
||||
}
|
||||
});
|
||||
|
||||
auto oldEntries = this->desktopEntries;
|
||||
auto newEntries = QHash<QString, DesktopEntry*>();
|
||||
auto newLowercaseEntries = QHash<QString, DesktopEntry*>();
|
||||
|
||||
for (const auto& data: scanResults) {
|
||||
auto lowerId = data.id.toLower();
|
||||
|
||||
if (data.hidden) {
|
||||
if (auto* victim = newEntries.take(data.id)) victim->deleteLater();
|
||||
newLowercaseEntries.remove(lowerId);
|
||||
|
||||
if (auto it = oldEntries.find(data.id); it != oldEntries.end()) {
|
||||
it.value()->deleteLater();
|
||||
oldEntries.erase(it);
|
||||
}
|
||||
|
||||
qCDebug(logDesktopEntry) << "Masking hidden desktop entry" << data.id;
|
||||
continue;
|
||||
}
|
||||
|
||||
DesktopEntry* dentry = nullptr;
|
||||
|
||||
if (auto it = oldEntries.find(data.id); it != oldEntries.end()) {
|
||||
dentry = it.value();
|
||||
oldEntries.erase(it);
|
||||
dentry->updateState(data);
|
||||
} else {
|
||||
dentry = new DesktopEntry(data.id, this);
|
||||
dentry->updateState(data);
|
||||
}
|
||||
|
||||
if (!dentry->isValid()) {
|
||||
qCDebug(logDesktopEntry) << "Skipping desktop entry" << data.id;
|
||||
if (!oldEntries.contains(data.id)) {
|
||||
dentry->deleteLater();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
qCDebug(logDesktopEntry) << "Found desktop entry" << data.id;
|
||||
|
||||
auto conflictingId = newEntries.contains(data.id);
|
||||
|
||||
if (conflictingId) {
|
||||
qCDebug(logDesktopEntry) << "Replacing old entry for" << data.id;
|
||||
if (auto* victim = newEntries.take(data.id)) victim->deleteLater();
|
||||
newLowercaseEntries.remove(lowerId);
|
||||
}
|
||||
|
||||
newEntries.insert(data.id, dentry);
|
||||
|
||||
if (newLowercaseEntries.contains(lowerId)) {
|
||||
qCInfo(logDesktopEntry).nospace()
|
||||
<< "Multiple desktop entries have the same lowercased id " << lowerId
|
||||
<< ". This can cause ambiguity when byId requests are not made with the correct case "
|
||||
"already.";
|
||||
|
||||
newLowercaseEntries.remove(lowerId);
|
||||
}
|
||||
|
||||
newLowercaseEntries.insert(lowerId, dentry);
|
||||
}
|
||||
|
||||
this->desktopEntries = newEntries;
|
||||
this->lowercaseDesktopEntries = newLowercaseEntries;
|
||||
|
||||
auto newApplications = QVector<DesktopEntry*>();
|
||||
for (auto* entry: this->desktopEntries.values())
|
||||
if (!entry->bNoDisplay) newApplications.append(entry);
|
||||
|
||||
this->mApplications.diffUpdate(newApplications);
|
||||
|
||||
emit this->applicationsChanged();
|
||||
|
||||
for (auto* e: oldEntries) e->deleteLater();
|
||||
}
|
||||
|
||||
DesktopEntries::DesktopEntries() {
|
||||
QObject::connect(
|
||||
DesktopEntryManager::instance(),
|
||||
&DesktopEntryManager::applicationsChanged,
|
||||
this,
|
||||
&DesktopEntries::applicationsChanged
|
||||
);
|
||||
}
|
||||
|
||||
DesktopEntry* DesktopEntries::byId(const QString& id) {
|
||||
return DesktopEntryManager::instance()->byId(id);
|
||||
}
|
||||
|
||||
DesktopEntry* DesktopEntries::heuristicLookup(const QString& name) {
|
||||
return DesktopEntryManager::instance()->heuristicLookup(name);
|
||||
}
|
||||
|
||||
ObjectModel<DesktopEntry>* DesktopEntries::applications() {
|
||||
return DesktopEntryManager::instance()->applications();
|
||||
}
|
||||
+199
-35
@@ -6,36 +6,84 @@
|
||||
#include <qdir.h>
|
||||
#include <qhash.h>
|
||||
#include <qobject.h>
|
||||
#include <qproperty.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qrunnable.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "desktopentrymonitor.hpp"
|
||||
#include "doc.hpp"
|
||||
#include "model.hpp"
|
||||
|
||||
class DesktopAction;
|
||||
class DesktopEntryMonitor;
|
||||
|
||||
struct DesktopActionData {
|
||||
QString id;
|
||||
QString name;
|
||||
QString icon;
|
||||
QString execString;
|
||||
QVector<QString> command;
|
||||
QHash<QString, QString> entries;
|
||||
};
|
||||
|
||||
struct ParsedDesktopEntryData {
|
||||
QString id;
|
||||
QString name;
|
||||
QString genericName;
|
||||
QString startupClass;
|
||||
bool noDisplay = false;
|
||||
bool hidden = false;
|
||||
QString comment;
|
||||
QString icon;
|
||||
QString execString;
|
||||
QVector<QString> command;
|
||||
QString workingDirectory;
|
||||
bool terminal = false;
|
||||
QVector<QString> categories;
|
||||
QVector<QString> keywords;
|
||||
QHash<QString, QString> entries;
|
||||
QHash<QString, DesktopActionData> 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<QString> 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<QString> categories MEMBER mCategories CONSTANT);
|
||||
Q_PROPERTY(QVector<QString> keywords MEMBER mKeywords CONSTANT);
|
||||
Q_PROPERTY(bool runInTerminal READ default WRITE default NOTIFY runInTerminalChanged BINDABLE bindableRunInTerminal);
|
||||
Q_PROPERTY(QVector<QString> categories READ default WRITE default NOTIFY categoriesChanged BINDABLE bindableCategories);
|
||||
Q_PROPERTY(QVector<QString> keywords READ default WRITE default NOTIFY keywordsChanged BINDABLE bindableKeywords);
|
||||
// clang-format on
|
||||
Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT);
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
|
||||
@@ -43,34 +91,82 @@ 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<DesktopAction*> actions() const;
|
||||
|
||||
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
|
||||
[[nodiscard]] QBindable<QString> bindableGenericName() const { return &this->bGenericName; }
|
||||
[[nodiscard]] QBindable<QString> bindableStartupClass() const { return &this->bStartupClass; }
|
||||
[[nodiscard]] QBindable<bool> bindableNoDisplay() const { return &this->bNoDisplay; }
|
||||
[[nodiscard]] QBindable<QString> bindableComment() const { return &this->bComment; }
|
||||
[[nodiscard]] QBindable<QString> bindableIcon() const { return &this->bIcon; }
|
||||
[[nodiscard]] QBindable<QString> bindableExecString() const { return &this->bExecString; }
|
||||
[[nodiscard]] QBindable<QVector<QString>> bindableCommand() const { return &this->bCommand; }
|
||||
[[nodiscard]] QBindable<QString> bindableWorkingDirectory() const {
|
||||
return &this->bWorkingDirectory;
|
||||
}
|
||||
[[nodiscard]] QBindable<bool> bindableRunInTerminal() const { return &this->bRunInTerminal; }
|
||||
[[nodiscard]] QBindable<QVector<QString>> bindableCategories() const {
|
||||
return &this->bCategories;
|
||||
}
|
||||
[[nodiscard]] QBindable<QVector<QString>> bindableKeywords() const { return &this->bKeywords; }
|
||||
|
||||
// currently ignores all field codes.
|
||||
static QVector<QString> parseExecString(const QString& execString);
|
||||
static void doExec(const QString& execString, const QString& workingDirectory);
|
||||
static void doExec(const QList<QString>& execString, const QString& workingDirectory);
|
||||
|
||||
signals:
|
||||
void nameChanged();
|
||||
void genericNameChanged();
|
||||
void startupClassChanged();
|
||||
void noDisplayChanged();
|
||||
void commentChanged();
|
||||
void iconChanged();
|
||||
void execStringChanged();
|
||||
void commandChanged();
|
||||
void workingDirectoryChanged();
|
||||
void runInTerminalChanged();
|
||||
void categoriesChanged();
|
||||
void keywordsChanged();
|
||||
|
||||
public:
|
||||
QString mId;
|
||||
QString mName;
|
||||
QString mGenericName;
|
||||
bool mNoDisplay = false;
|
||||
QString mComment;
|
||||
QString mIcon;
|
||||
QString mExecString;
|
||||
QString mWorkingDirectory;
|
||||
bool mTerminal = false;
|
||||
QVector<QString> mCategories;
|
||||
QVector<QString> 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<QString>, 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<QString>, bCategories, &DesktopEntry::categoriesChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bKeywords, &DesktopEntry::keywordsChanged);
|
||||
// clang-format on
|
||||
|
||||
private:
|
||||
QHash<QString, QString> mEntries;
|
||||
void updateActions(const QHash<QString, DesktopActionData>& newActions);
|
||||
|
||||
ParsedDesktopEntryData state;
|
||||
QHash<QString, DesktopAction*> mActions;
|
||||
|
||||
friend class DesktopAction;
|
||||
@@ -80,10 +176,23 @@ private:
|
||||
class DesktopAction: public QObject {
|
||||
Q_OBJECT;
|
||||
Q_PROPERTY(QString id MEMBER mId CONSTANT);
|
||||
Q_PROPERTY(QString name MEMBER mName CONSTANT);
|
||||
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);
|
||||
// clang-format off
|
||||
Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName);
|
||||
Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon);
|
||||
/// The raw `Exec` string from the action.
|
||||
///
|
||||
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
|
||||
Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString);
|
||||
/// The parsed `Exec` command in the action.
|
||||
///
|
||||
/// The entry can be run with @@execute(), or by using this command in
|
||||
/// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process.
|
||||
/// If used in `execDetached` or a `Process`, @@DesktopEntry.workingDirectory should also be passed to
|
||||
/// the invoked process.
|
||||
///
|
||||
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
|
||||
Q_PROPERTY(QVector<QString> command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand);
|
||||
// clang-format on
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
|
||||
|
||||
@@ -94,19 +203,52 @@ public:
|
||||
, mId(std::move(id)) {}
|
||||
|
||||
/// Run the application. Currently ignores @@DesktopEntry.runInTerminal and field codes.
|
||||
///
|
||||
/// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command
|
||||
/// and @@DesktopEntry.workingDirectory.
|
||||
Q_INVOKABLE void execute() const;
|
||||
|
||||
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
|
||||
[[nodiscard]] QBindable<QString> bindableIcon() const { return &this->bIcon; }
|
||||
[[nodiscard]] QBindable<QString> bindableExecString() const { return &this->bExecString; }
|
||||
[[nodiscard]] QBindable<QVector<QString>> bindableCommand() const { return &this->bCommand; }
|
||||
|
||||
signals:
|
||||
void nameChanged();
|
||||
void iconChanged();
|
||||
void execStringChanged();
|
||||
void commandChanged();
|
||||
|
||||
private:
|
||||
DesktopEntry* entry;
|
||||
QString mId;
|
||||
QString mName;
|
||||
QString mIcon;
|
||||
QString mExecString;
|
||||
QHash<QString, QString> mEntries;
|
||||
|
||||
// clang-format off
|
||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bName, &DesktopAction::nameChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bIcon, &DesktopAction::iconChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bExecString, &DesktopAction::execStringChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QVector<QString>, bCommand, &DesktopAction::commandChanged);
|
||||
// clang-format on
|
||||
|
||||
friend class DesktopEntry;
|
||||
};
|
||||
|
||||
class DesktopEntryManager;
|
||||
|
||||
class DesktopEntryScanner: public QRunnable {
|
||||
public:
|
||||
explicit DesktopEntryScanner(DesktopEntryManager* manager);
|
||||
|
||||
void run() override;
|
||||
// clang-format off
|
||||
void scanDirectory(const QDir& dir, const QString& idPrefix, QList<ParsedDesktopEntryData>& entries);
|
||||
// clang-format on
|
||||
|
||||
private:
|
||||
DesktopEntryManager* manager;
|
||||
};
|
||||
|
||||
class DesktopEntryManager: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
@@ -114,20 +256,32 @@ public:
|
||||
void scanDesktopEntries();
|
||||
|
||||
[[nodiscard]] DesktopEntry* byId(const QString& id);
|
||||
[[nodiscard]] DesktopEntry* heuristicLookup(const QString& name);
|
||||
|
||||
[[nodiscard]] ObjectModel<DesktopEntry>* applications();
|
||||
|
||||
static DesktopEntryManager* instance();
|
||||
|
||||
static const QStringList& desktopPaths();
|
||||
|
||||
signals:
|
||||
void applicationsChanged();
|
||||
|
||||
private slots:
|
||||
void handleFileChanges();
|
||||
void onScanCompleted(const QList<ParsedDesktopEntryData>& scanResults);
|
||||
|
||||
private:
|
||||
explicit DesktopEntryManager();
|
||||
|
||||
void populateApplications();
|
||||
void scanPath(const QDir& dir, const QString& prefix = QString());
|
||||
|
||||
QHash<QString, DesktopEntry*> desktopEntries;
|
||||
QHash<QString, DesktopEntry*> lowercaseDesktopEntries;
|
||||
ObjectModel<DesktopEntry> mApplications {this};
|
||||
DesktopEntryMonitor* monitor = nullptr;
|
||||
bool scanInProgress = false;
|
||||
bool scanQueued = false;
|
||||
|
||||
friend class DesktopEntryScanner;
|
||||
};
|
||||
|
||||
///! Desktop entry index.
|
||||
@@ -149,7 +303,17 @@ public:
|
||||
explicit DesktopEntries();
|
||||
|
||||
/// Look up a desktop entry by name. Includes NoDisplay entries. May return null.
|
||||
///
|
||||
/// While this function requires an exact match, @@heuristicLookup() will correctly
|
||||
/// find an entry more often and is generally more useful.
|
||||
Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id);
|
||||
/// Look up a desktop entry by name using heuristics. Unlike @@byId(),
|
||||
/// if no exact matches are found this function will try to guess - potentially incorrectly.
|
||||
/// May return null.
|
||||
Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name);
|
||||
|
||||
[[nodiscard]] static ObjectModel<DesktopEntry>* applications();
|
||||
|
||||
signals:
|
||||
void applicationsChanged();
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "desktopentrymonitor.hpp"
|
||||
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qobject.h>
|
||||
#include <qstring.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "desktopentry.hpp"
|
||||
|
||||
namespace {
|
||||
void addPathAndParents(QFileSystemWatcher& watcher, const QString& path) {
|
||||
watcher.addPath(path);
|
||||
|
||||
auto p = QFileInfo(path).absolutePath();
|
||||
while (!p.isEmpty()) {
|
||||
watcher.addPath(p);
|
||||
const auto parent = QFileInfo(p).dir().absolutePath();
|
||||
if (parent == p) break;
|
||||
p = parent;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) {
|
||||
this->debounceTimer.setSingleShot(true);
|
||||
this->debounceTimer.setInterval(100);
|
||||
|
||||
QObject::connect(
|
||||
&this->watcher,
|
||||
&QFileSystemWatcher::directoryChanged,
|
||||
this,
|
||||
&DesktopEntryMonitor::onDirectoryChanged
|
||||
);
|
||||
QObject::connect(
|
||||
&this->debounceTimer,
|
||||
&QTimer::timeout,
|
||||
this,
|
||||
&DesktopEntryMonitor::processChanges
|
||||
);
|
||||
|
||||
this->startMonitoring();
|
||||
}
|
||||
|
||||
void DesktopEntryMonitor::startMonitoring() {
|
||||
for (const auto& path: DesktopEntryManager::desktopPaths()) {
|
||||
if (!QDir(path).exists()) continue;
|
||||
addPathAndParents(this->watcher, path);
|
||||
this->scanAndWatch(path);
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) {
|
||||
auto dir = QDir(dirPath);
|
||||
if (!dir.exists()) return;
|
||||
|
||||
this->watcher.addPath(dirPath);
|
||||
|
||||
auto subdirs = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
|
||||
for (const auto& subdir: subdirs) this->watcher.addPath(subdir.absoluteFilePath());
|
||||
}
|
||||
|
||||
void DesktopEntryMonitor::onDirectoryChanged(const QString& /*path*/) {
|
||||
this->debounceTimer.start();
|
||||
}
|
||||
|
||||
void DesktopEntryMonitor::processChanges() { emit this->desktopEntriesChanged(); }
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qobject.h>
|
||||
#include <qstringlist.h>
|
||||
#include <qtimer.h>
|
||||
|
||||
class DesktopEntryMonitor: public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DesktopEntryMonitor(QObject* parent = nullptr);
|
||||
~DesktopEntryMonitor() override = default;
|
||||
DesktopEntryMonitor(const DesktopEntryMonitor&) = delete;
|
||||
DesktopEntryMonitor& operator=(const DesktopEntryMonitor&) = delete;
|
||||
DesktopEntryMonitor(DesktopEntryMonitor&&) = delete;
|
||||
DesktopEntryMonitor& operator=(DesktopEntryMonitor&&) = delete;
|
||||
|
||||
signals:
|
||||
void desktopEntriesChanged();
|
||||
|
||||
private slots:
|
||||
void onDirectoryChanged(const QString& path);
|
||||
void processChanges();
|
||||
|
||||
private:
|
||||
void startMonitoring();
|
||||
void scanAndWatch(const QString& dirPath);
|
||||
|
||||
QFileSystemWatcher watcher;
|
||||
QTimer debounceTimer;
|
||||
};
|
||||
+38
-101
@@ -16,18 +16,20 @@
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmlerror.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qquickwindow.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "iconimageprovider.hpp"
|
||||
#include "imageprovider.hpp"
|
||||
#include "incubator.hpp"
|
||||
#include "logcat.hpp"
|
||||
#include "plugin.hpp"
|
||||
#include "qsintercept.hpp"
|
||||
#include "reload.hpp"
|
||||
#include "scan.hpp"
|
||||
|
||||
namespace {
|
||||
Q_LOGGING_CATEGORY(logScene, "scene");
|
||||
QS_LOGGING_CATEGORY(logScene, "scene");
|
||||
}
|
||||
|
||||
static QHash<const QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
|
||||
@@ -36,7 +38,7 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
|
||||
: rootPath(rootPath)
|
||||
, scanner(std::move(scanner))
|
||||
, urlInterceptor(this->rootPath)
|
||||
, interceptNetFactory(this->scanner.fileIntercepts)
|
||||
, interceptNetFactory(this->rootPath, this->scanner.fileIntercepts)
|
||||
, engine(new QQmlEngine()) {
|
||||
g_generations.insert(this->engine, this);
|
||||
|
||||
@@ -44,6 +46,8 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
|
||||
QObject::connect(this->engine, &QQmlEngine::warnings, this, &EngineGeneration::onEngineWarnings);
|
||||
|
||||
this->engine->addUrlInterceptor(&this->urlInterceptor);
|
||||
this->engine->addImportPath("qs:@/");
|
||||
|
||||
this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory);
|
||||
this->engine->setIncubationController(&this->delayedIncubationController);
|
||||
|
||||
@@ -157,8 +161,9 @@ void EngineGeneration::postReload() {
|
||||
if (this->engine == nullptr || this->root == nullptr) return;
|
||||
|
||||
QsEnginePlugin::runOnReload();
|
||||
PostReloadHook::postReloadTree(this->root);
|
||||
this->singletonRegistry.onPostReload();
|
||||
|
||||
emit this->firePostReload();
|
||||
QObject::disconnect(this, &EngineGeneration::firePostReload, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void EngineGeneration::setWatchingFiles(bool watching) {
|
||||
@@ -218,6 +223,11 @@ void EngineGeneration::onFileChanged(const QString& name) {
|
||||
if (!this->watcher->files().contains(name)) {
|
||||
this->deletedWatchedFiles.push_back(name);
|
||||
} else {
|
||||
// some editors (e.g vscode) perform file saving in two steps: truncate + write
|
||||
// ignore the first event (truncate) with size 0 to prevent incorrect live reloading
|
||||
auto fileInfo = QFileInfo(name);
|
||||
if (fileInfo.isFile() && fileInfo.size() == 0) return;
|
||||
|
||||
emit this->filesChanged();
|
||||
}
|
||||
}
|
||||
@@ -232,98 +242,11 @@ void EngineGeneration::onDirectoryChanged() {
|
||||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) {
|
||||
// We only want controllers that we can swap out if destroyed.
|
||||
// This happens if the window owning the active controller dies.
|
||||
if (auto* obj = dynamic_cast<QObject*>(controller)) {
|
||||
QObject::connect(
|
||||
obj,
|
||||
&QObject::destroyed,
|
||||
this,
|
||||
&EngineGeneration::incubationControllerDestroyed
|
||||
);
|
||||
} else {
|
||||
qCWarning(logIncubator) << "Could not register incubation controller as it is not a QObject"
|
||||
<< controller;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this->incubationControllers.push_back(controller);
|
||||
qCDebug(logIncubator) << "Registered incubation controller" << controller << "to generation"
|
||||
<< this;
|
||||
|
||||
// This function can run during destruction.
|
||||
if (this->engine == nullptr) return;
|
||||
|
||||
if (this->engine->incubationController() == &this->delayedIncubationController) {
|
||||
this->assignIncubationController();
|
||||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) {
|
||||
if (auto* obj = dynamic_cast<QObject*>(controller)) {
|
||||
QObject::disconnect(obj, nullptr, this, nullptr);
|
||||
} else {
|
||||
qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, "
|
||||
"however only QObject controllers should be registered.";
|
||||
}
|
||||
|
||||
if (!this->incubationControllers.removeOne(controller)) {
|
||||
qCCritical(logIncubator) << "Failed to deregister incubation controller" << controller << "from"
|
||||
<< this << "as it was not registered to begin with";
|
||||
qCCritical(logIncubator) << "Current registered incuabation controllers"
|
||||
<< this->incubationControllers;
|
||||
} else {
|
||||
qCDebug(logIncubator) << "Deregistered incubation controller" << controller << "from" << this;
|
||||
}
|
||||
|
||||
// This function can run during destruction.
|
||||
if (this->engine == nullptr) return;
|
||||
|
||||
if (this->engine->incubationController() == controller) {
|
||||
qCDebug(logIncubator
|
||||
) << "Destroyed incubation controller was currently active, reassigning from pool";
|
||||
this->assignIncubationController();
|
||||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::incubationControllerDestroyed() {
|
||||
auto* sender = this->sender();
|
||||
auto* controller = dynamic_cast<QQmlIncubationController*>(sender);
|
||||
|
||||
if (controller == nullptr) {
|
||||
qCCritical(logIncubator) << "Destroyed incubation controller" << sender << "is not known to"
|
||||
<< this << ", this may cause memory corruption";
|
||||
qCCritical(logIncubator) << "Current registered incuabation controllers"
|
||||
<< this->incubationControllers;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->incubationControllers.removeOne(controller)) {
|
||||
qCDebug(logIncubator) << "Destroyed incubation controller" << controller << "deregistered from"
|
||||
<< this;
|
||||
} else {
|
||||
qCCritical(logIncubator) << "Destroyed incubation controller" << controller
|
||||
<< "was not registered, but its destruction was observed by" << this;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// This function can run during destruction.
|
||||
if (this->engine == nullptr) return;
|
||||
|
||||
if (this->engine->incubationController() == controller) {
|
||||
qCDebug(logIncubator
|
||||
) << "Destroyed incubation controller was currently active, reassigning from pool";
|
||||
this->assignIncubationController();
|
||||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) const {
|
||||
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) {
|
||||
for (const auto& error: warnings) {
|
||||
auto rel = "**/" % this->rootPath.relativeFilePath(error.url().path());
|
||||
const auto& url = error.url();
|
||||
auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5)
|
||||
: url.toString();
|
||||
|
||||
QString objectName;
|
||||
auto desc = error.description();
|
||||
@@ -360,13 +283,27 @@ void EngineGeneration::exit(int code) {
|
||||
this->destroy();
|
||||
}
|
||||
|
||||
void EngineGeneration::assignIncubationController() {
|
||||
QQmlIncubationController* controller = nullptr;
|
||||
void EngineGeneration::trackWindowIncubationController(QQuickWindow* window) {
|
||||
if (this->trackedWindows.contains(window)) return;
|
||||
|
||||
if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) {
|
||||
controller = &this->delayedIncubationController;
|
||||
} else {
|
||||
controller = this->incubationControllers.first();
|
||||
QObject::connect(window, &QObject::destroyed, this, &EngineGeneration::onTrackedWindowDestroyed);
|
||||
this->trackedWindows.append(window);
|
||||
this->assignIncubationController();
|
||||
}
|
||||
|
||||
void EngineGeneration::onTrackedWindowDestroyed(QObject* object) {
|
||||
this->trackedWindows.removeAll(static_cast<QQuickWindow*>(object)); // NOLINT
|
||||
this->assignIncubationController();
|
||||
}
|
||||
|
||||
void EngineGeneration::assignIncubationController() {
|
||||
QQmlIncubationController* controller = &this->delayedIncubationController;
|
||||
|
||||
for (auto* window: this->trackedWindows) {
|
||||
if (auto* wctl = window->incubationController()) {
|
||||
controller = wctl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmlerror.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qquickwindow.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
|
||||
#include "incubator.hpp"
|
||||
@@ -40,8 +41,7 @@ public:
|
||||
void setWatchingFiles(bool watching);
|
||||
bool setExtraWatchedFiles(const QVector<QString>& files);
|
||||
|
||||
void registerIncubationController(QQmlIncubationController* controller);
|
||||
void deregisterIncubationController(QQmlIncubationController* controller);
|
||||
void trackWindowIncubationController(QQuickWindow* window);
|
||||
|
||||
// takes ownership
|
||||
void registerExtension(const void* key, EngineGenerationExt* extension);
|
||||
@@ -75,6 +75,7 @@ public:
|
||||
signals:
|
||||
void filesChanged();
|
||||
void reloadFinished();
|
||||
void firePostReload();
|
||||
|
||||
public slots:
|
||||
void quit();
|
||||
@@ -83,13 +84,13 @@ public slots:
|
||||
private slots:
|
||||
void onFileChanged(const QString& name);
|
||||
void onDirectoryChanged();
|
||||
void incubationControllerDestroyed();
|
||||
void onEngineWarnings(const QList<QQmlError>& warnings) const;
|
||||
void onTrackedWindowDestroyed(QObject* object);
|
||||
static void onEngineWarnings(const QList<QQmlError>& warnings);
|
||||
|
||||
private:
|
||||
void postReload();
|
||||
void assignIncubationController();
|
||||
QVector<QQmlIncubationController*> incubationControllers;
|
||||
QVector<QQuickWindow*> trackedWindows;
|
||||
bool incubationControllersLocked = false;
|
||||
QHash<const void*, EngineGenerationExt*> extensions;
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#include "incubator.hpp"
|
||||
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
Q_LOGGING_CATEGORY(logIncubator, "quickshell.incubator", QtWarningMsg);
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_LOGGING_CATEGORY(logIncubator, "quickshell.incubator", QtWarningMsg);
|
||||
|
||||
void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) {
|
||||
switch (status) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <qloggingcategory.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logIncubator);
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_DECLARE_LOGGING_CATEGORY(logIncubator);
|
||||
|
||||
class QsQmlIncubator
|
||||
: public QObject
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
#include <qdatastream.h>
|
||||
|
||||
QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) {
|
||||
stream << info.instanceId << info.configPath << info.shellId << info.launchTime;
|
||||
stream << info.instanceId << info.configPath << info.shellId << info.launchTime << info.pid;
|
||||
return stream;
|
||||
}
|
||||
|
||||
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) {
|
||||
stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime;
|
||||
stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime >> info.pid;
|
||||
return stream;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
#include <qdatetime.h>
|
||||
#include <qlogging.h>
|
||||
#include <qstring.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
struct InstanceInfo {
|
||||
QString instanceId;
|
||||
QString configPath;
|
||||
QString shellId;
|
||||
QDateTime launchTime;
|
||||
pid_t pid = -1;
|
||||
|
||||
static InstanceInfo CURRENT; // NOLINT
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
|
||||
namespace qs::log {
|
||||
void initLogCategoryLevel(const char* name, QtMsgType defaultLevel = QtDebugMsg);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define QS_DECLARE_LOGGING_CATEGORY(name) \
|
||||
namespace qslogcat { \
|
||||
Q_DECLARE_LOGGING_CATEGORY(name); \
|
||||
} \
|
||||
const QLoggingCategory& name()
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define QS_LOGGING_CATEGORY(name, category, ...) \
|
||||
namespace qslogcat { \
|
||||
Q_LOGGING_CATEGORY(name, category __VA_OPT__(, __VA_ARGS__)); \
|
||||
} \
|
||||
const QLoggingCategory& name() { \
|
||||
static auto* init = []() { \
|
||||
qs::log::initLogCategoryLevel(category __VA_OPT__(, __VA_ARGS__)); \
|
||||
return &qslogcat::name; \
|
||||
}(); \
|
||||
return (init) (); \
|
||||
}
|
||||
+54
-25
@@ -30,17 +30,18 @@
|
||||
#include <sys/sendfile.h>
|
||||
|
||||
#include "instanceinfo.hpp"
|
||||
#include "logcat.hpp"
|
||||
#include "logging_p.hpp"
|
||||
#include "logging_qtprivate.cpp" // NOLINT
|
||||
#include "paths.hpp"
|
||||
#include "ringbuf.hpp"
|
||||
|
||||
Q_LOGGING_CATEGORY(logBare, "quickshell.bare");
|
||||
QS_LOGGING_CATEGORY(logBare, "quickshell.bare");
|
||||
|
||||
namespace qs::log {
|
||||
using namespace qt_logging_registry;
|
||||
|
||||
Q_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg);
|
||||
|
||||
bool LogMessage::operator==(const LogMessage& other) const {
|
||||
// note: not including time
|
||||
@@ -182,17 +183,24 @@ void LogManager::filterCategory(QLoggingCategory* category) {
|
||||
auto categoryName = QLatin1StringView(category->categoryName());
|
||||
auto isQs = categoryName.startsWith(QLatin1StringView("quickshell."));
|
||||
|
||||
if (instance->lastCategoryFilter) {
|
||||
instance->lastCategoryFilter(category);
|
||||
}
|
||||
|
||||
auto filter = CategoryFilter(category);
|
||||
CategoryFilter filter;
|
||||
|
||||
// We don't respect log filters for qs logs because some distros like to ship
|
||||
// default configs that hide everything. QT_LOGGING_RULES is considered via the filter list.
|
||||
if (isQs) {
|
||||
filter.debug = filter.debug || instance->mDefaultLevel == QtDebugMsg;
|
||||
filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg;
|
||||
filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg;
|
||||
filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg;
|
||||
// QtDebugMsg == 0, so default
|
||||
auto defaultLevel = instance->defaultLevels.value(categoryName);
|
||||
|
||||
filter = CategoryFilter();
|
||||
// clang-format off
|
||||
filter.debug = instance->mDefaultLevel == QtDebugMsg || defaultLevel == QtDebugMsg;
|
||||
filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg || defaultLevel == QtInfoMsg;
|
||||
filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg || defaultLevel == QtWarningMsg;
|
||||
filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg || defaultLevel == QtCriticalMsg;
|
||||
// clang-format on
|
||||
} else if (instance->lastCategoryFilter) {
|
||||
instance->lastCategoryFilter(category);
|
||||
filter = CategoryFilter(category);
|
||||
}
|
||||
|
||||
for (const auto& rule: *instance->rules) {
|
||||
@@ -235,8 +243,12 @@ void LogManager::init(
|
||||
|
||||
{
|
||||
QLoggingSettingsParser parser;
|
||||
parser.setContent(rules);
|
||||
// Load QT_LOGGING_RULES because we ignore the last category filter for QS messages
|
||||
// due to disk config files.
|
||||
parser.setContent(qEnvironmentVariable("QT_LOGGING_RULES"));
|
||||
instance->rules = new QList(parser.rules());
|
||||
parser.setContent(rules);
|
||||
instance->rules->append(parser.rules());
|
||||
}
|
||||
|
||||
qInstallMessageHandler(&LogManager::messageHandler);
|
||||
@@ -257,6 +269,10 @@ void LogManager::init(
|
||||
qCDebug(logLogging) << "Logger initialized.";
|
||||
}
|
||||
|
||||
void initLogCategoryLevel(const char* name, QtMsgType defaultLevel) {
|
||||
LogManager::instance()->defaultLevels.insert(QLatin1StringView(name), defaultLevel);
|
||||
}
|
||||
|
||||
void LogManager::initFs() {
|
||||
QMetaObject::invokeMethod(
|
||||
&LogManager::instance()->threadProxy,
|
||||
@@ -297,8 +313,12 @@ void ThreadLogging::init() {
|
||||
|
||||
if (logMfd != -1) {
|
||||
this->file = new QFile();
|
||||
this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle);
|
||||
this->fileStream.setDevice(this->file);
|
||||
|
||||
if (this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle)) {
|
||||
this->fileStream.setDevice(this->file);
|
||||
} else {
|
||||
qCCritical(logLogging) << "Failed to open early logging memfd.";
|
||||
}
|
||||
}
|
||||
|
||||
if (dlogMfd != -1) {
|
||||
@@ -306,14 +326,19 @@ void ThreadLogging::init() {
|
||||
|
||||
this->detailedFile = new QFile();
|
||||
// buffered by WriteBuffer
|
||||
this->detailedFile->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle);
|
||||
this->detailedWriter.setDevice(this->detailedFile);
|
||||
if (this->detailedFile
|
||||
->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle))
|
||||
{
|
||||
this->detailedWriter.setDevice(this->detailedFile);
|
||||
|
||||
if (!this->detailedWriter.writeHeader()) {
|
||||
qCCritical(logLogging) << "Could not write header for detailed logs.";
|
||||
this->detailedWriter.setDevice(nullptr);
|
||||
delete this->detailedFile;
|
||||
this->detailedFile = nullptr;
|
||||
if (!this->detailedWriter.writeHeader()) {
|
||||
qCCritical(logLogging) << "Could not write header for detailed logs.";
|
||||
this->detailedWriter.setDevice(nullptr);
|
||||
delete this->detailedFile;
|
||||
this->detailedFile = nullptr;
|
||||
}
|
||||
} else {
|
||||
qCCritical(logLogging) << "Failed to open early detailed logging memfd.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,10 +466,14 @@ void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) {
|
||||
this->fileStream << Qt::endl;
|
||||
}
|
||||
|
||||
if (this->detailedWriter.write(msg)) {
|
||||
this->detailedFile->flush();
|
||||
} else if (this->detailedFile != nullptr) {
|
||||
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
|
||||
if (!this->detailedWriter.write(msg) || (this->detailedFile && !this->detailedFile->flush())) {
|
||||
this->detailedWriter.setDevice(nullptr);
|
||||
|
||||
if (this->detailedFile) {
|
||||
this->detailedFile->close();
|
||||
this->detailedFile = nullptr;
|
||||
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <qbytearrayview.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdatetime.h>
|
||||
#include <qfile.h>
|
||||
@@ -12,7 +13,9 @@
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logBare);
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_DECLARE_LOGGING_CATEGORY(logBare);
|
||||
|
||||
namespace qs::log {
|
||||
|
||||
@@ -127,11 +130,14 @@ private:
|
||||
QString mRulesString;
|
||||
QList<qt_logging_registry::QLoggingRule>* rules = nullptr;
|
||||
QtMsgType mDefaultLevel = QtWarningMsg;
|
||||
QHash<QLatin1StringView, QtMsgType> defaultLevels;
|
||||
QHash<const void*, CategoryFilter> sparseFilters;
|
||||
QHash<QLatin1StringView, CategoryFilter> allFilters;
|
||||
|
||||
QTextStream stdoutStream;
|
||||
LoggingThreadProxy threadProxy;
|
||||
|
||||
friend void initLogCategoryLevel(const char* name, QtMsgType defaultLevel);
|
||||
};
|
||||
|
||||
bool readEncodedLogs(
|
||||
|
||||
@@ -16,10 +16,11 @@
|
||||
#include <qstringview.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "logcat.hpp"
|
||||
#include "logging_qtprivate.hpp"
|
||||
|
||||
namespace qs::log {
|
||||
Q_DECLARE_LOGGING_CATEGORY(logLogging);
|
||||
QS_DECLARE_LOGGING_CATEGORY(logLogging);
|
||||
|
||||
namespace qt_logging_registry {
|
||||
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
#include <qstringview.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "logcat.hpp"
|
||||
|
||||
namespace qs::log {
|
||||
Q_DECLARE_LOGGING_CATEGORY(logLogging);
|
||||
QS_DECLARE_LOGGING_CATEGORY(logLogging);
|
||||
|
||||
namespace qt_logging_registry {
|
||||
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ public:
|
||||
[[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override;
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
[[nodiscard]] QList<QObject*> values() const { return this->valuesList; };
|
||||
[[nodiscard]] QList<QObject*> values() const { return this->valuesList; }
|
||||
void removeAt(qsizetype index);
|
||||
|
||||
Q_INVOKABLE qsizetype indexOf(QObject* object);
|
||||
|
||||
+41
-11
@@ -9,15 +9,17 @@
|
||||
#include <qdir.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qpair.h>
|
||||
#include <qstandardpaths.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
#include <qtversionchecks.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "instanceinfo.hpp"
|
||||
#include "logcat.hpp"
|
||||
|
||||
namespace {
|
||||
Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
|
||||
}
|
||||
|
||||
QsPaths* QsPaths::instance() {
|
||||
@@ -133,6 +135,33 @@ QDir* QsPaths::instanceRunDir() {
|
||||
else return &this->mInstanceRunDir;
|
||||
}
|
||||
|
||||
QDir* QsPaths::shellVfsDir() {
|
||||
if (this->shellVfsState == DirState::Unknown) {
|
||||
if (auto* baseRunDir = this->baseRunDir()) {
|
||||
this->mShellVfsDir = QDir(baseRunDir->filePath("vfs"));
|
||||
this->mShellVfsDir = QDir(this->mShellVfsDir.filePath(this->shellId));
|
||||
|
||||
qCDebug(logPaths) << "Initialized runtime vfs path:" << this->mShellVfsDir.path();
|
||||
|
||||
if (!this->mShellVfsDir.mkpath(".")) {
|
||||
qCCritical(logPaths) << "Could not create runtime vfs directory at"
|
||||
<< this->mShellVfsDir.path();
|
||||
this->shellVfsState = DirState::Failed;
|
||||
} else {
|
||||
this->shellVfsState = DirState::Ready;
|
||||
}
|
||||
} else {
|
||||
qCCritical(logPaths) << "Could not create shell runtime vfs path as it was not possible to "
|
||||
"create the base runtime path.";
|
||||
|
||||
this->shellVfsState = DirState::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->shellVfsState == DirState::Failed) return nullptr;
|
||||
else return &this->mShellVfsDir;
|
||||
}
|
||||
|
||||
void QsPaths::linkRunDir() {
|
||||
if (auto* runDir = this->instanceRunDir()) {
|
||||
auto pidDir = QDir(this->baseRunDir()->filePath("by-pid"));
|
||||
@@ -367,29 +396,30 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<InstanceLockInfo> QsPaths::collectInstances(const QString& path, bool fallbackDead) {
|
||||
QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>>
|
||||
QsPaths::collectInstances(const QString& path) {
|
||||
qCDebug(logPaths) << "Collecting instances from" << path;
|
||||
auto instances = QVector<InstanceLockInfo>();
|
||||
auto liveInstances = QVector<InstanceLockInfo>();
|
||||
auto deadInstances = QVector<InstanceLockInfo>();
|
||||
auto dir = QDir(path);
|
||||
|
||||
InstanceLockInfo info;
|
||||
for (auto& entry: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
|
||||
auto path = dir.filePath(entry);
|
||||
|
||||
if (QsPaths::checkLock(path, &info, fallbackDead)) {
|
||||
if (fallbackDead && info.pid != -1) {
|
||||
fallbackDead = false;
|
||||
instances.clear();
|
||||
}
|
||||
|
||||
if (QsPaths::checkLock(path, &info, true)) {
|
||||
qCDebug(logPaths).nospace() << "Found instance " << info.instance.instanceId << " (pid "
|
||||
<< info.pid << ") at " << path;
|
||||
|
||||
instances.push_back(info);
|
||||
if (info.pid == -1) {
|
||||
deadInstances.push_back(info);
|
||||
} else {
|
||||
liveInstances.push_back(info);
|
||||
}
|
||||
} else {
|
||||
qCDebug(logPaths) << "Skipped potential instance at" << path;
|
||||
}
|
||||
}
|
||||
|
||||
return instances;
|
||||
return qMakePair(liveInstances, deadInstances);
|
||||
}
|
||||
+6
-1
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include <qdatetime.h>
|
||||
#include <qdir.h>
|
||||
#include <qpair.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "instanceinfo.hpp"
|
||||
@@ -22,10 +23,12 @@ public:
|
||||
static QString ipcPath(const QString& id);
|
||||
static bool
|
||||
checkLock(const QString& path, InstanceLockInfo* info = nullptr, bool allowDead = false);
|
||||
static QVector<InstanceLockInfo> collectInstances(const QString& path, bool fallbackDead = false);
|
||||
static QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>>
|
||||
collectInstances(const QString& path);
|
||||
|
||||
QDir* baseRunDir();
|
||||
QDir* shellRunDir();
|
||||
QDir* shellVfsDir();
|
||||
QDir* instanceRunDir();
|
||||
void linkRunDir();
|
||||
void linkPathDir();
|
||||
@@ -46,9 +49,11 @@ private:
|
||||
QString pathId;
|
||||
QDir mBaseRunDir;
|
||||
QDir mShellRunDir;
|
||||
QDir mShellVfsDir;
|
||||
QDir mInstanceRunDir;
|
||||
DirState baseRunState = DirState::Unknown;
|
||||
DirState shellRunState = DirState::Unknown;
|
||||
DirState shellVfsState = DirState::Unknown;
|
||||
DirState instanceRunState = DirState::Unknown;
|
||||
|
||||
QDir mShellDataDir;
|
||||
|
||||
@@ -189,11 +189,15 @@ void PopupAnchor::updateAnchor() {
|
||||
if (this->mItem && this->mProxyWindow) {
|
||||
auto baseRect =
|
||||
this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect();
|
||||
|
||||
auto rect = this->mProxyWindow->contentItem()->mapFromItem(
|
||||
this->mItem,
|
||||
baseRect.marginsRemoved(this->mMargins.qmargins())
|
||||
);
|
||||
|
||||
if (rect.width() < 1) rect.setWidth(1);
|
||||
if (rect.height() < 1) rect.setHeight(1);
|
||||
|
||||
this->setWindowRect(rect.toRect());
|
||||
}
|
||||
|
||||
|
||||
+62
-6
@@ -8,8 +8,10 @@
|
||||
#include <qguiapplication.h>
|
||||
#include <qicon.h>
|
||||
#include <qjsengine.h>
|
||||
#include <qlist.h>
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qprocess.h>
|
||||
#include <qqmlcontext.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmllist.h>
|
||||
@@ -21,6 +23,7 @@
|
||||
#include <qwindowdefs.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "../io/processcore.hpp"
|
||||
#include "generation.hpp"
|
||||
#include "iconimageprovider.hpp"
|
||||
#include "paths.hpp"
|
||||
@@ -177,12 +180,6 @@ void QuickshellGlobal::reload(bool hard) {
|
||||
root->reloadGraph(hard);
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::shellRoot() const {
|
||||
auto* generation = EngineGeneration::findObjectGeneration(this);
|
||||
// already canonical
|
||||
return generation->rootPath.path();
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::workingDirectory() const { // NOLINT
|
||||
return QuickshellSettings::instance()->workingDirectory();
|
||||
}
|
||||
@@ -213,6 +210,22 @@ void QuickshellGlobal::onClipboardChanged(QClipboard::Mode mode) {
|
||||
if (mode == QClipboard::Clipboard) emit this->clipboardTextChanged();
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::shellDir() const {
|
||||
return EngineGeneration::findObjectGeneration(this)->rootPath.path();
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::configDir() const {
|
||||
qWarning() << "Quickshell.configDir is deprecated and may be removed in a future release. Use "
|
||||
"Quickshell.shellDir.";
|
||||
return this->shellDir();
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::shellRoot() const {
|
||||
qWarning() << "Quickshell.shellRoot is deprecated and may be removed in a future release. Use "
|
||||
"Quickshell.shellDir.";
|
||||
return this->shellDir();
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::dataDir() const { // NOLINT
|
||||
return QsPaths::instance()->shellDataDir().path();
|
||||
}
|
||||
@@ -225,6 +238,16 @@ QString QuickshellGlobal::cacheDir() const { // NOLINT
|
||||
return QsPaths::instance()->shellCacheDir().path();
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::shellPath(const QString& path) const {
|
||||
return this->shellDir() % '/' % path;
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::configPath(const QString& path) const {
|
||||
qWarning() << "Quickshell.configPath() is deprecated and may be removed in a future release. Use "
|
||||
"Quickshell.shellPath().";
|
||||
return this->shellPath(path);
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::dataPath(const QString& path) const {
|
||||
return this->dataDir() % '/' % path;
|
||||
}
|
||||
@@ -244,6 +267,39 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT
|
||||
return qEnvironmentVariable(vstr.data());
|
||||
}
|
||||
|
||||
void QuickshellGlobal::execDetached(QList<QString> command) {
|
||||
QuickshellGlobal::execDetached(qs::io::process::ProcessContext(std::move(command)));
|
||||
}
|
||||
|
||||
void QuickshellGlobal::execDetached(const qs::io::process::ProcessContext& context) {
|
||||
if (context.command.isEmpty()) {
|
||||
qWarning() << "Cannot start process as command is empty.";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& cmd = context.command.first();
|
||||
auto args = context.command.sliced(1);
|
||||
|
||||
QProcess process;
|
||||
qs::io::process::setupProcessEnvironment(&process, context.clearEnvironment, context.environment);
|
||||
|
||||
if (!context.workingDirectory.isEmpty()) {
|
||||
process.setWorkingDirectory(context.workingDirectory);
|
||||
}
|
||||
|
||||
process.setProgram(cmd);
|
||||
process.setArguments(args);
|
||||
|
||||
process.setStandardInputFile(QProcess::nullDevice());
|
||||
|
||||
if (context.unbindStdout) {
|
||||
process.setStandardOutputFile(QProcess::nullDevice());
|
||||
process.setStandardErrorFile(QProcess::nullDevice());
|
||||
}
|
||||
|
||||
process.startDetached();
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::iconPath(const QString& icon) {
|
||||
return IconImageProvider::requestString(icon);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
#include <qclipboard.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qhash.h>
|
||||
#include <qjsengine.h>
|
||||
#include <qlist.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmlintegration.h>
|
||||
@@ -13,6 +15,8 @@
|
||||
#include <qvariant.h>
|
||||
#include <qwindowdefs.h>
|
||||
|
||||
#include "../io/processcore.hpp"
|
||||
#include "doc.hpp"
|
||||
#include "qmlscreen.hpp"
|
||||
|
||||
///! Accessor for some options under the Quickshell type.
|
||||
@@ -104,6 +108,10 @@ class QuickshellGlobal: public QObject {
|
||||
///
|
||||
/// The root directory is the folder containing the entrypoint to your shell, often referred
|
||||
/// to as `shell.qml`.
|
||||
Q_PROPERTY(QString shellDir READ shellDir CONSTANT);
|
||||
/// > [!WARNING] Deprecated: Renamed to @@shellDir for clarity.
|
||||
Q_PROPERTY(QString configDir READ configDir CONSTANT);
|
||||
/// > [!WARNING] Deprecated: Renamed to @@shellDir for consistency.
|
||||
Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT);
|
||||
/// Quickshell's working directory. Defaults to whereever quickshell was launched from.
|
||||
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged);
|
||||
@@ -152,6 +160,30 @@ public:
|
||||
/// Returns the string value of an environment variable or null if it is not set.
|
||||
Q_INVOKABLE QVariant env(const QString& variable);
|
||||
|
||||
// MUST be before execDetached(ctx) or the other will be called with a default constructed obj.
|
||||
QSDOC_HIDE Q_INVOKABLE static void execDetached(QList<QString> command);
|
||||
/// Launch a process detached from Quickshell.
|
||||
///
|
||||
/// The context parameter can either be a list of command arguments or a JS object with the following fields:
|
||||
/// - `command`: A list containing the command and all its arguments. See @@Quickshell.Io.Process.command.
|
||||
/// - `environment`: Changes to make to the process environment. See @@Quickshell.Io.Process.environment.
|
||||
/// - `clearEnvironment`: Removes all variables from the environment if true.
|
||||
/// - `workingDirectory`: The working directory the command should run in.
|
||||
///
|
||||
/// > [!WARNING] This does not run command in a shell. All arguments to the command
|
||||
/// > must be in separate values in the list, e.g. `["echo", "hello"]`
|
||||
/// > and not `["echo hello"]`.
|
||||
/// >
|
||||
/// > Additionally, shell scripts must be run by your shell,
|
||||
/// > e.g. `["sh", "script.sh"]` instead of `["script.sh"]` unless the script
|
||||
/// > has a shebang.
|
||||
///
|
||||
/// > [!INFO] You can use `["sh", "-c", <your command>]` to execute your command with
|
||||
/// > the system shell.
|
||||
///
|
||||
/// This function is equivalent to @@Quickshell.Io.Process.startDetached().
|
||||
Q_INVOKABLE static void execDetached(const qs::io::process::ProcessContext& context);
|
||||
|
||||
/// Returns a string usable for a @@QtQuick.Image.source for a given system icon.
|
||||
///
|
||||
/// > [!INFO] By default, icons are loaded from the theme selected by the qt platform theme,
|
||||
@@ -167,6 +199,10 @@ public:
|
||||
/// Setting the `fallback` parameter of `iconPath` will attempt to load the fallback
|
||||
/// icon if the requested one could not be loaded.
|
||||
Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback);
|
||||
/// Equivalent to `${Quickshell.configDir}/${path}`
|
||||
Q_INVOKABLE [[nodiscard]] QString shellPath(const QString& path) const;
|
||||
/// > [!WARNING] Deprecated: Renamed to @@shellPath() for clarity.
|
||||
Q_INVOKABLE [[nodiscard]] QString configPath(const QString& path) const;
|
||||
/// Equivalent to `${Quickshell.dataDir}/${path}`
|
||||
Q_INVOKABLE [[nodiscard]] QString dataPath(const QString& path) const;
|
||||
/// Equivalent to `${Quickshell.stateDir}/${path}`
|
||||
@@ -182,6 +218,8 @@ public:
|
||||
void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; }
|
||||
[[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; }
|
||||
|
||||
[[nodiscard]] QString shellDir() const;
|
||||
[[nodiscard]] QString configDir() const;
|
||||
[[nodiscard]] QString shellRoot() const;
|
||||
|
||||
[[nodiscard]] QString workingDirectory() const;
|
||||
|
||||
+47
-18
@@ -1,6 +1,7 @@
|
||||
#include "qsintercept.hpp"
|
||||
#include <cstring>
|
||||
|
||||
#include <qdir.h>
|
||||
#include <qhash.h>
|
||||
#include <qiodevice.h>
|
||||
#include <qlogging.h>
|
||||
@@ -14,7 +15,9 @@
|
||||
#include <qtypes.h>
|
||||
#include <qurl.h>
|
||||
|
||||
Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg);
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg);
|
||||
|
||||
QUrl QsUrlInterceptor::intercept(
|
||||
const QUrl& originalUrl,
|
||||
@@ -23,27 +26,44 @@ QUrl QsUrlInterceptor::intercept(
|
||||
auto url = originalUrl;
|
||||
|
||||
if (url.scheme() == "root") {
|
||||
url.setScheme("qsintercept");
|
||||
url.setScheme("qs");
|
||||
|
||||
auto path = url.path();
|
||||
if (path.startsWith('/')) path = path.sliced(1);
|
||||
url.setPath(this->configRoot.filePath(path));
|
||||
url.setPath("@/qs/" % path);
|
||||
|
||||
qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url;
|
||||
}
|
||||
|
||||
// Some types such as Image take into account where they are loading from, and force
|
||||
// asynchronous loading over a network. qsintercept is considered to be over a network.
|
||||
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") {
|
||||
// Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which
|
||||
// case we want to keep the intercept, otherwise objects created from those paths
|
||||
// will not be able to use singletons.
|
||||
if (url.path().endsWith(".qml")) return url;
|
||||
if (url.scheme() == "qs") {
|
||||
auto path = url.path();
|
||||
|
||||
auto newUrl = url;
|
||||
newUrl.setScheme("file");
|
||||
qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl;
|
||||
return newUrl;
|
||||
// Our import path is on "qs:@/".
|
||||
// We want to blackhole any import resolution outside of the config folder as it breaks Qt
|
||||
// but NOT file lookups that might be on "qs:/" due to a missing "file:/" prefix.
|
||||
if (path.startsWith("@/qs/")) {
|
||||
path = this->configRoot.filePath(path.sliced(5));
|
||||
} else if (!path.startsWith("/")) {
|
||||
qCDebug(logQsIntercept) << "Blackholed import URL" << url;
|
||||
return QUrl("qrc:/qs-blackhole");
|
||||
}
|
||||
|
||||
// Some types such as Image take into account where they are loading from, and force
|
||||
// asynchronous loading over a network. qs: is considered to be over a network.
|
||||
// In those cases we want to return a file:// url so asynchronous loading is not forced.
|
||||
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString) {
|
||||
// Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which
|
||||
// case we want to keep the intercept, otherwise objects created from those paths
|
||||
// will not be able to use singletons.
|
||||
if (path.endsWith(".qml")) return url;
|
||||
|
||||
auto newUrl = url;
|
||||
newUrl.setScheme("file");
|
||||
// above check asserts path starts with /qs/
|
||||
newUrl.setPath(path);
|
||||
qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl;
|
||||
return newUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
@@ -65,10 +85,12 @@ qint64 QsInterceptDataReply::readData(char* data, qint64 maxSize) {
|
||||
}
|
||||
|
||||
QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager(
|
||||
const QDir& configRoot,
|
||||
const QHash<QString, QString>& fileIntercepts,
|
||||
QObject* parent
|
||||
)
|
||||
: QNetworkAccessManager(parent)
|
||||
, configRoot(configRoot)
|
||||
, fileIntercepts(fileIntercepts) {}
|
||||
|
||||
QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
|
||||
@@ -77,19 +99,26 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
|
||||
QIODevice* outgoingData
|
||||
) {
|
||||
auto url = req.url();
|
||||
if (url.scheme() == "qsintercept") {
|
||||
|
||||
if (url.scheme() == "qs") {
|
||||
auto path = url.path();
|
||||
|
||||
if (path.startsWith("@/qs/")) path = this->configRoot.filePath(path.sliced(5));
|
||||
// otherwise pass through to fs
|
||||
|
||||
qCDebug(logQsIntercept) << "Got intercept for" << path << "contains"
|
||||
<< this->fileIntercepts.value(path);
|
||||
auto data = this->fileIntercepts.value(path);
|
||||
if (data != nullptr) {
|
||||
|
||||
if (auto data = this->fileIntercepts.value(path); !data.isEmpty()) {
|
||||
return new QsInterceptDataReply(data, this);
|
||||
}
|
||||
|
||||
auto fileReq = req;
|
||||
auto fileUrl = req.url();
|
||||
fileUrl.setScheme("file");
|
||||
fileUrl.setPath(path);
|
||||
qCDebug(logQsIntercept) << "Passing through intercept" << url << "to" << fileUrl;
|
||||
|
||||
fileReq.setUrl(fileUrl);
|
||||
return this->QNetworkAccessManager::createRequest(op, fileReq, outgoingData);
|
||||
}
|
||||
@@ -98,5 +127,5 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
|
||||
}
|
||||
|
||||
QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) {
|
||||
return new QsInterceptNetworkAccessManager(this->fileIntercepts, parent);
|
||||
return new QsInterceptNetworkAccessManager(this->configRoot, this->fileIntercepts, parent);
|
||||
}
|
||||
@@ -10,7 +10,9 @@
|
||||
#include <qqmlnetworkaccessmanagerfactory.h>
|
||||
#include <qurl.h>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logQsIntercept);
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_DECLARE_LOGGING_CATEGORY(logQsIntercept);
|
||||
|
||||
class QsUrlInterceptor: public QQmlAbstractUrlInterceptor {
|
||||
public:
|
||||
@@ -43,6 +45,7 @@ class QsInterceptNetworkAccessManager: public QNetworkAccessManager {
|
||||
|
||||
public:
|
||||
QsInterceptNetworkAccessManager(
|
||||
const QDir& configRoot,
|
||||
const QHash<QString, QString>& fileIntercepts,
|
||||
QObject* parent = nullptr
|
||||
);
|
||||
@@ -55,15 +58,21 @@ protected:
|
||||
) override;
|
||||
|
||||
private:
|
||||
QDir configRoot;
|
||||
const QHash<QString, QString>& fileIntercepts;
|
||||
};
|
||||
|
||||
class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory {
|
||||
public:
|
||||
QsInterceptNetworkAccessManagerFactory(const QHash<QString, QString>& fileIntercepts)
|
||||
: fileIntercepts(fileIntercepts) {}
|
||||
QsInterceptNetworkAccessManagerFactory(
|
||||
const QDir& configRoot,
|
||||
const QHash<QString, QString>& fileIntercepts
|
||||
)
|
||||
: configRoot(configRoot)
|
||||
, fileIntercepts(fileIntercepts) {}
|
||||
QNetworkAccessManager* create(QObject* parent) override;
|
||||
|
||||
private:
|
||||
QDir configRoot;
|
||||
const QHash<QString, QString>& fileIntercepts;
|
||||
};
|
||||
+2
-2
@@ -46,8 +46,8 @@ class QsMenuHandle: public QObject {
|
||||
public:
|
||||
explicit QsMenuHandle(QObject* parent): QObject(parent) {}
|
||||
|
||||
virtual void refHandle() {};
|
||||
virtual void unrefHandle() {};
|
||||
virtual void refHandle() {}
|
||||
virtual void unrefHandle() {}
|
||||
|
||||
[[nodiscard]] virtual QsMenuEntry* menu() = 0;
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ PendingRegion::PendingRegion(QObject* parent): QObject(parent) {
|
||||
}
|
||||
|
||||
void PendingRegion::setItem(QQuickItem* item) {
|
||||
if (item == this->mItem) return;
|
||||
|
||||
if (this->mItem != nullptr) {
|
||||
QObject::disconnect(this->mItem, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
+16
-7
@@ -126,12 +126,21 @@ QObject* Reloadable::getChildByReloadId(QObject* parent, const QString& reloadId
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void PostReloadHook::postReloadTree(QObject* root) {
|
||||
for (auto* child: root->children()) {
|
||||
PostReloadHook::postReloadTree(child);
|
||||
}
|
||||
|
||||
if (auto* self = dynamic_cast<PostReloadHook*>(root)) {
|
||||
self->onPostReload();
|
||||
void PostReloadHook::componentComplete() {
|
||||
auto* engineGeneration = EngineGeneration::findObjectGeneration(this);
|
||||
if (!engineGeneration || engineGeneration->reloadComplete) this->postReload();
|
||||
else {
|
||||
// disconnected by EngineGeneration::postReload
|
||||
QObject::connect(
|
||||
engineGeneration,
|
||||
&EngineGeneration::firePostReload,
|
||||
this,
|
||||
&PostReloadHook::postReload
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void PostReloadHook::postReload() {
|
||||
this->isPostReload = true;
|
||||
this->onPostReload();
|
||||
}
|
||||
+16
-9
@@ -57,7 +57,7 @@ public:
|
||||
|
||||
void reload(QObject* oldInstance = nullptr);
|
||||
|
||||
void classBegin() override {};
|
||||
void classBegin() override {}
|
||||
void componentComplete() override;
|
||||
|
||||
// Reload objects in the parent->child graph recursively.
|
||||
@@ -119,16 +119,23 @@ private:
|
||||
};
|
||||
|
||||
/// Hook that runs after the old widget tree is dropped during a reload.
|
||||
class PostReloadHook {
|
||||
class PostReloadHook
|
||||
: public QObject
|
||||
, public QQmlParserStatus {
|
||||
Q_OBJECT;
|
||||
QML_ANONYMOUS;
|
||||
Q_INTERFACES(QQmlParserStatus);
|
||||
|
||||
public:
|
||||
PostReloadHook() = default;
|
||||
virtual ~PostReloadHook() = default;
|
||||
PostReloadHook(PostReloadHook&&) = default;
|
||||
PostReloadHook(const PostReloadHook&) = default;
|
||||
PostReloadHook& operator=(PostReloadHook&&) = default;
|
||||
PostReloadHook& operator=(const PostReloadHook&) = default;
|
||||
PostReloadHook(QObject* parent = nullptr): QObject(parent) {}
|
||||
void classBegin() override {}
|
||||
void componentComplete() override;
|
||||
|
||||
virtual void onPostReload() = 0;
|
||||
|
||||
static void postReloadTree(QObject* root);
|
||||
public slots:
|
||||
void postReload();
|
||||
|
||||
protected:
|
||||
bool isPostReload = false;
|
||||
};
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlcomponent.h>
|
||||
@@ -18,15 +19,26 @@
|
||||
#include "instanceinfo.hpp"
|
||||
#include "qmlglobal.hpp"
|
||||
#include "scan.hpp"
|
||||
#include "toolsupport.hpp"
|
||||
|
||||
RootWrapper::RootWrapper(QString rootPath, QString shellId)
|
||||
: QObject(nullptr)
|
||||
, rootPath(std::move(rootPath))
|
||||
, shellId(std::move(shellId))
|
||||
, originalWorkingDirectory(QDir::current().absolutePath()) {
|
||||
// clang-format off
|
||||
QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::watchFilesChanged, this, &RootWrapper::onWatchFilesChanged);
|
||||
// clang-format on
|
||||
QObject::connect(
|
||||
QuickshellSettings::instance(),
|
||||
&QuickshellSettings::watchFilesChanged,
|
||||
this,
|
||||
&RootWrapper::onWatchFilesChanged
|
||||
);
|
||||
|
||||
QObject::connect(
|
||||
&this->configDirWatcher,
|
||||
&QFileSystemWatcher::directoryChanged,
|
||||
this,
|
||||
&RootWrapper::updateTooling
|
||||
);
|
||||
|
||||
this->reloadGraph(true);
|
||||
|
||||
@@ -43,9 +55,13 @@ RootWrapper::~RootWrapper() {
|
||||
}
|
||||
|
||||
void RootWrapper::reloadGraph(bool hard) {
|
||||
auto rootPath = QFileInfo(this->rootPath).dir();
|
||||
auto rootFile = QFileInfo(this->rootPath);
|
||||
auto rootPath = rootFile.dir();
|
||||
auto scanner = QmlScanner(rootPath);
|
||||
scanner.scanQmlFile(this->rootPath);
|
||||
scanner.scanQmlRoot(this->rootPath);
|
||||
|
||||
qs::core::QmlToolingSupport::updateTooling(rootPath, scanner);
|
||||
this->configDirWatcher.addPath(rootPath.path());
|
||||
|
||||
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
|
||||
generation->wrapper = this;
|
||||
@@ -58,9 +74,9 @@ void RootWrapper::reloadGraph(bool hard) {
|
||||
|
||||
QDir::setCurrent(this->originalWorkingDirectory);
|
||||
|
||||
auto url = QUrl::fromLocalFile(this->rootPath);
|
||||
// unless the original file comes from the qsintercept scheme
|
||||
url.setScheme("qsintercept");
|
||||
QUrl url;
|
||||
url.setScheme("qs");
|
||||
url.setPath("@/qs/" % rootFile.fileName());
|
||||
auto component = QQmlComponent(generation->engine, url);
|
||||
|
||||
if (!component.isReady()) {
|
||||
@@ -69,7 +85,9 @@ void RootWrapper::reloadGraph(bool hard) {
|
||||
|
||||
auto errors = component.errors();
|
||||
for (auto& error: errors) {
|
||||
auto rel = "**/" % rootPath.relativeFilePath(error.url().path());
|
||||
const auto& url = error.url();
|
||||
auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5)
|
||||
: url.toString();
|
||||
auto msg = " caused by " % rel % '[' % QString::number(error.line()) % ':'
|
||||
% QString::number(error.column()) % "]: " % error.description();
|
||||
errorString += '\n' % msg;
|
||||
@@ -165,3 +183,9 @@ void RootWrapper::onWatchFilesChanged() {
|
||||
}
|
||||
|
||||
void RootWrapper::onWatchedFilesChanged() { this->reloadGraph(false); }
|
||||
|
||||
void RootWrapper::updateTooling() {
|
||||
if (!this->generation) return;
|
||||
auto configDir = QFileInfo(this->rootPath).dir();
|
||||
qs::core::QmlToolingSupport::updateTooling(configDir, this->generation->scanner);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
@@ -22,10 +23,12 @@ private slots:
|
||||
void generationDestroyed();
|
||||
void onWatchFilesChanged();
|
||||
void onWatchedFilesChanged();
|
||||
void updateTooling();
|
||||
|
||||
private:
|
||||
QString rootPath;
|
||||
QString shellId;
|
||||
EngineGeneration* generation = nullptr;
|
||||
QString originalWorkingDirectory;
|
||||
QFileSystemWatcher configDirWatcher;
|
||||
};
|
||||
+104
-35
@@ -15,7 +15,9 @@
|
||||
#include <qstringliteral.h>
|
||||
#include <qtextstream.h>
|
||||
|
||||
Q_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg);
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg);
|
||||
|
||||
void QmlScanner::scanDir(const QString& path) {
|
||||
if (this->scannedDirs.contains(path)) return;
|
||||
@@ -24,42 +26,72 @@ void QmlScanner::scanDir(const QString& path) {
|
||||
qCDebug(logQmlScanner) << "Scanning directory" << path;
|
||||
auto dir = QDir(path);
|
||||
|
||||
struct Entry {
|
||||
QString name;
|
||||
bool singleton = false;
|
||||
bool internal = false;
|
||||
};
|
||||
|
||||
bool seenQmldir = false;
|
||||
auto singletons = QVector<QString>();
|
||||
auto entries = QVector<QString>();
|
||||
for (auto& entry: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) {
|
||||
if (entry == "qmldir") {
|
||||
auto entries = QVector<Entry>();
|
||||
|
||||
for (auto& name: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) {
|
||||
if (name == "qmldir") {
|
||||
qCDebug(logQmlScanner
|
||||
) << "Found qmldir file, qmldir synthesization will be disabled for directory"
|
||||
<< path;
|
||||
seenQmldir = true;
|
||||
} else if (entry.at(0).isUpper() && entry.endsWith(".qml")) {
|
||||
if (this->scanQmlFile(dir.filePath(entry))) {
|
||||
singletons.push_back(entry);
|
||||
} else if (name.at(0).isUpper() && name.endsWith(".qml")) {
|
||||
auto& entry = entries.emplaceBack();
|
||||
|
||||
if (this->scanQmlFile(dir.filePath(name), entry.singleton, entry.internal)) {
|
||||
entry.name = name;
|
||||
} else {
|
||||
entries.push_back(entry);
|
||||
entries.pop_back();
|
||||
}
|
||||
} else if (name.at(0).isUpper() && name.endsWith(".qml.json")) {
|
||||
if (this->scanQmlJson(dir.filePath(name))) {
|
||||
entries.push_back({
|
||||
.name = name.first(name.length() - 5),
|
||||
.singleton = true,
|
||||
});
|
||||
}
|
||||
} else if (entry.at(0).isUpper() && entry.endsWith(".qml.json")) {
|
||||
this->scanQmlJson(dir.filePath(entry));
|
||||
singletons.push_back(entry.first(entry.length() - 5));
|
||||
}
|
||||
}
|
||||
|
||||
// Due to the qsintercept:// protocol a qmldir is always required, even without singletons.
|
||||
if (!seenQmldir) {
|
||||
qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path << "singletons"
|
||||
<< singletons;
|
||||
qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path;
|
||||
|
||||
QString qmldir;
|
||||
auto stream = QTextStream(&qmldir);
|
||||
|
||||
for (auto& singleton: singletons) {
|
||||
stream << "singleton " << singleton.sliced(0, singleton.length() - 4) << " 1.0 " << singleton
|
||||
<< "\n";
|
||||
// cant derive a module name if not in shell path
|
||||
if (path.startsWith(this->rootPath.path())) {
|
||||
auto end = path.sliced(this->rootPath.path().length());
|
||||
|
||||
// verify we have a valid module name.
|
||||
for (auto& c: end) {
|
||||
if (c == '/') c = '.';
|
||||
else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|
||||
|| c == '_')
|
||||
{
|
||||
} else {
|
||||
qCWarning(logQmlScanner) << "Module path contains invalid characters for a module name: "
|
||||
<< path.sliced(this->rootPath.path().length());
|
||||
goto skipadd;
|
||||
}
|
||||
}
|
||||
|
||||
stream << "module qs" << end << '\n';
|
||||
skipadd:;
|
||||
} else {
|
||||
qCWarning(logQmlScanner) << "Module path" << path << "is outside of the config folder.";
|
||||
}
|
||||
|
||||
for (auto& entry: entries) {
|
||||
stream << entry.sliced(0, entry.length() - 4) << " 1.0 " << entry << "\n";
|
||||
for (const auto& entry: entries) {
|
||||
if (entry.internal) stream << "internal ";
|
||||
if (entry.singleton) stream << "singleton ";
|
||||
stream << entry.name.sliced(0, entry.name.length() - 4) << " 1.0 " << entry.name << '\n';
|
||||
}
|
||||
|
||||
qCDebug(logQmlScanner) << "Synthesized qmldir for" << path << qPrintable("\n" + qmldir);
|
||||
@@ -67,7 +99,7 @@ void QmlScanner::scanDir(const QString& path) {
|
||||
}
|
||||
}
|
||||
|
||||
bool QmlScanner::scanQmlFile(const QString& path) {
|
||||
bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& internal) {
|
||||
if (this->scannedFiles.contains(path)) return false;
|
||||
this->scannedFiles.push_back(path);
|
||||
|
||||
@@ -82,23 +114,47 @@ bool QmlScanner::scanQmlFile(const QString& path) {
|
||||
auto stream = QTextStream(&file);
|
||||
auto imports = QVector<QString>();
|
||||
|
||||
bool singleton = false;
|
||||
|
||||
while (!stream.atEnd()) {
|
||||
auto line = stream.readLine().trimmed();
|
||||
if (!singleton && line == "pragma Singleton") {
|
||||
qCDebug(logQmlScanner) << "Discovered singleton" << path;
|
||||
singleton = true;
|
||||
} else if (!internal && line == "//@ pragma Internal") {
|
||||
internal = true;
|
||||
} else if (line.startsWith("import")) {
|
||||
// we dont care about "import qs" as we always load the root folder
|
||||
if (auto importCursor = line.indexOf(" qs."); importCursor != -1) {
|
||||
importCursor += 4;
|
||||
QString path;
|
||||
|
||||
auto startQuot = line.indexOf('"');
|
||||
if (startQuot == -1 || line.length() < startQuot + 3) continue;
|
||||
auto endQuot = line.indexOf('"', startQuot + 1);
|
||||
if (endQuot == -1) continue;
|
||||
while (importCursor != line.length()) {
|
||||
auto c = line.at(importCursor);
|
||||
if (c == '.') c = '/';
|
||||
else if (c == ' ') break;
|
||||
else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|
||||
|| c == '_')
|
||||
{
|
||||
} else {
|
||||
qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line;
|
||||
goto next;
|
||||
}
|
||||
|
||||
auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1);
|
||||
imports.push_back(name);
|
||||
path.append(c);
|
||||
importCursor += 1;
|
||||
}
|
||||
|
||||
imports.append(this->rootPath.filePath(path));
|
||||
} else if (auto startQuot = line.indexOf('"');
|
||||
startQuot != -1 && line.length() >= startQuot + 3)
|
||||
{
|
||||
auto endQuot = line.indexOf('"', startQuot + 1);
|
||||
if (endQuot == -1) continue;
|
||||
|
||||
auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1);
|
||||
imports.push_back(name);
|
||||
}
|
||||
} else if (line.contains('{')) break;
|
||||
|
||||
next:;
|
||||
}
|
||||
|
||||
file.close();
|
||||
@@ -122,27 +178,39 @@ bool QmlScanner::scanQmlFile(const QString& path) {
|
||||
ipath = currentdir.filePath(import);
|
||||
}
|
||||
|
||||
auto cpath = QFileInfo(ipath).canonicalFilePath();
|
||||
auto pathInfo = QFileInfo(ipath);
|
||||
auto cpath = pathInfo.canonicalFilePath();
|
||||
|
||||
if (cpath.isEmpty()) {
|
||||
qCWarning(logQmlScanner) << "Ignoring unresolvable import" << ipath << "from" << path;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!pathInfo.isDir()) {
|
||||
qCDebug(logQmlScanner) << "Ignoring non-directory import" << ipath << "from" << path;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (import.endsWith(".js")) this->scannedFiles.push_back(cpath);
|
||||
else this->scanDir(cpath);
|
||||
}
|
||||
|
||||
return singleton;
|
||||
return true;
|
||||
}
|
||||
|
||||
void QmlScanner::scanQmlJson(const QString& path) {
|
||||
void QmlScanner::scanQmlRoot(const QString& path) {
|
||||
bool singleton = false;
|
||||
bool internal = false;
|
||||
this->scanQmlFile(path, singleton, internal);
|
||||
}
|
||||
|
||||
bool QmlScanner::scanQmlJson(const QString& path) {
|
||||
qCDebug(logQmlScanner) << "Scanning qml.json file" << path;
|
||||
|
||||
auto file = QFile(path);
|
||||
if (!file.open(QFile::ReadOnly | QFile::Text)) {
|
||||
qCWarning(logQmlScanner) << "Failed to open file" << path;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto data = file.readAll();
|
||||
@@ -154,7 +222,7 @@ void QmlScanner::scanQmlJson(const QString& path) {
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qCCritical(logQmlScanner).nospace()
|
||||
<< "Failed to parse qml.json file at " << path << ": " << error.errorString();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString body =
|
||||
@@ -164,6 +232,7 @@ void QmlScanner::scanQmlJson(const QString& path) {
|
||||
|
||||
this->fileIntercepts.insert(path.first(path.length() - 5), body);
|
||||
this->scannedFiles.push_back(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
QPair<QString, QString> QmlScanner::jsonToQml(const QJsonValue& value, int indent) {
|
||||
|
||||
+8
-4
@@ -6,7 +6,9 @@
|
||||
#include <qloggingcategory.h>
|
||||
#include <qvector.h>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logQmlScanner);
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_DECLARE_LOGGING_CATEGORY(logQmlScanner);
|
||||
|
||||
// expects canonical paths
|
||||
class QmlScanner {
|
||||
@@ -14,9 +16,10 @@ public:
|
||||
QmlScanner() = default;
|
||||
QmlScanner(const QDir& rootPath): rootPath(rootPath) {}
|
||||
|
||||
// path must be canonical
|
||||
void scanDir(const QString& path);
|
||||
// returns if the file has a singleton
|
||||
bool scanQmlFile(const QString& path);
|
||||
|
||||
void scanQmlRoot(const QString& path);
|
||||
|
||||
QVector<QString> scannedDirs;
|
||||
QVector<QString> scannedFiles;
|
||||
@@ -25,6 +28,7 @@ public:
|
||||
private:
|
||||
QDir rootPath;
|
||||
|
||||
void scanQmlJson(const QString& path);
|
||||
bool scanQmlFile(const QString& path, bool& singleton, bool& internal);
|
||||
bool scanQmlJson(const QString& path);
|
||||
[[nodiscard]] static QPair<QString, QString> jsonToQml(const QJsonValue& value, int indent = 0);
|
||||
};
|
||||
@@ -19,7 +19,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
|
||||
auto newIter = newValues.begin();
|
||||
|
||||
// TODO: cache this
|
||||
auto getCmpKey = [&](const QVariant& v) {
|
||||
auto getCmpKey = [this](const QVariant& v) {
|
||||
if (v.canConvert<QVariantMap>()) {
|
||||
auto vMap = v.value<QVariantMap>();
|
||||
if (vMap.contains(this->cmpKey)) {
|
||||
@@ -30,7 +30,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
|
||||
return v;
|
||||
};
|
||||
|
||||
auto variantCmp = [&](const QVariant& a, const QVariant& b) {
|
||||
auto variantCmp = [&, this](const QVariant& a, const QVariant& b) {
|
||||
if (!this->cmpKey.isEmpty()) return getCmpKey(a) == getCmpKey(b);
|
||||
else return a == b;
|
||||
};
|
||||
|
||||
@@ -51,9 +51,3 @@ void SingletonRegistry::onReload(SingletonRegistry* old) {
|
||||
singleton->reload(old == nullptr ? nullptr : old->registry.value(url));
|
||||
}
|
||||
}
|
||||
|
||||
void SingletonRegistry::onPostReload() {
|
||||
for (auto* singleton: this->registry.values()) {
|
||||
PostReloadHook::postReloadTree(singleton);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ public:
|
||||
|
||||
void registerSingleton(const QUrl& url, Singleton* singleton);
|
||||
void onReload(SingletonRegistry* old);
|
||||
void onPostReload();
|
||||
|
||||
private:
|
||||
QHash<QUrl, Singleton*> registry;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
function (qs_test name)
|
||||
add_executable(${name} ${ARGN})
|
||||
target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window quickshell-ui)
|
||||
target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window quickshell-ui quickshell-io)
|
||||
add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $<TARGET_FILE:${name}>)
|
||||
endfunction()
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ void TestScriptModel::unique_data() {
|
||||
void TestScriptModel::unique() {
|
||||
QFETCH(const QString, oldstr);
|
||||
QFETCH(const QString, newstr);
|
||||
QFETCH(OpList, operations);
|
||||
QFETCH(const OpList, operations);
|
||||
|
||||
auto strToVariantList = [](const QString& str) -> QVariantList {
|
||||
QVariantList list;
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
#include "toolsupport.hpp"
|
||||
#include <cerrno>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdebug.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qlist.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
|
||||
#include "logcat.hpp"
|
||||
#include "paths.hpp"
|
||||
#include "scan.hpp"
|
||||
|
||||
namespace qs::core {
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logTooling, "quickshell.tooling", QtWarningMsg);
|
||||
}
|
||||
|
||||
bool QmlToolingSupport::updateTooling(const QDir& configRoot, QmlScanner& scanner) {
|
||||
auto* vfs = QsPaths::instance()->shellVfsDir();
|
||||
|
||||
if (!vfs) {
|
||||
qCCritical(logTooling) << "Tooling dir could not be created";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!QmlToolingSupport::lockTooling()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!QmlToolingSupport::updateQmllsConfig(configRoot, false)) {
|
||||
QDir(vfs->filePath("qs")).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
QmlToolingSupport::updateToolingFs(scanner, configRoot, vfs->filePath("qs"));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QmlToolingSupport::lockTooling() {
|
||||
if (QmlToolingSupport::toolingLock) return true;
|
||||
|
||||
auto lockPath = QsPaths::instance()->shellVfsDir()->filePath("tooling.lock");
|
||||
auto* file = new QFile(lockPath);
|
||||
|
||||
if (!file->open(QFile::WriteOnly)) {
|
||||
qCCritical(logTooling) << "Could not open tooling lock for write";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto lock = flock {
|
||||
.l_type = F_WRLCK,
|
||||
.l_whence = SEEK_SET, // NOLINT (fcntl.h??)
|
||||
.l_start = 0,
|
||||
.l_len = 0,
|
||||
.l_pid = 0,
|
||||
};
|
||||
|
||||
if (fcntl(file->handle(), F_SETLK, &lock) == 0) {
|
||||
qCInfo(logTooling) << "Acquired tooling support lock";
|
||||
QmlToolingSupport::toolingLock = file;
|
||||
return true;
|
||||
} else if (errno == EACCES || errno == EAGAIN) {
|
||||
qCInfo(logTooling) << "Tooling support locked by another instance";
|
||||
return false;
|
||||
} else {
|
||||
qCCritical(logTooling).nospace() << "Could not create tooling lock at " << lockPath
|
||||
<< " with error code " << errno << ": " << qt_error_string();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QString QmlToolingSupport::getQmllsConfig() {
|
||||
static auto config = []() {
|
||||
// We can't replicate the algorithm used to create the import path list as it can have distro
|
||||
// specific patches, e.g. nixos.
|
||||
auto importPaths = QQmlEngine().importPathList();
|
||||
importPaths.removeIf([](const QString& path) { return path.startsWith("qrc:"); });
|
||||
|
||||
auto vfsPath = QsPaths::instance()->shellVfsDir()->path();
|
||||
auto importPathsStr = importPaths.join(u':');
|
||||
|
||||
QString qmllsConfig;
|
||||
auto print = QDebug(&qmllsConfig).nospace();
|
||||
print << "[General]\nno-cmake-calls=true\nbuildDir=" << vfsPath
|
||||
<< "\nimportPaths=" << importPathsStr << '\n';
|
||||
|
||||
return qmllsConfig;
|
||||
}();
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
bool QmlToolingSupport::updateQmllsConfig(const QDir& configRoot, bool create) {
|
||||
auto shellConfigPath = configRoot.filePath(".qmlls.ini");
|
||||
auto vfsConfigPath = QsPaths::instance()->shellVfsDir()->filePath(".qmlls.ini");
|
||||
|
||||
auto shellFileInfo = QFileInfo(shellConfigPath);
|
||||
if (!create && !shellFileInfo.exists() && !shellFileInfo.isSymLink()) {
|
||||
if (QmlToolingSupport::toolingEnabled) {
|
||||
qInfo() << "QML tooling support disabled";
|
||||
QmlToolingSupport::toolingEnabled = false;
|
||||
} else {
|
||||
qCInfo(logTooling) << "Not enabling QML tooling support, qmlls.ini is missing at path"
|
||||
<< shellConfigPath;
|
||||
}
|
||||
|
||||
QFile::remove(vfsConfigPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto vfsFile = QFile(vfsConfigPath);
|
||||
|
||||
if (!vfsFile.open(QFile::ReadWrite | QFile::Text)) {
|
||||
qCCritical(logTooling) << "Failed to create qmlls config in vfs";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto config = QmlToolingSupport::getQmllsConfig();
|
||||
|
||||
if (vfsFile.readAll() != config) {
|
||||
if (!vfsFile.resize(0) || !vfsFile.write(config.toUtf8())) {
|
||||
qCCritical(logTooling) << "Failed to write qmlls config in vfs";
|
||||
return false;
|
||||
}
|
||||
|
||||
qCDebug(logTooling) << "Wrote qmlls config in vfs";
|
||||
}
|
||||
|
||||
if (!shellFileInfo.isSymLink() || shellFileInfo.symLinkTarget() != vfsConfigPath) {
|
||||
QFile::remove(shellConfigPath);
|
||||
|
||||
if (!QFile::link(vfsConfigPath, shellConfigPath)) {
|
||||
qCCritical(logTooling) << "Failed to create qmlls config symlink";
|
||||
return false;
|
||||
}
|
||||
|
||||
qCDebug(logTooling) << "Created qmlls config symlink";
|
||||
}
|
||||
|
||||
if (!QmlToolingSupport::toolingEnabled) {
|
||||
qInfo() << "QML tooling support enabled";
|
||||
QmlToolingSupport::toolingEnabled = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void QmlToolingSupport::updateToolingFs(
|
||||
QmlScanner& scanner,
|
||||
const QDir& scanDir,
|
||||
const QDir& linkDir
|
||||
) {
|
||||
QList<QString> files;
|
||||
QSet<QString> subdirs;
|
||||
|
||||
auto scanPath = scanDir.path();
|
||||
|
||||
linkDir.mkpath(".");
|
||||
|
||||
for (auto& path: scanner.scannedFiles) {
|
||||
if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue;
|
||||
auto name = path.sliced(scanPath.length() + 1);
|
||||
|
||||
if (name.contains('/')) {
|
||||
auto dirname = name.first(name.indexOf('/'));
|
||||
subdirs.insert(dirname);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto fileInfo = QFileInfo(path);
|
||||
if (!fileInfo.isFile()) continue;
|
||||
|
||||
auto spath = linkDir.filePath(name);
|
||||
auto sFileInfo = QFileInfo(spath);
|
||||
|
||||
if (!sFileInfo.isSymLink() || sFileInfo.symLinkTarget() != path) {
|
||||
QFile::remove(spath);
|
||||
|
||||
if (QFile::link(path, spath)) {
|
||||
qCDebug(logTooling) << "Created symlink to" << path << "at" << spath;
|
||||
files.append(spath);
|
||||
} else {
|
||||
qCCritical(logTooling) << "Could not create symlink to" << path << "at" << spath;
|
||||
}
|
||||
} else {
|
||||
files.append(spath);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto [path, text]: scanner.fileIntercepts.asKeyValueRange()) {
|
||||
if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue;
|
||||
auto name = path.sliced(scanPath.length() + 1);
|
||||
|
||||
if (name.contains('/')) {
|
||||
auto dirname = name.first(name.indexOf('/'));
|
||||
subdirs.insert(dirname);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto spath = linkDir.filePath(name);
|
||||
auto file = QFile(spath);
|
||||
if (!file.open(QFile::ReadWrite | QFile::Text)) {
|
||||
qCCritical(logTooling) << "Failed to open injected file" << spath;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.readAll() == text) {
|
||||
files.append(spath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.resize(0) && file.write(text.toUtf8())) {
|
||||
files.append(spath);
|
||||
qCDebug(logTooling) << "Wrote injected file" << spath;
|
||||
} else {
|
||||
qCCritical(logTooling) << "Failed to write injected file" << spath;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& name: linkDir.entryList(QDir::Files | QDir::System)) { // System = broken symlinks
|
||||
auto path = linkDir.filePath(name);
|
||||
|
||||
if (!files.contains(path)) {
|
||||
if (QFile::remove(path)) qCDebug(logTooling) << "Removed old file at" << path;
|
||||
else qCWarning(logTooling) << "Failed to remove old file at" << path;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& subdir: subdirs) {
|
||||
QmlToolingSupport::updateToolingFs(scanner, scanDir.filePath(subdir), linkDir.filePath(subdir));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace qs::core
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <qdir.h>
|
||||
|
||||
#include "scan.hpp"
|
||||
|
||||
namespace qs::core {
|
||||
|
||||
class QmlToolingSupport {
|
||||
public:
|
||||
static bool updateTooling(const QDir& configRoot, QmlScanner& scanner);
|
||||
|
||||
private:
|
||||
static QString getQmllsConfig();
|
||||
static bool lockTooling();
|
||||
static bool updateQmllsConfig(const QDir& configRoot, bool create);
|
||||
static void updateToolingFs(QmlScanner& scanner, const QDir& scanDir, const QDir& linkDir);
|
||||
static inline bool toolingEnabled = false;
|
||||
static inline QFile* toolingLock = nullptr;
|
||||
};
|
||||
|
||||
} // namespace qs::core
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#include "../core/instanceinfo.hpp"
|
||||
#include "../core/logcat.hpp"
|
||||
|
||||
extern char** environ; // NOLINT
|
||||
|
||||
@@ -23,7 +24,7 @@ using namespace google_breakpad;
|
||||
namespace qs::crash {
|
||||
|
||||
namespace {
|
||||
Q_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg);
|
||||
}
|
||||
|
||||
struct CrashHandlerPrivate {
|
||||
@@ -76,7 +77,11 @@ void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) {
|
||||
}
|
||||
|
||||
QFile file;
|
||||
file.open(this->d->infoFd, QFile::ReadWrite);
|
||||
|
||||
if (!file.open(this->d->infoFd, QFile::ReadWrite)) {
|
||||
qCCritical(logCrashHandler
|
||||
) << "Failed to open instance info memfd, crash recovery will not work.";
|
||||
}
|
||||
|
||||
QDataStream ds(&file);
|
||||
ds << info;
|
||||
|
||||
+9
-3
@@ -17,6 +17,7 @@
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "../core/instanceinfo.hpp"
|
||||
#include "../core/logcat.hpp"
|
||||
#include "../core/logging.hpp"
|
||||
#include "../core/paths.hpp"
|
||||
#include "build.hpp"
|
||||
@@ -24,7 +25,7 @@
|
||||
|
||||
namespace {
|
||||
|
||||
Q_LOGGING_CATEGORY(logCrashReporter, "quickshell.crashreporter", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logCrashReporter, "quickshell.crashreporter", QtWarningMsg);
|
||||
|
||||
int tryDup(int fd, const QString& path) {
|
||||
QFile sourceFile;
|
||||
@@ -151,7 +152,6 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) {
|
||||
void qsCheckCrash(int argc, char** argv) {
|
||||
auto fd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD");
|
||||
if (fd.isEmpty()) return;
|
||||
auto app = QApplication(argc, argv);
|
||||
|
||||
RelaunchInfo info;
|
||||
|
||||
@@ -161,7 +161,10 @@ void qsCheckCrash(int argc, char** argv) {
|
||||
auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt();
|
||||
|
||||
QFile file;
|
||||
file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle);
|
||||
if (!file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle)) {
|
||||
qFatal() << "Failed to open instance info fd.";
|
||||
}
|
||||
|
||||
file.seek(0);
|
||||
|
||||
auto ds = QDataStream(&file);
|
||||
@@ -176,6 +179,9 @@ void qsCheckCrash(int argc, char** argv) {
|
||||
info.logRules
|
||||
);
|
||||
|
||||
auto app = QApplication(argc, argv);
|
||||
QApplication::setDesktopFileName("org.quickshell");
|
||||
|
||||
auto crashDir = QsPaths::crashDir(info.instance.instanceId);
|
||||
|
||||
qCInfo(logCrashReporter) << "Starting crash reporter...";
|
||||
|
||||
@@ -2,13 +2,24 @@ set_source_files_properties(org.freedesktop.DBus.Properties.xml PROPERTIES
|
||||
CLASSNAME DBusPropertiesInterface
|
||||
)
|
||||
|
||||
set_source_files_properties(org.freedesktop.DBus.ObjectManager.xml PROPERTIES
|
||||
CLASSNAME DBusObjectManagerInterface
|
||||
INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/dbus_objectmanager_types.hpp
|
||||
)
|
||||
|
||||
qt_add_dbus_interface(DBUS_INTERFACES
|
||||
org.freedesktop.DBus.Properties.xml
|
||||
dbus_properties
|
||||
)
|
||||
|
||||
qt_add_dbus_interface(DBUS_INTERFACES
|
||||
org.freedesktop.DBus.ObjectManager.xml
|
||||
dbus_objectmanager
|
||||
)
|
||||
|
||||
qt_add_library(quickshell-dbus STATIC
|
||||
properties.cpp
|
||||
objectmanager.cpp
|
||||
bus.cpp
|
||||
${DBUS_INTERFACES}
|
||||
)
|
||||
|
||||
+3
-1
@@ -12,10 +12,12 @@
|
||||
#include <qtypes.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
namespace {
|
||||
Q_LOGGING_CATEGORY(logDbus, "quickshell.dbus", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logDbus, "quickshell.dbus", QtWarningMsg);
|
||||
}
|
||||
|
||||
void tryLaunchService(
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <qdbusextratypes.h>
|
||||
#include <qhash.h>
|
||||
#include <qmap.h>
|
||||
#include <qstring.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
using DBusObjectManagerInterfaces = QHash<QString, QVariantMap>;
|
||||
using DBusObjectManagerObjects = QHash<QDBusObjectPath, DBusObjectManagerInterfaces>;
|
||||
@@ -21,13 +21,14 @@
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "../../core/iconimageprovider.hpp"
|
||||
#include "../../core/logcat.hpp"
|
||||
#include "../../core/model.hpp"
|
||||
#include "../../core/qsmenu.hpp"
|
||||
#include "../../dbus/properties.hpp"
|
||||
#include "dbus_menu.h"
|
||||
#include "dbus_menu_types.hpp"
|
||||
|
||||
Q_LOGGING_CATEGORY(logDbusMenu, "quickshell.dbus.dbusmenu", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logDbusMenu, "quickshell.dbus.dbusmenu", QtWarningMsg);
|
||||
|
||||
using namespace qs::menu;
|
||||
|
||||
@@ -182,7 +183,7 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString
|
||||
}
|
||||
} else if (removed.isEmpty() || removed.contains("icon-data")) {
|
||||
imageChanged = this->image.hasData();
|
||||
image.data.clear();
|
||||
this->image.data.clear();
|
||||
}
|
||||
|
||||
auto type = properties.value("type");
|
||||
@@ -311,8 +312,8 @@ void DBusMenu::prepareToShow(qint32 item, qint32 depth) {
|
||||
auto responseCallback = [this, item, depth](QDBusPendingCallWatcher* call) {
|
||||
const QDBusPendingReply<bool> reply = *call;
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDbusMenu) << "Error in AboutToShow, but showing anyway for menu" << item << "of"
|
||||
<< this << reply.error();
|
||||
qCDebug(logDbusMenu) << "Error in AboutToShow, but showing anyway for menu" << item << "of"
|
||||
<< this << reply.error();
|
||||
}
|
||||
|
||||
this->updateLayout(item, depth);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#include "../properties.hpp"
|
||||
#include "dbus_menu_types.hpp"
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logDbusMenu);
|
||||
QS_DECLARE_LOGGING_CATEGORY(logDbusMenu);
|
||||
|
||||
class DBusMenuInterface;
|
||||
|
||||
@@ -36,7 +36,7 @@ class DBusMenuPngImage: public QsIndexedImageHandle {
|
||||
public:
|
||||
explicit DBusMenuPngImage(): QsIndexedImageHandle(QQuickImageProvider::Image) {}
|
||||
|
||||
[[nodiscard]] bool hasData() const { return !data.isEmpty(); }
|
||||
[[nodiscard]] bool hasData() const { return !this->data.isEmpty(); }
|
||||
QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override;
|
||||
|
||||
QByteArray data;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#include "objectmanager.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdbusconnection.h>
|
||||
#include <qdbusmetatype.h>
|
||||
#include <qdbuspendingcall.h>
|
||||
#include <qdbuspendingreply.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "dbus_objectmanager.h"
|
||||
#include "dbus_objectmanager_types.hpp"
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logDbusObjectManager, "quickshell.dbus.objectmanager", QtWarningMsg);
|
||||
}
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
DBusObjectManager::DBusObjectManager(QObject* parent): QObject(parent) {
|
||||
qDBusRegisterMetaType<DBusObjectManagerInterfaces>();
|
||||
qDBusRegisterMetaType<DBusObjectManagerObjects>();
|
||||
}
|
||||
|
||||
bool DBusObjectManager::setInterface(
|
||||
const QString& service,
|
||||
const QString& path,
|
||||
const QDBusConnection& connection
|
||||
) {
|
||||
delete this->mInterface;
|
||||
this->mInterface = new DBusObjectManagerInterface(service, path, connection, this);
|
||||
|
||||
if (!this->mInterface->isValid()) {
|
||||
qCWarning(logDbusObjectManager) << "Failed to create DBusObjectManagerInterface for" << service
|
||||
<< path << ":" << this->mInterface->lastError();
|
||||
delete this->mInterface;
|
||||
this->mInterface = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
QObject::connect(
|
||||
this->mInterface,
|
||||
&DBusObjectManagerInterface::InterfacesAdded,
|
||||
this,
|
||||
&DBusObjectManager::interfacesAdded
|
||||
);
|
||||
|
||||
QObject::connect(
|
||||
this->mInterface,
|
||||
&DBusObjectManagerInterface::InterfacesRemoved,
|
||||
this,
|
||||
&DBusObjectManager::interfacesRemoved
|
||||
);
|
||||
|
||||
this->fetchInitialObjects();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DBusObjectManager::fetchInitialObjects() {
|
||||
if (!this->mInterface) return;
|
||||
|
||||
auto reply = this->mInterface->GetManagedObjects();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<DBusObjectManagerObjects> reply = *watcher;
|
||||
watcher->deleteLater();
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDbusObjectManager) << "Failed to get managed objects:" << reply.error();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& [path, interfaces]: reply.value().asKeyValueRange()) {
|
||||
emit this->interfacesAdded(path, interfaces);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace qs::dbus
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <qdbusconnection.h>
|
||||
#include <qobject.h>
|
||||
#include <qstring.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "dbus_objectmanager_types.hpp"
|
||||
|
||||
class DBusObjectManagerInterface;
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
class DBusObjectManager: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit DBusObjectManager(QObject* parent = nullptr);
|
||||
|
||||
bool setInterface(
|
||||
const QString& service,
|
||||
const QString& path,
|
||||
const QDBusConnection& connection = QDBusConnection::sessionBus()
|
||||
);
|
||||
|
||||
signals:
|
||||
void
|
||||
interfacesAdded(const QDBusObjectPath& objectPath, const DBusObjectManagerInterfaces& interfaces);
|
||||
void interfacesRemoved(const QDBusObjectPath& objectPath, const QStringList& interfaces);
|
||||
|
||||
private:
|
||||
void fetchInitialObjects();
|
||||
|
||||
DBusObjectManagerInterface* mInterface = nullptr;
|
||||
};
|
||||
|
||||
} // namespace qs::dbus
|
||||
@@ -0,0 +1,18 @@
|
||||
<node>
|
||||
<interface name="org.freedesktop.DBus.ObjectManager">
|
||||
<method name="GetManagedObjects">
|
||||
<arg name="objects" direction="out" type="a{oa{sa{sv}}}"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="DBusObjectManagerObjects"/>
|
||||
</method>
|
||||
|
||||
<signal name="InterfacesAdded">
|
||||
<arg name="object" type="o"/>
|
||||
<arg name="interfaces" type="a{sa{sv}}"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="DBusObjectManagerInterfaces"/>
|
||||
</signal>
|
||||
<signal name="InterfacesRemoved">
|
||||
<arg name="object" type="o"/>
|
||||
<arg name="interfaces" type="as"/>
|
||||
</signal>
|
||||
</interface>
|
||||
</node>
|
||||
+17
-3
@@ -17,11 +17,13 @@
|
||||
#include <qmetatype.h>
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtversionchecks.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "dbus_properties.h"
|
||||
|
||||
Q_LOGGING_CATEGORY(logDbusProperties, "quickshell.dbus.properties", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logDbusProperties, "quickshell.dbus.properties", QtWarningMsg);
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
@@ -244,8 +246,13 @@ void DBusPropertyGroup::requestPropertyUpdate(DBusPropertyCore* property) {
|
||||
const QDBusPendingReply<QDBusVariant> reply = *call;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr;
|
||||
qCWarning(logDbusProperties) << reply.error();
|
||||
if (!property->isRequired() && reply.error().type() == QDBusError::InvalidArgs) {
|
||||
qCDebug(logDbusProperties) << "Error updating non-required property" << propStr;
|
||||
qCDebug(logDbusProperties) << reply.error();
|
||||
} else {
|
||||
qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr;
|
||||
qCWarning(logDbusProperties) << reply.error();
|
||||
}
|
||||
} else {
|
||||
this->tryUpdateProperty(property, reply.value().variant());
|
||||
}
|
||||
@@ -326,3 +333,10 @@ void DBusPropertyGroup::onPropertiesChanged(
|
||||
}
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
|
||||
QDebug operator<<(QDebug debug, const QDBusObjectPath& path) {
|
||||
debug.nospace() << "QDBusObjectPath(" << path.path() << ")";
|
||||
return debug;
|
||||
}
|
||||
#endif
|
||||
+11
-5
@@ -20,13 +20,15 @@
|
||||
#include <qstringview.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtversionchecks.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "../core/util.hpp"
|
||||
|
||||
class DBusPropertiesInterface;
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logDbusProperties);
|
||||
QS_DECLARE_LOGGING_CATEGORY(logDbusProperties);
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
@@ -166,9 +168,9 @@ class DBusBindableProperty: public DBusPropertyCore {
|
||||
public:
|
||||
explicit DBusBindableProperty() { this->group()->attachProperty(this); }
|
||||
|
||||
[[nodiscard]] QString name() const override { return Name; };
|
||||
[[nodiscard]] QStringView nameRef() const override { return Name; };
|
||||
[[nodiscard]] bool isRequired() const override { return required; };
|
||||
[[nodiscard]] QString name() const override { return Name; }
|
||||
[[nodiscard]] QStringView nameRef() const override { return Name; }
|
||||
[[nodiscard]] bool isRequired() const override { return required; }
|
||||
|
||||
[[nodiscard]] QString valueString() override {
|
||||
QString str;
|
||||
@@ -234,6 +236,7 @@ public:
|
||||
void attachProperty(DBusPropertyCore* property);
|
||||
void updateAllDirect();
|
||||
void updateAllViaGetAll();
|
||||
void updatePropertySet(const QVariantMap& properties, bool complainMissing = true);
|
||||
[[nodiscard]] QString toString() const;
|
||||
[[nodiscard]] bool isConnected() const { return this->interface; }
|
||||
|
||||
@@ -252,7 +255,6 @@ private slots:
|
||||
);
|
||||
|
||||
private:
|
||||
void updatePropertySet(const QVariantMap& properties, bool complainMissing);
|
||||
void tryUpdateProperty(DBusPropertyCore* property, const QVariant& variant) const;
|
||||
[[nodiscard]] QString propertyString(const DBusPropertyCore* property) const;
|
||||
|
||||
@@ -265,6 +267,10 @@ private:
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
|
||||
QDebug operator<<(QDebug debug, const QDBusObjectPath& path);
|
||||
#endif
|
||||
|
||||
// NOLINTBEGIN
|
||||
#define QS_DBUS_BINDABLE_PROPERTY_GROUP(Class, name) qs::dbus::DBusPropertyGroup name {this};
|
||||
|
||||
|
||||
+3
-1
@@ -11,10 +11,12 @@
|
||||
#include <qquickwindow.h>
|
||||
#include <qstringliteral.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
|
||||
namespace qs::debug {
|
||||
|
||||
namespace {
|
||||
Q_LOGGING_CATEGORY(logLint, "quickshell.linter", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logLint, "quickshell.linter", QtWarningMsg);
|
||||
|
||||
void lintZeroSized(QQuickItem* item);
|
||||
bool isRenderable(QQuickItem* item);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
qt_add_library(quickshell-io STATIC
|
||||
datastream.cpp
|
||||
processcore.cpp
|
||||
process.cpp
|
||||
fileview.cpp
|
||||
jsonadapter.cpp
|
||||
@@ -20,6 +21,7 @@ qt_add_qml_module(quickshell-io
|
||||
FileView.qml
|
||||
)
|
||||
|
||||
qs_add_module_deps_light(quickshell-io Quickshell)
|
||||
install_qml_module(quickshell-io)
|
||||
|
||||
target_link_libraries(quickshell-io PRIVATE Qt::Quick)
|
||||
|
||||
@@ -55,7 +55,7 @@ public:
|
||||
|
||||
// the buffer will be sent in both slots if there is data remaining from a previous parser
|
||||
virtual void parseBytes(QByteArray& incoming, QByteArray& buffer) = 0;
|
||||
virtual void streamEnded(QByteArray& /*buffer*/) {};
|
||||
virtual void streamEnded(QByteArray& /*buffer*/) {}
|
||||
|
||||
signals:
|
||||
/// Emitted when data is read from the stream.
|
||||
@@ -63,7 +63,7 @@ signals:
|
||||
};
|
||||
|
||||
///! DataStreamParser for delimited data streams.
|
||||
/// DataStreamParser for delimited data streams. @@read() is emitted once per delimited chunk of the stream.
|
||||
/// DataStreamParser for delimited data streams. @@DataStreamParser.read(s) is emitted once per delimited chunk of the stream.
|
||||
class SplitParser: public DataStreamParser {
|
||||
Q_OBJECT;
|
||||
/// The delimiter for parsed data. May be multiple characters. Defaults to `\n`.
|
||||
|
||||
+9
-4
@@ -20,12 +20,13 @@
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "../core/util.hpp"
|
||||
|
||||
namespace qs::io {
|
||||
|
||||
namespace {
|
||||
Q_LOGGING_CATEGORY(logFileView, "quickshell.io.fileview", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logFileView, "quickshell.io.fileview", QtWarningMsg);
|
||||
}
|
||||
|
||||
QString FileViewError::toString(FileViewError::Enum value) {
|
||||
@@ -92,7 +93,8 @@ void FileViewReader::run() {
|
||||
FileViewReader::read(this->owner, this->state, this->doStringConversion, this->shouldCancel);
|
||||
|
||||
if (this->shouldCancel.loadAcquire()) {
|
||||
qCDebug(logFileView) << "Read" << this << "of" << state.path << "canceled for" << this->owner;
|
||||
qCDebug(logFileView) << "Read" << this << "of" << this->state.path << "canceled for"
|
||||
<< this->owner;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +207,7 @@ void FileViewWriter::run() {
|
||||
FileViewWriter::write(this->owner, this->state, this->doAtomicWrite, this->shouldCancel);
|
||||
|
||||
if (this->shouldCancel.loadAcquire()) {
|
||||
qCDebug(logFileView) << "Write" << this << "of" << state.path << "canceled for"
|
||||
qCDebug(logFileView) << "Write" << this << "of" << this->state.path << "canceled for"
|
||||
<< this->owner;
|
||||
}
|
||||
}
|
||||
@@ -492,7 +494,10 @@ void FileView::updatePath() {
|
||||
void FileView::updateWatchedFiles() {
|
||||
// If inotify events are sent to the watcher after deletion and deleteLater
|
||||
// isn't used, a use after free in the QML engine will occur.
|
||||
if (this->watcher) this->watcher->deleteLater();
|
||||
if (this->watcher) {
|
||||
this->watcher->deleteLater();
|
||||
this->watcher = nullptr;
|
||||
}
|
||||
|
||||
if (!this->targetPath.isEmpty() && this->bWatchChanges) {
|
||||
qCDebug(logFileView) << "Creating watcher for" << this << "at" << this->targetPath;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdebug.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qmetaobject.h>
|
||||
#include <qobject.h>
|
||||
#include <qobjectdefs.h>
|
||||
@@ -14,10 +15,15 @@
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../core/generation.hpp"
|
||||
#include "../core/logcat.hpp"
|
||||
#include "ipc.hpp"
|
||||
|
||||
namespace qs::io::ipc {
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logIpcHandler, "quickshell.ipchandler", QtWarningMsg)
|
||||
}
|
||||
|
||||
bool IpcFunction::resolve(QString& error) {
|
||||
if (this->method.parameterCount() > 10) {
|
||||
error = "Due to technical limitations, IPC functions can only have 10 arguments.";
|
||||
@@ -210,6 +216,7 @@ IpcHandlerRegistry* IpcHandlerRegistry::forGeneration(EngineGeneration* generati
|
||||
if (!ext) {
|
||||
ext = new IpcHandlerRegistry();
|
||||
generation->registerExtension(&key, ext);
|
||||
qCDebug(logIpcHandler) << "Created new IPC handler registry" << ext << "for" << generation;
|
||||
}
|
||||
|
||||
return dynamic_cast<IpcHandlerRegistry*>(ext);
|
||||
@@ -232,10 +239,12 @@ void IpcHandler::updateRegistration(bool destroying) {
|
||||
|
||||
if (this->registeredState.enabled) {
|
||||
registry->deregisterHandler(this);
|
||||
qCDebug(logIpcHandler) << "Deregistered" << this << "from registry" << registry;
|
||||
}
|
||||
|
||||
if (this->targetState.enabled && !this->targetState.target.isEmpty()) {
|
||||
registry->registerHandler(this);
|
||||
qCDebug(logIpcHandler) << "Registered" << this << "to registry" << registry;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,9 +154,7 @@ class IpcHandlerRegistry;
|
||||
/// #### Properties
|
||||
/// Properties of an IpcHanlder can be read using `qs ipc prop get` as long as they are
|
||||
/// of an IPC compatible type. See the table above for compatible types.
|
||||
class IpcHandler
|
||||
: public QObject
|
||||
, public PostReloadHook {
|
||||
class IpcHandler: public PostReloadHook {
|
||||
Q_OBJECT;
|
||||
/// If the handler should be able to receive calls. Defaults to true.
|
||||
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
|
||||
@@ -166,7 +164,7 @@ class IpcHandler
|
||||
QML_ELEMENT;
|
||||
|
||||
public:
|
||||
explicit IpcHandler(QObject* parent = nullptr): QObject(parent) {};
|
||||
explicit IpcHandler(QObject* parent = nullptr): PostReloadHook(parent) {}
|
||||
~IpcHandler() override;
|
||||
Q_DISABLE_COPY_MOVE(IpcHandler);
|
||||
|
||||
|
||||
+10
-10
@@ -44,7 +44,7 @@ void JsonAdapter::deserializeAdapter(const QByteArray& data) {
|
||||
|
||||
this->deserializeRec(json.object(), this, &JsonAdapter::staticMetaObject);
|
||||
|
||||
for (auto* object: oldCreatedObjects) {
|
||||
for (auto* object: this->oldCreatedObjects) {
|
||||
delete object; // FIXME: QMetaType::destroy?
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ void JsonAdapter::deserializeAdapter(const QByteArray& data) {
|
||||
|
||||
void JsonAdapter::connectNotifiers() {
|
||||
auto notifySlot = JsonAdapter::staticMetaObject.indexOfSlot("onPropertyChanged()");
|
||||
connectNotifiersRec(notifySlot, this, &JsonAdapter::staticMetaObject);
|
||||
this->connectNotifiersRec(notifySlot, this, &JsonAdapter::staticMetaObject);
|
||||
}
|
||||
|
||||
void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaObject* base) {
|
||||
@@ -71,7 +71,7 @@ void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaO
|
||||
auto val = prop.read(obj);
|
||||
if (val.canView<JsonObject*>()) {
|
||||
auto* pobj = prop.read(obj).view<JsonObject*>();
|
||||
if (pobj) connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject);
|
||||
if (pobj) this->connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject);
|
||||
} else if (val.canConvert<QQmlListProperty<JsonObject>>()) {
|
||||
auto listVal = val.value<QQmlListProperty<JsonObject>>();
|
||||
|
||||
@@ -79,7 +79,7 @@ void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaO
|
||||
for (auto i = 0; i != len; i++) {
|
||||
auto* pobj = listVal.at(&listVal, i);
|
||||
|
||||
if (pobj) connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject);
|
||||
if (pobj) this->connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,7 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas
|
||||
auto* pobj = val.view<JsonObject*>();
|
||||
|
||||
if (pobj) {
|
||||
json.insert(prop.name(), serializeRec(pobj, &JsonObject::staticMetaObject));
|
||||
json.insert(prop.name(), this->serializeRec(pobj, &JsonObject::staticMetaObject));
|
||||
} else {
|
||||
json.insert(prop.name(), QJsonValue::Null);
|
||||
}
|
||||
@@ -124,7 +124,7 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas
|
||||
auto* pobj = listVal.at(&listVal, i);
|
||||
|
||||
if (pobj) {
|
||||
array.push_back(serializeRec(pobj, &JsonObject::staticMetaObject));
|
||||
array.push_back(this->serializeRec(pobj, &JsonObject::staticMetaObject));
|
||||
} else {
|
||||
array.push_back(QJsonValue::Null);
|
||||
}
|
||||
@@ -178,8 +178,8 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM
|
||||
|
||||
currentValue->setParent(this);
|
||||
this->createdObjects.push_back(currentValue);
|
||||
} else if (oldCreatedObjects.removeOne(currentValue)) {
|
||||
createdObjects.push_back(currentValue);
|
||||
} else if (this->oldCreatedObjects.removeOne(currentValue)) {
|
||||
this->createdObjects.push_back(currentValue);
|
||||
}
|
||||
|
||||
this->deserializeRec(jval.toObject(), currentValue, &JsonObject::staticMetaObject);
|
||||
@@ -212,8 +212,8 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM
|
||||
if (jsonValue.isObject()) {
|
||||
if (isNew) {
|
||||
currentValue = lp.at(&lp, i);
|
||||
if (oldCreatedObjects.removeOne(currentValue)) {
|
||||
createdObjects.push_back(currentValue);
|
||||
if (this->oldCreatedObjects.removeOne(currentValue)) {
|
||||
this->createdObjects.push_back(currentValue);
|
||||
}
|
||||
} else {
|
||||
// FIXME: should be the type inside the QQmlListProperty but how can we get that?
|
||||
|
||||
@@ -91,6 +91,7 @@ class JsonAdapter
|
||||
, public QQmlParserStatus {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
Q_INTERFACES(QQmlParserStatus);
|
||||
|
||||
public:
|
||||
void classBegin() override {}
|
||||
|
||||
+57
-34
@@ -3,9 +3,9 @@
|
||||
#include <utility>
|
||||
|
||||
#include <qdir.h>
|
||||
#include <qhash.h>
|
||||
#include <qlist.h>
|
||||
#include <qlogging.h>
|
||||
#include <qmap.h>
|
||||
#include <qobject.h>
|
||||
#include <qprocess.h>
|
||||
#include <qqmlinfo.h>
|
||||
@@ -13,12 +13,13 @@
|
||||
#include <qtypes.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "../core/common.hpp"
|
||||
#include "../core/generation.hpp"
|
||||
#include "../core/qmlglobal.hpp"
|
||||
#include "../core/reload.hpp"
|
||||
#include "datastream.hpp"
|
||||
#include "processcore.hpp"
|
||||
|
||||
Process::Process(QObject* parent): QObject(parent) {
|
||||
Process::Process(QObject* parent): PostReloadHook(parent) {
|
||||
QObject::connect(
|
||||
QuickshellSettings::instance(),
|
||||
&QuickshellSettings::workingDirectoryChanged,
|
||||
@@ -27,6 +28,18 @@ Process::Process(QObject* parent): QObject(parent) {
|
||||
);
|
||||
}
|
||||
|
||||
Process::~Process() {
|
||||
if (this->process != nullptr && this->process->processId() != 0) {
|
||||
// Deleting after the process finishes hides the process destroyed warning in logs
|
||||
QObject::connect(this->process, &QProcess::finished, [p = this->process] { delete p; });
|
||||
|
||||
this->process->setParent(nullptr);
|
||||
this->process->kill();
|
||||
}
|
||||
}
|
||||
|
||||
void Process::onPostReload() { this->startProcessIfReady(); }
|
||||
|
||||
bool Process::isRunning() const { return this->process != nullptr; }
|
||||
|
||||
void Process::setRunning(bool running) {
|
||||
@@ -46,17 +59,7 @@ void Process::setCommand(QList<QString> command) {
|
||||
if (this->mCommand == command) return;
|
||||
this->mCommand = std::move(command);
|
||||
|
||||
auto& cmd = this->mCommand.first();
|
||||
if (cmd.startsWith("file://")) {
|
||||
cmd = cmd.sliced(7);
|
||||
} else if (cmd.startsWith("root://")) {
|
||||
cmd = cmd.sliced(7);
|
||||
auto& root = EngineGeneration::findObjectGeneration(this)->rootPath;
|
||||
cmd = root.filePath(cmd.startsWith('/') ? cmd.sliced(1) : cmd);
|
||||
}
|
||||
|
||||
emit this->commandChanged();
|
||||
|
||||
this->startProcessIfReady();
|
||||
}
|
||||
|
||||
@@ -79,9 +82,9 @@ void Process::onGlobalWorkingDirectoryChanged() {
|
||||
}
|
||||
}
|
||||
|
||||
QMap<QString, QVariant> Process::environment() const { return this->mEnvironment; }
|
||||
QHash<QString, QVariant> Process::environment() const { return this->mEnvironment; }
|
||||
|
||||
void Process::setEnvironment(QMap<QString, QVariant> environment) {
|
||||
void Process::setEnvironment(QHash<QString, QVariant> environment) {
|
||||
if (environment == this->mEnvironment) return;
|
||||
this->mEnvironment = std::move(environment);
|
||||
emit this->environmentChanged();
|
||||
@@ -175,10 +178,21 @@ void Process::setStdinEnabled(bool enabled) {
|
||||
}
|
||||
|
||||
void Process::startProcessIfReady() {
|
||||
if (this->process != nullptr || !this->targetRunning || this->mCommand.isEmpty()) return;
|
||||
if (this->process != nullptr || !this->isPostReload || !this->targetRunning
|
||||
|| this->mCommand.isEmpty())
|
||||
return;
|
||||
|
||||
this->targetRunning = false;
|
||||
|
||||
auto& cmd = this->mCommand.first();
|
||||
if (cmd.startsWith("file://")) {
|
||||
cmd = cmd.sliced(7);
|
||||
} else if (cmd.startsWith("root://")) {
|
||||
cmd = cmd.sliced(7);
|
||||
auto& root = EngineGeneration::findObjectGeneration(this)->rootPath;
|
||||
cmd = root.filePath(cmd.startsWith('/') ? cmd.sliced(1) : cmd);
|
||||
}
|
||||
|
||||
auto args = this->mCommand.sliced(1);
|
||||
|
||||
this->process = new QProcess(this);
|
||||
@@ -202,6 +216,25 @@ void Process::startProcessIfReady() {
|
||||
this->process->start(cmd, args);
|
||||
}
|
||||
|
||||
void Process::exec(QList<QString> command) {
|
||||
this->exec(qs::io::process::ProcessContext(std::move(command)));
|
||||
}
|
||||
|
||||
void Process::exec(const qs::io::process::ProcessContext& context) {
|
||||
this->setRunning(false);
|
||||
if (context.commandSet) this->setCommand(context.command);
|
||||
if (context.environmentSet) this->setEnvironment(context.environment);
|
||||
if (context.clearEnvironmentSet) this->setEnvironmentCleared(context.clearEnvironment);
|
||||
if (context.workingDirectorySet) this->setWorkingDirectory(context.workingDirectory);
|
||||
|
||||
if (this->mCommand.isEmpty()) {
|
||||
qmlWarning(this) << "Cannot start process as command is empty.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->setRunning(true);
|
||||
}
|
||||
|
||||
void Process::startDetached() {
|
||||
if (this->mCommand.isEmpty()) {
|
||||
qmlWarning(this) << "Cannot start process as command is empty.";
|
||||
@@ -216,6 +249,11 @@ void Process::startDetached() {
|
||||
this->setupEnvironment(&process);
|
||||
process.setProgram(cmd);
|
||||
process.setArguments(args);
|
||||
|
||||
process.setStandardInputFile(QProcess::nullDevice());
|
||||
process.setStandardOutputFile(QProcess::nullDevice());
|
||||
process.setStandardErrorFile(QProcess::nullDevice());
|
||||
|
||||
process.startDetached();
|
||||
}
|
||||
|
||||
@@ -224,24 +262,7 @@ void Process::setupEnvironment(QProcess* process) {
|
||||
process->setWorkingDirectory(this->mWorkingDirectory);
|
||||
}
|
||||
|
||||
const auto& sysenv = qs::Common::INITIAL_ENVIRONMENT;
|
||||
auto env = this->mClearEnvironment ? QProcessEnvironment() : sysenv;
|
||||
|
||||
for (auto& name: this->mEnvironment.keys()) {
|
||||
auto value = this->mEnvironment.value(name);
|
||||
if (!value.isValid()) continue;
|
||||
|
||||
if (this->mClearEnvironment) {
|
||||
if (value.isNull()) {
|
||||
if (sysenv.contains(name)) env.insert(name, sysenv.value(name));
|
||||
} else env.insert(name, value.toString());
|
||||
} else {
|
||||
if (value.isNull()) env.remove(name);
|
||||
else env.insert(name, value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
process->setProcessEnvironment(env);
|
||||
qs::io::process::setupProcessEnvironment(process, this->mClearEnvironment, this->mEnvironment);
|
||||
}
|
||||
|
||||
void Process::onStarted() {
|
||||
@@ -261,6 +282,8 @@ void Process::onFinished(qint32 exitCode, QProcess::ExitStatus exitStatus) {
|
||||
emit this->exited(exitCode, exitStatus);
|
||||
emit this->runningChanged();
|
||||
emit this->processIdChanged();
|
||||
|
||||
this->startProcessIfReady(); // for `running = false; running = true`
|
||||
}
|
||||
|
||||
void Process::onErrorOccurred(QProcess::ProcessError error) {
|
||||
|
||||
+50
-6
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qhash.h>
|
||||
#include <qobject.h>
|
||||
#include <qprocess.h>
|
||||
#include <qqmlintegration.h>
|
||||
@@ -9,7 +10,10 @@
|
||||
#include <qtypes.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "../core/doc.hpp"
|
||||
#include "../core/reload.hpp"
|
||||
#include "datastream.hpp"
|
||||
#include "processcore.hpp"
|
||||
|
||||
// Needed when compiling with clang musl-libc++.
|
||||
// Default include paths contain macros that cause name collisions.
|
||||
@@ -27,7 +31,7 @@
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
class Process: public QObject {
|
||||
class Process: public PostReloadHook {
|
||||
Q_OBJECT;
|
||||
// clang-format off
|
||||
/// If the process is currently running. Defaults to false.
|
||||
@@ -98,7 +102,7 @@ class Process: public QObject {
|
||||
/// If the process is already running changing this property will affect the next
|
||||
/// started process. If the property has been changed after starting a process it will
|
||||
/// return the new value, not the one for the currently running process.
|
||||
Q_PROPERTY(QMap<QString, QVariant> environment READ environment WRITE setEnvironment NOTIFY environmentChanged);
|
||||
Q_PROPERTY(QHash<QString, QVariant> environment READ environment WRITE setEnvironment NOTIFY environmentChanged);
|
||||
/// If the process's environment should be cleared prior to applying @@environment.
|
||||
/// Defaults to false.
|
||||
///
|
||||
@@ -133,6 +137,44 @@ class Process: public QObject {
|
||||
|
||||
public:
|
||||
explicit Process(QObject* parent = nullptr);
|
||||
~Process() override;
|
||||
Q_DISABLE_COPY_MOVE(Process);
|
||||
|
||||
void onPostReload() override;
|
||||
|
||||
// MUST be before exec(ctx) or the other will be called with a default constructed obj.
|
||||
QSDOC_HIDE Q_INVOKABLE void exec(QList<QString> command);
|
||||
/// Launch a process with the given arguments, stopping any currently running process.
|
||||
///
|
||||
/// The context parameter can either be a list of command arguments or a JS object with the following fields:
|
||||
/// - `command`: A list containing the command and all its arguments. See @@Quickshell.Io.Process.command.
|
||||
/// - `environment`: Changes to make to the process environment. See @@Quickshell.Io.Process.environment.
|
||||
/// - `clearEnvironment`: Removes all variables from the environment if true.
|
||||
/// - `workingDirectory`: The working directory the command should run in.
|
||||
///
|
||||
/// Passed parameters will change the values currently set in the process.
|
||||
///
|
||||
/// > [!WARNING] This does not run command in a shell. All arguments to the command
|
||||
/// > must be in separate values in the list, e.g. `["echo", "hello"]`
|
||||
/// > and not `["echo hello"]`.
|
||||
/// >
|
||||
/// > Additionally, shell scripts must be run by your shell,
|
||||
/// > e.g. `["sh", "script.sh"]` instead of `["script.sh"]` unless the script
|
||||
/// > has a shebang.
|
||||
///
|
||||
/// > [!INFO] You can use `["sh", "-c", <your command>]` to execute your command with
|
||||
/// > the system shell.
|
||||
///
|
||||
/// Calling this function is equivalent to running:
|
||||
/// ```qml
|
||||
/// process.running = false;
|
||||
/// process.command = ...
|
||||
/// process.environment = ...
|
||||
/// process.clearEnvironment = ...
|
||||
/// process.workingDirectory = ...
|
||||
/// process.running = true;
|
||||
/// ```
|
||||
Q_INVOKABLE void exec(const qs::io::process::ProcessContext& context);
|
||||
|
||||
/// Sends a signal to the process if @@running is true, otherwise does nothing.
|
||||
Q_INVOKABLE void signal(qint32 signal);
|
||||
@@ -140,10 +182,12 @@ public:
|
||||
/// Writes to the process's stdin. Does nothing if @@running is false.
|
||||
Q_INVOKABLE void write(const QString& data);
|
||||
|
||||
/// Launches an instance of the process detached from quickshell.
|
||||
/// Launches an instance of the process detached from Quickshell.
|
||||
///
|
||||
/// The subprocess will not be tracked, @@running will be false,
|
||||
/// and the subprocess will not be killed by Quickshell.
|
||||
///
|
||||
/// This function is equivalent to @@Quickshell.Quickshell.execDetached().
|
||||
Q_INVOKABLE void startDetached();
|
||||
|
||||
[[nodiscard]] bool isRunning() const;
|
||||
@@ -157,8 +201,8 @@ public:
|
||||
[[nodiscard]] QString workingDirectory() const;
|
||||
void setWorkingDirectory(const QString& workingDirectory);
|
||||
|
||||
[[nodiscard]] QMap<QString, QVariant> environment() const;
|
||||
void setEnvironment(QMap<QString, QVariant> environment);
|
||||
[[nodiscard]] QHash<QString, QVariant> environment() const;
|
||||
void setEnvironment(QHash<QString, QVariant> environment);
|
||||
|
||||
[[nodiscard]] bool environmentCleared() const;
|
||||
void setEnvironmentCleared(bool cleared);
|
||||
@@ -203,7 +247,7 @@ private:
|
||||
QProcess* process = nullptr;
|
||||
QList<QString> mCommand;
|
||||
QString mWorkingDirectory;
|
||||
QMap<QString, QVariant> mEnvironment;
|
||||
QHash<QString, QVariant> mEnvironment;
|
||||
DataStreamParser* mStdoutParser = nullptr;
|
||||
DataStreamParser* mStderrParser = nullptr;
|
||||
QByteArray stdoutBuffer;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "processcore.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qhash.h>
|
||||
#include <qprocess.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "../core/common.hpp"
|
||||
|
||||
namespace qs::io::process {
|
||||
|
||||
void setupProcessEnvironment(
|
||||
QProcess* process,
|
||||
bool clear,
|
||||
const QHash<QString, QVariant>& envChanges
|
||||
) {
|
||||
const auto& sysenv = qs::Common::INITIAL_ENVIRONMENT;
|
||||
auto env = clear ? QProcessEnvironment() : sysenv;
|
||||
|
||||
for (auto& name: envChanges.keys()) {
|
||||
auto value = envChanges.value(name);
|
||||
if (!value.isValid()) continue;
|
||||
|
||||
if (clear) {
|
||||
if (value.isNull()) {
|
||||
if (sysenv.contains(name)) env.insert(name, sysenv.value(name));
|
||||
} else env.insert(name, value.toString());
|
||||
} else {
|
||||
if (value.isNull()) env.remove(name);
|
||||
else env.insert(name, value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
process->setProcessEnvironment(env);
|
||||
}
|
||||
|
||||
} // namespace qs::io::process
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qhash.h>
|
||||
#include <qlist.h>
|
||||
#include <qprocess.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
namespace qs::io::process {
|
||||
|
||||
class ProcessContext {
|
||||
Q_PROPERTY(QList<QString> command MEMBER command WRITE setCommand);
|
||||
Q_PROPERTY(QHash<QString, QVariant> environment MEMBER environment WRITE setEnvironment);
|
||||
Q_PROPERTY(bool clearEnvironment MEMBER clearEnvironment WRITE setClearEnvironment);
|
||||
Q_PROPERTY(QString workingDirectory MEMBER workingDirectory WRITE setWorkingDirectory);
|
||||
Q_PROPERTY(bool unbindStdout MEMBER unbindStdout WRITE setUnbindStdout);
|
||||
Q_GADGET;
|
||||
QML_STRUCTURED_VALUE;
|
||||
QML_VALUE_TYPE(processContext);
|
||||
|
||||
public:
|
||||
ProcessContext() = default;
|
||||
// Making this a Q_INVOKABLE does not work with QML_STRUCTURED_VALUe in Qt 6.9.
|
||||
explicit ProcessContext(QList<QString> command): command(std::move(command)), commandSet(true) {}
|
||||
|
||||
void setCommand(QList<QString> command) {
|
||||
this->command = std::move(command);
|
||||
this->commandSet = true;
|
||||
}
|
||||
|
||||
void setEnvironment(QHash<QString, QVariant> environment) {
|
||||
this->environment = std::move(environment);
|
||||
this->environmentSet = true;
|
||||
}
|
||||
|
||||
void setClearEnvironment(bool clearEnvironment) {
|
||||
this->clearEnvironment = clearEnvironment;
|
||||
this->clearEnvironmentSet = true;
|
||||
}
|
||||
|
||||
void setWorkingDirectory(QString workingDirectory) {
|
||||
this->workingDirectory = std::move(workingDirectory);
|
||||
this->workingDirectorySet = true;
|
||||
}
|
||||
|
||||
void setUnbindStdout(bool unbindStdout) { this->unbindStdout = unbindStdout; }
|
||||
|
||||
QList<QString> command;
|
||||
QHash<QString, QVariant> environment;
|
||||
bool clearEnvironment = false;
|
||||
QString workingDirectory;
|
||||
|
||||
bool commandSet : 1 = false;
|
||||
bool environmentSet : 1 = false;
|
||||
bool clearEnvironmentSet : 1 = false;
|
||||
bool workingDirectorySet : 1 = false;
|
||||
bool unbindStdout : 1 = true;
|
||||
};
|
||||
|
||||
void setupProcessEnvironment(
|
||||
QProcess* process,
|
||||
bool clear,
|
||||
const QHash<QString, QVariant>& envChanges
|
||||
);
|
||||
|
||||
} // namespace qs::io::process
|
||||
+22
-8
@@ -11,9 +11,10 @@
|
||||
#include <qqmlengine.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "datastream.hpp"
|
||||
|
||||
Q_LOGGING_CATEGORY(logSocket, "quickshell.io.socket", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logSocket, "quickshell.io.socket", QtWarningMsg);
|
||||
|
||||
void Socket::setSocket(QLocalSocket* socket) {
|
||||
if (this->socket != nullptr) this->socket->deleteLater();
|
||||
@@ -107,7 +108,11 @@ void Socket::flush() {
|
||||
|
||||
SocketServer::~SocketServer() { this->disableServer(); }
|
||||
|
||||
void SocketServer::onPostReload() {
|
||||
void SocketServer::onReload(QObject* oldInstance) {
|
||||
if (auto* old = qobject_cast<SocketServer*>(oldInstance)) {
|
||||
old->disableServer();
|
||||
}
|
||||
|
||||
this->postReload = true;
|
||||
if (this->isActivatable()) this->enableServer();
|
||||
}
|
||||
@@ -152,6 +157,8 @@ bool SocketServer::isActivatable() {
|
||||
void SocketServer::enableServer() {
|
||||
this->disableServer();
|
||||
|
||||
qCDebug(logSocket) << "Enabling socket server" << this << "at" << this->mPath;
|
||||
|
||||
this->server = new QLocalServer(this);
|
||||
QObject::connect(
|
||||
this->server,
|
||||
@@ -160,31 +167,38 @@ void SocketServer::enableServer() {
|
||||
&SocketServer::onNewConnection
|
||||
);
|
||||
|
||||
if (QFile::remove(this->mPath)) {
|
||||
qCWarning(logSocket) << "Deleted existing file at" << this->mPath << "to create socket";
|
||||
}
|
||||
|
||||
if (!this->server->listen(this->mPath)) {
|
||||
qWarning() << "could not start socket server at" << this->mPath;
|
||||
qCWarning(logSocket) << "Could not start socket server at" << this->mPath;
|
||||
this->disableServer();
|
||||
}
|
||||
|
||||
this->activeTarget = false;
|
||||
this->activePath = this->mPath;
|
||||
emit this->activeStatusChanged();
|
||||
}
|
||||
|
||||
void SocketServer::disableServer() {
|
||||
auto wasActive = this->server != nullptr;
|
||||
|
||||
if (this->server != nullptr) {
|
||||
if (wasActive) {
|
||||
qCDebug(logSocket) << "Disabling socket server" << this << "at" << this->activePath;
|
||||
for (auto* socket: this->mSockets) {
|
||||
socket->deleteLater();
|
||||
}
|
||||
|
||||
this->mSockets.clear();
|
||||
this->server->close();
|
||||
this->server->deleteLater();
|
||||
this->server = nullptr;
|
||||
}
|
||||
|
||||
if (this->mPath != nullptr) {
|
||||
if (QFile::exists(this->mPath) && !QFile::remove(this->mPath)) {
|
||||
qWarning() << "failed to delete socket file at" << this->mPath;
|
||||
if (!this->activePath.isEmpty()) {
|
||||
if (QFile::exists(this->activePath) && !QFile::remove(this->activePath)) {
|
||||
qWarning() << "Failed to delete socket file at" << this->activePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 212 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user