mirror of
https://github.com/quickshell-mirror/quickshell.git
synced 2026-10-29 03:05:05 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5431dd02d | ||
|
|
f0d5f48a82 | ||
|
|
1c026545e9 | ||
|
|
0416032a7c | ||
|
|
1644ed5e19 | ||
|
|
91c9db581e | ||
|
|
ab096b7e78 | ||
|
|
448623de5a | ||
|
|
dfededc901 | ||
|
|
4dad447570 | ||
|
|
3bbf39c67e | ||
|
|
f90bef2d99 | ||
|
|
db77c71c21 | ||
|
|
fcffbbced8 | ||
|
|
759bd721df | ||
|
|
63a6d27213 | ||
|
|
77de23bb71 | ||
|
|
7b417bb808 | ||
|
|
e55d519c28 | ||
|
|
ecc4a1249d | ||
|
|
6572a7f61d | ||
|
|
e885f4aec1 | ||
|
|
115d6717a8 | ||
|
|
91dcb41d22 | ||
|
|
201c559dcd | ||
|
|
78e3874ac6 | ||
|
|
986749cdb9 | ||
|
|
4d8055f1cd | ||
|
|
a45fc03c7d | ||
|
|
c40074dd56 | ||
|
|
3dfb7d8827 | ||
|
|
a2146f6394 | ||
|
|
5706c09e6f | ||
|
|
5ac9096c1c | ||
|
|
05fbead660 | ||
|
|
478aa2bda1 | ||
|
|
cee1f5837e | ||
|
|
71334bfcaf | ||
|
|
de25787451 | ||
|
|
b011cd9d33 | ||
|
|
1e1ba93713 | ||
|
|
59d29bb254 | ||
|
|
9604302415 | ||
|
|
479ff58f84 | ||
|
|
3b4ebc5f16 | ||
|
|
bb206e3a19 | ||
|
|
0c9c5be8dd | ||
|
|
49a3752b9d | ||
|
|
026aac3756 | ||
|
|
d7079b7524 | ||
|
|
6f774af11e | ||
|
|
5703fbae21 | ||
|
|
07ea4de248 | ||
|
|
2629e211fa | ||
|
|
b4c62b8ff9 | ||
|
|
1af08c0c52 | ||
|
|
4b35d7b51b | ||
|
|
3d594e16dd | ||
|
|
5d7e07508a | ||
|
|
87d99b866f | ||
|
|
7eff415b25 | ||
|
|
3cc7ced3a0 | ||
|
|
fb37be7611 | ||
|
|
9708d8212a | ||
|
|
0e6518a706 | ||
|
|
86591f122d | ||
|
|
f681e2016f | ||
|
|
1d02292fbf | ||
|
|
f842b84a5a | ||
|
|
d949f91347 | ||
|
|
27f97c3283 | ||
|
|
20c3da01f1 | ||
|
|
8be18c05ed | ||
|
|
98d09b5a36 | ||
|
|
8fc3e1cb6e | ||
|
|
c17ea54371 | ||
|
|
362c8e1b69 | ||
|
|
c115df8d34 | ||
|
|
02362c3e94 | ||
|
|
3d3b7f1c05 | ||
|
|
79b2204af8 | ||
|
|
95d0af8113 | ||
|
|
579d589290 | ||
|
|
9a30333405 | ||
|
|
d9164578a2 | ||
|
|
20322484b9 | ||
|
|
0499518143 | ||
|
|
0140356d99 | ||
|
|
71fe3d9165 | ||
|
|
517143adf9 | ||
|
|
05b5eccf2e |
No files matched your search
@@ -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
@@ -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
|
||||
)
|
||||
@@ -2,6 +2,10 @@
|
||||
See the [website](https://quickshell.outfoxxed.me) for more information
|
||||
and installation instructions.
|
||||
|
||||
This repo is hosted at:
|
||||
- https://git.outfoxxed.me/quickshell/quickshell
|
||||
- https://github.com/quickshell-mirror/quickshell
|
||||
|
||||
# Contributing / Development
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Version=1.5
|
||||
Type=Application
|
||||
NoDisplay=true
|
||||
|
||||
Name=Quickshell
|
||||
Icon=org.quickshell
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="724.635" height="724.635"><path fill="#359757" stroke="#359757" stroke-linecap="square" stroke-linejoin="round" stroke-width="74.755" d="m37.378 160.237 122.859-122.86h527.02v527.02l-122.86 122.86H37.378Z"/><path fill="#fff" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="12.201" d="M323.051 96.412a268.74 268.74 0 0 0-3.51.542c-4.052 14.481-7.815 29.941-14.904 42.692a230.02 230.02 0 0 0-59.406 24.679c-14.036-3.974-27.647-12.214-40.766-19.562a268.788 268.788 0 0 0-60.035 60.16c7.376 13.105 15.645 26.698 19.648 40.726a230.02 230.02 0 0 0-24.554 59.458c-12.735 7.115-28.186 10.913-42.66 14.994a268.789 268.789 0 0 0 .09 84.992c14.48 4.05 29.941 7.814 42.691 14.903a230.02 230.02 0 0 0 24.68 59.406c-3.974 14.037-12.215 27.647-19.563 40.766a268.788 268.788 0 0 0 60.161 60.035c13.104-7.376 26.696-15.645 40.725-19.648a230.02 230.02 0 0 0 59.457 24.555c7.116 12.735 10.913 28.186 14.995 42.659a268.788 268.788 0 0 0 84.99-.09c4.052-14.482 7.817-29.941 14.906-42.691a230.02 230.02 0 0 0 59.405-24.68c14.037 3.974 33.069 17.638 46.188 24.986a268.788 268.788 0 0 0 60.035-60.161c-7.376-13.105-21.068-32.12-25.071-46.149a230.02 230.02 0 0 0 24.555-59.457c12.735-7.116 28.186-10.912 42.659-14.993a268.788 268.788 0 0 0-.089-84.993c-14.482-4.051-29.942-7.814-42.692-14.904a230.02 230.02 0 0 0-24.68-59.405c3.974-14.037 12.216-27.647 19.565-40.767a268.788 268.788 0 0 0-60.161-60.035c-13.105 7.376-26.698 15.645-40.726 19.649a230.02 230.02 0 0 0-59.458-24.555c-7.115-12.735-10.913-28.187-14.994-42.66a268.788 268.788 0 0 0-81.481-.452zm15.778 106.85c58.282-8.328 116.455 15.865 151.643 63.065 35.19 47.2 41.766 109.86 17.144 163.337l-41.728-22.688s-38.558 31.44-57.344 63.012l23.893 36.326a160.78 160.78 0 0 1-46.633 15.058c-87.854 12.99-169.6-47.708-182.573-135.564-12.974-87.855 47.74-169.59 135.598-182.546Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1 @@
|
||||
Initial release
|
||||
@@ -0,0 +1,84 @@
|
||||
## Breaking Changes
|
||||
|
||||
- Files outside of the shell directory can no longer be referenced with relative paths, e.g. '../../foo.png'.
|
||||
- PanelWindow's Automatic exclusion mode now adds an exclusion zone for panels with a single anchor.
|
||||
- `QT_QUICK_CONTROLS_STYLE` and `QT_STYLE_OVERRIDE` are ignored unless `//@ pragma RespectSystemStyle` is set.
|
||||
|
||||
## New Features
|
||||
|
||||
### Root-Relative Imports
|
||||
|
||||
Quickshell 0.2 comes with a new method to import QML modules which is supported by QMLLS.
|
||||
This replaces "root:/" imports for QML modules.
|
||||
|
||||
The new syntax is `import qs.path.to.module`, where `path/to/module` is the path to
|
||||
a module/subdirectory relative to the config root (`qs`).
|
||||
|
||||
### Better LSP support
|
||||
|
||||
LSP support for Singletons and Root-Relative imports can be enabled by creating a file named
|
||||
`.qmlls.ini` in the shell root directory. Quickshell will detect this file and automatically
|
||||
populate it with an LSP configuration. This file should be gitignored in your configuration,
|
||||
as it is system dependent.
|
||||
|
||||
The generated configuration also includes QML import paths available to Quickshell, meaning
|
||||
QMLLS no longer requires the `-E` flag.
|
||||
|
||||
### Bluetooth Module
|
||||
|
||||
Quickshell can now manage your bluetooth devices through BlueZ. While authenticated pairing
|
||||
has not landed in 0.2, support for connecting and disconnecting devices, basic device information,
|
||||
and non-authenticated pairing are now supported.
|
||||
|
||||
### Other Features
|
||||
|
||||
- Added `HyprlandToplevel` and related toplevel/window management APIs in the Hyprland module.
|
||||
- Added `Quickshell.execDetached()`, which spawns a detached process without a `Process` object.
|
||||
- Added `Process.exec()` for easier reconfiguration of process commands when starting them.
|
||||
- Added `FloatingWindow.title`, which allows changing the title of a floating window.
|
||||
- Added `signal QsWindow.closed()`, fired when a window is closed externally.
|
||||
- Added support for inline replies in notifications, when supported by applications.
|
||||
- Added `DesktopEntry.startupWmClass` and `DesktopEntry.heuristicLookup()` to better identify toplevels.
|
||||
- Added `DesktopEntry.command` which can be run as an alternative to `DesktopEntry.execute()`.
|
||||
- Added `//@ pragma Internal`, which makes a QML component impossible to import outside of its module.
|
||||
- Added dead instance selection for some subcommands, such as `qs log` and `qs list`.
|
||||
|
||||
## Other Changes
|
||||
|
||||
- `Quickshell.shellRoot` has been renamed to `Quickshell.shellDir`.
|
||||
- PanelWindow margins opposite the window's anchorpoint are now added to exclusion zone.
|
||||
- stdout/stderr or detached processes and executed desktop entries are now hidden by default.
|
||||
- Various warnings caused by other applications Quickshell communicates with over D-BUS have been hidden in logs.
|
||||
- Quickshell's new logo is now shown in any floating windows.
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- Fixed pipewire device volume and mute states not updating before the device has been used.
|
||||
- Fixed a crash when changing the volume of any pipewire device on a sound card another removed device was using.
|
||||
- Fixed a crash when accessing a removed previous default pipewire node from the default sink/source changed signals.
|
||||
- Fixed session locks crashing if all monitors are disconnected.
|
||||
- Fixed session locks crashing if unsupported by the compositor.
|
||||
- Fixed a crash when creating a session lock and destroying it before acknowledged by the compositor.
|
||||
- Fixed window input masks not updating after a reload.
|
||||
- Fixed PanelWindows being unconfigurable unless `screen` was set under X11.
|
||||
- Fixed a crash when anchoring a popup to a zero sized `Item`.
|
||||
- Fixed `FileView` crashing if `watchChanges` was used.
|
||||
- Fixed `SocketServer` sockets disappearing after a reload.
|
||||
- Fixed `ScreencopyView` having incorrect rotation when displaying a rotated monitor.
|
||||
- Fixed `MarginWrapperManager` breaking pixel alignment of child items when centering.
|
||||
- Fixed `IpcHandler`, `NotificationServer` and `GlobalShortcut` not activating with certain QML structures.
|
||||
- Fixed tracking of QML incubator destruction and deregistration, which occasionally caused crashes.
|
||||
- Fixed FloatingWindows being constrained to the smallest window manager supported size unless max size was set.
|
||||
- Fixed `MprisPlayer.lengthSupported` not updating reactively.
|
||||
- Fixed normal tray icon being ignored when status is `NeedsAttention` and no attention icon is provided.
|
||||
- Fixed `HyprlandWorkspace.activate()` sending invalid commands to Hyprland for named or special workspaces.
|
||||
- Fixed file watcher occasionally breaking when using VSCode to edit QML files.
|
||||
- Fixed crashes when screencopy buffer creation fails.
|
||||
- Fixed a crash when wayland layer surfaces are recreated for the same window.
|
||||
- Fixed the `QsWindow` attached object not working when using `WlrLayershell` directly.
|
||||
- Fixed a crash when attempting to create a window without available VRAM.
|
||||
- Fixed OOM crash when failing to write to detailed log file.
|
||||
- Prevented distro logging configurations for Qt from interfering with Quickshell commands.
|
||||
- Removed the "QProcess destroyed for running process" warning when destroying `Process` objects.
|
||||
- Fixed `ColorQuantizer` printing a pointer to an error message instead of an error message.
|
||||
- Fixed notification pixmap rowstride warning showing for correct rowstrides.
|
||||
+85
-57
@@ -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
@@ -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,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
@@ -35,9 +35,9 @@
|
||||
pkg-config
|
||||
qtshadertools
|
||||
spirv-tools
|
||||
wayland-protocols))
|
||||
(inputs (list cli11
|
||||
jemalloc
|
||||
wayland-protocols
|
||||
cli11))
|
||||
(inputs (list jemalloc
|
||||
libdrm
|
||||
libxcb
|
||||
libxkbcommon
|
||||
|
||||
@@ -29,3 +29,7 @@ if (X11)
|
||||
endif()
|
||||
|
||||
add_subdirectory(services)
|
||||
|
||||
if (BLUETOOTH)
|
||||
add_subdirectory(bluetooth)
|
||||
endif()
|
||||
@@ -0,0 +1,42 @@
|
||||
set_source_files_properties(org.bluez.Adapter.xml PROPERTIES
|
||||
CLASSNAME DBusBluezAdapterInterface
|
||||
)
|
||||
|
||||
set_source_files_properties(org.bluez.Device.xml PROPERTIES
|
||||
CLASSNAME DBusBluezDeviceInterface
|
||||
)
|
||||
|
||||
qt_add_dbus_interface(DBUS_INTERFACES
|
||||
org.bluez.Adapter.xml
|
||||
dbus_adapter
|
||||
)
|
||||
|
||||
qt_add_dbus_interface(DBUS_INTERFACES
|
||||
org.bluez.Device.xml
|
||||
dbus_device
|
||||
)
|
||||
|
||||
qt_add_library(quickshell-bluetooth STATIC
|
||||
adapter.cpp
|
||||
bluez.cpp
|
||||
device.cpp
|
||||
${DBUS_INTERFACES}
|
||||
)
|
||||
|
||||
qt_add_qml_module(quickshell-bluetooth
|
||||
URI Quickshell.Bluetooth
|
||||
VERSION 0.1
|
||||
DEPENDENCIES QtQml
|
||||
)
|
||||
|
||||
install_qml_module(quickshell-bluetooth)
|
||||
|
||||
# dbus headers
|
||||
target_include_directories(quickshell-bluetooth PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
target_link_libraries(quickshell-bluetooth PRIVATE Qt::Qml Qt::DBus)
|
||||
qs_add_link_dependencies(quickshell-bluetooth quickshell-dbus)
|
||||
|
||||
qs_module_pch(quickshell-bluetooth SET dbus)
|
||||
|
||||
target_link_libraries(quickshell PRIVATE quickshell-bluetoothplugin)
|
||||
@@ -0,0 +1,224 @@
|
||||
#include "adapter.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdbusconnection.h>
|
||||
#include <qdbusextratypes.h>
|
||||
#include <qdbuspendingcall.h>
|
||||
#include <qdbuspendingreply.h>
|
||||
#include <qdebug.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qstring.h>
|
||||
#include <qstringliteral.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "../dbus/properties.hpp"
|
||||
#include "dbus_adapter.h"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logAdapter, "quickshell.bluetooth.adapter", QtWarningMsg);
|
||||
}
|
||||
|
||||
QString BluetoothAdapterState::toString(BluetoothAdapterState::Enum state) {
|
||||
switch (state) {
|
||||
case BluetoothAdapterState::Disabled: return QStringLiteral("Disabled");
|
||||
case BluetoothAdapterState::Enabled: return QStringLiteral("Enabled");
|
||||
case BluetoothAdapterState::Enabling: return QStringLiteral("Enabling");
|
||||
case BluetoothAdapterState::Disabling: return QStringLiteral("Disabling");
|
||||
case BluetoothAdapterState::Blocked: return QStringLiteral("Blocked");
|
||||
default: return QStringLiteral("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
BluetoothAdapter::BluetoothAdapter(const QString& path, QObject* parent): QObject(parent) {
|
||||
this->mInterface =
|
||||
new DBusBluezAdapterInterface("org.bluez", path, QDBusConnection::systemBus(), this);
|
||||
|
||||
if (!this->mInterface->isValid()) {
|
||||
qCWarning(logAdapter) << "Could not create DBus interface for adapter at" << path;
|
||||
this->mInterface = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
this->properties.setInterface(this->mInterface);
|
||||
}
|
||||
|
||||
QString BluetoothAdapter::adapterId() const {
|
||||
auto path = this->path();
|
||||
return path.sliced(path.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setEnabled(bool enabled) {
|
||||
if (enabled == this->bEnabled) return;
|
||||
|
||||
if (enabled && this->bState == BluetoothAdapterState::Blocked) {
|
||||
qCCritical(logAdapter) << "Cannot enable adapter because it is blocked by rfkill.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->bEnabled = enabled;
|
||||
this->pEnabled.write();
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setDiscoverable(bool discoverable) {
|
||||
if (discoverable == this->bDiscoverable) return;
|
||||
this->bDiscoverable = discoverable;
|
||||
this->pDiscoverable.write();
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setDiscovering(bool discovering) {
|
||||
if (discovering) {
|
||||
this->startDiscovery();
|
||||
} else {
|
||||
this->stopDiscovery();
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setDiscoverableTimeout(quint32 timeout) {
|
||||
if (timeout == this->bDiscoverableTimeout) return;
|
||||
this->bDiscoverableTimeout = timeout;
|
||||
this->pDiscoverableTimeout.write();
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setPairable(bool pairable) {
|
||||
if (pairable == this->bPairable) return;
|
||||
this->bPairable = pairable;
|
||||
this->pPairable.write();
|
||||
}
|
||||
|
||||
void BluetoothAdapter::setPairableTimeout(quint32 timeout) {
|
||||
if (timeout == this->bPairableTimeout) return;
|
||||
this->bPairableTimeout = timeout;
|
||||
this->pPairableTimeout.write();
|
||||
}
|
||||
|
||||
void BluetoothAdapter::addInterface(const QString& interface, const QVariantMap& properties) {
|
||||
if (interface == "org.bluez.Adapter1") {
|
||||
this->properties.updatePropertySet(properties, false);
|
||||
qCDebug(logAdapter) << "Updated Adapter properties for" << this;
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothAdapter::removeDevice(const QString& devicePath) {
|
||||
qCDebug(logAdapter) << "Removing device" << devicePath << "from adapter" << this;
|
||||
|
||||
auto reply = this->mInterface->RemoveDevice(QDBusObjectPath(devicePath));
|
||||
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this, devicePath](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logAdapter).nospace()
|
||||
<< "Failed to remove device " << devicePath << " from adapter" << this << ": "
|
||||
<< reply.error().message();
|
||||
} else {
|
||||
qCDebug(logAdapter) << "Successfully removed device" << devicePath << "from adapter"
|
||||
<< this;
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothAdapter::startDiscovery() {
|
||||
if (this->bDiscovering) return;
|
||||
qCDebug(logAdapter) << "Starting discovery for adapter" << this;
|
||||
|
||||
auto reply = this->mInterface->StartDiscovery();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logAdapter).nospace()
|
||||
<< "Failed to start discovery on adapter" << this << ": " << reply.error().message();
|
||||
} else {
|
||||
qCDebug(logAdapter) << "Successfully started discovery on adapter" << this;
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothAdapter::stopDiscovery() {
|
||||
if (!this->bDiscovering) return;
|
||||
qCDebug(logAdapter) << "Stopping discovery for adapter" << this;
|
||||
|
||||
auto reply = this->mInterface->StopDiscovery();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logAdapter).nospace()
|
||||
<< "Failed to stop discovery on adapter " << this << ": " << reply.error().message();
|
||||
} else {
|
||||
qCDebug(logAdapter) << "Successfully stopped discovery on adapter" << this;
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
using namespace qs::bluetooth;
|
||||
|
||||
DBusResult<BluetoothAdapterState::Enum>
|
||||
DBusDataTransform<BluetoothAdapterState::Enum>::fromWire(const Wire& wire) {
|
||||
if (wire == QStringLiteral("off")) {
|
||||
return BluetoothAdapterState::Disabled;
|
||||
} else if (wire == QStringLiteral("on")) {
|
||||
return BluetoothAdapterState::Enabled;
|
||||
} else if (wire == QStringLiteral("off-enabling")) {
|
||||
return BluetoothAdapterState::Enabling;
|
||||
} else if (wire == QStringLiteral("on-disabling")) {
|
||||
return BluetoothAdapterState::Disabling;
|
||||
} else if (wire == QStringLiteral("off-blocked")) {
|
||||
return BluetoothAdapterState::Blocked;
|
||||
} else {
|
||||
return QDBusError(
|
||||
QDBusError::InvalidArgs,
|
||||
QString("Invalid BluetoothAdapterState: %1").arg(wire)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter) {
|
||||
auto saver = QDebugStateSaver(debug);
|
||||
|
||||
if (adapter) {
|
||||
debug.nospace() << "BluetoothAdapter(" << static_cast<const void*>(adapter)
|
||||
<< ", path=" << adapter->path() << ")";
|
||||
} else {
|
||||
debug << "BluetoothAdapter(nullptr)";
|
||||
}
|
||||
|
||||
return debug;
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../core/doc.hpp"
|
||||
#include "../core/model.hpp"
|
||||
#include "../dbus/properties.hpp"
|
||||
#include "dbus_adapter.h"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
///! Power state of a Bluetooth adapter.
|
||||
class BluetoothAdapterState: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
||||
public:
|
||||
enum Enum : quint8 {
|
||||
/// The adapter is powered off.
|
||||
Disabled = 0,
|
||||
/// The adapter is powered on.
|
||||
Enabled = 1,
|
||||
/// The adapter is transitioning from off to on.
|
||||
Enabling = 2,
|
||||
/// The adapter is transitioning from on to off.
|
||||
Disabling = 3,
|
||||
/// The adapter is blocked by rfkill.
|
||||
Blocked = 4,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
Q_INVOKABLE static QString toString(BluetoothAdapterState::Enum state);
|
||||
};
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
template <>
|
||||
struct DBusDataTransform<qs::bluetooth::BluetoothAdapterState::Enum> {
|
||||
using Wire = QString;
|
||||
using Data = qs::bluetooth::BluetoothAdapterState::Enum;
|
||||
static DBusResult<Data> fromWire(const Wire& wire);
|
||||
};
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
class BluetoothAdapter;
|
||||
class BluetoothDevice;
|
||||
|
||||
///! A Bluetooth adapter
|
||||
class BluetoothAdapter: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("");
|
||||
// clang-format off
|
||||
/// System provided name of the adapter. See @@adapterId for the internal identifier.
|
||||
Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName);
|
||||
/// True if the adapter is currently enabled. More detailed state is available from @@state.
|
||||
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
|
||||
/// Detailed power state of the adapter.
|
||||
Q_PROPERTY(BluetoothAdapterState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
|
||||
/// True if the adapter can be discovered by other bluetooth devices.
|
||||
Q_PROPERTY(bool discoverable READ discoverable WRITE setDiscoverable NOTIFY discoverableChanged);
|
||||
/// Timeout in seconds for how long the adapter stays discoverable after @@discoverable is set to true.
|
||||
/// A value of 0 means the adapter stays discoverable forever.
|
||||
Q_PROPERTY(quint32 discoverableTimeout READ discoverableTimeout WRITE setDiscoverableTimeout NOTIFY discoverableTimeoutChanged);
|
||||
/// True if the adapter is scanning for new devices.
|
||||
Q_PROPERTY(bool discovering READ discovering WRITE setDiscovering NOTIFY discoveringChanged);
|
||||
/// True if the adapter is accepting incoming pairing requests.
|
||||
///
|
||||
/// This only affects incoming pairing requests and should typically only be changed
|
||||
/// by system settings applications. Defaults to true.
|
||||
Q_PROPERTY(bool pairable READ pairable WRITE setPairable NOTIFY pairableChanged);
|
||||
/// Timeout in seconds for how long the adapter stays pairable after @@pairable is set to true.
|
||||
/// A value of 0 means the adapter stays pairable forever. Defaults to 0.
|
||||
Q_PROPERTY(quint32 pairableTimeout READ pairableTimeout WRITE setPairableTimeout NOTIFY pairableTimeoutChanged);
|
||||
/// Bluetooth devices connected to this adapter.
|
||||
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothDevice>*);
|
||||
Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
|
||||
/// The internal ID of the adapter (e.g., "hci0").
|
||||
Q_PROPERTY(QString adapterId READ adapterId CONSTANT);
|
||||
/// DBus path of the adapter under the `org.bluez` system service.
|
||||
Q_PROPERTY(QString dbusPath READ path CONSTANT);
|
||||
// clang-format on
|
||||
|
||||
public:
|
||||
explicit BluetoothAdapter(const QString& path, QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] bool isValid() const { return this->mInterface->isValid(); }
|
||||
[[nodiscard]] QString path() const { return this->mInterface->path(); }
|
||||
[[nodiscard]] QString adapterId() const;
|
||||
|
||||
[[nodiscard]] bool enabled() const { return this->bEnabled; }
|
||||
void setEnabled(bool enabled);
|
||||
|
||||
[[nodiscard]] bool discoverable() const { return this->bDiscoverable; }
|
||||
void setDiscoverable(bool discoverable);
|
||||
|
||||
[[nodiscard]] bool discovering() const { return this->bDiscovering; }
|
||||
void setDiscovering(bool discovering);
|
||||
|
||||
[[nodiscard]] quint32 discoverableTimeout() const { return this->bDiscoverableTimeout; }
|
||||
void setDiscoverableTimeout(quint32 timeout);
|
||||
|
||||
[[nodiscard]] bool pairable() const { return this->bPairable; }
|
||||
void setPairable(bool pairable);
|
||||
|
||||
[[nodiscard]] quint32 pairableTimeout() const { return this->bPairableTimeout; }
|
||||
void setPairableTimeout(quint32 timeout);
|
||||
|
||||
[[nodiscard]] QBindable<QString> bindableName() { return &this->bName; }
|
||||
[[nodiscard]] QBindable<bool> bindableEnabled() { return &this->bEnabled; }
|
||||
[[nodiscard]] QBindable<BluetoothAdapterState::Enum> bindableState() { return &this->bState; }
|
||||
[[nodiscard]] QBindable<bool> bindableDiscoverable() { return &this->bDiscoverable; }
|
||||
[[nodiscard]] QBindable<quint32> bindableDiscoverableTimeout() {
|
||||
return &this->bDiscoverableTimeout;
|
||||
}
|
||||
[[nodiscard]] QBindable<bool> bindableDiscovering() { return &this->bDiscovering; }
|
||||
[[nodiscard]] QBindable<bool> bindablePairable() { return &this->bPairable; }
|
||||
[[nodiscard]] QBindable<quint32> bindablePairableTimeout() { return &this->bPairableTimeout; }
|
||||
[[nodiscard]] ObjectModel<BluetoothDevice>* devices() { return &this->mDevices; }
|
||||
|
||||
void addInterface(const QString& interface, const QVariantMap& properties);
|
||||
void removeDevice(const QString& devicePath);
|
||||
|
||||
void startDiscovery();
|
||||
void stopDiscovery();
|
||||
|
||||
signals:
|
||||
void nameChanged();
|
||||
void enabledChanged();
|
||||
void stateChanged();
|
||||
void discoverableChanged();
|
||||
void discoverableTimeoutChanged();
|
||||
void discoveringChanged();
|
||||
void pairableChanged();
|
||||
void pairableTimeoutChanged();
|
||||
|
||||
private:
|
||||
DBusBluezAdapterInterface* mInterface = nullptr;
|
||||
ObjectModel<BluetoothDevice> mDevices {this};
|
||||
|
||||
// clang-format off
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, QString, bName, &BluetoothAdapter::nameChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bEnabled, &BluetoothAdapter::enabledChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, BluetoothAdapterState::Enum, bState, &BluetoothAdapter::stateChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscoverable, &BluetoothAdapter::discoverableChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bDiscoverableTimeout, &BluetoothAdapter::discoverableTimeoutChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscovering, &BluetoothAdapter::discoveringChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bPairable, &BluetoothAdapter::pairableChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bPairableTimeout, &BluetoothAdapter::pairableTimeoutChanged);
|
||||
|
||||
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothAdapter, properties);
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pName, bName, properties, "Alias");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pEnabled, bEnabled, properties, "Powered");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pState, bState, properties, "PowerState");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverable, bDiscoverable, properties, "Discoverable");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverableTimeout, bDiscoverableTimeout, properties, "DiscoverableTimeout");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscovering, bDiscovering, properties, "Discovering");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairable, bPairable, properties, "Pairable");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairableTimeout, bPairableTimeout, properties, "PairableTimeout");
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter);
|
||||
@@ -0,0 +1,168 @@
|
||||
#include "bluez.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdbusconnection.h>
|
||||
#include <qdbusextratypes.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "../dbus/dbus_objectmanager_types.hpp"
|
||||
#include "../dbus/objectmanager.hpp"
|
||||
#include "adapter.hpp"
|
||||
#include "device.hpp"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logBluetooth, "quickshell.bluetooth", QtWarningMsg);
|
||||
}
|
||||
|
||||
Bluez* Bluez::instance() {
|
||||
static auto* instance = new Bluez();
|
||||
return instance;
|
||||
}
|
||||
|
||||
Bluez::Bluez() { this->init(); }
|
||||
|
||||
void Bluez::updateDefaultAdapter() {
|
||||
const auto& adapters = this->mAdapters.valueList();
|
||||
this->bDefaultAdapter = adapters.empty() ? nullptr : adapters.first();
|
||||
}
|
||||
|
||||
void Bluez::init() {
|
||||
qCDebug(logBluetooth) << "Connecting to BlueZ";
|
||||
|
||||
auto bus = QDBusConnection::systemBus();
|
||||
|
||||
if (!bus.isConnected()) {
|
||||
qCWarning(logBluetooth) << "Could not connect to DBus. Bluetooth integration is not available.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->objectManager = new qs::dbus::DBusObjectManager(this);
|
||||
|
||||
QObject::connect(
|
||||
this->objectManager,
|
||||
&qs::dbus::DBusObjectManager::interfacesAdded,
|
||||
this,
|
||||
&Bluez::onInterfacesAdded
|
||||
);
|
||||
|
||||
QObject::connect(
|
||||
this->objectManager,
|
||||
&qs::dbus::DBusObjectManager::interfacesRemoved,
|
||||
this,
|
||||
&Bluez::onInterfacesRemoved
|
||||
);
|
||||
|
||||
if (!this->objectManager->setInterface("org.bluez", "/", bus)) {
|
||||
qCDebug(logBluetooth) << "BlueZ is not running. Bluetooth integration will not work.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Bluez::onInterfacesAdded(
|
||||
const QDBusObjectPath& path,
|
||||
const DBusObjectManagerInterfaces& interfaces
|
||||
) {
|
||||
if (auto* adapter = this->mAdapterMap.value(path.path())) {
|
||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
||||
adapter->addInterface(interface, properties);
|
||||
}
|
||||
} else if (auto* device = this->mDeviceMap.value(path.path())) {
|
||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
||||
device->addInterface(interface, properties);
|
||||
}
|
||||
} else if (interfaces.contains("org.bluez.Adapter1")) {
|
||||
auto* adapter = new BluetoothAdapter(path.path(), this);
|
||||
|
||||
if (!adapter->isValid()) {
|
||||
qCWarning(logBluetooth) << "Adapter path is not valid, cannot track: " << device;
|
||||
delete adapter;
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logBluetooth) << "Tracked new adapter" << adapter;
|
||||
|
||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
||||
adapter->addInterface(interface, properties);
|
||||
}
|
||||
|
||||
for (auto* device: this->mDevices.valueList()) {
|
||||
if (device->adapterPath() == path) {
|
||||
adapter->devices()->insertObject(device);
|
||||
qCDebug(logBluetooth) << "Added tracked device" << device << "to new adapter" << adapter;
|
||||
emit device->adapterChanged();
|
||||
}
|
||||
}
|
||||
|
||||
this->mAdapterMap.insert(path.path(), adapter);
|
||||
this->mAdapters.insertObject(adapter);
|
||||
this->updateDefaultAdapter();
|
||||
} else if (interfaces.contains("org.bluez.Device1")) {
|
||||
auto* device = new BluetoothDevice(path.path(), this);
|
||||
|
||||
if (!device->isValid()) {
|
||||
qCWarning(logBluetooth) << "Device path is not valid, cannot track: " << device;
|
||||
delete device;
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logBluetooth) << "Tracked new device" << device;
|
||||
|
||||
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
|
||||
device->addInterface(interface, properties);
|
||||
}
|
||||
|
||||
if (auto* adapter = device->adapter()) {
|
||||
adapter->devices()->insertObject(device);
|
||||
qCDebug(logBluetooth) << "Added device" << device << "to adapter" << adapter;
|
||||
}
|
||||
|
||||
this->mDeviceMap.insert(path.path(), device);
|
||||
this->mDevices.insertObject(device);
|
||||
}
|
||||
}
|
||||
|
||||
void Bluez::onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces) {
|
||||
if (auto* adapter = this->mAdapterMap.value(path.path())) {
|
||||
if (interfaces.contains("org.bluez.Adapter1")) {
|
||||
qCDebug(logBluetooth) << "Adapter removed:" << adapter;
|
||||
|
||||
this->mAdapterMap.remove(path.path());
|
||||
this->mAdapters.removeObject(adapter);
|
||||
this->updateDefaultAdapter();
|
||||
delete adapter;
|
||||
}
|
||||
} else if (auto* device = this->mDeviceMap.value(path.path())) {
|
||||
if (interfaces.contains("org.bluez.Device1")) {
|
||||
qCDebug(logBluetooth) << "Device removed:" << device;
|
||||
|
||||
if (auto* adapter = device->adapter()) {
|
||||
adapter->devices()->removeObject(device);
|
||||
}
|
||||
|
||||
this->mDeviceMap.remove(path.path());
|
||||
this->mDevices.removeObject(device);
|
||||
delete device;
|
||||
} else {
|
||||
for (const auto& interface: interfaces) {
|
||||
device->removeInterface(interface);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BluezQml::BluezQml() {
|
||||
QObject::connect(
|
||||
Bluez::instance(),
|
||||
&Bluez::defaultAdapterChanged,
|
||||
this,
|
||||
&BluezQml::defaultAdapterChanged
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qhash.h>
|
||||
#include <qobject.h>
|
||||
#include <qproperty.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../core/doc.hpp"
|
||||
#include "../core/model.hpp"
|
||||
#include "../dbus/dbus_objectmanager_types.hpp"
|
||||
#include "../dbus/objectmanager.hpp"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
class BluetoothAdapter;
|
||||
class BluetoothDevice;
|
||||
|
||||
class Bluez: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
[[nodiscard]] ObjectModel<BluetoothAdapter>* adapters() { return &this->mAdapters; }
|
||||
[[nodiscard]] ObjectModel<BluetoothDevice>* devices() { return &this->mDevices; }
|
||||
|
||||
[[nodiscard]] BluetoothAdapter* adapter(const QString& path) {
|
||||
return this->mAdapterMap.value(path);
|
||||
}
|
||||
|
||||
static Bluez* instance();
|
||||
|
||||
signals:
|
||||
void defaultAdapterChanged();
|
||||
|
||||
private slots:
|
||||
void
|
||||
onInterfacesAdded(const QDBusObjectPath& path, const DBusObjectManagerInterfaces& interfaces);
|
||||
void onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces);
|
||||
void updateDefaultAdapter();
|
||||
|
||||
private:
|
||||
explicit Bluez();
|
||||
void init();
|
||||
|
||||
qs::dbus::DBusObjectManager* objectManager = nullptr;
|
||||
QHash<QString, BluetoothAdapter*> mAdapterMap;
|
||||
QHash<QString, BluetoothDevice*> mDeviceMap;
|
||||
ObjectModel<BluetoothAdapter> mAdapters {this};
|
||||
ObjectModel<BluetoothDevice> mDevices {this};
|
||||
|
||||
public:
|
||||
Q_OBJECT_BINDABLE_PROPERTY(
|
||||
Bluez,
|
||||
BluetoothAdapter*,
|
||||
bDefaultAdapter,
|
||||
&Bluez::defaultAdapterChanged
|
||||
);
|
||||
};
|
||||
|
||||
///! Bluetooth manager
|
||||
/// Provides access to bluetooth devices and adapters.
|
||||
class BluezQml: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_NAMED_ELEMENT(Bluetooth);
|
||||
QML_SINGLETON;
|
||||
// clang-format off
|
||||
/// The default bluetooth adapter. Usually there is only one.
|
||||
Q_PROPERTY(BluetoothAdapter* defaultAdapter READ default NOTIFY defaultAdapterChanged BINDABLE bindableDefaultAdapter);
|
||||
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothAdapter>*);
|
||||
/// A list of all bluetooth adapters. See @@defaultAdapter for the default.
|
||||
Q_PROPERTY(UntypedObjectModel* adapters READ adapters CONSTANT);
|
||||
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothDevice>*);
|
||||
/// A list of all connected bluetooth devices across all adapters.
|
||||
/// See @@BluetoothAdapter.devices for the devices connected to a single adapter.
|
||||
Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
|
||||
// clang-format on
|
||||
|
||||
signals:
|
||||
void defaultAdapterChanged();
|
||||
|
||||
public:
|
||||
explicit BluezQml();
|
||||
|
||||
[[nodiscard]] static ObjectModel<BluetoothAdapter>* adapters() {
|
||||
return Bluez::instance()->adapters();
|
||||
}
|
||||
|
||||
[[nodiscard]] static ObjectModel<BluetoothDevice>* devices() {
|
||||
return Bluez::instance()->devices();
|
||||
}
|
||||
|
||||
[[nodiscard]] static QBindable<BluetoothAdapter*> bindableDefaultAdapter() {
|
||||
return &Bluez::instance()->bDefaultAdapter;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
@@ -0,0 +1,319 @@
|
||||
#include "device.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdbusconnection.h>
|
||||
#include <qdbuspendingcall.h>
|
||||
#include <qdbuspendingreply.h>
|
||||
#include <qdebug.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qstring.h>
|
||||
#include <qstringliteral.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "../dbus/properties.hpp"
|
||||
#include "adapter.hpp"
|
||||
#include "bluez.hpp"
|
||||
#include "dbus_device.h"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logDevice, "quickshell.bluetooth.device", QtWarningMsg);
|
||||
}
|
||||
|
||||
QString BluetoothDeviceState::toString(BluetoothDeviceState::Enum state) {
|
||||
switch (state) {
|
||||
case BluetoothDeviceState::Disconnected: return QStringLiteral("Disconnected");
|
||||
case BluetoothDeviceState::Connected: return QStringLiteral("Connected");
|
||||
case BluetoothDeviceState::Disconnecting: return QStringLiteral("Disconnecting");
|
||||
case BluetoothDeviceState::Connecting: return QStringLiteral("Connecting");
|
||||
default: return QStringLiteral("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
BluetoothDevice::BluetoothDevice(const QString& path, QObject* parent): QObject(parent) {
|
||||
this->mInterface =
|
||||
new DBusBluezDeviceInterface("org.bluez", path, QDBusConnection::systemBus(), this);
|
||||
|
||||
if (!this->mInterface->isValid()) {
|
||||
qCWarning(logDevice) << "Could not create DBus interface for device at" << path;
|
||||
delete this->mInterface;
|
||||
this->mInterface = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
this->properties.setInterface(this->mInterface);
|
||||
}
|
||||
|
||||
BluetoothAdapter* BluetoothDevice::adapter() const {
|
||||
return Bluez::instance()->adapter(this->bAdapterPath.value().path());
|
||||
}
|
||||
|
||||
void BluetoothDevice::setConnected(bool connected) {
|
||||
if (connected == this->bConnected) return;
|
||||
|
||||
if (connected) {
|
||||
this->connect();
|
||||
} else {
|
||||
this->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothDevice::setTrusted(bool trusted) {
|
||||
if (trusted == this->bTrusted) return;
|
||||
this->bTrusted = trusted;
|
||||
this->pTrusted.write();
|
||||
}
|
||||
|
||||
void BluetoothDevice::setBlocked(bool blocked) {
|
||||
if (blocked == this->bBlocked) return;
|
||||
this->bBlocked = blocked;
|
||||
this->pBlocked.write();
|
||||
}
|
||||
|
||||
void BluetoothDevice::setName(const QString& name) {
|
||||
if (name == this->bName) return;
|
||||
this->bName = name;
|
||||
this->pName.write();
|
||||
}
|
||||
|
||||
void BluetoothDevice::setWakeAllowed(bool wakeAllowed) {
|
||||
if (wakeAllowed == this->bWakeAllowed) return;
|
||||
this->bWakeAllowed = wakeAllowed;
|
||||
this->pWakeAllowed.write();
|
||||
}
|
||||
|
||||
void BluetoothDevice::connect() {
|
||||
if (this->bConnected) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already connected";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->bState == BluetoothDeviceState::Connecting) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already connecting";
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logDevice) << "Connecting to device" << this;
|
||||
this->bState = BluetoothDeviceState::Connecting;
|
||||
|
||||
auto reply = this->mInterface->Connect();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDevice).nospace()
|
||||
<< "Failed to connect to device " << this << ": " << reply.error().message();
|
||||
|
||||
this->bState = this->bConnected ? BluetoothDeviceState::Connected
|
||||
: BluetoothDeviceState::Disconnected;
|
||||
} else {
|
||||
qCDebug(logDevice) << "Successfully connected to to device" << this;
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothDevice::disconnect() {
|
||||
if (!this->bConnected) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already disconnected";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->bState == BluetoothDeviceState::Disconnecting) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already disconnecting";
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logDevice) << "Disconnecting from device" << this;
|
||||
this->bState = BluetoothDeviceState::Disconnecting;
|
||||
|
||||
auto reply = this->mInterface->Disconnect();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDevice).nospace()
|
||||
<< "Failed to disconnect from device " << this << ": " << reply.error().message();
|
||||
|
||||
this->bState = this->bConnected ? BluetoothDeviceState::Connected
|
||||
: BluetoothDeviceState::Disconnected;
|
||||
} else {
|
||||
qCDebug(logDevice) << "Successfully disconnected from from device" << this;
|
||||
}
|
||||
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothDevice::pair() {
|
||||
if (this->bPaired) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already paired";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->bPairing) {
|
||||
qCCritical(logDevice) << "Device" << this << "is already pairing";
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logDevice) << "Pairing with device" << this;
|
||||
this->bPairing = true;
|
||||
|
||||
auto reply = this->mInterface->Pair();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDevice).nospace()
|
||||
<< "Failed to pair with device " << this << ": " << reply.error().message();
|
||||
} else {
|
||||
qCDebug(logDevice) << "Successfully initiated pairing with device" << this;
|
||||
}
|
||||
|
||||
this->bPairing = false;
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothDevice::cancelPair() {
|
||||
if (!this->bPairing) {
|
||||
qCCritical(logDevice) << "Device" << this << "is not currently pairing";
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(logDevice) << "Cancelling pairing with device" << this;
|
||||
|
||||
auto reply = this->mInterface->CancelPairing();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<> reply = *watcher;
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDevice) << "Failed to cancel pairing with device" << this << ":"
|
||||
<< reply.error().message();
|
||||
} else {
|
||||
qCDebug(logDevice) << "Successfully cancelled pairing with device" << this;
|
||||
}
|
||||
|
||||
this->bPairing = false;
|
||||
delete watcher;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void BluetoothDevice::forget() {
|
||||
if (!this->mInterface || !this->mInterface->isValid()) {
|
||||
qCCritical(logDevice) << "Cannot forget - device interface is invalid";
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto* adapter = Bluez::instance()->adapter(this->bAdapterPath.value().path())) {
|
||||
qCDebug(logDevice) << "Forgetting device" << this << "via adapter" << adapter;
|
||||
adapter->removeDevice(this->path());
|
||||
} else {
|
||||
qCCritical(logDevice) << "Could not find adapter for path" << this->bAdapterPath.value().path()
|
||||
<< "to forget from";
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothDevice::addInterface(const QString& interface, const QVariantMap& properties) {
|
||||
if (interface == "org.bluez.Device1") {
|
||||
this->properties.updatePropertySet(properties, false);
|
||||
qCDebug(logDevice) << "Updated Device properties for" << this;
|
||||
} else if (interface == "org.bluez.Battery1") {
|
||||
if (!this->mBatteryInterface) {
|
||||
this->mBatteryInterface = new QDBusInterface(
|
||||
"org.bluez",
|
||||
this->path(),
|
||||
"org.bluez.Battery1",
|
||||
QDBusConnection::systemBus(),
|
||||
this
|
||||
);
|
||||
|
||||
if (!this->mBatteryInterface->isValid()) {
|
||||
qCWarning(logDevice) << "Could not create Battery interface for device at" << this;
|
||||
delete this->mBatteryInterface;
|
||||
this->mBatteryInterface = nullptr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->batteryProperties.setInterface(this->mBatteryInterface);
|
||||
this->batteryProperties.updatePropertySet(properties, false);
|
||||
|
||||
emit this->batteryAvailableChanged();
|
||||
qCDebug(logDevice) << "Updated Battery properties for" << this;
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothDevice::removeInterface(const QString& interface) {
|
||||
if (interface == "org.bluez.Battery1" && this->mBatteryInterface) {
|
||||
this->batteryProperties.setInterface(nullptr);
|
||||
delete this->mBatteryInterface;
|
||||
this->mBatteryInterface = nullptr;
|
||||
this->bBattery = 0;
|
||||
|
||||
emit this->batteryAvailableChanged();
|
||||
qCDebug(logDevice) << "Battery interface removed from device" << this;
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothDevice::onConnectedChanged() {
|
||||
this->bState =
|
||||
this->bConnected ? BluetoothDeviceState::Connected : BluetoothDeviceState::Disconnected;
|
||||
emit this->connectedChanged();
|
||||
}
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
using namespace qs::bluetooth;
|
||||
|
||||
DBusResult<qreal> DBusDataTransform<BatteryPercentage>::fromWire(quint8 percentage) {
|
||||
return DBusResult(percentage * 0.01);
|
||||
}
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device) {
|
||||
auto saver = QDebugStateSaver(debug);
|
||||
|
||||
if (device) {
|
||||
debug.nospace() << "BluetoothDevice(" << static_cast<const void*>(device)
|
||||
<< ", path=" << device->path() << ")";
|
||||
} else {
|
||||
debug << "BluetoothDevice(nullptr)";
|
||||
}
|
||||
|
||||
return debug;
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
#pragma once
|
||||
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../dbus/properties.hpp"
|
||||
#include "dbus_device.h"
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
///! Connection state of a Bluetooth device.
|
||||
class BluetoothDeviceState: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_SINGLETON;
|
||||
|
||||
public:
|
||||
enum Enum : quint8 {
|
||||
/// The device is not connected.
|
||||
Disconnected = 0,
|
||||
/// The device is connected.
|
||||
Connected = 1,
|
||||
/// The device is disconnecting.
|
||||
Disconnecting = 2,
|
||||
/// The device is connecting.
|
||||
Connecting = 3,
|
||||
};
|
||||
Q_ENUM(Enum);
|
||||
|
||||
Q_INVOKABLE static QString toString(BluetoothDeviceState::Enum state);
|
||||
};
|
||||
|
||||
struct BatteryPercentage {};
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
template <>
|
||||
struct DBusDataTransform<qs::bluetooth::BatteryPercentage> {
|
||||
using Wire = quint8;
|
||||
using Data = qreal;
|
||||
static DBusResult<Data> fromWire(Wire percentage);
|
||||
};
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
namespace qs::bluetooth {
|
||||
|
||||
class BluetoothAdapter;
|
||||
|
||||
///! A tracked Bluetooth device.
|
||||
class BluetoothDevice: public QObject {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
QML_UNCREATABLE("");
|
||||
// clang-format off
|
||||
/// MAC address of the device.
|
||||
Q_PROPERTY(QString address READ default NOTIFY addressChanged BINDABLE bindableAddress);
|
||||
/// The name of the Bluetooth device. This property may be written to create an alias, or set to
|
||||
/// an empty string to fall back to the device provided name.
|
||||
///
|
||||
/// See @@deviceName for the name provided by the device.
|
||||
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged);
|
||||
/// The name of the Bluetooth device, ignoring user provided aliases. See also @@name
|
||||
/// which returns a user provided alias if set.
|
||||
Q_PROPERTY(QString deviceName READ default NOTIFY deviceNameChanged BINDABLE bindableDeviceName);
|
||||
/// System icon representing the device type. Use @@Quickshell.Quickshell.iconPath() to display this in an image.
|
||||
Q_PROPERTY(QString icon READ default NOTIFY iconChanged BINDABLE bindableIcon);
|
||||
/// Connection state of the device.
|
||||
Q_PROPERTY(BluetoothDeviceState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
|
||||
/// True if the device is currently connected to the computer.
|
||||
///
|
||||
/// Setting this property is equivalent to calling @@connect() and @@disconnect().
|
||||
///
|
||||
/// > [!NOTE] @@state provides more detailed information if required.
|
||||
Q_PROPERTY(bool connected READ connected WRITE setConnected NOTIFY connectedChanged);
|
||||
/// True if the device is paired to the computer.
|
||||
///
|
||||
/// > [!NOTE] @@pair() can be used to pair a device, however you must @@forget() the device to unpair it.
|
||||
Q_PROPERTY(bool paired READ default NOTIFY pairedChanged BINDABLE bindablePaired);
|
||||
/// True if pairing information is stored for future connections.
|
||||
Q_PROPERTY(bool bonded READ default NOTIFY bondedChanged BINDABLE bindableBonded);
|
||||
/// True if the device is currently being paired.
|
||||
///
|
||||
/// > [!NOTE] @@cancelPair() can be used to cancel the pairing process.
|
||||
Q_PROPERTY(bool pairing READ pairing NOTIFY pairingChanged);
|
||||
/// True if the device is considered to be trusted by the system.
|
||||
/// Trusted devices are allowed to reconnect themselves to the system without intervention.
|
||||
Q_PROPERTY(bool trusted READ trusted WRITE setTrusted NOTIFY trustedChanged);
|
||||
/// True if the device is blocked from connecting.
|
||||
/// If a device is blocked, any connection attempts will be immediately rejected by the system.
|
||||
Q_PROPERTY(bool blocked READ blocked WRITE setBlocked NOTIFY blockedChanged);
|
||||
/// True if the device is allowed to wake up the host system from suspend.
|
||||
Q_PROPERTY(bool wakeAllowed READ wakeAllowed WRITE setWakeAllowed NOTIFY wakeAllowedChanged);
|
||||
/// True if the connected device reports its battery level. Battery level can be accessed via @@battery.
|
||||
Q_PROPERTY(bool batteryAvailable READ batteryAvailable NOTIFY batteryAvailableChanged);
|
||||
/// Battery level of the connected device, from `0.0` to `1.0`. Only valid if @@batteryAvailable is true.
|
||||
Q_PROPERTY(qreal battery READ default NOTIFY batteryChanged BINDABLE bindableBattery);
|
||||
/// The Bluetooth adapter this device belongs to.
|
||||
Q_PROPERTY(BluetoothAdapter* adapter READ adapter NOTIFY adapterChanged);
|
||||
/// DBus path of the device under the `org.bluez` system service.
|
||||
Q_PROPERTY(QString dbusPath READ path CONSTANT);
|
||||
// clang-format on
|
||||
|
||||
public:
|
||||
explicit BluetoothDevice(const QString& path, QObject* parent = nullptr);
|
||||
|
||||
/// Attempt to connect to the device.
|
||||
Q_INVOKABLE void connect();
|
||||
/// Disconnect from the device.
|
||||
Q_INVOKABLE void disconnect();
|
||||
/// Attempt to pair the device.
|
||||
///
|
||||
/// > [!NOTE] @@paired and @@pairing return the current pairing status of the device.
|
||||
Q_INVOKABLE void pair();
|
||||
/// Cancel an active pairing attempt.
|
||||
Q_INVOKABLE void cancelPair();
|
||||
/// Forget the device.
|
||||
Q_INVOKABLE void forget();
|
||||
|
||||
[[nodiscard]] bool isValid() const { return this->mInterface && this->mInterface->isValid(); }
|
||||
[[nodiscard]] QString path() const {
|
||||
return this->mInterface ? this->mInterface->path() : QString();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool batteryAvailable() const { return this->mBatteryInterface != nullptr; }
|
||||
[[nodiscard]] BluetoothAdapter* adapter() const;
|
||||
[[nodiscard]] QDBusObjectPath adapterPath() const { return this->bAdapterPath.value(); }
|
||||
|
||||
[[nodiscard]] bool connected() const { return this->bConnected; }
|
||||
void setConnected(bool connected);
|
||||
|
||||
[[nodiscard]] bool trusted() const { return this->bTrusted; }
|
||||
void setTrusted(bool trusted);
|
||||
|
||||
[[nodiscard]] bool blocked() const { return this->bBlocked; }
|
||||
void setBlocked(bool blocked);
|
||||
|
||||
[[nodiscard]] QString name() const { return this->bName; }
|
||||
void setName(const QString& name);
|
||||
|
||||
[[nodiscard]] bool wakeAllowed() const { return this->bWakeAllowed; }
|
||||
void setWakeAllowed(bool wakeAllowed);
|
||||
|
||||
[[nodiscard]] bool pairing() const { return this->bPairing; }
|
||||
|
||||
[[nodiscard]] QBindable<QString> bindableAddress() { return &this->bAddress; }
|
||||
[[nodiscard]] QBindable<QString> bindableDeviceName() { return &this->bDeviceName; }
|
||||
[[nodiscard]] QBindable<QString> bindableName() { return &this->bName; }
|
||||
[[nodiscard]] QBindable<bool> bindableConnected() { return &this->bConnected; }
|
||||
[[nodiscard]] QBindable<bool> bindablePaired() { return &this->bPaired; }
|
||||
[[nodiscard]] QBindable<bool> bindableBonded() { return &this->bBonded; }
|
||||
[[nodiscard]] QBindable<bool> bindableTrusted() { return &this->bTrusted; }
|
||||
[[nodiscard]] QBindable<bool> bindableBlocked() { return &this->bBlocked; }
|
||||
[[nodiscard]] QBindable<bool> bindableWakeAllowed() { return &this->bWakeAllowed; }
|
||||
[[nodiscard]] QBindable<QString> bindableIcon() { return &this->bIcon; }
|
||||
[[nodiscard]] QBindable<qreal> bindableBattery() { return &this->bBattery; }
|
||||
[[nodiscard]] QBindable<BluetoothDeviceState::Enum> bindableState() { return &this->bState; }
|
||||
|
||||
void addInterface(const QString& interface, const QVariantMap& properties);
|
||||
void removeInterface(const QString& interface);
|
||||
|
||||
signals:
|
||||
void addressChanged();
|
||||
void deviceNameChanged();
|
||||
void nameChanged();
|
||||
void connectedChanged();
|
||||
void stateChanged();
|
||||
void pairedChanged();
|
||||
void bondedChanged();
|
||||
void pairingChanged();
|
||||
void trustedChanged();
|
||||
void blockedChanged();
|
||||
void wakeAllowedChanged();
|
||||
void iconChanged();
|
||||
void batteryAvailableChanged();
|
||||
void batteryChanged();
|
||||
void adapterChanged();
|
||||
|
||||
private:
|
||||
void onConnectedChanged();
|
||||
|
||||
DBusBluezDeviceInterface* mInterface = nullptr;
|
||||
QDBusInterface* mBatteryInterface = nullptr;
|
||||
|
||||
// clang-format off
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bAddress, &BluetoothDevice::addressChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bDeviceName, &BluetoothDevice::deviceNameChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bName, &BluetoothDevice::nameChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bConnected, &BluetoothDevice::onConnectedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPaired, &BluetoothDevice::pairedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBonded, &BluetoothDevice::bondedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bTrusted, &BluetoothDevice::trustedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBlocked, &BluetoothDevice::blockedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bWakeAllowed, &BluetoothDevice::wakeAllowedChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bIcon, &BluetoothDevice::iconChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QDBusObjectPath, bAdapterPath);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, qreal, bBattery, &BluetoothDevice::batteryChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, BluetoothDeviceState::Enum, bState, &BluetoothDevice::stateChanged);
|
||||
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPairing, &BluetoothDevice::pairingChanged);
|
||||
|
||||
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, properties);
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAddress, bAddress, properties, "Address");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pDeviceName, bDeviceName, properties, "Name");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pName, bName, properties, "Alias");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pConnected, bConnected, properties, "Connected");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pPaired, bPaired, properties, "Paired");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBonded, bBonded, properties, "Bonded");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pTrusted, bTrusted, properties, "Trusted");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBlocked, bBlocked, properties, "Blocked");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pWakeAllowed, bWakeAllowed, properties, "WakeAllowed");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pIcon, bIcon, properties, "Icon");
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAdapterPath, bAdapterPath, properties, "Adapter");
|
||||
|
||||
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, batteryProperties);
|
||||
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, BatteryPercentage, pBattery, bBattery, batteryProperties, "Percentage", true);
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
} // namespace qs::bluetooth
|
||||
|
||||
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device);
|
||||
@@ -0,0 +1,12 @@
|
||||
name = "Quickshell.Bluetooth"
|
||||
description = "Bluetooth API"
|
||||
headers = [
|
||||
"bluez.hpp",
|
||||
"adapter.hpp",
|
||||
"device.hpp",
|
||||
]
|
||||
-----
|
||||
This module exposes Bluetooth management APIs provided by the BlueZ DBus interface.
|
||||
Both DBus and BlueZ must be running to use it.
|
||||
|
||||
See the @@Quickshell.Bluetooth.Bluetooth singleton.
|
||||
@@ -0,0 +1,9 @@
|
||||
<node>
|
||||
<interface name="org.bluez.Adapter1">
|
||||
<method name="StartDiscovery"/>
|
||||
<method name="StopDiscovery"/>
|
||||
<method name="RemoveDevice">
|
||||
<arg name="device" type="o"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>
|
||||
@@ -0,0 +1,8 @@
|
||||
<node>
|
||||
<interface name="org.bluez.Device1">
|
||||
<method name="Connect"/>
|
||||
<method name="Disconnect"/>
|
||||
<method name="Pair"/>
|
||||
<method name="CancelPairing"/>
|
||||
</interface>
|
||||
</node>
|
||||
@@ -0,0 +1,200 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Bluetooth
|
||||
|
||||
FloatingWindow {
|
||||
color: contentItem.palette.window
|
||||
|
||||
ListView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
model: Bluetooth.adapters
|
||||
|
||||
delegate: WrapperRectangle {
|
||||
width: parent.width
|
||||
color: "transparent"
|
||||
border.color: palette.button
|
||||
border.width: 1
|
||||
margin: 5
|
||||
|
||||
ColumnLayout {
|
||||
Label { text: `Adapter: ${modelData.name} (${modelData.adapterId})` }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
CheckBox {
|
||||
text: "Enable"
|
||||
checked: modelData.enabled
|
||||
onToggled: modelData.enabled = checked
|
||||
}
|
||||
|
||||
Label {
|
||||
color: modelData.state === BluetoothAdapterState.Blocked ? palette.errorText : palette.placeholderText
|
||||
text: BluetoothAdapterState.toString(modelData.state)
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
text: "Discoverable"
|
||||
checked: modelData.discoverable
|
||||
onToggled: modelData.discoverable = checked
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
text: "Discovering"
|
||||
checked: modelData.discovering
|
||||
onToggled: modelData.discovering = checked
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
text: "Pairable"
|
||||
checked: modelData.pairable
|
||||
onToggled: modelData.pairable = checked
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label { text: "Discoverable timeout:" }
|
||||
|
||||
SpinBox {
|
||||
from: 0
|
||||
to: 3600
|
||||
value: modelData.discoverableTimeout
|
||||
onValueModified: modelData.discoverableTimeout = value
|
||||
textFromValue: time => time === 0 ? "∞" : time + "s"
|
||||
}
|
||||
|
||||
Label { text: "Pairable timeout:" }
|
||||
|
||||
SpinBox {
|
||||
from: 0
|
||||
to: 3600
|
||||
value: modelData.pairableTimeout
|
||||
onValueModified: modelData.pairableTimeout = value
|
||||
textFromValue: time => time === 0 ? "∞" : time + "s"
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: modelData.devices
|
||||
|
||||
WrapperRectangle {
|
||||
Layout.fillWidth: true
|
||||
color: palette.button
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
margin: 5
|
||||
|
||||
RowLayout {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
IconImage {
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: height
|
||||
source: Quickshell.iconPath(modelData.icon)
|
||||
}
|
||||
|
||||
TextField {
|
||||
text: modelData.name
|
||||
font.bold: true
|
||||
background: null
|
||||
readOnly: false
|
||||
selectByMouse: true
|
||||
onEditingFinished: modelData.name = text
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: modelData.name && modelData.name !== modelData.deviceName
|
||||
text: `(${modelData.deviceName})`
|
||||
color: palette.placeholderText
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Label {
|
||||
text: modelData.address
|
||||
color: palette.placeholderText
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: modelData.batteryAvailable
|
||||
text: `| Battery: ${Math.round(modelData.battery * 100)}%`
|
||||
color: palette.placeholderText
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Label {
|
||||
text: BluetoothDeviceState.toString(modelData.state)
|
||||
|
||||
color: modelData.connected ? palette.link : palette.placeholderText
|
||||
}
|
||||
|
||||
Label {
|
||||
text: modelData.pairing ? "Pairing" : (modelData.paired ? "Paired" : "Not Paired")
|
||||
color: modelData.paired || modelData.pairing ? palette.link : palette.placeholderText
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: modelData.bonded
|
||||
text: "| Bonded"
|
||||
color: palette.link
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
text: "Trusted"
|
||||
checked: modelData.trusted
|
||||
onToggled: modelData.trusted = checked
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
text: "Blocked"
|
||||
checked: modelData.blocked
|
||||
onToggled: modelData.blocked = checked
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
text: "Wake Allowed"
|
||||
checked: modelData.wakeAllowed
|
||||
onToggled: modelData.wakeAllowed = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: modelData.connected ? "Disconnect" : "Connect"
|
||||
onClicked: modelData.connected = !modelData.connected
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: modelData.pairing ? "Cancel" : (modelData.paired ? "Forget" : "Pair")
|
||||
onClicked: {
|
||||
if (modelData.pairing) {
|
||||
modelData.cancelPair();
|
||||
} else if (modelData.paired) {
|
||||
modelData.forget();
|
||||
} else {
|
||||
modelData.pair();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ qt_add_library(quickshell-core STATIC
|
||||
iconprovider.cpp
|
||||
scriptmodel.cpp
|
||||
colorquantizer.cpp
|
||||
toolsupport.cpp
|
||||
)
|
||||
|
||||
qt_add_qml_module(quickshell-core
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#include "common.hpp"
|
||||
|
||||
#include <qdatetime.h>
|
||||
#include <qprocess.h>
|
||||
|
||||
namespace qs {
|
||||
|
||||
const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime();
|
||||
QProcessEnvironment Common::INITIAL_ENVIRONMENT = {}; // NOLINT
|
||||
|
||||
} // namespace qs
|
||||
+1
-1
@@ -7,7 +7,7 @@ namespace qs {
|
||||
|
||||
struct Common {
|
||||
static const QDateTime LAUNCH_TIME;
|
||||
static QProcessEnvironment INITIAL_ENVIRONMENT; // NOLINT
|
||||
static inline QProcessEnvironment INITIAL_ENVIRONMENT = {}; // NOLINT
|
||||
};
|
||||
|
||||
} // namespace qs
|
||||
+44
-21
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#include "incubator.hpp"
|
||||
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
Q_LOGGING_CATEGORY(logIncubator, "quickshell.incubator", QtWarningMsg);
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_LOGGING_CATEGORY(logIncubator, "quickshell.incubator", QtWarningMsg);
|
||||
|
||||
void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) {
|
||||
switch (status) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <qloggingcategory.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlincubator.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logIncubator);
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_DECLARE_LOGGING_CATEGORY(logIncubator);
|
||||
|
||||
class QsQmlIncubator
|
||||
: public QObject
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
#include <qdatastream.h>
|
||||
|
||||
QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) {
|
||||
stream << info.instanceId << info.configPath << info.shellId << info.launchTime;
|
||||
stream << info.instanceId << info.configPath << info.shellId << info.launchTime << info.pid;
|
||||
return stream;
|
||||
}
|
||||
|
||||
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) {
|
||||
stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime;
|
||||
stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime >> info.pid;
|
||||
return stream;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
#include <qdatetime.h>
|
||||
#include <qlogging.h>
|
||||
#include <qstring.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
struct InstanceInfo {
|
||||
QString instanceId;
|
||||
QString configPath;
|
||||
QString shellId;
|
||||
QDateTime launchTime;
|
||||
pid_t pid = -1;
|
||||
|
||||
static InstanceInfo CURRENT; // NOLINT
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
|
||||
namespace qs::log {
|
||||
void initLogCategoryLevel(const char* name, QtMsgType defaultLevel = QtDebugMsg);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define QS_DECLARE_LOGGING_CATEGORY(name) \
|
||||
namespace qslogcat { \
|
||||
Q_DECLARE_LOGGING_CATEGORY(name); \
|
||||
} \
|
||||
const QLoggingCategory& name()
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define QS_LOGGING_CATEGORY(name, category, ...) \
|
||||
namespace qslogcat { \
|
||||
Q_LOGGING_CATEGORY(name, category __VA_OPT__(, __VA_ARGS__)); \
|
||||
} \
|
||||
const QLoggingCategory& name() { \
|
||||
static auto* init = []() { \
|
||||
qs::log::initLogCategoryLevel(category __VA_OPT__(, __VA_ARGS__)); \
|
||||
return &qslogcat::name; \
|
||||
}(); \
|
||||
return (init) (); \
|
||||
}
|
||||
+36
-16
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <qbytearrayview.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdatetime.h>
|
||||
#include <qfile.h>
|
||||
@@ -12,7 +13,9 @@
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logBare);
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_DECLARE_LOGGING_CATEGORY(logBare);
|
||||
|
||||
namespace qs::log {
|
||||
|
||||
@@ -127,11 +130,14 @@ private:
|
||||
QString mRulesString;
|
||||
QList<qt_logging_registry::QLoggingRule>* rules = nullptr;
|
||||
QtMsgType mDefaultLevel = QtWarningMsg;
|
||||
QHash<QLatin1StringView, QtMsgType> defaultLevels;
|
||||
QHash<const void*, CategoryFilter> sparseFilters;
|
||||
QHash<QLatin1StringView, CategoryFilter> allFilters;
|
||||
|
||||
QTextStream stdoutStream;
|
||||
LoggingThreadProxy threadProxy;
|
||||
|
||||
friend void initLogCategoryLevel(const char* name, QtMsgType defaultLevel);
|
||||
};
|
||||
|
||||
bool readEncodedLogs(
|
||||
|
||||
@@ -16,10 +16,11 @@
|
||||
#include <qstringview.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "logcat.hpp"
|
||||
#include "logging_qtprivate.hpp"
|
||||
|
||||
namespace qs::log {
|
||||
Q_DECLARE_LOGGING_CATEGORY(logLogging);
|
||||
QS_DECLARE_LOGGING_CATEGORY(logLogging);
|
||||
|
||||
namespace qt_logging_registry {
|
||||
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
#include <qstringview.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "logcat.hpp"
|
||||
|
||||
namespace qs::log {
|
||||
Q_DECLARE_LOGGING_CATEGORY(logLogging);
|
||||
QS_DECLARE_LOGGING_CATEGORY(logLogging);
|
||||
|
||||
namespace qt_logging_registry {
|
||||
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ public:
|
||||
[[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override;
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
[[nodiscard]] QList<QObject*> values() const { return this->valuesList; };
|
||||
[[nodiscard]] QList<QObject*> values() const { return this->valuesList; }
|
||||
void removeAt(qsizetype index);
|
||||
|
||||
Q_INVOKABLE qsizetype indexOf(QObject* object);
|
||||
|
||||
+41
-11
@@ -9,15 +9,17 @@
|
||||
#include <qdir.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qpair.h>
|
||||
#include <qstandardpaths.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
#include <qtversionchecks.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "instanceinfo.hpp"
|
||||
#include "logcat.hpp"
|
||||
|
||||
namespace {
|
||||
Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
|
||||
}
|
||||
|
||||
QsPaths* QsPaths::instance() {
|
||||
@@ -133,6 +135,33 @@ QDir* QsPaths::instanceRunDir() {
|
||||
else return &this->mInstanceRunDir;
|
||||
}
|
||||
|
||||
QDir* QsPaths::shellVfsDir() {
|
||||
if (this->shellVfsState == DirState::Unknown) {
|
||||
if (auto* baseRunDir = this->baseRunDir()) {
|
||||
this->mShellVfsDir = QDir(baseRunDir->filePath("vfs"));
|
||||
this->mShellVfsDir = QDir(this->mShellVfsDir.filePath(this->shellId));
|
||||
|
||||
qCDebug(logPaths) << "Initialized runtime vfs path:" << this->mShellVfsDir.path();
|
||||
|
||||
if (!this->mShellVfsDir.mkpath(".")) {
|
||||
qCCritical(logPaths) << "Could not create runtime vfs directory at"
|
||||
<< this->mShellVfsDir.path();
|
||||
this->shellVfsState = DirState::Failed;
|
||||
} else {
|
||||
this->shellVfsState = DirState::Ready;
|
||||
}
|
||||
} else {
|
||||
qCCritical(logPaths) << "Could not create shell runtime vfs path as it was not possible to "
|
||||
"create the base runtime path.";
|
||||
|
||||
this->shellVfsState = DirState::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->shellVfsState == DirState::Failed) return nullptr;
|
||||
else return &this->mShellVfsDir;
|
||||
}
|
||||
|
||||
void QsPaths::linkRunDir() {
|
||||
if (auto* runDir = this->instanceRunDir()) {
|
||||
auto pidDir = QDir(this->baseRunDir()->filePath("by-pid"));
|
||||
@@ -367,29 +396,30 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<InstanceLockInfo> QsPaths::collectInstances(const QString& path, bool fallbackDead) {
|
||||
QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>>
|
||||
QsPaths::collectInstances(const QString& path) {
|
||||
qCDebug(logPaths) << "Collecting instances from" << path;
|
||||
auto instances = QVector<InstanceLockInfo>();
|
||||
auto liveInstances = QVector<InstanceLockInfo>();
|
||||
auto deadInstances = QVector<InstanceLockInfo>();
|
||||
auto dir = QDir(path);
|
||||
|
||||
InstanceLockInfo info;
|
||||
for (auto& entry: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
|
||||
auto path = dir.filePath(entry);
|
||||
|
||||
if (QsPaths::checkLock(path, &info, fallbackDead)) {
|
||||
if (fallbackDead && info.pid != -1) {
|
||||
fallbackDead = false;
|
||||
instances.clear();
|
||||
}
|
||||
|
||||
if (QsPaths::checkLock(path, &info, true)) {
|
||||
qCDebug(logPaths).nospace() << "Found instance " << info.instance.instanceId << " (pid "
|
||||
<< info.pid << ") at " << path;
|
||||
|
||||
instances.push_back(info);
|
||||
if (info.pid == -1) {
|
||||
deadInstances.push_back(info);
|
||||
} else {
|
||||
liveInstances.push_back(info);
|
||||
}
|
||||
} else {
|
||||
qCDebug(logPaths) << "Skipped potential instance at" << path;
|
||||
}
|
||||
}
|
||||
|
||||
return instances;
|
||||
return qMakePair(liveInstances, deadInstances);
|
||||
}
|
||||
+6
-1
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include <qdatetime.h>
|
||||
#include <qdir.h>
|
||||
#include <qpair.h>
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "instanceinfo.hpp"
|
||||
@@ -22,10 +23,12 @@ public:
|
||||
static QString ipcPath(const QString& id);
|
||||
static bool
|
||||
checkLock(const QString& path, InstanceLockInfo* info = nullptr, bool allowDead = false);
|
||||
static QVector<InstanceLockInfo> collectInstances(const QString& path, bool fallbackDead = false);
|
||||
static QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>>
|
||||
collectInstances(const QString& path);
|
||||
|
||||
QDir* baseRunDir();
|
||||
QDir* shellRunDir();
|
||||
QDir* shellVfsDir();
|
||||
QDir* instanceRunDir();
|
||||
void linkRunDir();
|
||||
void linkPathDir();
|
||||
@@ -46,9 +49,11 @@ private:
|
||||
QString pathId;
|
||||
QDir mBaseRunDir;
|
||||
QDir mShellRunDir;
|
||||
QDir mShellVfsDir;
|
||||
QDir mInstanceRunDir;
|
||||
DirState baseRunState = DirState::Unknown;
|
||||
DirState shellRunState = DirState::Unknown;
|
||||
DirState shellVfsState = DirState::Unknown;
|
||||
DirState instanceRunState = DirState::Unknown;
|
||||
|
||||
QDir mShellDataDir;
|
||||
|
||||
@@ -189,11 +189,15 @@ void PopupAnchor::updateAnchor() {
|
||||
if (this->mItem && this->mProxyWindow) {
|
||||
auto baseRect =
|
||||
this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect();
|
||||
|
||||
auto rect = this->mProxyWindow->contentItem()->mapFromItem(
|
||||
this->mItem,
|
||||
baseRect.marginsRemoved(this->mMargins.qmargins())
|
||||
);
|
||||
|
||||
if (rect.width() < 1) rect.setWidth(1);
|
||||
if (rect.height() < 1) rect.setHeight(1);
|
||||
|
||||
this->setWindowRect(rect.toRect());
|
||||
}
|
||||
|
||||
|
||||
+62
-6
@@ -8,8 +8,10 @@
|
||||
#include <qguiapplication.h>
|
||||
#include <qicon.h>
|
||||
#include <qjsengine.h>
|
||||
#include <qlist.h>
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qprocess.h>
|
||||
#include <qqmlcontext.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmllist.h>
|
||||
@@ -21,6 +23,7 @@
|
||||
#include <qwindowdefs.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "../io/processcore.hpp"
|
||||
#include "generation.hpp"
|
||||
#include "iconimageprovider.hpp"
|
||||
#include "paths.hpp"
|
||||
@@ -177,12 +180,6 @@ void QuickshellGlobal::reload(bool hard) {
|
||||
root->reloadGraph(hard);
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::shellRoot() const {
|
||||
auto* generation = EngineGeneration::findObjectGeneration(this);
|
||||
// already canonical
|
||||
return generation->rootPath.path();
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::workingDirectory() const { // NOLINT
|
||||
return QuickshellSettings::instance()->workingDirectory();
|
||||
}
|
||||
@@ -213,6 +210,22 @@ void QuickshellGlobal::onClipboardChanged(QClipboard::Mode mode) {
|
||||
if (mode == QClipboard::Clipboard) emit this->clipboardTextChanged();
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::shellDir() const {
|
||||
return EngineGeneration::findObjectGeneration(this)->rootPath.path();
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::configDir() const {
|
||||
qWarning() << "Quickshell.configDir is deprecated and may be removed in a future release. Use "
|
||||
"Quickshell.shellDir.";
|
||||
return this->shellDir();
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::shellRoot() const {
|
||||
qWarning() << "Quickshell.shellRoot is deprecated and may be removed in a future release. Use "
|
||||
"Quickshell.shellDir.";
|
||||
return this->shellDir();
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::dataDir() const { // NOLINT
|
||||
return QsPaths::instance()->shellDataDir().path();
|
||||
}
|
||||
@@ -225,6 +238,16 @@ QString QuickshellGlobal::cacheDir() const { // NOLINT
|
||||
return QsPaths::instance()->shellCacheDir().path();
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::shellPath(const QString& path) const {
|
||||
return this->shellDir() % '/' % path;
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::configPath(const QString& path) const {
|
||||
qWarning() << "Quickshell.configPath() is deprecated and may be removed in a future release. Use "
|
||||
"Quickshell.shellPath().";
|
||||
return this->shellPath(path);
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::dataPath(const QString& path) const {
|
||||
return this->dataDir() % '/' % path;
|
||||
}
|
||||
@@ -244,6 +267,39 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT
|
||||
return qEnvironmentVariable(vstr.data());
|
||||
}
|
||||
|
||||
void QuickshellGlobal::execDetached(QList<QString> command) {
|
||||
QuickshellGlobal::execDetached(qs::io::process::ProcessContext(std::move(command)));
|
||||
}
|
||||
|
||||
void QuickshellGlobal::execDetached(const qs::io::process::ProcessContext& context) {
|
||||
if (context.command.isEmpty()) {
|
||||
qWarning() << "Cannot start process as command is empty.";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& cmd = context.command.first();
|
||||
auto args = context.command.sliced(1);
|
||||
|
||||
QProcess process;
|
||||
qs::io::process::setupProcessEnvironment(&process, context.clearEnvironment, context.environment);
|
||||
|
||||
if (!context.workingDirectory.isEmpty()) {
|
||||
process.setWorkingDirectory(context.workingDirectory);
|
||||
}
|
||||
|
||||
process.setProgram(cmd);
|
||||
process.setArguments(args);
|
||||
|
||||
process.setStandardInputFile(QProcess::nullDevice());
|
||||
|
||||
if (context.unbindStdout) {
|
||||
process.setStandardOutputFile(QProcess::nullDevice());
|
||||
process.setStandardErrorFile(QProcess::nullDevice());
|
||||
}
|
||||
|
||||
process.startDetached();
|
||||
}
|
||||
|
||||
QString QuickshellGlobal::iconPath(const QString& icon) {
|
||||
return IconImageProvider::requestString(icon);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
#include <qclipboard.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qhash.h>
|
||||
#include <qjsengine.h>
|
||||
#include <qlist.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qqmlintegration.h>
|
||||
@@ -13,6 +15,8 @@
|
||||
#include <qvariant.h>
|
||||
#include <qwindowdefs.h>
|
||||
|
||||
#include "../io/processcore.hpp"
|
||||
#include "doc.hpp"
|
||||
#include "qmlscreen.hpp"
|
||||
|
||||
///! Accessor for some options under the Quickshell type.
|
||||
@@ -104,6 +108,10 @@ class QuickshellGlobal: public QObject {
|
||||
///
|
||||
/// The root directory is the folder containing the entrypoint to your shell, often referred
|
||||
/// to as `shell.qml`.
|
||||
Q_PROPERTY(QString shellDir READ shellDir CONSTANT);
|
||||
/// > [!WARNING] Deprecated: Renamed to @@shellDir for clarity.
|
||||
Q_PROPERTY(QString configDir READ configDir CONSTANT);
|
||||
/// > [!WARNING] Deprecated: Renamed to @@shellDir for consistency.
|
||||
Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT);
|
||||
/// Quickshell's working directory. Defaults to whereever quickshell was launched from.
|
||||
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged);
|
||||
@@ -152,6 +160,30 @@ public:
|
||||
/// Returns the string value of an environment variable or null if it is not set.
|
||||
Q_INVOKABLE QVariant env(const QString& variable);
|
||||
|
||||
// MUST be before execDetached(ctx) or the other will be called with a default constructed obj.
|
||||
QSDOC_HIDE Q_INVOKABLE static void execDetached(QList<QString> command);
|
||||
/// Launch a process detached from Quickshell.
|
||||
///
|
||||
/// The context parameter can either be a list of command arguments or a JS object with the following fields:
|
||||
/// - `command`: A list containing the command and all its arguments. See @@Quickshell.Io.Process.command.
|
||||
/// - `environment`: Changes to make to the process environment. See @@Quickshell.Io.Process.environment.
|
||||
/// - `clearEnvironment`: Removes all variables from the environment if true.
|
||||
/// - `workingDirectory`: The working directory the command should run in.
|
||||
///
|
||||
/// > [!WARNING] This does not run command in a shell. All arguments to the command
|
||||
/// > must be in separate values in the list, e.g. `["echo", "hello"]`
|
||||
/// > and not `["echo hello"]`.
|
||||
/// >
|
||||
/// > Additionally, shell scripts must be run by your shell,
|
||||
/// > e.g. `["sh", "script.sh"]` instead of `["script.sh"]` unless the script
|
||||
/// > has a shebang.
|
||||
///
|
||||
/// > [!INFO] You can use `["sh", "-c", <your command>]` to execute your command with
|
||||
/// > the system shell.
|
||||
///
|
||||
/// This function is equivalent to @@Quickshell.Io.Process.startDetached().
|
||||
Q_INVOKABLE static void execDetached(const qs::io::process::ProcessContext& context);
|
||||
|
||||
/// Returns a string usable for a @@QtQuick.Image.source for a given system icon.
|
||||
///
|
||||
/// > [!INFO] By default, icons are loaded from the theme selected by the qt platform theme,
|
||||
@@ -167,6 +199,10 @@ public:
|
||||
/// Setting the `fallback` parameter of `iconPath` will attempt to load the fallback
|
||||
/// icon if the requested one could not be loaded.
|
||||
Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback);
|
||||
/// Equivalent to `${Quickshell.configDir}/${path}`
|
||||
Q_INVOKABLE [[nodiscard]] QString shellPath(const QString& path) const;
|
||||
/// > [!WARNING] Deprecated: Renamed to @@shellPath() for clarity.
|
||||
Q_INVOKABLE [[nodiscard]] QString configPath(const QString& path) const;
|
||||
/// Equivalent to `${Quickshell.dataDir}/${path}`
|
||||
Q_INVOKABLE [[nodiscard]] QString dataPath(const QString& path) const;
|
||||
/// Equivalent to `${Quickshell.stateDir}/${path}`
|
||||
@@ -182,6 +218,8 @@ public:
|
||||
void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; }
|
||||
[[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; }
|
||||
|
||||
[[nodiscard]] QString shellDir() const;
|
||||
[[nodiscard]] QString configDir() const;
|
||||
[[nodiscard]] QString shellRoot() const;
|
||||
|
||||
[[nodiscard]] QString workingDirectory() const;
|
||||
|
||||
+47
-18
@@ -1,6 +1,7 @@
|
||||
#include "qsintercept.hpp"
|
||||
#include <cstring>
|
||||
|
||||
#include <qdir.h>
|
||||
#include <qhash.h>
|
||||
#include <qiodevice.h>
|
||||
#include <qlogging.h>
|
||||
@@ -14,7 +15,9 @@
|
||||
#include <qtypes.h>
|
||||
#include <qurl.h>
|
||||
|
||||
Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg);
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg);
|
||||
|
||||
QUrl QsUrlInterceptor::intercept(
|
||||
const QUrl& originalUrl,
|
||||
@@ -23,27 +26,44 @@ QUrl QsUrlInterceptor::intercept(
|
||||
auto url = originalUrl;
|
||||
|
||||
if (url.scheme() == "root") {
|
||||
url.setScheme("qsintercept");
|
||||
url.setScheme("qs");
|
||||
|
||||
auto path = url.path();
|
||||
if (path.startsWith('/')) path = path.sliced(1);
|
||||
url.setPath(this->configRoot.filePath(path));
|
||||
url.setPath("@/qs/" % path);
|
||||
|
||||
qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url;
|
||||
}
|
||||
|
||||
// Some types such as Image take into account where they are loading from, and force
|
||||
// asynchronous loading over a network. qsintercept is considered to be over a network.
|
||||
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") {
|
||||
// Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which
|
||||
// case we want to keep the intercept, otherwise objects created from those paths
|
||||
// will not be able to use singletons.
|
||||
if (url.path().endsWith(".qml")) return url;
|
||||
if (url.scheme() == "qs") {
|
||||
auto path = url.path();
|
||||
|
||||
auto newUrl = url;
|
||||
newUrl.setScheme("file");
|
||||
qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl;
|
||||
return newUrl;
|
||||
// Our import path is on "qs:@/".
|
||||
// We want to blackhole any import resolution outside of the config folder as it breaks Qt
|
||||
// but NOT file lookups that might be on "qs:/" due to a missing "file:/" prefix.
|
||||
if (path.startsWith("@/qs/")) {
|
||||
path = this->configRoot.filePath(path.sliced(5));
|
||||
} else if (!path.startsWith("/")) {
|
||||
qCDebug(logQsIntercept) << "Blackholed import URL" << url;
|
||||
return QUrl("qrc:/qs-blackhole");
|
||||
}
|
||||
|
||||
// Some types such as Image take into account where they are loading from, and force
|
||||
// asynchronous loading over a network. qs: is considered to be over a network.
|
||||
// In those cases we want to return a file:// url so asynchronous loading is not forced.
|
||||
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString) {
|
||||
// Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which
|
||||
// case we want to keep the intercept, otherwise objects created from those paths
|
||||
// will not be able to use singletons.
|
||||
if (path.endsWith(".qml")) return url;
|
||||
|
||||
auto newUrl = url;
|
||||
newUrl.setScheme("file");
|
||||
// above check asserts path starts with /qs/
|
||||
newUrl.setPath(path);
|
||||
qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl;
|
||||
return newUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
@@ -65,10 +85,12 @@ qint64 QsInterceptDataReply::readData(char* data, qint64 maxSize) {
|
||||
}
|
||||
|
||||
QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager(
|
||||
const QDir& configRoot,
|
||||
const QHash<QString, QString>& fileIntercepts,
|
||||
QObject* parent
|
||||
)
|
||||
: QNetworkAccessManager(parent)
|
||||
, configRoot(configRoot)
|
||||
, fileIntercepts(fileIntercepts) {}
|
||||
|
||||
QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
|
||||
@@ -77,19 +99,26 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
|
||||
QIODevice* outgoingData
|
||||
) {
|
||||
auto url = req.url();
|
||||
if (url.scheme() == "qsintercept") {
|
||||
|
||||
if (url.scheme() == "qs") {
|
||||
auto path = url.path();
|
||||
|
||||
if (path.startsWith("@/qs/")) path = this->configRoot.filePath(path.sliced(5));
|
||||
// otherwise pass through to fs
|
||||
|
||||
qCDebug(logQsIntercept) << "Got intercept for" << path << "contains"
|
||||
<< this->fileIntercepts.value(path);
|
||||
auto data = this->fileIntercepts.value(path);
|
||||
if (data != nullptr) {
|
||||
|
||||
if (auto data = this->fileIntercepts.value(path); !data.isEmpty()) {
|
||||
return new QsInterceptDataReply(data, this);
|
||||
}
|
||||
|
||||
auto fileReq = req;
|
||||
auto fileUrl = req.url();
|
||||
fileUrl.setScheme("file");
|
||||
fileUrl.setPath(path);
|
||||
qCDebug(logQsIntercept) << "Passing through intercept" << url << "to" << fileUrl;
|
||||
|
||||
fileReq.setUrl(fileUrl);
|
||||
return this->QNetworkAccessManager::createRequest(op, fileReq, outgoingData);
|
||||
}
|
||||
@@ -98,5 +127,5 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
|
||||
}
|
||||
|
||||
QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) {
|
||||
return new QsInterceptNetworkAccessManager(this->fileIntercepts, parent);
|
||||
return new QsInterceptNetworkAccessManager(this->configRoot, this->fileIntercepts, parent);
|
||||
}
|
||||
@@ -10,7 +10,9 @@
|
||||
#include <qqmlnetworkaccessmanagerfactory.h>
|
||||
#include <qurl.h>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logQsIntercept);
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_DECLARE_LOGGING_CATEGORY(logQsIntercept);
|
||||
|
||||
class QsUrlInterceptor: public QQmlAbstractUrlInterceptor {
|
||||
public:
|
||||
@@ -43,6 +45,7 @@ class QsInterceptNetworkAccessManager: public QNetworkAccessManager {
|
||||
|
||||
public:
|
||||
QsInterceptNetworkAccessManager(
|
||||
const QDir& configRoot,
|
||||
const QHash<QString, QString>& fileIntercepts,
|
||||
QObject* parent = nullptr
|
||||
);
|
||||
@@ -55,15 +58,21 @@ protected:
|
||||
) override;
|
||||
|
||||
private:
|
||||
QDir configRoot;
|
||||
const QHash<QString, QString>& fileIntercepts;
|
||||
};
|
||||
|
||||
class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory {
|
||||
public:
|
||||
QsInterceptNetworkAccessManagerFactory(const QHash<QString, QString>& fileIntercepts)
|
||||
: fileIntercepts(fileIntercepts) {}
|
||||
QsInterceptNetworkAccessManagerFactory(
|
||||
const QDir& configRoot,
|
||||
const QHash<QString, QString>& fileIntercepts
|
||||
)
|
||||
: configRoot(configRoot)
|
||||
, fileIntercepts(fileIntercepts) {}
|
||||
QNetworkAccessManager* create(QObject* parent) override;
|
||||
|
||||
private:
|
||||
QDir configRoot;
|
||||
const QHash<QString, QString>& fileIntercepts;
|
||||
};
|
||||
+2
-2
@@ -46,8 +46,8 @@ class QsMenuHandle: public QObject {
|
||||
public:
|
||||
explicit QsMenuHandle(QObject* parent): QObject(parent) {}
|
||||
|
||||
virtual void refHandle() {};
|
||||
virtual void unrefHandle() {};
|
||||
virtual void refHandle() {}
|
||||
virtual void unrefHandle() {}
|
||||
|
||||
[[nodiscard]] virtual QsMenuEntry* menu() = 0;
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ PendingRegion::PendingRegion(QObject* parent): QObject(parent) {
|
||||
}
|
||||
|
||||
void PendingRegion::setItem(QQuickItem* item) {
|
||||
if (item == this->mItem) return;
|
||||
|
||||
if (this->mItem != nullptr) {
|
||||
QObject::disconnect(this->mItem, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
+16
-7
@@ -126,12 +126,21 @@ QObject* Reloadable::getChildByReloadId(QObject* parent, const QString& reloadId
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void PostReloadHook::postReloadTree(QObject* root) {
|
||||
for (auto* child: root->children()) {
|
||||
PostReloadHook::postReloadTree(child);
|
||||
}
|
||||
|
||||
if (auto* self = dynamic_cast<PostReloadHook*>(root)) {
|
||||
self->onPostReload();
|
||||
void PostReloadHook::componentComplete() {
|
||||
auto* engineGeneration = EngineGeneration::findObjectGeneration(this);
|
||||
if (!engineGeneration || engineGeneration->reloadComplete) this->postReload();
|
||||
else {
|
||||
// disconnected by EngineGeneration::postReload
|
||||
QObject::connect(
|
||||
engineGeneration,
|
||||
&EngineGeneration::firePostReload,
|
||||
this,
|
||||
&PostReloadHook::postReload
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void PostReloadHook::postReload() {
|
||||
this->isPostReload = true;
|
||||
this->onPostReload();
|
||||
}
|
||||
+16
-9
@@ -57,7 +57,7 @@ public:
|
||||
|
||||
void reload(QObject* oldInstance = nullptr);
|
||||
|
||||
void classBegin() override {};
|
||||
void classBegin() override {}
|
||||
void componentComplete() override;
|
||||
|
||||
// Reload objects in the parent->child graph recursively.
|
||||
@@ -119,16 +119,23 @@ private:
|
||||
};
|
||||
|
||||
/// Hook that runs after the old widget tree is dropped during a reload.
|
||||
class PostReloadHook {
|
||||
class PostReloadHook
|
||||
: public QObject
|
||||
, public QQmlParserStatus {
|
||||
Q_OBJECT;
|
||||
QML_ANONYMOUS;
|
||||
Q_INTERFACES(QQmlParserStatus);
|
||||
|
||||
public:
|
||||
PostReloadHook() = default;
|
||||
virtual ~PostReloadHook() = default;
|
||||
PostReloadHook(PostReloadHook&&) = default;
|
||||
PostReloadHook(const PostReloadHook&) = default;
|
||||
PostReloadHook& operator=(PostReloadHook&&) = default;
|
||||
PostReloadHook& operator=(const PostReloadHook&) = default;
|
||||
PostReloadHook(QObject* parent = nullptr): QObject(parent) {}
|
||||
void classBegin() override {}
|
||||
void componentComplete() override;
|
||||
|
||||
virtual void onPostReload() = 0;
|
||||
|
||||
static void postReloadTree(QObject* root);
|
||||
public slots:
|
||||
void postReload();
|
||||
|
||||
protected:
|
||||
bool isPostReload = false;
|
||||
};
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlcomponent.h>
|
||||
@@ -18,15 +19,26 @@
|
||||
#include "instanceinfo.hpp"
|
||||
#include "qmlglobal.hpp"
|
||||
#include "scan.hpp"
|
||||
#include "toolsupport.hpp"
|
||||
|
||||
RootWrapper::RootWrapper(QString rootPath, QString shellId)
|
||||
: QObject(nullptr)
|
||||
, rootPath(std::move(rootPath))
|
||||
, shellId(std::move(shellId))
|
||||
, originalWorkingDirectory(QDir::current().absolutePath()) {
|
||||
// clang-format off
|
||||
QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::watchFilesChanged, this, &RootWrapper::onWatchFilesChanged);
|
||||
// clang-format on
|
||||
QObject::connect(
|
||||
QuickshellSettings::instance(),
|
||||
&QuickshellSettings::watchFilesChanged,
|
||||
this,
|
||||
&RootWrapper::onWatchFilesChanged
|
||||
);
|
||||
|
||||
QObject::connect(
|
||||
&this->configDirWatcher,
|
||||
&QFileSystemWatcher::directoryChanged,
|
||||
this,
|
||||
&RootWrapper::updateTooling
|
||||
);
|
||||
|
||||
this->reloadGraph(true);
|
||||
|
||||
@@ -43,9 +55,13 @@ RootWrapper::~RootWrapper() {
|
||||
}
|
||||
|
||||
void RootWrapper::reloadGraph(bool hard) {
|
||||
auto rootPath = QFileInfo(this->rootPath).dir();
|
||||
auto rootFile = QFileInfo(this->rootPath);
|
||||
auto rootPath = rootFile.dir();
|
||||
auto scanner = QmlScanner(rootPath);
|
||||
scanner.scanQmlFile(this->rootPath);
|
||||
scanner.scanQmlRoot(this->rootPath);
|
||||
|
||||
qs::core::QmlToolingSupport::updateTooling(rootPath, scanner);
|
||||
this->configDirWatcher.addPath(rootPath.path());
|
||||
|
||||
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
|
||||
generation->wrapper = this;
|
||||
@@ -58,9 +74,9 @@ void RootWrapper::reloadGraph(bool hard) {
|
||||
|
||||
QDir::setCurrent(this->originalWorkingDirectory);
|
||||
|
||||
auto url = QUrl::fromLocalFile(this->rootPath);
|
||||
// unless the original file comes from the qsintercept scheme
|
||||
url.setScheme("qsintercept");
|
||||
QUrl url;
|
||||
url.setScheme("qs");
|
||||
url.setPath("@/qs/" % rootFile.fileName());
|
||||
auto component = QQmlComponent(generation->engine, url);
|
||||
|
||||
if (!component.isReady()) {
|
||||
@@ -69,7 +85,9 @@ void RootWrapper::reloadGraph(bool hard) {
|
||||
|
||||
auto errors = component.errors();
|
||||
for (auto& error: errors) {
|
||||
auto rel = "**/" % rootPath.relativeFilePath(error.url().path());
|
||||
const auto& url = error.url();
|
||||
auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5)
|
||||
: url.toString();
|
||||
auto msg = " caused by " % rel % '[' % QString::number(error.line()) % ':'
|
||||
% QString::number(error.column()) % "]: " % error.description();
|
||||
errorString += '\n' % msg;
|
||||
@@ -165,3 +183,9 @@ void RootWrapper::onWatchFilesChanged() {
|
||||
}
|
||||
|
||||
void RootWrapper::onWatchedFilesChanged() { this->reloadGraph(false); }
|
||||
|
||||
void RootWrapper::updateTooling() {
|
||||
if (!this->generation) return;
|
||||
auto configDir = QFileInfo(this->rootPath).dir();
|
||||
qs::core::QmlToolingSupport::updateTooling(configDir, this->generation->scanner);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
@@ -22,10 +23,12 @@ private slots:
|
||||
void generationDestroyed();
|
||||
void onWatchFilesChanged();
|
||||
void onWatchedFilesChanged();
|
||||
void updateTooling();
|
||||
|
||||
private:
|
||||
QString rootPath;
|
||||
QString shellId;
|
||||
EngineGeneration* generation = nullptr;
|
||||
QString originalWorkingDirectory;
|
||||
QFileSystemWatcher configDirWatcher;
|
||||
};
|
||||
+104
-35
@@ -15,7 +15,9 @@
|
||||
#include <qstringliteral.h>
|
||||
#include <qtextstream.h>
|
||||
|
||||
Q_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg);
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg);
|
||||
|
||||
void QmlScanner::scanDir(const QString& path) {
|
||||
if (this->scannedDirs.contains(path)) return;
|
||||
@@ -24,42 +26,72 @@ void QmlScanner::scanDir(const QString& path) {
|
||||
qCDebug(logQmlScanner) << "Scanning directory" << path;
|
||||
auto dir = QDir(path);
|
||||
|
||||
struct Entry {
|
||||
QString name;
|
||||
bool singleton = false;
|
||||
bool internal = false;
|
||||
};
|
||||
|
||||
bool seenQmldir = false;
|
||||
auto singletons = QVector<QString>();
|
||||
auto entries = QVector<QString>();
|
||||
for (auto& entry: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) {
|
||||
if (entry == "qmldir") {
|
||||
auto entries = QVector<Entry>();
|
||||
|
||||
for (auto& name: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) {
|
||||
if (name == "qmldir") {
|
||||
qCDebug(logQmlScanner
|
||||
) << "Found qmldir file, qmldir synthesization will be disabled for directory"
|
||||
<< path;
|
||||
seenQmldir = true;
|
||||
} else if (entry.at(0).isUpper() && entry.endsWith(".qml")) {
|
||||
if (this->scanQmlFile(dir.filePath(entry))) {
|
||||
singletons.push_back(entry);
|
||||
} else if (name.at(0).isUpper() && name.endsWith(".qml")) {
|
||||
auto& entry = entries.emplaceBack();
|
||||
|
||||
if (this->scanQmlFile(dir.filePath(name), entry.singleton, entry.internal)) {
|
||||
entry.name = name;
|
||||
} else {
|
||||
entries.push_back(entry);
|
||||
entries.pop_back();
|
||||
}
|
||||
} else if (name.at(0).isUpper() && name.endsWith(".qml.json")) {
|
||||
if (this->scanQmlJson(dir.filePath(name))) {
|
||||
entries.push_back({
|
||||
.name = name.first(name.length() - 5),
|
||||
.singleton = true,
|
||||
});
|
||||
}
|
||||
} else if (entry.at(0).isUpper() && entry.endsWith(".qml.json")) {
|
||||
this->scanQmlJson(dir.filePath(entry));
|
||||
singletons.push_back(entry.first(entry.length() - 5));
|
||||
}
|
||||
}
|
||||
|
||||
// Due to the qsintercept:// protocol a qmldir is always required, even without singletons.
|
||||
if (!seenQmldir) {
|
||||
qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path << "singletons"
|
||||
<< singletons;
|
||||
qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path;
|
||||
|
||||
QString qmldir;
|
||||
auto stream = QTextStream(&qmldir);
|
||||
|
||||
for (auto& singleton: singletons) {
|
||||
stream << "singleton " << singleton.sliced(0, singleton.length() - 4) << " 1.0 " << singleton
|
||||
<< "\n";
|
||||
// cant derive a module name if not in shell path
|
||||
if (path.startsWith(this->rootPath.path())) {
|
||||
auto end = path.sliced(this->rootPath.path().length());
|
||||
|
||||
// verify we have a valid module name.
|
||||
for (auto& c: end) {
|
||||
if (c == '/') c = '.';
|
||||
else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|
||||
|| c == '_')
|
||||
{
|
||||
} else {
|
||||
qCWarning(logQmlScanner) << "Module path contains invalid characters for a module name: "
|
||||
<< path.sliced(this->rootPath.path().length());
|
||||
goto skipadd;
|
||||
}
|
||||
}
|
||||
|
||||
stream << "module qs" << end << '\n';
|
||||
skipadd:;
|
||||
} else {
|
||||
qCWarning(logQmlScanner) << "Module path" << path << "is outside of the config folder.";
|
||||
}
|
||||
|
||||
for (auto& entry: entries) {
|
||||
stream << entry.sliced(0, entry.length() - 4) << " 1.0 " << entry << "\n";
|
||||
for (const auto& entry: entries) {
|
||||
if (entry.internal) stream << "internal ";
|
||||
if (entry.singleton) stream << "singleton ";
|
||||
stream << entry.name.sliced(0, entry.name.length() - 4) << " 1.0 " << entry.name << '\n';
|
||||
}
|
||||
|
||||
qCDebug(logQmlScanner) << "Synthesized qmldir for" << path << qPrintable("\n" + qmldir);
|
||||
@@ -67,7 +99,7 @@ void QmlScanner::scanDir(const QString& path) {
|
||||
}
|
||||
}
|
||||
|
||||
bool QmlScanner::scanQmlFile(const QString& path) {
|
||||
bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& internal) {
|
||||
if (this->scannedFiles.contains(path)) return false;
|
||||
this->scannedFiles.push_back(path);
|
||||
|
||||
@@ -82,23 +114,47 @@ bool QmlScanner::scanQmlFile(const QString& path) {
|
||||
auto stream = QTextStream(&file);
|
||||
auto imports = QVector<QString>();
|
||||
|
||||
bool singleton = false;
|
||||
|
||||
while (!stream.atEnd()) {
|
||||
auto line = stream.readLine().trimmed();
|
||||
if (!singleton && line == "pragma Singleton") {
|
||||
qCDebug(logQmlScanner) << "Discovered singleton" << path;
|
||||
singleton = true;
|
||||
} else if (!internal && line == "//@ pragma Internal") {
|
||||
internal = true;
|
||||
} else if (line.startsWith("import")) {
|
||||
// we dont care about "import qs" as we always load the root folder
|
||||
if (auto importCursor = line.indexOf(" qs."); importCursor != -1) {
|
||||
importCursor += 4;
|
||||
QString path;
|
||||
|
||||
auto startQuot = line.indexOf('"');
|
||||
if (startQuot == -1 || line.length() < startQuot + 3) continue;
|
||||
auto endQuot = line.indexOf('"', startQuot + 1);
|
||||
if (endQuot == -1) continue;
|
||||
while (importCursor != line.length()) {
|
||||
auto c = line.at(importCursor);
|
||||
if (c == '.') c = '/';
|
||||
else if (c == ' ') break;
|
||||
else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|
||||
|| c == '_')
|
||||
{
|
||||
} else {
|
||||
qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line;
|
||||
goto next;
|
||||
}
|
||||
|
||||
auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1);
|
||||
imports.push_back(name);
|
||||
path.append(c);
|
||||
importCursor += 1;
|
||||
}
|
||||
|
||||
imports.append(this->rootPath.filePath(path));
|
||||
} else if (auto startQuot = line.indexOf('"');
|
||||
startQuot != -1 && line.length() >= startQuot + 3)
|
||||
{
|
||||
auto endQuot = line.indexOf('"', startQuot + 1);
|
||||
if (endQuot == -1) continue;
|
||||
|
||||
auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1);
|
||||
imports.push_back(name);
|
||||
}
|
||||
} else if (line.contains('{')) break;
|
||||
|
||||
next:;
|
||||
}
|
||||
|
||||
file.close();
|
||||
@@ -122,27 +178,39 @@ bool QmlScanner::scanQmlFile(const QString& path) {
|
||||
ipath = currentdir.filePath(import);
|
||||
}
|
||||
|
||||
auto cpath = QFileInfo(ipath).canonicalFilePath();
|
||||
auto pathInfo = QFileInfo(ipath);
|
||||
auto cpath = pathInfo.canonicalFilePath();
|
||||
|
||||
if (cpath.isEmpty()) {
|
||||
qCWarning(logQmlScanner) << "Ignoring unresolvable import" << ipath << "from" << path;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!pathInfo.isDir()) {
|
||||
qCDebug(logQmlScanner) << "Ignoring non-directory import" << ipath << "from" << path;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (import.endsWith(".js")) this->scannedFiles.push_back(cpath);
|
||||
else this->scanDir(cpath);
|
||||
}
|
||||
|
||||
return singleton;
|
||||
return true;
|
||||
}
|
||||
|
||||
void QmlScanner::scanQmlJson(const QString& path) {
|
||||
void QmlScanner::scanQmlRoot(const QString& path) {
|
||||
bool singleton = false;
|
||||
bool internal = false;
|
||||
this->scanQmlFile(path, singleton, internal);
|
||||
}
|
||||
|
||||
bool QmlScanner::scanQmlJson(const QString& path) {
|
||||
qCDebug(logQmlScanner) << "Scanning qml.json file" << path;
|
||||
|
||||
auto file = QFile(path);
|
||||
if (!file.open(QFile::ReadOnly | QFile::Text)) {
|
||||
qCWarning(logQmlScanner) << "Failed to open file" << path;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto data = file.readAll();
|
||||
@@ -154,7 +222,7 @@ void QmlScanner::scanQmlJson(const QString& path) {
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qCCritical(logQmlScanner).nospace()
|
||||
<< "Failed to parse qml.json file at " << path << ": " << error.errorString();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString body =
|
||||
@@ -164,6 +232,7 @@ void QmlScanner::scanQmlJson(const QString& path) {
|
||||
|
||||
this->fileIntercepts.insert(path.first(path.length() - 5), body);
|
||||
this->scannedFiles.push_back(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
QPair<QString, QString> QmlScanner::jsonToQml(const QJsonValue& value, int indent) {
|
||||
|
||||
+8
-4
@@ -6,7 +6,9 @@
|
||||
#include <qloggingcategory.h>
|
||||
#include <qvector.h>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logQmlScanner);
|
||||
#include "logcat.hpp"
|
||||
|
||||
QS_DECLARE_LOGGING_CATEGORY(logQmlScanner);
|
||||
|
||||
// expects canonical paths
|
||||
class QmlScanner {
|
||||
@@ -14,9 +16,10 @@ public:
|
||||
QmlScanner() = default;
|
||||
QmlScanner(const QDir& rootPath): rootPath(rootPath) {}
|
||||
|
||||
// path must be canonical
|
||||
void scanDir(const QString& path);
|
||||
// returns if the file has a singleton
|
||||
bool scanQmlFile(const QString& path);
|
||||
|
||||
void scanQmlRoot(const QString& path);
|
||||
|
||||
QVector<QString> scannedDirs;
|
||||
QVector<QString> scannedFiles;
|
||||
@@ -25,6 +28,7 @@ public:
|
||||
private:
|
||||
QDir rootPath;
|
||||
|
||||
void scanQmlJson(const QString& path);
|
||||
bool scanQmlFile(const QString& path, bool& singleton, bool& internal);
|
||||
bool scanQmlJson(const QString& path);
|
||||
[[nodiscard]] static QPair<QString, QString> jsonToQml(const QJsonValue& value, int indent = 0);
|
||||
};
|
||||
@@ -51,9 +51,3 @@ void SingletonRegistry::onReload(SingletonRegistry* old) {
|
||||
singleton->reload(old == nullptr ? nullptr : old->registry.value(url));
|
||||
}
|
||||
}
|
||||
|
||||
void SingletonRegistry::onPostReload() {
|
||||
for (auto* singleton: this->registry.values()) {
|
||||
PostReloadHook::postReloadTree(singleton);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ public:
|
||||
|
||||
void registerSingleton(const QUrl& url, Singleton* singleton);
|
||||
void onReload(SingletonRegistry* old);
|
||||
void onPostReload();
|
||||
|
||||
private:
|
||||
QHash<QUrl, Singleton*> registry;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
function (qs_test name)
|
||||
add_executable(${name} ${ARGN})
|
||||
target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window quickshell-ui)
|
||||
target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window quickshell-ui quickshell-io)
|
||||
add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $<TARGET_FILE:${name}>)
|
||||
endfunction()
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ void TestScriptModel::unique_data() {
|
||||
void TestScriptModel::unique() {
|
||||
QFETCH(const QString, oldstr);
|
||||
QFETCH(const QString, newstr);
|
||||
QFETCH(OpList, operations);
|
||||
QFETCH(const OpList, operations);
|
||||
|
||||
auto strToVariantList = [](const QString& str) -> QVariantList {
|
||||
QVariantList list;
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
#include "toolsupport.hpp"
|
||||
#include <cerrno>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdebug.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qlist.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qtenvironmentvariables.h>
|
||||
|
||||
#include "logcat.hpp"
|
||||
#include "paths.hpp"
|
||||
#include "scan.hpp"
|
||||
|
||||
namespace qs::core {
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logTooling, "quickshell.tooling", QtWarningMsg);
|
||||
}
|
||||
|
||||
bool QmlToolingSupport::updateTooling(const QDir& configRoot, QmlScanner& scanner) {
|
||||
auto* vfs = QsPaths::instance()->shellVfsDir();
|
||||
|
||||
if (!vfs) {
|
||||
qCCritical(logTooling) << "Tooling dir could not be created";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!QmlToolingSupport::lockTooling()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!QmlToolingSupport::updateQmllsConfig(configRoot, false)) {
|
||||
QDir(vfs->filePath("qs")).removeRecursively();
|
||||
return false;
|
||||
}
|
||||
|
||||
QmlToolingSupport::updateToolingFs(scanner, configRoot, vfs->filePath("qs"));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QmlToolingSupport::lockTooling() {
|
||||
if (QmlToolingSupport::toolingLock) return true;
|
||||
|
||||
auto lockPath = QsPaths::instance()->shellVfsDir()->filePath("tooling.lock");
|
||||
auto* file = new QFile(lockPath);
|
||||
|
||||
if (!file->open(QFile::WriteOnly)) {
|
||||
qCCritical(logTooling) << "Could not open tooling lock for write";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto lock = flock {
|
||||
.l_type = F_WRLCK,
|
||||
.l_whence = SEEK_SET, // NOLINT (fcntl.h??)
|
||||
.l_start = 0,
|
||||
.l_len = 0,
|
||||
.l_pid = 0,
|
||||
};
|
||||
|
||||
if (fcntl(file->handle(), F_SETLK, &lock) == 0) {
|
||||
qCInfo(logTooling) << "Acquired tooling support lock";
|
||||
QmlToolingSupport::toolingLock = file;
|
||||
return true;
|
||||
} else if (errno == EACCES || errno == EAGAIN) {
|
||||
qCInfo(logTooling) << "Tooling support locked by another instance";
|
||||
return false;
|
||||
} else {
|
||||
qCCritical(logTooling).nospace() << "Could not create tooling lock at " << lockPath
|
||||
<< " with error code " << errno << ": " << qt_error_string();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QString QmlToolingSupport::getQmllsConfig() {
|
||||
static auto config = []() {
|
||||
// We can't replicate the algorithm used to create the import path list as it can have distro
|
||||
// specific patches, e.g. nixos.
|
||||
auto importPaths = QQmlEngine().importPathList();
|
||||
importPaths.removeIf([](const QString& path) { return path.startsWith("qrc:"); });
|
||||
|
||||
auto vfsPath = QsPaths::instance()->shellVfsDir()->path();
|
||||
auto importPathsStr = importPaths.join(u':');
|
||||
|
||||
QString qmllsConfig;
|
||||
auto print = QDebug(&qmllsConfig).nospace();
|
||||
print << "[General]\nno-cmake-calls=true\nbuildDir=" << vfsPath
|
||||
<< "\nimportPaths=" << importPathsStr << '\n';
|
||||
|
||||
return qmllsConfig;
|
||||
}();
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
bool QmlToolingSupport::updateQmllsConfig(const QDir& configRoot, bool create) {
|
||||
auto shellConfigPath = configRoot.filePath(".qmlls.ini");
|
||||
auto vfsConfigPath = QsPaths::instance()->shellVfsDir()->filePath(".qmlls.ini");
|
||||
|
||||
auto shellFileInfo = QFileInfo(shellConfigPath);
|
||||
if (!create && !shellFileInfo.exists() && !shellFileInfo.isSymLink()) {
|
||||
if (QmlToolingSupport::toolingEnabled) {
|
||||
qInfo() << "QML tooling support disabled";
|
||||
QmlToolingSupport::toolingEnabled = false;
|
||||
} else {
|
||||
qCInfo(logTooling) << "Not enabling QML tooling support, qmlls.ini is missing at path"
|
||||
<< shellConfigPath;
|
||||
}
|
||||
|
||||
QFile::remove(vfsConfigPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto vfsFile = QFile(vfsConfigPath);
|
||||
|
||||
if (!vfsFile.open(QFile::ReadWrite | QFile::Text)) {
|
||||
qCCritical(logTooling) << "Failed to create qmlls config in vfs";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto config = QmlToolingSupport::getQmllsConfig();
|
||||
|
||||
if (vfsFile.readAll() != config) {
|
||||
if (!vfsFile.resize(0) || !vfsFile.write(config.toUtf8())) {
|
||||
qCCritical(logTooling) << "Failed to write qmlls config in vfs";
|
||||
return false;
|
||||
}
|
||||
|
||||
qCDebug(logTooling) << "Wrote qmlls config in vfs";
|
||||
}
|
||||
|
||||
if (!shellFileInfo.isSymLink() || shellFileInfo.symLinkTarget() != vfsConfigPath) {
|
||||
QFile::remove(shellConfigPath);
|
||||
|
||||
if (!QFile::link(vfsConfigPath, shellConfigPath)) {
|
||||
qCCritical(logTooling) << "Failed to create qmlls config symlink";
|
||||
return false;
|
||||
}
|
||||
|
||||
qCDebug(logTooling) << "Created qmlls config symlink";
|
||||
}
|
||||
|
||||
if (!QmlToolingSupport::toolingEnabled) {
|
||||
qInfo() << "QML tooling support enabled";
|
||||
QmlToolingSupport::toolingEnabled = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void QmlToolingSupport::updateToolingFs(
|
||||
QmlScanner& scanner,
|
||||
const QDir& scanDir,
|
||||
const QDir& linkDir
|
||||
) {
|
||||
QList<QString> files;
|
||||
QSet<QString> subdirs;
|
||||
|
||||
auto scanPath = scanDir.path();
|
||||
|
||||
linkDir.mkpath(".");
|
||||
|
||||
for (auto& path: scanner.scannedFiles) {
|
||||
if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue;
|
||||
auto name = path.sliced(scanPath.length() + 1);
|
||||
|
||||
if (name.contains('/')) {
|
||||
auto dirname = name.first(name.indexOf('/'));
|
||||
subdirs.insert(dirname);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto fileInfo = QFileInfo(path);
|
||||
if (!fileInfo.isFile()) continue;
|
||||
|
||||
auto spath = linkDir.filePath(name);
|
||||
auto sFileInfo = QFileInfo(spath);
|
||||
|
||||
if (!sFileInfo.isSymLink() || sFileInfo.symLinkTarget() != path) {
|
||||
QFile::remove(spath);
|
||||
|
||||
if (QFile::link(path, spath)) {
|
||||
qCDebug(logTooling) << "Created symlink to" << path << "at" << spath;
|
||||
files.append(spath);
|
||||
} else {
|
||||
qCCritical(logTooling) << "Could not create symlink to" << path << "at" << spath;
|
||||
}
|
||||
} else {
|
||||
files.append(spath);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto [path, text]: scanner.fileIntercepts.asKeyValueRange()) {
|
||||
if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue;
|
||||
auto name = path.sliced(scanPath.length() + 1);
|
||||
|
||||
if (name.contains('/')) {
|
||||
auto dirname = name.first(name.indexOf('/'));
|
||||
subdirs.insert(dirname);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto spath = linkDir.filePath(name);
|
||||
auto file = QFile(spath);
|
||||
if (!file.open(QFile::ReadWrite | QFile::Text)) {
|
||||
qCCritical(logTooling) << "Failed to open injected file" << spath;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.readAll() == text) {
|
||||
files.append(spath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.resize(0) && file.write(text.toUtf8())) {
|
||||
files.append(spath);
|
||||
qCDebug(logTooling) << "Wrote injected file" << spath;
|
||||
} else {
|
||||
qCCritical(logTooling) << "Failed to write injected file" << spath;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& name: linkDir.entryList(QDir::Files | QDir::System)) { // System = broken symlinks
|
||||
auto path = linkDir.filePath(name);
|
||||
|
||||
if (!files.contains(path)) {
|
||||
if (QFile::remove(path)) qCDebug(logTooling) << "Removed old file at" << path;
|
||||
else qCWarning(logTooling) << "Failed to remove old file at" << path;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& subdir: subdirs) {
|
||||
QmlToolingSupport::updateToolingFs(scanner, scanDir.filePath(subdir), linkDir.filePath(subdir));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace qs::core
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <qdir.h>
|
||||
|
||||
#include "scan.hpp"
|
||||
|
||||
namespace qs::core {
|
||||
|
||||
class QmlToolingSupport {
|
||||
public:
|
||||
static bool updateTooling(const QDir& configRoot, QmlScanner& scanner);
|
||||
|
||||
private:
|
||||
static QString getQmllsConfig();
|
||||
static bool lockTooling();
|
||||
static bool updateQmllsConfig(const QDir& configRoot, bool create);
|
||||
static void updateToolingFs(QmlScanner& scanner, const QDir& scanDir, const QDir& linkDir);
|
||||
static inline bool toolingEnabled = false;
|
||||
static inline QFile* toolingLock = nullptr;
|
||||
};
|
||||
|
||||
} // namespace qs::core
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#include "../core/instanceinfo.hpp"
|
||||
#include "../core/logcat.hpp"
|
||||
|
||||
extern char** environ; // NOLINT
|
||||
|
||||
@@ -23,7 +24,7 @@ using namespace google_breakpad;
|
||||
namespace qs::crash {
|
||||
|
||||
namespace {
|
||||
Q_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg);
|
||||
}
|
||||
|
||||
struct CrashHandlerPrivate {
|
||||
|
||||
+5
-2
@@ -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...";
|
||||
|
||||
@@ -2,13 +2,24 @@ set_source_files_properties(org.freedesktop.DBus.Properties.xml PROPERTIES
|
||||
CLASSNAME DBusPropertiesInterface
|
||||
)
|
||||
|
||||
set_source_files_properties(org.freedesktop.DBus.ObjectManager.xml PROPERTIES
|
||||
CLASSNAME DBusObjectManagerInterface
|
||||
INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/dbus_objectmanager_types.hpp
|
||||
)
|
||||
|
||||
qt_add_dbus_interface(DBUS_INTERFACES
|
||||
org.freedesktop.DBus.Properties.xml
|
||||
dbus_properties
|
||||
)
|
||||
|
||||
qt_add_dbus_interface(DBUS_INTERFACES
|
||||
org.freedesktop.DBus.ObjectManager.xml
|
||||
dbus_objectmanager
|
||||
)
|
||||
|
||||
qt_add_library(quickshell-dbus STATIC
|
||||
properties.cpp
|
||||
objectmanager.cpp
|
||||
bus.cpp
|
||||
${DBUS_INTERFACES}
|
||||
)
|
||||
|
||||
+3
-1
@@ -12,10 +12,12 @@
|
||||
#include <qtypes.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
namespace {
|
||||
Q_LOGGING_CATEGORY(logDbus, "quickshell.dbus", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logDbus, "quickshell.dbus", QtWarningMsg);
|
||||
}
|
||||
|
||||
void tryLaunchService(
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <qdbusextratypes.h>
|
||||
#include <qhash.h>
|
||||
#include <qmap.h>
|
||||
#include <qstring.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
using DBusObjectManagerInterfaces = QHash<QString, QVariantMap>;
|
||||
using DBusObjectManagerObjects = QHash<QDBusObjectPath, DBusObjectManagerInterfaces>;
|
||||
@@ -21,13 +21,14 @@
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "../../core/iconimageprovider.hpp"
|
||||
#include "../../core/logcat.hpp"
|
||||
#include "../../core/model.hpp"
|
||||
#include "../../core/qsmenu.hpp"
|
||||
#include "../../dbus/properties.hpp"
|
||||
#include "dbus_menu.h"
|
||||
#include "dbus_menu_types.hpp"
|
||||
|
||||
Q_LOGGING_CATEGORY(logDbusMenu, "quickshell.dbus.dbusmenu", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logDbusMenu, "quickshell.dbus.dbusmenu", QtWarningMsg);
|
||||
|
||||
using namespace qs::menu;
|
||||
|
||||
@@ -311,8 +312,8 @@ void DBusMenu::prepareToShow(qint32 item, qint32 depth) {
|
||||
auto responseCallback = [this, item, depth](QDBusPendingCallWatcher* call) {
|
||||
const QDBusPendingReply<bool> reply = *call;
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDbusMenu) << "Error in AboutToShow, but showing anyway for menu" << item << "of"
|
||||
<< this << reply.error();
|
||||
qCDebug(logDbusMenu) << "Error in AboutToShow, but showing anyway for menu" << item << "of"
|
||||
<< this << reply.error();
|
||||
}
|
||||
|
||||
this->updateLayout(item, depth);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#include "../properties.hpp"
|
||||
#include "dbus_menu_types.hpp"
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logDbusMenu);
|
||||
QS_DECLARE_LOGGING_CATEGORY(logDbusMenu);
|
||||
|
||||
class DBusMenuInterface;
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#include "objectmanager.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdbusconnection.h>
|
||||
#include <qdbusmetatype.h>
|
||||
#include <qdbuspendingcall.h>
|
||||
#include <qdbuspendingreply.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "dbus_objectmanager.h"
|
||||
#include "dbus_objectmanager_types.hpp"
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logDbusObjectManager, "quickshell.dbus.objectmanager", QtWarningMsg);
|
||||
}
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
DBusObjectManager::DBusObjectManager(QObject* parent): QObject(parent) {
|
||||
qDBusRegisterMetaType<DBusObjectManagerInterfaces>();
|
||||
qDBusRegisterMetaType<DBusObjectManagerObjects>();
|
||||
}
|
||||
|
||||
bool DBusObjectManager::setInterface(
|
||||
const QString& service,
|
||||
const QString& path,
|
||||
const QDBusConnection& connection
|
||||
) {
|
||||
delete this->mInterface;
|
||||
this->mInterface = new DBusObjectManagerInterface(service, path, connection, this);
|
||||
|
||||
if (!this->mInterface->isValid()) {
|
||||
qCWarning(logDbusObjectManager) << "Failed to create DBusObjectManagerInterface for" << service
|
||||
<< path << ":" << this->mInterface->lastError();
|
||||
delete this->mInterface;
|
||||
this->mInterface = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
QObject::connect(
|
||||
this->mInterface,
|
||||
&DBusObjectManagerInterface::InterfacesAdded,
|
||||
this,
|
||||
&DBusObjectManager::interfacesAdded
|
||||
);
|
||||
|
||||
QObject::connect(
|
||||
this->mInterface,
|
||||
&DBusObjectManagerInterface::InterfacesRemoved,
|
||||
this,
|
||||
&DBusObjectManager::interfacesRemoved
|
||||
);
|
||||
|
||||
this->fetchInitialObjects();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DBusObjectManager::fetchInitialObjects() {
|
||||
if (!this->mInterface) return;
|
||||
|
||||
auto reply = this->mInterface->GetManagedObjects();
|
||||
auto* watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QDBusPendingCallWatcher::finished,
|
||||
this,
|
||||
[this](QDBusPendingCallWatcher* watcher) {
|
||||
const QDBusPendingReply<DBusObjectManagerObjects> reply = *watcher;
|
||||
watcher->deleteLater();
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDbusObjectManager) << "Failed to get managed objects:" << reply.error();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& [path, interfaces]: reply.value().asKeyValueRange()) {
|
||||
emit this->interfacesAdded(path, interfaces);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace qs::dbus
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <qdbusconnection.h>
|
||||
#include <qobject.h>
|
||||
#include <qstring.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "dbus_objectmanager_types.hpp"
|
||||
|
||||
class DBusObjectManagerInterface;
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
class DBusObjectManager: public QObject {
|
||||
Q_OBJECT;
|
||||
|
||||
public:
|
||||
explicit DBusObjectManager(QObject* parent = nullptr);
|
||||
|
||||
bool setInterface(
|
||||
const QString& service,
|
||||
const QString& path,
|
||||
const QDBusConnection& connection = QDBusConnection::sessionBus()
|
||||
);
|
||||
|
||||
signals:
|
||||
void
|
||||
interfacesAdded(const QDBusObjectPath& objectPath, const DBusObjectManagerInterfaces& interfaces);
|
||||
void interfacesRemoved(const QDBusObjectPath& objectPath, const QStringList& interfaces);
|
||||
|
||||
private:
|
||||
void fetchInitialObjects();
|
||||
|
||||
DBusObjectManagerInterface* mInterface = nullptr;
|
||||
};
|
||||
|
||||
} // namespace qs::dbus
|
||||
@@ -0,0 +1,18 @@
|
||||
<node>
|
||||
<interface name="org.freedesktop.DBus.ObjectManager">
|
||||
<method name="GetManagedObjects">
|
||||
<arg name="objects" direction="out" type="a{oa{sa{sv}}}"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="DBusObjectManagerObjects"/>
|
||||
</method>
|
||||
|
||||
<signal name="InterfacesAdded">
|
||||
<arg name="object" type="o"/>
|
||||
<arg name="interfaces" type="a{sa{sv}}"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="DBusObjectManagerInterfaces"/>
|
||||
</signal>
|
||||
<signal name="InterfacesRemoved">
|
||||
<arg name="object" type="o"/>
|
||||
<arg name="interfaces" type="as"/>
|
||||
</signal>
|
||||
</interface>
|
||||
</node>
|
||||
+17
-3
@@ -17,11 +17,13 @@
|
||||
#include <qmetatype.h>
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtversionchecks.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "dbus_properties.h"
|
||||
|
||||
Q_LOGGING_CATEGORY(logDbusProperties, "quickshell.dbus.properties", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logDbusProperties, "quickshell.dbus.properties", QtWarningMsg);
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
@@ -244,8 +246,13 @@ void DBusPropertyGroup::requestPropertyUpdate(DBusPropertyCore* property) {
|
||||
const QDBusPendingReply<QDBusVariant> reply = *call;
|
||||
|
||||
if (reply.isError()) {
|
||||
qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr;
|
||||
qCWarning(logDbusProperties) << reply.error();
|
||||
if (!property->isRequired() && reply.error().type() == QDBusError::InvalidArgs) {
|
||||
qCDebug(logDbusProperties) << "Error updating non-required property" << propStr;
|
||||
qCDebug(logDbusProperties) << reply.error();
|
||||
} else {
|
||||
qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr;
|
||||
qCWarning(logDbusProperties) << reply.error();
|
||||
}
|
||||
} else {
|
||||
this->tryUpdateProperty(property, reply.value().variant());
|
||||
}
|
||||
@@ -326,3 +333,10 @@ void DBusPropertyGroup::onPropertiesChanged(
|
||||
}
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
|
||||
QDebug operator<<(QDebug debug, const QDBusObjectPath& path) {
|
||||
debug.nospace() << "QDBusObjectPath(" << path.path() << ")";
|
||||
return debug;
|
||||
}
|
||||
#endif
|
||||
+11
-5
@@ -20,13 +20,15 @@
|
||||
#include <qstringview.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qtversionchecks.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "../core/util.hpp"
|
||||
|
||||
class DBusPropertiesInterface;
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(logDbusProperties);
|
||||
QS_DECLARE_LOGGING_CATEGORY(logDbusProperties);
|
||||
|
||||
namespace qs::dbus {
|
||||
|
||||
@@ -166,9 +168,9 @@ class DBusBindableProperty: public DBusPropertyCore {
|
||||
public:
|
||||
explicit DBusBindableProperty() { this->group()->attachProperty(this); }
|
||||
|
||||
[[nodiscard]] QString name() const override { return Name; };
|
||||
[[nodiscard]] QStringView nameRef() const override { return Name; };
|
||||
[[nodiscard]] bool isRequired() const override { return required; };
|
||||
[[nodiscard]] QString name() const override { return Name; }
|
||||
[[nodiscard]] QStringView nameRef() const override { return Name; }
|
||||
[[nodiscard]] bool isRequired() const override { return required; }
|
||||
|
||||
[[nodiscard]] QString valueString() override {
|
||||
QString str;
|
||||
@@ -234,6 +236,7 @@ public:
|
||||
void attachProperty(DBusPropertyCore* property);
|
||||
void updateAllDirect();
|
||||
void updateAllViaGetAll();
|
||||
void updatePropertySet(const QVariantMap& properties, bool complainMissing = true);
|
||||
[[nodiscard]] QString toString() const;
|
||||
[[nodiscard]] bool isConnected() const { return this->interface; }
|
||||
|
||||
@@ -252,7 +255,6 @@ private slots:
|
||||
);
|
||||
|
||||
private:
|
||||
void updatePropertySet(const QVariantMap& properties, bool complainMissing);
|
||||
void tryUpdateProperty(DBusPropertyCore* property, const QVariant& variant) const;
|
||||
[[nodiscard]] QString propertyString(const DBusPropertyCore* property) const;
|
||||
|
||||
@@ -265,6 +267,10 @@ private:
|
||||
|
||||
} // namespace qs::dbus
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
|
||||
QDebug operator<<(QDebug debug, const QDBusObjectPath& path);
|
||||
#endif
|
||||
|
||||
// NOLINTBEGIN
|
||||
#define QS_DBUS_BINDABLE_PROPERTY_GROUP(Class, name) qs::dbus::DBusPropertyGroup name {this};
|
||||
|
||||
|
||||
+3
-1
@@ -11,10 +11,12 @@
|
||||
#include <qquickwindow.h>
|
||||
#include <qstringliteral.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
|
||||
namespace qs::debug {
|
||||
|
||||
namespace {
|
||||
Q_LOGGING_CATEGORY(logLint, "quickshell.linter", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logLint, "quickshell.linter", QtWarningMsg);
|
||||
|
||||
void lintZeroSized(QQuickItem* item);
|
||||
bool isRenderable(QQuickItem* item);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
qt_add_library(quickshell-io STATIC
|
||||
datastream.cpp
|
||||
processcore.cpp
|
||||
process.cpp
|
||||
fileview.cpp
|
||||
jsonadapter.cpp
|
||||
@@ -20,6 +21,7 @@ qt_add_qml_module(quickshell-io
|
||||
FileView.qml
|
||||
)
|
||||
|
||||
qs_add_module_deps_light(quickshell-io Quickshell)
|
||||
install_qml_module(quickshell-io)
|
||||
|
||||
target_link_libraries(quickshell-io PRIVATE Qt::Quick)
|
||||
|
||||
@@ -55,7 +55,7 @@ public:
|
||||
|
||||
// the buffer will be sent in both slots if there is data remaining from a previous parser
|
||||
virtual void parseBytes(QByteArray& incoming, QByteArray& buffer) = 0;
|
||||
virtual void streamEnded(QByteArray& /*buffer*/) {};
|
||||
virtual void streamEnded(QByteArray& /*buffer*/) {}
|
||||
|
||||
signals:
|
||||
/// Emitted when data is read from the stream.
|
||||
@@ -63,7 +63,7 @@ signals:
|
||||
};
|
||||
|
||||
///! DataStreamParser for delimited data streams.
|
||||
/// DataStreamParser for delimited data streams. @@read() is emitted once per delimited chunk of the stream.
|
||||
/// DataStreamParser for delimited data streams. @@DataStreamParser.read(s) is emitted once per delimited chunk of the stream.
|
||||
class SplitParser: public DataStreamParser {
|
||||
Q_OBJECT;
|
||||
/// The delimiter for parsed data. May be multiple characters. Defaults to `\n`.
|
||||
|
||||
+6
-2
@@ -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;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qdebug.h>
|
||||
#include <qlogging.h>
|
||||
#include <qloggingcategory.h>
|
||||
#include <qmetaobject.h>
|
||||
#include <qobject.h>
|
||||
#include <qobjectdefs.h>
|
||||
@@ -14,10 +15,15 @@
|
||||
#include <qtypes.h>
|
||||
|
||||
#include "../core/generation.hpp"
|
||||
#include "../core/logcat.hpp"
|
||||
#include "ipc.hpp"
|
||||
|
||||
namespace qs::io::ipc {
|
||||
|
||||
namespace {
|
||||
QS_LOGGING_CATEGORY(logIpcHandler, "quickshell.ipchandler", QtWarningMsg)
|
||||
}
|
||||
|
||||
bool IpcFunction::resolve(QString& error) {
|
||||
if (this->method.parameterCount() > 10) {
|
||||
error = "Due to technical limitations, IPC functions can only have 10 arguments.";
|
||||
@@ -210,6 +216,7 @@ IpcHandlerRegistry* IpcHandlerRegistry::forGeneration(EngineGeneration* generati
|
||||
if (!ext) {
|
||||
ext = new IpcHandlerRegistry();
|
||||
generation->registerExtension(&key, ext);
|
||||
qCDebug(logIpcHandler) << "Created new IPC handler registry" << ext << "for" << generation;
|
||||
}
|
||||
|
||||
return dynamic_cast<IpcHandlerRegistry*>(ext);
|
||||
@@ -232,10 +239,12 @@ void IpcHandler::updateRegistration(bool destroying) {
|
||||
|
||||
if (this->registeredState.enabled) {
|
||||
registry->deregisterHandler(this);
|
||||
qCDebug(logIpcHandler) << "Deregistered" << this << "from registry" << registry;
|
||||
}
|
||||
|
||||
if (this->targetState.enabled && !this->targetState.target.isEmpty()) {
|
||||
registry->registerHandler(this);
|
||||
qCDebug(logIpcHandler) << "Registered" << this << "to registry" << registry;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,9 +154,7 @@ class IpcHandlerRegistry;
|
||||
/// #### Properties
|
||||
/// Properties of an IpcHanlder can be read using `qs ipc prop get` as long as they are
|
||||
/// of an IPC compatible type. See the table above for compatible types.
|
||||
class IpcHandler
|
||||
: public QObject
|
||||
, public PostReloadHook {
|
||||
class IpcHandler: public PostReloadHook {
|
||||
Q_OBJECT;
|
||||
/// If the handler should be able to receive calls. Defaults to true.
|
||||
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
|
||||
@@ -166,7 +164,7 @@ class IpcHandler
|
||||
QML_ELEMENT;
|
||||
|
||||
public:
|
||||
explicit IpcHandler(QObject* parent = nullptr): QObject(parent) {};
|
||||
explicit IpcHandler(QObject* parent = nullptr): PostReloadHook(parent) {}
|
||||
~IpcHandler() override;
|
||||
Q_DISABLE_COPY_MOVE(IpcHandler);
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ class JsonAdapter
|
||||
, public QQmlParserStatus {
|
||||
Q_OBJECT;
|
||||
QML_ELEMENT;
|
||||
Q_INTERFACES(QQmlParserStatus);
|
||||
|
||||
public:
|
||||
void classBegin() override {}
|
||||
|
||||
+57
-34
@@ -3,9 +3,9 @@
|
||||
#include <utility>
|
||||
|
||||
#include <qdir.h>
|
||||
#include <qhash.h>
|
||||
#include <qlist.h>
|
||||
#include <qlogging.h>
|
||||
#include <qmap.h>
|
||||
#include <qobject.h>
|
||||
#include <qprocess.h>
|
||||
#include <qqmlinfo.h>
|
||||
@@ -13,12 +13,13 @@
|
||||
#include <qtypes.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "../core/common.hpp"
|
||||
#include "../core/generation.hpp"
|
||||
#include "../core/qmlglobal.hpp"
|
||||
#include "../core/reload.hpp"
|
||||
#include "datastream.hpp"
|
||||
#include "processcore.hpp"
|
||||
|
||||
Process::Process(QObject* parent): QObject(parent) {
|
||||
Process::Process(QObject* parent): PostReloadHook(parent) {
|
||||
QObject::connect(
|
||||
QuickshellSettings::instance(),
|
||||
&QuickshellSettings::workingDirectoryChanged,
|
||||
@@ -27,6 +28,18 @@ Process::Process(QObject* parent): QObject(parent) {
|
||||
);
|
||||
}
|
||||
|
||||
Process::~Process() {
|
||||
if (this->process != nullptr && this->process->processId() != 0) {
|
||||
// Deleting after the process finishes hides the process destroyed warning in logs
|
||||
QObject::connect(this->process, &QProcess::finished, [p = this->process] { delete p; });
|
||||
|
||||
this->process->setParent(nullptr);
|
||||
this->process->kill();
|
||||
}
|
||||
}
|
||||
|
||||
void Process::onPostReload() { this->startProcessIfReady(); }
|
||||
|
||||
bool Process::isRunning() const { return this->process != nullptr; }
|
||||
|
||||
void Process::setRunning(bool running) {
|
||||
@@ -46,17 +59,7 @@ void Process::setCommand(QList<QString> command) {
|
||||
if (this->mCommand == command) return;
|
||||
this->mCommand = std::move(command);
|
||||
|
||||
auto& cmd = this->mCommand.first();
|
||||
if (cmd.startsWith("file://")) {
|
||||
cmd = cmd.sliced(7);
|
||||
} else if (cmd.startsWith("root://")) {
|
||||
cmd = cmd.sliced(7);
|
||||
auto& root = EngineGeneration::findObjectGeneration(this)->rootPath;
|
||||
cmd = root.filePath(cmd.startsWith('/') ? cmd.sliced(1) : cmd);
|
||||
}
|
||||
|
||||
emit this->commandChanged();
|
||||
|
||||
this->startProcessIfReady();
|
||||
}
|
||||
|
||||
@@ -79,9 +82,9 @@ void Process::onGlobalWorkingDirectoryChanged() {
|
||||
}
|
||||
}
|
||||
|
||||
QMap<QString, QVariant> Process::environment() const { return this->mEnvironment; }
|
||||
QHash<QString, QVariant> Process::environment() const { return this->mEnvironment; }
|
||||
|
||||
void Process::setEnvironment(QMap<QString, QVariant> environment) {
|
||||
void Process::setEnvironment(QHash<QString, QVariant> environment) {
|
||||
if (environment == this->mEnvironment) return;
|
||||
this->mEnvironment = std::move(environment);
|
||||
emit this->environmentChanged();
|
||||
@@ -175,10 +178,21 @@ void Process::setStdinEnabled(bool enabled) {
|
||||
}
|
||||
|
||||
void Process::startProcessIfReady() {
|
||||
if (this->process != nullptr || !this->targetRunning || this->mCommand.isEmpty()) return;
|
||||
if (this->process != nullptr || !this->isPostReload || !this->targetRunning
|
||||
|| this->mCommand.isEmpty())
|
||||
return;
|
||||
|
||||
this->targetRunning = false;
|
||||
|
||||
auto& cmd = this->mCommand.first();
|
||||
if (cmd.startsWith("file://")) {
|
||||
cmd = cmd.sliced(7);
|
||||
} else if (cmd.startsWith("root://")) {
|
||||
cmd = cmd.sliced(7);
|
||||
auto& root = EngineGeneration::findObjectGeneration(this)->rootPath;
|
||||
cmd = root.filePath(cmd.startsWith('/') ? cmd.sliced(1) : cmd);
|
||||
}
|
||||
|
||||
auto args = this->mCommand.sliced(1);
|
||||
|
||||
this->process = new QProcess(this);
|
||||
@@ -202,6 +216,25 @@ void Process::startProcessIfReady() {
|
||||
this->process->start(cmd, args);
|
||||
}
|
||||
|
||||
void Process::exec(QList<QString> command) {
|
||||
this->exec(qs::io::process::ProcessContext(std::move(command)));
|
||||
}
|
||||
|
||||
void Process::exec(const qs::io::process::ProcessContext& context) {
|
||||
this->setRunning(false);
|
||||
if (context.commandSet) this->setCommand(context.command);
|
||||
if (context.environmentSet) this->setEnvironment(context.environment);
|
||||
if (context.clearEnvironmentSet) this->setEnvironmentCleared(context.clearEnvironment);
|
||||
if (context.workingDirectorySet) this->setWorkingDirectory(context.workingDirectory);
|
||||
|
||||
if (this->mCommand.isEmpty()) {
|
||||
qmlWarning(this) << "Cannot start process as command is empty.";
|
||||
return;
|
||||
}
|
||||
|
||||
this->setRunning(true);
|
||||
}
|
||||
|
||||
void Process::startDetached() {
|
||||
if (this->mCommand.isEmpty()) {
|
||||
qmlWarning(this) << "Cannot start process as command is empty.";
|
||||
@@ -216,6 +249,11 @@ void Process::startDetached() {
|
||||
this->setupEnvironment(&process);
|
||||
process.setProgram(cmd);
|
||||
process.setArguments(args);
|
||||
|
||||
process.setStandardInputFile(QProcess::nullDevice());
|
||||
process.setStandardOutputFile(QProcess::nullDevice());
|
||||
process.setStandardErrorFile(QProcess::nullDevice());
|
||||
|
||||
process.startDetached();
|
||||
}
|
||||
|
||||
@@ -224,24 +262,7 @@ void Process::setupEnvironment(QProcess* process) {
|
||||
process->setWorkingDirectory(this->mWorkingDirectory);
|
||||
}
|
||||
|
||||
const auto& sysenv = qs::Common::INITIAL_ENVIRONMENT;
|
||||
auto env = this->mClearEnvironment ? QProcessEnvironment() : sysenv;
|
||||
|
||||
for (auto& name: this->mEnvironment.keys()) {
|
||||
auto value = this->mEnvironment.value(name);
|
||||
if (!value.isValid()) continue;
|
||||
|
||||
if (this->mClearEnvironment) {
|
||||
if (value.isNull()) {
|
||||
if (sysenv.contains(name)) env.insert(name, sysenv.value(name));
|
||||
} else env.insert(name, value.toString());
|
||||
} else {
|
||||
if (value.isNull()) env.remove(name);
|
||||
else env.insert(name, value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
process->setProcessEnvironment(env);
|
||||
qs::io::process::setupProcessEnvironment(process, this->mClearEnvironment, this->mEnvironment);
|
||||
}
|
||||
|
||||
void Process::onStarted() {
|
||||
@@ -261,6 +282,8 @@ void Process::onFinished(qint32 exitCode, QProcess::ExitStatus exitStatus) {
|
||||
emit this->exited(exitCode, exitStatus);
|
||||
emit this->runningChanged();
|
||||
emit this->processIdChanged();
|
||||
|
||||
this->startProcessIfReady(); // for `running = false; running = true`
|
||||
}
|
||||
|
||||
void Process::onErrorOccurred(QProcess::ProcessError error) {
|
||||
|
||||
+50
-6
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qhash.h>
|
||||
#include <qobject.h>
|
||||
#include <qprocess.h>
|
||||
#include <qqmlintegration.h>
|
||||
@@ -9,7 +10,10 @@
|
||||
#include <qtypes.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "../core/doc.hpp"
|
||||
#include "../core/reload.hpp"
|
||||
#include "datastream.hpp"
|
||||
#include "processcore.hpp"
|
||||
|
||||
// Needed when compiling with clang musl-libc++.
|
||||
// Default include paths contain macros that cause name collisions.
|
||||
@@ -27,7 +31,7 @@
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
class Process: public QObject {
|
||||
class Process: public PostReloadHook {
|
||||
Q_OBJECT;
|
||||
// clang-format off
|
||||
/// If the process is currently running. Defaults to false.
|
||||
@@ -98,7 +102,7 @@ class Process: public QObject {
|
||||
/// If the process is already running changing this property will affect the next
|
||||
/// started process. If the property has been changed after starting a process it will
|
||||
/// return the new value, not the one for the currently running process.
|
||||
Q_PROPERTY(QMap<QString, QVariant> environment READ environment WRITE setEnvironment NOTIFY environmentChanged);
|
||||
Q_PROPERTY(QHash<QString, QVariant> environment READ environment WRITE setEnvironment NOTIFY environmentChanged);
|
||||
/// If the process's environment should be cleared prior to applying @@environment.
|
||||
/// Defaults to false.
|
||||
///
|
||||
@@ -133,6 +137,44 @@ class Process: public QObject {
|
||||
|
||||
public:
|
||||
explicit Process(QObject* parent = nullptr);
|
||||
~Process() override;
|
||||
Q_DISABLE_COPY_MOVE(Process);
|
||||
|
||||
void onPostReload() override;
|
||||
|
||||
// MUST be before exec(ctx) or the other will be called with a default constructed obj.
|
||||
QSDOC_HIDE Q_INVOKABLE void exec(QList<QString> command);
|
||||
/// Launch a process with the given arguments, stopping any currently running process.
|
||||
///
|
||||
/// The context parameter can either be a list of command arguments or a JS object with the following fields:
|
||||
/// - `command`: A list containing the command and all its arguments. See @@Quickshell.Io.Process.command.
|
||||
/// - `environment`: Changes to make to the process environment. See @@Quickshell.Io.Process.environment.
|
||||
/// - `clearEnvironment`: Removes all variables from the environment if true.
|
||||
/// - `workingDirectory`: The working directory the command should run in.
|
||||
///
|
||||
/// Passed parameters will change the values currently set in the process.
|
||||
///
|
||||
/// > [!WARNING] This does not run command in a shell. All arguments to the command
|
||||
/// > must be in separate values in the list, e.g. `["echo", "hello"]`
|
||||
/// > and not `["echo hello"]`.
|
||||
/// >
|
||||
/// > Additionally, shell scripts must be run by your shell,
|
||||
/// > e.g. `["sh", "script.sh"]` instead of `["script.sh"]` unless the script
|
||||
/// > has a shebang.
|
||||
///
|
||||
/// > [!INFO] You can use `["sh", "-c", <your command>]` to execute your command with
|
||||
/// > the system shell.
|
||||
///
|
||||
/// Calling this function is equivalent to running:
|
||||
/// ```qml
|
||||
/// process.running = false;
|
||||
/// process.command = ...
|
||||
/// process.environment = ...
|
||||
/// process.clearEnvironment = ...
|
||||
/// process.workingDirectory = ...
|
||||
/// process.running = true;
|
||||
/// ```
|
||||
Q_INVOKABLE void exec(const qs::io::process::ProcessContext& context);
|
||||
|
||||
/// Sends a signal to the process if @@running is true, otherwise does nothing.
|
||||
Q_INVOKABLE void signal(qint32 signal);
|
||||
@@ -140,10 +182,12 @@ public:
|
||||
/// Writes to the process's stdin. Does nothing if @@running is false.
|
||||
Q_INVOKABLE void write(const QString& data);
|
||||
|
||||
/// Launches an instance of the process detached from quickshell.
|
||||
/// Launches an instance of the process detached from Quickshell.
|
||||
///
|
||||
/// The subprocess will not be tracked, @@running will be false,
|
||||
/// and the subprocess will not be killed by Quickshell.
|
||||
///
|
||||
/// This function is equivalent to @@Quickshell.Quickshell.execDetached().
|
||||
Q_INVOKABLE void startDetached();
|
||||
|
||||
[[nodiscard]] bool isRunning() const;
|
||||
@@ -157,8 +201,8 @@ public:
|
||||
[[nodiscard]] QString workingDirectory() const;
|
||||
void setWorkingDirectory(const QString& workingDirectory);
|
||||
|
||||
[[nodiscard]] QMap<QString, QVariant> environment() const;
|
||||
void setEnvironment(QMap<QString, QVariant> environment);
|
||||
[[nodiscard]] QHash<QString, QVariant> environment() const;
|
||||
void setEnvironment(QHash<QString, QVariant> environment);
|
||||
|
||||
[[nodiscard]] bool environmentCleared() const;
|
||||
void setEnvironmentCleared(bool cleared);
|
||||
@@ -203,7 +247,7 @@ private:
|
||||
QProcess* process = nullptr;
|
||||
QList<QString> mCommand;
|
||||
QString mWorkingDirectory;
|
||||
QMap<QString, QVariant> mEnvironment;
|
||||
QHash<QString, QVariant> mEnvironment;
|
||||
DataStreamParser* mStdoutParser = nullptr;
|
||||
DataStreamParser* mStderrParser = nullptr;
|
||||
QByteArray stdoutBuffer;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "processcore.hpp"
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qhash.h>
|
||||
#include <qprocess.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
#include "../core/common.hpp"
|
||||
|
||||
namespace qs::io::process {
|
||||
|
||||
void setupProcessEnvironment(
|
||||
QProcess* process,
|
||||
bool clear,
|
||||
const QHash<QString, QVariant>& envChanges
|
||||
) {
|
||||
const auto& sysenv = qs::Common::INITIAL_ENVIRONMENT;
|
||||
auto env = clear ? QProcessEnvironment() : sysenv;
|
||||
|
||||
for (auto& name: envChanges.keys()) {
|
||||
auto value = envChanges.value(name);
|
||||
if (!value.isValid()) continue;
|
||||
|
||||
if (clear) {
|
||||
if (value.isNull()) {
|
||||
if (sysenv.contains(name)) env.insert(name, sysenv.value(name));
|
||||
} else env.insert(name, value.toString());
|
||||
} else {
|
||||
if (value.isNull()) env.remove(name);
|
||||
else env.insert(name, value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
process->setProcessEnvironment(env);
|
||||
}
|
||||
|
||||
} // namespace qs::io::process
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
#include <qhash.h>
|
||||
#include <qlist.h>
|
||||
#include <qprocess.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
namespace qs::io::process {
|
||||
|
||||
class ProcessContext {
|
||||
Q_PROPERTY(QList<QString> command MEMBER command WRITE setCommand);
|
||||
Q_PROPERTY(QHash<QString, QVariant> environment MEMBER environment WRITE setEnvironment);
|
||||
Q_PROPERTY(bool clearEnvironment MEMBER clearEnvironment WRITE setClearEnvironment);
|
||||
Q_PROPERTY(QString workingDirectory MEMBER workingDirectory WRITE setWorkingDirectory);
|
||||
Q_PROPERTY(bool unbindStdout MEMBER unbindStdout WRITE setUnbindStdout);
|
||||
Q_GADGET;
|
||||
QML_STRUCTURED_VALUE;
|
||||
QML_VALUE_TYPE(processContext);
|
||||
|
||||
public:
|
||||
ProcessContext() = default;
|
||||
// Making this a Q_INVOKABLE does not work with QML_STRUCTURED_VALUe in Qt 6.9.
|
||||
explicit ProcessContext(QList<QString> command): command(std::move(command)), commandSet(true) {}
|
||||
|
||||
void setCommand(QList<QString> command) {
|
||||
this->command = std::move(command);
|
||||
this->commandSet = true;
|
||||
}
|
||||
|
||||
void setEnvironment(QHash<QString, QVariant> environment) {
|
||||
this->environment = std::move(environment);
|
||||
this->environmentSet = true;
|
||||
}
|
||||
|
||||
void setClearEnvironment(bool clearEnvironment) {
|
||||
this->clearEnvironment = clearEnvironment;
|
||||
this->clearEnvironmentSet = true;
|
||||
}
|
||||
|
||||
void setWorkingDirectory(QString workingDirectory) {
|
||||
this->workingDirectory = std::move(workingDirectory);
|
||||
this->workingDirectorySet = true;
|
||||
}
|
||||
|
||||
void setUnbindStdout(bool unbindStdout) { this->unbindStdout = unbindStdout; }
|
||||
|
||||
QList<QString> command;
|
||||
QHash<QString, QVariant> environment;
|
||||
bool clearEnvironment = false;
|
||||
QString workingDirectory;
|
||||
|
||||
bool commandSet : 1 = false;
|
||||
bool environmentSet : 1 = false;
|
||||
bool clearEnvironmentSet : 1 = false;
|
||||
bool workingDirectorySet : 1 = false;
|
||||
bool unbindStdout : 1 = true;
|
||||
};
|
||||
|
||||
void setupProcessEnvironment(
|
||||
QProcess* process,
|
||||
bool clear,
|
||||
const QHash<QString, QVariant>& envChanges
|
||||
);
|
||||
|
||||
} // namespace qs::io::process
|
||||
+22
-8
@@ -11,9 +11,10 @@
|
||||
#include <qqmlengine.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
#include "../core/logcat.hpp"
|
||||
#include "datastream.hpp"
|
||||
|
||||
Q_LOGGING_CATEGORY(logSocket, "quickshell.io.socket", QtWarningMsg);
|
||||
QS_LOGGING_CATEGORY(logSocket, "quickshell.io.socket", QtWarningMsg);
|
||||
|
||||
void Socket::setSocket(QLocalSocket* socket) {
|
||||
if (this->socket != nullptr) this->socket->deleteLater();
|
||||
@@ -107,7 +108,11 @@ void Socket::flush() {
|
||||
|
||||
SocketServer::~SocketServer() { this->disableServer(); }
|
||||
|
||||
void SocketServer::onPostReload() {
|
||||
void SocketServer::onReload(QObject* oldInstance) {
|
||||
if (auto* old = qobject_cast<SocketServer*>(oldInstance)) {
|
||||
old->disableServer();
|
||||
}
|
||||
|
||||
this->postReload = true;
|
||||
if (this->isActivatable()) this->enableServer();
|
||||
}
|
||||
@@ -152,6 +157,8 @@ bool SocketServer::isActivatable() {
|
||||
void SocketServer::enableServer() {
|
||||
this->disableServer();
|
||||
|
||||
qCDebug(logSocket) << "Enabling socket server" << this << "at" << this->mPath;
|
||||
|
||||
this->server = new QLocalServer(this);
|
||||
QObject::connect(
|
||||
this->server,
|
||||
@@ -160,31 +167,38 @@ void SocketServer::enableServer() {
|
||||
&SocketServer::onNewConnection
|
||||
);
|
||||
|
||||
if (QFile::remove(this->mPath)) {
|
||||
qCWarning(logSocket) << "Deleted existing file at" << this->mPath << "to create socket";
|
||||
}
|
||||
|
||||
if (!this->server->listen(this->mPath)) {
|
||||
qWarning() << "could not start socket server at" << this->mPath;
|
||||
qCWarning(logSocket) << "Could not start socket server at" << this->mPath;
|
||||
this->disableServer();
|
||||
}
|
||||
|
||||
this->activeTarget = false;
|
||||
this->activePath = this->mPath;
|
||||
emit this->activeStatusChanged();
|
||||
}
|
||||
|
||||
void SocketServer::disableServer() {
|
||||
auto wasActive = this->server != nullptr;
|
||||
|
||||
if (this->server != nullptr) {
|
||||
if (wasActive) {
|
||||
qCDebug(logSocket) << "Disabling socket server" << this << "at" << this->activePath;
|
||||
for (auto* socket: this->mSockets) {
|
||||
socket->deleteLater();
|
||||
}
|
||||
|
||||
this->mSockets.clear();
|
||||
this->server->close();
|
||||
this->server->deleteLater();
|
||||
this->server = nullptr;
|
||||
}
|
||||
|
||||
if (this->mPath != nullptr) {
|
||||
if (QFile::exists(this->mPath) && !QFile::remove(this->mPath)) {
|
||||
qWarning() << "failed to delete socket file at" << this->mPath;
|
||||
if (!this->activePath.isEmpty()) {
|
||||
if (QFile::exists(this->activePath) && !QFile::remove(this->activePath)) {
|
||||
qWarning() << "Failed to delete socket file at" << this->activePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-6
@@ -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;
|
||||
};
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -61,6 +61,7 @@ struct CommandState {
|
||||
QStringOption id;
|
||||
pid_t pid = -1; // NOLINT (include)
|
||||
bool all = false;
|
||||
bool includeDead = false;
|
||||
} instance;
|
||||
|
||||
struct {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in new issue
Block a user