169 Commits
Author SHA1 Message Date
outfoxxed da4ea5ff95 crash: switch to cpptrace from breakpad 2026-03-02 09:58:26 -08:00
bbedward 36517a2c10 services/pipewire: manage default objs using normal qt properties
Fixes use after free bugs due to pointer mismatches in destructors.
Drops SimpleObjectHandle.
2026-02-23 23:17:42 -08:00
reakjra c3c3e2ca25 wayland/screencopy: pin XRGB alpha to 1 in vulkan mode
While EGL handles this internally, vulkan's alpha channel behavior is
undefined when rendering depending on the driver. Notably intel does
not treat it as 1.0.
2026-02-23 22:47:42 -08:00
bbedward 2cf57f43d5 core/proxywindow: expose updatesEnabled property 2026-02-22 22:38:22 -08:00
reakjra a99519c3ad wayland/screencopy: support dmabufs in vulkan mode 2026-02-22 22:11:19 -08:00
Bryan Paradis 158db16b93 wayland: check screen isPlaceholder and if wl_output is null
Fixes crashes on disconnected monitors
2026-02-22 19:27:26 -08:00
outfoxxed e7cd1e9982 core: add env and isEnvSet functions to pragma context 2026-02-21 21:11:45 -08:00
Andrei afbc5ffd4e services/pipewire: use node volume control when device missing
Some outputs which present a pipewire device object do not present
routes, instead expecting volume to be set on the associated pipewire node.
2026-02-21 20:39:03 -08:00
outfoxxed dacfa9de82 widgets/cliprect: use ShaderEffectSource to propagate mouse events 2026-02-09 19:14:36 -08:00
outfoxxed 4429c03837 widgets/cliprect: fix ShaderEffect warnings on reload
layer.effect causes warnings on reload for an unknown reason which
seems to be ownership or destruction time related. This commit uses
an alternate strategy to create the shader which does not show this
warning.
2026-02-08 20:10:11 -08:00
kossLAN 395a1301a8 core: add hasThemeIcon mapping 2026-02-08 01:26:08 -08:00
outfoxxed 1e4d804e7f widgets/cliprect: use layer.effect on content item over property
ShaderEffectSource as a property not parented to an item does not
update its sourceItem's QQuickWindow when its own is changed. This
lead to use after frees and broken effects when using ClippingRectangle.
2026-01-28 01:43:31 -08:00
Manuel Romei 191085a882 ipc: use deleteLater() in IpcServerConnection to prevent use-after-free 2026-01-21 00:50:39 -08:00
bbedward 8fd0de4580 core/proxywindow: create window on visibility for lazily initialized windows 2026-01-20 16:10:45 -05:00
bbedward 7a427ce197 core: fix inverted inHeader condition in preprocesso 2026-01-17 17:30:40 -05:00
outfoxxed 5eb6f51f4a core: add preprocessor for versioning 2026-01-17 03:14:30 -08:00
outfoxxed d03c59768c io/ipchandler: add signal listener support 2026-01-15 23:40:03 -08:00
outfoxxed 783b97152a build: update CI, nix checkouts, lints 2026-01-13 23:20:55 -08:00
outfoxxed dca652366a core/popupwindow: add grabFocus
Allows standard wayland focus grabs on non hyprland compositors.
2026-01-13 01:25:38 -08:00
outfoxxed de1bfe028d core/popupwindow: clean up popup lifecycle and window init
- Makes popup lifecycle less complex
- Creates all QWindows lazily
- May break live reloading of open popups to some degree
2026-01-13 01:21:08 -08:00
Carson Powers db37dc580a networking: add networking library 2026-01-11 23:51:29 -08:00
outfoxxed bcc3d4265e core: switch to custom incubation controller
This change requires more QtPrivate usage but eliminates generation or
cleanup related window incubation controller bugs. Additionally it
enables async loads prior to rendering windows.
2026-01-10 13:22:50 -08:00
outfoxxed eecc2f88b3 services/pipewire: ignore monitors in PwNodeLinkTracker 2026-01-09 01:09:25 -08:00
outfoxxed 11d6d67961 services/pipewire: add peak detection 2026-01-09 01:09:24 -08:00
outfoxxed 5d8354a88b services/pipewire: add reconnect support 2026-01-08 04:11:09 -08:00
outfoxxed 8d19beb69e core/log: copy early logs with sendfile/readwrite again
copy_file_range does not work across devices and memfds count as a
separate device.
2026-01-08 02:35:08 -08:00
molyuu 6742148cf4 all: initial support for freebsd
- Use `copy_file_range(2)` over `sendfile(2)` which has wider
compatibility.
- Special case pam on freebsd and document `configDirectory`
incompatibility.
- Disable jemalloc for FreeBSD by default as it is the system allocator.
- Disable breakpad by default on FreeBSD as breakpad is not supported.
2026-01-06 01:50:58 -08:00
outfoxxed 341a07d05b io/process: use QVariantHash over QHash in Q_PROPERTY
Fixes qmlls for these properties
2026-01-06 01:05:57 -08:00
outfoxxed 41828c4180 services/pipewire: use node volume controls when routeDevice missing
For bluez audio streams and potentially other types of synthetic
device, volume controls are managed via the node and persistence is
handled by the service.
2025-12-25 20:58:05 -08:00
bbedward 3918290c1b core/window: add min/max/fullscreen properties, and move/resize fns to FloatingWindow 2025-12-21 23:01:36 -08:00
Tobias Pisani 26531fc46e service/tray: emit change signals for item title and description 2025-12-02 00:22:52 -08:00
Alejandro Pinar Ruiz 667bd38489 nix: update version to 0.2.1 2025-12-02 00:13:23 -08:00
EvilLary 9cdda21974 core/command: reset color after compatibility warning msg 2025-12-01 15:10:59 +03:00
nemalex d24e8e9736 hyprland/ipc: swap windowTitle and windowClass in openwindow handler
The openwindow event format is ADDRESS,WORKSPACE,CLASS,TITLE but the
handler was parsing args.at(2) as title and args.at(3) as class,
which is reversed.

This caused windows to display their class name instead of their
actual title when the openwindow event arrived after windowtitlev2,
since updateInitial would overwrite the correct title with the class.
2025-11-29 22:06:37 -08:00
outfoxxed e9bad67619 hyprland/ipc: fix activeToplevel not resetting after closewindow 2025-11-24 20:39:43 -08:00
bbedward ed036d514b wayland/shortcuts-inhibit: add shortcuts inhibitor 2025-11-20 01:12:14 -08:00
cameron 1ddb355121 core/icon: add searching custom file paths 2025-11-17 22:16:14 -08:00
Ala Alkhafaji ab494dd982 i3/ipc: implement IPC listener to receive arbitrary events 2025-11-17 22:04:58 -08:00
outfoxxed fdbb86a06a core/model: fix recursion in emptyInstance 2025-11-15 17:41:54 -08:00
outfoxxed 0a7dcf30ea build: update clang tooling and reformat 2025-11-15 04:43:27 -08:00
outfoxxed 1552aca3df build: fix new clang-tidy lints 2025-11-15 04:29:12 -08:00
outfoxxed 0a36e3ed40 ci: add qt6.10.0 checkout 2025-11-15 02:31:58 -08:00
outfoxxed a00ff03944 services/pipewire: cache route device volumes to initialize nodes
Nodes referencing a device can be bound later than the device is
bound. If this happens, the node will not receive an initial route
device volume change event. This change caches the last known route
device volume and initializes the device with it if present.
2025-11-14 02:12:42 -08:00
outfoxxed fc704e6b5d core: reference scanned paths by QDir over QString
Fixes a bug introduced in 3e2ce40 where a directory imported with a
"../name" path import would be passed to scanDir as ending in '/' which
created an invalid duplicate scan entry.
2025-10-31 00:56:30 -07:00
Cu3PO42 db1777c20b service/polkit: add service module to write Polkit agents 2025-10-30 03:53:01 -07:00
bbedward 1b147a2c78 core/desktopentry: handle string escape sequences 2025-10-28 02:20:28 -07:00
outfoxxed 3e2ce40b18 core: reference configs by absolute instead of canonical paths 2025-10-18 14:22:26 -07:00
outfoxxed 00858812f2 core/command: filter instance selection by current display 2025-10-12 17:33:21 -07:00
outfoxxed 1e8cc2e78d core: add CacheDir pragma
Closes #293
2025-10-12 00:14:36 -07:00
outfoxxed ea79eaceb0 services/pipewire: do not use device for pro audio node controls
Note that the device object is currently still bound. This should not
be necessary.

Fixes #178
2025-10-11 21:42:58 -07:00
outfoxxed c9d3ffb604 version: bump to 0.2.1 2025-10-11 17:16:19 -07:00
Gregor Kleen f12f0e7c7d service/greetd: always send responses 2025-10-11 01:03:05 -07:00
bbedward 3e32ae595f core/desktopentry: don't match keys with wrong modifier or country 2025-10-09 01:12:48 -07:00
bbedward 2eacb713b9 core/desktopentry: mask entries with priority less than hidden entry 2025-10-09 01:11:50 -07:00
outfoxxed c5c438f1cd all: fix gcc warnings and lints 2025-10-04 13:43:41 -07:00
outfoxxed 9bb2c043ae nix: remove qtwayland dependency when qt >= 6.10
QtWaylandClient was moved into QtBase in 6.10. The QtWayland packages
is now only the wayland server code which Quickshell does not need.
2025-10-04 13:00:35 -07:00
outfoxxed 3bcc1993f4 wayland/lock: support Qt 6.10 2025-10-04 13:00:33 -07:00
outfoxxed 9662234759 services/pipewire: consider device volume step when sending updates
Previously a hardcoded 0.0001 offset was used to determine if a volume
change was significant enough to send to a device, however some
devices have a much more granular step size, which caused future
volume updates to be blocked.

This change replaces the hardcoded offset with the volumeStep device
route property which should be large enough for the device to work with.

Fixes #279
2025-10-01 00:29:45 -07:00
outfoxxed 475856b767 docs: start tracking qs-next changelog 2025-09-30 23:28:05 -07:00
outfoxxed 482744cfa9 ci: fix magic-nix-cache write permissions 2025-09-29 21:59:46 -07:00
outfoxxed 6092b37c56 build: explicitly depend on private qt modules
In Qt 6.10, private Qt modules must be depended on explicitly.
2025-09-29 21:33:34 -07:00
outfoxxed 1d94144976 all: fix lints 2025-09-28 23:56:32 -07:00
outfoxxed f78078dfaf nix: update flake + tidyfox 2025-09-28 23:56:32 -07:00
outfoxxed eeb8181cb1 ci: add detsys nix cache 2025-09-28 23:56:32 -07:00
outfoxxed a922694a7d ci: use unwrapped package for dependencies derivation
Since adding the wrapper, CI built qs as it was a dependency of the
wrapper instead of dependencies of qs itself.
2025-09-28 23:56:32 -07:00
outfoxxed afada1eb6c ci: add qt 6.9.2 and 6.9.1 checkouts 2025-09-28 23:56:32 -07:00
outfoxxed b9905ef824 nix: add overlay 2025-09-28 23:56:30 -07:00
outfoxxed 2119eb2205 build: fix cross compilation 2025-09-28 18:55:45 -07:00
outfoxxed e9a574d919 core: derive incubation controllers from tracked windows list
Replaces the attempts to track incubation controllers directly with a
list of all known windows, then pulls the first usable incubation
controller when an assignment is requested.

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

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

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

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

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

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

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

This fixes missing state changes not conveyed by the pw_node.

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

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
AlignArrayOfStructures: None
AlignAfterOpenBracket: BlockIndent
AllowShortBlocksOnASingleLine: Always
AllowShortBlocksOnASingleLine: Empty
AllowShortCaseLabelsOnASingleLine: true
AllowShortEnumsOnASingleLine: true
AllowShortFunctionsOnASingleLine: All
+3
View File
@@ -20,6 +20,7 @@ Checks: >
-cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-pro-type-reinterpret-cast,
-cppcoreguidelines-pro-type-vararg,
-cppcoreguidelines-use-enum-class,
google-global-names-in-headers,
google-readability-casting,
google-runtime-int,
@@ -63,6 +64,8 @@ CheckOptions:
readability-identifier-naming.ParameterCase: camelBack
readability-identifier-naming.VariableCase: camelBack
misc-const-correctness.WarnPointersAsPointers: false
# does not appear to work
readability-operators-representation.BinaryOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^='
readability-operators-representation.OverloadedOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^='
+1 -1
View File
@@ -1,4 +1,4 @@
name: Crash Report
name: Crash Report (v1)
description: Quickshell has crashed
labels: ["bug", "crash"]
body:
+49
View File
@@ -0,0 +1,49 @@
name: Crash Report (v2)
description: Quickshell has crashed
labels: ["bug", "crash"]
body:
- type: textarea
id: userinfo
attributes:
label: What caused the crash
description: |
Any information likely to help debug the crash. What were you doing when the crash occurred,
what changes did you make, can you get it to happen again?
- type: textarea
id: report
attributes:
label: Report file
description: Attach `report.txt` here.
validations:
required: true
- type: textarea
id: logs
attributes:
label: Log file
description: |
Attach `log.qslog.log` here. If it is too big to upload, compress it.
You can preview the log if you'd like using `quickshell read-log <path-to-log>`.
validations:
required: true
- type: textarea
id: config
attributes:
label: Configuration
description: |
Attach your configuration here, preferrably in full (not just one file).
Compress it into a zip, tar, etc.
This will help us reproduce the crash ourselves.
- type: textarea
id: bt
attributes:
label: Backtrace
description: |
GDB usually produces better stacktraces than quickshell can. Consider attaching a gdb backtrace
following the instructions below.
1. Run `coredumpctl debug <pid>` where `pid` is the number shown after "Crashed process ID"
in the crash reporter.
2. Once it loads, type `bt -full` (then enter)
3. Copy the output and attach it as a file or in a spoiler.
+14 -5
View File
@@ -6,17 +6,23 @@ jobs:
name: Nix
strategy:
matrix:
qtver: [qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0]
qtver: [qt6.10.1, qt6.10.0, qt6.9.2, qt6.9.1, qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0]
compiler: [clang, gcc]
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
# Use cachix action over detsys for testing with act.
# - uses: cachix/install-nix-action@v27
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
with:
use-flakehub: false
- name: Download Dependencies
run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).inputDerivation'
run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).unwrapped.inputDerivation'
- name: Build
run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }'
@@ -44,13 +50,16 @@ jobs:
wayland-protocols \
wayland \
libdrm \
vulkan-headers \
libxcb \
libpipewire \
cli11 \
jemalloc
polkit \
jemalloc \
libunwind \
git # for cpptrace clone
- name: Build
# breakpad is annoying to build in ci due to makepkg not running as root
run: |
cmake -GNinja -B build -DCRASH_REPORTER=OFF
cmake -GNinja -B build -DVENDOR_CPPTRACE=ON
cmake --build build
+6
View File
@@ -5,11 +5,17 @@ jobs:
lint:
name: Lint
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
# Use cachix action over detsys for testing with act.
# - uses: cachix/install-nix-action@v27
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
with:
use-flakehub: false
- uses: nicknovitski/nix-develop@v1
- name: Check formatting
+32 -13
View File
@@ -41,16 +41,21 @@ Quickshell has a set of base dependencies you will always need, names vary by di
- `cmake`
- `qt6base`
- `qt6declarative`
- `qtshadertools` (build-time only)
- `spirv-tools` (build-time only)
- `pkg-config` (build-time only)
- `cli11`
- `qtshadertools` (build-time)
- `spirv-tools` (build-time)
- `pkg-config` (build-time)
- `cli11` (static library)
Build time dependencies and static libraries don't have to exist at runtime,
however build time dependencies must be compiled for the architecture of
the builder, while static libraries must be compiled for the architecture
of the target.
On some distros, private Qt headers are in separate packages which you may have to install.
We currently require private headers for the following libraries:
- `qt6declarative`
- `qt6wayland`
- `qt6wayland` (for Qt versions prior to 6.10)
We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and
svg icons will not work, including system ones.
@@ -59,14 +64,17 @@ At least Qt 6.6 is required.
All features are enabled by default and some have their own dependencies.
### Crash Reporter
The crash reporter catches crashes, restarts quickshell when it crashes,
### Crash Handler
The crash reporter catches crashes, restarts Quickshell when it crashes,
and collects useful crash information in one place. Leaving this enabled will
enable us to fix bugs far more easily.
To disable: `-DCRASH_REPORTER=OFF`
To disable: `-DCRASH_HANDLER=OFF`
Dependencies: `google-breakpad`
Dependencies: `cpptrace`
Note: `-DVENDOR_CPPTRACE=ON` can be set to vendor cpptrace using FetchContent.
This still requires `libunwind` to build.
### Jemalloc
We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused
@@ -99,10 +107,13 @@ Currently supported Qt versions: `6.6`, `6.7`.
To disable: `-DWAYLAND=OFF`
Dependencies:
- `qt6wayland`
- `qt6wayland` (for Qt versions prior to 6.10)
- `wayland` (libwayland-client)
- `wayland-scanner` (may be part of your distro's wayland package)
- `wayland-protocols`
- `wayland-scanner` (build time)
- `wayland-protocols` (static library)
Note that one or both of `wayland-scanner` and `wayland-protocols` may be bundled
with you distro's wayland package.
#### Wlroots Layershell
Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol,
@@ -138,6 +149,7 @@ To disable: `-DSCREENCOPY=OFF`
Dependencies:
- `libdrm`
- `libgbm`
- `vulkan-headers` (build-time)
Specific protocols can also be disabled:
- `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1]
@@ -184,6 +196,13 @@ To disable: `-DSERVICE_PAM=OFF`
Dependencies: `pam`
### Polkit
This feature enables creating Polkit agents that can prompt user for authentication.
To disable: `-DSERVICE_POLKIT=OFF`
Dependencies: `polkit`, `glib`
### Hyprland
This feature enables hyprland specific integrations. It requires wayland support
but has no extra dependencies.
@@ -220,7 +239,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
+33 -4
View File
@@ -1,5 +1,7 @@
cmake_minimum_required(VERSION 3.20)
project(quickshell VERSION "0.1.0" LANGUAGES CXX C)
project(quickshell VERSION "0.2.1" LANGUAGES CXX C)
set(UNRELEASED_FEATURES)
set(QT_MIN_VERSION "6.6.0")
set(CMAKE_CXX_STANDARD 20)
@@ -44,8 +46,12 @@ boption(BUILD_TESTING "Build tests (dev)" OFF)
boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang
boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN})
boption(CRASH_REPORTER "Crash Handling" ON)
boption(USE_JEMALLOC "Use jemalloc" ON)
if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
boption(USE_JEMALLOC "Use jemalloc" OFF)
else()
boption(USE_JEMALLOC "Use jemalloc" ON)
endif()
boption(CRASH_HANDLER "Crash Handling" ON)
boption(SOCKETS "Unix Sockets" ON)
boption(WAYLAND "Wayland" ON)
boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND)
@@ -67,9 +73,12 @@ boption(SERVICE_STATUS_NOTIFIER "System Tray" ON)
boption(SERVICE_PIPEWIRE "PipeWire" ON)
boption(SERVICE_MPRIS "Mpris" ON)
boption(SERVICE_PAM "Pam" ON)
boption(SERVICE_POLKIT "Polkit" ON)
boption(SERVICE_GREETD "Greetd" ON)
boption(SERVICE_UPOWER "UPower" ON)
boption(SERVICE_NOTIFICATIONS "Notifications" ON)
boption(BLUETOOTH "Bluetooth" ON)
boption(NETWORK "Network" ON)
include(cmake/install-qml-module.cmake)
include(cmake/util.cmake)
@@ -99,6 +108,7 @@ if (NOT CMAKE_BUILD_TYPE)
endif()
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools)
set(QT_PRIVDEPS QuickPrivate)
include(cmake/pch.cmake)
@@ -114,9 +124,10 @@ endif()
if (WAYLAND)
list(APPEND QT_FPDEPS WaylandClient)
list(APPEND QT_PRIVDEPS WaylandClientPrivate)
endif()
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS)
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH OR NETWORK)
set(DBUS ON)
endif()
@@ -126,6 +137,13 @@ endif()
find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS})
# In Qt 6.10, private dependencies are required to be explicit,
# but they could not be explicitly depended on prior to 6.9.
if (Qt6_VERSION VERSION_GREATER_EQUAL "6.9.0")
set(QT_NO_PRIVATE_MODULE_WARNING ON)
find_package(Qt6 REQUIRED COMPONENTS ${QT_PRIVDEPS})
endif()
set(CMAKE_AUTOUIC OFF)
qt_standard_project_setup(REQUIRES 6.6)
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules)
@@ -145,3 +163,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
)
+3
View File
@@ -12,6 +12,9 @@ lint-ci:
lint-changed:
git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
lint-staged:
git diff --staged --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
configure target='debug' *FLAGS='':
cmake -GNinja -B {{builddir}} \
-DCMAKE_BUILD_TYPE={{ if target == "debug" { "Debug" } else { "RelWithDebInfo" } }} \
+4
View File
@@ -2,6 +2,10 @@
See the [website](https://quickshell.outfoxxed.me) for more information
and installation instructions.
This repo is hosted at:
- https://git.outfoxxed.me/quickshell/quickshell
- https://github.com/quickshell-mirror/quickshell
# Contributing / Development
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
+7
View File
@@ -0,0 +1,7 @@
[Desktop Entry]
Version=1.5
Type=Application
NoDisplay=true
Name=Quickshell
Icon=org.quickshell
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="724.635" height="724.635"><path fill="#359757" stroke="#359757" stroke-linecap="square" stroke-linejoin="round" stroke-width="74.755" d="m37.378 160.237 122.859-122.86h527.02v527.02l-122.86 122.86H37.378Z"/><path fill="#fff" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="12.201" d="M323.051 96.412a268.74 268.74 0 0 0-3.51.542c-4.052 14.481-7.815 29.941-14.904 42.692a230.02 230.02 0 0 0-59.406 24.679c-14.036-3.974-27.647-12.214-40.766-19.562a268.788 268.788 0 0 0-60.035 60.16c7.376 13.105 15.645 26.698 19.648 40.726a230.02 230.02 0 0 0-24.554 59.458c-12.735 7.115-28.186 10.913-42.66 14.994a268.789 268.789 0 0 0 .09 84.992c14.48 4.05 29.941 7.814 42.691 14.903a230.02 230.02 0 0 0 24.68 59.406c-3.974 14.037-12.215 27.647-19.563 40.766a268.788 268.788 0 0 0 60.161 60.035c13.104-7.376 26.696-15.645 40.725-19.648a230.02 230.02 0 0 0 59.457 24.555c7.116 12.735 10.913 28.186 14.995 42.659a268.788 268.788 0 0 0 84.99-.09c4.052-14.482 7.817-29.941 14.906-42.691a230.02 230.02 0 0 0 59.405-24.68c14.037 3.974 33.069 17.638 46.188 24.986a268.788 268.788 0 0 0 60.035-60.161c-7.376-13.105-21.068-32.12-25.071-46.149a230.02 230.02 0 0 0 24.555-59.457c12.735-7.116 28.186-10.912 42.659-14.993a268.788 268.788 0 0 0-.089-84.993c-14.482-4.051-29.942-7.814-42.692-14.904a230.02 230.02 0 0 0-24.68-59.405c3.974-14.037 12.216-27.647 19.565-40.767a268.788 268.788 0 0 0-60.161-60.035c-13.105 7.376-26.698 15.645-40.726 19.649a230.02 230.02 0 0 0-59.458-24.555c-7.115-12.735-10.913-28.187-14.994-42.66a268.788 268.788 0 0 0-81.481-.452zm15.778 106.85c58.282-8.328 116.455 15.865 151.643 63.065 35.19 47.2 41.766 109.86 17.144 163.337l-41.728-22.688s-38.558 31.44-57.344 63.012l23.893 36.326a160.78 160.78 0 0 1-46.633 15.058c-87.854 12.99-169.6-47.708-182.573-135.564-12.974-87.855 47.74-169.59 135.598-182.546Z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+58
View File
@@ -0,0 +1,58 @@
## Breaking Changes
### Config paths are no longer canonicalized
This fixes nix configs changing shell-ids on rebuild as the shell id is now derived from
the symlink path. Configs with a symlink in their path will have a different shell id.
Shell ids are used to derive the default config / state / cache folders, so those files
will need to be manually moved if using a config behind a symlinked path without an explicitly
set shell id.
## New Features
- Added support for creating Polkit agents.
- Added support for creating wayland idle inhibitors.
- Added support for wayland idle timeouts.
- Added support for inhibiting wayland compositor shortcuts for focused windows.
- Added the ability to override Quickshell.cacheDir with a custom path.
- Added minimized, maximized, and fullscreen properties to FloatingWindow.
- Added the ability to handle move and resize events to FloatingWindow.
- Pipewire service now reconnects if pipewire dies or a protocol error occurs.
- Added pipewire audio peak detection.
- Added initial support for network management.
- Added support for grabbing focus from popup windows.
- Added support for IPC signal listeners.
- Added Quickshell version checking and version gated preprocessing.
- Added a way to detect if an icon is from the system icon theme or not.
- Added vulkan support to screencopy.
## Other Changes
- FreeBSD is now partially supported.
- IPC operations filter available instances to the current display connection by default.
- PwNodeLinkTracker ignores sound level monitoring programs.
- Replaced breakpad with cpptrace.
## Bug Fixes
- Fixed volume control breaking with pipewire pro audio mode.
- Fixed volume control breaking with bluez streams and potentially others.
- Fixed volume control breaking for devices without route definitions.
- Fixed escape sequence handling in desktop entries.
- Fixed volumes not initializing if a pipewire device was already loaded before its node.
- Fixed hyprland active toplevel not resetting after window closes.
- Fixed hyprland ipc window names and titles being reversed.
- Fixed missing signals for system tray item title and description updates.
- Fixed asynchronous loaders not working after reload.
- Fixed asynchronous loaders not working before window creation.
- Fixed memory leak in IPC handlers.
- Fixed ClippingRectangle related crashes.
- Fixed crashes when monitors are unplugged.
- Fixed crashes when default pipewire devices are lost.
## Packaging Changes
- `glib` and `polkit` have been added as dependencies when compiling with polkit agent support.
- `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support).
- `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore.
+1
View File
@@ -0,0 +1 @@
Initial release
+84
View File
@@ -0,0 +1,84 @@
## Breaking Changes
- Files outside of the shell directory can no longer be referenced with relative paths, e.g. '../../foo.png'.
- PanelWindow's Automatic exclusion mode now adds an exclusion zone for panels with a single anchor.
- `QT_QUICK_CONTROLS_STYLE` and `QT_STYLE_OVERRIDE` are ignored unless `//@ pragma RespectSystemStyle` is set.
## New Features
### Root-Relative Imports
Quickshell 0.2 comes with a new method to import QML modules which is supported by QMLLS.
This replaces "root:/" imports for QML modules.
The new syntax is `import qs.path.to.module`, where `path/to/module` is the path to
a module/subdirectory relative to the config root (`qs`).
### Better LSP support
LSP support for Singletons and Root-Relative imports can be enabled by creating a file named
`.qmlls.ini` in the shell root directory. Quickshell will detect this file and automatically
populate it with an LSP configuration. This file should be gitignored in your configuration,
as it is system dependent.
The generated configuration also includes QML import paths available to Quickshell, meaning
QMLLS no longer requires the `-E` flag.
### Bluetooth Module
Quickshell can now manage your bluetooth devices through BlueZ. While authenticated pairing
has not landed in 0.2, support for connecting and disconnecting devices, basic device information,
and non-authenticated pairing are now supported.
### Other Features
- Added `HyprlandToplevel` and related toplevel/window management APIs in the Hyprland module.
- Added `Quickshell.execDetached()`, which spawns a detached process without a `Process` object.
- Added `Process.exec()` for easier reconfiguration of process commands when starting them.
- Added `FloatingWindow.title`, which allows changing the title of a floating window.
- Added `signal QsWindow.closed()`, fired when a window is closed externally.
- Added support for inline replies in notifications, when supported by applications.
- Added `DesktopEntry.startupWmClass` and `DesktopEntry.heuristicLookup()` to better identify toplevels.
- Added `DesktopEntry.command` which can be run as an alternative to `DesktopEntry.execute()`.
- Added `//@ pragma Internal`, which makes a QML component impossible to import outside of its module.
- Added dead instance selection for some subcommands, such as `qs log` and `qs list`.
## Other Changes
- `Quickshell.shellRoot` has been renamed to `Quickshell.shellDir`.
- PanelWindow margins opposite the window's anchorpoint are now added to exclusion zone.
- stdout/stderr or detached processes and executed desktop entries are now hidden by default.
- Various warnings caused by other applications Quickshell communicates with over D-BUS have been hidden in logs.
- Quickshell's new logo is now shown in any floating windows.
## Bug Fixes
- Fixed pipewire device volume and mute states not updating before the device has been used.
- Fixed a crash when changing the volume of any pipewire device on a sound card another removed device was using.
- Fixed a crash when accessing a removed previous default pipewire node from the default sink/source changed signals.
- Fixed session locks crashing if all monitors are disconnected.
- Fixed session locks crashing if unsupported by the compositor.
- Fixed a crash when creating a session lock and destroying it before acknowledged by the compositor.
- Fixed window input masks not updating after a reload.
- Fixed PanelWindows being unconfigurable unless `screen` was set under X11.
- Fixed a crash when anchoring a popup to a zero sized `Item`.
- Fixed `FileView` crashing if `watchChanges` was used.
- Fixed `SocketServer` sockets disappearing after a reload.
- Fixed `ScreencopyView` having incorrect rotation when displaying a rotated monitor.
- Fixed `MarginWrapperManager` breaking pixel alignment of child items when centering.
- Fixed `IpcHandler`, `NotificationServer` and `GlobalShortcut` not activating with certain QML structures.
- Fixed tracking of QML incubator destruction and deregistration, which occasionally caused crashes.
- Fixed FloatingWindows being constrained to the smallest window manager supported size unless max size was set.
- Fixed `MprisPlayer.lengthSupported` not updating reactively.
- Fixed normal tray icon being ignored when status is `NeedsAttention` and no attention icon is provided.
- Fixed `HyprlandWorkspace.activate()` sending invalid commands to Hyprland for named or special workspaces.
- Fixed file watcher occasionally breaking when using VSCode to edit QML files.
- Fixed crashes when screencopy buffer creation fails.
- Fixed a crash when wayland layer surfaces are recreated for the same window.
- Fixed the `QsWindow` attached object not working when using `WlrLayershell` directly.
- Fixed a crash when attempting to create a window without available VRAM.
- Fixed OOM crash when failing to write to detailed log file.
- Prevented distro logging configurations for Qt from interfering with Quickshell commands.
- Removed the "QProcess destroyed for running process" warning when destroying `Process` objects.
- Fixed `ColorQuantizer` printing a pointer to an error message instead of an error message.
- Fixed notification pixmap rowstride warning showing for correct rowstrides.
+17
View File
@@ -0,0 +1,17 @@
## New Features
- Changes to desktop entries are now tracked in real time.
## Other Changes
- Added support for Qt 6.10
## Bug Fixes
- Fixed volumes getting stuck on change for pipewire devices with few volume steps.
- Fixed a crash when running out of disk space to write log files.
- Fixed a rare crash when disconnecting a monitor.
- Fixed build issues preventing cross compilation from working.
- Fixed dekstop entries with lower priority than a hidden entry not being hidden.
- Fixed desktop entry keys with mismatched modifier or country not being discarded.
- Fixed greetd hanging when authenticating with a fingerprint.
+5 -2
View File
@@ -2,7 +2,10 @@
qtver,
compiler,
}: let
nixpkgs = (import ./nix-checkouts.nix).${builtins.replaceStrings ["."] ["_"] qtver};
checkouts = import ./nix-checkouts.nix;
nixpkgs = checkouts.${builtins.replaceStrings ["."] ["_"] qtver};
compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler};
pkg = (nixpkgs.callPackage ../default.nix {}).override compilerOverride;
pkg = (nixpkgs.callPackage ../default.nix {}).override (compilerOverride // {
wayland-protocols = checkouts.latest.wayland-protocols;
});
in pkg
+22 -3
View File
@@ -7,9 +7,28 @@ let
url = "https://github.com/nixos/nixpkgs/archive/${commit}.tar.gz";
inherit sha256;
}) {};
in {
# For old qt versions, grab the commit before the version bump that has all the patches
# instead of the bumped version.
in rec {
latest = qt6_10_0;
qt6_10_1 = byCommit {
commit = "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38";
sha256 = "0fvbizl7j5rv2rf8j76yw0xb3d9l06hahkjys2a7k1yraznvnafm";
};
qt6_10_0 = byCommit {
commit = "c5ae371f1a6a7fd27823bc500d9390b38c05fa55";
sha256 = "18g0f8cb9m8mxnz9cf48sks0hib79b282iajl2nysyszph993yp0";
};
qt6_9_2 = byCommit {
commit = "e9f00bd893984bc8ce46c895c3bf7cac95331127";
sha256 = "0s2mhbrgzxlgkg2yxb0q0hpk8lby1a7w67dxvfmaz4gsmc0bnvfj";
};
qt6_9_1 = byCommit {
commit = "4c202d26483c5ccf3cb95e0053163facde9f047e";
sha256 = "06l2w4bcgfw7dfanpzpjcf25ydf84in240yplqsss82qx405y9di";
};
qt6_9_0 = byCommit {
commit = "546c545bd0594809a28ab7e869b5f80dd7243ef6";
+2 -2
View File
@@ -2,6 +2,6 @@
clangStdenv,
gccStdenv,
}: {
clang = { buildStdenv = clangStdenv; };
gcc = { buildStdenv = gccStdenv; };
clang = { stdenv = clangStdenv; };
gcc = { stdenv = gccStdenv; };
}
+112 -59
View File
@@ -2,23 +2,30 @@
lib,
nix-gitignore,
pkgs,
stdenv,
keepDebugInfo,
buildStdenv ? pkgs.clangStdenv,
pkg-config,
cmake,
ninja,
qt6,
spirv-tools,
cli11,
breakpad,
qt6,
cpptrace ? null,
libunwind,
libdwarf,
jemalloc,
cli11,
wayland,
wayland-protocols,
wayland-scanner,
xorg,
libdrm,
libgbm ? null,
xorg,
vulkan-headers,
pipewire,
pam,
polkit,
glib,
gitRev ? (let
headExists = builtins.pathExists ./.git/HEAD;
@@ -41,64 +48,110 @@
withPam ? true,
withHyprland ? true,
withI3 ? true,
}: buildStdenv.mkDerivation {
pname = "quickshell${lib.optionalString debug "-debug"}";
version = "0.1.0";
src = nix-gitignore.gitignoreSource "/docs\n/examples\n" ./.;
withPolkit ? true,
withNetworkManager ? true,
}: let
withCrashHandler = withCrashReporter && cpptrace != null && lib.strings.compareVersions cpptrace.version "0.7.2" >= 0;
nativeBuildInputs = with pkgs; [
cmake
ninja
qt6.qtshadertools
spirv-tools
qt6.wrapQtAppsHook
pkg-config
] ++ (lib.optionals withWayland [
wayland-protocols
wayland-scanner
]);
unwrapped = stdenv.mkDerivation {
pname = "quickshell${lib.optionalString debug "-debug"}";
version = "0.2.1";
src = nix-gitignore.gitignoreSource "/default.nix\n" ./.;
buildInputs = [
qt6.qtbase
qt6.qtdeclarative
cli11
]
++ lib.optional withCrashReporter breakpad
++ lib.optional withJemalloc jemalloc
++ lib.optional withQtSvg qt6.qtsvg
++ lib.optionals withWayland ([ qt6.qtwayland wayland ] ++ (if libgbm != null then [ libdrm libgbm ] else []))
++ lib.optional withX11 xorg.libxcb
++ lib.optional withPam pam
++ lib.optional withPipewire pipewire;
dontWrapQtApps = true; # see wrappers
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
nativeBuildInputs = [
cmake
ninja
spirv-tools
pkg-config
]
++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
++ lib.optionals withWayland [
qt6.qtwayland # qtwaylandscanner required at build time
wayland-scanner
];
cmakeFlags = [
(lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake")
(lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
(lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true)
(lib.cmakeFeature "GIT_REVISION" gitRev)
(lib.cmakeBool "CRASH_REPORTER" withCrashReporter)
(lib.cmakeBool "USE_JEMALLOC" withJemalloc)
(lib.cmakeBool "WAYLAND" withWayland)
(lib.cmakeBool "SCREENCOPY" (libgbm != null))
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
(lib.cmakeBool "SERVICE_PAM" withPam)
(lib.cmakeBool "HYPRLAND" withHyprland)
(lib.cmakeBool "I3" withI3)
];
buildInputs = [
qt6.qtbase
qt6.qtdeclarative
cli11
]
++ lib.optional withQtSvg qt6.qtsvg
++ lib.optionals withCrashHandler [
(cpptrace.overrideAttrs (prev: {
cmakeFlags = prev.cmakeFlags ++ [
"-DCPPTRACE_UNWIND_WITH_LIBUNWIND=TRUE"
];
buildInputs = prev.buildInputs ++ [ libunwind ];
}))
libunwind
libdwarf
]
++ lib.optional withJemalloc jemalloc
++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
++ lib.optionals withWayland [ wayland wayland-protocols ]
++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm vulkan-headers ]
++ lib.optional withX11 xorg.libxcb
++ lib.optional withPam pam
++ lib.optional withPipewire pipewire
++ lib.optionals withPolkit [ polkit glib ];
# How to get debuginfo in gdb from a release build:
# 1. build `quickshell.debug`
# 2. set NIX_DEBUG_INFO_DIRS="<quickshell.debug store path>/lib/debug"
# 3. launch gdb / coredumpctl and debuginfo will work
separateDebugInfo = !debug;
dontStrip = debug;
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
meta = with lib; {
homepage = "https://git.outfoxxed.me/outfoxxed/quickshell";
description = "Flexbile QtQuick based desktop shell toolkit";
license = licenses.lgpl3Only;
platforms = platforms.linux;
cmakeFlags = [
(lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake")
(lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
(lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true)
(lib.cmakeFeature "GIT_REVISION" gitRev)
(lib.cmakeBool "CRASH_HANDLER" withCrashHandler)
(lib.cmakeBool "USE_JEMALLOC" withJemalloc)
(lib.cmakeBool "WAYLAND" withWayland)
(lib.cmakeBool "SCREENCOPY" (libgbm != null))
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
(lib.cmakeBool "SERVICE_PAM" withPam)
(lib.cmakeBool "SERVICE_NETWORKMANAGER" withNetworkManager)
(lib.cmakeBool "SERVICE_POLKIT" withPolkit)
(lib.cmakeBool "HYPRLAND" withHyprland)
(lib.cmakeBool "I3" withI3)
];
# How to get debuginfo in gdb from a release build:
# 1. build `quickshell.debug`
# 2. set NIX_DEBUG_INFO_DIRS="<quickshell.debug store path>/lib/debug"
# 3. launch gdb / coredumpctl and debuginfo will work
separateDebugInfo = !debug;
dontStrip = debug;
meta = with lib; {
homepage = "https://quickshell.org";
description = "Flexbile QtQuick based desktop shell toolkit";
license = licenses.lgpl3Only;
platforms = platforms.linux;
mainProgram = "quickshell";
};
};
}
wrapper = unwrapped.stdenv.mkDerivation {
inherit (unwrapped) version meta buildInputs;
pname = "${unwrapped.pname}-wrapped";
nativeBuildInputs = unwrapped.nativeBuildInputs ++ [ qt6.wrapQtAppsHook ];
dontUnpack = true;
dontConfigure = true;
dontBuild = true;
installPhase = ''
mkdir -p $out
cp -r ${unwrapped}/* $out
'';
passthru = {
unwrapped = unwrapped;
withModules = modules: wrapper.overrideAttrs (prev: {
buildInputs = prev.buildInputs ++ modules;
});
};
};
in wrapper
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1736012469,
"narHash": "sha256-/qlNWm/IEVVH7GfgAIyP6EsVZI6zjAx1cV5zNyrs+rI=",
"lastModified": 1768127708,
"narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8f3e1f807051e32d8c95cd12b9b421623850a34d",
"rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38",
"type": "github"
},
"original": {
+15 -9
View File
@@ -4,22 +4,28 @@
};
outputs = { self, nixpkgs }: let
forEachSystem = fn: nixpkgs.lib.genAttrs
[ "x86_64-linux" "aarch64-linux" ]
(system: fn system nixpkgs.legacyPackages.${system});
in {
packages = forEachSystem (system: pkgs: rec {
quickshell = pkgs.callPackage ./default.nix {
gitRev = self.rev or self.dirtyRev;
};
overlayPkgs = p: p.appendOverlays [ self.overlays.default ];
forEachSystem = fn:
nixpkgs.lib.genAttrs
nixpkgs.lib.platforms.linux
(system: fn system (overlayPkgs nixpkgs.legacyPackages.${system}));
in {
overlays.default = import ./overlay.nix {
rev = self.rev or self.dirtyRev;
};
packages = forEachSystem (system: pkgs: rec {
quickshell = pkgs.quickshell;
default = quickshell;
});
devShells = forEachSystem (system: pkgs: rec {
default = import ./shell.nix {
inherit pkgs;
inherit (self.packages.${system}) quickshell;
quickshell = self.packages.${system}.quickshell.override {
stdenv = pkgs.clangStdenv;
};
};
});
};
+5
View File
@@ -0,0 +1,5 @@
{ rev ? null }: (final: prev: {
quickshell = final.callPackage ./default.nix {
gitRev = rev;
};
})
+5 -5
View File
@@ -35,13 +35,14 @@
pkg-config
qtshadertools
spirv-tools
wayland-protocols))
(inputs (list cli11
jemalloc
wayland-protocols
cli11))
(inputs (list jemalloc
libdrm
libxcb
libxkbcommon
linux-pam
polkit
mesa
pipewire
qtbase
@@ -55,8 +56,7 @@
#~(list "-GNinja"
"-DDISTRIBUTOR=\"In-tree Guix channel\""
"-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO"
;; Breakpad is not currently packaged for Guix.
"-DCRASH_REPORTER=OFF")
"-DCRASH_HANDLER=OFF")
#:phases
#~(modify-phases %standard-phases
(replace 'build (lambda _ (invoke "cmake" "--build" ".")))
+4 -3
View File
@@ -1,14 +1,15 @@
{
pkgs ? import <nixpkgs> {},
quickshell ? pkgs.callPackage ./default.nix {},
stdenv ? pkgs.clangStdenv, # faster compiles than gcc
quickshell ? pkgs.callPackage ./default.nix { inherit stdenv; },
...
}: let
tidyfox = import (pkgs.fetchFromGitea {
domain = "git.outfoxxed.me";
owner = "outfoxxed";
repo = "tidyfox";
rev = "1f062cc198d1112d13e5128fa1f2ee3dbffe613b";
sha256 = "kbt0Zc1qHE5fhqBkKz8iue+B+ZANjF1AR/RdgmX1r0I=";
rev = "9d85d7e7dea2602aa74ec3168955fee69967e92f";
hash = "sha256-77ERiweF6lumonp2c/124rAoVG6/o9J+Aajhttwtu0w=";
}) { inherit pkgs; };
in pkgs.mkShell.override { stdenv = quickshell.stdenv; } {
inputsFrom = [ quickshell ];
+9 -1
View File
@@ -12,7 +12,7 @@ add_subdirectory(io)
add_subdirectory(widgets)
add_subdirectory(ui)
if (CRASH_REPORTER)
if (CRASH_HANDLER)
add_subdirectory(crash)
endif()
@@ -29,3 +29,11 @@ if (X11)
endif()
add_subdirectory(services)
if (BLUETOOTH)
add_subdirectory(bluetooth)
endif()
if (NETWORK)
add_subdirectory(network)
endif()
+42
View File
@@ -0,0 +1,42 @@
set_source_files_properties(org.bluez.Adapter.xml PROPERTIES
CLASSNAME DBusBluezAdapterInterface
)
set_source_files_properties(org.bluez.Device.xml PROPERTIES
CLASSNAME DBusBluezDeviceInterface
)
qt_add_dbus_interface(DBUS_INTERFACES
org.bluez.Adapter.xml
dbus_adapter
)
qt_add_dbus_interface(DBUS_INTERFACES
org.bluez.Device.xml
dbus_device
)
qt_add_library(quickshell-bluetooth STATIC
adapter.cpp
bluez.cpp
device.cpp
${DBUS_INTERFACES}
)
qt_add_qml_module(quickshell-bluetooth
URI Quickshell.Bluetooth
VERSION 0.1
DEPENDENCIES QtQml
)
install_qml_module(quickshell-bluetooth)
# dbus headers
target_include_directories(quickshell-bluetooth PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(quickshell-bluetooth PRIVATE Qt::Qml Qt::DBus)
qs_add_link_dependencies(quickshell-bluetooth quickshell-dbus)
qs_module_pch(quickshell-bluetooth SET dbus)
target_link_libraries(quickshell PRIVATE quickshell-bluetoothplugin)
+223
View File
@@ -0,0 +1,223 @@
#include "adapter.hpp"
#include <qcontainerfwd.h>
#include <qdbusconnection.h>
#include <qdbusextratypes.h>
#include <qdbuspendingcall.h>
#include <qdbuspendingreply.h>
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qstring.h>
#include <qtypes.h>
#include "../core/logcat.hpp"
#include "../dbus/properties.hpp"
#include "dbus_adapter.h"
namespace qs::bluetooth {
namespace {
QS_LOGGING_CATEGORY(logAdapter, "quickshell.bluetooth.adapter", QtWarningMsg);
}
QString BluetoothAdapterState::toString(BluetoothAdapterState::Enum state) {
switch (state) {
case BluetoothAdapterState::Disabled: return QStringLiteral("Disabled");
case BluetoothAdapterState::Enabled: return QStringLiteral("Enabled");
case BluetoothAdapterState::Enabling: return QStringLiteral("Enabling");
case BluetoothAdapterState::Disabling: return QStringLiteral("Disabling");
case BluetoothAdapterState::Blocked: return QStringLiteral("Blocked");
default: return QStringLiteral("Unknown");
}
}
BluetoothAdapter::BluetoothAdapter(const QString& path, QObject* parent): QObject(parent) {
this->mInterface =
new DBusBluezAdapterInterface("org.bluez", path, QDBusConnection::systemBus(), this);
if (!this->mInterface->isValid()) {
qCWarning(logAdapter) << "Could not create DBus interface for adapter at" << path;
this->mInterface = nullptr;
return;
}
this->properties.setInterface(this->mInterface);
}
QString BluetoothAdapter::adapterId() const {
auto path = this->path();
return path.sliced(path.lastIndexOf('/') + 1);
}
void BluetoothAdapter::setEnabled(bool enabled) {
if (enabled == this->bEnabled) return;
if (enabled && this->bState == BluetoothAdapterState::Blocked) {
qCCritical(logAdapter) << "Cannot enable adapter because it is blocked by rfkill.";
return;
}
this->bEnabled = enabled;
this->pEnabled.write();
}
void BluetoothAdapter::setDiscoverable(bool discoverable) {
if (discoverable == this->bDiscoverable) return;
this->bDiscoverable = discoverable;
this->pDiscoverable.write();
}
void BluetoothAdapter::setDiscovering(bool discovering) {
if (discovering) {
this->startDiscovery();
} else {
this->stopDiscovery();
}
}
void BluetoothAdapter::setDiscoverableTimeout(quint32 timeout) {
if (timeout == this->bDiscoverableTimeout) return;
this->bDiscoverableTimeout = timeout;
this->pDiscoverableTimeout.write();
}
void BluetoothAdapter::setPairable(bool pairable) {
if (pairable == this->bPairable) return;
this->bPairable = pairable;
this->pPairable.write();
}
void BluetoothAdapter::setPairableTimeout(quint32 timeout) {
if (timeout == this->bPairableTimeout) return;
this->bPairableTimeout = timeout;
this->pPairableTimeout.write();
}
void BluetoothAdapter::addInterface(const QString& interface, const QVariantMap& properties) {
if (interface == "org.bluez.Adapter1") {
this->properties.updatePropertySet(properties, false);
qCDebug(logAdapter) << "Updated Adapter properties for" << this;
}
}
void BluetoothAdapter::removeDevice(const QString& devicePath) {
qCDebug(logAdapter) << "Removing device" << devicePath << "from adapter" << this;
auto reply = this->mInterface->RemoveDevice(QDBusObjectPath(devicePath));
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this, devicePath](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logAdapter).nospace()
<< "Failed to remove device " << devicePath << " from adapter" << this << ": "
<< reply.error().message();
} else {
qCDebug(logAdapter) << "Successfully removed device" << devicePath << "from adapter"
<< this;
}
delete watcher;
}
);
}
void BluetoothAdapter::startDiscovery() {
if (this->bDiscovering) return;
qCDebug(logAdapter) << "Starting discovery for adapter" << this;
auto reply = this->mInterface->StartDiscovery();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logAdapter).nospace()
<< "Failed to start discovery on adapter" << this << ": " << reply.error().message();
} else {
qCDebug(logAdapter) << "Successfully started discovery on adapter" << this;
}
delete watcher;
}
);
}
void BluetoothAdapter::stopDiscovery() {
if (!this->bDiscovering) return;
qCDebug(logAdapter) << "Stopping discovery for adapter" << this;
auto reply = this->mInterface->StopDiscovery();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logAdapter).nospace()
<< "Failed to stop discovery on adapter " << this << ": " << reply.error().message();
} else {
qCDebug(logAdapter) << "Successfully stopped discovery on adapter" << this;
}
delete watcher;
}
);
}
} // namespace qs::bluetooth
namespace qs::dbus {
using namespace qs::bluetooth;
DBusResult<BluetoothAdapterState::Enum>
DBusDataTransform<BluetoothAdapterState::Enum>::fromWire(const Wire& wire) {
if (wire == QStringLiteral("off")) {
return BluetoothAdapterState::Disabled;
} else if (wire == QStringLiteral("on")) {
return BluetoothAdapterState::Enabled;
} else if (wire == QStringLiteral("off-enabling")) {
return BluetoothAdapterState::Enabling;
} else if (wire == QStringLiteral("on-disabling")) {
return BluetoothAdapterState::Disabling;
} else if (wire == QStringLiteral("off-blocked")) {
return BluetoothAdapterState::Blocked;
} else {
return QDBusError(
QDBusError::InvalidArgs,
QString("Invalid BluetoothAdapterState: %1").arg(wire)
);
}
}
} // namespace qs::dbus
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter) {
auto saver = QDebugStateSaver(debug);
if (adapter) {
debug.nospace() << "BluetoothAdapter(" << static_cast<const void*>(adapter)
<< ", path=" << adapter->path() << ")";
} else {
debug << "BluetoothAdapter(nullptr)";
}
return debug;
}
+173
View File
@@ -0,0 +1,173 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include "../core/doc.hpp"
#include "../core/model.hpp"
#include "../dbus/properties.hpp"
#include "dbus_adapter.h"
namespace qs::bluetooth {
///! Power state of a Bluetooth adapter.
class BluetoothAdapterState: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum : quint8 {
/// The adapter is powered off.
Disabled = 0,
/// The adapter is powered on.
Enabled = 1,
/// The adapter is transitioning from off to on.
Enabling = 2,
/// The adapter is transitioning from on to off.
Disabling = 3,
/// The adapter is blocked by rfkill.
Blocked = 4,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(BluetoothAdapterState::Enum state);
};
} // namespace qs::bluetooth
namespace qs::dbus {
template <>
struct DBusDataTransform<qs::bluetooth::BluetoothAdapterState::Enum> {
using Wire = QString;
using Data = qs::bluetooth::BluetoothAdapterState::Enum;
static DBusResult<Data> fromWire(const Wire& wire);
};
} // namespace qs::dbus
namespace qs::bluetooth {
class BluetoothAdapter;
class BluetoothDevice;
///! A Bluetooth adapter
class BluetoothAdapter: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_UNCREATABLE("");
// clang-format off
/// System provided name of the adapter. See @@adapterId for the internal identifier.
Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName);
/// True if the adapter is currently enabled. More detailed state is available from @@state.
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
/// Detailed power state of the adapter.
Q_PROPERTY(BluetoothAdapterState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
/// True if the adapter can be discovered by other bluetooth devices.
Q_PROPERTY(bool discoverable READ discoverable WRITE setDiscoverable NOTIFY discoverableChanged);
/// Timeout in seconds for how long the adapter stays discoverable after @@discoverable is set to true.
/// A value of 0 means the adapter stays discoverable forever.
Q_PROPERTY(quint32 discoverableTimeout READ discoverableTimeout WRITE setDiscoverableTimeout NOTIFY discoverableTimeoutChanged);
/// True if the adapter is scanning for new devices.
Q_PROPERTY(bool discovering READ discovering WRITE setDiscovering NOTIFY discoveringChanged);
/// True if the adapter is accepting incoming pairing requests.
///
/// This only affects incoming pairing requests and should typically only be changed
/// by system settings applications. Defaults to true.
Q_PROPERTY(bool pairable READ pairable WRITE setPairable NOTIFY pairableChanged);
/// Timeout in seconds for how long the adapter stays pairable after @@pairable is set to true.
/// A value of 0 means the adapter stays pairable forever. Defaults to 0.
Q_PROPERTY(quint32 pairableTimeout READ pairableTimeout WRITE setPairableTimeout NOTIFY pairableTimeoutChanged);
/// Bluetooth devices connected to this adapter.
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothDevice>*);
Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
/// The internal ID of the adapter (e.g., "hci0").
Q_PROPERTY(QString adapterId READ adapterId CONSTANT);
/// DBus path of the adapter under the `org.bluez` system service.
Q_PROPERTY(QString dbusPath READ path CONSTANT);
// clang-format on
public:
explicit BluetoothAdapter(const QString& path, QObject* parent = nullptr);
[[nodiscard]] bool isValid() const { return this->mInterface->isValid(); }
[[nodiscard]] QString path() const { return this->mInterface->path(); }
[[nodiscard]] QString adapterId() const;
[[nodiscard]] bool enabled() const { return this->bEnabled; }
void setEnabled(bool enabled);
[[nodiscard]] bool discoverable() const { return this->bDiscoverable; }
void setDiscoverable(bool discoverable);
[[nodiscard]] bool discovering() const { return this->bDiscovering; }
void setDiscovering(bool discovering);
[[nodiscard]] quint32 discoverableTimeout() const { return this->bDiscoverableTimeout; }
void setDiscoverableTimeout(quint32 timeout);
[[nodiscard]] bool pairable() const { return this->bPairable; }
void setPairable(bool pairable);
[[nodiscard]] quint32 pairableTimeout() const { return this->bPairableTimeout; }
void setPairableTimeout(quint32 timeout);
[[nodiscard]] QBindable<QString> bindableName() { return &this->bName; }
[[nodiscard]] QBindable<bool> bindableEnabled() { return &this->bEnabled; }
[[nodiscard]] QBindable<BluetoothAdapterState::Enum> bindableState() { return &this->bState; }
[[nodiscard]] QBindable<bool> bindableDiscoverable() { return &this->bDiscoverable; }
[[nodiscard]] QBindable<quint32> bindableDiscoverableTimeout() {
return &this->bDiscoverableTimeout;
}
[[nodiscard]] QBindable<bool> bindableDiscovering() { return &this->bDiscovering; }
[[nodiscard]] QBindable<bool> bindablePairable() { return &this->bPairable; }
[[nodiscard]] QBindable<quint32> bindablePairableTimeout() { return &this->bPairableTimeout; }
[[nodiscard]] ObjectModel<BluetoothDevice>* devices() { return &this->mDevices; }
void addInterface(const QString& interface, const QVariantMap& properties);
void removeDevice(const QString& devicePath);
void startDiscovery();
void stopDiscovery();
signals:
void nameChanged();
void enabledChanged();
void stateChanged();
void discoverableChanged();
void discoverableTimeoutChanged();
void discoveringChanged();
void pairableChanged();
void pairableTimeoutChanged();
private:
DBusBluezAdapterInterface* mInterface = nullptr;
ObjectModel<BluetoothDevice> mDevices {this};
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, QString, bName, &BluetoothAdapter::nameChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bEnabled, &BluetoothAdapter::enabledChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, BluetoothAdapterState::Enum, bState, &BluetoothAdapter::stateChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscoverable, &BluetoothAdapter::discoverableChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bDiscoverableTimeout, &BluetoothAdapter::discoverableTimeoutChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscovering, &BluetoothAdapter::discoveringChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bPairable, &BluetoothAdapter::pairableChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bPairableTimeout, &BluetoothAdapter::pairableTimeoutChanged);
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothAdapter, properties);
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pName, bName, properties, "Alias");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pEnabled, bEnabled, properties, "Powered");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pState, bState, properties, "PowerState");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverable, bDiscoverable, properties, "Discoverable");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverableTimeout, bDiscoverableTimeout, properties, "DiscoverableTimeout");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscovering, bDiscovering, properties, "Discovering");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairable, bPairable, properties, "Pairable");
QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairableTimeout, bPairableTimeout, properties, "PairableTimeout");
// clang-format on
};
} // namespace qs::bluetooth
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter);
+168
View File
@@ -0,0 +1,168 @@
#include "bluez.hpp"
#include <qcontainerfwd.h>
#include <qdbusconnection.h>
#include <qdbusextratypes.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include "../core/logcat.hpp"
#include "../dbus/dbus_objectmanager_types.hpp"
#include "../dbus/objectmanager.hpp"
#include "adapter.hpp"
#include "device.hpp"
namespace qs::bluetooth {
namespace {
QS_LOGGING_CATEGORY(logBluetooth, "quickshell.bluetooth", QtWarningMsg);
}
Bluez* Bluez::instance() {
static auto* instance = new Bluez();
return instance;
}
Bluez::Bluez() { this->init(); }
void Bluez::updateDefaultAdapter() {
const auto& adapters = this->mAdapters.valueList();
this->bDefaultAdapter = adapters.empty() ? nullptr : adapters.first();
}
void Bluez::init() {
qCDebug(logBluetooth) << "Connecting to BlueZ";
auto bus = QDBusConnection::systemBus();
if (!bus.isConnected()) {
qCWarning(logBluetooth) << "Could not connect to DBus. Bluetooth integration is not available.";
return;
}
this->objectManager = new qs::dbus::DBusObjectManager(this);
QObject::connect(
this->objectManager,
&qs::dbus::DBusObjectManager::interfacesAdded,
this,
&Bluez::onInterfacesAdded
);
QObject::connect(
this->objectManager,
&qs::dbus::DBusObjectManager::interfacesRemoved,
this,
&Bluez::onInterfacesRemoved
);
if (!this->objectManager->setInterface("org.bluez", "/", bus)) {
qCDebug(logBluetooth) << "BlueZ is not running. Bluetooth integration will not work.";
return;
}
}
void Bluez::onInterfacesAdded(
const QDBusObjectPath& path,
const DBusObjectManagerInterfaces& interfaces
) {
if (auto* adapter = this->mAdapterMap.value(path.path())) {
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
adapter->addInterface(interface, properties);
}
} else if (auto* device = this->mDeviceMap.value(path.path())) {
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
device->addInterface(interface, properties);
}
} else if (interfaces.contains("org.bluez.Adapter1")) {
auto* adapter = new BluetoothAdapter(path.path(), this);
if (!adapter->isValid()) {
qCWarning(logBluetooth) << "Adapter path is not valid, cannot track: " << device;
delete adapter;
return;
}
qCDebug(logBluetooth) << "Tracked new adapter" << adapter;
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
adapter->addInterface(interface, properties);
}
for (auto* device: this->mDevices.valueList()) {
if (device->adapterPath() == path) {
adapter->devices()->insertObject(device);
qCDebug(logBluetooth) << "Added tracked device" << device << "to new adapter" << adapter;
emit device->adapterChanged();
}
}
this->mAdapterMap.insert(path.path(), adapter);
this->mAdapters.insertObject(adapter);
this->updateDefaultAdapter();
} else if (interfaces.contains("org.bluez.Device1")) {
auto* device = new BluetoothDevice(path.path(), this);
if (!device->isValid()) {
qCWarning(logBluetooth) << "Device path is not valid, cannot track: " << device;
delete device;
return;
}
qCDebug(logBluetooth) << "Tracked new device" << device;
for (const auto& [interface, properties]: interfaces.asKeyValueRange()) {
device->addInterface(interface, properties);
}
if (auto* adapter = device->adapter()) {
adapter->devices()->insertObject(device);
qCDebug(logBluetooth) << "Added device" << device << "to adapter" << adapter;
}
this->mDeviceMap.insert(path.path(), device);
this->mDevices.insertObject(device);
}
}
void Bluez::onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces) {
if (auto* adapter = this->mAdapterMap.value(path.path())) {
if (interfaces.contains("org.bluez.Adapter1")) {
qCDebug(logBluetooth) << "Adapter removed:" << adapter;
this->mAdapterMap.remove(path.path());
this->mAdapters.removeObject(adapter);
this->updateDefaultAdapter();
delete adapter;
}
} else if (auto* device = this->mDeviceMap.value(path.path())) {
if (interfaces.contains("org.bluez.Device1")) {
qCDebug(logBluetooth) << "Device removed:" << device;
if (auto* adapter = device->adapter()) {
adapter->devices()->removeObject(device);
}
this->mDeviceMap.remove(path.path());
this->mDevices.removeObject(device);
delete device;
} else {
for (const auto& interface: interfaces) {
device->removeInterface(interface);
}
}
}
}
BluezQml::BluezQml() {
QObject::connect(
Bluez::instance(),
&Bluez::defaultAdapterChanged,
this,
&BluezQml::defaultAdapterChanged
);
}
} // namespace qs::bluetooth
+98
View File
@@ -0,0 +1,98 @@
#pragma once
#include <qcontainerfwd.h>
#include <qhash.h>
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include "../core/doc.hpp"
#include "../core/model.hpp"
#include "../dbus/dbus_objectmanager_types.hpp"
#include "../dbus/objectmanager.hpp"
namespace qs::bluetooth {
class BluetoothAdapter;
class BluetoothDevice;
class Bluez: public QObject {
Q_OBJECT;
public:
[[nodiscard]] ObjectModel<BluetoothAdapter>* adapters() { return &this->mAdapters; }
[[nodiscard]] ObjectModel<BluetoothDevice>* devices() { return &this->mDevices; }
[[nodiscard]] BluetoothAdapter* adapter(const QString& path) {
return this->mAdapterMap.value(path);
}
static Bluez* instance();
signals:
void defaultAdapterChanged();
private slots:
void
onInterfacesAdded(const QDBusObjectPath& path, const DBusObjectManagerInterfaces& interfaces);
void onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces);
void updateDefaultAdapter();
private:
explicit Bluez();
void init();
qs::dbus::DBusObjectManager* objectManager = nullptr;
QHash<QString, BluetoothAdapter*> mAdapterMap;
QHash<QString, BluetoothDevice*> mDeviceMap;
ObjectModel<BluetoothAdapter> mAdapters {this};
ObjectModel<BluetoothDevice> mDevices {this};
public:
Q_OBJECT_BINDABLE_PROPERTY(
Bluez,
BluetoothAdapter*,
bDefaultAdapter,
&Bluez::defaultAdapterChanged
);
};
///! Bluetooth manager
/// Provides access to bluetooth devices and adapters.
class BluezQml: public QObject {
Q_OBJECT;
QML_NAMED_ELEMENT(Bluetooth);
QML_SINGLETON;
// clang-format off
/// The default bluetooth adapter. Usually there is only one.
Q_PROPERTY(BluetoothAdapter* defaultAdapter READ default NOTIFY defaultAdapterChanged BINDABLE bindableDefaultAdapter);
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothAdapter>*);
/// A list of all bluetooth adapters. See @@defaultAdapter for the default.
Q_PROPERTY(UntypedObjectModel* adapters READ adapters CONSTANT);
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::bluetooth::BluetoothDevice>*);
/// A list of all connected bluetooth devices across all adapters.
/// See @@BluetoothAdapter.devices for the devices connected to a single adapter.
Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
// clang-format on
signals:
void defaultAdapterChanged();
public:
explicit BluezQml();
[[nodiscard]] static ObjectModel<BluetoothAdapter>* adapters() {
return Bluez::instance()->adapters();
}
[[nodiscard]] static ObjectModel<BluetoothDevice>* devices() {
return Bluez::instance()->devices();
}
[[nodiscard]] static QBindable<BluetoothAdapter*> bindableDefaultAdapter() {
return &Bluez::instance()->bDefaultAdapter;
}
};
} // namespace qs::bluetooth
+318
View File
@@ -0,0 +1,318 @@
#include "device.hpp"
#include <qcontainerfwd.h>
#include <qdbusconnection.h>
#include <qdbuspendingcall.h>
#include <qdbuspendingreply.h>
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../core/logcat.hpp"
#include "../dbus/properties.hpp"
#include "adapter.hpp"
#include "bluez.hpp"
#include "dbus_device.h"
namespace qs::bluetooth {
namespace {
QS_LOGGING_CATEGORY(logDevice, "quickshell.bluetooth.device", QtWarningMsg);
}
QString BluetoothDeviceState::toString(BluetoothDeviceState::Enum state) {
switch (state) {
case BluetoothDeviceState::Disconnected: return QStringLiteral("Disconnected");
case BluetoothDeviceState::Connected: return QStringLiteral("Connected");
case BluetoothDeviceState::Disconnecting: return QStringLiteral("Disconnecting");
case BluetoothDeviceState::Connecting: return QStringLiteral("Connecting");
default: return QStringLiteral("Unknown");
}
}
BluetoothDevice::BluetoothDevice(const QString& path, QObject* parent): QObject(parent) {
this->mInterface =
new DBusBluezDeviceInterface("org.bluez", path, QDBusConnection::systemBus(), this);
if (!this->mInterface->isValid()) {
qCWarning(logDevice) << "Could not create DBus interface for device at" << path;
delete this->mInterface;
this->mInterface = nullptr;
return;
}
this->properties.setInterface(this->mInterface);
}
BluetoothAdapter* BluetoothDevice::adapter() const {
return Bluez::instance()->adapter(this->bAdapterPath.value().path());
}
void BluetoothDevice::setConnected(bool connected) {
if (connected == this->bConnected) return;
if (connected) {
this->connect();
} else {
this->disconnect();
}
}
void BluetoothDevice::setTrusted(bool trusted) {
if (trusted == this->bTrusted) return;
this->bTrusted = trusted;
this->pTrusted.write();
}
void BluetoothDevice::setBlocked(bool blocked) {
if (blocked == this->bBlocked) return;
this->bBlocked = blocked;
this->pBlocked.write();
}
void BluetoothDevice::setName(const QString& name) {
if (name == this->bName) return;
this->bName = name;
this->pName.write();
}
void BluetoothDevice::setWakeAllowed(bool wakeAllowed) {
if (wakeAllowed == this->bWakeAllowed) return;
this->bWakeAllowed = wakeAllowed;
this->pWakeAllowed.write();
}
void BluetoothDevice::connect() {
if (this->bConnected) {
qCCritical(logDevice) << "Device" << this << "is already connected";
return;
}
if (this->bState == BluetoothDeviceState::Connecting) {
qCCritical(logDevice) << "Device" << this << "is already connecting";
return;
}
qCDebug(logDevice) << "Connecting to device" << this;
this->bState = BluetoothDeviceState::Connecting;
auto reply = this->mInterface->Connect();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logDevice).nospace()
<< "Failed to connect to device " << this << ": " << reply.error().message();
this->bState = this->bConnected ? BluetoothDeviceState::Connected
: BluetoothDeviceState::Disconnected;
} else {
qCDebug(logDevice) << "Successfully connected to to device" << this;
}
delete watcher;
}
);
}
void BluetoothDevice::disconnect() {
if (!this->bConnected) {
qCCritical(logDevice) << "Device" << this << "is already disconnected";
return;
}
if (this->bState == BluetoothDeviceState::Disconnecting) {
qCCritical(logDevice) << "Device" << this << "is already disconnecting";
return;
}
qCDebug(logDevice) << "Disconnecting from device" << this;
this->bState = BluetoothDeviceState::Disconnecting;
auto reply = this->mInterface->Disconnect();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logDevice).nospace()
<< "Failed to disconnect from device " << this << ": " << reply.error().message();
this->bState = this->bConnected ? BluetoothDeviceState::Connected
: BluetoothDeviceState::Disconnected;
} else {
qCDebug(logDevice) << "Successfully disconnected from from device" << this;
}
delete watcher;
}
);
}
void BluetoothDevice::pair() {
if (this->bPaired) {
qCCritical(logDevice) << "Device" << this << "is already paired";
return;
}
if (this->bPairing) {
qCCritical(logDevice) << "Device" << this << "is already pairing";
return;
}
qCDebug(logDevice) << "Pairing with device" << this;
this->bPairing = true;
auto reply = this->mInterface->Pair();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logDevice).nospace()
<< "Failed to pair with device " << this << ": " << reply.error().message();
} else {
qCDebug(logDevice) << "Successfully initiated pairing with device" << this;
}
this->bPairing = false;
delete watcher;
}
);
}
void BluetoothDevice::cancelPair() {
if (!this->bPairing) {
qCCritical(logDevice) << "Device" << this << "is not currently pairing";
return;
}
qCDebug(logDevice) << "Cancelling pairing with device" << this;
auto reply = this->mInterface->CancelPairing();
auto* watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
this,
[this](QDBusPendingCallWatcher* watcher) {
const QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
qCWarning(logDevice) << "Failed to cancel pairing with device" << this << ":"
<< reply.error().message();
} else {
qCDebug(logDevice) << "Successfully cancelled pairing with device" << this;
}
this->bPairing = false;
delete watcher;
}
);
}
void BluetoothDevice::forget() {
if (!this->mInterface || !this->mInterface->isValid()) {
qCCritical(logDevice) << "Cannot forget - device interface is invalid";
return;
}
if (auto* adapter = Bluez::instance()->adapter(this->bAdapterPath.value().path())) {
qCDebug(logDevice) << "Forgetting device" << this << "via adapter" << adapter;
adapter->removeDevice(this->path());
} else {
qCCritical(logDevice) << "Could not find adapter for path" << this->bAdapterPath.value().path()
<< "to forget from";
}
}
void BluetoothDevice::addInterface(const QString& interface, const QVariantMap& properties) {
if (interface == "org.bluez.Device1") {
this->properties.updatePropertySet(properties, false);
qCDebug(logDevice) << "Updated Device properties for" << this;
} else if (interface == "org.bluez.Battery1") {
if (!this->mBatteryInterface) {
this->mBatteryInterface = new QDBusInterface(
"org.bluez",
this->path(),
"org.bluez.Battery1",
QDBusConnection::systemBus(),
this
);
if (!this->mBatteryInterface->isValid()) {
qCWarning(logDevice) << "Could not create Battery interface for device at" << this;
delete this->mBatteryInterface;
this->mBatteryInterface = nullptr;
return;
}
}
this->batteryProperties.setInterface(this->mBatteryInterface);
this->batteryProperties.updatePropertySet(properties, false);
emit this->batteryAvailableChanged();
qCDebug(logDevice) << "Updated Battery properties for" << this;
}
}
void BluetoothDevice::removeInterface(const QString& interface) {
if (interface == "org.bluez.Battery1" && this->mBatteryInterface) {
this->batteryProperties.setInterface(nullptr);
delete this->mBatteryInterface;
this->mBatteryInterface = nullptr;
this->bBattery = 0;
emit this->batteryAvailableChanged();
qCDebug(logDevice) << "Battery interface removed from device" << this;
}
}
void BluetoothDevice::onConnectedChanged() {
this->bState =
this->bConnected ? BluetoothDeviceState::Connected : BluetoothDeviceState::Disconnected;
emit this->connectedChanged();
}
} // namespace qs::bluetooth
namespace qs::dbus {
using namespace qs::bluetooth;
DBusResult<qreal> DBusDataTransform<BatteryPercentage>::fromWire(quint8 percentage) {
return DBusResult(percentage * 0.01);
}
} // namespace qs::dbus
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device) {
auto saver = QDebugStateSaver(debug);
if (device) {
debug.nospace() << "BluetoothDevice(" << static_cast<const void*>(device)
<< ", path=" << device->path() << ")";
} else {
debug << "BluetoothDevice(nullptr)";
}
return debug;
}
+225
View File
@@ -0,0 +1,225 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../dbus/properties.hpp"
#include "dbus_device.h"
namespace qs::bluetooth {
///! Connection state of a Bluetooth device.
class BluetoothDeviceState: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum : quint8 {
/// The device is not connected.
Disconnected = 0,
/// The device is connected.
Connected = 1,
/// The device is disconnecting.
Disconnecting = 2,
/// The device is connecting.
Connecting = 3,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(BluetoothDeviceState::Enum state);
};
struct BatteryPercentage {};
} // namespace qs::bluetooth
namespace qs::dbus {
template <>
struct DBusDataTransform<qs::bluetooth::BatteryPercentage> {
using Wire = quint8;
using Data = qreal;
static DBusResult<Data> fromWire(Wire percentage);
};
} // namespace qs::dbus
namespace qs::bluetooth {
class BluetoothAdapter;
///! A tracked Bluetooth device.
class BluetoothDevice: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_UNCREATABLE("");
// clang-format off
/// MAC address of the device.
Q_PROPERTY(QString address READ default NOTIFY addressChanged BINDABLE bindableAddress);
/// The name of the Bluetooth device. This property may be written to create an alias, or set to
/// an empty string to fall back to the device provided name.
///
/// See @@deviceName for the name provided by the device.
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged);
/// The name of the Bluetooth device, ignoring user provided aliases. See also @@name
/// which returns a user provided alias if set.
Q_PROPERTY(QString deviceName READ default NOTIFY deviceNameChanged BINDABLE bindableDeviceName);
/// System icon representing the device type. Use @@Quickshell.Quickshell.iconPath() to display this in an image.
Q_PROPERTY(QString icon READ default NOTIFY iconChanged BINDABLE bindableIcon);
/// Connection state of the device.
Q_PROPERTY(BluetoothDeviceState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState);
/// True if the device is currently connected to the computer.
///
/// Setting this property is equivalent to calling @@connect() and @@disconnect().
///
/// > [!NOTE] @@state provides more detailed information if required.
Q_PROPERTY(bool connected READ connected WRITE setConnected NOTIFY connectedChanged);
/// True if the device is paired to the computer.
///
/// > [!NOTE] @@pair() can be used to pair a device, however you must @@forget() the device to unpair it.
Q_PROPERTY(bool paired READ default NOTIFY pairedChanged BINDABLE bindablePaired);
/// True if pairing information is stored for future connections.
Q_PROPERTY(bool bonded READ default NOTIFY bondedChanged BINDABLE bindableBonded);
/// True if the device is currently being paired.
///
/// > [!NOTE] @@cancelPair() can be used to cancel the pairing process.
Q_PROPERTY(bool pairing READ pairing NOTIFY pairingChanged);
/// True if the device is considered to be trusted by the system.
/// Trusted devices are allowed to reconnect themselves to the system without intervention.
Q_PROPERTY(bool trusted READ trusted WRITE setTrusted NOTIFY trustedChanged);
/// True if the device is blocked from connecting.
/// If a device is blocked, any connection attempts will be immediately rejected by the system.
Q_PROPERTY(bool blocked READ blocked WRITE setBlocked NOTIFY blockedChanged);
/// True if the device is allowed to wake up the host system from suspend.
Q_PROPERTY(bool wakeAllowed READ wakeAllowed WRITE setWakeAllowed NOTIFY wakeAllowedChanged);
/// True if the connected device reports its battery level. Battery level can be accessed via @@battery.
Q_PROPERTY(bool batteryAvailable READ batteryAvailable NOTIFY batteryAvailableChanged);
/// Battery level of the connected device, from `0.0` to `1.0`. Only valid if @@batteryAvailable is true.
Q_PROPERTY(qreal battery READ default NOTIFY batteryChanged BINDABLE bindableBattery);
/// The Bluetooth adapter this device belongs to.
Q_PROPERTY(BluetoothAdapter* adapter READ adapter NOTIFY adapterChanged);
/// DBus path of the device under the `org.bluez` system service.
Q_PROPERTY(QString dbusPath READ path CONSTANT);
// clang-format on
public:
explicit BluetoothDevice(const QString& path, QObject* parent = nullptr);
/// Attempt to connect to the device.
Q_INVOKABLE void connect();
/// Disconnect from the device.
Q_INVOKABLE void disconnect();
/// Attempt to pair the device.
///
/// > [!NOTE] @@paired and @@pairing return the current pairing status of the device.
Q_INVOKABLE void pair();
/// Cancel an active pairing attempt.
Q_INVOKABLE void cancelPair();
/// Forget the device.
Q_INVOKABLE void forget();
[[nodiscard]] bool isValid() const { return this->mInterface && this->mInterface->isValid(); }
[[nodiscard]] QString path() const {
return this->mInterface ? this->mInterface->path() : QString();
}
[[nodiscard]] bool batteryAvailable() const { return this->mBatteryInterface != nullptr; }
[[nodiscard]] BluetoothAdapter* adapter() const;
[[nodiscard]] QDBusObjectPath adapterPath() const { return this->bAdapterPath.value(); }
[[nodiscard]] bool connected() const { return this->bConnected; }
void setConnected(bool connected);
[[nodiscard]] bool trusted() const { return this->bTrusted; }
void setTrusted(bool trusted);
[[nodiscard]] bool blocked() const { return this->bBlocked; }
void setBlocked(bool blocked);
[[nodiscard]] QString name() const { return this->bName; }
void setName(const QString& name);
[[nodiscard]] bool wakeAllowed() const { return this->bWakeAllowed; }
void setWakeAllowed(bool wakeAllowed);
[[nodiscard]] bool pairing() const { return this->bPairing; }
[[nodiscard]] QBindable<QString> bindableAddress() { return &this->bAddress; }
[[nodiscard]] QBindable<QString> bindableDeviceName() { return &this->bDeviceName; }
[[nodiscard]] QBindable<QString> bindableName() { return &this->bName; }
[[nodiscard]] QBindable<bool> bindableConnected() { return &this->bConnected; }
[[nodiscard]] QBindable<bool> bindablePaired() { return &this->bPaired; }
[[nodiscard]] QBindable<bool> bindableBonded() { return &this->bBonded; }
[[nodiscard]] QBindable<bool> bindableTrusted() { return &this->bTrusted; }
[[nodiscard]] QBindable<bool> bindableBlocked() { return &this->bBlocked; }
[[nodiscard]] QBindable<bool> bindableWakeAllowed() { return &this->bWakeAllowed; }
[[nodiscard]] QBindable<QString> bindableIcon() { return &this->bIcon; }
[[nodiscard]] QBindable<qreal> bindableBattery() { return &this->bBattery; }
[[nodiscard]] QBindable<BluetoothDeviceState::Enum> bindableState() { return &this->bState; }
void addInterface(const QString& interface, const QVariantMap& properties);
void removeInterface(const QString& interface);
signals:
void addressChanged();
void deviceNameChanged();
void nameChanged();
void connectedChanged();
void stateChanged();
void pairedChanged();
void bondedChanged();
void pairingChanged();
void trustedChanged();
void blockedChanged();
void wakeAllowedChanged();
void iconChanged();
void batteryAvailableChanged();
void batteryChanged();
void adapterChanged();
private:
void onConnectedChanged();
DBusBluezDeviceInterface* mInterface = nullptr;
QDBusInterface* mBatteryInterface = nullptr;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bAddress, &BluetoothDevice::addressChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bDeviceName, &BluetoothDevice::deviceNameChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bName, &BluetoothDevice::nameChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bConnected, &BluetoothDevice::onConnectedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPaired, &BluetoothDevice::pairedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBonded, &BluetoothDevice::bondedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bTrusted, &BluetoothDevice::trustedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBlocked, &BluetoothDevice::blockedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bWakeAllowed, &BluetoothDevice::wakeAllowedChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bIcon, &BluetoothDevice::iconChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QDBusObjectPath, bAdapterPath);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, qreal, bBattery, &BluetoothDevice::batteryChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, BluetoothDeviceState::Enum, bState, &BluetoothDevice::stateChanged);
Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPairing, &BluetoothDevice::pairingChanged);
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, properties);
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAddress, bAddress, properties, "Address");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pDeviceName, bDeviceName, properties, "Name");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pName, bName, properties, "Alias");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pConnected, bConnected, properties, "Connected");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pPaired, bPaired, properties, "Paired");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBonded, bBonded, properties, "Bonded");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pTrusted, bTrusted, properties, "Trusted");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBlocked, bBlocked, properties, "Blocked");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pWakeAllowed, bWakeAllowed, properties, "WakeAllowed");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pIcon, bIcon, properties, "Icon");
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAdapterPath, bAdapterPath, properties, "Adapter");
QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, batteryProperties);
QS_DBUS_PROPERTY_BINDING(BluetoothDevice, BatteryPercentage, pBattery, bBattery, batteryProperties, "Percentage", true);
// clang-format on
};
} // namespace qs::bluetooth
QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device);
+12
View File
@@ -0,0 +1,12 @@
name = "Quickshell.Bluetooth"
description = "Bluetooth API"
headers = [
"bluez.hpp",
"adapter.hpp",
"device.hpp",
]
-----
This module exposes Bluetooth management APIs provided by the BlueZ DBus interface.
Both DBus and BlueZ must be running to use it.
See the @@Quickshell.Bluetooth.Bluetooth singleton.
+9
View File
@@ -0,0 +1,9 @@
<node>
<interface name="org.bluez.Adapter1">
<method name="StartDiscovery"/>
<method name="StopDiscovery"/>
<method name="RemoveDevice">
<arg name="device" type="o"/>
</method>
</interface>
</node>
+8
View File
@@ -0,0 +1,8 @@
<node>
<interface name="org.bluez.Device1">
<method name="Connect"/>
<method name="Disconnect"/>
<method name="Pair"/>
<method name="CancelPairing"/>
</interface>
</node>
+200
View File
@@ -0,0 +1,200 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Bluetooth
FloatingWindow {
color: contentItem.palette.window
ListView {
anchors.fill: parent
anchors.margins: 5
model: Bluetooth.adapters
delegate: WrapperRectangle {
width: parent.width
color: "transparent"
border.color: palette.button
border.width: 1
margin: 5
ColumnLayout {
Label { text: `Adapter: ${modelData.name} (${modelData.adapterId})` }
RowLayout {
Layout.fillWidth: true
CheckBox {
text: "Enable"
checked: modelData.enabled
onToggled: modelData.enabled = checked
}
Label {
color: modelData.state === BluetoothAdapterState.Blocked ? palette.errorText : palette.placeholderText
text: BluetoothAdapterState.toString(modelData.state)
}
CheckBox {
text: "Discoverable"
checked: modelData.discoverable
onToggled: modelData.discoverable = checked
}
CheckBox {
text: "Discovering"
checked: modelData.discovering
onToggled: modelData.discovering = checked
}
CheckBox {
text: "Pairable"
checked: modelData.pairable
onToggled: modelData.pairable = checked
}
}
RowLayout {
Layout.fillWidth: true
Label { text: "Discoverable timeout:" }
SpinBox {
from: 0
to: 3600
value: modelData.discoverableTimeout
onValueModified: modelData.discoverableTimeout = value
textFromValue: time => time === 0 ? "∞" : time + "s"
}
Label { text: "Pairable timeout:" }
SpinBox {
from: 0
to: 3600
value: modelData.pairableTimeout
onValueModified: modelData.pairableTimeout = value
textFromValue: time => time === 0 ? "∞" : time + "s"
}
}
Repeater {
model: modelData.devices
WrapperRectangle {
Layout.fillWidth: true
color: palette.button
border.color: palette.mid
border.width: 1
margin: 5
RowLayout {
ColumnLayout {
Layout.fillWidth: true
RowLayout {
IconImage {
Layout.fillHeight: true
implicitWidth: height
source: Quickshell.iconPath(modelData.icon)
}
TextField {
text: modelData.name
font.bold: true
background: null
readOnly: false
selectByMouse: true
onEditingFinished: modelData.name = text
}
Label {
visible: modelData.name && modelData.name !== modelData.deviceName
text: `(${modelData.deviceName})`
color: palette.placeholderText
}
}
RowLayout {
Label {
text: modelData.address
color: palette.placeholderText
}
Label {
visible: modelData.batteryAvailable
text: `| Battery: ${Math.round(modelData.battery * 100)}%`
color: palette.placeholderText
}
}
RowLayout {
Label {
text: BluetoothDeviceState.toString(modelData.state)
color: modelData.connected ? palette.link : palette.placeholderText
}
Label {
text: modelData.pairing ? "Pairing" : (modelData.paired ? "Paired" : "Not Paired")
color: modelData.paired || modelData.pairing ? palette.link : palette.placeholderText
}
Label {
visible: modelData.bonded
text: "| Bonded"
color: palette.link
}
CheckBox {
text: "Trusted"
checked: modelData.trusted
onToggled: modelData.trusted = checked
}
CheckBox {
text: "Blocked"
checked: modelData.blocked
onToggled: modelData.blocked = checked
}
CheckBox {
text: "Wake Allowed"
checked: modelData.wakeAllowed
onToggled: modelData.wakeAllowed = checked
}
}
}
ColumnLayout {
Layout.alignment: Qt.AlignRight
Button {
Layout.alignment: Qt.AlignRight
text: modelData.connected ? "Disconnect" : "Connect"
onClicked: modelData.connected = !modelData.connected
}
Button {
Layout.alignment: Qt.AlignRight
text: modelData.pairing ? "Cancel" : (modelData.paired ? "Forget" : "Pair")
onClicked: {
if (modelData.pairing) {
modelData.cancelPair();
} else if (modelData.paired) {
modelData.forget();
} else {
modelData.pair();
}
}
}
}
}
}
}
}
}
}
}
+3 -3
View File
@@ -9,10 +9,10 @@ if (NOT DEFINED GIT_REVISION)
)
endif()
if (CRASH_REPORTER)
set(CRASH_REPORTER_DEF 1)
if (CRASH_HANDLER)
set(CRASH_HANDLER_DEF 1)
else()
set(CRASH_REPORTER_DEF 0)
set(CRASH_HANDLER_DEF 0)
endif()
if (DISTRIBUTOR_DEBUGINFO_AVAILABLE)
+6 -1
View File
@@ -1,10 +1,15 @@
#pragma once
// NOLINTBEGIN
#define QS_VERSION "@quickshell_VERSION@"
#define QS_VERSION_MAJOR @quickshell_VERSION_MAJOR@
#define QS_VERSION_MINOR @quickshell_VERSION_MINOR@
#define QS_VERSION_PATCH @quickshell_VERSION_PATCH@
#define QS_UNRELEASED_FEATURES "@UNRELEASED_FEATURES@"
#define GIT_REVISION "@GIT_REVISION@"
#define DISTRIBUTOR "@DISTRIBUTOR@"
#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@
#define CRASH_REPORTER @CRASH_REPORTER_DEF@
#define CRASH_HANDLER @CRASH_HANDLER_DEF@
#define BUILD_TYPE "@CMAKE_BUILD_TYPE@"
#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)"
#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@"
+4 -2
View File
@@ -12,6 +12,7 @@ qt_add_library(quickshell-core STATIC
singleton.cpp
generation.cpp
scan.cpp
scanenv.cpp
qsintercept.cpp
incubator.cpp
lazyloader.cpp
@@ -23,7 +24,7 @@ qt_add_library(quickshell-core STATIC
model.cpp
elapsedtimer.cpp
desktopentry.cpp
objectrepeater.cpp
desktopentrymonitor.cpp
platformmenu.cpp
qsmenu.cpp
retainable.cpp
@@ -38,6 +39,7 @@ qt_add_library(quickshell-core STATIC
iconprovider.cpp
scriptmodel.cpp
colorquantizer.cpp
toolsupport.cpp
)
qt_add_qml_module(quickshell-core
@@ -50,7 +52,7 @@ qt_add_qml_module(quickshell-core
install_qml_module(quickshell-core)
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets)
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build)
qs_module_pch(quickshell-core SET large)
+32 -27
View File
@@ -18,34 +18,38 @@
#include <qtmetamacros.h>
#include <qtypes.h>
#include "logcat.hpp"
namespace {
Q_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg);
QS_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg);
}
ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize)
: source(source)
, maxDepth(depth)
, rescaleSize(rescaleSize) {
setAutoDelete(false);
this->setAutoDelete(false);
}
void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) {
if (shouldCancel.loadAcquire() || source->isEmpty()) return;
if (shouldCancel.loadAcquire() || this->source->isEmpty()) return;
colors.clear();
this->colors.clear();
auto image = QImage(source->toLocalFile());
if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) {
auto image = QImage(this->source->toLocalFile());
if ((image.width() > this->rescaleSize || image.height() > this->rescaleSize)
&& this->rescaleSize > 0)
{
image = image.scaled(
static_cast<int>(rescaleSize),
static_cast<int>(rescaleSize),
static_cast<int>(this->rescaleSize),
static_cast<int>(this->rescaleSize),
Qt::KeepAspectRatio,
Qt::SmoothTransformation
);
}
if (image.isNull()) {
qCWarning(logColorQuantizer) << "Failed to load image from" << source;
qCWarning(logColorQuantizer) << "Failed to load image from" << this->source->toString();
return;
}
@@ -61,7 +65,7 @@ void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCa
auto startTime = QDateTime::currentDateTime();
colors = quantization(pixels, 0);
this->colors = this->quantization(pixels, 0);
auto endTime = QDateTime::currentDateTime();
auto milliseconds = startTime.msecsTo(endTime);
@@ -75,7 +79,7 @@ QList<QColor> ColorQuantizerOperation::quantization(
) {
if (shouldCancel.loadAcquire()) return QList<QColor>();
if (depth >= maxDepth || rgbValues.isEmpty()) {
if (depth >= this->maxDepth || rgbValues.isEmpty()) {
if (rgbValues.isEmpty()) return QList<QColor>();
auto totalR = 0;
@@ -112,8 +116,8 @@ QList<QColor> ColorQuantizerOperation::quantization(
auto rightHalf = rgbValues.mid(mid);
QList<QColor> result;
result.append(quantization(leftHalf, depth + 1));
result.append(quantization(rightHalf, depth + 1));
result.append(this->quantization(leftHalf, depth + 1));
result.append(this->quantization(rightHalf, depth + 1));
return result;
}
@@ -157,7 +161,7 @@ void ColorQuantizerOperation::finishRun() {
}
void ColorQuantizerOperation::finished() {
emit this->done(colors);
emit this->done(this->colors);
delete this;
}
@@ -176,39 +180,39 @@ void ColorQuantizerOperation::run() {
void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); }
void ColorQuantizer::componentComplete() {
componentCompleted = true;
if (!mSource.isEmpty()) quantizeAsync();
this->componentCompleted = true;
if (!this->mSource.isEmpty()) this->quantizeAsync();
}
void ColorQuantizer::setSource(const QUrl& source) {
if (mSource != source) {
mSource = source;
if (this->mSource != source) {
this->mSource = source;
emit this->sourceChanged();
if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync();
if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync();
}
}
void ColorQuantizer::setDepth(qreal depth) {
if (mDepth != depth) {
mDepth = depth;
if (this->mDepth != depth) {
this->mDepth = depth;
emit this->depthChanged();
if (this->componentCompleted) quantizeAsync();
if (this->componentCompleted) this->quantizeAsync();
}
}
void ColorQuantizer::setRescaleSize(int rescaleSize) {
if (mRescaleSize != rescaleSize) {
mRescaleSize = rescaleSize;
if (this->mRescaleSize != rescaleSize) {
this->mRescaleSize = rescaleSize;
emit this->rescaleSizeChanged();
if (this->componentCompleted) quantizeAsync();
if (this->componentCompleted) this->quantizeAsync();
}
}
void ColorQuantizer::operationFinished(const QList<QColor>& result) {
bColors = result;
this->bColors = result;
this->liveOperation = nullptr;
emit this->colorsChanged();
}
@@ -217,7 +221,8 @@ void ColorQuantizer::quantizeAsync() {
if (this->liveOperation) this->cancelAsync();
qCDebug(logColorQuantizer) << "Starting color quantization asynchronously";
this->liveOperation = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize);
this->liveOperation =
new ColorQuantizerOperation(&this->mSource, this->mDepth, this->mRescaleSize);
QObject::connect(
this->liveOperation,
+3 -3
View File
@@ -91,13 +91,13 @@ public:
[[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; }
[[nodiscard]] QUrl source() const { return mSource; }
[[nodiscard]] QUrl source() const { return this->mSource; }
void setSource(const QUrl& source);
[[nodiscard]] qreal depth() const { return mDepth; }
[[nodiscard]] qreal depth() const { return this->mDepth; }
void setDepth(qreal depth);
[[nodiscard]] qreal rescaleSize() const { return mRescaleSize; }
[[nodiscard]] qreal rescaleSize() const { return this->mRescaleSize; }
void setRescaleSize(int rescaleSize);
signals:
-2
View File
@@ -1,11 +1,9 @@
#include "common.hpp"
#include <qdatetime.h>
#include <qprocess.h>
namespace qs {
const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime();
QProcessEnvironment Common::INITIAL_ENVIRONMENT = {}; // NOLINT
} // namespace qs
+1 -1
View File
@@ -7,7 +7,7 @@ namespace qs {
struct Common {
static const QDateTime LAUNCH_TIME;
static QProcessEnvironment INITIAL_ENVIRONMENT; // NOLINT
static inline QProcessEnvironment INITIAL_ENVIRONMENT = {}; // NOLINT
};
} // namespace qs
+331 -133
View File
@@ -1,26 +1,33 @@
#include "desktopentry.hpp"
#include <algorithm>
#include <utility>
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qdir.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qhash.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qpair.h>
#include <qprocess.h>
#include <qstringview.h>
#include <qproperty.h>
#include <qscopeguard.h>
#include <qtenvironmentvariables.h>
#include <qthreadpool.h>
#include <qtmetamacros.h>
#include <ranges>
#include "common.hpp"
#include "../io/processcore.hpp"
#include "desktopentrymonitor.hpp"
#include "logcat.hpp"
#include "model.hpp"
#include "qmlglobal.hpp"
namespace {
Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
QS_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
}
struct Locale {
@@ -54,12 +61,14 @@ struct Locale {
[[nodiscard]] int matchScore(const Locale& other) const {
if (this->language != other.language) return 0;
auto territoryMatches = !this->territory.isEmpty() && this->territory == other.territory;
auto modifierMatches = !this->modifier.isEmpty() && this->modifier == other.modifier;
if (!other.modifier.isEmpty() && this->modifier != other.modifier) return 0;
if (!other.territory.isEmpty() && this->territory != other.territory) return 0;
auto score = 1;
if (territoryMatches) score += 2;
if (modifierMatches) score += 1;
if (!other.territory.isEmpty()) score += 2;
if (!other.modifier.isEmpty()) score += 1;
return score;
}
@@ -85,51 +94,60 @@ struct Locale {
QDebug operator<<(QDebug debug, const Locale& locale) {
auto saver = QDebugStateSaver(debug);
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
<< ", modifier" << locale.modifier << ')';
<< ", modifier=" << locale.modifier << ')';
return debug;
}
void DesktopEntry::parseEntry(const QString& text) {
ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& text) {
ParsedDesktopEntryData data;
data.id = id;
const auto& system = Locale::system();
auto groupName = QString();
auto entries = QHash<QString, QPair<Locale, QString>>();
auto finishCategory = [this, &groupName, &entries]() {
auto finishCategory = [&data, &groupName, &entries]() {
if (groupName == "Desktop Entry") {
if (entries["Type"].second != "Application") return;
if (entries.contains("Hidden") && entries["Hidden"].second == "true") return;
if (entries.value("Type").second != "Application") return;
for (const auto& [key, pair]: entries.asKeyValueRange()) {
auto& [_, value] = pair;
this->mEntries.insert(key, value);
data.entries.insert(key, value);
if (key == "Name") this->mName = value;
else if (key == "GenericName") this->mGenericName = value;
else if (key == "NoDisplay") this->mNoDisplay = value == "true";
else if (key == "Comment") this->mComment = value;
else if (key == "Icon") this->mIcon = value;
else if (key == "Exec") this->mExecString = value;
else if (key == "Path") this->mWorkingDirectory = value;
else if (key == "Terminal") this->mTerminal = value == "true";
else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts);
else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts);
if (key == "Name") data.name = value;
else if (key == "GenericName") data.genericName = value;
else if (key == "StartupWMClass") data.startupClass = value;
else if (key == "NoDisplay") data.noDisplay = value == "true";
else if (key == "Hidden") data.hidden = value == "true";
else if (key == "Comment") data.comment = value;
else if (key == "Icon") data.icon = value;
else if (key == "Exec") {
data.execString = value;
data.command = DesktopEntry::parseExecString(value);
} else if (key == "Path") data.workingDirectory = value;
else if (key == "Terminal") data.terminal = value == "true";
else if (key == "Categories") data.categories = value.split(u';', Qt::SkipEmptyParts);
else if (key == "Keywords") data.keywords = value.split(u';', Qt::SkipEmptyParts);
}
} else if (groupName.startsWith("Desktop Action ")) {
auto actionName = groupName.sliced(16);
auto* action = new DesktopAction(actionName, this);
DesktopActionData action;
action.id = actionName;
for (const auto& [key, pair]: entries.asKeyValueRange()) {
const auto& [_, value] = pair;
action->mEntries.insert(key, value);
action.entries.insert(key, value);
if (key == "Name") action->mName = value;
else if (key == "Icon") action->mIcon = value;
else if (key == "Exec") action->mExecString = value;
if (key == "Name") action.name = value;
else if (key == "Icon") action.icon = value;
else if (key == "Exec") {
action.execString = value;
action.command = DesktopEntry::parseExecString(value);
}
}
this->mActions.insert(actionName, action);
data.actions.insert(actionName, action);
}
entries.clear();
@@ -175,14 +193,62 @@ void DesktopEntry::parseEntry(const QString& text) {
}
finishCategory();
return data;
}
void DesktopEntry::updateState(const ParsedDesktopEntryData& newState) {
Qt::beginPropertyUpdateGroup();
this->bName = newState.name;
this->bGenericName = newState.genericName;
this->bStartupClass = newState.startupClass;
this->bNoDisplay = newState.noDisplay;
this->bComment = newState.comment;
this->bIcon = newState.icon;
this->bExecString = newState.execString;
this->bCommand = newState.command;
this->bWorkingDirectory = newState.workingDirectory;
this->bRunInTerminal = newState.terminal;
this->bCategories = newState.categories;
this->bKeywords = newState.keywords;
Qt::endPropertyUpdateGroup();
this->state = newState;
this->updateActions(newState.actions);
}
void DesktopEntry::updateActions(const QHash<QString, DesktopActionData>& newActions) {
auto old = this->mActions;
for (const auto& [key, d]: newActions.asKeyValueRange()) {
DesktopAction* act = nullptr;
if (auto found = old.find(key); found != old.end()) {
act = found.value();
old.erase(found);
} else {
act = new DesktopAction(d.id, this);
this->mActions.insert(key, act);
}
Qt::beginPropertyUpdateGroup();
act->bName = d.name;
act->bIcon = d.icon;
act->bExecString = d.execString;
act->bCommand = d.command;
Qt::endPropertyUpdateGroup();
act->mEntries = d.entries;
}
for (auto* leftover: old) {
leftover->deleteLater();
}
}
void DesktopEntry::execute() const {
DesktopEntry::doExec(this->mExecString, this->mWorkingDirectory);
DesktopEntry::doExec(this->bCommand.value(), this->bWorkingDirectory.value());
}
bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); }
bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); }
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); }
@@ -203,16 +269,22 @@ QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
currentArgument += '\\';
escape = 0;
}
} else if (escape == 2) {
currentArgument += c;
escape = 0;
} else if (escape != 0) {
if (escape != 2) {
// Technically this is an illegal state, but the spec has a terrible double escape
// rule in strings for no discernable reason. Assuming someone might understandably
// misunderstand it, treat it as a normal escape and log it.
switch (c.unicode()) {
case 's': currentArgument += u' '; break;
case 'n': currentArgument += u'\n'; break;
case 't': currentArgument += u'\t'; break;
case 'r': currentArgument += u'\r'; break;
case '\\': currentArgument += u'\\'; break;
default:
qCWarning(logDesktopEntry).noquote()
<< "Illegal escape sequence in desktop entry exec string:" << execString;
currentArgument += c;
break;
}
currentArgument += c;
escape = 0;
} else if (c == u'"' || c == u'\'') {
parsingString = false;
@@ -250,75 +322,52 @@ QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
return arguments;
}
void DesktopEntry::doExec(const QString& execString, const QString& workingDirectory) {
auto args = DesktopEntry::parseExecString(execString);
if (args.isEmpty()) {
qCWarning(logDesktopEntry) << "Tried to exec string" << execString << "which parsed as empty.";
return;
}
auto process = QProcess();
process.setProgram(args.at(0));
process.setArguments(args.sliced(1));
if (!workingDirectory.isEmpty()) process.setWorkingDirectory(workingDirectory);
process.setProcessEnvironment(qs::Common::INITIAL_ENVIRONMENT);
process.startDetached();
void DesktopEntry::doExec(const QList<QString>& execString, const QString& workingDirectory) {
qs::io::process::ProcessContext ctx;
ctx.setCommand(execString);
ctx.setWorkingDirectory(workingDirectory);
QuickshellGlobal::execDetached(ctx);
}
void DesktopAction::execute() const {
DesktopEntry::doExec(this->mExecString, this->entry->mWorkingDirectory);
DesktopEntry::doExec(this->bCommand.value(), this->entry->bWorkingDirectory.value());
}
DesktopEntryManager::DesktopEntryManager() {
this->scanDesktopEntries();
this->populateApplications();
DesktopEntryScanner::DesktopEntryScanner(DesktopEntryManager* manager): manager(manager) {
this->setAutoDelete(true);
}
void DesktopEntryManager::scanDesktopEntries() {
QList<QString> dataPaths;
void DesktopEntryScanner::run() {
const auto& desktopPaths = DesktopEntryManager::desktopPaths();
auto scanResults = QList<ParsedDesktopEntryData>();
if (qEnvironmentVariableIsSet("XDG_DATA_HOME")) {
dataPaths.push_back(qEnvironmentVariable("XDG_DATA_HOME"));
} else if (qEnvironmentVariableIsSet("HOME")) {
dataPaths.push_back(qEnvironmentVariable("HOME") + "/.local/share");
for (const auto& path: desktopPaths | std::views::reverse) {
auto file = QFileInfo(path);
if (!file.isDir()) continue;
this->scanDirectory(QDir(path), QString(), scanResults);
}
if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) {
auto var = qEnvironmentVariable("XDG_DATA_DIRS");
dataPaths += var.split(u':', Qt::SkipEmptyParts);
} else {
dataPaths.push_back("/usr/local/share");
dataPaths.push_back("/usr/share");
}
qCDebug(logDesktopEntry) << "Creating desktop entry scanners";
for (auto& path: std::ranges::reverse_view(dataPaths)) {
auto p = QDir(path).filePath("applications");
auto file = QFileInfo(p);
if (!file.isDir()) {
qCDebug(logDesktopEntry) << "Not scanning path" << p << "as it is not a directory";
continue;
}
qCDebug(logDesktopEntry) << "Scanning path" << p;
this->scanPath(p);
}
QMetaObject::invokeMethod(
this->manager,
"onScanCompleted",
Qt::QueuedConnection,
Q_ARG(QList<ParsedDesktopEntryData>, scanResults)
);
}
void DesktopEntryManager::populateApplications() {
for (auto& entry: this->desktopEntries.values()) {
if (!entry->noDisplay()) this->mApplications.insertObject(entry);
}
}
void DesktopEntryScanner::scanDirectory(
const QDir& dir,
const QString& idPrefix,
QList<ParsedDesktopEntryData>& entries
) {
auto dirEntries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
for (auto& entry: entries) {
if (entry.isDir()) this->scanPath(entry.absoluteFilePath(), prefix + dir.dirName() + "-");
else if (entry.isFile()) {
for (auto& entry: dirEntries) {
if (entry.isDir()) {
auto subdirPrefix = idPrefix.isEmpty() ? entry.fileName() : idPrefix + '-' + entry.fileName();
this->scanDirectory(QDir(entry.absoluteFilePath()), subdirPrefix, entries);
} else if (entry.isFile()) {
auto path = entry.filePath();
if (!path.endsWith(".desktop")) {
qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
@@ -331,46 +380,42 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
continue;
}
auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8);
auto lowerId = id.toLower();
auto basename = QFileInfo(entry.fileName()).completeBaseName();
auto id = idPrefix.isEmpty() ? basename : idPrefix + '-' + basename;
auto content = QString::fromUtf8(file.readAll());
auto text = QString::fromUtf8(file.readAll());
auto* dentry = new DesktopEntry(id, this);
dentry->parseEntry(text);
if (!dentry->isValid()) {
qCDebug(logDesktopEntry) << "Skipping desktop entry" << path;
delete dentry;
continue;
}
qCDebug(logDesktopEntry) << "Found desktop entry" << id << "at" << path;
auto conflictingId = this->desktopEntries.contains(id);
if (conflictingId) {
qCDebug(logDesktopEntry) << "Replacing old entry for" << id;
delete this->desktopEntries.value(id);
this->desktopEntries.remove(id);
this->lowercaseDesktopEntries.remove(lowerId);
}
this->desktopEntries.insert(id, dentry);
if (this->lowercaseDesktopEntries.contains(lowerId)) {
qCInfo(logDesktopEntry).nospace()
<< "Multiple desktop entries have the same lowercased id " << lowerId
<< ". This can cause ambiguity when byId requests are not made with the correct case "
"already.";
this->lowercaseDesktopEntries.remove(lowerId);
}
this->lowercaseDesktopEntries.insert(lowerId, dentry);
auto data = DesktopEntry::parseText(id, content);
entries.append(std::move(data));
}
}
}
DesktopEntryManager::DesktopEntryManager(): monitor(new DesktopEntryMonitor(this)) {
QObject::connect(
this->monitor,
&DesktopEntryMonitor::desktopEntriesChanged,
this,
&DesktopEntryManager::handleFileChanges
);
DesktopEntryScanner(this).run();
}
void DesktopEntryManager::scanDesktopEntries() {
qCDebug(logDesktopEntry) << "Starting desktop entry scan";
if (this->scanInProgress) {
qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan";
this->scanQueued = true;
return;
}
this->scanInProgress = true;
this->scanQueued = false;
auto* scanner = new DesktopEntryScanner(this);
QThreadPool::globalInstance()->start(scanner);
}
DesktopEntryManager* DesktopEntryManager::instance() {
static auto* instance = new DesktopEntryManager(); // NOLINT
return instance;
@@ -386,14 +431,167 @@ DesktopEntry* DesktopEntryManager::byId(const QString& id) {
}
}
DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) {
if (auto* entry = this->byId(name)) return entry;
auto list = this->desktopEntries.values();
auto iter = std::ranges::find_if(list, [&](DesktopEntry* entry) {
return name == entry->bStartupClass.value();
});
if (iter != list.end()) return *iter;
iter = std::ranges::find_if(list, [&](DesktopEntry* entry) {
return name.toLower() == entry->bStartupClass.value().toLower();
});
if (iter != list.end()) return *iter;
return nullptr;
}
ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); }
void DesktopEntryManager::handleFileChanges() {
qCDebug(logDesktopEntry) << "Directory change detected, performing full rescan";
if (this->scanInProgress) {
qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan";
this->scanQueued = true;
return;
}
this->scanInProgress = true;
this->scanQueued = false;
auto* scanner = new DesktopEntryScanner(this);
QThreadPool::globalInstance()->start(scanner);
}
const QStringList& DesktopEntryManager::desktopPaths() {
static const auto paths = []() {
auto dataPaths = QStringList();
auto dataHome = qEnvironmentVariable("XDG_DATA_HOME");
if (dataHome.isEmpty() && qEnvironmentVariableIsSet("HOME"))
dataHome = qEnvironmentVariable("HOME") + "/.local/share";
if (!dataHome.isEmpty()) dataPaths.append(dataHome + "/applications");
auto dataDirs = qEnvironmentVariable("XDG_DATA_DIRS");
if (dataDirs.isEmpty()) dataDirs = "/usr/local/share:/usr/share";
for (const auto& dir: dataDirs.split(':', Qt::SkipEmptyParts)) {
dataPaths.append(dir + "/applications");
}
return dataPaths;
}();
return paths;
}
void DesktopEntryManager::onScanCompleted(const QList<ParsedDesktopEntryData>& scanResults) {
auto guard = qScopeGuard([this] {
this->scanInProgress = false;
if (this->scanQueued) {
this->scanQueued = false;
this->scanDesktopEntries();
}
});
auto oldEntries = this->desktopEntries;
auto newEntries = QHash<QString, DesktopEntry*>();
auto newLowercaseEntries = QHash<QString, DesktopEntry*>();
for (const auto& data: scanResults) {
auto lowerId = data.id.toLower();
if (data.hidden) {
if (auto* victim = newEntries.take(data.id)) victim->deleteLater();
newLowercaseEntries.remove(lowerId);
if (auto it = oldEntries.find(data.id); it != oldEntries.end()) {
it.value()->deleteLater();
oldEntries.erase(it);
}
qCDebug(logDesktopEntry) << "Masking hidden desktop entry" << data.id;
continue;
}
DesktopEntry* dentry = nullptr;
if (auto it = oldEntries.find(data.id); it != oldEntries.end()) {
dentry = it.value();
oldEntries.erase(it);
dentry->updateState(data);
} else {
dentry = new DesktopEntry(data.id, this);
dentry->updateState(data);
}
if (!dentry->isValid()) {
qCDebug(logDesktopEntry) << "Skipping desktop entry" << data.id;
if (!oldEntries.contains(data.id)) {
dentry->deleteLater();
}
continue;
}
qCDebug(logDesktopEntry) << "Found desktop entry" << data.id;
auto conflictingId = newEntries.contains(data.id);
if (conflictingId) {
qCDebug(logDesktopEntry) << "Replacing old entry for" << data.id;
if (auto* victim = newEntries.take(data.id)) victim->deleteLater();
newLowercaseEntries.remove(lowerId);
}
newEntries.insert(data.id, dentry);
if (newLowercaseEntries.contains(lowerId)) {
qCInfo(logDesktopEntry).nospace()
<< "Multiple desktop entries have the same lowercased id " << lowerId
<< ". This can cause ambiguity when byId requests are not made with the correct case "
"already.";
newLowercaseEntries.remove(lowerId);
}
newLowercaseEntries.insert(lowerId, dentry);
}
this->desktopEntries = newEntries;
this->lowercaseDesktopEntries = newLowercaseEntries;
auto newApplications = QVector<DesktopEntry*>();
for (auto* entry: this->desktopEntries.values())
if (!entry->bNoDisplay) newApplications.append(entry);
this->mApplications.diffUpdate(newApplications);
emit this->applicationsChanged();
for (auto* e: oldEntries) e->deleteLater();
}
DesktopEntries::DesktopEntries() {
QObject::connect(
DesktopEntryManager::instance(),
&DesktopEntryManager::applicationsChanged,
this,
&DesktopEntries::applicationsChanged
);
}
DesktopEntry* DesktopEntries::byId(const QString& id) {
return DesktopEntryManager::instance()->byId(id);
}
DesktopEntry* DesktopEntries::heuristicLookup(const QString& name) {
return DesktopEntryManager::instance()->heuristicLookup(name);
}
ObjectModel<DesktopEntry>* DesktopEntries::applications() {
return DesktopEntryManager::instance()->applications();
}
+199 -35
View File
@@ -6,36 +6,84 @@
#include <qdir.h>
#include <qhash.h>
#include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qrunnable.h>
#include <qtmetamacros.h>
#include "desktopentrymonitor.hpp"
#include "doc.hpp"
#include "model.hpp"
class DesktopAction;
class DesktopEntryMonitor;
struct DesktopActionData {
QString id;
QString name;
QString icon;
QString execString;
QVector<QString> command;
QHash<QString, QString> entries;
};
struct ParsedDesktopEntryData {
QString id;
QString name;
QString genericName;
QString startupClass;
bool noDisplay = false;
bool hidden = false;
QString comment;
QString icon;
QString execString;
QVector<QString> command;
QString workingDirectory;
bool terminal = false;
QVector<QString> categories;
QVector<QString> keywords;
QHash<QString, QString> entries;
QHash<QString, DesktopActionData> actions;
};
/// A desktop entry. See @@DesktopEntries for details.
class DesktopEntry: public QObject {
Q_OBJECT;
Q_PROPERTY(QString id MEMBER mId CONSTANT);
/// Name of the specific application, such as "Firefox".
Q_PROPERTY(QString name MEMBER mName CONSTANT);
// clang-format off
Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName);
/// Short description of the application, such as "Web Browser". May be empty.
Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT);
Q_PROPERTY(QString genericName READ default WRITE default NOTIFY genericNameChanged BINDABLE bindableGenericName);
/// Initial class or app id the app intends to use. May be useful for matching running apps
/// to desktop entries.
Q_PROPERTY(QString startupClass READ default WRITE default NOTIFY startupClassChanged BINDABLE bindableStartupClass);
/// If true, this application should not be displayed in menus and launchers.
Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT);
Q_PROPERTY(bool noDisplay READ default WRITE default NOTIFY noDisplayChanged BINDABLE bindableNoDisplay);
/// Long description of the application, such as "View websites on the internet". May be empty.
Q_PROPERTY(QString comment MEMBER mComment CONSTANT);
Q_PROPERTY(QString comment READ default WRITE default NOTIFY commentChanged BINDABLE bindableComment);
/// Name of the icon associated with this application. May be empty.
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
/// The raw `Exec` string from the desktop entry. You probably want @@execute().
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon);
/// The raw `Exec` string from the desktop entry.
///
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString);
/// The parsed `Exec` command in the desktop entry.
///
/// The entry can be run with @@execute(), or by using this command in
/// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process.
/// If used in `execDetached` or a `Process`, @@workingDirectory should also be passed to
/// the invoked process. See @@execute() for details.
///
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
Q_PROPERTY(QVector<QString> command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand);
/// The working directory to execute from.
Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT);
Q_PROPERTY(QString workingDirectory READ default WRITE default NOTIFY workingDirectoryChanged BINDABLE bindableWorkingDirectory);
/// If the application should run in a terminal.
Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT);
Q_PROPERTY(QVector<QString> categories MEMBER mCategories CONSTANT);
Q_PROPERTY(QVector<QString> keywords MEMBER mKeywords CONSTANT);
Q_PROPERTY(bool runInTerminal READ default WRITE default NOTIFY runInTerminalChanged BINDABLE bindableRunInTerminal);
Q_PROPERTY(QVector<QString> categories READ default WRITE default NOTIFY categoriesChanged BINDABLE bindableCategories);
Q_PROPERTY(QVector<QString> keywords READ default WRITE default NOTIFY keywordsChanged BINDABLE bindableKeywords);
// clang-format on
Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT);
QML_ELEMENT;
QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
@@ -43,34 +91,82 @@ class DesktopEntry: public QObject {
public:
explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
void parseEntry(const QString& text);
static ParsedDesktopEntryData parseText(const QString& id, const QString& text);
void updateState(const ParsedDesktopEntryData& newState);
/// Run the application. Currently ignores @@runInTerminal and field codes.
///
/// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command
/// and @@DesktopEntry.workingDirectory as shown below:
///
/// ```qml
/// Quickshell.execDetached({
/// command: desktopEntry.command,
/// workingDirectory: desktopEntry.workingDirectory,
/// });
/// ```
Q_INVOKABLE void execute() const;
[[nodiscard]] bool isValid() const;
[[nodiscard]] bool noDisplay() const;
[[nodiscard]] QVector<DesktopAction*> actions() const;
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
[[nodiscard]] QBindable<QString> bindableGenericName() const { return &this->bGenericName; }
[[nodiscard]] QBindable<QString> bindableStartupClass() const { return &this->bStartupClass; }
[[nodiscard]] QBindable<bool> bindableNoDisplay() const { return &this->bNoDisplay; }
[[nodiscard]] QBindable<QString> bindableComment() const { return &this->bComment; }
[[nodiscard]] QBindable<QString> bindableIcon() const { return &this->bIcon; }
[[nodiscard]] QBindable<QString> bindableExecString() const { return &this->bExecString; }
[[nodiscard]] QBindable<QVector<QString>> bindableCommand() const { return &this->bCommand; }
[[nodiscard]] QBindable<QString> bindableWorkingDirectory() const {
return &this->bWorkingDirectory;
}
[[nodiscard]] QBindable<bool> bindableRunInTerminal() const { return &this->bRunInTerminal; }
[[nodiscard]] QBindable<QVector<QString>> bindableCategories() const {
return &this->bCategories;
}
[[nodiscard]] QBindable<QVector<QString>> bindableKeywords() const { return &this->bKeywords; }
// currently ignores all field codes.
static QVector<QString> parseExecString(const QString& execString);
static void doExec(const QString& execString, const QString& workingDirectory);
static void doExec(const QList<QString>& execString, const QString& workingDirectory);
signals:
void nameChanged();
void genericNameChanged();
void startupClassChanged();
void noDisplayChanged();
void commentChanged();
void iconChanged();
void execStringChanged();
void commandChanged();
void workingDirectoryChanged();
void runInTerminalChanged();
void categoriesChanged();
void keywordsChanged();
public:
QString mId;
QString mName;
QString mGenericName;
bool mNoDisplay = false;
QString mComment;
QString mIcon;
QString mExecString;
QString mWorkingDirectory;
bool mTerminal = false;
QVector<QString> mCategories;
QVector<QString> mKeywords;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bName, &DesktopEntry::nameChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bGenericName, &DesktopEntry::genericNameChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bStartupClass, &DesktopEntry::startupClassChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bNoDisplay, &DesktopEntry::noDisplayChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bComment, &DesktopEntry::commentChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bIcon, &DesktopEntry::iconChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bExecString, &DesktopEntry::execStringChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bCommand, &DesktopEntry::commandChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bWorkingDirectory, &DesktopEntry::workingDirectoryChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bRunInTerminal, &DesktopEntry::runInTerminalChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bCategories, &DesktopEntry::categoriesChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bKeywords, &DesktopEntry::keywordsChanged);
// clang-format on
private:
QHash<QString, QString> mEntries;
void updateActions(const QHash<QString, DesktopActionData>& newActions);
ParsedDesktopEntryData state;
QHash<QString, DesktopAction*> mActions;
friend class DesktopAction;
@@ -80,10 +176,23 @@ private:
class DesktopAction: public QObject {
Q_OBJECT;
Q_PROPERTY(QString id MEMBER mId CONSTANT);
Q_PROPERTY(QString name MEMBER mName CONSTANT);
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
/// The raw `Exec` string from the desktop entry. You probably want @@execute().
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
// clang-format off
Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName);
Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon);
/// The raw `Exec` string from the action.
///
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString);
/// The parsed `Exec` command in the action.
///
/// The entry can be run with @@execute(), or by using this command in
/// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process.
/// If used in `execDetached` or a `Process`, @@DesktopEntry.workingDirectory should also be passed to
/// the invoked process.
///
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
Q_PROPERTY(QVector<QString> command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand);
// clang-format on
QML_ELEMENT;
QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
@@ -94,19 +203,52 @@ public:
, mId(std::move(id)) {}
/// Run the application. Currently ignores @@DesktopEntry.runInTerminal and field codes.
///
/// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command
/// and @@DesktopEntry.workingDirectory.
Q_INVOKABLE void execute() const;
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
[[nodiscard]] QBindable<QString> bindableIcon() const { return &this->bIcon; }
[[nodiscard]] QBindable<QString> bindableExecString() const { return &this->bExecString; }
[[nodiscard]] QBindable<QVector<QString>> bindableCommand() const { return &this->bCommand; }
signals:
void nameChanged();
void iconChanged();
void execStringChanged();
void commandChanged();
private:
DesktopEntry* entry;
QString mId;
QString mName;
QString mIcon;
QString mExecString;
QHash<QString, QString> mEntries;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bName, &DesktopAction::nameChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bIcon, &DesktopAction::iconChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bExecString, &DesktopAction::execStringChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QVector<QString>, bCommand, &DesktopAction::commandChanged);
// clang-format on
friend class DesktopEntry;
};
class DesktopEntryManager;
class DesktopEntryScanner: public QRunnable {
public:
explicit DesktopEntryScanner(DesktopEntryManager* manager);
void run() override;
// clang-format off
void scanDirectory(const QDir& dir, const QString& idPrefix, QList<ParsedDesktopEntryData>& entries);
// clang-format on
private:
DesktopEntryManager* manager;
};
class DesktopEntryManager: public QObject {
Q_OBJECT;
@@ -114,20 +256,32 @@ public:
void scanDesktopEntries();
[[nodiscard]] DesktopEntry* byId(const QString& id);
[[nodiscard]] DesktopEntry* heuristicLookup(const QString& name);
[[nodiscard]] ObjectModel<DesktopEntry>* applications();
static DesktopEntryManager* instance();
static const QStringList& desktopPaths();
signals:
void applicationsChanged();
private slots:
void handleFileChanges();
void onScanCompleted(const QList<ParsedDesktopEntryData>& scanResults);
private:
explicit DesktopEntryManager();
void populateApplications();
void scanPath(const QDir& dir, const QString& prefix = QString());
QHash<QString, DesktopEntry*> desktopEntries;
QHash<QString, DesktopEntry*> lowercaseDesktopEntries;
ObjectModel<DesktopEntry> mApplications {this};
DesktopEntryMonitor* monitor = nullptr;
bool scanInProgress = false;
bool scanQueued = false;
friend class DesktopEntryScanner;
};
///! Desktop entry index.
@@ -149,7 +303,17 @@ public:
explicit DesktopEntries();
/// Look up a desktop entry by name. Includes NoDisplay entries. May return null.
///
/// While this function requires an exact match, @@heuristicLookup() will correctly
/// find an entry more often and is generally more useful.
Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id);
/// Look up a desktop entry by name using heuristics. Unlike @@byId(),
/// if no exact matches are found this function will try to guess - potentially incorrectly.
/// May return null.
Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name);
[[nodiscard]] static ObjectModel<DesktopEntry>* applications();
signals:
void applicationsChanged();
};
+68
View File
@@ -0,0 +1,68 @@
#include "desktopentrymonitor.hpp"
#include <qdir.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qobject.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include "desktopentry.hpp"
namespace {
void addPathAndParents(QFileSystemWatcher& watcher, const QString& path) {
watcher.addPath(path);
auto p = QFileInfo(path).absolutePath();
while (!p.isEmpty()) {
watcher.addPath(p);
const auto parent = QFileInfo(p).dir().absolutePath();
if (parent == p) break;
p = parent;
}
}
} // namespace
DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) {
this->debounceTimer.setSingleShot(true);
this->debounceTimer.setInterval(100);
QObject::connect(
&this->watcher,
&QFileSystemWatcher::directoryChanged,
this,
&DesktopEntryMonitor::onDirectoryChanged
);
QObject::connect(
&this->debounceTimer,
&QTimer::timeout,
this,
&DesktopEntryMonitor::processChanges
);
this->startMonitoring();
}
void DesktopEntryMonitor::startMonitoring() {
for (const auto& path: DesktopEntryManager::desktopPaths()) {
if (!QDir(path).exists()) continue;
addPathAndParents(this->watcher, path);
this->scanAndWatch(path);
}
}
void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) {
auto dir = QDir(dirPath);
if (!dir.exists()) return;
this->watcher.addPath(dirPath);
auto subdirs = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
for (const auto& subdir: subdirs) this->watcher.addPath(subdir.absoluteFilePath());
}
void DesktopEntryMonitor::onDirectoryChanged(const QString& /*path*/) {
this->debounceTimer.start();
}
void DesktopEntryMonitor::processChanges() { emit this->desktopEntriesChanged(); }
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <qfilesystemwatcher.h>
#include <qobject.h>
#include <qstringlist.h>
#include <qtimer.h>
class DesktopEntryMonitor: public QObject {
Q_OBJECT
public:
explicit DesktopEntryMonitor(QObject* parent = nullptr);
~DesktopEntryMonitor() override = default;
DesktopEntryMonitor(const DesktopEntryMonitor&) = delete;
DesktopEntryMonitor& operator=(const DesktopEntryMonitor&) = delete;
DesktopEntryMonitor(DesktopEntryMonitor&&) = delete;
DesktopEntryMonitor& operator=(DesktopEntryMonitor&&) = delete;
signals:
void desktopEntriesChanged();
private slots:
void onDirectoryChanged(const QString& path);
void processChanges();
private:
void startMonitoring();
void scanAndWatch(const QString& dirPath);
QFileSystemWatcher watcher;
QTimer debounceTimer;
};
+35 -108
View File
@@ -16,18 +16,20 @@
#include <qqmlengine.h>
#include <qqmlerror.h>
#include <qqmlincubator.h>
#include <qquickwindow.h>
#include <qtmetamacros.h>
#include "iconimageprovider.hpp"
#include "imageprovider.hpp"
#include "incubator.hpp"
#include "logcat.hpp"
#include "plugin.hpp"
#include "qsintercept.hpp"
#include "reload.hpp"
#include "scan.hpp"
namespace {
Q_LOGGING_CATEGORY(logScene, "scene");
QS_LOGGING_CATEGORY(logScene, "scene");
}
static QHash<const QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
@@ -36,7 +38,7 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
: rootPath(rootPath)
, scanner(std::move(scanner))
, urlInterceptor(this->rootPath)
, interceptNetFactory(this->scanner.fileIntercepts)
, interceptNetFactory(this->rootPath, this->scanner.fileIntercepts)
, engine(new QQmlEngine()) {
g_generations.insert(this->engine, this);
@@ -44,8 +46,11 @@ 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);
this->incubationController.initLoop();
this->engine->setIncubationController(&this->incubationController);
this->engine->addImageProvider("icon", new IconImageProvider());
this->engine->addImageProvider("qsimage", new QsImageProvider());
@@ -130,7 +135,7 @@ void EngineGeneration::onReload(EngineGeneration* old) {
// new generation acquires it then incubators will hang intermittently
qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old;
old->incubationControllersLocked = true;
old->assignIncubationController();
old->updateIncubationMode();
}
QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit);
@@ -157,8 +162,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 +224,11 @@ void EngineGeneration::onFileChanged(const QString& name) {
if (!this->watcher->files().contains(name)) {
this->deletedWatchedFiles.push_back(name);
} else {
// some editors (e.g vscode) perform file saving in two steps: truncate + write
// ignore the first event (truncate) with size 0 to prevent incorrect live reloading
auto fileInfo = QFileInfo(name);
if (fileInfo.isFile() && fileInfo.size() == 0) return;
emit this->filesChanged();
}
}
@@ -232,98 +243,11 @@ void EngineGeneration::onDirectoryChanged() {
}
}
void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) {
// We only want controllers that we can swap out if destroyed.
// This happens if the window owning the active controller dies.
if (auto* obj = dynamic_cast<QObject*>(controller)) {
QObject::connect(
obj,
&QObject::destroyed,
this,
&EngineGeneration::incubationControllerDestroyed
);
} else {
qCWarning(logIncubator) << "Could not register incubation controller as it is not a QObject"
<< controller;
return;
}
this->incubationControllers.push_back(controller);
qCDebug(logIncubator) << "Registered incubation controller" << controller << "to generation"
<< this;
// This function can run during destruction.
if (this->engine == nullptr) return;
if (this->engine->incubationController() == &this->delayedIncubationController) {
this->assignIncubationController();
}
}
void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) {
if (auto* obj = dynamic_cast<QObject*>(controller)) {
QObject::disconnect(obj, nullptr, this, nullptr);
} else {
qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, "
"however only QObject controllers should be registered.";
}
if (!this->incubationControllers.removeOne(controller)) {
qCCritical(logIncubator) << "Failed to deregister incubation controller" << controller << "from"
<< this << "as it was not registered to begin with";
qCCritical(logIncubator) << "Current registered incuabation controllers"
<< this->incubationControllers;
} else {
qCDebug(logIncubator) << "Deregistered incubation controller" << controller << "from" << this;
}
// This function can run during destruction.
if (this->engine == nullptr) return;
if (this->engine->incubationController() == controller) {
qCDebug(logIncubator
) << "Destroyed incubation controller was currently active, reassigning from pool";
this->assignIncubationController();
}
}
void EngineGeneration::incubationControllerDestroyed() {
auto* sender = this->sender();
auto* controller = dynamic_cast<QQmlIncubationController*>(sender);
if (controller == nullptr) {
qCCritical(logIncubator) << "Destroyed incubation controller" << sender << "is not known to"
<< this << ", this may cause memory corruption";
qCCritical(logIncubator) << "Current registered incuabation controllers"
<< this->incubationControllers;
return;
}
if (this->incubationControllers.removeOne(controller)) {
qCDebug(logIncubator) << "Destroyed incubation controller" << controller << "deregistered from"
<< this;
} else {
qCCritical(logIncubator) << "Destroyed incubation controller" << controller
<< "was not registered, but its destruction was observed by" << this;
return;
}
// This function can run during destruction.
if (this->engine == nullptr) return;
if (this->engine->incubationController() == controller) {
qCDebug(logIncubator
) << "Destroyed incubation controller was currently active, reassigning from pool";
this->assignIncubationController();
}
}
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) const {
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) {
for (const auto& error: warnings) {
auto rel = "**/" % this->rootPath.relativeFilePath(error.url().path());
const auto& url = error.url();
auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5)
: url.toString();
QString objectName;
auto desc = error.description();
@@ -360,20 +284,23 @@ void EngineGeneration::exit(int code) {
this->destroy();
}
void EngineGeneration::assignIncubationController() {
QQmlIncubationController* controller = nullptr;
void EngineGeneration::trackWindowIncubationController(QQuickWindow* window) {
if (this->trackedWindows.contains(window)) return;
if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) {
controller = &this->delayedIncubationController;
} else {
controller = this->incubationControllers.first();
}
QObject::connect(window, &QObject::destroyed, this, &EngineGeneration::onTrackedWindowDestroyed);
this->trackedWindows.append(window);
this->updateIncubationMode();
}
qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"
<< this
<< "fallback:" << (controller == &this->delayedIncubationController);
void EngineGeneration::onTrackedWindowDestroyed(QObject* object) {
this->trackedWindows.removeAll(static_cast<QQuickWindow*>(object)); // NOLINT
this->updateIncubationMode();
}
this->engine->setIncubationController(controller);
void EngineGeneration::updateIncubationMode() {
// If we're in a situation with only hidden but tracked windows this might be wrong,
// but it seems to at least work.
this->incubationController.setIncubationMode(!this->trackedWindows.empty());
}
EngineGeneration* EngineGeneration::currentGeneration() {
+8 -7
View File
@@ -9,6 +9,7 @@
#include <qqmlengine.h>
#include <qqmlerror.h>
#include <qqmlincubator.h>
#include <qquickwindow.h>
#include <qtclasshelpermacros.h>
#include "incubator.hpp"
@@ -40,8 +41,7 @@ public:
void setWatchingFiles(bool watching);
bool setExtraWatchedFiles(const QVector<QString>& files);
void registerIncubationController(QQmlIncubationController* controller);
void deregisterIncubationController(QQmlIncubationController* controller);
void trackWindowIncubationController(QQuickWindow* window);
// takes ownership
void registerExtension(const void* key, EngineGenerationExt* extension);
@@ -65,7 +65,7 @@ public:
QFileSystemWatcher* watcher = nullptr;
QVector<QString> deletedWatchedFiles;
QVector<QString> extraWatchedFiles;
DelayedQmlIncubationController delayedIncubationController;
QsIncubationController incubationController;
bool reloadComplete = false;
QuickshellGlobal* qsgInstance = nullptr;
@@ -75,6 +75,7 @@ public:
signals:
void filesChanged();
void reloadFinished();
void firePostReload();
public slots:
void quit();
@@ -83,13 +84,13 @@ public slots:
private slots:
void onFileChanged(const QString& name);
void onDirectoryChanged();
void incubationControllerDestroyed();
void onEngineWarnings(const QList<QQmlError>& warnings) const;
void onTrackedWindowDestroyed(QObject* object);
static void onEngineWarnings(const QList<QQmlError>& warnings);
private:
void postReload();
void assignIncubationController();
QVector<QQmlIncubationController*> incubationControllers;
void updateIncubationMode();
QVector<QQuickWindow*> trackedWindows;
bool incubationControllersLocked = false;
QHash<const void*, EngineGenerationExt*> extensions;
+3 -3
View File
@@ -19,8 +19,7 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
if (splitIdx != -1) {
iconName = id.sliced(0, splitIdx);
path = id.sliced(splitIdx + 6);
qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for"
<< id;
path = QString("/%1/%2").arg(path, iconName.sliced(iconName.lastIndexOf('/') + 1));
} else {
splitIdx = id.indexOf("?fallback=");
if (splitIdx != -1) {
@@ -32,7 +31,8 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
}
auto icon = QIcon::fromTheme(iconName);
if (icon.isNull()) icon = QIcon::fromTheme(fallbackName);
if (icon.isNull() && !fallbackName.isEmpty()) icon = QIcon::fromTheme(fallbackName);
if (icon.isNull() && !path.isEmpty()) icon = QPixmap(path);
auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100);
if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2);
+2 -2
View File
@@ -22,8 +22,8 @@ class PixmapCacheIconEngine: public QIconEngine {
QIcon::Mode /*unused*/,
QIcon::State /*unused*/
) override {
qFatal(
) << "Unexpected icon paint request bypassed pixmap method. Please report this as a bug.";
qFatal()
<< "Unexpected icon paint request bypassed pixmap method. Please report this as a bug.";
}
QPixmap pixmap(const QSize& size, QIcon::Mode /*unused*/, QIcon::State /*unused*/) override {
+120 -1
View File
@@ -1,11 +1,21 @@
#include "incubator.hpp"
#include <private/qsgrenderloop_p.h>
#include <qabstractanimation.h>
#include <qguiapplication.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qminmax.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qqmlincubator.h>
#include <qscreen.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) {
@@ -14,3 +24,112 @@ void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) {
default: break;
}
}
void QsIncubationController::initLoop() {
auto* app = static_cast<QGuiApplication*>(QGuiApplication::instance()); // NOLINT
this->renderLoop = QSGRenderLoop::instance();
QObject::connect(
app,
&QGuiApplication::screenAdded,
this,
&QsIncubationController::updateIncubationTime
);
QObject::connect(
app,
&QGuiApplication::screenRemoved,
this,
&QsIncubationController::updateIncubationTime
);
this->updateIncubationTime();
QObject::connect(
this->renderLoop,
&QSGRenderLoop::timeToIncubate,
this,
&QsIncubationController::incubate
);
QAnimationDriver* animationDriver = this->renderLoop->animationDriver();
if (animationDriver) {
QObject::connect(
animationDriver,
&QAnimationDriver::stopped,
this,
&QsIncubationController::animationStopped
);
} else {
qCInfo(logIncubator) << "Render loop does not have animation driver, animationStopped cannot "
"be used to trigger incubation.";
}
}
void QsIncubationController::setIncubationMode(bool render) {
if (render == this->followRenderloop) return;
this->followRenderloop = render;
if (render) {
qCDebug(logIncubator) << "Incubation mode changed: render loop driven";
} else {
qCDebug(logIncubator) << "Incubation mode changed: event loop driven";
}
if (!render && this->incubatingObjectCount()) this->incubateLater();
}
void QsIncubationController::timerEvent(QTimerEvent* /*event*/) {
this->killTimer(this->timerId);
this->timerId = 0;
this->incubate();
}
void QsIncubationController::incubateLater() {
if (this->followRenderloop) {
if (this->timerId != 0) {
this->killTimer(this->timerId);
this->timerId = 0;
}
// Incubate again at the end of the event processing queue
QMetaObject::invokeMethod(this, &QsIncubationController::incubate, Qt::QueuedConnection);
} else if (this->timerId == 0) {
// Wait for a while before processing the next batch. Using a
// timer to avoid starvation of system events.
this->timerId = this->startTimer(this->incubationTime);
}
}
void QsIncubationController::incubate() {
if ((!this->followRenderloop || this->renderLoop) && this->incubatingObjectCount()) {
if (!this->followRenderloop) {
this->incubateFor(10);
if (this->incubatingObjectCount()) this->incubateLater();
} else if (this->renderLoop->interleaveIncubation()) {
this->incubateFor(this->incubationTime);
} else {
this->incubateFor(this->incubationTime * 2);
if (this->incubatingObjectCount()) this->incubateLater();
}
}
}
void QsIncubationController::animationStopped() { this->incubate(); }
void QsIncubationController::incubatingObjectCountChanged(int count) {
if (count
&& (!this->followRenderloop
|| (this->renderLoop && !this->renderLoop->interleaveIncubation())))
{
this->incubateLater();
}
}
void QsIncubationController::updateIncubationTime() {
auto* screen = QGuiApplication::primaryScreen();
if (!screen) return;
// 1/3 frame on primary screen
this->incubationTime = qMax(1, static_cast<int>(1000 / screen->refreshRate() / 3));
}
+37 -5
View File
@@ -1,11 +1,13 @@
#pragma once
#include <qloggingcategory.h>
#include <qobject.h>
#include <qpointer.h>
#include <qqmlincubator.h>
#include <qtmetamacros.h>
Q_DECLARE_LOGGING_CATEGORY(logIncubator);
#include "logcat.hpp"
QS_DECLARE_LOGGING_CATEGORY(logIncubator);
class QsQmlIncubator
: public QObject
@@ -24,7 +26,37 @@ signals:
void failed();
};
class DelayedQmlIncubationController: public QQmlIncubationController {
// Do nothing.
// This ensures lazy loaders don't start blocking before onReload creates windows.
class QSGRenderLoop;
class QsIncubationController
: public QObject
, public QQmlIncubationController {
Q_OBJECT
public:
void initLoop();
void setIncubationMode(bool render);
void incubateLater();
protected:
void timerEvent(QTimerEvent* event) override;
public slots:
void incubate();
void animationStopped();
void updateIncubationTime();
protected:
void incubatingObjectCountChanged(int count) override;
private:
// QPointer did not work with forward declarations prior to 6.7
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
QPointer<QSGRenderLoop> renderLoop = nullptr;
#else
QSGRenderLoop* renderLoop = nullptr;
#endif
int incubationTime = 0;
int timerId = 0;
bool followRenderloop = false;
};
+4 -2
View File
@@ -3,12 +3,14 @@
#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
<< info.display;
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
>> info.display;
return stream;
}
+5
View File
@@ -3,12 +3,15 @@
#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;
QString display;
static InstanceInfo CURRENT; // NOLINT
};
@@ -32,6 +35,8 @@ namespace qs::crash {
struct CrashInfo {
int logFd = -1;
int traceFd = -1;
int infoFd = -1;
static CrashInfo INSTANCE; // NOLINT
};
-3
View File
@@ -82,9 +82,6 @@
/// > Notably, @@Variants does not corrently support asynchronous
/// > loading, meaning using it inside a LazyLoader will block similarly to not
/// > having a loader to start with.
///
/// > [!WARNING] LazyLoaders do not start loading before the first window is created,
/// > meaning if you create all windows inside of lazy loaders, none of them will ever load.
class LazyLoader: public Reloadable {
Q_OBJECT;
/// The fully loaded item if the loader is @@loading or @@active, or `null`
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <qlogging.h>
#include <qloggingcategory.h>
namespace qs::log {
void initLogCategoryLevel(const char* name, QtMsgType defaultLevel = QtDebugMsg);
}
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define QS_DECLARE_LOGGING_CATEGORY(name) \
namespace qslogcat { \
Q_DECLARE_LOGGING_CATEGORY(name); \
} \
const QLoggingCategory& name()
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define QS_LOGGING_CATEGORY(name, category, ...) \
namespace qslogcat { \
Q_LOGGING_CATEGORY(name, category __VA_OPT__(, __VA_ARGS__)); \
} \
const QLoggingCategory& name() { \
static auto* init = []() { \
qs::log::initLogCategoryLevel(category __VA_OPT__(, __VA_ARGS__)); \
return &qslogcat::name; \
}(); \
return (init) (); \
}
+127 -34
View File
@@ -27,20 +27,75 @@
#include <qtmetamacros.h>
#include <qtypes.h>
#include <sys/mman.h>
#ifdef __linux__
#include <sys/sendfile.h>
#include <sys/types.h>
#endif
#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);
namespace {
bool copyFileData(int sourceFd, int destFd, qint64 size) {
auto usize = static_cast<size_t>(size);
#ifdef __linux__
off_t offset = 0;
auto remaining = usize;
while (remaining > 0) {
auto r = sendfile(destFd, sourceFd, &offset, remaining);
if (r == -1) {
if (errno == EINTR) continue;
return false;
}
if (r == 0) break;
remaining -= static_cast<size_t>(r);
}
return true;
#else
std::array<char, 64 * 1024> buffer = {};
auto remaining = totalTarget;
while (remaining > 0) {
auto chunk = std::min(remaining, buffer.size());
auto r = ::read(sourceFd, buffer.data(), chunk);
if (r == -1) {
if (errno == EINTR) continue;
return false;
}
if (r == 0) break;
auto readBytes = static_cast<size_t>(r);
size_t written = 0;
while (written < readBytes) {
auto w = ::write(destFd, buffer.data() + written, readBytes - written);
if (w == -1) {
if (errno == EINTR) continue;
return false;
}
written += static_cast<size_t>(w);
}
remaining -= readBytes;
}
return true;
#endif
}
} // namespace
bool LogMessage::operator==(const LogMessage& other) const {
// note: not including time
@@ -182,17 +237,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 +297,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 +323,10 @@ void LogManager::init(
qCDebug(logLogging) << "Logger initialized.";
}
void initLogCategoryLevel(const char* name, QtMsgType defaultLevel) {
LogManager::instance()->defaultLevels.insert(QLatin1StringView(name), defaultLevel);
}
void LogManager::initFs() {
QMetaObject::invokeMethod(
&LogManager::instance()->threadProxy,
@@ -297,8 +367,12 @@ void ThreadLogging::init() {
if (logMfd != -1) {
this->file = new QFile();
this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle);
this->fileStream.setDevice(this->file);
if (this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle)) {
this->fileStream.setDevice(this->file);
} else {
qCCritical(logLogging) << "Failed to open early logging memfd.";
}
}
if (dlogMfd != -1) {
@@ -306,14 +380,19 @@ void ThreadLogging::init() {
this->detailedFile = new QFile();
// buffered by WriteBuffer
this->detailedFile->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle);
this->detailedWriter.setDevice(this->detailedFile);
if (this->detailedFile
->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle))
{
this->detailedWriter.setDevice(this->detailedFile);
if (!this->detailedWriter.writeHeader()) {
qCCritical(logLogging) << "Could not write header for detailed logs.";
this->detailedWriter.setDevice(nullptr);
delete this->detailedFile;
this->detailedFile = nullptr;
if (!this->detailedWriter.writeHeader()) {
qCCritical(logLogging) << "Could not write header for detailed logs.";
this->detailedWriter.setDevice(nullptr);
delete this->detailedFile;
this->detailedFile = nullptr;
}
} else {
qCCritical(logLogging) << "Failed to open early detailed logging memfd.";
}
}
@@ -336,7 +415,8 @@ void ThreadLogging::initFs() {
auto* runDir = QsPaths::instance()->instanceRunDir();
if (!runDir) {
qCCritical(logLogging
qCCritical(
logLogging
) << "Could not start filesystem logging as the runtime directory could not be created.";
return;
}
@@ -347,7 +427,8 @@ void ThreadLogging::initFs() {
auto* detailedFile = new QFile(detailedPath);
if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
qCCritical(logLogging
qCCritical(
logLogging
) << "Could not start filesystem logger as the log file could not be created:"
<< path;
delete file;
@@ -358,13 +439,14 @@ void ThreadLogging::initFs() {
// buffered by WriteBuffer
if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) {
qCCritical(logLogging
qCCritical(
logLogging
) << "Could not start detailed filesystem logger as the log file could not be created:"
<< detailedPath;
delete detailedFile;
detailedFile = nullptr;
} else {
auto lock = flock {
struct flock lock = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
@@ -386,7 +468,11 @@ void ThreadLogging::initFs() {
auto* oldFile = this->file;
if (oldFile) {
oldFile->seek(0);
sendfile(file->handle(), oldFile->handle(), nullptr, oldFile->size());
if (!copyFileData(oldFile->handle(), file->handle(), oldFile->size())) {
qCritical(logLogging) << "Failed to copy log from memfd with error code " << errno
<< qt_error_string(errno);
}
}
this->file = file;
@@ -398,7 +484,10 @@ void ThreadLogging::initFs() {
auto* oldFile = this->detailedFile;
if (oldFile) {
oldFile->seek(0);
sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size());
if (!copyFileData(oldFile->handle(), detailedFile->handle(), oldFile->size())) {
qCritical(logLogging) << "Failed to copy detailed log from memfd with error code " << errno
<< qt_error_string(errno);
}
}
crash::CrashInfo::INSTANCE.logFd = detailedFile->handle();
@@ -441,10 +530,14 @@ void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) {
this->fileStream << Qt::endl;
}
if (this->detailedWriter.write(msg)) {
this->detailedFile->flush();
} else if (this->detailedFile != nullptr) {
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
if (!this->detailedWriter.write(msg) || (this->detailedFile && !this->detailedFile->flush())) {
this->detailedWriter.setDevice(nullptr);
if (this->detailedFile) {
this->detailedFile->close();
this->detailedFile = nullptr;
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
}
}
}
@@ -717,11 +810,11 @@ bool EncodedLogReader::readVarInt(quint32* slot) {
if (!this->reader.skip(1)) return false;
*slot = qFromLittleEndian(n);
} else if ((bytes[1] != 0xff || bytes[2] != 0xff) && readLength >= 3) {
auto n = *reinterpret_cast<quint16*>(bytes.data() + 1);
auto n = *reinterpret_cast<quint16*>(bytes.data() + 1); // NOLINT
if (!this->reader.skip(3)) return false;
*slot = qFromLittleEndian(n);
} else if (readLength == 7) {
auto n = *reinterpret_cast<quint32*>(bytes.data() + 3);
auto n = *reinterpret_cast<quint32*>(bytes.data() + 3); // NOLINT
if (!this->reader.skip(7)) return false;
*slot = qFromLittleEndian(n);
} else return false;
@@ -857,7 +950,7 @@ bool LogReader::continueReading() {
}
void LogFollower::FcntlWaitThread::run() {
auto lock = flock {
struct flock lock = {
.l_type = F_RDLCK, // won't block other read locks when we take it
.l_whence = SEEK_SET,
.l_start = 0,
+7 -1
View File
@@ -2,6 +2,7 @@
#include <utility>
#include <qbytearrayview.h>
#include <qcontainerfwd.h>
#include <qdatetime.h>
#include <qfile.h>
@@ -12,7 +13,9 @@
#include <qobject.h>
#include <qtmetamacros.h>
Q_DECLARE_LOGGING_CATEGORY(logBare);
#include "logcat.hpp"
QS_DECLARE_LOGGING_CATEGORY(logBare);
namespace qs::log {
@@ -127,11 +130,14 @@ private:
QString mRulesString;
QList<qt_logging_registry::QLoggingRule>* rules = nullptr;
QtMsgType mDefaultLevel = QtWarningMsg;
QHash<QLatin1StringView, QtMsgType> defaultLevels;
QHash<const void*, CategoryFilter> sparseFilters;
QHash<QLatin1StringView, CategoryFilter> allFilters;
QTextStream stdoutStream;
LoggingThreadProxy threadProxy;
friend void initLogCategoryLevel(const char* name, QtMsgType defaultLevel);
};
bool readEncodedLogs(
+2 -1
View File
@@ -16,10 +16,11 @@
#include <qstringview.h>
#include <qtypes.h>
#include "logcat.hpp"
#include "logging_qtprivate.hpp"
namespace qs::log {
Q_DECLARE_LOGGING_CATEGORY(logLogging);
QS_DECLARE_LOGGING_CATEGORY(logLogging);
namespace qt_logging_registry {
+3 -1
View File
@@ -12,8 +12,10 @@
#include <qstringview.h>
#include <qtypes.h>
#include "logcat.hpp"
namespace qs::log {
Q_DECLARE_LOGGING_CATEGORY(logLogging);
QS_DECLARE_LOGGING_CATEGORY(logLogging);
namespace qt_logging_registry {
+2 -69
View File
@@ -1,81 +1,14 @@
#include "model.hpp"
#include <qabstractitemmodel.h>
#include <qbytearray.h>
#include <qhash.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
qint32 UntypedObjectModel::rowCount(const QModelIndex& parent) const {
if (parent != QModelIndex()) return 0;
return static_cast<qint32>(this->valuesList.length());
}
QVariant UntypedObjectModel::data(const QModelIndex& index, qint32 role) const {
if (role != Qt::UserRole) return QVariant();
return QVariant::fromValue(this->valuesList.at(index.row()));
}
QHash<int, QByteArray> UntypedObjectModel::roleNames() const {
return {{Qt::UserRole, "modelData"}};
}
void UntypedObjectModel::insertObject(QObject* object, qsizetype index) {
auto iindex = index == -1 ? this->valuesList.length() : index;
emit this->objectInsertedPre(object, iindex);
auto intIndex = static_cast<qint32>(iindex);
this->beginInsertRows(QModelIndex(), intIndex, intIndex);
this->valuesList.insert(iindex, object);
this->endInsertRows();
emit this->valuesChanged();
emit this->objectInsertedPost(object, iindex);
}
void UntypedObjectModel::removeAt(qsizetype index) {
auto* object = this->valuesList.at(index);
emit this->objectRemovedPre(object, index);
auto intIndex = static_cast<qint32>(index);
this->beginRemoveRows(QModelIndex(), intIndex, intIndex);
this->valuesList.removeAt(index);
this->endRemoveRows();
emit this->valuesChanged();
emit this->objectRemovedPost(object, index);
}
bool UntypedObjectModel::removeObject(const QObject* object) {
auto index = this->valuesList.indexOf(object);
if (index == -1) return false;
this->removeAt(index);
return true;
}
void UntypedObjectModel::diffUpdate(const QVector<QObject*>& newValues) {
for (qsizetype i = 0; i < this->valuesList.length();) {
if (newValues.contains(this->valuesList.at(i))) i++;
else this->removeAt(i);
}
qsizetype oi = 0;
for (auto* object: newValues) {
if (this->valuesList.length() == oi || this->valuesList.at(oi) != object) {
this->insertObject(object, oi);
}
oi++;
}
}
qsizetype UntypedObjectModel::indexOf(QObject* object) { return this->valuesList.indexOf(object); }
UntypedObjectModel* UntypedObjectModel::emptyInstance() {
static auto* instance = new UntypedObjectModel(nullptr); // NOLINT
static auto* instance = new ObjectModel<void>(nullptr);
return instance;
}
+73 -25
View File
@@ -2,7 +2,7 @@
#include <functional>
#include <bit>
#include <QtCore/qtmetamacros.h>
#include <qabstractitemmodel.h>
#include <qcontainerfwd.h>
#include <qobject.h>
@@ -49,14 +49,11 @@ class UntypedObjectModel: public QAbstractListModel {
public:
explicit UntypedObjectModel(QObject* parent): QAbstractListModel(parent) {}
[[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override;
[[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] QList<QObject*> values() const { return this->valuesList; };
void removeAt(qsizetype index);
[[nodiscard]] virtual QList<QObject*> values() = 0;
Q_INVOKABLE qsizetype indexOf(QObject* object);
Q_INVOKABLE virtual qsizetype indexOf(QObject* object) const = 0;
static UntypedObjectModel* emptyInstance();
@@ -71,15 +68,6 @@ signals:
/// Sent immediately after an object is removed from the list.
void objectRemovedPost(QObject* object, qsizetype index);
protected:
void insertObject(QObject* object, qsizetype index = -1);
bool removeObject(const QObject* object);
// Assumes only one instance of a specific value
void diffUpdate(const QVector<QObject*>& newValues);
QVector<QObject*> valuesList;
private:
static qsizetype valuesCount(QQmlListProperty<QObject>* property);
static QObject* valueAt(QQmlListProperty<QObject>* property, qsizetype index);
@@ -90,14 +78,20 @@ class ObjectModel: public UntypedObjectModel {
public:
explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {}
[[nodiscard]] QVector<T*>& valueList() { return *std::bit_cast<QVector<T*>*>(&this->valuesList); }
[[nodiscard]] const QVector<T*>& valueList() const {
return *std::bit_cast<const QVector<T*>*>(&this->valuesList);
}
[[nodiscard]] const QList<T*>& valueList() const { return this->mValuesList; }
[[nodiscard]] QList<T*>& valueList() { return this->mValuesList; }
void insertObject(T* object, qsizetype index = -1) {
this->UntypedObjectModel::insertObject(object, index);
auto iindex = index == -1 ? this->mValuesList.length() : index;
emit this->objectInsertedPre(object, iindex);
auto intIndex = static_cast<qint32>(iindex);
this->beginInsertRows(QModelIndex(), intIndex, intIndex);
this->mValuesList.insert(iindex, object);
this->endInsertRows();
emit this->valuesChanged();
emit this->objectInsertedPost(object, iindex);
}
void insertObjectSorted(T* object, const std::function<bool(T*, T*)>& compare) {
@@ -110,17 +104,71 @@ public:
}
auto idx = iter - list.begin();
this->UntypedObjectModel::insertObject(object, idx);
this->insertObject(object, idx);
}
void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); }
bool removeObject(const T* object) {
auto index = this->mValuesList.indexOf(object);
if (index == -1) return false;
this->removeAt(index);
return true;
}
void removeAt(qsizetype index) {
auto* object = this->mValuesList.at(index);
emit this->objectRemovedPre(object, index);
auto intIndex = static_cast<qint32>(index);
this->beginRemoveRows(QModelIndex(), intIndex, intIndex);
this->mValuesList.removeAt(index);
this->endRemoveRows();
emit this->valuesChanged();
emit this->objectRemovedPost(object, index);
}
// Assumes only one instance of a specific value
void diffUpdate(const QVector<T*>& newValues) {
this->UntypedObjectModel::diffUpdate(*std::bit_cast<const QVector<QObject*>*>(&newValues));
void diffUpdate(const QList<T*>& newValues) {
for (qsizetype i = 0; i < this->mValuesList.length();) {
if (newValues.contains(this->mValuesList.at(i))) i++;
else this->removeAt(i);
}
qsizetype oi = 0;
for (auto* object: newValues) {
if (this->mValuesList.length() == oi || this->mValuesList.at(oi) != object) {
this->insertObject(object, oi);
}
oi++;
}
}
static ObjectModel<T>* emptyInstance() {
return static_cast<ObjectModel<T>*>(UntypedObjectModel::emptyInstance());
}
[[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override {
if (parent != QModelIndex()) return 0;
return static_cast<qint32>(this->mValuesList.length());
}
[[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override {
if (role != Qt::UserRole) return QVariant();
// Values must be QObject derived, but we can't assert that here without breaking forward decls,
// so no static_cast.
return QVariant::fromValue(reinterpret_cast<QObject*>(this->mValuesList.at(index.row())));
}
qsizetype indexOf(QObject* object) const override {
return this->mValuesList.indexOf(reinterpret_cast<T*>(object));
}
[[nodiscard]] QList<QObject*> values() override {
return *reinterpret_cast<QList<QObject*>*>(&this->mValuesList);
}
private:
QList<T*> mValuesList;
};
-1
View File
@@ -21,7 +21,6 @@ headers = [
"model.hpp",
"elapsedtimer.hpp",
"desktopentry.hpp",
"objectrepeater.hpp",
"qsmenu.hpp",
"retainable.hpp",
"popupanchor.hpp",
-190
View File
@@ -1,190 +0,0 @@
#include "objectrepeater.hpp"
#include <utility>
#include <qabstractitemmodel.h>
#include <qcontainerfwd.h>
#include <qhash.h>
#include <qlogging.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
QVariant ObjectRepeater::model() const { return this->mModel; }
void ObjectRepeater::setModel(QVariant model) {
if (model == this->mModel) return;
if (this->itemModel != nullptr) {
QObject::disconnect(this->itemModel, nullptr, this, nullptr);
}
this->mModel = std::move(model);
emit this->modelChanged();
this->reloadElements();
}
void ObjectRepeater::onModelDestroyed() {
this->mModel.clear();
this->itemModel = nullptr;
emit this->modelChanged();
this->reloadElements();
}
QQmlComponent* ObjectRepeater::delegate() const { return this->mDelegate; }
void ObjectRepeater::setDelegate(QQmlComponent* delegate) {
if (delegate == this->mDelegate) return;
if (this->mDelegate != nullptr) {
QObject::disconnect(this->mDelegate, nullptr, this, nullptr);
}
this->mDelegate = delegate;
if (delegate != nullptr) {
QObject::connect(
this->mDelegate,
&QObject::destroyed,
this,
&ObjectRepeater::onDelegateDestroyed
);
}
emit this->delegateChanged();
this->reloadElements();
}
void ObjectRepeater::onDelegateDestroyed() {
this->mDelegate = nullptr;
emit this->delegateChanged();
this->reloadElements();
}
void ObjectRepeater::reloadElements() {
for (auto i = this->valuesList.length() - 1; i >= 0; i--) {
this->removeComponent(i);
}
if (this->mDelegate == nullptr || !this->mModel.isValid()) return;
if (this->mModel.canConvert<QAbstractItemModel*>()) {
auto* model = this->mModel.value<QAbstractItemModel*>();
this->itemModel = model;
this->insertModelElements(model, 0, model->rowCount() - 1); // -1 is fine
// clang-format off
QObject::connect(model, &QObject::destroyed, this, &ObjectRepeater::onModelDestroyed);
QObject::connect(model, &QAbstractItemModel::rowsInserted, this, &ObjectRepeater::onModelRowsInserted);
QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &ObjectRepeater::onModelRowsRemoved);
QObject::connect(model, &QAbstractItemModel::rowsMoved, this, &ObjectRepeater::onModelRowsMoved);
QObject::connect(model, &QAbstractItemModel::modelAboutToBeReset, this, &ObjectRepeater::onModelAboutToBeReset);
// clang-format on
} else if (this->mModel.canConvert<QQmlListReference>()) {
auto values = this->mModel.value<QQmlListReference>();
auto len = values.count();
for (auto i = 0; i != len; i++) {
this->insertComponent(i, {{"modelData", QVariant::fromValue(values.at(i))}});
}
} else if (this->mModel.canConvert<QVector<QVariant>>()) {
auto values = this->mModel.value<QVector<QVariant>>();
for (auto& value: values) {
this->insertComponent(this->valuesList.length(), {{"modelData", value}});
}
} else {
qCritical() << this
<< "Cannot create components as the model is not compatible:" << this->mModel;
}
}
void ObjectRepeater::insertModelElements(QAbstractItemModel* model, int first, int last) {
auto roles = model->roleNames();
auto roleDataVec = QVector<QModelRoleData>();
for (auto id: roles.keys()) {
roleDataVec.push_back(QModelRoleData(id));
}
auto values = QModelRoleDataSpan(roleDataVec);
auto props = QVariantMap();
for (auto i = first; i != last + 1; i++) {
auto index = model->index(i, 0);
model->multiData(index, values);
for (auto [id, name]: roles.asKeyValueRange()) {
props.insert(name, *values.dataForRole(id));
}
this->insertComponent(i, props);
props.clear();
}
}
void ObjectRepeater::onModelRowsInserted(const QModelIndex& parent, int first, int last) {
if (parent != QModelIndex()) return;
this->insertModelElements(this->itemModel, first, last);
}
void ObjectRepeater::onModelRowsRemoved(const QModelIndex& parent, int first, int last) {
if (parent != QModelIndex()) return;
for (auto i = last; i != first - 1; i--) {
this->removeComponent(i);
}
}
void ObjectRepeater::onModelRowsMoved(
const QModelIndex& sourceParent,
int sourceStart,
int sourceEnd,
const QModelIndex& destParent,
int destStart
) {
auto hasSource = sourceParent != QModelIndex();
auto hasDest = destParent != QModelIndex();
if (!hasSource && !hasDest) return;
if (hasSource) {
this->onModelRowsRemoved(sourceParent, sourceStart, sourceEnd);
}
if (hasDest) {
this->onModelRowsInserted(destParent, destStart, destStart + (sourceEnd - sourceStart));
}
}
void ObjectRepeater::onModelAboutToBeReset() {
auto last = static_cast<int>(this->valuesList.length() - 1);
this->onModelRowsRemoved(QModelIndex(), 0, last); // -1 is fine
}
void ObjectRepeater::insertComponent(qsizetype index, const QVariantMap& properties) {
auto* context = QQmlEngine::contextForObject(this);
auto* instance = this->mDelegate->createWithInitialProperties(properties, context);
if (instance == nullptr) {
qWarning().noquote() << this->mDelegate->errorString();
qWarning() << this << "failed to create object for model data" << properties;
} else {
QQmlEngine::setObjectOwnership(instance, QQmlEngine::CppOwnership);
instance->setParent(this);
}
this->insertObject(instance, index);
}
void ObjectRepeater::removeComponent(qsizetype index) {
auto* instance = this->valuesList.at(index);
this->removeAt(index);
delete instance;
}
-85
View File
@@ -1,85 +0,0 @@
#pragma once
#include <qabstractitemmodel.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
#include "model.hpp"
///! A Repeater / for loop / map for non Item derived objects.
/// > [!ERROR] Removed in favor of @@QtQml.Models.Instantiator
///
/// The ObjectRepeater creates instances of the provided delegate for every entry in the
/// given model, similarly to a @@QtQuick.Repeater but for non visual types.
class ObjectRepeater: public ObjectModel<QObject> {
Q_OBJECT;
/// The model providing data to the ObjectRepeater.
///
/// Currently accepted model types are `list<T>` lists, javascript arrays,
/// and [QAbstractListModel] derived models, though only one column will be repeated
/// from the latter.
///
/// Note: @@ObjectModel is a [QAbstractListModel] with a single column.
///
/// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html
Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged);
/// The delegate component to repeat.
///
/// The delegate is given the same properties as in a Repeater, except `index` which
/// is not currently implemented.
///
/// If the model is a `list<T>` or javascript array, a `modelData` property will be
/// exposed containing the entry from the model. If the model is a [QAbstractListModel],
/// the roles from the model will be exposed.
///
/// Note: @@ObjectModel has a single role named `modelData` for compatibility with normal lists.
///
/// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html
Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged);
Q_CLASSINFO("DefaultProperty", "delegate");
QML_ELEMENT;
QML_UNCREATABLE("ObjectRepeater has been removed in favor of QtQml.Models.Instantiator.");
public:
explicit ObjectRepeater(QObject* parent = nullptr): ObjectModel(parent) {}
[[nodiscard]] QVariant model() const;
void setModel(QVariant model);
[[nodiscard]] QQmlComponent* delegate() const;
void setDelegate(QQmlComponent* delegate);
signals:
void modelChanged();
void delegateChanged();
private slots:
void onDelegateDestroyed();
void onModelDestroyed();
void onModelRowsInserted(const QModelIndex& parent, int first, int last);
void onModelRowsRemoved(const QModelIndex& parent, int first, int last);
void onModelRowsMoved(
const QModelIndex& sourceParent,
int sourceStart,
int sourceEnd,
const QModelIndex& destParent,
int destStart
);
void onModelAboutToBeReset();
private:
void reloadElements();
void insertModelElements(QAbstractItemModel* model, int first, int last);
void insertComponent(qsizetype index, const QVariantMap& properties);
void removeComponent(qsizetype index);
QVariant mModel;
QAbstractItemModel* itemModel = nullptr;
QQmlComponent* mDelegate = nullptr;
};
+70 -19
View File
@@ -9,15 +9,17 @@
#include <qdir.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qpair.h>
#include <qstandardpaths.h>
#include <qtenvironmentvariables.h>
#include <qtversionchecks.h>
#include <unistd.h>
#include "instanceinfo.hpp"
#include "logcat.hpp"
namespace {
Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
QS_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
}
QsPaths* QsPaths::instance() {
@@ -25,12 +27,19 @@ QsPaths* QsPaths::instance() {
return instance;
}
void QsPaths::init(QString shellId, QString pathId, QString dataOverride, QString stateOverride) {
void QsPaths::init(
QString shellId,
QString pathId,
QString dataOverride,
QString stateOverride,
QString cacheOverride
) {
auto* instance = QsPaths::instance();
instance->shellId = std::move(shellId);
instance->pathId = std::move(pathId);
instance->shellDataOverride = std::move(dataOverride);
instance->shellStateOverride = std::move(stateOverride);
instance->shellCacheOverride = std::move(cacheOverride);
}
QDir QsPaths::crashDir(const QString& id) {
@@ -133,13 +142,41 @@ 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"));
auto* shellDir = this->shellRunDir();
if (!shellDir) {
qCCritical(logPaths
qCCritical(
logPaths
) << "Could not create by-id symlink as the shell runtime path could not be created.";
} else {
auto shellPath = shellDir->filePath(runDir->dirName());
@@ -287,9 +324,16 @@ QDir QsPaths::shellStateDir() {
QDir QsPaths::shellCacheDir() {
if (this->shellCacheState == DirState::Unknown) {
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
dir = QDir(dir.filePath("by-shell"));
dir = QDir(dir.filePath(this->shellId));
QDir dir;
if (this->shellCacheOverride.isEmpty()) {
dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
dir = QDir(dir.filePath("by-shell"));
dir = QDir(dir.filePath(this->shellId));
} else {
auto basedir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
dir = QDir(this->shellCacheOverride.replace("$BASE", basedir));
}
this->mShellCacheDir = dir;
qCDebug(logPaths) << "Initialized cache path:" << dir.path();
@@ -317,7 +361,7 @@ void QsPaths::createLock() {
return;
}
auto lock = flock {
struct flock lock = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
@@ -335,7 +379,8 @@ void QsPaths::createLock() {
qCDebug(logPaths) << "Created instance lock at" << path;
}
} else {
qCCritical(logPaths
qCCritical(
logPaths
) << "Could not create instance lock, as the instance runtime directory could not be created.";
}
}
@@ -344,7 +389,7 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD
auto file = QFile(QDir(path).filePath("instance.lock"));
if (!file.open(QFile::ReadOnly)) return false;
auto lock = flock {
struct flock lock = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
@@ -367,29 +412,35 @@ 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, const QString& display) {
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 (!display.isEmpty() && info.instance.display != display) {
qCDebug(logPaths) << "Skipped instance with mismatched display at" << path;
continue;
}
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);
}
+14 -2
View File
@@ -1,6 +1,7 @@
#pragma once
#include <qdatetime.h>
#include <qdir.h>
#include <qpair.h>
#include <qtypes.h>
#include "instanceinfo.hpp"
@@ -16,16 +17,24 @@ QDataStream& operator>>(QDataStream& stream, InstanceLockInfo& info);
class QsPaths {
public:
static QsPaths* instance();
static void init(QString shellId, QString pathId, QString dataOverride, QString stateOverride);
static void init(
QString shellId,
QString pathId,
QString dataOverride,
QString stateOverride,
QString cacheOverride
);
static QDir crashDir(const QString& id);
static QString basePath(const QString& id);
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, const QString& display);
QDir* baseRunDir();
QDir* shellRunDir();
QDir* shellVfsDir();
QDir* instanceRunDir();
void linkRunDir();
void linkPathDir();
@@ -46,9 +55,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;
@@ -60,4 +71,5 @@ private:
QString shellDataOverride;
QString shellStateOverride;
QString shellCacheOverride;
};
+13 -9
View File
@@ -28,7 +28,7 @@ void PopupAnchor::markClean() { this->lastState = this->state; }
void PopupAnchor::markDirty() { this->lastState.reset(); }
QWindow* PopupAnchor::backingWindow() const {
return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr;
return this->bProxyWindow ? this->bProxyWindow->backingWindow() : nullptr;
}
void PopupAnchor::setWindowInternal(QObject* window) {
@@ -36,14 +36,14 @@ void PopupAnchor::setWindowInternal(QObject* window) {
if (this->mWindow) {
QObject::disconnect(this->mWindow, nullptr, this, nullptr);
QObject::disconnect(this->mProxyWindow, nullptr, this, nullptr);
QObject::disconnect(this->bProxyWindow, nullptr, this, nullptr);
}
if (window) {
if (auto* proxy = qobject_cast<ProxyWindowBase*>(window)) {
this->mProxyWindow = proxy;
this->bProxyWindow = proxy;
} else if (auto* interface = qobject_cast<WindowInterface*>(window)) {
this->mProxyWindow = interface->proxyWindow();
this->bProxyWindow = interface->proxyWindow();
} else {
qWarning() << "Tried to set popup anchor window to" << window
<< "which is not a quickshell window.";
@@ -55,7 +55,7 @@ void PopupAnchor::setWindowInternal(QObject* window) {
QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed);
QObject::connect(
this->mProxyWindow,
this->bProxyWindow,
&ProxyWindowBase::backerVisibilityChanged,
this,
&PopupAnchor::backingWindowVisibilityChanged
@@ -70,7 +70,7 @@ void PopupAnchor::setWindowInternal(QObject* window) {
setnull:
if (this->mWindow) {
this->mWindow = nullptr;
this->mProxyWindow = nullptr;
this->bProxyWindow = nullptr;
emit this->windowChanged();
emit this->backingWindowVisibilityChanged();
@@ -100,7 +100,7 @@ void PopupAnchor::setItem(QQuickItem* item) {
void PopupAnchor::onWindowDestroyed() {
this->mWindow = nullptr;
this->mProxyWindow = nullptr;
this->bProxyWindow = nullptr;
emit this->windowChanged();
emit this->backingWindowVisibilityChanged();
}
@@ -186,14 +186,18 @@ void PopupAnchor::updatePlacement(const QPoint& anchorpoint, const QSize& size)
}
void PopupAnchor::updateAnchor() {
if (this->mItem && this->mProxyWindow) {
if (this->mItem && this->bProxyWindow) {
auto baseRect =
this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect();
auto rect = this->mProxyWindow->contentItem()->mapFromItem(
auto rect = this->bProxyWindow->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());
}
+6 -2
View File
@@ -6,6 +6,7 @@
#include <qnamespace.h>
#include <qobject.h>
#include <qpoint.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qquickitem.h>
#include <qsize.h>
@@ -139,7 +140,9 @@ public:
void markDirty();
[[nodiscard]] QObject* window() const { return this->mWindow; }
[[nodiscard]] ProxyWindowBase* proxyWindow() const { return this->mProxyWindow; }
[[nodiscard]] QBindable<ProxyWindowBase*> bindableProxyWindow() const {
return &this->bProxyWindow;
}
[[nodiscard]] QWindow* backingWindow() const;
void setWindowInternal(QObject* window);
void setWindow(QObject* window);
@@ -193,11 +196,12 @@ private slots:
private:
QObject* mWindow = nullptr;
QQuickItem* mItem = nullptr;
ProxyWindowBase* mProxyWindow = nullptr;
PopupAnchorState state;
Box mUserRect;
Margins mMargins;
std::optional<PopupAnchorState> lastState;
Q_OBJECT_BINDABLE_PROPERTY(PopupAnchor, ProxyWindowBase*, bProxyWindow);
};
class PopupPositioner {
+73 -6
View File
@@ -8,8 +8,10 @@
#include <qguiapplication.h>
#include <qicon.h>
#include <qjsengine.h>
#include <qlist.h>
#include <qlogging.h>
#include <qobject.h>
#include <qprocess.h>
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmllist.h>
@@ -21,11 +23,13 @@
#include <qwindowdefs.h>
#include <unistd.h>
#include "../io/processcore.hpp"
#include "generation.hpp"
#include "iconimageprovider.hpp"
#include "paths.hpp"
#include "qmlscreen.hpp"
#include "rootwrapper.hpp"
#include "scanenv.hpp"
QuickshellSettings::QuickshellSettings() {
QObject::connect(
@@ -177,12 +181,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 +211,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 +239,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 +268,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);
}
@@ -257,6 +314,16 @@ QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback)
return IconImageProvider::requestString(icon, "", fallback);
}
bool QuickshellGlobal::hasThemeIcon(const QString& icon) { return QIcon::hasThemeIcon(icon); }
bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor, const QStringList& features) {
return qs::scan::env::PreprocEnv::hasVersion(major, minor, features);
}
bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor) {
return QuickshellGlobal::hasVersion(major, minor, QStringList());
}
QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) {
auto* qsg = new QuickshellGlobal();
auto* generation = EngineGeneration::findEngineGeneration(engine);
+60 -2
View File
@@ -2,7 +2,9 @@
#include <qclipboard.h>
#include <qcontainerfwd.h>
#include <qhash.h>
#include <qjsengine.h>
#include <qlist.h>
#include <qobject.h>
#include <qqmlengine.h>
#include <qqmlintegration.h>
@@ -13,6 +15,8 @@
#include <qvariant.h>
#include <qwindowdefs.h>
#include "../io/processcore.hpp"
#include "doc.hpp"
#include "qmlscreen.hpp"
///! Accessor for some options under the Quickshell type.
@@ -104,6 +108,10 @@ class QuickshellGlobal: public QObject {
///
/// The root directory is the folder containing the entrypoint to your shell, often referred
/// to as `shell.qml`.
Q_PROPERTY(QString shellDir READ shellDir CONSTANT);
/// > [!WARNING] Deprecated: Renamed to @@shellDir for clarity.
Q_PROPERTY(QString configDir READ configDir CONSTANT);
/// > [!WARNING] Deprecated: Renamed to @@shellDir for consistency.
Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT);
/// Quickshell's working directory. Defaults to whereever quickshell was launched from.
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged);
@@ -119,18 +127,21 @@ class QuickshellGlobal: public QObject {
/// Usually `~/.local/share/quickshell/by-shell/<shell-id>`
///
/// Can be overridden using `//@ pragma DataDir $BASE/path` in the root qml file, where `$BASE`
/// corrosponds to `$XDG_DATA_HOME` (usually `~/.local/share`).
/// corresponds to `$XDG_DATA_HOME` (usually `~/.local/share`).
Q_PROPERTY(QString dataDir READ dataDir CONSTANT);
/// The per-shell state directory.
///
/// Usually `~/.local/state/quickshell/by-shell/<shell-id>`
///
/// Can be overridden using `//@ pragma StateDir $BASE/path` in the root qml file, where `$BASE`
/// corrosponds to `$XDG_STATE_HOME` (usually `~/.local/state`).
/// corresponds to `$XDG_STATE_HOME` (usually `~/.local/state`).
Q_PROPERTY(QString stateDir READ stateDir CONSTANT);
/// The per-shell cache directory.
///
/// Usually `~/.cache/quickshell/by-shell/<shell-id>`
///
/// Can be overridden using `//@ pragma CacheDir $BASE/path` in the root qml file, where `$BASE`
/// corresponds to `$XDG_CACHE_HOME` (usually `~/.cache`).
Q_PROPERTY(QString cacheDir READ cacheDir CONSTANT);
// clang-format on
QML_SINGLETON;
@@ -152,6 +163,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 +202,12 @@ 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);
/// Check if specified icon has an available icon in your icon theme
Q_INVOKABLE static bool hasThemeIcon(const QString& icon);
/// Equivalent to `${Quickshell.configDir}/${path}`
Q_INVOKABLE [[nodiscard]] QString shellPath(const QString& path) const;
/// > [!WARNING] Deprecated: Renamed to @@shellPath() for clarity.
Q_INVOKABLE [[nodiscard]] QString configPath(const QString& path) const;
/// Equivalent to `${Quickshell.dataDir}/${path}`
Q_INVOKABLE [[nodiscard]] QString dataPath(const QString& path) const;
/// Equivalent to `${Quickshell.stateDir}/${path}`
@@ -178,10 +219,27 @@ public:
///
/// The popup can also be blocked by setting `QS_NO_RELOAD_POPUP=1`.
Q_INVOKABLE void inhibitReloadPopup() { this->mInhibitReloadPopup = true; }
/// Check if Quickshell's version is at least `major.minor` and the listed
/// unreleased features are available. If Quickshell is newer than the given version
/// it is assumed that all unreleased features are present. The unreleased feature list
/// may be omitted.
///
/// > [!NOTE] You can feature gate code blocks using Quickshell's preprocessor which
/// > has the same function available.
/// >
/// > ```qml
/// > //@ if hasVersion(0, 3, ["feature"])
/// > ...
/// > //@ endif
/// > ```
Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor, const QStringList& features);
Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor);
void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; }
[[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; }
[[nodiscard]] QString shellDir() const;
[[nodiscard]] QString configDir() const;
[[nodiscard]] QString shellRoot() const;
[[nodiscard]] QString workingDirectory() const;
+47 -18
View File
@@ -1,6 +1,7 @@
#include "qsintercept.hpp"
#include <cstring>
#include <qdir.h>
#include <qhash.h>
#include <qiodevice.h>
#include <qlogging.h>
@@ -14,7 +15,9 @@
#include <qtypes.h>
#include <qurl.h>
Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg);
#include "logcat.hpp"
QS_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg);
QUrl QsUrlInterceptor::intercept(
const QUrl& originalUrl,
@@ -23,27 +26,44 @@ QUrl QsUrlInterceptor::intercept(
auto url = originalUrl;
if (url.scheme() == "root") {
url.setScheme("qsintercept");
url.setScheme("qs");
auto path = url.path();
if (path.startsWith('/')) path = path.sliced(1);
url.setPath(this->configRoot.filePath(path));
url.setPath("@/qs/" % path);
qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url;
}
// Some types such as Image take into account where they are loading from, and force
// asynchronous loading over a network. qsintercept is considered to be over a network.
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") {
// Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which
// case we want to keep the intercept, otherwise objects created from those paths
// will not be able to use singletons.
if (url.path().endsWith(".qml")) return url;
if (url.scheme() == "qs") {
auto path = url.path();
auto newUrl = url;
newUrl.setScheme("file");
qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl;
return newUrl;
// Our import path is on "qs:@/".
// We want to blackhole any import resolution outside of the config folder as it breaks Qt
// but NOT file lookups that might be on "qs:/" due to a missing "file:/" prefix.
if (path.startsWith("@/qs/")) {
path = this->configRoot.filePath(path.sliced(5));
} else if (!path.startsWith("/")) {
qCDebug(logQsIntercept) << "Blackholed import URL" << url;
return QUrl("qrc:/qs-blackhole");
}
// Some types such as Image take into account where they are loading from, and force
// asynchronous loading over a network. qs: is considered to be over a network.
// In those cases we want to return a file:// url so asynchronous loading is not forced.
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString) {
// Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which
// case we want to keep the intercept, otherwise objects created from those paths
// will not be able to use singletons.
if (path.endsWith(".qml")) return url;
auto newUrl = url;
newUrl.setScheme("file");
// above check asserts path starts with /qs/
newUrl.setPath(path);
qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl;
return newUrl;
}
}
return url;
@@ -65,10 +85,12 @@ qint64 QsInterceptDataReply::readData(char* data, qint64 maxSize) {
}
QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager(
const QDir& configRoot,
const QHash<QString, QString>& fileIntercepts,
QObject* parent
)
: QNetworkAccessManager(parent)
, configRoot(configRoot)
, fileIntercepts(fileIntercepts) {}
QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
@@ -77,19 +99,26 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
QIODevice* outgoingData
) {
auto url = req.url();
if (url.scheme() == "qsintercept") {
if (url.scheme() == "qs") {
auto path = url.path();
if (path.startsWith("@/qs/")) path = this->configRoot.filePath(path.sliced(5));
// otherwise pass through to fs
qCDebug(logQsIntercept) << "Got intercept for" << path << "contains"
<< this->fileIntercepts.value(path);
auto data = this->fileIntercepts.value(path);
if (data != nullptr) {
if (auto data = this->fileIntercepts.value(path); !data.isEmpty()) {
return new QsInterceptDataReply(data, this);
}
auto fileReq = req;
auto fileUrl = req.url();
fileUrl.setScheme("file");
fileUrl.setPath(path);
qCDebug(logQsIntercept) << "Passing through intercept" << url << "to" << fileUrl;
fileReq.setUrl(fileUrl);
return this->QNetworkAccessManager::createRequest(op, fileReq, outgoingData);
}
@@ -98,5 +127,5 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
}
QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) {
return new QsInterceptNetworkAccessManager(this->fileIntercepts, parent);
return new QsInterceptNetworkAccessManager(this->configRoot, this->fileIntercepts, parent);
}
+12 -3
View File
@@ -10,7 +10,9 @@
#include <qqmlnetworkaccessmanagerfactory.h>
#include <qurl.h>
Q_DECLARE_LOGGING_CATEGORY(logQsIntercept);
#include "logcat.hpp"
QS_DECLARE_LOGGING_CATEGORY(logQsIntercept);
class QsUrlInterceptor: public QQmlAbstractUrlInterceptor {
public:
@@ -43,6 +45,7 @@ class QsInterceptNetworkAccessManager: public QNetworkAccessManager {
public:
QsInterceptNetworkAccessManager(
const QDir& configRoot,
const QHash<QString, QString>& fileIntercepts,
QObject* parent = nullptr
);
@@ -55,15 +58,21 @@ protected:
) override;
private:
QDir configRoot;
const QHash<QString, QString>& fileIntercepts;
};
class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory {
public:
QsInterceptNetworkAccessManagerFactory(const QHash<QString, QString>& fileIntercepts)
: fileIntercepts(fileIntercepts) {}
QsInterceptNetworkAccessManagerFactory(
const QDir& configRoot,
const QHash<QString, QString>& fileIntercepts
)
: configRoot(configRoot)
, fileIntercepts(fileIntercepts) {}
QNetworkAccessManager* create(QObject* parent) override;
private:
QDir configRoot;
const QHash<QString, QString>& fileIntercepts;
};
+2 -2
View File
@@ -46,8 +46,8 @@ class QsMenuHandle: public QObject {
public:
explicit QsMenuHandle(QObject* parent): QObject(parent) {}
virtual void refHandle() {};
virtual void unrefHandle() {};
virtual void refHandle() {}
virtual void unrefHandle() {}
[[nodiscard]] virtual QsMenuEntry* menu() = 0;
+2
View File
@@ -22,6 +22,8 @@ PendingRegion::PendingRegion(QObject* parent): QObject(parent) {
}
void PendingRegion::setItem(QQuickItem* item) {
if (item == this->mItem) return;
if (this->mItem != nullptr) {
QObject::disconnect(this->mItem, nullptr, this, nullptr);
}
+16 -7
View File
@@ -126,12 +126,21 @@ QObject* Reloadable::getChildByReloadId(QObject* parent, const QString& reloadId
return nullptr;
}
void PostReloadHook::postReloadTree(QObject* root) {
for (auto* child: root->children()) {
PostReloadHook::postReloadTree(child);
}
if (auto* self = dynamic_cast<PostReloadHook*>(root)) {
self->onPostReload();
void PostReloadHook::componentComplete() {
auto* engineGeneration = EngineGeneration::findObjectGeneration(this);
if (!engineGeneration || engineGeneration->reloadComplete) this->postReload();
else {
// disconnected by EngineGeneration::postReload
QObject::connect(
engineGeneration,
&EngineGeneration::firePostReload,
this,
&PostReloadHook::postReload
);
}
}
void PostReloadHook::postReload() {
this->isPostReload = true;
this->onPostReload();
}
+16 -9
View File
@@ -57,7 +57,7 @@ public:
void reload(QObject* oldInstance = nullptr);
void classBegin() override {};
void classBegin() override {}
void componentComplete() override;
// Reload objects in the parent->child graph recursively.
@@ -119,16 +119,23 @@ private:
};
/// Hook that runs after the old widget tree is dropped during a reload.
class PostReloadHook {
class PostReloadHook
: public QObject
, public QQmlParserStatus {
Q_OBJECT;
QML_ANONYMOUS;
Q_INTERFACES(QQmlParserStatus);
public:
PostReloadHook() = default;
virtual ~PostReloadHook() = default;
PostReloadHook(PostReloadHook&&) = default;
PostReloadHook(const PostReloadHook&) = default;
PostReloadHook& operator=(PostReloadHook&&) = default;
PostReloadHook& operator=(const PostReloadHook&) = default;
PostReloadHook(QObject* parent = nullptr): QObject(parent) {}
void classBegin() override {}
void componentComplete() override;
virtual void onPostReload() = 0;
static void postReloadTree(QObject* root);
public slots:
void postReload();
protected:
bool isPostReload = false;
};
+59 -11
View File
@@ -4,6 +4,7 @@
#include <qdir.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qlogging.h>
#include <qobject.h>
#include <qqmlcomponent.h>
@@ -18,15 +19,26 @@
#include "instanceinfo.hpp"
#include "qmlglobal.hpp"
#include "scan.hpp"
#include "toolsupport.hpp"
RootWrapper::RootWrapper(QString rootPath, QString shellId)
: QObject(nullptr)
, rootPath(std::move(rootPath))
, shellId(std::move(shellId))
, originalWorkingDirectory(QDir::current().absolutePath()) {
// clang-format off
QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::watchFilesChanged, this, &RootWrapper::onWatchFilesChanged);
// clang-format on
QObject::connect(
QuickshellSettings::instance(),
&QuickshellSettings::watchFilesChanged,
this,
&RootWrapper::onWatchFilesChanged
);
QObject::connect(
&this->configDirWatcher,
&QFileSystemWatcher::directoryChanged,
this,
&RootWrapper::updateTooling
);
this->reloadGraph(true);
@@ -43,12 +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);
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
generation->wrapper = this;
qs::core::QmlToolingSupport::updateTooling(rootPath, scanner);
this->configDirWatcher.addPath(rootPath.path());
// todo: move into EngineGeneration
if (this->generation != nullptr) {
@@ -58,9 +71,36 @@ 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");
if (!scanner.scanErrors.isEmpty()) {
qCritical() << "Failed to load configuration";
QString errorString = "Failed to load configuration";
for (auto& error: scanner.scanErrors) {
const auto& file = error.file;
QString rel;
if (file.startsWith(rootPath.path() % '/')) {
rel = '@' % file.sliced(rootPath.path().length() + 1);
} else {
rel = file;
}
auto msg = " error in " % rel % '[' % QString::number(error.line) % ":0]: " % error.message;
errorString += '\n' % msg;
qCritical().noquote() << msg;
}
if (this->generation != nullptr && this->generation->qsgInstance != nullptr) {
emit this->generation->qsgInstance->reloadFailed(errorString);
}
return;
}
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
generation->wrapper = this;
QUrl url;
url.setScheme("qs");
url.setPath("@/qs/" % rootFile.fileName());
auto component = QQmlComponent(generation->engine, url);
if (!component.isReady()) {
@@ -69,7 +109,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 +207,9 @@ void RootWrapper::onWatchFilesChanged() {
}
void RootWrapper::onWatchedFilesChanged() { this->reloadGraph(false); }
void RootWrapper::updateTooling() {
if (!this->generation) return;
auto configDir = QFileInfo(this->rootPath).dir();
qs::core::QmlToolingSupport::updateTooling(configDir, this->generation->scanner);
}
+3
View File
@@ -1,5 +1,6 @@
#pragma once
#include <qfilesystemwatcher.h>
#include <qobject.h>
#include <qqmlengine.h>
#include <qtclasshelpermacros.h>
@@ -22,10 +23,12 @@ private slots:
void generationDestroyed();
void onWatchFilesChanged();
void onWatchedFilesChanged();
void updateTooling();
private:
QString rootPath;
QString shellId;
EngineGeneration* generation = nullptr;
QString originalWorkingDirectory;
QFileSystemWatcher configDirWatcher;
};
+183 -48
View File
@@ -1,9 +1,11 @@
#include "scan.hpp"
#include <cmath>
#include <utility>
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qjsengine.h>
#include <qjsonarray.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
@@ -12,54 +14,88 @@
#include <qloggingcategory.h>
#include <qpair.h>
#include <qstring.h>
#include <qstringliteral.h>
#include <qtextstream.h>
Q_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg);
#include "logcat.hpp"
#include "scanenv.hpp"
void QmlScanner::scanDir(const QString& path) {
if (this->scannedDirs.contains(path)) return;
this->scannedDirs.push_back(path);
QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg);
void QmlScanner::scanDir(const QDir& dir) {
if (this->scannedDirs.contains(dir)) return;
this->scannedDirs.push_back(dir);
const auto& path = dir.path();
qCDebug(logQmlScanner) << "Scanning directory" << path;
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") {
qCDebug(logQmlScanner
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 +103,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,35 +118,121 @@ bool QmlScanner::scanQmlFile(const QString& path) {
auto stream = QTextStream(&file);
auto imports = QVector<QString>();
bool singleton = false;
bool inHeader = true;
auto ifScopes = QVector<bool>();
bool sourceMasked = false;
int lineNum = 0;
QString overrideText;
bool isOverridden = false;
auto pragmaEngine = QJSEngine();
pragmaEngine.globalObject().setPrototype(
pragmaEngine.newQObject(new qs::scan::env::PreprocEnv())
);
auto postError = [&, this](QString error) {
this->scanErrors.append({.file = path, .message = std::move(error), .line = lineNum});
};
while (!stream.atEnd()) {
auto line = stream.readLine().trimmed();
if (!singleton && line == "pragma Singleton") {
qCDebug(logQmlScanner) << "Discovered singleton" << path;
singleton = true;
} else if (line.startsWith("import")) {
++lineNum;
bool hideMask = false;
auto rawLine = stream.readLine();
auto line = rawLine.trimmed();
if (!sourceMasked && inHeader) {
if (!singleton && line == "pragma Singleton") {
singleton = 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);
} else if (line.contains('{')) break;
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 (!internal && line == "//@ pragma Internal") {
internal = true;
} else if (line.contains('{')) {
inHeader = false;
}
}
if (line.startsWith("//@ if ")) {
auto code = line.sliced(7);
auto value = pragmaEngine.evaluate(code, path, 1234);
bool mask = true;
if (value.isError()) {
postError(QString("Evaluating if: %0").arg(value.toString()));
} else if (!value.isBool()) {
postError(QString("If expression \"%0\" is not a boolean").arg(value.toString()));
} else if (value.toBool()) {
mask = false;
}
if (!sourceMasked && mask) hideMask = true;
mask = sourceMasked || mask; // cant unmask if a nested if passes
ifScopes.append(mask);
if (mask) isOverridden = true;
sourceMasked = mask;
} else if (line.startsWith("//@ endif")) {
if (ifScopes.isEmpty()) {
postError("endif without matching if");
} else {
ifScopes.pop_back();
if (ifScopes.isEmpty()) sourceMasked = false;
else sourceMasked = ifScopes.last();
}
}
if (!hideMask && sourceMasked) overrideText.append("// MASKED: " % rawLine % '\n');
else overrideText.append(rawLine % '\n');
next:;
}
if (!ifScopes.isEmpty()) {
postError("unclosed preprocessor if block");
}
file.close();
if (isOverridden) {
this->fileIntercepts.insert(path, overrideText);
}
if (logQmlScanner().isDebugEnabled() && !imports.isEmpty()) {
qCDebug(logQmlScanner) << "Found imports" << imports;
}
auto currentdir = QDir(QFileInfo(path).canonicalPath());
auto currentdir = QDir(QFileInfo(path).absolutePath());
// the root can never be a singleton so it dosent matter if we skip it
this->scanDir(currentdir.path());
this->scanDir(currentdir);
for (auto& import: imports) {
QString ipath;
@@ -122,27 +244,39 @@ bool QmlScanner::scanQmlFile(const QString& path) {
ipath = currentdir.filePath(import);
}
auto cpath = QFileInfo(ipath).canonicalFilePath();
auto pathInfo = QFileInfo(ipath);
auto cpath = pathInfo.absoluteFilePath();
if (cpath.isEmpty()) {
if (!pathInfo.exists()) {
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 +288,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 +298,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) {
+16 -6
View File
@@ -6,7 +6,9 @@
#include <qloggingcategory.h>
#include <qvector.h>
Q_DECLARE_LOGGING_CATEGORY(logQmlScanner);
#include "logcat.hpp"
QS_DECLARE_LOGGING_CATEGORY(logQmlScanner);
// expects canonical paths
class QmlScanner {
@@ -14,17 +16,25 @@ public:
QmlScanner() = default;
QmlScanner(const QDir& rootPath): rootPath(rootPath) {}
void scanDir(const QString& path);
// returns if the file has a singleton
bool scanQmlFile(const QString& path);
void scanDir(const QDir& dir);
void scanQmlRoot(const QString& path);
QVector<QString> scannedDirs;
QVector<QDir> scannedDirs;
QVector<QString> scannedFiles;
QHash<QString, QString> fileIntercepts;
struct ScanError {
QString file;
QString message;
int line;
};
QVector<ScanError> scanErrors;
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);
};
+31
View File
@@ -0,0 +1,31 @@
#include "scanenv.hpp"
#include <qcontainerfwd.h>
#include <qtenvironmentvariables.h>
#include "build.hpp"
namespace qs::scan::env {
bool PreprocEnv::hasVersion(int major, int minor, const QStringList& features) {
if (QS_VERSION_MAJOR > major) return true;
if (QS_VERSION_MAJOR == major && QS_VERSION_MINOR > minor) return true;
auto availFeatures = QString(QS_UNRELEASED_FEATURES).split(';');
for (const auto& feature: features) {
if (!availFeatures.contains(feature)) return false;
}
return QS_VERSION_MAJOR == major && QS_VERSION_MINOR == minor;
}
QString PreprocEnv::env(const QString& variable) {
return qEnvironmentVariable(variable.toStdString().c_str());
}
bool PreprocEnv::isEnvSet(const QString& variable) {
return qEnvironmentVariableIsSet(variable.toStdString().c_str());
}
} // namespace qs::scan::env
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qtmetamacros.h>
namespace qs::scan::env {
class PreprocEnv: public QObject {
Q_OBJECT;
public:
Q_INVOKABLE static bool
hasVersion(int major, int minor, const QStringList& features = QStringList());
Q_INVOKABLE static QString env(const QString& variable);
Q_INVOKABLE static bool isEnvSet(const QString& variable);
};
} // namespace qs::scan::env
+4 -4
View File
@@ -19,7 +19,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
auto newIter = newValues.begin();
// TODO: cache this
auto getCmpKey = [&](const QVariant& v) {
auto getCmpKey = [this](const QVariant& v) {
if (v.canConvert<QVariantMap>()) {
auto vMap = v.value<QVariantMap>();
if (vMap.contains(this->cmpKey)) {
@@ -30,7 +30,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
return v;
};
auto variantCmp = [&](const QVariant& a, const QVariant& b) {
auto variantCmp = [&, this](const QVariant& a, const QVariant& b) {
if (!this->cmpKey.isEmpty()) return getCmpKey(a) == getCmpKey(b);
else return a == b;
};
@@ -72,8 +72,8 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
do {
++iter;
} while (iter != this->mValues.end()
&& std::find_if(newIter, newValues.end(), eqPredicate(*iter)) == newValues.end()
);
&& std::find_if(newIter, newValues.end(), eqPredicate(*iter))
== newValues.end());
auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
auto startIndex = static_cast<qint32>(std::distance(this->mValues.begin(), startIter));
-6
View File
@@ -51,9 +51,3 @@ void SingletonRegistry::onReload(SingletonRegistry* old) {
singleton->reload(old == nullptr ? nullptr : old->registry.value(url));
}
}
void SingletonRegistry::onPostReload() {
for (auto* singleton: this->registry.values()) {
PostReloadHook::postReloadTree(singleton);
}
}
-1
View File
@@ -26,7 +26,6 @@ public:
void registerSingleton(const QUrl& url, Singleton* singleton);
void onReload(SingletonRegistry* old);
void onPostReload();
private:
QHash<QUrl, Singleton*> registry;
+1 -1
View File
@@ -1,6 +1,6 @@
function (qs_test name)
add_executable(${name} ${ARGN})
target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window quickshell-ui)
target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window quickshell-ui quickshell-io)
add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $<TARGET_FILE:${name}>)
endfunction()
+1 -1
View File
@@ -115,7 +115,7 @@ void TestScriptModel::unique_data() {
void TestScriptModel::unique() {
QFETCH(const QString, oldstr);
QFETCH(const QString, newstr);
QFETCH(OpList, operations);
QFETCH(const OpList, operations);
auto strToVariantList = [](const QString& str) -> QVariantList {
QVariantList list;
+241
View File
@@ -0,0 +1,241 @@
#include "toolsupport.hpp"
#include <cerrno>
#include <fcntl.h>
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qqmlengine.h>
#include <qtenvironmentvariables.h>
#include "logcat.hpp"
#include "paths.hpp"
#include "scan.hpp"
namespace qs::core {
namespace {
QS_LOGGING_CATEGORY(logTooling, "quickshell.tooling", QtWarningMsg);
}
bool QmlToolingSupport::updateTooling(const QDir& configRoot, QmlScanner& scanner) {
auto* vfs = QsPaths::instance()->shellVfsDir();
if (!vfs) {
qCCritical(logTooling) << "Tooling dir could not be created";
return false;
}
if (!QmlToolingSupport::lockTooling()) {
return false;
}
if (!QmlToolingSupport::updateQmllsConfig(configRoot, false)) {
QDir(vfs->filePath("qs")).removeRecursively();
return false;
}
QmlToolingSupport::updateToolingFs(scanner, configRoot, vfs->filePath("qs"));
return true;
}
bool QmlToolingSupport::lockTooling() {
if (QmlToolingSupport::toolingLock) return true;
auto lockPath = QsPaths::instance()->shellVfsDir()->filePath("tooling.lock");
auto* file = new QFile(lockPath);
if (!file->open(QFile::WriteOnly)) {
qCCritical(logTooling) << "Could not open tooling lock for write";
return false;
}
struct flock lock = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET, // NOLINT (fcntl.h??)
.l_start = 0,
.l_len = 0,
.l_pid = 0,
};
if (fcntl(file->handle(), F_SETLK, &lock) == 0) {
qCInfo(logTooling) << "Acquired tooling support lock";
QmlToolingSupport::toolingLock = file;
return true;
} else if (errno == EACCES || errno == EAGAIN) {
qCInfo(logTooling) << "Tooling support locked by another instance";
return false;
} else {
qCCritical(logTooling).nospace() << "Could not create tooling lock at " << lockPath
<< " with error code " << errno << ": " << qt_error_string();
return false;
}
}
QString QmlToolingSupport::getQmllsConfig() {
static auto config = []() {
// We can't replicate the algorithm used to create the import path list as it can have distro
// specific patches, e.g. nixos.
auto importPaths = QQmlEngine().importPathList();
importPaths.removeIf([](const QString& path) { return path.startsWith("qrc:"); });
auto vfsPath = QsPaths::instance()->shellVfsDir()->path();
auto importPathsStr = importPaths.join(u':');
QString qmllsConfig;
auto print = QDebug(&qmllsConfig).nospace();
print << "[General]\nno-cmake-calls=true\nbuildDir=" << vfsPath
<< "\nimportPaths=" << importPathsStr << '\n';
return qmllsConfig;
}();
return config;
}
bool QmlToolingSupport::updateQmllsConfig(const QDir& configRoot, bool create) {
auto shellConfigPath = configRoot.filePath(".qmlls.ini");
auto vfsConfigPath = QsPaths::instance()->shellVfsDir()->filePath(".qmlls.ini");
auto shellFileInfo = QFileInfo(shellConfigPath);
if (!create && !shellFileInfo.exists() && !shellFileInfo.isSymLink()) {
if (QmlToolingSupport::toolingEnabled) {
qInfo() << "QML tooling support disabled";
QmlToolingSupport::toolingEnabled = false;
} else {
qCInfo(logTooling) << "Not enabling QML tooling support, qmlls.ini is missing at path"
<< shellConfigPath;
}
QFile::remove(vfsConfigPath);
return false;
}
auto vfsFile = QFile(vfsConfigPath);
if (!vfsFile.open(QFile::ReadWrite | QFile::Text)) {
qCCritical(logTooling) << "Failed to create qmlls config in vfs";
return false;
}
auto config = QmlToolingSupport::getQmllsConfig();
if (vfsFile.readAll() != config) {
if (!vfsFile.resize(0) || !vfsFile.write(config.toUtf8())) {
qCCritical(logTooling) << "Failed to write qmlls config in vfs";
return false;
}
qCDebug(logTooling) << "Wrote qmlls config in vfs";
}
if (!shellFileInfo.isSymLink() || shellFileInfo.symLinkTarget() != vfsConfigPath) {
QFile::remove(shellConfigPath);
if (!QFile::link(vfsConfigPath, shellConfigPath)) {
qCCritical(logTooling) << "Failed to create qmlls config symlink";
return false;
}
qCDebug(logTooling) << "Created qmlls config symlink";
}
if (!QmlToolingSupport::toolingEnabled) {
qInfo() << "QML tooling support enabled";
QmlToolingSupport::toolingEnabled = true;
}
return true;
}
void QmlToolingSupport::updateToolingFs(
QmlScanner& scanner,
const QDir& scanDir,
const QDir& linkDir
) {
QList<QString> files;
QSet<QString> subdirs;
auto scanPath = scanDir.path();
linkDir.mkpath(".");
for (auto& path: scanner.scannedFiles) {
if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue;
auto name = path.sliced(scanPath.length() + 1);
if (name.contains('/')) {
auto dirname = name.first(name.indexOf('/'));
subdirs.insert(dirname);
continue;
}
auto fileInfo = QFileInfo(path);
if (!fileInfo.isFile()) continue;
auto spath = linkDir.filePath(name);
auto sFileInfo = QFileInfo(spath);
if (!sFileInfo.isSymLink() || sFileInfo.symLinkTarget() != path) {
QFile::remove(spath);
if (QFile::link(path, spath)) {
qCDebug(logTooling) << "Created symlink to" << path << "at" << spath;
files.append(spath);
} else {
qCCritical(logTooling) << "Could not create symlink to" << path << "at" << spath;
}
} else {
files.append(spath);
}
}
for (auto [path, text]: scanner.fileIntercepts.asKeyValueRange()) {
if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue;
auto name = path.sliced(scanPath.length() + 1);
if (name.contains('/')) {
auto dirname = name.first(name.indexOf('/'));
subdirs.insert(dirname);
continue;
}
auto spath = linkDir.filePath(name);
auto file = QFile(spath);
if (!file.open(QFile::ReadWrite | QFile::Text)) {
qCCritical(logTooling) << "Failed to open injected file" << spath;
continue;
}
if (file.readAll() == text) {
files.append(spath);
continue;
}
if (file.resize(0) && file.write(text.toUtf8())) {
files.append(spath);
qCDebug(logTooling) << "Wrote injected file" << spath;
} else {
qCCritical(logTooling) << "Failed to write injected file" << spath;
}
}
for (auto& name: linkDir.entryList(QDir::Files | QDir::System)) { // System = broken symlinks
auto path = linkDir.filePath(name);
if (!files.contains(path)) {
if (QFile::remove(path)) qCDebug(logTooling) << "Removed old file at" << path;
else qCWarning(logTooling) << "Failed to remove old file at" << path;
}
}
for (const auto& subdir: subdirs) {
QmlToolingSupport::updateToolingFs(scanner, scanDir.filePath(subdir), linkDir.filePath(subdir));
}
}
} // namespace qs::core
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <qdir.h>
#include "scan.hpp"
namespace qs::core {
class QmlToolingSupport {
public:
static bool updateTooling(const QDir& configRoot, QmlScanner& scanner);
private:
static QString getQmllsConfig();
static bool lockTooling();
static bool updateQmllsConfig(const QDir& configRoot, bool create);
static void updateToolingFs(QmlScanner& scanner, const QDir& scanDir, const QDir& linkDir);
static inline bool toolingEnabled = false;
static inline QFile* toolingLock = nullptr;
};
} // namespace qs::core
+1 -32
View File
@@ -29,7 +29,7 @@ struct StringLiteral16 {
}
[[nodiscard]] constexpr const QChar* qCharPtr() const noexcept {
return std::bit_cast<const QChar*>(&this->value);
return std::bit_cast<const QChar*>(&this->value); // NOLINT
}
[[nodiscard]] Q_ALWAYS_INLINE operator QString() const noexcept {
@@ -251,37 +251,6 @@ public:
GuardedEmitBlocker block() { return GuardedEmitBlocker(&this->blocked); }
};
template <auto member, auto destroyedSlot, auto changedSignal>
class SimpleObjectHandleOps {
using Traits = MemberPointerTraits<decltype(member)>;
public:
static bool setObject(Traits::Class* parent, Traits::Type value) {
if (value == parent->*member) return false;
if (parent->*member != nullptr) {
QObject::disconnect(parent->*member, &QObject::destroyed, parent, destroyedSlot);
}
parent->*member = value;
if (value != nullptr) {
QObject::connect(parent->*member, &QObject::destroyed, parent, destroyedSlot);
}
if constexpr (changedSignal != nullptr) {
emit(parent->*changedSignal)();
}
return true;
}
};
template <auto member, auto destroyedSlot, auto changedSignal = nullptr>
bool setSimpleObjectHandle(auto* parent, auto* value) {
return SimpleObjectHandleOps<member, destroyedSlot, changedSignal>::setObject(parent, value);
}
template <auto methodPtr>
class MethodFunctor {
using PtrMeta = MemberPointerTraits<decltype(methodPtr)>;
+44 -5
View File
@@ -6,12 +6,51 @@ qt_add_library(quickshell-crash STATIC
qs_pch(quickshell-crash SET large)
find_package(PkgConfig REQUIRED)
pkg_check_modules(breakpad REQUIRED IMPORTED_TARGET breakpad)
# only need client?? take only includes from pkg config todo
target_link_libraries(quickshell-crash PRIVATE PkgConfig::breakpad -lbreakpad_client)
if (VENDOR_CPPTRACE)
message(STATUS "Vendoring cpptrace...")
include(FetchContent)
# For use without internet access see: https://cmake.org/cmake/help/latest/module/FetchContent.html#variable:FETCHCONTENT_SOURCE_DIR_%3CuppercaseName%3E
FetchContent_Declare(
cpptrace
GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git
GIT_TAG v1.0.4
)
set(CPPTRACE_UNWIND_WITH_LIBUNWIND TRUE)
FetchContent_MakeAvailable(cpptrace)
else ()
find_package(cpptrace REQUIRED)
# useful for cross after you have already checked cpptrace is built correctly
if (NOT DO_NOT_CHECK_CPPTRACE_USABILITY)
try_run(CPPTRACE_SIGNAL_SAFE_UNWIND CPPTRACE_SIGNAL_SAFE_UNWIND_COMP
SOURCE_FROM_CONTENT check.cxx "
#include <cpptrace/basic.hpp>
int main() {
return cpptrace::can_signal_safe_unwind() ? 0 : 1;
}
"
LOG_DESCRIPTION "Checking ${CPPTRACE_SIGNAL_SAFE_UNWIND}"
LINK_LIBRARIES cpptrace::cpptrace
COMPILE_OUTPUT_VARIABLE CPPTRACE_SIGNAL_SAFE_UNWIND_LOG
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
)
if (NOT CPPTRACE_SIGNAL_SAFE_UNWIND_COMP)
message(STATUS "${CPPTRACE_SIGNAL_SAFE_UNWIND_LOG}")
message(FATAL_ERROR "Failed to compile cpptrace signal safe unwind tester.")
endif()
if (NOT CPPTRACE_SIGNAL_SAFE_UNWIND EQUAL 0)
message(STATUS "Cpptrace signal safe unwind test exited with: ${CPPTRACE_SIGNAL_SAFE_UNWIND}")
message(FATAL_ERROR "Cpptrace was built without CPPTRACE_UNWIND_WITH_LIBUNWIND set to true. Enable libunwind support in the package or set VENDOR_CPPTRACE to true when building Quickshell.")
endif()
endif ()
endif ()
# quick linked for pch compat
target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt::Quick Qt::Widgets)
target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt::Quick Qt::Widgets cpptrace::cpptrace)
target_link_libraries(quickshell PRIVATE quickshell-crash)
+137 -102
View File
@@ -1,12 +1,12 @@
#include "handler.hpp"
#include <algorithm>
#include <array>
#include <csignal>
#include <cstdio>
#include <cstring>
#include <bits/types/sigset_t.h>
#include <breakpad/client/linux/handler/exception_handler.h>
#include <breakpad/client/linux/handler/minidump_descriptor.h>
#include <breakpad/common/linux/linux_libc_support.h>
#include <cpptrace/basic.hpp>
#include <cpptrace/forward.hpp>
#include <qdatastream.h>
#include <qfile.h>
#include <qlogging.h>
@@ -15,94 +15,64 @@
#include <unistd.h>
#include "../core/instanceinfo.hpp"
#include "../core/logcat.hpp"
extern char** environ; // NOLINT
using namespace google_breakpad;
namespace qs::crash {
namespace {
Q_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg);
}
struct CrashHandlerPrivate {
ExceptionHandler* exceptionHandler = nullptr;
int minidumpFd = -1;
int infoFd = -1;
QS_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg);
static bool minidumpCallback(const MinidumpDescriptor& descriptor, void* context, bool succeeded);
};
void writeEnvInt(char* buf, const char* name, int value) {
// NOLINTBEGIN (cppcoreguidelines-pro-bounds-pointer-arithmetic)
while (*name != '\0') *buf++ = *name++;
*buf++ = '=';
CrashHandler::CrashHandler(): d(new CrashHandlerPrivate()) {}
void CrashHandler::init() {
// MinidumpDescriptor has no move constructor and the copy constructor breaks fds.
auto createHandler = [this](const MinidumpDescriptor& desc) {
this->d->exceptionHandler = new ExceptionHandler(
desc,
nullptr,
&CrashHandlerPrivate::minidumpCallback,
this->d,
true,
-1
);
};
qCDebug(logCrashHandler) << "Starting crash handler...";
this->d->minidumpFd = memfd_create("quickshell:minidump", MFD_CLOEXEC);
if (this->d->minidumpFd == -1) {
qCCritical(logCrashHandler
) << "Failed to allocate minidump memfd, minidumps will be saved in the working directory.";
createHandler(MinidumpDescriptor("."));
} else {
qCDebug(logCrashHandler) << "Created memfd" << this->d->minidumpFd
<< "for holding possible minidumps.";
createHandler(MinidumpDescriptor(this->d->minidumpFd));
if (value < 0) {
*buf++ = '-';
value = -value;
}
qCInfo(logCrashHandler) << "Crash handler initialized.";
}
void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) {
this->d->infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC);
if (this->d->infoFd == -1) {
qCCritical(logCrashHandler
) << "Failed to allocate instance info memfd, crash recovery will not work.";
if (value == 0) {
*buf++ = '0';
*buf = '\0';
return;
}
QFile file;
file.open(this->d->infoFd, QFile::ReadWrite);
QDataStream ds(&file);
ds << info;
file.flush();
qCDebug(logCrashHandler) << "Stored instance info in memfd" << this->d->infoFd;
}
CrashHandler::~CrashHandler() {
delete this->d->exceptionHandler;
delete this->d;
}
bool CrashHandlerPrivate::minidumpCallback(
const MinidumpDescriptor& /*descriptor*/,
void* context,
bool /*success*/
) {
// A fork that just dies to ensure the coredump is caught by the system.
auto coredumpPid = fork();
if (coredumpPid == 0) {
return false;
auto* start = buf;
while (value > 0) {
*buf++ = static_cast<char>('0' + (value % 10));
value /= 10;
}
auto* self = static_cast<CrashHandlerPrivate*>(context);
*buf = '\0';
std::reverse(start, buf);
// NOLINTEND
}
void signalHandler(
int sig,
siginfo_t* /*info*/, // NOLINT (misc-include-cleaner)
void* /*context*/
) {
if (CrashInfo::INSTANCE.traceFd != -1) {
auto traceBuffer = std::array<cpptrace::frame_ptr, 1024>();
auto frameCount = cpptrace::safe_generate_raw_trace(traceBuffer.data(), traceBuffer.size(), 1);
for (size_t i = 0; i < static_cast<size_t>(frameCount); i++) {
auto frame = cpptrace::safe_object_frame();
cpptrace::get_safe_object_frame(traceBuffer[i], &frame);
write(CrashInfo::INSTANCE.traceFd, &frame, sizeof(cpptrace::safe_object_frame));
}
}
auto coredumpPid = fork();
if (coredumpPid == 0) {
raise(sig);
_exit(-1);
}
auto exe = std::array<char, 4096>();
if (readlink("/proc/self/exe", exe.data(), exe.size() - 1) == -1) {
@@ -115,17 +85,19 @@ bool CrashHandlerPrivate::minidumpCallback(
auto env = std::array<char*, 4096>();
auto envi = 0;
auto infoFd = dup(self->infoFd);
auto infoFdStr = std::array<char, 38>();
memcpy(infoFdStr.data(), "__QUICKSHELL_CRASH_INFO_FD=-1" /*\0*/, 30);
if (infoFd != -1) my_uitos(&infoFdStr[27], infoFd, 10);
// dup to remove CLOEXEC
auto infoFdStr = std::array<char, 48>();
writeEnvInt(infoFdStr.data(), "__QUICKSHELL_CRASH_INFO_FD", dup(CrashInfo::INSTANCE.infoFd));
env[envi++] = infoFdStr.data();
auto corePidStr = std::array<char, 39>();
memcpy(corePidStr.data(), "__QUICKSHELL_CRASH_DUMP_PID=-1" /*\0*/, 31);
if (coredumpPid != -1) my_uitos(&corePidStr[28], coredumpPid, 10);
auto corePidStr = std::array<char, 48>();
writeEnvInt(corePidStr.data(), "__QUICKSHELL_CRASH_DUMP_PID", coredumpPid);
env[envi++] = corePidStr.data();
auto sigStr = std::array<char, 48>();
writeEnvInt(sigStr.data(), "__QUICKSHELL_CRASH_SIGNAL", sig);
env[envi++] = sigStr.data();
auto populateEnv = [&]() {
auto senvi = 0;
while (envi != 4095) {
@@ -137,30 +109,18 @@ bool CrashHandlerPrivate::minidumpCallback(
env[envi] = nullptr;
};
sigset_t sigset;
sigemptyset(&sigset); // NOLINT (include)
sigprocmask(SIG_SETMASK, &sigset, nullptr); // NOLINT
auto pid = fork();
if (pid == -1) {
perror("Failed to fork and launch crash reporter.\n");
return false;
_exit(-1);
} else if (pid == 0) {
// dup to remove CLOEXEC
// if already -1 will return -1
auto dumpFd = dup(self->minidumpFd);
auto logFd = dup(CrashInfo::INSTANCE.logFd);
// allow up to 10 digits, which should never happen
auto dumpFdStr = std::array<char, 38>();
auto logFdStr = std::array<char, 37>();
memcpy(dumpFdStr.data(), "__QUICKSHELL_CRASH_DUMP_FD=-1" /*\0*/, 30);
memcpy(logFdStr.data(), "__QUICKSHELL_CRASH_LOG_FD=-1" /*\0*/, 29);
if (dumpFd != -1) my_uitos(&dumpFdStr[27], dumpFd, 10);
if (logFd != -1) my_uitos(&logFdStr[26], logFd, 10);
auto dumpFdStr = std::array<char, 48>();
auto logFdStr = std::array<char, 48>();
writeEnvInt(dumpFdStr.data(), "__QUICKSHELL_CRASH_DUMP_FD", dup(CrashInfo::INSTANCE.traceFd));
writeEnvInt(logFdStr.data(), "__QUICKSHELL_CRASH_LOG_FD", dup(CrashInfo::INSTANCE.logFd));
env[envi++] = dumpFdStr.data();
env[envi++] = logFdStr.data();
@@ -177,8 +137,83 @@ bool CrashHandlerPrivate::minidumpCallback(
perror("Failed to relaunch quickshell.\n");
_exit(-1);
}
}
return false; // should make sure it hits the system coredump handler
} // namespace
void CrashHandler::init() {
qCDebug(logCrashHandler) << "Starting crash handler...";
CrashInfo::INSTANCE.traceFd = memfd_create("quickshell:trace", MFD_CLOEXEC);
if (CrashInfo::INSTANCE.traceFd == -1) {
qCCritical(logCrashHandler) << "Failed to allocate trace memfd, stack traces will not be "
"available in crash reports.";
} else {
qCDebug(logCrashHandler) << "Created memfd" << CrashInfo::INSTANCE.traceFd
<< "for holding possible stack traces.";
}
{
// Preload anything dynamically linked to avoid malloc etc in the dynamic loader.
// See cpptrace documentation for more information.
auto buffer = std::array<cpptrace::frame_ptr, 10>();
cpptrace::safe_generate_raw_trace(buffer.data(), buffer.size());
auto frame = cpptrace::safe_object_frame();
cpptrace::get_safe_object_frame(buffer[0], &frame);
}
// NOLINTBEGIN (misc-include-cleaner)
// Set up alternate signal stack for stack overflow handling
auto ss = stack_t();
ss.ss_sp = new char[SIGSTKSZ];
;
ss.ss_size = SIGSTKSZ;
ss.ss_flags = 0;
sigaltstack(&ss, nullptr);
// Install signal handlers
struct sigaction sa {};
sa.sa_sigaction = &signalHandler;
sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESETHAND;
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, nullptr);
sigaction(SIGABRT, &sa, nullptr);
sigaction(SIGFPE, &sa, nullptr);
sigaction(SIGILL, &sa, nullptr);
sigaction(SIGBUS, &sa, nullptr);
sigaction(SIGTRAP, &sa, nullptr);
// NOLINTEND (misc-include-cleaner)
qCInfo(logCrashHandler) << "Crash handler initialized.";
}
void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) {
CrashInfo::INSTANCE.infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC);
if (CrashInfo::INSTANCE.infoFd == -1) {
qCCritical(
logCrashHandler
) << "Failed to allocate instance info memfd, crash recovery will not work.";
return;
}
QFile file;
if (!file.open(CrashInfo::INSTANCE.infoFd, QFile::ReadWrite)) {
qCCritical(
logCrashHandler
) << "Failed to open instance info memfd, crash recovery will not work.";
}
QDataStream ds(&file);
ds << info;
file.flush();
qCDebug(logCrashHandler) << "Stored instance info in memfd" << CrashInfo::INSTANCE.infoFd;
}
} // namespace qs::crash
+2 -11
View File
@@ -5,19 +5,10 @@
#include "../core/instanceinfo.hpp"
namespace qs::crash {
struct CrashHandlerPrivate;
class CrashHandler {
public:
explicit CrashHandler();
~CrashHandler();
Q_DISABLE_COPY_MOVE(CrashHandler);
void init();
void setRelaunchInfo(const RelaunchInfo& info);
private:
CrashHandlerPrivate* d;
static void init();
static void setRelaunchInfo(const RelaunchInfo& info);
};
} // namespace qs::crash
+4 -3
View File
@@ -66,7 +66,8 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid)
mainLayout->addSpacing(textHeight);
if (qtVersionMatches) {
mainLayout->addWidget(new QLabel("Please open a bug report for this issue via github or email.")
mainLayout->addWidget(
new QLabel("Please open a bug report for this issue via github or email.")
);
} else {
mainLayout->addWidget(new QLabel(
@@ -77,7 +78,7 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid)
mainLayout->addWidget(new ReportLabel(
"Github:",
"https://github.com/quickshell-mirror/quickshell/issues/new?template=crash.yml",
"https://github.com/quickshell-mirror/quickshell/issues/new?template=crash2.yml",
this
));
@@ -113,7 +114,7 @@ void CrashReporterGui::openFolder() {
void CrashReporterGui::openReportUrl() {
QDesktopServices::openUrl(
QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash.yml")
QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml")
);
}
+109 -25
View File
@@ -1,7 +1,10 @@
#include "main.hpp"
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <cpptrace/basic.hpp>
#include <cpptrace/formatting.hpp>
#include <qapplication.h>
#include <qconfig.h>
#include <qcoreapplication.h>
@@ -13,18 +16,23 @@
#include <qtenvironmentvariables.h>
#include <qtextstream.h>
#include <qtversion.h>
#include <qtypes.h>
#include <sys/sendfile.h>
#include <sys/types.h>
#include <unistd.h>
#include "../core/instanceinfo.hpp"
#include "../core/logcat.hpp"
#include "../core/logging.hpp"
#include "../core/logging_p.hpp"
#include "../core/paths.hpp"
#include "../core/ringbuf.hpp"
#include "build.hpp"
#include "interface.hpp"
namespace {
Q_LOGGING_CATEGORY(logCrashReporter, "quickshell.crashreporter", QtWarningMsg);
QS_LOGGING_CATEGORY(logCrashReporter, "quickshell.crashreporter", QtWarningMsg);
int tryDup(int fd, const QString& path) {
QFile sourceFile;
@@ -60,6 +68,76 @@ int tryDup(int fd, const QString& path) {
return 0;
}
QString readRecentLogs(int logFd, int maxLines, qint64 maxAgeSecs) {
QFile file;
if (!file.open(logFd, QFile::ReadOnly, QFile::AutoCloseHandle)) {
return QStringLiteral("(failed to open log fd)\n");
}
file.seek(0);
qs::log::EncodedLogReader reader;
reader.setDevice(&file);
bool readable = false;
quint8 logVersion = 0;
quint8 readerVersion = 0;
if (!reader.readHeader(&readable, &logVersion, &readerVersion) || !readable) {
return QStringLiteral("(failed to read log header)\n");
}
// Read all messages, keeping last maxLines in a ring buffer
auto tail = RingBuffer<qs::log::LogMessage>(maxLines);
qs::log::LogMessage message;
while (reader.read(&message)) {
tail.emplace(message);
}
if (tail.size() == 0) {
return QStringLiteral("(no logs)\n");
}
// Filter to only messages within maxAgeSecs of the newest message
auto cutoff = tail.at(0).time.addSecs(-maxAgeSecs);
QString result;
auto stream = QTextStream(&result);
for (auto i = tail.size() - 1; i != -1; i--) {
if (tail.at(i).time < cutoff) continue;
qs::log::LogMessage::formatMessage(stream, tail.at(i), false, true);
stream << '\n';
}
if (result.isEmpty()) {
return QStringLiteral("(no recent logs)\n");
}
return result;
}
cpptrace::stacktrace resolveStacktrace(int dumpFd) {
QFile sourceFile;
if (!sourceFile.open(dumpFd, QFile::ReadOnly, QFile::AutoCloseHandle)) {
qCCritical(logCrashReporter) << "Failed to open trace memfd.";
return {};
}
sourceFile.seek(0);
auto data = sourceFile.readAll();
auto frameCount = static_cast<size_t>(data.size()) / sizeof(cpptrace::safe_object_frame);
if (frameCount == 0) return {};
const auto* frames = reinterpret_cast<const cpptrace::safe_object_frame*>(data.constData());
cpptrace::object_trace objectTrace;
for (size_t i = 0; i < frameCount; i++) {
objectTrace.frames.push_back(frames[i].resolve()); // NOLINT
}
return objectTrace.resolve();
}
void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) {
qCDebug(logCrashReporter) << "Recording crash information at" << crashDir.path();
@@ -70,32 +148,25 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) {
}
auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt();
auto crashSignal = qEnvironmentVariable("__QUICKSHELL_CRASH_SIGNAL").toInt();
auto dumpFd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD").toInt();
auto logFd = qEnvironmentVariable("__QUICKSHELL_CRASH_LOG_FD").toInt();
qCDebug(logCrashReporter) << "Saving minidump from fd" << dumpFd;
auto dumpDupStatus = tryDup(dumpFd, crashDir.filePath("minidump.dmp.log"));
if (dumpDupStatus != 0) {
qCCritical(logCrashReporter) << "Failed to write minidump:" << dumpDupStatus;
}
qCDebug(logCrashReporter) << "Resolving stacktrace from fd" << dumpFd;
auto stacktrace = resolveStacktrace(dumpFd);
qCDebug(logCrashReporter) << "Saving log from fd" << logFd;
auto logDupStatus = tryDup(logFd, crashDir.filePath("log.qslog.log"));
qCDebug(logCrashReporter) << "Reading recent log lines from fd" << logFd;
auto logDupFd = dup(logFd);
auto recentLogs = readRecentLogs(logFd, 100, 10);
qCDebug(logCrashReporter) << "Saving log from fd" << logDupFd;
auto logDupStatus = tryDup(logDupFd, crashDir.filePath("log.qslog.log"));
if (logDupStatus != 0) {
qCCritical(logCrashReporter) << "Failed to save log:" << logDupStatus;
}
auto copyBinStatus = 0;
if (!DISTRIBUTOR_DEBUGINFO_AVAILABLE) {
qCDebug(logCrashReporter) << "Copying binary to crash folder";
if (!QFile(QCoreApplication::applicationFilePath()).copy(crashDir.filePath("executable.txt"))) {
copyBinStatus = 1;
qCCritical(logCrashReporter) << "Failed to copy binary.";
}
}
{
auto extraInfoFile = QFile(crashDir.filePath("info.txt"));
auto extraInfoFile = QFile(crashDir.filePath("report.txt"));
if (!extraInfoFile.open(QFile::WriteOnly)) {
qCCritical(logCrashReporter) << "Failed to open crash info file for writing.";
} else {
@@ -110,16 +181,12 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) {
stream << "\n===== Runtime Information =====\n";
stream << "Runtime Qt Version: " << qVersion() << '\n';
stream << "Signal: " << strsignal(crashSignal) << " (" << crashSignal << ")\n"; // NOLINT
stream << "Crashed process ID: " << crashProc << '\n';
stream << "Run ID: " << instance.instanceId << '\n';
stream << "Shell ID: " << instance.shellId << '\n';
stream << "Config Path: " << instance.configPath << '\n';
stream << "\n===== Report Integrity =====\n";
stream << "Minidump save status: " << dumpDupStatus << '\n';
stream << "Log save status: " << logDupStatus << '\n';
stream << "Binary copy status: " << copyBinStatus << '\n';
stream << "\n===== System Information =====\n\n";
stream << "/etc/os-release:";
auto osReleaseFile = QFile("/etc/os-release");
@@ -139,6 +206,18 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) {
stream << "FAILED TO OPEN\n";
}
stream << "\n===== Stacktrace =====\n";
if (stacktrace.empty()) {
stream << "(no trace available)\n";
} else {
auto formatter = cpptrace::formatter().header(std::string());
auto traceStr = formatter.format(stacktrace);
stream << QString::fromStdString(traceStr) << '\n';
}
stream << "\n===== Log Tail =====\n";
stream << recentLogs;
extraInfoFile.close();
}
}
@@ -151,7 +230,6 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) {
void qsCheckCrash(int argc, char** argv) {
auto fd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD");
if (fd.isEmpty()) return;
auto app = QApplication(argc, argv);
RelaunchInfo info;
@@ -161,7 +239,10 @@ void qsCheckCrash(int argc, char** argv) {
auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt();
QFile file;
file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle);
if (!file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle)) {
qFatal() << "Failed to open instance info fd.";
}
file.seek(0);
auto ds = QDataStream(&file);
@@ -176,6 +257,9 @@ void qsCheckCrash(int argc, char** argv) {
info.logRules
);
auto app = QApplication(argc, argv);
QApplication::setDesktopFileName("org.quickshell");
auto crashDir = QsPaths::crashDir(info.instance.instanceId);
qCInfo(logCrashReporter) << "Starting crash reporter...";
+11
View File
@@ -2,13 +2,24 @@ set_source_files_properties(org.freedesktop.DBus.Properties.xml PROPERTIES
CLASSNAME DBusPropertiesInterface
)
set_source_files_properties(org.freedesktop.DBus.ObjectManager.xml PROPERTIES
CLASSNAME DBusObjectManagerInterface
INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/dbus_objectmanager_types.hpp
)
qt_add_dbus_interface(DBUS_INTERFACES
org.freedesktop.DBus.Properties.xml
dbus_properties
)
qt_add_dbus_interface(DBUS_INTERFACES
org.freedesktop.DBus.ObjectManager.xml
dbus_objectmanager
)
qt_add_library(quickshell-dbus STATIC
properties.cpp
objectmanager.cpp
bus.cpp
${DBUS_INTERFACES}
)
+3 -1
View File
@@ -12,10 +12,12 @@
#include <qtypes.h>
#include <qvariant.h>
#include "../core/logcat.hpp"
namespace qs::dbus {
namespace {
Q_LOGGING_CATEGORY(logDbus, "quickshell.dbus", QtWarningMsg);
QS_LOGGING_CATEGORY(logDbus, "quickshell.dbus", QtWarningMsg);
}
void tryLaunchService(
Loaded 100 of 335 files, more files were not shown because too many files have changed in this diff. Show more