91 Commits
Author SHA1 Message Date
outfoxxed a5431dd02d version: bump to 0.2.0 2025-07-26 22:50:52 -07:00
outfoxxed f0d5f48a82 docs: add changelogs 2025-07-26 22:50:32 -07:00
outfoxxed 1c026545e9 core/desktopentry: use this-> in heuristicLookup 2025-07-26 22:50:17 -07:00
outfoxxed 0416032a7c core/reloader: trigger postReload with a signal
A signal is now used over the previous tree-searching method as some
QML components such as Repeater fail to reparent created children to
themselves, which breaks the tree.
2025-07-26 17:52:06 -07:00
outfoxxed 1644ed5e19 bluetooth: do not try to enable rfkilled devices
Bluez will not do this and reports a property change failure.
2025-07-26 17:02:35 -07:00
outfoxxed 91c9db581e wayland/screencopy: handle buffer creation failures 2025-07-26 00:48:21 -07:00
outfoxxed ab096b7e78 wayland/screencopy: reset buffer requests between frames
Prevents buffer requests from collecting a huge set of duplicate
dmabuf and shm formats.
2025-07-26 00:45:31 -07:00
outfoxxed 448623de5a service/notifications: use bytes over bits in pixmap rowstride check
Fixes incorrect rowstride warnings.
2025-07-25 22:08:15 -07:00
outfoxxed dfededc901 launch: ignore QT_STYLE_OVERRIDE and QT_QUICK_CONTROLS_STYLE
QT_STYLE_OVERRIDE often results in unexpected QML dependencies that
don't exist being required. QT_QUICK_CONTROLS_STYLE can vary across
systems and produce unexpected results.
2025-07-25 18:24:43 -07:00
outfoxxed 4dad447570 docs: remove }; in headers + typo fixes
}; breaks the docgen regex
2025-07-24 17:15:03 -07:00
Karboggy 3bbf39c67e core/reloader: fix file watcher compatibility with vscode 2025-07-24 15:42:58 -07:00
cameron f90bef2d99 hyprland/workspace: Use name instead of id for activate 2025-07-24 15:40:54 +10:00
outfoxxed db77c71c21 wayland/layershell: use width over height in horizontal auto exclude
Fixes #135
2025-07-21 02:38:50 -07:00
outfoxxed fcffbbced8 core/desktopentry: lookup wm class in nodisplay entries 2025-07-19 14:26:18 -07:00
outfoxxed 759bd721df core/log: stop trying to store detailed logs after write fail
Not stopping will cause the logger's write buffer to fill until OOM if
writing fails.
2025-07-19 03:41:24 -07:00
outfoxxed 63a6d27213 core/qmlglobal: configDir, configPath() -> shellDir, shellPath() 2025-07-19 02:58:55 -07:00
outfoxxed 77de23bb71 core/desktopentry: add StartupWMClass and heuristicLookup 2025-07-18 22:32:48 -07:00
outfoxxed 7b417bb808 build: add /lib/qt-6 to wrapped nix package
Fixes #130
2025-07-18 17:58:20 -07:00
Rexiel Scarlet e55d519c28 build: split derivation for extensible wrapper 2025-07-18 15:25:46 +04:00
outfoxxed ecc4a1249d all: mask various useless dbus errors 2025-07-18 04:14:58 -07:00
outfoxxed 6572a7f61d tooling: derive import paths from QML engine import paths
Due to distro patches and default locations, we can't correctly derive
it without calling the QQmlEngine function.
2025-07-18 00:33:58 -07:00
outfoxxed e885f4aec1 tooling: check if .qmlls.ini is a symlink in addition to exists
QFileInfo::exists() returns false on broken symlinks.
2025-07-18 00:07:25 -07:00
ipg0 115d6717a8 services/tray: use normal icon as fallback for attention custom icon
Signed-off-by: ipg0 <[email protected]>
2025-07-17 22:27:46 +03:00
outfoxxed 91dcb41d22 services/pipewire: destroy qml ifaces early to avoid user callbacks
Consumers of defaultAudio*Changed signals can run code between
safeDestroy being called and PwObjectIface destruction due to
signal connection order. This change destroys ifaces earlier so they
are nulled by the time a changed signal is fired from destruction,
preventing access between ~PwNode() and ~QObject() completion.

Fixes #116 #122 #124
2025-07-17 00:22:58 -07:00
outfoxxed 201c559dcd core: add Internal pragma 2025-07-16 20:13:59 -07:00
outfoxxed 78e3874ac6 tooling: add per-shell tooling lock to prevent races 2025-07-16 17:47:28 -07:00
outfoxxed 986749cdb9 tooling: add automatic QMLLS support for new imports and singletons 2025-07-16 14:35:46 -07:00
outfoxxed 4d8055f1cd build: fix PostReloadHook resolution in LSP 2025-07-15 19:03:27 -07:00
outfoxxed a45fc03c7d service/tray: fix missing documentation for invokables
'};' prior to invokables caused the docgen regex to miss them
2025-07-15 15:58:56 -07:00
ipg0 c40074dd56 service/notifications: add inline-reply action support
Signed-off-by: ipg0 <[email protected]>
2025-07-15 15:49:59 -07:00
outfoxxed 3dfb7d8827 core/window: handle graphics context loss 2025-07-15 15:40:10 -07:00
outfoxxed a2146f6394 core/window: add closed() signal to all window types 2025-07-15 15:39:55 -07:00
outfoxxed 5706c09e6f core/window: clean up window interface property proxies 2025-07-15 14:06:26 -07:00
outfoxxed 5ac9096c1c Revert "core/region: use QList over QQmlListProperty for child regions"
This reverts commit 0c9c5be8dd.

Using QList breaks the default property usage.
2025-07-14 02:56:34 -07:00
outfoxxed 05fbead660 x11/panelwindow: calc screen geom with exclusions of other panels
Fixes a typo in 9604302 which calculated panel stack offsets from the
current panel instead of others in the stack.
2025-07-13 22:54:20 -07:00
outfoxxed 478aa2bda1 core/window: run polish in onExposed instead of polishItems
Fixes hyprland visible regions created before window expose.
2025-07-13 22:27:44 -07:00
outfoxxed cee1f5837e service/mpris: make lengthSupported bindable and notify for changes
Fixes #109
2025-07-13 20:32:51 -07:00
outfoxxed 71334bfcaf core/desktopentry: expose exec command and use execDetached on call 2025-07-13 20:05:54 -07:00
outfoxxed de25787451 io/process: null stdio channels in detached processes 2025-07-13 20:05:54 -07:00
outfoxxed b011cd9d33 core/window: set FloatingWindow default max size to QWINDOWSIZE_MAX
Was previously zero, which will shrink the window to 1px depending on
the display server.
2025-07-13 20:05:53 -07:00
outfoxxed 1e1ba93713 core/window: add manual PanelWindow tester 2025-07-13 18:35:41 -07:00
outfoxxed 59d29bb254 x11/panelwindow: use Qt window default screen if none is provided
Fixes panels not updating geometry or attachments under X if a screen
was not explicitly provided by the user.
2025-07-13 18:30:48 -07:00
outfoxxed 9604302415 x11/panelwindow: convert to bindable properties 2025-07-13 17:50:17 -07:00
outfoxxed 479ff58f84 wayland/layershell: support opposite-to-exclusion edge margins 2025-07-13 17:08:12 -07:00
outfoxxed 3b4ebc5f16 wayland/layershell: support auto exclusive zone without constraint 2025-07-13 17:02:18 -07:00
outfoxxed bb206e3a19 core/window: run window-level polish along with item polish
Fixes input masks not updating after a reload.
2025-07-12 22:02:33 -07:00
outfoxxed 0c9c5be8dd core/region: use QList over QQmlListProperty for child regions 2025-07-12 22:02:31 -07:00
outfoxxed 49a3752b9d core: correctly deregister QML incubators on destruction
Previously we'd try to cast the QObject* sender from
QObject::destroyed to a QQmlIncubationController*. This will always
return nullptr because C++ destructors change the type of the object
and the QQmlIncubationController destructor has already run at this
point. We now store controllers as QObject*s.

Fixes #108
2025-07-11 00:38:58 -07:00
outfoxxed 026aac3756 build: add icon and desktop file 2025-07-10 21:57:10 -07:00
outfoxxed d7079b7524 core: allow qml scanner to detect namespaced and versioned imports 2025-07-10 04:28:05 -07:00
outfoxxed 6f774af11e core/colorquant: print image source url vs pointer on err 2025-07-10 04:05:16 -07:00
outfoxxed 5703fbae21 wayland/lock: handle null window in configure()
Has caused a crash.
2025-07-10 04:01:00 -07:00
outfoxxed 07ea4de248 io/ipchandler: add registry logs 2025-07-10 03:50:11 -07:00
outfoxxed 2629e211fa crash: initialize QApplication after logging to run cat filter 2025-07-10 03:40:55 -07:00
outfoxxed b4c62b8ff9 core: only log warn+ from quickshell.paths 2025-07-10 03:40:30 -07:00
outfoxxed 1af08c0c52 core: only call QmlScanner::scanDir() on directories
Removes a bogus warning message.
2025-07-10 03:12:43 -07:00
outfoxxed 4b35d7b51b core: support qs. imports 2025-07-10 01:50:53 -07:00
outfoxxed 3d594e16dd core/log: track default logging categories
Fixes a bug in fb37be7 which ignored default logging categories due to
skipping QLoggingRegistry's filter.
2025-07-08 13:49:06 -07:00
outfoxxed 5d7e07508a bluetooth: fix defaultAdapter reactivity
Fixes #100
2025-07-07 02:21:50 -07:00
outfoxxed 87d99b866f services/pipewire: destroy bound audio object when node is destroyed
Fixes a leak and prevents a UAF via device volume signals
derefing the freed node.

Fixes #91
2025-07-04 20:29:50 -07:00
outfoxxed 7eff415b25 core/qmlglobal: re-add shellRoot as a deprecated property 2025-07-04 20:06:22 -07:00
outfoxxed 3cc7ced3a0 core/window: fix QsWindow being null for WlrLayershell 2025-07-04 17:58:55 -07:00
outfoxxed fb37be7611 core/log: ignore on-disk logging configs for quickshell* rules.
Fixes fedora hiding all command output by default.
2025-07-04 16:43:01 -07:00
outfoxxed 9708d8212a core/reloader: trigger onPostReload if launched post-reload
This is similar to the check in Reloadable, and fixes a number of hard
to debug issues with Process, IpcHandler, NotificationServer, and
GlobalShortcut not working depending on where you put them in a QML file.
2025-07-04 16:02:14 -07:00
outfoxxed 0e6518a706 core/command: improve dead instance selection
Prints dead instances if they exist, as well as allowing dead instance
selection for a substring if no live instances exist.
2025-07-02 22:47:19 -07:00
outfoxxed 86591f122d io/process: mask the "QProcess destroyed for running process" warn 2025-07-02 20:16:47 -07:00
outfoxxed f681e2016f bluetooth: add bluetooth integration
Missing support for things that require an agent, but has most basics.

Closes #17
2025-07-01 00:25:16 -07:00
outfoxxed 1d02292fbf hyprland/ipc: actually set lastIpcObject 2025-06-27 04:09:14 -07:00
outfoxxed f842b84a5a widgets/wrapper: round child position when centering
Fixes misalignment when resizeChild is false and wrapper width is odd.
2025-06-26 12:43:59 -07:00
outfoxxed d949f91347 wayland/screencopy: apply output transform to wlr screencopy
Note that this only fixes output copies, and not toplevel copies.
Toplevel copies are harder because a toplevel can be on more than
one output. Hopefully we'll all be using image-copy-capture before
this one comes up.

Fixes #75
2025-06-25 12:34:00 -07:00
outfoxxed 27f97c3283 wayland/toplevel: refactor toplevel output tracking to its own file 2025-06-24 19:38:34 -07:00
outfoxxed 20c3da01f1 io/fileview: null watcher ptr after deletion to avoid UAF
Fixes #69
2025-06-21 12:57:15 -07:00
outfoxxed 8be18c05ed hyprland/ipc: expose HyprlandToplevel jsons 2025-06-20 21:31:44 -07:00
outfoxxed 98d09b5a36 io/process: add Process.exec() 2025-06-20 20:32:42 -07:00
outfoxxed 8fc3e1cb6e docs: include HyprlandToplevel in module file 2025-06-20 19:06:59 -07:00
outfoxxed c17ea54371 wayland/lock: check for protocol availability before use
Fixes #66
2025-06-20 16:37:22 -07:00
Maeeen 362c8e1b69 hyprland/ipc: expose Hyprland toplevels 2025-06-20 04:09:37 -07:00
outfoxxed c115df8d34 docs: mention github mirror in README 2025-06-20 03:34:05 -07:00
outfoxxed 02362c3e94 services/pipewire: add missing ; after Q_ENUM for docgen 2025-06-20 02:53:30 -07:00
outfoxxed 3d3b7f1c05 wayland/lock: avoid creating lock surfaces for the fallback screen
Fixes #61
2025-06-19 14:54:52 -07:00
outfoxxed 79b2204af8 io/socketserver: correctly order startup/teardown across generations
Fixes #60
2025-06-19 13:50:47 -07:00
outfoxxed 95d0af8113 services/pipewire: update volume props from device for device nodes
Yet another device node edge case. In addition to only writing via
a pw_device when present, now we only read from one as well.

This fixes missing state changes not conveyed by the pw_node.

Fixes #35
2025-06-19 05:12:24 -07:00
outfoxxed 579d589290 core/popupanchor: ensure item-derived rect is at least 1x1 pixels 2025-06-18 13:41:14 -07:00
outfoxxed 9a30333405 build: clarify shared libraries 2025-06-15 23:00:56 -07:00
outfoxxed d9164578a2 core/window: add title property to floating windows 2025-06-15 03:12:51 -07:00
outfoxxed 20322484b9 wayland/layershell: fix bridge destructor use after free on reload
Under some conditions, Qt will recreate the layer surface. The layer
surface destructor tries to destroy the bridge, but doesn't actually
need to because the bridge is a child of the QWindow owning the layer,
meaning not destroying it is actually completely fine.
2025-06-15 02:52:16 -07:00
outfoxxed 0499518143 core/qmlglobal: add execDetached functions for spawning processes 2025-06-15 02:52:16 -07:00
outfoxxed 0140356d99 core/qmlglobal!: rename shellRoot to configDir + add configPath 2025-06-14 14:45:04 -07:00
outfoxxed 71fe3d9165 x11/panelwindow: do not look up engine generation in ~XPanelWindow()
Looking up engine generation in the destructor causes occasional
crashes. This commit caches it to prevent that from happening.
2025-06-13 20:12:32 -07:00
outfoxxed 517143adf9 all: fix new lints 2025-06-12 17:01:13 -07:00
outfoxxed 05b5eccf2e build: update build guide, nix and guix packages 2025-06-12 12:59:36 -07:00
193 changed files with 5146 additions and 1169 deletions

No files matched your search

+16 -8
View File
@@ -41,10 +41,15 @@ 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:
@@ -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
@@ -101,8 +106,11 @@ To disable: `-DWAYLAND=OFF`
Dependencies:
- `qt6wayland`
- `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
+14 -2
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20)
project(quickshell VERSION "0.1.0" LANGUAGES CXX C)
project(quickshell VERSION "0.2.0" 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)
@@ -116,7 +117,7 @@ if (WAYLAND)
list(APPEND QT_FPDEPS WaylandClient)
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()
@@ -145,3 +146,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
)
+4
View File
@@ -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.
+7
View File
@@ -0,0 +1,7 @@
[Desktop Entry]
Version=1.5
Type=Application
NoDisplay=true
Name=Quickshell
Icon=org.quickshell
+1
View File
@@ -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

+1
View File
@@ -0,0 +1 @@
Initial release
+84
View File
@@ -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.
+85 -57
View File
@@ -5,18 +5,20 @@
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,90 @@
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 = buildStdenv.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
qt6.qtshadertools
spirv-tools
pkg-config
]
++ lib.optional withWayland 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.optionals withWayland [ qt6.qtwayland 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
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1736012469,
"narHash": "sha256-/qlNWm/IEVVH7GfgAIyP6EsVZI6zjAx1cV5zNyrs+rI=",
"lastModified": 1749285348,
"narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8f3e1f807051e32d8c95cd12b9b421623850a34d",
"rev": "3e3afe5174c561dee0df6f2c2b2236990146329f",
"type": "github"
},
"original": {
+4 -3
View File
@@ -4,9 +4,10 @@
};
outputs = { self, nixpkgs }: let
forEachSystem = fn: nixpkgs.lib.genAttrs
[ "x86_64-linux" "aarch64-linux" ]
(system: fn system nixpkgs.legacyPackages.${system});
forEachSystem = fn:
nixpkgs.lib.genAttrs
nixpkgs.lib.platforms.linux
(system: fn system nixpkgs.legacyPackages.${system});
in {
packages = forEachSystem (system: pkgs: rec {
quickshell = pkgs.callPackage ./default.nix {
+3 -3
View File
@@ -35,9 +35,9 @@
pkg-config
qtshadertools
spirv-tools
wayland-protocols))
(inputs (list cli11
jemalloc
wayland-protocols
cli11))
(inputs (list jemalloc
libdrm
libxcb
libxkbcommon
+4
View File
@@ -29,3 +29,7 @@ if (X11)
endif()
add_subdirectory(services)
if (BLUETOOTH)
add_subdirectory(bluetooth)
endif()
+42
View File
@@ -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)
+224
View File
@@ -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;
}
+173
View File
@@ -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);
+168
View File
@@ -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
+98
View File
@@ -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
+319
View File
@@ -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;
}
+225
View File
@@ -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);
+12
View File
@@ -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.
+9
View File
@@ -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>
+8
View File
@@ -0,0 +1,8 @@
<node>
<interface name="org.bluez.Device1">
<method name="Connect"/>
<method name="Disconnect"/>
<method name="Pair"/>
<method name="CancelPairing"/>
</interface>
</node>
+200
View File
@@ -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();
}
}
}
}
}
}
}
}
}
}
}
+1
View File
@@ -38,6 +38,7 @@ qt_add_library(quickshell-core STATIC
iconprovider.cpp
scriptmodel.cpp
colorquantizer.cpp
toolsupport.cpp
)
qt_add_qml_module(quickshell-core
+4 -2
View File
@@ -18,8 +18,10 @@
#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)
@@ -45,7 +47,7 @@ void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCa
}
if (image.isNull()) {
qCWarning(logColorQuantizer) << "Failed to load image from" << source;
qCWarning(logColorQuantizer) << "Failed to load image from" << source->toString();
return;
}
-2
View File
@@ -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
View File
@@ -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
+44 -21
View File
@@ -1,4 +1,5 @@
#include "desktopentry.hpp"
#include <algorithm>
#include <qcontainerfwd.h>
#include <qdebug.h>
@@ -11,16 +12,17 @@
#include <qnamespace.h>
#include <qobject.h>
#include <qpair.h>
#include <qprocess.h>
#include <qstringview.h>
#include <qtenvironmentvariables.h>
#include <ranges>
#include "common.hpp"
#include "../io/processcore.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 {
@@ -107,11 +109,14 @@ void DesktopEntry::parseEntry(const QString& text) {
if (key == "Name") this->mName = value;
else if (key == "GenericName") this->mGenericName = value;
else if (key == "StartupWMClass") this->mStartupClass = 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 == "Exec") {
this->mExecString = value;
this->mCommand = DesktopEntry::parseExecString(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);
@@ -126,7 +131,10 @@ void DesktopEntry::parseEntry(const QString& text) {
if (key == "Name") action->mName = value;
else if (key == "Icon") action->mIcon = value;
else if (key == "Exec") action->mExecString = value;
else if (key == "Exec") {
action->mExecString = value;
action->mCommand = DesktopEntry::parseExecString(value);
}
}
this->mActions.insert(actionName, action);
@@ -178,7 +186,7 @@ void DesktopEntry::parseEntry(const QString& text) {
}
void DesktopEntry::execute() const {
DesktopEntry::doExec(this->mExecString, this->mWorkingDirectory);
DesktopEntry::doExec(this->mCommand, this->mWorkingDirectory);
}
bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); }
@@ -250,23 +258,15 @@ 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->mCommand, this->entry->mWorkingDirectory);
}
DesktopEntryManager::DesktopEntryManager() {
@@ -386,6 +386,25 @@ 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, [&](const DesktopEntry* entry) {
return name == entry->mStartupClass;
});
if (iter != list.end()) return *iter;
iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) {
return name.toLower() == entry->mStartupClass.toLower();
});
if (iter != list.end()) return *iter;
return nullptr;
}
ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); }
@@ -394,6 +413,10 @@ 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();
}
+52 -3
View File
@@ -22,14 +22,28 @@ class DesktopEntry: public QObject {
Q_PROPERTY(QString name MEMBER mName CONSTANT);
/// Short description of the application, such as "Web Browser". May be empty.
Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT);
/// Initial class or app id the app intends to use. May be useful for matching running apps
/// to desktop entries.
Q_PROPERTY(QString startupClass MEMBER mStartupClass CONSTANT);
/// If true, this application should not be displayed in menus and launchers.
Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT);
/// Long description of the application, such as "View websites on the internet". May be empty.
Q_PROPERTY(QString comment MEMBER mComment CONSTANT);
/// 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().
/// 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 MEMBER mExecString CONSTANT);
/// 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 MEMBER mCommand CONSTANT);
/// The working directory to execute from.
Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT);
/// If the application should run in a terminal.
@@ -46,6 +60,16 @@ public:
void parseEntry(const QString& text);
/// 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;
@@ -54,16 +78,18 @@ public:
// 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);
public:
QString mId;
QString mName;
QString mGenericName;
QString mStartupClass;
bool mNoDisplay = false;
QString mComment;
QString mIcon;
QString mExecString;
QVector<QString> mCommand;
QString mWorkingDirectory;
bool mTerminal = false;
QVector<QString> mCategories;
@@ -82,8 +108,19 @@ class DesktopAction: public QObject {
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().
/// 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 MEMBER mExecString CONSTANT);
/// 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 MEMBER mCommand CONSTANT);
QML_ELEMENT;
QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
@@ -94,6 +131,9 @@ 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;
private:
@@ -102,6 +142,7 @@ private:
QString mName;
QString mIcon;
QString mExecString;
QVector<QString> mCommand;
QHash<QString, QString> mEntries;
friend class DesktopEntry;
@@ -114,6 +155,7 @@ public:
void scanDesktopEntries();
[[nodiscard]] DesktopEntry* byId(const QString& id);
[[nodiscard]] DesktopEntry* heuristicLookup(const QString& name);
[[nodiscard]] ObjectModel<DesktopEntry>* applications();
@@ -149,7 +191,14 @@ 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();
};
+46 -39
View File
@@ -11,6 +11,7 @@
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmlcontext.h>
#include <qqmlengine.h>
@@ -21,13 +22,14 @@
#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();
}
}
@@ -235,23 +245,24 @@ 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 {
auto* obj = dynamic_cast<QObject*>(controller);
if (!obj) {
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;
QObject::connect(
obj,
&QObject::destroyed,
this,
&EngineGeneration::incubationControllerDestroyed,
Qt::UniqueConnection
);
this->incubationControllers.push_back(obj);
qCDebug(logIncubator) << "Registered incubation controller" << obj << "to generation" << this;
// This function can run during destruction.
if (this->engine == nullptr) return;
@@ -261,21 +272,25 @@ void EngineGeneration::registerIncubationController(QQmlIncubationController* co
}
}
// Multiple controllers may be destroyed at once. Dynamic casts must be performed before working
// with any controllers. The QQmlIncubationController destructor will already have run by the
// point QObject::destroyed is called, so we can't cast to that.
void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) {
if (auto* obj = dynamic_cast<QObject*>(controller)) {
QObject::disconnect(obj, nullptr, this, nullptr);
} else {
auto* obj = dynamic_cast<QObject*>(controller);
if (!obj) {
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"
QObject::disconnect(obj, nullptr, this, nullptr);
if (this->incubationControllers.removeOne(obj)) {
qCDebug(logIncubator) << "Deregistered incubation controller" << obj << "from" << this;
} else {
qCCritical(logIncubator) << "Failed to deregister incubation controller" << obj << "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.
@@ -290,22 +305,12 @@ void EngineGeneration::deregisterIncubationController(QQmlIncubationController*
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"
if (this->incubationControllers.removeAll(sender) != 0) {
qCDebug(logIncubator) << "Destroyed incubation controller" << sender << "deregistered from"
<< this;
} else {
qCCritical(logIncubator) << "Destroyed incubation controller" << controller
qCCritical(logIncubator) << "Destroyed incubation controller" << sender
<< "was not registered, but its destruction was observed by" << this;
return;
@@ -314,16 +319,18 @@ void EngineGeneration::incubationControllerDestroyed() {
// This function can run during destruction.
if (this->engine == nullptr) return;
if (this->engine->incubationController() == controller) {
if (dynamic_cast<QObject*>(this->engine->incubationController()) == sender) {
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();
@@ -366,7 +373,7 @@ void EngineGeneration::assignIncubationController() {
if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) {
controller = &this->delayedIncubationController;
} else {
controller = this->incubationControllers.first();
controller = dynamic_cast<QQmlIncubationController*>(this->incubationControllers.first());
}
qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"
+3 -2
View File
@@ -75,6 +75,7 @@ public:
signals:
void filesChanged();
void reloadFinished();
void firePostReload();
public slots:
void quit();
@@ -84,12 +85,12 @@ private slots:
void onFileChanged(const QString& name);
void onDirectoryChanged();
void incubationControllerDestroyed();
void onEngineWarnings(const QList<QQmlError>& warnings) const;
static void onEngineWarnings(const QList<QQmlError>& warnings);
private:
void postReload();
void assignIncubationController();
QVector<QQmlIncubationController*> incubationControllers;
QVector<QObject*> incubationControllers;
bool incubationControllersLocked = false;
QHash<const void*, EngineGenerationExt*> extensions;
+3 -2
View File
@@ -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) {
+3 -2
View File
@@ -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
+2 -2
View File
@@ -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;
}
+2
View File
@@ -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
};
+28
View File
@@ -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) (); \
}
+36 -16
View File
@@ -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,
@@ -441,10 +457,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())) {
if (this->detailedFile) {
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
}
this->detailedWriter.setDevice(nullptr);
this->detailedFile->close();
this->detailedFile = nullptr;
}
}
+7 -1
View File
@@ -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(
+2 -1
View File
@@ -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 {
+3 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+4
View File
@@ -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
View File
@@ -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);
}
+38
View File
@@ -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
View File
@@ -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);
}
+12 -3
View File
@@ -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
View File
@@ -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;
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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;
};
+33 -9
View File
@@ -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);
}
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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);
};
-6
View File
@@ -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);
}
}
-1
View File
@@ -26,7 +26,6 @@ public:
void registerSingleton(const QUrl& url, Singleton* singleton);
void onReload(SingletonRegistry* old);
void onPostReload();
private:
QHash<QUrl, Singleton*> registry;
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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;
+241
View File
@@ -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
+22
View File
@@ -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
+2 -1
View File
@@ -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 {
+5 -2
View File
@@ -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;
@@ -176,6 +176,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...";
+11
View File
@@ -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
View File
@@ -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(
+10
View File
@@ -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>;
+4 -3
View File
@@ -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;
@@ -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);
+1 -1
View File
@@ -20,7 +20,7 @@
#include "../properties.hpp"
#include "dbus_menu_types.hpp"
Q_DECLARE_LOGGING_CATEGORY(logDbusMenu);
QS_DECLARE_LOGGING_CATEGORY(logDbusMenu);
class DBusMenuInterface;
+87
View File
@@ -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
+37
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+2
View File
@@ -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)
+2 -2
View File
@@ -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`.
+6 -2
View File
@@ -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) {
@@ -492,7 +493,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;
+9
View File
@@ -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;
}
}
+2 -4
View File
@@ -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);
+1
View File
@@ -91,6 +91,7 @@ class JsonAdapter
, public QQmlParserStatus {
Q_OBJECT;
QML_ELEMENT;
Q_INTERFACES(QQmlParserStatus);
public:
void classBegin() override {}
+57 -34
View File
@@ -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
View File
@@ -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;
+37
View File
@@ -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
+69
View File
@@ -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
View File
@@ -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;
}
}
}
+7 -6
View File
@@ -1,5 +1,6 @@
#pragma once
#include <qcontainerfwd.h>
#include <qlocalserver.h>
#include <qlocalsocket.h>
#include <qloggingcategory.h>
@@ -9,10 +10,11 @@
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include "../core/logcat.hpp"
#include "../core/reload.hpp"
#include "datastream.hpp"
Q_DECLARE_LOGGING_CATEGORY(logSocket);
QS_DECLARE_LOGGING_CATEGORY(logSocket);
///! Unix socket listener.
class Socket: public DataStream {
@@ -90,9 +92,7 @@ private:
/// }
/// }
/// ```
class SocketServer
: public QObject
, public PostReloadHook {
class SocketServer: public Reloadable {
Q_OBJECT;
/// If the socket server is currently active. Defaults to false.
///
@@ -115,11 +115,11 @@ class SocketServer
QML_ELEMENT;
public:
explicit SocketServer(QObject* parent = nullptr): QObject(parent) {}
explicit SocketServer(QObject* parent = nullptr): Reloadable(parent) {}
~SocketServer() override;
Q_DISABLE_COPY_MOVE(SocketServer);
void onPostReload() override;
void onReload(QObject* oldInstance) override;
[[nodiscard]] bool isActive() const;
void setActive(bool active);
@@ -149,4 +149,5 @@ private:
bool activeTarget = false;
bool postReload = false;
QString mPath;
QString activePath;
};
+2 -1
View File
@@ -1,7 +1,8 @@
function (qs_test name)
add_executable(${name} ${ARGN})
target_link_libraries(${name} PRIVATE Qt::Quick Qt::Network Qt::Test)
target_link_libraries(${name} PRIVATE Qt::Quick Qt::Network Qt::Test quickshell-io quickshell-core quickshell-window quickshell-ui)
add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $<TARGET_FILE:${name}>)
endfunction()
qs_test(datastream datastream.cpp ../datastream.cpp)
qs_test(process process.cpp ../process.cpp ../datastream.cpp ../processcore.cpp)
+47
View File
@@ -0,0 +1,47 @@
#include "process.hpp"
#include <qlist.h>
#include <qsignalspy.h>
#include <qtest.h>
#include <qtestcase.h>
#include "../process.hpp"
void TestProcess::startAfterReload() {
auto process = Process();
auto startedSpy = QSignalSpy(&process, &Process::started);
auto exitedSpy = QSignalSpy(&process, &Process::exited);
process.setCommand({"true"});
process.setRunning(true);
QVERIFY(!process.isRunning());
QCOMPARE(startedSpy.count(), 0);
process.postReload();
QVERIFY(process.isRunning());
QVERIFY(startedSpy.wait(100));
}
void TestProcess::testExec() {
auto process = Process();
auto startedSpy = QSignalSpy(&process, &Process::started);
auto exitedSpy = QSignalSpy(&process, &Process::exited);
process.postReload();
process.setCommand({"sleep", "30"});
process.setRunning(true);
QVERIFY(process.isRunning());
QVERIFY(startedSpy.wait(100));
process.exec({"true"});
QVERIFY(exitedSpy.wait(100));
QVERIFY(startedSpy.wait(100));
QVERIFY(process.isRunning());
}
QTEST_MAIN(TestProcess);
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <qobject.h>
#include <qtmetamacros.h>
class TestProcess: public QObject {
Q_OBJECT;
private slots:
static void startAfterReload();
static void testExec();
};
+2 -1
View File
@@ -10,12 +10,13 @@
#include <qobject.h>
#include "../core/generation.hpp"
#include "../core/logcat.hpp"
#include "../core/paths.hpp"
#include "ipccommand.hpp"
namespace qs::ipc {
Q_LOGGING_CATEGORY(logIpc, "quickshell.ipc", QtWarningMsg);
QS_LOGGING_CATEGORY(logIpc, "quickshell.ipc", QtWarningMsg);
IpcServer::IpcServer(const QString& path) {
QObject::connect(&this->server, &QLocalServer::newConnection, this, &IpcServer::onNewConnection);
+3 -1
View File
@@ -15,6 +15,8 @@
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../core/logcat.hpp"
template <typename... Types>
constexpr void assertSerializable() {
// monostate being zero ensures transactional reads wont break
@@ -109,7 +111,7 @@ DEFINE_SIMPLE_DATASTREAM_OPS(std::monostate);
namespace qs::ipc {
Q_DECLARE_LOGGING_CATEGORY(logIpc);
QS_DECLARE_LOGGING_CATEGORY(logIpc);
template <typename T>
class MessageStream {
+63 -12
View File
@@ -3,6 +3,7 @@
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <utility>
#include <qconfig.h>
#include <qcontainerfwd.h>
@@ -177,14 +178,33 @@ int selectInstance(CommandState& cmd, InstanceLockInfo* instance, bool deadFallb
}
} else if (!cmd.instance.id->isEmpty()) {
path = basePath->filePath("by-pid");
auto instances = QsPaths::collectInstances(path, deadFallback);
auto [liveInstances, deadInstances] = QsPaths::collectInstances(path);
instances.removeIf([&](const InstanceLockInfo& info) {
liveInstances.removeIf([&](const InstanceLockInfo& info) {
return !info.instance.instanceId.startsWith(*cmd.instance.id);
});
deadInstances.removeIf([&](const InstanceLockInfo& info) {
return !info.instance.instanceId.startsWith(*cmd.instance.id);
});
auto instances = liveInstances.isEmpty() && deadFallback ? deadInstances : liveInstances;
if (instances.isEmpty()) {
qCInfo(logBare) << "No running instances start with" << *cmd.instance.id;
if (deadFallback) {
qCInfo(logBare) << "No instances start with" << *cmd.instance.id;
} else {
qCInfo(logBare) << "No running instances start with" << *cmd.instance.id;
if (!deadInstances.isEmpty()) {
qCInfo(logBare) << "Some dead instances match:";
for (auto& instance: deadInstances) {
qCInfo(logBare).noquote() << " -" << instance.instance.instanceId;
}
}
}
return -1;
} else if (instances.length() != 1) {
qCInfo(logBare) << "More than one instance starts with" << *cmd.instance.id;
@@ -208,14 +228,29 @@ int selectInstance(CommandState& cmd, InstanceLockInfo* instance, bool deadFallb
path = QDir(basePath->filePath("by-path")).filePath(pathId);
auto instances = QsPaths::collectInstances(path, deadFallback);
auto [liveInstances, deadInstances] = QsPaths::collectInstances(path);
auto instances = liveInstances;
if (instances.isEmpty() && deadFallback) {
instances = deadInstances;
}
sortInstances(
instances,
cmd.config.newest || (!instances.empty() && instances.first().pid == -1)
);
if (instances.isEmpty()) {
qCInfo(logBare) << "No running instances for" << configFilePath;
if (liveInstances.isEmpty() && deadInstances.length() > 1) {
qCInfo(logBare) << "No running instances for" << configFilePath;
qCInfo(logBare) << "Dead instances:";
sortInstances(deadInstances, cmd.config.newest);
for (auto& instance: deadInstances) {
qCInfo(logBare).noquote() << " -" << instance.instance.instanceId;
}
} else {
qCInfo(logBare) << "No running instances for" << configFilePath;
}
return -1;
}
@@ -276,7 +311,18 @@ int listInstances(CommandState& cmd) {
path = QDir(basePath->filePath("by-path")).filePath(pathId);
}
auto instances = QsPaths::collectInstances(path);
auto [liveInstances, deadInstances] = QsPaths::collectInstances(path);
sortInstances(liveInstances, cmd.config.newest);
QList<InstanceLockInfo> instances;
if (cmd.instance.includeDead) {
sortInstances(deadInstances, cmd.config.newest);
instances = std::move(deadInstances);
instances.append(liveInstances);
} else {
instances = std::move(liveInstances);
}
if (instances.isEmpty()) {
if (cmd.instance.all) {
@@ -286,7 +332,6 @@ int listInstances(CommandState& cmd) {
qCInfo(logBare) << "Use --all to list all instances.";
}
} else {
sortInstances(instances, cmd.config.newest);
if (cmd.output.json) {
auto array = QJsonArray();
@@ -295,7 +340,7 @@ int listInstances(CommandState& cmd) {
auto json = QJsonObject();
json["id"] = instance.instance.instanceId;
json["pid"] = instance.pid;
json["pid"] = instance.instance.pid;
json["shell_id"] = instance.instance.shellId;
json["config_path"] = instance.instance.configPath;
json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate);
@@ -319,12 +364,18 @@ int listInstances(CommandState& cmd) {
.arg(remMinutes)
.arg(remSeconds);
auto isDead = instance.pid == -1;
auto gray = !cmd.log.noColor && isDead;
qCInfo(logBare).noquote().nospace()
<< "Instance " << instance.instance.instanceId << ":\n"
<< " Process ID: " << instance.pid << '\n'
<< (gray ? "\033[90m" : "") << "Instance " << instance.instance.instanceId
<< (isDead ? " (dead)" : "") << ":\n"
<< " Process ID: " << instance.instance.pid << '\n'
<< " Shell ID: " << instance.instance.shellId << '\n'
<< " Config path: " << instance.instance.configPath << '\n'
<< " Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n";
<< " Launch time: " << launchTimeStr
<< (isDead ? "" : " (running for " + runtimeStr + ")") << '\n'
<< (gray ? "\033[0m" : "");
}
}
}
@@ -458,7 +509,7 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
if (state.misc.printVersion) {
qCInfo(logBare).noquote().nospace()
<< "quickshell 0.1.0, revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR;
<< "quickshell 0.2.0, revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR;
if (state.log.verbosity > 1) {
qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR;
+10
View File
@@ -73,6 +73,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio
bool useQApplication = false;
bool nativeTextRendering = false;
bool desktopSettingsAware = true;
bool useSystemStyle = false;
QString iconTheme = qEnvironmentVariable("QS_ICON_THEME");
QHash<QString, QString> envOverrides;
QString dataDir;
@@ -88,6 +89,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio
if (pragma == "UseQApplication") pragmas.useQApplication = true;
else if (pragma == "NativeTextRendering") pragmas.nativeTextRendering = true;
else if (pragma == "IgnoreSystemSettings") pragmas.desktopSettingsAware = false;
else if (pragma == "RespectSystemStyle") pragmas.useSystemStyle = true;
else if (pragma.startsWith("IconTheme ")) pragmas.iconTheme = pragma.sliced(10);
else if (pragma.startsWith("Env ")) {
auto envPragma = pragma.sliced(4);
@@ -128,6 +130,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio
.configPath = args.configPath,
.shellId = shellId,
.launchTime = qs::Common::LAUNCH_TIME,
.pid = getpid(),
};
#if CRASH_REPORTER
@@ -154,6 +157,11 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio
Common::INITIAL_ENVIRONMENT = QProcessEnvironment::systemEnvironment();
if (!pragmas.useSystemStyle) {
qunsetenv("QT_STYLE_OVERRIDE");
qputenv("QT_QUICK_CONTROLS_STYLE", "Fusion");
}
for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) {
qputenv(var.toUtf8(), val.toUtf8());
}
@@ -218,6 +226,8 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio
app = new QGuiApplication(qArgC, argv);
}
QGuiApplication::setDesktopFileName("org.quickshell");
if (args.debugPort != -1) {
QQmlDebuggingEnabler::enableDebugging(true);
auto wait = args.waitForDebug ? QQmlDebuggingEnabler::WaitForClient
+1
View File
@@ -61,6 +61,7 @@ struct CommandState {
QStringOption id;
pid_t pid = -1; // NOLINT (include)
bool all = false;
bool includeDead = false;
} instance;
struct {
+3
View File
@@ -163,6 +163,9 @@ int parseCommand(int argc, char** argv, CommandState& state) {
sub->add_flag("-j,--json", state.output.json, "Output the list as a json.");
sub->add_flag("--show-dead", state.instance.includeDead)
->description("Include dead instances in the list.");
addConfigSelection(sub, true)->excludes(all);
addLoggingOptions(sub, false, true);
+2 -1
View File
@@ -14,9 +14,10 @@
#include <qtypes.h>
#include "../../core/generation.hpp"
#include "../../core/logcat.hpp"
namespace {
Q_LOGGING_CATEGORY(logGreetd, "quickshell.service.greetd");
QS_LOGGING_CATEGORY(logGreetd, "quickshell.service.greetd");
}
QString GreetdState::toString(GreetdState::Enum value) {
+48 -44
View File
@@ -3,6 +3,7 @@
#include <qcontainerfwd.h>
#include <qdatetime.h>
#include <qdbusconnection.h>
#include <qdbuserror.h>
#include <qdbusextratypes.h>
#include <qlist.h>
#include <qlogging.h>
@@ -14,6 +15,7 @@
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../core/logcat.hpp"
#include "../../dbus/properties.hpp"
#include "dbus_player.h"
#include "dbus_player_app.h"
@@ -23,7 +25,7 @@ using namespace qs::dbus;
namespace qs::service::mpris {
namespace {
Q_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg);
QS_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg);
}
QString MprisPlaybackState::toString(MprisPlaybackState::Enum status) {
@@ -98,41 +100,12 @@ MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(paren
} else return static_cast<qlonglong>(-1);
});
this->bPlaybackState.setBinding([this]() {
const auto& status = this->bpPlaybackStatus.value();
if (status == "Playing") {
return MprisPlaybackState::Playing;
} else if (status == "Paused") {
this->pausedTime = QDateTime::currentDateTimeUtc();
return MprisPlaybackState::Paused;
} else if (status == "Stopped") {
return MprisPlaybackState::Stopped;
} else {
qWarning() << "Received unexpected PlaybackStatus for" << this << status;
return MprisPlaybackState::Stopped;
}
});
this->bLengthSupported.setBinding([this]() { return this->bInternalLength != -1; });
this->bIsPlaying.setBinding([this]() {
return this->bPlaybackState == MprisPlaybackState::Playing;
});
this->bLoopState.setBinding([this]() {
const auto& status = this->bpLoopStatus.value();
if (status == "None") {
return MprisLoopState::None;
} else if (status == "Track") {
return MprisLoopState::Track;
} else if (status == "Playlist") {
return MprisLoopState::Playlist;
} else {
qWarning() << "Received unexpected LoopStatus for" << this << status;
return MprisLoopState::None;
}
});
// clang-format off
QObject::connect(this->player, &DBusMprisPlayer::Seeked, this, &MprisPlayer::onSeek);
QObject::connect(&this->playerProperties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished);
@@ -257,21 +230,19 @@ void MprisPlayer::setPosition(qlonglong position) {
}
void MprisPlayer::onExportedPositionChanged() {
if (!this->lengthSupported()) emit this->lengthChanged();
if (!this->bLengthSupported) emit this->lengthChanged();
}
void MprisPlayer::onSeek(qlonglong time) { this->setPosition(time); }
qreal MprisPlayer::length() const {
if (this->bInternalLength == -1) {
if (!this->bLengthSupported) {
return this->position(); // unsupported
} else {
return static_cast<qreal>(this->bInternalLength / 1000) / 1000; // NOLINT
}
}
bool MprisPlayer::lengthSupported() const { return this->bInternalLength != -1; }
bool MprisPlayer::volumeSupported() const { return this->pVolume.exists(); }
void MprisPlayer::setVolume(qreal volume) {
@@ -431,18 +402,11 @@ void MprisPlayer::setLoopState(MprisLoopState::Enum loopState) {
}
if (loopState == this->bLoopState) return;
QString loopStatusStr;
switch (loopState) {
case MprisLoopState::None: loopStatusStr = "None"; break;
case MprisLoopState::Track: loopStatusStr = "Track"; break;
case MprisLoopState::Playlist: loopStatusStr = "Playlist"; break;
default:
if (loopState < MprisLoopState::None || loopState > MprisLoopState::Playlist) {
qWarning() << "Cannot set loopState of" << this << "to unknown value" << loopState;
return;
}
this->bpLoopStatus = loopStatusStr;
this->bLoopState = loopState;
this->pLoopStatus.write();
}
@@ -495,3 +459,43 @@ void MprisPlayer::onGetAllFinished() {
}
} // namespace qs::service::mpris
namespace qs::dbus {
using namespace qs::service::mpris;
DBusResult<MprisPlaybackState::Enum>
DBusDataTransform<MprisPlaybackState::Enum>::fromWire(const QString& wire) {
if (wire == "Playing") return MprisPlaybackState::Playing;
if (wire == "Paused") return MprisPlaybackState::Paused;
if (wire == "Stopped") return MprisPlaybackState::Stopped;
return QDBusError(QDBusError::InvalidArgs, QString("Invalid MprisPlaybackState: %1").arg(wire));
}
QString DBusDataTransform<MprisPlaybackState::Enum>::toWire(MprisPlaybackState::Enum data) {
switch (data) {
case MprisPlaybackState::Playing: return "Playing";
case MprisPlaybackState::Paused: return "Paused";
case MprisPlaybackState::Stopped: return "Stopped";
default: qFatal() << "Tried to convert an invalid MprisPlaybackState to String"; return QString();
}
}
DBusResult<MprisLoopState::Enum>
DBusDataTransform<MprisLoopState::Enum>::fromWire(const QString& wire) {
if (wire == "None") return MprisLoopState::None;
if (wire == "Track") return MprisLoopState::Track;
if (wire == "Playlist") return MprisLoopState::Playlist;
return QDBusError(QDBusError::InvalidArgs, QString("Invalid MprisLoopState: %1").arg(wire));
}
QString DBusDataTransform<MprisLoopState::Enum>::toWire(MprisLoopState::Enum data) {
switch (data) {
case MprisLoopState::None: return "None";
case MprisLoopState::Track: return "Track";
case MprisLoopState::Playlist: return "Playlist";
default: qFatal() << "Tried to convert an invalid MprisLoopState to String"; return QString();
}
}
} // namespace qs::dbus
+65 -42
View File
@@ -51,6 +51,30 @@ public:
Q_INVOKABLE static QString toString(qs::service::mpris::MprisLoopState::Enum status);
};
}; // namespace qs::service::mpris
namespace qs::dbus {
template <>
struct DBusDataTransform<qs::service::mpris::MprisPlaybackState::Enum> {
using Wire = QString;
using Data = qs::service::mpris::MprisPlaybackState::Enum;
static DBusResult<Data> fromWire(const QString& wire);
static QString toWire(Data data);
};
template <>
struct DBusDataTransform<qs::service::mpris::MprisLoopState::Enum> {
using Wire = QString;
using Data = qs::service::mpris::MprisLoopState::Enum;
static DBusResult<Data> fromWire(const QString& wire);
static QString toWire(Data data);
};
}; // namespace qs::dbus
namespace qs::service::mpris {
///! A media player exposed over MPRIS.
/// A media player exposed over MPRIS.
///
@@ -117,7 +141,7 @@ class MprisPlayer: public QObject {
/// The length of the playing track, as seconds, with millisecond precision,
/// or the value of @@position if @@lengthSupported is false.
Q_PROPERTY(qreal length READ length NOTIFY lengthChanged);
Q_PROPERTY(bool lengthSupported READ lengthSupported NOTIFY lengthSupportedChanged);
Q_PROPERTY(bool lengthSupported READ default NOTIFY lengthSupportedChanged BINDABLE bindableLengthSupported);
/// The volume of the playing track from 0.0 to 1.0, or 1.0 if @@volumeSupported is false.
///
/// May only be written to if @@canControl and @@volumeSupported are true.
@@ -250,23 +274,23 @@ public:
[[nodiscard]] bool isValid() const;
[[nodiscard]] QString address() const;
[[nodiscard]] QBindable<bool> bindableCanControl() const { return &this->bCanControl; };
[[nodiscard]] QBindable<bool> bindableCanSeek() const { return &this->bCanSeek; };
[[nodiscard]] QBindable<bool> bindableCanGoNext() const { return &this->bCanGoNext; };
[[nodiscard]] QBindable<bool> bindableCanGoPrevious() const { return &this->bCanGoPrevious; };
[[nodiscard]] QBindable<bool> bindableCanPlay() const { return &this->bCanPlay; };
[[nodiscard]] QBindable<bool> bindableCanPause() const { return &this->bCanPause; };
[[nodiscard]] QBindable<bool> bindableCanControl() const { return &this->bCanControl; }
[[nodiscard]] QBindable<bool> bindableCanSeek() const { return &this->bCanSeek; }
[[nodiscard]] QBindable<bool> bindableCanGoNext() const { return &this->bCanGoNext; }
[[nodiscard]] QBindable<bool> bindableCanGoPrevious() const { return &this->bCanGoPrevious; }
[[nodiscard]] QBindable<bool> bindableCanPlay() const { return &this->bCanPlay; }
[[nodiscard]] QBindable<bool> bindableCanPause() const { return &this->bCanPause; }
[[nodiscard]] QBindable<bool> bindableCanTogglePlaying() const {
return &this->bCanTogglePlaying;
};
[[nodiscard]] QBindable<bool> bindableCanQuit() const { return &this->bCanQuit; };
[[nodiscard]] QBindable<bool> bindableCanRaise() const { return &this->bCanRaise; };
}
[[nodiscard]] QBindable<bool> bindableCanQuit() const { return &this->bCanQuit; }
[[nodiscard]] QBindable<bool> bindableCanRaise() const { return &this->bCanRaise; }
[[nodiscard]] QBindable<bool> bindableCanSetFullscreen() const {
return &this->bCanSetFullscreen;
};
}
[[nodiscard]] QBindable<QString> bindableIdentity() const { return &this->bIdentity; };
[[nodiscard]] QBindable<QString> bindableDesktopEntry() const { return &this->bDesktopEntry; };
[[nodiscard]] QBindable<QString> bindableIdentity() const { return &this->bIdentity; }
[[nodiscard]] QBindable<QString> bindableDesktopEntry() const { return &this->bDesktopEntry; }
[[nodiscard]] qlonglong positionMs() const;
[[nodiscard]] qreal position() const;
@@ -274,51 +298,51 @@ public:
void setPosition(qreal position);
[[nodiscard]] qreal length() const;
[[nodiscard]] bool lengthSupported() const;
[[nodiscard]] QBindable<bool> bindableLengthSupported() const { return &this->bLengthSupported; }
[[nodiscard]] qreal volume() const { return this->bVolume; };
[[nodiscard]] qreal volume() const { return this->bVolume; }
[[nodiscard]] bool volumeSupported() const;
void setVolume(qreal volume);
[[nodiscard]] QBindable<quint32> bindableUniqueId() const { return &this->bUniqueId; };
[[nodiscard]] QBindable<QVariantMap> bindableMetadata() const { return &this->bMetadata; };
[[nodiscard]] QBindable<QString> bindableTrackTitle() const { return &this->bTrackTitle; };
[[nodiscard]] QBindable<QString> bindableTrackAlbum() const { return &this->bTrackAlbum; };
[[nodiscard]] QBindable<quint32> bindableUniqueId() const { return &this->bUniqueId; }
[[nodiscard]] QBindable<QVariantMap> bindableMetadata() const { return &this->bMetadata; }
[[nodiscard]] QBindable<QString> bindableTrackTitle() const { return &this->bTrackTitle; }
[[nodiscard]] QBindable<QString> bindableTrackAlbum() const { return &this->bTrackAlbum; }
[[nodiscard]] QBindable<QString> bindableTrackAlbumArtist() const {
return &this->bTrackAlbumArtist;
};
[[nodiscard]] QBindable<QString> bindableTrackArtist() const { return &this->bTrackArtist; };
[[nodiscard]] QBindable<QString> bindableTrackArtUrl() const { return &this->bTrackArtUrl; };
}
[[nodiscard]] QBindable<QString> bindableTrackArtist() const { return &this->bTrackArtist; }
[[nodiscard]] QBindable<QString> bindableTrackArtUrl() const { return &this->bTrackArtUrl; }
[[nodiscard]] MprisPlaybackState::Enum playbackState() const { return this->bPlaybackState; };
[[nodiscard]] MprisPlaybackState::Enum playbackState() const { return this->bPlaybackState; }
void setPlaybackState(MprisPlaybackState::Enum playbackState);
[[nodiscard]] bool isPlaying() const { return this->bIsPlaying; };
[[nodiscard]] bool isPlaying() const { return this->bIsPlaying; }
void setPlaying(bool playing);
[[nodiscard]] MprisLoopState::Enum loopState() const { return this->bLoopState; };
[[nodiscard]] MprisLoopState::Enum loopState() const { return this->bLoopState; }
[[nodiscard]] bool loopSupported() const;
void setLoopState(MprisLoopState::Enum loopState);
[[nodiscard]] qreal rate() const { return this->bRate; };
[[nodiscard]] QBindable<qreal> bindableMinRate() const { return &this->bRate; };
[[nodiscard]] QBindable<qreal> bindableMaxRate() const { return &this->bRate; };
[[nodiscard]] qreal rate() const { return this->bRate; }
[[nodiscard]] QBindable<qreal> bindableMinRate() const { return &this->bRate; }
[[nodiscard]] QBindable<qreal> bindableMaxRate() const { return &this->bRate; }
void setRate(qreal rate);
[[nodiscard]] bool shuffle() const { return this->bShuffle; };
[[nodiscard]] bool shuffle() const { return this->bShuffle; }
[[nodiscard]] bool shuffleSupported() const;
void setShuffle(bool shuffle);
[[nodiscard]] bool fullscreen() const { return this->bFullscreen; };
[[nodiscard]] bool fullscreen() const { return this->bFullscreen; }
void setFullscreen(bool fullscreen);
[[nodiscard]] QBindable<QList<QString>> bindableSupportedUriSchemes() const {
return &this->bSupportedUriSchemes;
};
}
[[nodiscard]] QBindable<QList<QString>> bindableSupportedMimeTypes() const {
return &this->bSupportedMimeTypes;
};
}
signals:
/// The track has changed.
@@ -390,7 +414,7 @@ private:
void onPlaybackStatusUpdated();
// call instead of setting bpPosition
void setPosition(qlonglong position);
void requestPositionUpdate() { this->pPosition.requestUpdate(); };
void requestPositionUpdate() { this->pPosition.requestUpdate(); }
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bIdentity, &MprisPlayer::identityChanged);
@@ -404,13 +428,13 @@ private:
QS_DBUS_BINDABLE_PROPERTY_GROUP(MprisPlayer, appProperties);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pIdentity, bIdentity, appProperties, "Identity");
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pDesktopEntry, bDesktopEntry, appProperties, "DesktopEntry");
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanQuit, bCanQuit, appProperties, "CanQuit");
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanRaise, bCanRaise, appProperties, "CanRaise");
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pDesktopEntry, bDesktopEntry, appProperties, "DesktopEntry", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanQuit, bCanQuit, appProperties, "CanQuit", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanRaise, bCanRaise, appProperties, "CanRaise", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pFullscreen, bFullscreen, appProperties, "Fullscreen", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanSetFullscreen, bCanSetFullscreen, appProperties, "CanSetFullscreen", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pSupportedUriSchemes, bSupportedUriSchemes, appProperties, "SupportedUriSchemes");
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pSupportedMimeTypes, bSupportedMimeTypes, appProperties, "SupportedMimeTypes");
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pSupportedUriSchemes, bSupportedUriSchemes, appProperties, "SupportedUriSchemes", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pSupportedMimeTypes, bSupportedMimeTypes, appProperties, "SupportedMimeTypes", false);
Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bpCanPlay);
Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bpCanPause);
@@ -420,8 +444,6 @@ private:
Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QVariantMap, bpMetadata);
QS_BINDING_SUBSCRIBE_METHOD(MprisPlayer, bpMetadata, onMetadataChanged, onValueChanged);
Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(MprisPlayer, qlonglong, bpPosition, -1, &MprisPlayer::positionChanged);
Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bpPlaybackStatus);
Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bpLoopStatus);
Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bCanControl, &MprisPlayer::canControlChanged);
Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bCanPlay, &MprisPlayer::canPlayChanged);
@@ -447,6 +469,7 @@ private:
Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bTrackAlbumArtist, &MprisPlayer::trackAlbumArtistChanged);
Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bTrackArtUrl, &MprisPlayer::trackArtUrlChanged);
Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, qlonglong, bInternalLength, &MprisPlayer::lengthChanged);
Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bLengthSupported, &MprisPlayer::lengthSupportedChanged);
Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bShuffle, &MprisPlayer::shuffleChanged);
QS_DBUS_BINDABLE_PROPERTY_GROUP(MprisPlayer, playerProperties);
@@ -459,8 +482,8 @@ private:
QS_DBUS_PROPERTY_BINDING(MprisPlayer, qlonglong, pPosition, bpPosition, onPositionUpdated, playerProperties, "Position", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pVolume, bVolume, playerProperties, "Volume", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMetadata, bpMetadata, playerProperties, "Metadata");
QS_DBUS_PROPERTY_BINDING(MprisPlayer, void, pPlaybackStatus, bpPlaybackStatus, onPlaybackStatusUpdated, playerProperties, "PlaybackStatus", true);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pLoopStatus, bpLoopStatus, playerProperties, "LoopStatus", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, void, pPlaybackStatus, bPlaybackState, onPlaybackStatusUpdated, playerProperties, "PlaybackStatus", true);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pLoopStatus, bLoopState, playerProperties, "LoopStatus", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pRate, bRate, playerProperties, "Rate", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMinRate, bMinRate, playerProperties, "MinimumRate", false);
QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMaxRate, bMaxRate, playerProperties, "MaximumRate", false);
+2 -1
View File
@@ -9,13 +9,14 @@
#include <qobject.h>
#include <qqmllist.h>
#include "../../core/logcat.hpp"
#include "../../core/model.hpp"
#include "player.hpp"
namespace qs::service::mpris {
namespace {
Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg);
QS_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg);
}
MprisWatcher::MprisWatcher() {
Loaded 100 of 193 files, more files were not shown because too many files have changed in this diff. Show more