1
0
Fork 0

Compare commits

...

270 commits

Author SHA1 Message Date
outfoxxed 66b9917e70
service/mpris: trigger onPositionUpdated when seeking 2025-01-01 19:56:51 -08:00
outfoxxed 3a40174ed6
hyprland/surface: add hyprland surface opacity support 2025-01-01 17:45:23 -08:00
outfoxxed 08836ca1f3
core/scriptmodel: add expression model for unique lists 2024-12-27 04:16:12 -08:00
outfoxxed 2f194b7894
service/upower: track device additions/removals
Also ensures displayDevice is always present.
2024-12-20 15:58:44 -08:00
outfoxxed 611cd76abc
core/proxywindow: connect mScreen's destroy signal in all cases
Fixes edge case crashes when unplugging and replugging monitors.
2024-12-19 03:34:07 -08:00
outfoxxed 27840db7a6
service/mpris: don't send postTrackChanged unless trackChanged sent 2024-12-13 15:04:28 -08:00
outfoxxed a053373d57
core/qsmenu!: improve menu layout change UX
Exposes QsMenuOpener.children as an ObjectModel instead of a list to
allow smoother layout change handling in custom menu renderers.

Fixes QsMenuAnchor/platform menus closing whenever menu content changes.
2024-12-13 01:30:11 -08:00
outfoxxed 3fc1c914c7
lint: remove reinterpret_cast lint
Unhelpful.
2024-12-06 20:18:38 -08:00
outfoxxed be5e5fc4a5
lint: remove broken lint selection
The first line ended with a period instead of a comma, but both lints
were additionally unhelpful for quickshell.
2024-12-06 19:43:16 -08:00
outfoxxed ded3708762
io/fileview: correctly mark signals as signals in docs 2024-12-06 03:19:58 -08:00
outfoxxed 69d13967c9
io/fileview: add support for watching changes 2024-12-06 02:32:19 -08:00
outfoxxed ccf885081c
build: add progress bar to just lint-changed 2024-12-06 01:20:05 -08:00
outfoxxed 70be74e80d
io/fileview: add write support
FileView is now getting somewhat out of hand. The asynchronous parts
especially need to be redone, but this will work for now.
2024-12-06 01:18:31 -08:00
outfoxxed 2d05c7a89e
core/menu: correctly handle menu destruction while open 2024-12-05 19:46:08 -08:00
outfoxxed 26280b34b4
widgets/cliprect: fix typo in bottomRightRadius 2024-12-03 23:27:50 -08:00
outfoxxed af14a416c1
widgets/wrapper: update child geometry when implicit size changes
The implicit size update from a child item of a MarginWrapper
component triggers an implicit size update of the wrapper component,
but this does not necessarily result in the actual size of the wrapper
changing (e.g. when it is positioned by a layout).
2024-11-29 02:03:54 -08:00
outfoxxed cb05e9a327
core/reloader: fix incubator warnings 2024-11-29 01:37:14 -08:00
outfoxxed 8882f7ca50
core/proxywindow: fix ProxiedWindow proxy pointer after reload
Previously was not updated after reload, causing QsWindowAttached to
use the old window pointer after it had been freed.
2024-11-29 00:11:56 -08:00
outfoxxed 59298f6507
i3: fix Q_PROPERTY definitions for use with typegen
The regex currently can't handle line breaks
2024-11-27 23:47:28 -08:00
outfoxxed fd87be1355
widgets/cliprect: pass user input to contained items 2024-11-27 23:43:03 -08:00
outfoxxed b6a79fe99c
core/proxywindow: improve QsWindowAttached robustness
Can now track window parent window changes.
Added tests.
2024-11-27 23:30:38 -08:00
outfoxxed 539692bc11
service/tray: re-add Q_INVOKABLE to invokable functions
Was accidentally removed in the last refactor.
2024-11-26 14:48:54 -08:00
outfoxxed 87a57b7a2c
launch: don't try to write daemon exit from monitor process 2024-11-24 13:38:14 -08:00
outfoxxed e3d003e7ab
core/popupanchor: emit anchoring() before checking anchor props 2024-11-24 13:22:10 -08:00
outfoxxed 6f9993394a
hyprland/ipc: pad event argument list to given count
Fixes crash when assuming more arguments than given will be available,
and trailing ",".
2024-11-24 13:21:09 -08:00
Nydragon 31adcaac76
i3/sway: add support for the I3 and Sway IPC
sway: add urgent and focused dispatchers to workspaces

flake: add sway toggle

WIP sway: add monitor status

sway: handle multiple ipc events in one line

sway: reuse socket connection for dispatches & better command type handling

WIP sway: add associated monitor to a workspace

i3/sway: update to allow for i3 compatibility

i3/sway: manage setting the focused monitors

i3/sway: fix multi monitor crash

i3/sway: fix linting errors

i3/sway: update nix package flag naming to i3

i3/sway: add documentation, fix module.md and impl monitorFor

i3/sway: handle more workspace ipc events

i3/sway: fix review

i3/sway: fix crash due to newline breaking up an IPC message

i3/sway: handle broken messages by forwarding to the next magic sequence

i3/sway: break loop when buffer is empty

i3/sway: fix monitor focus & focused monitor signal not being emitted

i3/sway: use datastreams instead of qbytearrays for socket reading

i3/sway: fix lint issues

i3/sway: drop second socket connection, remove dispatch return value, recreate IPC connection on fatal error

i3/sway: handle run_command responses

i3/sway: remove reconnection on unknown event

i3/sway: fix formatting, lint & avoid writing to socket if connection is not open
2024-11-24 12:50:22 +01:00
Nydragon 84ce47b6d3
build: add justfile entry to lint only changed files 2024-11-24 12:50:10 +01:00
outfoxxed 2571766d3b
all: fix clang 18 lints 2024-11-24 03:36:04 -08:00
outfoxxed e957e88ccb
ci: run lints and test compile on arch 2024-11-24 02:09:41 -08:00
outfoxxed 9b409c0e38
ci: use qt6.7.2 from cached nixpkgs commit
No point in building qtdeclarative every ci run.
2024-11-23 05:39:27 -08:00
outfoxxed cb426973d7
ci: test compilation against supported qt version / compiler matrix 2024-11-23 05:20:51 -08:00
outfoxxed 57a5d8e1ed
core/reloader: wrap QQuickItem root nodes in a floating window
Useful for testing
2024-11-22 20:18:04 -08:00
outfoxxed c21df95087
core/reloader: do not require ShellRoot 2024-11-22 19:40:39 -08:00
outfoxxed 2996e40ff9
core/plugin: rename QuickshellPlugin to QsEnginePlugin
Fixes conflict with the autogenerated qml plugin of the same name.
2024-11-22 18:59:15 -08:00
outfoxxed afa1b6f88b
wayland/layershell: link to xdg-shell protocol codegen 2024-11-22 18:57:11 -08:00
outfoxxed a8901fde67
debug/lint: run lints on reload for visible windows 2024-11-22 17:55:45 -08:00
outfoxxed 8d63006bba
widgets/wrapper: fix default child not being assigned initially 2024-11-22 17:35:02 -08:00
outfoxxed 5b01ec032e
debug/linter: remove log messages
Printing qml items here breaks the qml engine in a way that made
transparent rectangles in the scene graph opaque.
2024-11-22 16:15:03 -08:00
outfoxxed f0aca2030e
core/proxywindow: notify for width/height changes after connect
Previously the content item would resize but w/h wouldn't be updated.
2024-11-22 15:39:39 -08:00
outfoxxed 5301227ec1
service/tray: fix compile on qt versions older than 6.8 2024-11-22 15:35:21 -08:00
outfoxxed ec143d6119
dbus/properties: remove non bindable based dbus property impl 2024-11-21 19:54:07 -08:00
outfoxxed 324fe9274d
all: remove unused dbus props and warnings for non-required ones 2024-11-21 19:45:45 -08:00
outfoxxed b43b4a06d0
service/tray: adopt bindable properties 2024-11-21 19:44:51 -08:00
outfoxxed 0e9e593078
dbus/properties: allow removing to/from wire transforms
Useful when properties are only read/written in one direction.
2024-11-21 19:14:06 -08:00
outfoxxed ac50767873
service/tray!: refactor qml bindings to StatusNotifierItem
Breaking: Dropped SystemTrayMenuWatcher.
2024-11-21 05:10:54 -08:00
outfoxxed f53e6fb515
dbus/dbusmenu: use bindable dbus properties 2024-11-21 04:06:24 -08:00
outfoxxed ff55ac874b
service/upower: adopt bindable properties 2024-11-21 03:40:53 -08:00
outfoxxed d4deb11216
dbus/properties: support data transformation/validation before store 2024-11-21 03:28:33 -08:00
outfoxxed a13c9d91b5
service/notifications: adopt bindable properties 2024-11-20 22:26:51 -08:00
outfoxxed abb900b7ff
service/mpris!: do not provide fallback track information
See the [!TIP] messages for more information.
2024-11-20 19:58:57 -08:00
outfoxxed e2ef7b7982
service/mpris: add isPlaying 2024-11-20 19:52:11 -08:00
outfoxxed db9e633197
service/mpris: adopt bindable properties 2024-11-20 19:31:40 -08:00
outfoxxed 1955deee74
dbus/properties: add QObjectBindableProperty based dbus property
Many times more lightweight than the original QObject-based one.
2024-11-20 19:22:23 -08:00
outfoxxed 4163713bc4
dbus/properties: decouple properties from AbstractDBusProperty
Importantly, this decouples properties from having to be QObjects,
allowing future property types to be much lighter.
2024-11-20 03:28:48 -08:00
outfoxxed dca75b7d6a
service/mpris: clarify trackinfo emit order and use QBindings 2024-11-20 00:52:47 -08:00
outfoxxed 8450543e09
service/mpris!: convert trackArtists from list<string> to string
Most people treat it as a string already, which breaks in Qt 6.8,
and I have not seen a meaningful multi-artist response.
2024-11-19 18:28:19 -08:00
outfoxxed dbaaf55eb6
core/popupwindow: remove parentWindow deprecation message
Was being falsely triggered by lints.
2024-11-19 17:20:53 -08:00
outfoxxed eb5a5b8b67
debug: run lints after window expose
Ensures items are at their final sizes before checking them,
fixing some false positives.
2024-11-19 15:58:55 -08:00
outfoxxed 6ceee06884
debug: add lint for zero sized items 2024-11-19 15:25:42 -08:00
outfoxxed 66b494d760
build: add qs_add_link_dependencies
Further inspection as to what libraries actually require which others
will be required before this can be used as a hint for shared builds.
2024-11-19 13:58:34 -08:00
outfoxxed f4066cb4ed
core/popupanchor: add anchoring signal for last second repositioning 2024-11-19 03:29:31 -08:00
outfoxxed ee93306312
widgets/wrapper: fix margin wrapper reactvity and margins
Fixed reactivity of the paren't actual size not working before child
had been assigned.

Fixed incorrect margins when actual size is less than implicit size.
2024-11-19 02:57:04 -08:00
outfoxxed 033e810871
widgets: add ClippingWrapperRectangle 2024-11-19 02:52:49 -08:00
outfoxxed 401ee4cec6
widgets: add wrapper components and managers 2024-11-19 02:02:55 -08:00
outfoxxed 79fca3cab8
docs: mention spirv-tools in BUILD.md 2024-11-17 21:38:56 -08:00
outfoxxed 36174854ad
services/tray: fix const lint in item 2024-11-17 19:28:07 -08:00
outfoxxed fdc13023b7
widgets: add ClippingRectangle 2024-11-17 19:27:59 -08:00
outfoxxed 68ba5005ce
core/icon: ability to specify a fallback or check if an icon exists 2024-11-17 14:46:34 -08:00
outfoxxed d2667369e1
core/qmlglobal: add shellRoot property 2024-11-17 01:49:27 -08:00
outfoxxed 7db3772641
core/generation: short circuit findObjectGeneration if only one exists 2024-11-17 01:46:49 -08:00
outfoxxed 29d31f5d3b
docs: add note that private qt headers are required for some libs 2024-11-17 01:36:25 -08:00
outfoxxed 36d1dbeb69
service/tray: report misbehaving tray hosts
I've debugged broken tray items that just end up being a bad host far
too many times.
2024-11-17 01:30:54 -08:00
outfoxxed 0445eee33a
io/process: support commands at file:// and root:// paths. 2024-11-17 00:47:22 -08:00
outfoxxed 60dfa67ec7
io/fileview: support zero-sized files (/proc) 2024-11-14 17:54:16 -08:00
outfoxxed 0dd19d4a18
core/proxywindow: remove blank frame when destroying window
Removes the blank frame caused by removing the content item from the
window. Fixes an issue with hyprland's window exit animations.
2024-11-12 04:35:42 -08:00
outfoxxed 2c0e46cedb
core/lazyloader: fix incubator UAF in forceCompletion
The incubator was deleted via onIncubationCompleted before it was done
working when completed via forceCompletion().
2024-11-12 03:23:59 -08:00
outfoxxed 74f371850d
launch: fix use after free of command options 2024-11-11 22:01:08 -08:00
outfoxxed b528be9426
all: fix gcc warnings 2024-11-05 13:31:24 -08:00
outfoxxed 92252c36a3
build: fix gcc 2024-11-05 12:14:45 -08:00
outfoxxed 7ffce72b31
all: optimize build 2024-11-05 04:15:17 -08:00
outfoxxed 1168879d6d
build: only install necessary qml module files 2024-11-04 14:13:37 -08:00
outfoxxed 2e18340995
build: allow specifying QML install dir 2024-11-04 13:42:21 -08:00
outfoxxed cdeec6ee83
all: use fully qualified type names in signals and invokables
Further fixes qmllint/qmlls
2024-11-01 21:10:21 -07:00
outfoxxed 98cdb87181
all: use UntypedObjectModel instead of ObjectModel in Q_PROPERTY
Fixes qmllint/qmlls type deduction for ObjectModels
2024-11-01 03:12:07 -07:00
outfoxxed 746b0e70d7
all: use fully qualified type names in Q_PROPERTY
Fixes type deduction issues with qmllint/qmlls.
2024-11-01 01:43:22 -07:00
outfoxxed a931adf033
all: add DEPENDENCIES entries to qml modules
Fixes some qmlls/qmllint issues.
2024-10-31 14:05:02 -07:00
outfoxxed 9980f8587e
window: generate qmltypes 2024-10-31 14:04:58 -07:00
outfoxxed 4e48c6eefb
all: refactor windows code out of core
There are still some links from core to window but its now separate
enough to fix PanelWindow in qml tooling.
2024-10-28 16:18:41 -07:00
outfoxxed 1adad9e822
build: avoid creating qs symlink in privileged directory 2024-10-18 14:57:13 -07:00
outfoxxed 4c2d7a7e41
crash: print warning messages for run/buildtime Qt version mismatch 2024-10-17 14:58:45 -07:00
outfoxxed 89d04f34a5
build: find waylandscanner and qtwaylandscanner from imported target
Removes the QTWAYLANDSCANNER env hack.
2024-10-16 00:08:17 -07:00
outfoxxed 23f59ec4c3
crash: add build configuration and distributor information
Also adds distributor to --version and
build configuration to --version --verbose
2024-10-16 00:08:15 -07:00
outfoxxed 8e40112d14
service/pipewire: ignore metadata updates with null keys
Fixes 
2024-10-06 00:57:19 -07:00
outfoxxed 3ed39b2a79
service/pipewire: fix metadata permission checks 2024-09-26 15:52:31 -07:00
outfoxxed fbaec141c0
service/pipewire: improve documentation 2024-09-24 01:59:38 -07:00
outfoxxed fdc78ae16f
service/pipewire: add a way to set preferred default nodes 2024-09-24 01:59:01 -07:00
outfoxxed f889f08901
service/pipewire: refactor defaults and metadata handling 2024-09-23 23:53:54 -07:00
outfoxxed 7f9762be53
service/pipewire: disconnect link tracker from registry on node destroy
Caused duplicate entries to be created due to double connection, which
then caused a crash.
2024-09-17 23:44:41 -07:00
outfoxxed 931aca5392
service/pipewire: don't use configured default devices
These don't appear to be intended for use by applications, only the
non configured ones.

This fixes the default device being unset on many computers and the
device being lost on actions like headphone unplug which replace it.
2024-09-17 23:04:06 -07:00
outfoxxed bd8978375b
core/icon: allow changing the icon theme 2024-09-17 14:21:34 -07:00
outfoxxed 7a283089b1
core/command: rename --instance to --id and --info to --show
Fixes conflicting short flags.
2024-09-17 14:04:54 -07:00
outfoxxed c57ac4b1f2
core/menu: disconnect menu before unref when changed 2024-09-15 16:06:20 -07:00
outfoxxed 08966f91c5
service/tray: always mark the root menu item as having children
Blueman doesn't for some reason. This causes
PlatformMenuEntry::display to crash after ::relayout created a QAction
instead of a QMenu.

Fixes 
2024-09-15 15:57:29 -07:00
outfoxxed 84e3f04f3c
service/tray: disconnect menu from handle on deletion
Fixes loaded being set to true after deleting the menu.
2024-09-15 15:32:01 -07:00
outfoxxed bdc9fe958b
service/tray: delete image pixmaps created with new[] using delete[] 2024-09-15 13:50:00 -07:00
outfoxxed 01f2be057e
widgets/iconimage: add typegen hints to alias properties 2024-09-15 02:23:46 -07:00
outfoxxed abe0327e67
widgets: add IconImage widget
Docs currently cannot be generated due to lack of qml parsing support
in typegen.
2024-09-14 03:10:44 -07:00
outfoxxed accdc59a1c
wayland/all: scale layers and popup anchors correctly
Layers now scale window size and exclusive zone to native
pixels. Popup anchors do the same.
2024-09-14 01:31:39 -07:00
outfoxxed 293341c9e1
core/reloader: ensure generation ptrs are removed on destroy
Broke things that used currentGeneration, and we shouldn't have a list
of dangling pointers anyway.
2024-09-13 04:18:52 -07:00
outfoxxed 5e2fb14551
io/ipchandler: add IpcHandler and qs msg
Also reworks the whole ipc system to use serialized variants.
2024-09-13 02:50:42 -07:00
outfoxxed 3690812919
core/log: fix encoding 29 second deltas (again)
Forgot the second if statement and didn't actually fix the bug last time.
2024-09-10 16:30:50 -07:00
outfoxxed 01f6331cb7
core/command: add --daemonize 2024-09-10 15:53:16 -07:00
outfoxxed 9d21a01153
core/command: add --no-duplicate 2024-09-10 14:35:30 -07:00
outfoxxed 47ec85ffef
core/command: make log --file positional
Also frees up -f for --follow.
2024-09-10 04:55:44 -07:00
outfoxxed 01deefe241
core/log: encode category log levels 2024-09-10 04:48:54 -07:00
outfoxxed a82fbf40c2
core/command: add log --follow 2024-09-10 03:31:49 -07:00
outfoxxed c78381f6d0
core/command: add --tail to log subcommand 2024-09-10 01:02:43 -07:00
outfoxxed f810c63ffc
core/command: allow log files to be specified w/ instance selectors 2024-09-10 00:32:39 -07:00
outfoxxed 19d74595d6
core/window: premultiply background colors
Apparently these are supposed to be premultiplied. Some docs would be nice.
2024-09-10 00:01:17 -07:00
outfoxxed 2c485e415d
nix: update lockfile to avoid mesa mismatches 2024-09-09 03:27:58 -07:00
outfoxxed 8cdb41317f
nix: modernize cmake options 2024-09-09 03:23:27 -07:00
outfoxxed 85be3861ce
io/fileview: add FileView 2024-09-09 03:15:16 -07:00
outfoxxed 3a1eec0ed5
core/log: fix sparse logs being on by default 2024-09-05 21:44:05 -07:00
outfoxxed 465d5402f2
crash: fix off-end read when copying environ array 2024-09-02 22:19:36 -07:00
outfoxxed 397476244c
x11/panelwindow: add option to disable Xinerama aware struts
Breaks bad WMs less.
2024-09-01 19:00:13 -07:00
outfoxxed 6cb7d894ab
x11/panelwindow: fix multi monitor struts 2024-09-01 18:26:54 -07:00
outfoxxed 95245cb6a5
x11/panelwindow: fix strut start/end, patch around awesome, resize all panels 2024-09-01 17:32:47 -07:00
outfoxxed 94e881e6b0
core!: refactor launch sequence
Also includes slight changes to the command syntax.
See --help for details.
2024-09-01 14:17:39 -07:00
outfoxxed da043e092a
core/ipc: add ipc server/client
Currently can only kill a remote instance.
2024-08-30 21:45:20 -07:00
outfoxxed 13b6eeaa22
core/reloader: null generation ref in reloadables on destruction
On the post-reload reloadable initialzation path, a timer is used to
delay reload(). This change fixes a UAF when switching generations
while that timer is running.
2024-08-30 16:29:59 -07:00
outfoxxed 3edb3f4efa
core/reloader: disconnect old generation before reloading
Previously the old generation was not disconnected, causing the root
wrapper's generation pointer to be nulled when pointing to the new
generation, leaving multiple shell versions running simultaneously.
2024-08-30 16:29:48 -07:00
outfoxxed 60349f1894
core: set application name to avoid bin name fallback 2024-08-29 14:43:25 -07:00
outfoxxed 77c5a2d569
build: add "qs" as a symlink to the "quickshell" binary 2024-08-29 14:11:40 -07:00
outfoxxed f6ad617b67
service/pipewire: ignore insignificant device volume changes
Fixes devices getting stuck in a "waiting for volume change
acknowledgement" state forever.
2024-08-29 13:21:07 -07:00
outfoxxed a116f39c63
core/desktopentry: prioritize fallback keys over mismatched keys
The fallback key will now be selected when there isn't a more specific
key to select, instead of the first key.
2024-08-28 22:05:21 -07:00
outfoxxed af29bc277e
core: add by-pid symlinks to instance runtime paths 2024-08-28 17:53:39 -07:00
outfoxxed 9967e2e03b
core: fix UAF when calling Qt.quit or Qt.exit
A pointer to the last generation had shutdown() called on it after deletion.
2024-08-28 15:30:02 -07:00
outfoxxed e327d6750d
build: fix -DCRASH_REPORTER=OFF 2024-08-28 11:32:14 -07:00
outfoxxed 79b22af093
service/pipewire: avoid overloading devices with volume changes
Wait until in-flight changes have been responded to before sending more.
2024-08-27 20:34:11 -07:00
outfoxxed c60871a7fb
service/pipewire: set device node volumes with device object
Fixes discrepancies between pulse and qs volumes, and volumes not
persisting across reboot or vt switches.
2024-08-27 01:34:34 -07:00
Nydragon b40d4147e0
build: add opt-in installation of QML lib
Override the package with `withQMLLib = true;` to enable lib
installation, alternatively add `-DINSTALL_QML_LIB=ON` to your cmake
build command.

Co-authored-by: a-usr <81042605+a-usr@users.noreply.github.com>
2024-08-25 22:59:41 +02:00
outfoxxed f95e7dbaf6
hyprland/focus_grab: wait for surface creation if null
Fixes an occasional crash with QWaylandWindow::surface() returning null.
2024-08-20 16:41:04 -07:00
outfoxxed fe1d15e8f6
crash: add crash reporter 2024-08-20 00:55:07 -07:00
outfoxxed 5040f3796c
core/reloader: delay post-reload reload hooks
Ensures onReload runs after Component.onCompleted.
2024-08-18 19:54:36 -07:00
outfoxxed 5a038f085d
service/mpris: support trackids in object path form
Chromium reports trackids as object paths, which caused us to fall
back to Seek, which is also entirely broken on chromium.
2024-08-18 13:41:16 -07:00
outfoxxed e223408143
service/mpris: fix display position when paused 2024-08-18 13:07:52 -07:00
outfoxxed f89c504b55
core/menu: opening platform menus w/o QApplication no longer crashes
An error is displayed instead.
2024-08-16 16:47:50 -07:00
outfoxxed 1d2bf5d7b4
core/clock: fix behavior with odd time changes 2024-08-16 02:35:03 -07:00
outfoxxed 815867c178
x11/panelwindow: fix multi monitor
Previously attached panels to the virtual desktop geometry instead of
the screen geometry.
2024-08-15 18:46:18 -07:00
outfoxxed 22c397bbb0
x11/panelwindow: respect exclusive zones per layer 2024-08-15 17:15:30 -07:00
outfoxxed 23cd6cd9e1
x11/panelwindow: set _NET_WM_DESKTOP to stay on all desktops 2024-08-15 17:14:00 -07:00
outfoxxed 683d92a05f
core/command: add --version 2024-08-10 01:59:40 -07:00
outfoxxed 14852700cb
core/log: ensure malformed logs cannot overflow ring buffer 2024-08-10 01:40:51 -07:00
outfoxxed 5f4d7f89db
core/log: fix log corruption with messages at 29 second deltas
29, or 0x1d is used as a marker to mean the log level and time delta
cannot fit in a single byte, and the time delta will be a varint
following the current byte.

Prior to this commit, 29 second deltas would be written as 0x1d
instead of 0x1d1d, which the parser interpreted as a hint to read the
next byte, causing the parser to become offset by one byte and all
following logs to be potentially corrupt.
2024-08-10 01:35:52 -07:00
outfoxxed 53b8f1ee0b
core/log: add read-log --no-time 2024-08-09 23:58:30 -07:00
outfoxxed c2b4610acb
core/log: add read-log --filter 2024-08-09 23:45:46 -07:00
outfoxxed 0fc98652a8
core/log: create fully detailed logs by default
The .qslog logs now log messages for quickshell* by default.
2024-08-09 20:24:17 -07:00
outfoxxed 291179ede2
core/command: rewrite command parser with CLI11 2024-08-09 19:25:18 -07:00
outfoxxed bdbf5b9af9
core/log: add custom log encoder for smaller log storage
Will be used to store more detailed logs in the future without using
as much disk space.
2024-08-09 14:43:18 -07:00
outfoxxed 8364e94d26
core/log: capture early logs in fs logger 2024-08-07 15:53:11 -07:00
outfoxxed 7c7326ec52
core/log: add timestamps to log files 2024-08-07 13:40:37 -07:00
outfoxxed 38ba3fff24
core/popupanchor: pick flip direction based on available width 2024-08-06 22:24:31 -07:00
outfoxxed 6bf4826ae7
core/log: add filesystem logger 2024-08-02 21:37:52 -07:00
outfoxxed 46f48f2f87
core/log: add fancy logger 2024-08-02 18:52:05 -07:00
outfoxxed 533b389742
nix: build with split debuginfo in release mode 2024-08-02 13:56:30 -07:00
outfoxxed d582bb7b57
core: add per-config shell id
Will be useful for future functionality such as IPC and caching.
2024-08-02 02:09:55 -07:00
outfoxxed 79b2fea52e
core/util: fix MemberMetadata compile on gcc 2024-08-02 01:32:12 -07:00
outfoxxed 2c87cc3803
core: stop using the simple animation driver by default 2024-08-01 21:47:18 -07:00
outfoxxed cb2862eca9
wayland/toplevel_management: add ToplevelManager.activeToplevel 2024-07-31 23:10:08 -07:00
outfoxxed 9555b201fe
core/clock: fix instability causing timer to fire multiple times
If the signal was fired slightly before the scheduled time, it would
schedule itself again a couple ms in the future.
2024-07-31 23:09:39 -07:00
outfoxxed a4903eaefc
core/clock: fix breakage at midnight
The difference between 23:59 and 00:00 is -23:59, not 00:01.
2024-07-31 01:51:53 -07:00
outfoxxed 76744c903a
core/clock: add SystemClock 2024-07-30 23:24:54 -07:00
outfoxxed ba1e535f9c
core/util: add experimental member macros
An experiment that should reduce boilerplate for properties that just
access a backing value. Code also exists for using it as an interface
for other properties as well, but isn't currently in use.
2024-07-30 20:23:57 -07:00
outfoxxed 8873a06962
service/notifications: use DROP_EMIT_SET for notification properties 2024-07-30 12:20:39 -07:00
outfoxxed 3a8e67e8ab
core/util: move DropEmitter to utils and add generic accessor macros 2024-07-30 12:19:59 -07:00
outfoxxed abc0201f6e
service/mpris: add uniqueId property to detect track changes
Also emits all property changes after trackChanged
2024-07-29 01:34:44 -07:00
outfoxxed d9f66e63a3
service/upower!: divide percentage by 100
Brings range back to the expected 0-1 instead of 0-100.
2024-07-28 20:28:45 -07:00
outfoxxed 18563b1273
wayland/popupanchor: fix anchor state breaking show after reposition
If the popup was hidden and reposition was called to update qt's
initial positioning properties it would be cancelled by the dirty
marker being unset. This includes if the popup is shown or not into
its dirty state.
2024-07-27 02:28:21 -07:00
outfoxxed 4b2e569e94
core/types: allow implicit conversion from point to box 2024-07-26 10:06:56 -07:00
outfoxxed 58c3718287
core/types: add implicit coversion from rect to box 2024-07-26 00:55:42 -07:00
outfoxxed 6b9b1fcb53
core/menu: add QsMenuAnchor for more control of platform menus 2024-07-25 20:44:26 -07:00
outfoxxed 54350277be
core/menu: add handle support to QsMenuOpener + add handle to tray 2024-07-25 02:51:17 -07:00
outfoxxed acdbe73c10
dbus/dbusmenu: separate menu handles from status notifier items
No api changes yet.
2024-07-25 01:32:05 -07:00
outfoxxed a71a6fb3ac
core/popupanchor: fix flip with opposite anchors and gravity
Flips into the anchor rect instead of over it when anchors and gravity
are opposite.
2024-07-24 01:36:51 -07:00
outfoxxed 60388f10ca
core/popupanchor: reposition on popup size change 2024-07-24 00:44:42 -07:00
outfoxxed ebfa8ec448
core/popupanchor: rework popup anchoring and add PopupAnchor 2024-07-23 22:12:27 -07:00
outfoxxed 14910b1b60
docs: mention member reference syntax in CONTRIBUTING 2024-07-21 17:44:09 -07:00
outfoxxed a9e4720fae
docs: use new member reference shorthand 2024-07-21 17:41:49 -07:00
outfoxxed dfcf533424
core/window!: rename QSWindow to QsWindow 2024-07-21 16:15:11 -07:00
outfoxxed aa3f7daea2
wayland/platformmenu: fix flipped positions and submenu y positions 2024-07-19 02:55:38 -07:00
outfoxxed 6367b56f55
core/window: fix attached property prior to backer creation 2024-07-18 01:57:40 -07:00
outfoxxed e48af44607
core/window: add QsWindow attached object to contained Items 2024-07-17 20:54:29 -07:00
outfoxxed d1c33d48cd
docs: explain type reference shorthand in CONTRIBUTING 2024-07-14 16:22:01 -07:00
outfoxxed e9cacbd92d
all: use type/prop shorthand in docs 2024-07-14 16:17:51 -07:00
outfoxxed c4cc662bcc
core/objectmodel: fix objectInserted signal indexes 2024-07-12 22:52:40 -07:00
outfoxxed e23923d9a2
service/notifications: make notifications Retainable 2024-07-12 21:25:46 -07:00
outfoxxed 609834d8f2
core/retainable: add Retainable and RetainableLock 2024-07-12 21:21:35 -07:00
outfoxxed 7c5632ef5f
service/upower: start upower dbus service if inactive 2024-07-12 20:16:10 -07:00
outfoxxed d630cc7f76
service/notifications: add notifications service 2024-07-12 00:50:00 -07:00
outfoxxed 79cbfba48a
wayland/layershell: add warning that exclusive focus is not a lock
Apparently this needed to be said.
2024-07-11 22:32:21 -07:00
outfoxxed c758421af6
core/reloader: fix Reloadable::onReload being called multiple times
onReload was called multiple times due to Reloadable::reloadRecursive
calling onReload instead of reload, which didn't set reloadComplete.
This allowed the componentComplete fallback to call reload later.
2024-07-11 01:43:54 -07:00
outfoxxed 49b309247d
all: fix formatting 2024-07-11 00:16:44 -07:00
outfoxxed bb33c9a0c4
core/global: add Quickshell.iconPath
Replaces "image://icon/" in user facing code.
2024-07-11 00:09:34 -07:00
Ben 24f54f579f service/upower: add upower service 2024-07-10 14:21:34 -04:00
outfoxxed 497c9c4e50
core/window: ensure items are polished before setting window visible
Hacks around a bug in layouts that commonly results in popups being
wrongly sized for at least a frame.
2024-07-10 03:44:55 -07:00
outfoxxed db23c0264a
core/desktopentry: paper over id casing issues 2024-07-08 15:37:49 -07:00
outfoxxed fdbb490537
service/tray: fix crash when display is called on a menuless item 2024-07-02 10:52:11 -07:00
outfoxxed b4be383695
service/tray: log menu refcount updates 2024-07-02 10:50:07 -07:00
outfoxxed ec362637b8
service/tray!: redesign menus / dbusmenu and add native menu support
Reworks dbusmenu menus to be displayable with a system context menu.

Breaks the entire DBusMenu api.
2024-07-01 20:50:30 -07:00
outfoxxed c31bbea837
docs: add breaking change notice 2024-07-01 20:50:07 -07:00
outfoxxed d8b900ed0b
lint: allow implicit bool conversions 2024-06-28 01:05:59 -07:00
outfoxxed 8547d12396
service/pipewire: make binding warnings in docs more obvious 2024-06-27 20:45:27 -07:00
outfoxxed d7149d5641
core/objectrepeater: soft-remove in favor of Instantiator
RIP my time.
2024-06-23 14:05:34 -07:00
outfoxxed c78c86425d
core/objectrepeater: delete delegate instances after removal 2024-06-23 03:18:27 -07:00
outfoxxed 09d8a7a07d
core/objectrepeater: add ObjectRepeater 2024-06-22 01:57:48 -07:00
outfoxxed d8fa9e7bb3
service/mpris: add properties for common track metadata
This was done to work around bad player implementations sending weird
metadata, such as removing the art url halfway through a song.
2024-06-21 19:03:40 -07:00
outfoxxed c56a3ec966
service/mpris: add shorthand for playback state changes 2024-06-21 16:31:02 -07:00
outfoxxed b6612bd56c
core/panelwindow: remove QSDOC_HIDE for above and focusable props 2024-06-21 10:11:57 -07:00
outfoxxed 3573663ab6
service/greetd: add greetd service 2024-06-20 15:39:49 -07:00
outfoxxed 72956185bd
wayland/lock: only update surfaces on screens changed if locked
Fixes crash when a WlSessionLock object exists but is unlocked and a
screen is added or removed.
2024-06-19 11:16:51 -07:00
outfoxxed 59cf60d83e
service/pam: add responseVisible
Fixes misunderstanding of "echo".
2024-06-19 00:31:09 -07:00
outfoxxed 6efa05a8eb
core: run full destruction sequence before exiting
Fixes QTimer messages.
2024-06-18 20:58:33 -07:00
outfoxxed 3033cba52d
all: fix failing lints 2024-06-18 20:46:58 -07:00
outfoxxed 8ec245ac66
wayland/lock: initialize lock content before starting lock
Reduces any chances of the compositor displaying a blank frame first.
2024-06-18 20:34:16 -07:00
outfoxxed 71a65c4d3c
docs: mention Fedora COPR package 2024-06-18 17:57:20 -07:00
outfoxxed 9e58077c61
core: fix shutdown sequence crashing 2024-06-18 17:03:38 -07:00
outfoxxed 3991726b9b
docs: document PAM feature in build instructions 2024-06-18 15:25:10 -07:00
outfoxxed ae762f5c6e
hyprland/ipc: ensure requests are flushed 2024-06-18 12:26:23 -07:00
outfoxxed e89035b18c
service/pam: move pam execution to subprocess to allow killing it
Many pam modules can't be aborted well without this.
2024-06-18 03:29:25 -07:00
outfoxxed b5c8774a79
service/pam: send completed messages after destroying pam conv
Allows context to be restarted in a complete handler.
2024-06-17 20:30:23 -07:00
outfoxxed 7e5d128a91
service/pam: add pam service 2024-06-17 18:32:13 -07:00
outfoxxed f655875547
core/desktopentry: add limited desktop entry api 2024-06-16 01:58:24 -07:00
outfoxxed ce5ddbf8ba
core: add $XDG_DATA_DIRS/pixmaps to QIcon fallback path
Picks up some missing app icons.
2024-06-14 19:18:43 -07:00
outfoxxed d8b72b4c31
wayland/lock: notify on screen change 2024-06-13 16:25:07 -07:00
outfoxxed 523de78796
wayland/layershell: ensure state changes are comitted without render
Previously they were not comitted and did not apply until the next
rendered frame.
2024-06-13 16:23:28 -07:00
outfoxxed 67783ec24c
core/transformwatcher: fix crash when a or b is destroyed
Usually happens during reload.
2024-06-09 15:42:38 -07:00
outfoxxed b5b9c1f6c3
wayland/toplevel_management: add foreign toplevel management 2024-06-07 04:31:20 -07:00
outfoxxed 5d1def3e49
hyprland/ipc: fix monitorFor returning null during HyprlandIpc init 2024-06-06 00:59:17 -07:00
outfoxxed bc349998df
hyprland/ipc: match by name in refreshMonitors instead of id
Was causing ghost/duplicate monitors from usages where the id was not known.
2024-06-06 00:58:10 -07:00
outfoxxed ef1a4134f0
hyprland/ipc: re-request monitors and workspaces on fail 2024-06-06 00:46:38 -07:00
outfoxxed d14ca70984
hyprland/ipc: add hyprland ipc
Only monitors and workspaces are fully tracked for now.
2024-06-05 19:26:20 -07:00
outfoxxed be237b6ab5
core/elapsedtimer: add ElapsedTimer 2024-06-04 13:48:54 -07:00
outfoxxed 37fecfc990
docs: add commit style instructions 2024-06-03 00:38:22 -07:00
outfoxxed b1f5a5eb94
service/mpris: preserve mpris watcher and players across reload 2024-06-02 16:18:45 -07:00
outfoxxed 9d5dd402b9
docs: recommend packagers add a dependency on qtsvg 2024-06-02 15:37:47 -07:00
outfoxxed 29f02d837d
all: remove NVIDIA workarounds
They fixed the driver.
2024-06-02 15:36:33 -07:00
outfoxxed 7d20b472dd
misc: remove the docs and examples submodules
They have not been correctly updated in lock-step for a while now.
2024-06-02 15:23:19 -07:00
outfoxxed bd504daf56
docs: add build, packaging and development instructions 2024-06-02 14:50:23 -07:00
outfoxxed 238ca8cf0b
core/reloader: fix crashing on failed reload 2024-05-31 04:03:00 -07:00
outfoxxed a8506edbb9
build: link jemalloc by default to reduce heap fragmentation
The QML engine and the quickshell reloader both cause large amounts of
heap fragmentation that stacks up over time, leading to a perceived
memory leak. Jemalloc is able to handle the fragmentation much better,
leading to lower user facing memory usage.
2024-05-31 01:28:35 -07:00
outfoxxed d56c07ceb3
core/reloader: simplify generation teardown
The extra complexity previously masked the use after free in 6c95267.
2024-05-31 00:27:18 -07:00
outfoxxed 84bb4098ad
core/reloader: fix incorrect generation teardown on hard reload 2024-05-31 00:26:34 -07:00
outfoxxed 6c9526761c
wayland: fix UAF in layershell surface destructor 2024-05-31 00:24:58 -07:00
outfoxxed 7feae55ebe
core/reloader: add reload signals for visual notifications 2024-05-30 02:39:37 -07:00
outfoxxed 569c40494d
all: import module dependencies via qmldir
Improves compatibility with qml tooling.
2024-05-29 19:29:57 -07:00
outfoxxed 0519acf1d6
core: support root: and root:/ paths for the config root
This works everywhere urls are accepted and rewrites them from the
config root as a qsintercept url.
2024-05-29 15:07:10 -07:00
outfoxxed 33fac67798
core: use the simple animation driver
Seems to provide much higher quality animations.
2024-05-28 20:22:01 -07:00
outfoxxed 7ad3671dd1
core/reloader: fix file watcher compatibility with vim 2024-05-28 15:36:25 -07:00
outfoxxed 4e92d82992
core: add options to enable QML debugging 2024-05-27 22:51:49 -07:00
outfoxxed 5a84e73442
core/objectmodel: add signals for changes to the list 2024-05-23 19:16:08 -07:00
outfoxxed 06240ccf80
service/mpris: improve compatibility with noncompliant players 2024-05-23 18:15:49 -07:00
outfoxxed 5016dbf0d4
all: replace list properties with ObjectModels 2024-05-23 17:28:07 -07:00
outfoxxed 6326f60ce2
service/mpris: re-query position on playback and metadata change 2024-05-23 02:38:26 -07:00
outfoxxed ac339cb23b
service/mpris: expose desktopEntry property 2024-05-22 05:40:03 -07:00
outfoxxed f2df3da596
service/mpris: fix position being incorrect after pausing 2024-05-22 04:34:56 -07:00
outfoxxed ed3708f5cb
service/mpris: add trackChanged signal 2024-05-21 05:07:24 -07:00
outfoxxed af45502913
service/mpris: add mpris module 2024-05-21 04:10:30 -07:00
outfoxxed 4ee9ac7f7c
service/mpris: finish mpris implementation 2024-05-21 04:09:19 -07:00
kossLAN 3b6d1c3bd8
feat: mpris 2024-05-21 04:09:19 -07:00
outfoxxed 73cfeba61b
x11: add XPanelWindow 2024-05-20 02:16:44 -07:00
330 changed files with 25985 additions and 2412 deletions

View file

@ -5,6 +5,8 @@ Checks: >
-*,
bugprone-*,
-bugprone-easily-swappable-parameters,
-bugprone-forward-declararion-namespace,
-bugprone-forward-declararion-namespace,
concurrency-*,
cppcoreguidelines-*,
-cppcoreguidelines-owning-memory,
@ -13,8 +15,10 @@ Checks: >
-cppcoreguidelines-avoid-const-or-ref-data-members,
-cppcoreguidelines-non-private-member-variables-in-classes,
-cppcoreguidelines-avoid-goto,
google-build-using-namespace.
google-explicit-constructor,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-pro-type-reinterpret-cast,
-cppcoreguidelines-pro-type-vararg,
google-global-names-in-headers,
google-readability-casting,
google-runtime-int,
@ -26,6 +30,7 @@ Checks: >
-modernize-return-braced-init-list,
-modernize-use-trailing-return-type,
performance-*,
-performance-avoid-endl,
portability-std-allocator-const,
readability-*,
-readability-function-cognitive-complexity,
@ -37,6 +42,8 @@ Checks: >
-readability-redundant-access-specifiers,
-readability-else-after-return,
-readability-container-data-pointer,
-readability-implicit-bool-conversion,
-readability-avoid-nested-conditional-operator,
tidyfox-*,
CheckOptions:
performance-for-range-copy.WarnOnAllAutoCopies: true

View file

@ -9,3 +9,7 @@ indent_style = tab
[*.nix]
indent_style = space
indent_size = 2
[*.{yml,yaml}]
indent_style = space
indent_size = 2

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1 @@
blank_issues_enabled: true

82
.github/ISSUE_TEMPLATE/crash.yml vendored Normal file
View file

@ -0,0 +1,82 @@
name: Crash Report
description: Quickshell has crashed
labels: ["bug", "crash"]
body:
- type: textarea
id: crashinfo
attributes:
label: General crash information
description: |
Paste the contents of the `info.txt` file in your crash folder here.
value: "<details> <summary>General information</summary>
```
<Paste the contents of the file here inside of the triple backticks>
```
</details>"
validations:
required: true
- 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: dump
attributes:
label: Minidump
description: |
Attach `minidump.dmp.log` here. If it is too big to upload, compress it.
You may skip this step if quickshell crashed while processing a password
or other sensitive information. If you skipped it write why instead.
validations:
required: true
- type: textarea
id: logs
attributes:
label: Log file
description: |
Attach `log.qslog.log` here. If it is too big to upload, compress it.
You can preview the log if you'd like using `quickshell read-log <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: |
If you have gdb installed and use systemd, or otherwise know how to get a backtrace,
we would appreciate one. (You may have gdb installed without knowing it)
1. Run `coredumpctl debug <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.
- type: textarea
id: exe
attributes:
label: Executable
description: |
If the crash folder contains a executable.txt file, upload it here. If not you can ignore this field.
If it is too big to upload, compress it.
Note: executable.txt is the quickshell binary. It has a .txt extension due to github's limitations on
filetypes.

55
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,55 @@
name: Build
on: [push, pull_request, workflow_dispatch]
jobs:
nix:
name: Nix
strategy:
matrix:
qtver: [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
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
- name: Download Dependencies
run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).inputDerivation'
- name: Build
run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }'
archlinux:
name: Archlinux
runs-on: ubuntu-latest
container: archlinux
steps:
- uses: actions/checkout@v4
- name: Download Dependencies
run: |
pacman --noconfirm --noprogressbar -Syyu
pacman --noconfirm --noprogressbar -Sy \
base-devel \
cmake \
ninja \
pkgconf \
qt6-base \
qt6-declarative \
qt6-svg \
qt6-wayland \
qt6-shadertools \
wayland-protocols \
wayland \
libxcb \
libpipewire \
cli11 \
jemalloc
- 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 --build build

25
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: Lint
on: [push, pull_request, workflow_dispatch]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
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: nicknovitski/nix-develop@v1
- name: Check formatting
run: clang-format -Werror --dry-run src/**/*.{cpp,hpp}
# required for lint
- name: Build
run: |
just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
just build
- name: Run lints
run: just lint-ci

4
.gitignore vendored
View file

@ -1,3 +1,7 @@
# related repos
/docs
/examples
# build files
/result
/build/

6
.gitmodules vendored
View file

@ -1,6 +0,0 @@
[submodule "docs"]
path = docs
url = https://git.outfoxxed.me/outfoxxed/quickshell-docs
[submodule "examples"]
path = examples
url = https://git.outfoxxed.me/outfoxxed/quickshell-examples

225
BUILD.md Normal file
View file

@ -0,0 +1,225 @@
# Build instructions
Instructions for building from source and distro packagers. We highly recommend
distro packagers read through this page fully.
## Packaging
If you are packaging quickshell for official or unofficial distribution channels,
such as a distro package repository, user repository, or other shared build location,
please set the following CMake flags.
`-DDISTRIBUTOR="your distribution platform"`
Please make this descriptive enough to identify your specific package, for example:
- `Official Nix Flake`
- `AUR (quickshell-git)`
- `Nixpkgs`
- `Fedora COPR (errornointernet/quickshell)`
`-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=YES/NO`
If we can retrieve binaries and debug information for the package without actually running your
distribution (e.g. from an website), and you would like to strip the binary, please set this to `YES`.
If we cannot retrieve debug information, please set this to `NO` and
**ensure you aren't distributing stripped (non debuggable) binaries**.
In both cases you should build with `-DCMAKE_BUILD_TYPE=RelWithDebInfo` (then split or keep the debuginfo).
### QML Module dir
Currently all QML modules are statically linked to quickshell, but this is where
tooling information will go.
`-DINSTALL_QML_PREFIX="path/to/qml"`
`-DINSTALL_QMLDIR="/full/path/to/qml"`
`INSTALL_QML_PREFIX` works the same as `INSTALL_QMLDIR`, except it prepends `CMAKE_INSTALL_PREFIX`. You usually want this.
## Dependencies
Quickshell has a set of base dependencies you will always need, names vary by distro:
- `cmake`
- `qt6base`
- `qt6declarative`
- `qtshadertools` (build-time only)
- `spirv-tools` (build-time only)
- `pkg-config` (build-time only)
- `cli11`
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`
We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and
svg icons will not work, including system ones.
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,
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`
Dependencies: `google-breakpad`
### Jemalloc
We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused
by the QML engine, which results in much lower memory usage. Without this you
will get a perceived memory leak.
To disable: `-DUSE_JEMALLOC=OFF`
Dependencies: `jemalloc`
### Unix Sockets
This feature allows interaction with unix sockets and creating socket servers
which is useful for IPC and has no additional dependencies.
WARNING: Disabling unix sockets will NOT make it safe to run arbitrary code using quickshell.
There are many vectors which mallicious code can use to escape into your system.
To disable: `-DSOCKETS=OFF`
### Wayland
This feature enables wayland support. Subfeatures exist for each particular wayland integration.
WARNING: Wayland integration relies on features that are not part of the public Qt API and which
may break in minor releases. Updating quickshell's dependencies without ensuring without ensuring
that the current Qt version is supported WILL result in quickshell failing to build or misbehaving
at runtime.
Currently supported Qt versions: `6.6`, `6.7`.
To disable: `-DWAYLAND=OFF`
Dependencies:
- `qt6wayland`
- `wayland` (libwayland-client)
- `wayland-scanner` (may be part of your distro's wayland package)
- `wayland-protocols`
#### Wlroots Layershell
Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol,
enabling use cases such as bars overlays and backgrounds.
This feature has no extra dependencies.
To disable: `-DWAYLAND_WLR_LAYERSHELL=OFF`
[zwlr-layer-shell-v1]: https://wayland.app/protocols/wlr-layer-shell-unstable-v1
#### Session Lock
Enables session lock support through the [ext-session-lock-v1] protocol,
which allows quickshell to be used as a session lock under compatible wayland compositors.
To disable: `-DWAYLAND_SESSION_LOCK=OFF`
[ext-session-lock-v1]: https://wayland.app/protocols/ext-session-lock-v1
#### Foreign Toplevel Management
Enables management of windows of other clients through the [zwlr-foreign-toplevel-management-v1] protocol,
which allows quickshell to be used as a session lock under compatible wayland compositors.
[zwlr-foreign-toplevel-management-v1]: https://wayland.app/protocols/wlr-foreign-toplevel-management-unstable-v1
To disable: `-DWAYLAND_TOPLEVEL_MANAGEMENT=OFF`
### X11
This feature enables x11 support. Currently this implements panel windows for X11 similarly
to the wlroots layershell above.
To disable: `-DX11=OFF`
Dependencies: `libxcb`
### Pipewire
This features enables viewing and management of pipewire nodes.
To disable: `-DSERVICE_PIPEWIRE=OFF`
Dependencies: `libpipewire`
### StatusNotifier / System Tray
This feature enables system tray support using the status notifier dbus protocol.
To disable: `-DSERVICE_STATUS_NOTIFIER=OFF`
Dependencies: `qt6dbus` (usually part of qt6base)
### MPRIS
This feature enables access to MPRIS compatible media players using its dbus protocol.
To disable: `-DSERVICE_MPRIS=OFF`
Dependencies: `qt6dbus` (usually part of qt6base)
### PAM
This feature enables PAM integration for user authentication.
To disable: `-DSERVICE_PAM=OFF`
Dependencies: `pam`
### Hyprland
This feature enables hyprland specific integrations. It requires wayland support
but has no extra dependencies.
To disable: `-DHYPRLAND=OFF`
#### Hyprland Global Shortcuts
Enables creation of global shortcuts under hyprland through the [hyprland-global-shortcuts-v1]
protocol. Generally a much nicer alternative to using unix sockets to implement the same thing.
This feature has no extra dependencies.
To disable: `-DHYPRLAND_GLOBAL_SHORTCUTS=OFF`
[hyprland-global-shortcuts-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml
#### Hyprland Focus Grab
Enables windows to grab focus similarly to a context menu under hyprland through the
[hyprland-focus-grab-v1] protocol. This feature has no extra dependencies.
To disable: `-DHYPRLAND_FOCUS_GRAB=OFF`
[hyprland-focus-grab-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-focus-grab-v1.xml
### i3/Sway
Enables i3 and Sway specific features, does not have any dependency on Wayland or x11.
To disable: `-DI3=OFF`
#### i3/Sway IPC
Enables interfacing with i3 and Sway's IPC.
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.
#### Configuring the build
```sh
$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here]
```
Note that features you do not supply dependencies for MUST be disabled with their associated flags
or quickshell will fail to build.
Additionally, note that clang builds much faster than gcc if you care.
#### Building
```sh
$ cmake --build build
```
#### Installing
```sh
$ cmake --install build
```

View file

@ -5,50 +5,76 @@ set(QT_MIN_VERSION "6.6.0")
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(BUILD_TESTING "Build tests" OFF)
option(ASAN "Enable ASAN" OFF)
option(FRAME_POINTERS "Always keep frame pointers" ${ASAN})
set(QS_BUILD_OPTIONS "")
option(NVIDIA_COMPAT "Workarounds for nvidia gpus" OFF)
option(SOCKETS "Enable unix socket support" ON)
option(WAYLAND "Enable wayland support" ON)
option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON)
option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON)
option(HYPRLAND "Support hyprland specific features" ON)
option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON)
option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON)
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
option(SERVICE_PIPEWIRE "PipeWire service" ON)
function(boption VAR NAME DEFAULT)
cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "")
option(${VAR} ${NAME} ${DEFAULT})
set(STATUS "${VAR}_status")
set(EFFECTIVE "${VAR}_effective")
set(${STATUS} ${${VAR}})
set(${EFFECTIVE} ${${VAR}})
if (${${VAR}} AND DEFINED arg_REQUIRES)
set(REQUIRED_EFFECTIVE "${arg_REQUIRES}_effective")
if (NOT ${${REQUIRED_EFFECTIVE}})
set(${STATUS} "OFF (Requires ${arg_REQUIRES})")
set(${EFFECTIVE} OFF)
endif()
endif()
set(${EFFECTIVE} "${${EFFECTIVE}}" PARENT_SCOPE)
message(STATUS " ${NAME}: ${${STATUS}}")
string(APPEND QS_BUILD_OPTIONS "\\n ${NAME}: ${${STATUS}}")
set(QS_BUILD_OPTIONS "${QS_BUILD_OPTIONS}" PARENT_SCOPE)
endfunction()
set(DISTRIBUTOR "Unset" CACHE STRING "Distributor")
string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}")
message(STATUS "Quickshell configuration")
message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}")
message(STATUS " Build tests: ${BUILD_TESTING}")
message(STATUS " Sockets: ${SOCKETS}")
message(STATUS " Wayland: ${WAYLAND}")
if (WAYLAND)
message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}")
message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}")
endif ()
message(STATUS " Services")
message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}")
message(STATUS " Hyprland: ${HYPRLAND}")
if (HYPRLAND)
message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")
message(STATUS " Global Shortcuts: ${HYPRLAND_GLOBAL_SHORTCUTS}")
endif()
message(STATUS " Distributor: ${DISTRIBUTOR}")
boption(DISTRIBUTOR_DEBUGINFO_AVAILABLE "Distributor provided debuginfo" NO)
boption(NO_PCH "Disable precompild headers (dev)" OFF)
boption(BUILD_TESTING "Build tests (dev)" OFF)
boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang
boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN})
if (NOT DEFINED GIT_REVISION)
execute_process(
COMMAND git rev-parse HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_REVISION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
endif()
boption(CRASH_REPORTER "Crash Handling" ON)
boption(USE_JEMALLOC "Use jemalloc" ON)
boption(SOCKETS "Unix Sockets" ON)
boption(WAYLAND "Wayland" ON)
boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND)
boption(WAYLAND_SESSION_LOCK " Session Lock" ON REQUIRES WAYLAND)
boption(WAYLAND_TOPLEVEL_MANAGEMENT " Foreign Toplevel Management" ON REQUIRES WAYLAND)
boption(HYPRLAND " Hyprland" ON REQUIRES WAYLAND)
boption(HYPRLAND_IPC " Hyprland IPC" ON REQUIRES HYPRLAND)
boption(HYPRLAND_GLOBAL_SHORTCUTS " Hyprland Global Shortcuts" ON REQUIRES HYPRLAND)
boption(HYPRLAND_FOCUS_GRAB " Hyprland Focus Grabbing" ON REQUIRES HYPRLAND)
boption(HYPRLAND_SURFACE_EXTENSIONS " Hyprland Surface Extensions" ON REQUIRES HYPRLAND)
boption(X11 "X11" ON)
boption(I3 "I3/Sway" ON)
boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3)
boption(SERVICE_STATUS_NOTIFIER "System Tray" ON)
boption(SERVICE_PIPEWIRE "PipeWire" ON)
boption(SERVICE_MPRIS "Mpris" ON)
boption(SERVICE_PAM "Pam" ON)
boption(SERVICE_GREETD "Greetd" ON)
boption(SERVICE_UPOWER "UPower" ON)
boption(SERVICE_NOTIFICATIONS "Notifications" ON)
include(cmake/install-qml-module.cmake)
include(cmake/util.cmake)
add_compile_options(-Wall -Wextra)
# pipewire defines this, breaking PCH
add_compile_definitions(_REENTRANT)
if (FRAME_POINTERS)
add_compile_options(-fno-omit-frame-pointer)
endif()
@ -68,8 +94,9 @@ if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug)
endif()
set(QT_DEPS Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2 Qt6::Widgets)
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets)
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools)
include(cmake/pch.cmake)
if (BUILD_TESTING)
enable_testing()
@ -78,58 +105,39 @@ if (BUILD_TESTING)
endif()
if (SOCKETS)
list(APPEND QT_DEPS Qt6::Network)
list(APPEND QT_FPDEPS Network)
endif()
if (WAYLAND)
list(APPEND QT_DEPS Qt6::WaylandClient Qt6::WaylandClientPrivate)
list(APPEND QT_FPDEPS WaylandClient)
endif()
if (SERVICE_STATUS_NOTIFIER)
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS)
set(DBUS ON)
endif()
if (DBUS)
list(APPEND QT_DEPS Qt6::DBus)
list(APPEND QT_FPDEPS DBus)
endif()
find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS})
set(CMAKE_AUTOUIC OFF)
qt_standard_project_setup(REQUIRES 6.6)
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules)
# pch breaks clang-tidy..... somehow
if (NOT NO_PCH)
file(GENERATE
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/pchstub.cpp
CONTENT "// intentionally empty"
)
add_library(qt-pch ${CMAKE_CURRENT_BINARY_DIR}/pchstub.cpp)
target_link_libraries(qt-pch PRIVATE ${QT_DEPS})
target_precompile_headers(qt-pch PUBLIC
<memory>
<qobject.h>
<qqmlengine.h>
<qlist.h>
<qcolor.h>
<qquickitem.h>
<qevent.h>
)
endif()
function (qs_pch target)
if (NOT NO_PCH)
target_precompile_headers(${target} REUSE_FROM qt-pch)
target_link_libraries(${target} PRIVATE ${QT_DEPS}) # required for gcc to accept the pch on plugin targets
endif()
endfunction()
if (NVIDIA_COMPAT)
add_compile_definitions(NVIDIA_COMPAT)
endif()
add_subdirectory(src)
if (USE_JEMALLOC)
find_package(PkgConfig REQUIRED)
# IMPORTED_TARGET not working for some reason
pkg_check_modules(JEMALLOC REQUIRED jemalloc)
target_link_libraries(quickshell PRIVATE ${JEMALLOC_LIBRARIES})
endif()
install(CODE "
execute_process(
COMMAND ${CMAKE_COMMAND} -E create_symlink \
${CMAKE_INSTALL_FULL_BINDIR}/quickshell \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/qs
)
")

102
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,102 @@
# Contributing / Development
Instructions for development setup and upstreaming patches.
If you just want to build or package quickshell see [BUILD.md](BUILD.md).
## Development
Install the dependencies listed in [BUILD.md](BUILD.md).
You probably want all of them even if you don't use all of them
to ensure tests work correctly and avoid passing a bunch of configure
flags when you need to wipe the build directory.
Quickshell also uses `just` for common development command aliases.
The dependencies are also available as a nix shell or nix flake which we recommend
using with nix-direnv.
Common aliases:
- `just configure [<debug|release> [extra cmake args]]` (note that you must specify debug/release to specify extra args)
- `just build` - runs the build, configuring if not configured already.
- `just run [args]` - runs quickshell with the given arguments
- `just clean` - clean up build artifacts. `just clean build` is somewhat common.
### Formatting
All contributions should be formatted similarly to what already exists.
Group related functionality together.
Run the formatter using `just fmt`.
If the results look stupid, fix the clang-format file if possible,
or disable clang-format in the affected area
using `// clang-format off` and `// clang-format on`.
### Linter
All contributions should pass the linter.
Note that running the linter requires disabling precompiled
headers and including the test codepaths:
```sh
$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON
$ just lint
```
If the linter is complaining about something that you think it should not,
please disable the lint in your MR and explain your reasoning.
### Tests
If you feel like the feature you are working on is very complex or likely to break,
please write some tests. We will ask you to directly if you send in an MR for an
overly complex or breakable feature.
At least all tests that passed before your changes should still be passing
by the time your contribution is ready.
You can run the tests using `just test` but you must enable them first
using `-DBUILD_TESTING=ON`.
### Documentation
Most of quickshell's documentation is automatically generated from the source code.
You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser
cannot handle random line breaks and will usually require you to disable clang-format if the
lines are too long.
Before submitting an MR, if adding new features please make sure the documentation is generated
reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo.
Doc comments take the form `///` or `///!` (summary) and work with markdown.
You can reference other types using the `@@[Module.][Type.][member]` shorthand
where all parts are optional. If module or type are not specified they will
be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`.
Look at existing code for how it works.
Quickshell modules additionally have a `module.md` file which contains a summary, description,
and list of headers to scan for documentation.
## Contributing
### Commits
Please structure your commit messages as `scope[!]: commit` where
the scope is something like `core` or `service/mpris`. (pick what has been
used historically or what makes sense if new.) Add `!` for changes that break
existing APIs or functionality.
Commit descriptions should contain a summary of the changes if they are not
sufficiently addressed in the commit message.
Please squash/rebase additions or edits to previous changes and follow the
commit style to keep the history easily searchable at a glance.
Depending on the change, it is often reasonable to squash it into just
a single commit. (If you do not follow this we will squash your changes
for you.)
### Sending patches
You may contribute by submitting a pull request on github, asking for
an account on our git server, or emailing patches / git bundles
directly to `outfoxxed@outfoxxed.me`.
### Getting help
If you're getting stuck, you can come talk to us in the
[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me)
for help on implementation, conventions, etc.
Feel free to ask for advice early in your implementation if you are
unsure.

View file

@ -4,7 +4,13 @@ fmt:
find src -type f \( -name "*.cpp" -o -name "*.hpp" \) -print0 | xargs -0 clang-format -i
lint:
find src -type f -name "*.cpp" -print0 | parallel -q0 --eta clang-tidy --load={{ env_var("TIDYFOX") }}
find src -type f -name "*.cpp" -print0 | parallel -q0 --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
lint-ci:
find src -type f -name "*.cpp" -print0 | parallel -q0 --no-notice --will-cite --tty clang-tidy --load={{ env_var("TIDYFOX") }}
lint-changed:
git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
configure target='debug' *FLAGS='':
cmake -GNinja -B {{builddir}} \

112
README.md
View file

@ -1,7 +1,7 @@
# quickshell
<a href="https://matrix.to/#/#quickshell:outfoxxed.me"><img src="https://img.shields.io/badge/Join%20the%20matrix%20room-%23quickshell:outfoxxed.me-0dbd8b?logo=matrix&style=flat-square"></a>
Simple and flexbile QtQuick based desktop shell toolkit.
Flexbile QtQuick based desktop shell toolkit.
Hosted on: [outfoxxed's gitea], [github]
@ -11,21 +11,14 @@ Hosted on: [outfoxxed's gitea], [github]
Documentation available at [quickshell.outfoxxed.me](https://quickshell.outfoxxed.me) or
can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quickshell-docs) repo.
Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples)
Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples)
repo.
Both the documentation and examples are included as submodules with revisions that work with the current
version of quickshell.
# Breaking Changes
Quickshell is still in alpha and there will be breaking changes.
You can clone everything with
```
$ git clone --recursive https://git.outfoxxed.me/outfoxxed/quickshell.git
```
Or clone missing submodules later with
```
$ git submodule update --init --recursive
```
Commits with breaking qml api changes will contain a `!` at the end of the scope
(`thing!: foo`) and the commit description will contain details about the broken api.
# Installation
@ -39,6 +32,9 @@ This repo has a nix flake you can use to install the package directly:
quickshell = {
url = "git+https://git.outfoxxed.me/outfoxxed/quickshell";
# THIS IS IMPORTANT
# Mismatched system dependencies will lead to crashes and other issues.
inputs.nixpkgs.follows = "nixpkgs";
};
};
@ -48,75 +44,45 @@ This repo has a nix flake you can use to install the package directly:
Quickshell's binary is available at `quickshell.packages.<system>.default` to be added to
lists such as `environment.systemPackages` or `home.packages`.
`quickshell.packages.<system>.nvidia` is also available for nvidia users which fixes some
common crashes.
The package contains several features detailed in [BUILD.md](BUILD.md) which can be enabled
or disabled with overrides:
```nix
quickshell.packages.<system>.default.override {
withJemalloc = true;
withQtSvg = true;
withWayland = true;
withX11 = true;
withPipewire = true;
withPam = true;
withHyprland = true;
}
```
Note: by default this package is built with clang as it is significantly faster.
## Manual
## Arch (AUR)
Quickshell has a third party [AUR package] available under the same name.
It is not managed by us and should be looked over before use.
If not using nix, you'll have to build from source.
[AUR package]: https://aur.archlinux.org/packages/quickshell
### Dependencies
To build quickshell at all, you will need the following packages (names may vary by distro)
> [!CAUTION]
> The AUR provides no way to force the quickshell package to rebuild when the Qt version changes.
> If you experience crashes after updating Qt, please try rebuilding Quickshell against the
> current Qt version before opening an issue.
- just
- cmake
- pkg-config
- ninja
- Qt6 [ QtBase, QtDeclarative ]
## Fedora (COPR)
Quickshell has a third party [Fedora COPR package] available under the same name.
It is not managed by us and should be looked over before use.
To build with wayland support you will additionally need:
- wayland
- wayland-scanner (may be part of wayland on some distros)
- wayland-protocols
- Qt6 [ QtWayland ]
[Fedora COPR package]: https://copr.fedorainfracloud.org/coprs/errornointernet/quickshell
### Building
## Anything else
See [BUILD.md](BUILD.md) for instructions on building and packaging quickshell.
To make a release build of quickshell run:
```sh
$ just release
```
If running an nvidia GPU, instead run:
```sh
$ just configure release -DNVIDIA_COMPAT=ON
$ just build
```
(These commands are just aliases for cmake commands you can run directly,
see the Justfile for more information.)
If you have all the dependencies installed and they are in expected
locations this will build correctly.
To install to /usr/local/bin run as root (usually `sudo`) in the same folder:
```
$ just install
```
### Building (Nix)
You can build directly using the provided nix flake or nix package.
```
nix build
nix build -f package.nix # calls default.nix with a basic callPackage expression
```
# Development
For nix there is a devshell available from `shell.nix` and as a devShell
output from the flake.
The Justfile contains various useful aliases:
- `just configure [<debug|release> [extra cmake args]]`
- `just build` (runs configure for debug mode)
- `just run [args]`
- `just clean`
- `just test [args]` (configure with `-DBUILD_TESTING=ON` first)
- `just fmt`
- `just lint`
# Contributing / Development
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
#### License

8
ci/matrix.nix Normal file
View file

@ -0,0 +1,8 @@
{
qtver,
compiler,
}: let
nixpkgs = (import ./nix-checkouts.nix).${builtins.replaceStrings ["."] ["_"] qtver};
compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler};
pkg = (nixpkgs.callPackage ../default.nix {}).override compilerOverride;
in pkg

58
ci/nix-checkouts.nix Normal file
View file

@ -0,0 +1,58 @@
let
byCommit = {
commit,
sha256,
}: import (builtins.fetchTarball {
name = "nixpkgs-${commit}";
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.
qt6_8_0 = byCommit {
commit = "23e89b7da85c3640bbc2173fe04f4bd114342367";
sha256 = "1b2v6y3bja4br5ribh9lj6xzz2k81dggz708b2mib83rwb509wyb";
};
qt6_7_3 = byCommit {
commit = "273673e839189c26130d48993d849a84199523e6";
sha256 = "0aca369hdxb8j0vx9791anyzy4m65zckx0lriicqhp95kv9q6m7z";
};
qt6_7_2 = byCommit {
commit = "841f166ff96fc2f3ecd1c0cc08072633033d41bf";
sha256 = "0d7p0cp7zjiadhpa6sdafxvrpw4lnmb1h673w17q615vm1yaasvy";
};
qt6_7_1 = byCommit {
commit = "69bee9866a4e2708b3153fdb61c1425e7857d6b8";
sha256 = "1an4sha4jsa29dvc4n9mqxbq8jjwg7frl0rhy085g73m7l1yx0lj";
};
qt6_7_0 = byCommit {
commit = "4fbbc17ccf11bc80002b19b31387c9c80276f076";
sha256 = "09lhgdqlx8j9a7vpdcf8sddlhbzjq0s208spfmxfjdn14fvx8k0j";
};
qt6_6_3 = byCommit {
commit = "8f1a3fbaa92f1d59b09f2d24af6a607b5a280071";
sha256 = "0322zwxvmg8v2wkm03xpk6mqmmbfjgrhc9prcx0zd36vjl6jmi18";
};
qt6_6_2 = byCommit {
commit = "0bb9cfbd69459488576a0ef3c0e0477bedc3a29e";
sha256 = "172ww486jm1mczk9id78s32p7ps9m9qgisml286flc8jffb6yad8";
};
qt6_6_1 = byCommit {
commit = "8eecc3342103c38eea666309a7c0d90d403a039a";
sha256 = "1lakc0immsgrpz3basaysdvd0sx01r0mcbyymx6id12fk0404z5r";
};
qt6_6_0 = byCommit {
commit = "1ded005f95a43953112ffc54b39593ea2f16409f";
sha256 = "1xvyd3lj81hak9j53mrhdsqx78x5v2ppv8m2s54qa2099anqgm0f";
};
}

7
ci/variations.nix Normal file
View file

@ -0,0 +1,7 @@
{
clangStdenv,
gccStdenv,
}: {
clang = { buildStdenv = clangStdenv; };
gcc = { buildStdenv = gccStdenv; };
}

View file

@ -0,0 +1,89 @@
set(INSTALL_QMLDIR "" CACHE STRING "QML install dir")
set(INSTALL_QML_PREFIX "" CACHE STRING "QML install prefix")
# There doesn't seem to be a standard cross-distro qml install path.
if ("${INSTALL_QMLDIR}" STREQUAL "" AND "${INSTALL_QML_PREFIX}" STREQUAL "")
message(WARNING "Neither INSTALL_QMLDIR nor INSTALL_QML_PREFIX is set. QML modules will not be installed.")
else()
if ("${INSTALL_QMLDIR}" STREQUAL "")
set(QML_FULL_INSTALLDIR "${CMAKE_INSTALL_PREFIX}/${INSTALL_QML_PREFIX}")
else()
set(QML_FULL_INSTALLDIR "${INSTALL_QMLDIR}")
endif()
message(STATUS "QML install dir: ${QML_FULL_INSTALLDIR}")
endif()
# Install a given target as a QML module. This is mostly pulled from ECM, as there does not seem
# to be an official way to do it.
# see https://github.com/KDE/extra-cmake-modules/blob/fe0f606bf7f222e36f7560fd7a2c33ef993e23bb/modules/ECMQmlModule6.cmake#L160
function(install_qml_module arg_TARGET)
if (NOT DEFINED QML_FULL_INSTALLDIR)
return()
endif()
qt_query_qml_module(${arg_TARGET}
URI module_uri
VERSION module_version
PLUGIN_TARGET module_plugin_target
TARGET_PATH module_target_path
QMLDIR module_qmldir
TYPEINFO module_typeinfo
QML_FILES module_qml_files
RESOURCES module_resources
)
set(module_dir "${QML_FULL_INSTALLDIR}/${module_target_path}")
if (NOT TARGET "${module_plugin_target}")
message(FATAL_ERROR "install_qml_modules called for a target without a plugin")
endif()
get_target_property(target_type "${arg_TARGET}" TYPE)
if (NOT "${target_type}" STREQUAL "STATIC_LIBRARY")
install(
TARGETS "${arg_TARGET}"
LIBRARY DESTINATION "${module_dir}"
RUNTIME DESTINATION "${module_dir}"
)
install(
TARGETS "${module_plugin_target}"
LIBRARY DESTINATION "${module_dir}"
RUNTIME DESTINATION "${module_dir}"
)
endif()
install(FILES "${module_qmldir}" DESTINATION "${module_dir}")
install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
# Install QML files
list(LENGTH module_qml_files num_files)
if (NOT "${module_qml_files}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0)
qt_query_qml_module(${arg_TARGET} QML_FILES_DEPLOY_PATHS qml_files_deploy_paths)
math(EXPR last_index "${num_files} - 1")
foreach(i RANGE 0 ${last_index})
list(GET module_qml_files ${i} src_file)
list(GET qml_files_deploy_paths ${i} deploy_path)
get_filename_component(dst_name "${deploy_path}" NAME)
get_filename_component(dest_dir "${deploy_path}" DIRECTORY)
install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}")
endforeach()
endif()
# Install resources
list(LENGTH module_resources num_files)
if (NOT "${module_resources}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0)
qt_query_qml_module(${arg_TARGET} RESOURCES_DEPLOY_PATHS resources_deploy_paths)
math(EXPR last_index "${num_files} - 1")
foreach(i RANGE 0 ${last_index})
list(GET module_resources ${i} src_file)
list(GET resources_deploy_paths ${i} deploy_path)
get_filename_component(dst_name "${deploy_path}" NAME)
get_filename_component(dest_dir "${deploy_path}" DIRECTORY)
install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}")
endforeach()
endif()
endfunction()

85
cmake/pch.cmake Normal file
View file

@ -0,0 +1,85 @@
# pch breaks clang-tidy..... somehow
if (NOT NO_PCH)
file(GENERATE
OUTPUT ${CMAKE_BINARY_DIR}/pchstub.cpp
CONTENT "// intentionally empty"
)
endif()
function (qs_pch target)
if (NO_PCH)
return()
endif()
cmake_parse_arguments(PARSE_ARGV 1 arg "" "SET" "")
if ("${arg_SET}" STREQUAL "")
set(arg_SET "common")
endif()
target_precompile_headers(${target} REUSE_FROM "qs-pchset-${arg_SET}")
endfunction()
function (qs_module_pch target)
qs_pch(${target} ${ARGN})
qs_pch("${target}plugin" SET plugin)
qs_pch("${target}plugin_init" SET plugin)
endfunction()
function (qs_add_pchset SETNAME)
if (NO_PCH)
return()
endif()
cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "HEADERS;DEPENDENCIES")
set(LIBNAME "qs-pchset-${SETNAME}")
add_library(${LIBNAME} ${CMAKE_BINARY_DIR}/pchstub.cpp)
target_link_libraries(${LIBNAME} ${arg_DEPENDENCIES})
target_precompile_headers(${LIBNAME} PUBLIC ${arg_HEADERS})
endfunction()
set(COMMON_PCH_SET
<chrono>
<memory>
<vector>
<qdebug.h>
<qobject.h>
<qmetatype.h>
<qstring.h>
<qchar.h>
<qlist.h>
<qabstractitemmodel.h>
)
qs_add_pchset(common
DEPENDENCIES Qt::Quick
HEADERS ${COMMON_PCH_SET}
)
qs_add_pchset(large
DEPENDENCIES Qt::Quick
HEADERS
${COMMON_PCH_SET}
<qiodevice.h>
<qevent.h>
<qcoreapplication.h>
<qqmlengine.h>
<qquickitem.h>
<qquickwindow.h>
<qcolor.h>
<qdir.h>
<qtimer.h>
<qabstractitemmodel.h>
)
# including qplugin.h directly will cause required symbols to disappear
qs_add_pchset(plugin
DEPENDENCIES Qt::Qml
HEADERS
<qobject.h>
<qjsonobject.h>
<qpointer.h>
)

29
cmake/util.cmake Normal file
View file

@ -0,0 +1,29 @@
# Adds a dependency hint to the link order, but does not block build on the dependency.
function (qs_add_link_dependencies target)
set_property(
TARGET ${target}
APPEND PROPERTY INTERFACE_LINK_LIBRARIES
${ARGN}
)
endfunction()
function (qs_append_qmldir target text)
get_property(qmldir_content TARGET ${target} PROPERTY _qt_internal_qmldir_content)
if ("${qmldir_content}" STREQUAL "")
message(WARNING "qs_append_qmldir depends on private Qt cmake code, which has broken.")
return()
endif()
set_property(TARGET ${target} APPEND_STRING PROPERTY _qt_internal_qmldir_content ${text})
endfunction()
# DEPENDENCIES introduces a cmake dependency which we don't need with static modules.
# This greatly improves comp speed by not introducing those dependencies.
function (qs_add_module_deps_light target)
foreach (dep IN LISTS ARGN)
string(APPEND qmldir_extra "depends ${dep}\n")
endforeach()
qs_append_qmldir(${target} "${qmldir_extra}")
endfunction()

View file

@ -3,13 +3,20 @@
nix-gitignore,
pkgs,
keepDebugInfo,
buildStdenv ? pkgs.clang17Stdenv,
buildStdenv ? pkgs.clangStdenv,
cmake,
ninja,
qt6,
spirv-tools,
cli11,
breakpad,
jemalloc,
wayland,
wayland-protocols,
xorg,
pipewire,
pam,
gitRev ? (let
headExists = builtins.pathExists ./.git/HEAD;
@ -23,10 +30,15 @@
else "unknown"),
debug ? false,
enableWayland ? true,
enablePipewire ? true,
nvidiaCompat ? false,
svgSupport ? true, # you almost always want this
withCrashReporter ? true,
withJemalloc ? true, # masks heap fragmentation
withQtSvg ? true,
withWayland ? true,
withX11 ? true,
withPipewire ? true,
withPam ? true,
withHyprland ? true,
withI3 ? true,
}: buildStdenv.mkDerivation {
pname = "quickshell${lib.optionalString debug "-debug"}";
version = "0.1.0";
@ -35,45 +47,54 @@
nativeBuildInputs = with pkgs; [
cmake
ninja
qt6.qtshadertools
spirv-tools
qt6.wrapQtAppsHook
] ++ (lib.optionals enableWayland [
pkg-config
] ++ (lib.optionals withWayland [
wayland-protocols
wayland-scanner
]);
buildInputs = with pkgs; [
buildInputs = [
qt6.qtbase
qt6.qtdeclarative
cli11
]
++ (lib.optionals enableWayland [ qt6.qtwayland wayland ])
++ (lib.optionals svgSupport [ qt6.qtsvg ])
++ (lib.optionals enablePipewire [ pipewire ]);
++ lib.optional withCrashReporter breakpad
++ lib.optional withJemalloc jemalloc
++ lib.optional withQtSvg qt6.qtsvg
++ lib.optionals withWayland [ qt6.qtwayland wayland ]
++ lib.optional withX11 xorg.libxcb
++ lib.optional withPam pam
++ lib.optional withPipewire pipewire;
QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner";
configurePhase = let
cmakeBuildType = if debug
then "Debug"
else "RelWithDebInfo";
in ''
cmakeBuildType=${cmakeBuildType} # qt6 setup hook resets this for some godforsaken reason
cmakeConfigurePhase
'';
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
cmakeFlags = [
"-DGIT_REVISION=${gitRev}"
] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF"
++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON"
++ lib.optional (!enablePipewire) "-DSERVICE_PIPEWIRE=OFF";
(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 "SERVICE_PIPEWIRE" withPipewire)
(lib.cmakeBool "SERVICE_PAM" withPam)
(lib.cmakeBool "HYPRLAND" withHyprland)
(lib.cmakeBool "I3" withI3)
];
buildPhase = "ninjaBuildPhase";
enableParallelBuilding = true;
dontStrip = true;
# 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://git.outfoxxed.me/outfoxxed/quickshell";
description = "Simple and flexbile QtQuick based desktop shell toolkit";
description = "Flexbile QtQuick based desktop shell toolkit";
license = licenses.lgpl3Only;
platforms = platforms.linux;
};

1
docs

@ -1 +0,0 @@
Subproject commit ff5da84a8b258a9b2caaf978ddb6de23635d8903

@ -1 +0,0 @@
Subproject commit b9e744b50673304dfddb68f3da2a2e906d028b96

View file

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1709237383,
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
"lastModified": 1732014248,
"narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
"rev": "23e89b7da85c3640bbc2173fe04f4bd114342367",
"type": "github"
},
"original": {

View file

@ -12,10 +12,8 @@
quickshell = pkgs.callPackage ./default.nix {
gitRev = self.rev or self.dirtyRev;
};
quickshell-nvidia = quickshell.override { nvidiaCompat = true; };
default = quickshell;
nvidia = quickshell-nvidia;
});
devShells = forEachSystem (system: pkgs: rec {

View file

@ -15,13 +15,12 @@ in pkgs.mkShell.override { stdenv = quickshell.stdenv; } {
nativeBuildInputs = with pkgs; [
just
clang-tools_17
clang-tools
parallel
makeWrapper
];
TIDYFOX = "${tidyfox}/lib/libtidyfox.so";
QTWAYLANDSCANNER = quickshell.QTWAYLANDSCANNER;
shellHook = ''
export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc)

View file

@ -2,8 +2,18 @@ qt_add_executable(quickshell main.cpp)
install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
add_subdirectory(build)
add_subdirectory(launch)
add_subdirectory(core)
add_subdirectory(debug)
add_subdirectory(ipc)
add_subdirectory(window)
add_subdirectory(io)
add_subdirectory(widgets)
if (CRASH_REPORTER)
add_subdirectory(crash)
endif()
if (DBUS)
add_subdirectory(dbus)
@ -11,6 +21,10 @@ endif()
if (WAYLAND)
add_subdirectory(wayland)
endif ()
endif()
if (X11)
add_subdirectory(x11)
endif()
add_subdirectory(services)

26
src/build/CMakeLists.txt Normal file
View file

@ -0,0 +1,26 @@
add_library(quickshell-build INTERFACE)
if (NOT DEFINED GIT_REVISION)
execute_process(
COMMAND git rev-parse HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_REVISION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
endif()
if (CRASH_REPORTER)
set(CRASH_REPORTER_DEF 1)
else()
set(CRASH_REPORTER_DEF 0)
endif()
if (DISTRIBUTOR_DEBUGINFO_AVAILABLE)
set(DEBUGINFO_AVAILABLE 1)
else()
set(DEBUGINFO_AVAILABLE 0)
endif()
configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES)
target_include_directories(quickshell-build INTERFACE ${CMAKE_CURRENT_BINARY_DIR})

12
src/build/build.hpp.in Normal file
View file

@ -0,0 +1,12 @@
#pragma once
// NOLINTBEGIN
#define GIT_REVISION "@GIT_REVISION@"
#define DISTRIBUTOR "@DISTRIBUTOR@"
#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@
#define CRASH_REPORTER @CRASH_REPORTER_DEF@
#define BUILD_TYPE "@CMAKE_BUILD_TYPE@"
#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)"
#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@"
#define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@"
// NOLINTEND

View file

@ -1,20 +1,14 @@
qt_add_library(quickshell-core STATIC
main.cpp
plugin.cpp
shell.cpp
variants.cpp
rootwrapper.cpp
proxywindow.cpp
reload.cpp
rootwrapper.cpp
qmlglobal.cpp
qmlscreen.cpp
region.cpp
persistentprops.cpp
windowinterface.cpp
floatingwindow.cpp
panelinterface.cpp
popupwindow.cpp
singleton.cpp
generation.cpp
scan.cpp
@ -26,13 +20,38 @@ qt_add_library(quickshell-core STATIC
imageprovider.cpp
transformwatcher.cpp
boundcomponent.cpp
model.cpp
elapsedtimer.cpp
desktopentry.cpp
objectrepeater.cpp
platformmenu.cpp
qsmenu.cpp
retainable.cpp
popupanchor.cpp
types.cpp
qsmenuanchor.cpp
clock.cpp
logging.cpp
paths.cpp
instanceinfo.cpp
common.cpp
iconprovider.cpp
scriptmodel.cpp
)
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
qt_add_qml_module(quickshell-core URI Quickshell VERSION 0.1)
qt_add_qml_module(quickshell-core
URI Quickshell
VERSION 0.1
DEPENDENCIES QtQuick
OPTIONAL_IMPORTS Quickshell._Window
DEFAULT_IMPORTS Quickshell._Window
)
target_link_libraries(quickshell-core PRIVATE ${QT_DEPS})
qs_pch(quickshell-core)
install_qml_module(quickshell-core)
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets)
qs_module_pch(quickshell-core SET large)
target_link_libraries(quickshell PRIVATE quickshell-coreplugin)

97
src/core/clock.cpp Normal file
View file

@ -0,0 +1,97 @@
#include "clock.hpp"
#include <qdatetime.h>
#include <qobject.h>
#include <qtimer.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "util.hpp"
SystemClock::SystemClock(QObject* parent): QObject(parent) {
QObject::connect(&this->timer, &QTimer::timeout, this, &SystemClock::onTimeout);
this->update();
}
bool SystemClock::enabled() const { return this->mEnabled; }
void SystemClock::setEnabled(bool enabled) {
if (enabled == this->mEnabled) return;
this->mEnabled = enabled;
emit this->enabledChanged();
this->update();
}
SystemClock::Enum SystemClock::precision() const { return this->mPrecision; }
void SystemClock::setPrecision(SystemClock::Enum precision) {
if (precision == this->mPrecision) return;
this->mPrecision = precision;
emit this->precisionChanged();
this->update();
}
void SystemClock::onTimeout() {
this->setTime(this->targetTime);
this->schedule(this->targetTime);
}
void SystemClock::update() {
if (this->mEnabled) {
this->setTime(QDateTime::fromMSecsSinceEpoch(0));
this->schedule(QDateTime::fromMSecsSinceEpoch(0));
} else {
this->timer.stop();
}
}
void SystemClock::setTime(const QDateTime& targetTime) {
auto currentTime = QDateTime::currentDateTime();
auto offset = currentTime.msecsTo(targetTime);
auto dtime = offset > -500 && offset < 500 ? targetTime : currentTime;
auto time = dtime.time();
auto secondPrecision = this->mPrecision >= SystemClock::Seconds;
auto secondChanged = this->setSeconds(secondPrecision ? time.second() : 0);
auto minutePrecision = this->mPrecision >= SystemClock::Minutes;
auto minuteChanged = this->setMinutes(minutePrecision ? time.minute() : 0);
auto hourPrecision = this->mPrecision >= SystemClock::Hours;
auto hourChanged = this->setHours(hourPrecision ? time.hour() : 0);
DropEmitter::call(secondChanged, minuteChanged, hourChanged);
}
void SystemClock::schedule(const QDateTime& targetTime) {
auto secondPrecision = this->mPrecision >= SystemClock::Seconds;
auto minutePrecision = this->mPrecision >= SystemClock::Minutes;
auto hourPrecision = this->mPrecision >= SystemClock::Hours;
auto currentTime = QDateTime::currentDateTime();
auto offset = currentTime.msecsTo(targetTime);
// timer skew
auto nextTime = offset > 0 && offset < 500 ? targetTime : currentTime;
auto baseTimeT = nextTime.time();
nextTime.setTime(
{hourPrecision ? baseTimeT.hour() : 0,
minutePrecision ? baseTimeT.minute() : 0,
secondPrecision ? baseTimeT.second() : 0}
);
if (secondPrecision) nextTime = nextTime.addSecs(1);
else if (minutePrecision) nextTime = nextTime.addSecs(60);
else if (hourPrecision) nextTime = nextTime.addSecs(3600);
auto delay = currentTime.msecsTo(nextTime);
this->timer.start(static_cast<qint32>(delay));
this->targetTime = nextTime;
}
DEFINE_MEMBER_GETSET(SystemClock, hours, setHours);
DEFINE_MEMBER_GETSET(SystemClock, minutes, setMinutes);
DEFINE_MEMBER_GETSET(SystemClock, seconds, setSeconds);

72
src/core/clock.hpp Normal file
View file

@ -0,0 +1,72 @@
#pragma once
#include <qdatetime.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtimer.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "util.hpp"
///! System clock accessor.
class SystemClock: public QObject {
Q_OBJECT;
/// If the clock should update. Defaults to true.
///
/// Setting enabled to false pauses the clock.
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
/// The precision the clock should measure at. Defaults to `SystemClock.Seconds`.
Q_PROPERTY(SystemClock::Enum precision READ precision WRITE setPrecision NOTIFY precisionChanged);
/// The current hour.
Q_PROPERTY(quint32 hours READ hours NOTIFY hoursChanged);
/// The current minute, or 0 if @@precision is `SystemClock.Hours`.
Q_PROPERTY(quint32 minutes READ minutes NOTIFY minutesChanged);
/// The current second, or 0 if @@precision is `SystemClock.Hours` or `SystemClock.Minutes`.
Q_PROPERTY(quint32 seconds READ seconds NOTIFY secondsChanged);
QML_ELEMENT;
public:
// must be named enum until docgen is ready to handle member enums better
enum Enum : quint8 {
Hours = 1,
Minutes = 2,
Seconds = 3,
};
Q_ENUM(Enum);
explicit SystemClock(QObject* parent = nullptr);
[[nodiscard]] bool enabled() const;
void setEnabled(bool enabled);
[[nodiscard]] SystemClock::Enum precision() const;
void setPrecision(SystemClock::Enum precision);
signals:
void enabledChanged();
void precisionChanged();
void hoursChanged();
void minutesChanged();
void secondsChanged();
private slots:
void onTimeout();
private:
bool mEnabled = true;
SystemClock::Enum mPrecision = SystemClock::Seconds;
quint32 mHours = 0;
quint32 mMinutes = 0;
quint32 mSeconds = 0;
QTimer timer;
QDateTime targetTime;
void update();
void setTime(const QDateTime& targetTime);
void schedule(const QDateTime& targetTime);
DECLARE_PRIVATE_MEMBER(SystemClock, hours, setHours, mHours, hoursChanged);
DECLARE_PRIVATE_MEMBER(SystemClock, minutes, setMinutes, mMinutes, minutesChanged);
DECLARE_PRIVATE_MEMBER(SystemClock, seconds, setSeconds, mSeconds, secondsChanged);
};

9
src/core/common.cpp Normal file
View file

@ -0,0 +1,9 @@
#include "common.hpp"
#include <qdatetime.h>
namespace qs {
const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime();
}

11
src/core/common.hpp Normal file
View file

@ -0,0 +1,11 @@
#pragma once
#include <qdatetime.h>
namespace qs {
struct Common {
static const QDateTime LAUNCH_TIME;
};
} // namespace qs

389
src/core/desktopentry.cpp Normal file
View file

@ -0,0 +1,389 @@
#include "desktopentry.hpp"
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qhash.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qpair.h>
#include <qprocess.h>
#include <qstringview.h>
#include <qtenvironmentvariables.h>
#include <ranges>
#include "model.hpp"
Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg);
struct Locale {
explicit Locale() = default;
explicit Locale(const QString& string) {
auto territoryIdx = string.indexOf('_');
auto codesetIdx = string.indexOf('.');
auto modifierIdx = string.indexOf('@');
auto parseEnd = string.length();
if (modifierIdx != -1) {
this->modifier = string.sliced(modifierIdx + 1, parseEnd - modifierIdx - 1);
parseEnd = modifierIdx;
}
if (codesetIdx != -1) {
parseEnd = codesetIdx;
}
if (territoryIdx != -1) {
this->territory = string.sliced(territoryIdx + 1, parseEnd - territoryIdx - 1);
parseEnd = territoryIdx;
}
this->language = string.sliced(0, parseEnd);
}
[[nodiscard]] bool isValid() const { return !this->language.isEmpty(); }
[[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;
auto score = 1;
if (territoryMatches) score += 2;
if (modifierMatches) score += 1;
return score;
}
static const Locale& system() {
static Locale* locale = nullptr; // NOLINT
if (locale == nullptr) {
auto lstr = qEnvironmentVariable("LC_MESSAGES");
if (lstr.isEmpty()) lstr = qEnvironmentVariable("LANG");
locale = new Locale(lstr);
}
return *locale;
}
QString language;
QString territory;
QString modifier;
};
QDebug operator<<(QDebug debug, const Locale& locale) {
auto saver = QDebugStateSaver(debug);
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
<< ", modifier" << locale.modifier << ')';
return debug;
}
void DesktopEntry::parseEntry(const QString& text) {
const auto& system = Locale::system();
auto groupName = QString();
auto entries = QHash<QString, QPair<Locale, QString>>();
auto finishCategory = [this, &groupName, &entries]() {
if (groupName == "Desktop Entry") {
if (entries["Type"].second != "Application") return;
if (entries.contains("Hidden") && entries["Hidden"].second == "true") return;
for (const auto& [key, pair]: entries.asKeyValueRange()) {
auto& [_, value] = pair;
this->mEntries.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);
}
} else if (groupName.startsWith("Desktop Action ")) {
auto actionName = groupName.sliced(16);
auto* action = new DesktopAction(actionName, this);
for (const auto& [key, pair]: entries.asKeyValueRange()) {
const auto& [_, value] = pair;
action->mEntries.insert(key, value);
if (key == "Name") action->mName = value;
else if (key == "Icon") action->mIcon = value;
else if (key == "Exec") action->mExecString = value;
}
this->mActions.insert(actionName, action);
}
entries.clear();
};
for (auto& line: text.split(u'\n', Qt::SkipEmptyParts)) {
if (line.startsWith(u'#')) continue;
if (line.startsWith(u'[') && line.endsWith(u']')) {
finishCategory();
groupName = line.sliced(1, line.length() - 2);
continue;
}
auto splitIdx = line.indexOf(u'=');
if (splitIdx == -1) {
qCWarning(logDesktopEntry) << "Encountered invalid line in desktop entry (no =)" << line;
continue;
}
auto key = line.sliced(0, splitIdx);
const auto& value = line.sliced(splitIdx + 1);
auto localeIdx = key.indexOf('[');
Locale locale;
if (localeIdx != -1 && localeIdx != key.length() - 1) {
locale = Locale(key.sliced(localeIdx + 1, key.length() - localeIdx - 2));
key = key.sliced(0, localeIdx);
}
if (entries.contains(key)) {
const auto& old = entries.value(key);
auto oldScore = system.matchScore(old.first);
auto newScore = system.matchScore(locale);
if (newScore > oldScore || (oldScore == 0 && !locale.isValid())) {
entries.insert(key, qMakePair(locale, value));
}
} else {
entries.insert(key, qMakePair(locale, value));
}
}
finishCategory();
}
void DesktopEntry::execute() const {
DesktopEntry::doExec(this->mExecString, this->mWorkingDirectory);
}
bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); }
bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); }
QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
QVector<QString> arguments;
QString currentArgument;
auto parsingString = false;
auto escape = 0;
auto percent = false;
for (auto c: execString) {
if (escape == 0 && c == u'\\') {
escape = 1;
} else if (parsingString) {
if (c == '\\') {
escape++;
if (escape == 4) {
currentArgument += '\\';
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.
qCWarning(logDesktopEntry).noquote()
<< "Illegal escape sequence in desktop entry exec string:" << execString;
}
currentArgument += c;
escape = 0;
} else if (c == u'"') {
parsingString = false;
} else {
currentArgument += c;
}
} else if (escape != 0) {
currentArgument += c;
escape = 0;
} else if (percent) {
if (c == '%') {
currentArgument += '%';
} // else discard
percent = false;
} else if (c == '%') {
percent = true;
} else if (c == u'"') {
parsingString = true;
} else if (c == u' ') {
if (!currentArgument.isEmpty()) {
arguments.push_back(currentArgument);
currentArgument.clear();
}
} else {
currentArgument += c;
}
}
if (!currentArgument.isEmpty()) {
arguments.push_back(currentArgument);
currentArgument.clear();
}
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.startDetached();
}
void DesktopAction::execute() const {
DesktopEntry::doExec(this->mExecString, this->entry->mWorkingDirectory);
}
DesktopEntryManager::DesktopEntryManager() {
this->scanDesktopEntries();
this->populateApplications();
}
void DesktopEntryManager::scanDesktopEntries() {
QList<QString> dataPaths;
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);
}
}
void DesktopEntryManager::populateApplications() {
for (auto& entry: this->desktopEntries.values()) {
if (!entry->noDisplay()) this->mApplications.insertObject(entry);
}
}
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.path(), prefix + dir.dirName() + "-");
else if (entry.isFile()) {
auto path = entry.filePath();
if (!path.endsWith(".desktop")) {
qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
continue;
}
auto* file = new QFile(path);
if (!file->open(QFile::ReadOnly)) {
qCDebug(logDesktopEntry) << "Could not open file" << path;
continue;
}
auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8);
auto lowerId = id.toLower();
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);
}
}
}
DesktopEntryManager* DesktopEntryManager::instance() {
static auto* instance = new DesktopEntryManager(); // NOLINT
return instance;
}
DesktopEntry* DesktopEntryManager::byId(const QString& id) {
if (auto* entry = this->desktopEntries.value(id)) {
return entry;
} else if (auto* entry = this->lowercaseDesktopEntries.value(id.toLower())) {
return entry;
} else {
return nullptr;
}
}
ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); }
DesktopEntry* DesktopEntries::byId(const QString& id) {
return DesktopEntryManager::instance()->byId(id);
}
ObjectModel<DesktopEntry>* DesktopEntries::applications() {
return DesktopEntryManager::instance()->applications();
}

155
src/core/desktopentry.hpp Normal file
View file

@ -0,0 +1,155 @@
#pragma once
#include <utility>
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qhash.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include "doc.hpp"
#include "model.hpp"
class DesktopAction;
/// 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);
/// Short description of the application, such as "Web Browser". May be empty.
Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT);
/// If true, this application should not be displayed in menus and launchers.
Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT);
/// Long description of the application, such as "View websites on the internet". May be empty.
Q_PROPERTY(QString comment MEMBER mComment CONSTANT);
/// Name of the icon associated with this application. May be empty.
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT);
/// The raw `Exec` string from the desktop entry. You probably want @@execute().
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT);
/// The working directory to execute from.
Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT);
/// 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(QVector<DesktopAction*> actions READ actions CONSTANT);
QML_ELEMENT;
QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
public:
explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
void parseEntry(const QString& text);
/// Run the application. Currently ignores @@runInTerminal and field codes.
Q_INVOKABLE void execute() const;
[[nodiscard]] bool isValid() const;
[[nodiscard]] bool noDisplay() const;
[[nodiscard]] QVector<DesktopAction*> actions() const;
// currently ignores all field codes.
static QVector<QString> parseExecString(const QString& execString);
static void doExec(const QString& execString, const QString& workingDirectory);
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;
private:
QHash<QString, QString> mEntries;
QHash<QString, DesktopAction*> mActions;
friend class DesktopAction;
};
/// An action of a @@DesktopEntry$.
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);
QML_ELEMENT;
QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
public:
explicit DesktopAction(QString id, DesktopEntry* entry)
: QObject(entry)
, entry(entry)
, mId(std::move(id)) {}
/// Run the application. Currently ignores @@DesktopEntry.runInTerminal and field codes.
Q_INVOKABLE void execute() const;
private:
DesktopEntry* entry;
QString mId;
QString mName;
QString mIcon;
QString mExecString;
QHash<QString, QString> mEntries;
friend class DesktopEntry;
};
class DesktopEntryManager: public QObject {
Q_OBJECT;
public:
void scanDesktopEntries();
[[nodiscard]] DesktopEntry* byId(const QString& id);
[[nodiscard]] ObjectModel<DesktopEntry>* applications();
static DesktopEntryManager* instance();
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};
};
///! Desktop entry index.
/// Index of desktop entries according to the [desktop entry specification].
///
/// Primarily useful for looking up icons and metadata from an id, as there is
/// currently no mechanism for usage based sorting of entries and other launcher niceties.
///
/// [desktop entry specification]: https://specifications.freedesktop.org/desktop-entry-spec/latest/
class DesktopEntries: public QObject {
Q_OBJECT;
/// All desktop entries of type Application that are not Hidden or NoDisplay.
QSDOC_TYPE_OVERRIDE(ObjectModel<DesktopEntry>*);
Q_PROPERTY(UntypedObjectModel* applications READ applications CONSTANT);
QML_ELEMENT;
QML_SINGLETON;
public:
explicit DesktopEntries();
/// Look up a desktop entry by name. Includes NoDisplay entries. May return null.
Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id);
[[nodiscard]] static ObjectModel<DesktopEntry>* applications();
};

View file

@ -10,5 +10,14 @@
#define QSDOC_ELEMENT
#define QSDOC_NAMED_ELEMENT(name)
// unmark uncreatable (will be overlayed by other types)
#define QSDOC_CREATABLE
// change the cname used for this type
#define QSDOC_CNAME(name)
// overridden properties
#define QSDOC_PROPERTY_OVERRIDE(...)
// override types of properties for docs
#define QSDOC_TYPE_OVERRIDE(type)

22
src/core/elapsedtimer.cpp Normal file
View file

@ -0,0 +1,22 @@
#include "elapsedtimer.hpp"
#include <qtypes.h>
ElapsedTimer::ElapsedTimer() { this->timer.start(); }
qreal ElapsedTimer::elapsed() { return static_cast<qreal>(this->elapsedNs()) / 1000000000.0; }
qreal ElapsedTimer::restart() { return static_cast<qreal>(this->restartNs()) / 1000000000.0; }
qint64 ElapsedTimer::elapsedMs() { return this->timer.elapsed(); }
qint64 ElapsedTimer::restartMs() { return this->timer.restart(); }
qint64 ElapsedTimer::elapsedNs() { return this->timer.nsecsElapsed(); }
qint64 ElapsedTimer::restartNs() {
// see qelapsedtimer.cpp
auto old = this->timer;
this->timer.start();
return old.durationTo(this->timer).count();
}

45
src/core/elapsedtimer.hpp Normal file
View file

@ -0,0 +1,45 @@
#pragma once
#include <qelapsedtimer.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
///! Measures time between events
/// The ElapsedTimer measures time since its last restart, and is useful
/// for determining the time between events that don't supply it.
class ElapsedTimer: public QObject {
Q_OBJECT;
QML_ELEMENT;
public:
explicit ElapsedTimer();
/// Return the number of seconds since the timer was last
/// started or restarted, with nanosecond precision.
Q_INVOKABLE qreal elapsed();
/// Restart the timer, returning the number of seconds since
/// the timer was last started or restarted, with nanosecond precision.
Q_INVOKABLE qreal restart();
/// Return the number of milliseconds since the timer was last
/// started or restarted.
Q_INVOKABLE qint64 elapsedMs();
/// Restart the timer, returning the number of milliseconds since
/// the timer was last started or restarted.
Q_INVOKABLE qint64 restartMs();
/// Return the number of nanoseconds since the timer was last
/// started or restarted.
Q_INVOKABLE qint64 elapsedNs();
/// Restart the timer, returning the number of nanoseconds since
/// the timer was last started or restarted.
Q_INVOKABLE qint64 restartNs();
private:
QElapsedTimer timer;
};

View file

@ -4,6 +4,8 @@
#include <qcontainerfwd.h>
#include <qcoreapplication.h>
#include <qdebug.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qhash.h>
#include <qlogging.h>
@ -12,7 +14,6 @@
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmlincubator.h>
#include <qtimer.h>
#include <qtmetamacros.h>
#include "iconimageprovider.hpp"
@ -23,10 +24,12 @@
#include "reload.hpp"
#include "scan.hpp"
static QHash<QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
static QHash<const QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
EngineGeneration::EngineGeneration(QmlScanner scanner)
: scanner(std::move(scanner))
EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
: rootPath(rootPath)
, scanner(std::move(scanner))
, urlInterceptor(this->rootPath)
, interceptNetFactory(this->scanner.qmldirIntercepts)
, engine(new QQmlEngine()) {
g_generations.insert(this->engine, this);
@ -39,56 +42,93 @@ EngineGeneration::EngineGeneration(QmlScanner scanner)
this->engine->addImageProvider("qsimage", new QsImageProvider());
this->engine->addImageProvider("qspixmap", new QsPixmapProvider());
QuickshellPlugin::runConstructGeneration(*this);
QsEnginePlugin::runConstructGeneration(*this);
}
EngineGeneration::~EngineGeneration() {
g_generations.remove(this->engine);
delete this->engine;
if (this->engine != nullptr) {
qFatal() << this << "destroyed without calling destroy()";
}
}
void EngineGeneration::destroy() {
// Multiple generations can detect a reload at the same time.
delete this->watcher;
this->watcher = nullptr;
if (this->destroying) return;
this->destroying = true;
// Yes all of this is actually necessary.
if (this->engine != nullptr && this->root != nullptr) {
if (this->watcher != nullptr) {
// Multiple generations can detect a reload at the same time.
QObject::disconnect(this->watcher, nullptr, this, nullptr);
this->watcher->deleteLater();
this->watcher = nullptr;
}
for (auto* extension: this->extensions.values()) {
delete extension;
}
if (this->root != nullptr) {
QObject::connect(this->root, &QObject::destroyed, this, [this]() {
// The timer seems to fix *one* of the possible qml item destructor crashes.
QTimer::singleShot(0, [this]() {
// Garbage is not collected during engine destruction.
this->engine->collectGarbage();
// prevent further js execution between garbage collection and engine destruction.
this->engine->setInterrupted(true);
QObject::connect(this->engine, &QObject::destroyed, this, [this]() { delete this; });
g_generations.remove(this->engine);
// Even after all of that there's still multiple failing assertions and segfaults.
// Pray you don't hit one.
// Note: it appeats *some* of the crashes are related to values owned by the generation.
// Test by commenting the connect() above.
this->engine->deleteLater();
this->engine = nullptr;
});
// Garbage is not collected during engine destruction.
this->engine->collectGarbage();
delete this->engine;
this->engine = nullptr;
auto terminate = this->shouldTerminate;
auto code = this->exitCode;
delete this;
if (terminate) QCoreApplication::exit(code);
});
this->root->deleteLater();
this->root = nullptr;
} else {
g_generations.remove(this->engine);
// the engine has never been used, no need to clean up
delete this->engine;
this->engine = nullptr;
auto terminate = this->shouldTerminate;
auto code = this->exitCode;
delete this;
if (terminate) QCoreApplication::exit(code);
}
}
void EngineGeneration::shutdown() {
if (this->destroying) return;
delete this->root;
this->root = nullptr;
delete this->engine;
this->engine = nullptr;
delete this;
}
void EngineGeneration::onReload(EngineGeneration* old) {
if (old != nullptr) {
// if the old generation holds the window incubation controller as the
// new generation acquires it then incubators will hang intermittently
old->incubationControllers.clear();
qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old;
old->incubationControllersLocked = true;
old->assignIncubationController();
}
auto* app = QCoreApplication::instance();
QObject::connect(this->engine, &QQmlEngine::quit, app, &QCoreApplication::quit);
QObject::connect(this->engine, &QQmlEngine::exit, app, &QCoreApplication::exit);
QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit);
QObject::connect(this->engine, &QQmlEngine::exit, this, &EngineGeneration::exit);
if (auto* reloadable = qobject_cast<Reloadable*>(this->root)) {
reloadable->reload(old ? old->root : nullptr);
}
this->root->reload(old == nullptr ? nullptr : old->root);
this->singletonRegistry.onReload(old == nullptr ? nullptr : &old->singletonRegistry);
this->reloadComplete = true;
emit this->reloadFinished();
@ -105,7 +145,7 @@ void EngineGeneration::postReload() {
// This can be called on a generation during its destruction.
if (this->engine == nullptr || this->root == nullptr) return;
QuickshellPlugin::runOnReload();
QsEnginePlugin::runOnReload();
PostReloadHook::postReloadTree(this->root);
this->singletonRegistry.onPostReload();
}
@ -117,13 +157,21 @@ void EngineGeneration::setWatchingFiles(bool watching) {
for (auto& file: this->scanner.scannedFiles) {
this->watcher->addPath(file);
this->watcher->addPath(QFileInfo(file).dir().absolutePath());
}
QObject::connect(
this->watcher,
&QFileSystemWatcher::fileChanged,
this,
&EngineGeneration::filesChanged
&EngineGeneration::onFileChanged
);
QObject::connect(
this->watcher,
&QFileSystemWatcher::directoryChanged,
this,
&EngineGeneration::onDirectoryChanged
);
}
} else {
@ -134,28 +182,44 @@ void EngineGeneration::setWatchingFiles(bool watching) {
}
}
void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) {
auto* obj = dynamic_cast<QObject*>(controller);
void EngineGeneration::onFileChanged(const QString& name) {
if (!this->watcher->files().contains(name)) {
this->deletedWatchedFiles.push_back(name);
} else {
emit this->filesChanged();
}
}
void EngineGeneration::onDirectoryChanged() {
// try to find any files that were just deleted from a replace operation
for (auto& file: this->deletedWatchedFiles) {
if (QFileInfo(file).exists()) {
emit this->filesChanged();
break;
}
}
}
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 (obj == nullptr) {
qCDebug(logIncubator) << "Could not register incubation controller as it is not a QObject"
<< controller;
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, obj});
QObject::connect(
obj,
&QObject::destroyed,
this,
&EngineGeneration::incubationControllerDestroyed
);
qCDebug(logIncubator) << "Registered incubation controller" << controller;
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;
@ -166,22 +230,20 @@ void EngineGeneration::registerIncubationController(QQmlIncubationController* co
}
void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) {
QObject* obj = nullptr;
this->incubationControllers.removeIf([&](QPair<QQmlIncubationController*, QObject*> other) {
if (controller == other.first) {
obj = other.second;
return true;
} else return false;
});
if (obj == nullptr) {
qCWarning(logIncubator) << "Failed to deregister incubation controller" << controller
<< "as it was not registered to begin with";
qCWarning(logIncubator) << "Current registered incuabation controllers"
<< this->incubationControllers;
} else {
if (auto* obj = dynamic_cast<QObject*>(controller)) {
QObject::disconnect(obj, nullptr, this, nullptr);
qCDebug(logIncubator) << "Deregistered incubation controller" << controller;
} 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.
@ -196,22 +258,25 @@ void EngineGeneration::deregisterIncubationController(QQmlIncubationController*
void EngineGeneration::incubationControllerDestroyed() {
auto* sender = this->sender();
QQmlIncubationController* controller = nullptr;
this->incubationControllers.removeIf([&](QPair<QQmlIncubationController*, QObject*> other) {
if (sender == other.second) {
controller = other.first;
return true;
} else return false;
});
auto* controller = dynamic_cast<QQmlIncubationController*>(sender);
if (controller == nullptr) {
qCCritical(logIncubator) << "Destroyed incubation controller" << this->sender()
<< "could not be identified, this may cause memory corruption";
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 {
qCDebug(logIncubator) << "Destroyed incubation controller" << controller << "deregistered";
qCCritical(logIncubator) << "Destroyed incubation controller" << controller
<< "was not registered, but its destruction was observed by" << this;
return;
}
// This function can run during destruction.
@ -224,23 +289,64 @@ void EngineGeneration::incubationControllerDestroyed() {
}
}
void EngineGeneration::registerExtension(const void* key, EngineGenerationExt* extension) {
if (this->extensions.contains(key)) {
delete this->extensions.value(key);
}
this->extensions.insert(key, extension);
}
EngineGenerationExt* EngineGeneration::findExtension(const void* key) {
return this->extensions.value(key);
}
void EngineGeneration::quit() {
this->shouldTerminate = true;
this->destroy();
}
void EngineGeneration::exit(int code) {
this->shouldTerminate = true;
this->exitCode = code;
this->destroy();
}
void EngineGeneration::assignIncubationController() {
QQmlIncubationController* controller = nullptr;
if (this->incubationControllers.isEmpty()) controller = &this->delayedIncubationController;
else controller = this->incubationControllers.first().first;
qCDebug(logIncubator) << "Assigning incubation controller to engine:" << controller
if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) {
controller = &this->delayedIncubationController;
} else {
controller = this->incubationControllers.first();
}
qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation"
<< this
<< "fallback:" << (controller == &this->delayedIncubationController);
this->engine->setIncubationController(controller);
}
EngineGeneration* EngineGeneration::findObjectGeneration(QObject* object) {
EngineGeneration* EngineGeneration::currentGeneration() {
if (g_generations.size() == 1) {
return *g_generations.begin();
} else return nullptr;
}
EngineGeneration* EngineGeneration::findEngineGeneration(const QQmlEngine* engine) {
return g_generations.value(engine);
}
EngineGeneration* EngineGeneration::findObjectGeneration(const QObject* object) {
// Objects can still attempt to find their generation after it has been destroyed.
// if (g_generations.size() == 1) return EngineGeneration::currentGeneration();
while (object != nullptr) {
auto* context = QQmlEngine::contextForObject(object);
if (context != nullptr) {
if (auto* generation = g_generations.value(context->engine())) {
if (auto* generation = EngineGeneration::findEngineGeneration(context->engine())) {
return generation;
}
}

View file

@ -1,25 +1,34 @@
#pragma once
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qfilesystemwatcher.h>
#include <qhash.h>
#include <qobject.h>
#include <qpair.h>
#include <qqmlengine.h>
#include <qqmlincubator.h>
#include <qtclasshelpermacros.h>
#include "incubator.hpp"
#include "qsintercept.hpp"
#include "scan.hpp"
#include "shell.hpp"
#include "singleton.hpp"
class RootWrapper;
class QuickshellGlobal;
class EngineGenerationExt {
public:
EngineGenerationExt() = default;
virtual ~EngineGenerationExt() = default;
Q_DISABLE_COPY_MOVE(EngineGenerationExt);
};
class EngineGeneration: public QObject {
Q_OBJECT;
public:
explicit EngineGeneration(QmlScanner scanner);
explicit EngineGeneration(const QDir& rootPath, QmlScanner scanner);
~EngineGeneration() override;
Q_DISABLE_COPY_MOVE(EngineGeneration);
@ -30,30 +39,55 @@ public:
void registerIncubationController(QQmlIncubationController* controller);
void deregisterIncubationController(QQmlIncubationController* controller);
static EngineGeneration* findObjectGeneration(QObject* object);
// takes ownership
void registerExtension(const void* key, EngineGenerationExt* extension);
EngineGenerationExt* findExtension(const void* key);
static EngineGeneration* findEngineGeneration(const QQmlEngine* engine);
static EngineGeneration* findObjectGeneration(const QObject* object);
// Returns the current generation if there is only one generation,
// otherwise null.
static EngineGeneration* currentGeneration();
RootWrapper* wrapper = nullptr;
QDir rootPath;
QmlScanner scanner;
QsUrlInterceptor urlInterceptor;
QsInterceptNetworkAccessManagerFactory interceptNetFactory;
QQmlEngine* engine = nullptr;
ShellRoot* root = nullptr;
QObject* root = nullptr;
SingletonRegistry singletonRegistry;
QFileSystemWatcher* watcher = nullptr;
QVector<QString> deletedWatchedFiles;
DelayedQmlIncubationController delayedIncubationController;
bool reloadComplete = false;
QuickshellGlobal* qsgInstance = nullptr;
void destroy();
void shutdown();
signals:
void filesChanged();
void reloadFinished();
public slots:
void quit();
void exit(int code);
private slots:
void onFileChanged(const QString& name);
void onDirectoryChanged();
void incubationControllerDestroyed();
private:
void postReload();
void assignIncubationController();
QVector<QPair<QQmlIncubationController*, QObject*>> incubationControllers;
QVector<QQmlIncubationController*> incubationControllers;
bool incubationControllersLocked = false;
QHash<const void*, EngineGenerationExt*> extensions;
bool destroying = false;
bool shouldTerminate = false;
int exitCode = 0;
};

View file

@ -11,7 +11,9 @@
QPixmap
IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) {
QString iconName;
QString fallbackName;
QString path;
auto splitIdx = id.indexOf("?path=");
if (splitIdx != -1) {
iconName = id.sliced(0, splitIdx);
@ -19,10 +21,17 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for"
<< id;
} else {
iconName = id;
splitIdx = id.indexOf("?fallback=");
if (splitIdx != -1) {
iconName = id.sliced(0, splitIdx);
fallbackName = id.sliced(splitIdx + 10);
} else {
iconName = id;
}
}
auto icon = QIcon::fromTheme(iconName);
if (icon.isNull()) icon = QIcon::fromTheme(fallbackName);
auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100);
if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2);
@ -55,12 +64,20 @@ QPixmap IconImageProvider::missingPixmap(const QSize& size) {
return pixmap;
}
QString IconImageProvider::requestString(const QString& icon, const QString& path) {
QString IconImageProvider::requestString(
const QString& icon,
const QString& path,
const QString& fallback
) {
auto req = "image://icon/" + icon;
if (!path.isEmpty()) {
req += "?path=" + path;
}
if (!fallback.isEmpty()) {
req += "?fallback=" + fallback;
}
return req;
}

View file

@ -10,5 +10,10 @@ public:
QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override;
static QPixmap missingPixmap(const QSize& size);
static QString requestString(const QString& icon, const QString& path);
static QString requestString(
const QString& icon,
const QString& path = QString(),
const QString& fallback = QString()
);
};

105
src/core/iconprovider.cpp Normal file
View file

@ -0,0 +1,105 @@
#include "iconprovider.hpp"
#include <utility>
#include <qicon.h>
#include <qiconengine.h>
#include <qlogging.h>
#include <qobject.h>
#include <qpixmap.h>
#include <qqmlengine.h>
#include <qquickimageprovider.h>
#include <qrect.h>
#include <qsize.h>
#include <qstring.h>
#include "generation.hpp"
// QMenu re-calls pixmap() every time the mouse moves so its important to cache it.
class PixmapCacheIconEngine: public QIconEngine {
void paint(
QPainter* /*unused*/,
const QRect& /*unused*/,
QIcon::Mode /*unused*/,
QIcon::State /*unused*/
) override {
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 {
if (this->lastPixmap.isNull() || size != this->lastSize) {
this->lastPixmap = this->createPixmap(size);
this->lastSize = size;
}
return this->lastPixmap;
}
virtual QPixmap createPixmap(const QSize& size) = 0;
private:
QSize lastSize;
QPixmap lastPixmap;
};
class ImageProviderIconEngine: public PixmapCacheIconEngine {
public:
explicit ImageProviderIconEngine(QQuickImageProvider* provider, QString id)
: provider(provider)
, id(std::move(id)) {}
QPixmap createPixmap(const QSize& size) override {
if (this->provider->imageType() == QQmlImageProviderBase::Pixmap) {
return this->provider->requestPixmap(this->id, nullptr, size);
} else if (this->provider->imageType() == QQmlImageProviderBase::Image) {
auto image = this->provider->requestImage(this->id, nullptr, size);
return QPixmap::fromImage(image);
} else {
qFatal() << "Unexpected ImageProviderIconEngine image type" << this->provider->imageType();
return QPixmap(); // never reached, satisfies lint
}
}
[[nodiscard]] QIconEngine* clone() const override {
return new ImageProviderIconEngine(this->provider, this->id);
}
private:
QQuickImageProvider* provider;
QString id;
};
QIcon getEngineImageAsIcon(QQmlEngine* engine, const QUrl& url) {
if (!engine || url.isEmpty()) return QIcon();
auto scheme = url.scheme();
if (scheme == "image") {
auto providerName = url.authority();
auto path = url.path();
if (!path.isEmpty()) path = path.sliced(1);
auto* provider = qobject_cast<QQuickImageProvider*>(engine->imageProvider(providerName));
if (provider == nullptr) {
qWarning() << "iconByUrl failed: no provider found for" << url;
return QIcon();
}
if (provider->imageType() == QQmlImageProviderBase::Pixmap
|| provider->imageType() == QQmlImageProviderBase::Image)
{
return QIcon(new ImageProviderIconEngine(provider, path));
}
} else {
qWarning() << "iconByUrl failed: unsupported scheme" << scheme << "in path" << url;
}
return QIcon();
}
QIcon getCurrentEngineImageAsIcon(const QUrl& url) {
auto* generation = EngineGeneration::currentGeneration();
if (!generation) return QIcon();
return getEngineImageAsIcon(generation->engine, url);
}

View file

@ -0,0 +1,8 @@
#pragma once
#include <qicon.h>
#include <qqmlengine.h>
#include <qurl.h>
QIcon getEngineImageAsIcon(QQmlEngine* engine, const QUrl& url);
QIcon getCurrentEngineImageAsIcon(const QUrl& url);

35
src/core/instanceinfo.cpp Normal file
View file

@ -0,0 +1,35 @@
#include "instanceinfo.hpp"
#include <qdatastream.h>
QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) {
stream << info.instanceId << info.configPath << info.shellId << info.launchTime;
return stream;
}
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) {
stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime;
return stream;
}
QDataStream& operator<<(QDataStream& stream, const RelaunchInfo& info) {
stream << info.instance << info.noColor << info.timestamp << info.sparseLogsOnly
<< info.defaultLogLevel << info.logRules;
return stream;
}
QDataStream& operator>>(QDataStream& stream, RelaunchInfo& info) {
stream >> info.instance >> info.noColor >> info.timestamp >> info.sparseLogsOnly
>> info.defaultLogLevel >> info.logRules;
return stream;
}
InstanceInfo InstanceInfo::CURRENT = {}; // NOLINT
namespace qs::crash {
CrashInfo CrashInfo::INSTANCE = {}; // NOLINT
}

39
src/core/instanceinfo.hpp Normal file
View file

@ -0,0 +1,39 @@
#pragma once
#include <qdatetime.h>
#include <qlogging.h>
#include <qstring.h>
struct InstanceInfo {
QString instanceId;
QString configPath;
QString shellId;
QDateTime launchTime;
static InstanceInfo CURRENT; // NOLINT
};
struct RelaunchInfo {
InstanceInfo instance;
bool noColor = false;
bool timestamp = false;
bool sparseLogsOnly = false;
QtMsgType defaultLogLevel = QtWarningMsg;
QString logRules;
};
QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info);
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info);
QDataStream& operator<<(QDataStream& stream, const RelaunchInfo& info);
QDataStream& operator>>(QDataStream& stream, RelaunchInfo& info);
namespace qs::crash {
struct CrashInfo {
int logFd = -1;
static CrashInfo INSTANCE; // NOLINT
};
} // namespace qs::crash

View file

@ -179,7 +179,9 @@ void LazyLoader::incubateIfReady(bool overrideReloadCheck) {
void LazyLoader::onIncubationCompleted() {
this->setItem(this->incubator->object());
delete this->incubator;
// The incubator is not necessarily inert at the time of this callback,
// so deleteLater is required.
this->incubator->deleteLater();
this->incubator = nullptr;
this->targetLoading = false;
emit this->loadingChanged();

View file

@ -79,7 +79,7 @@
/// > [!WARNING] Components that internally load other components must explicitly
/// > support asynchronous loading to avoid blocking.
/// >
/// > Notably, [Variants](../variants) does not corrently support asynchronous
/// > 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.
///
@ -87,8 +87,8 @@
/// > 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`
/// if neither `loading` or `active`.
/// The fully loaded item if the loader is @@loading or @@active, or `null`
/// if neither @@loading nor @@active.
///
/// Note that the item is owned by the LazyLoader, and destroying the LazyLoader
/// will destroy the item.
@ -96,7 +96,7 @@ class LazyLoader: public Reloadable {
/// > [!WARNING] If you access the `item` of a loader that is currently loading,
/// > it will block as if you had set `active` to true immediately beforehand.
/// >
/// > You can instead set `loading` and listen to the `activeChanged` signal to
/// > You can instead set @@loading and listen to @@activeChanged(s) signal to
/// > ensure loading happens asynchronously.
Q_PROPERTY(QObject* item READ item NOTIFY itemChanged);
/// If the loader is actively loading.
@ -105,7 +105,7 @@ class LazyLoader: public Reloadable {
/// loading it asynchronously. If the component is already loaded, setting
/// this property has no effect.
///
/// See also: [activeAsync](#prop.activeAsync).
/// See also: @@activeAsync.
Q_PROPERTY(bool loading READ isLoading WRITE setLoading NOTIFY loadingChanged);
/// If the component is fully loaded.
///
@ -113,17 +113,17 @@ class LazyLoader: public Reloadable {
/// blocking the UI, and setting it to `false` will destroy the component, requiring
/// it to be loaded again.
///
/// See also: [activeAsync](#prop.activeAsync).
/// See also: @@activeAsync.
Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged);
/// If the component is fully loaded.
///
/// Setting this property to true will asynchronously load the component similarly to
/// [loading](#prop.loading). Reading it or setting it to false will behanve
/// the same as [active](#prop.active).
/// @@loading. Reading it or setting it to false will behanve
/// the same as @@active.
Q_PROPERTY(bool activeAsync READ isActive WRITE setActiveAsync NOTIFY activeChanged);
/// The component to load. Mutually exclusive to `source`.
/// The component to load. Mutually exclusive to @@source.
Q_PROPERTY(QQmlComponent* component READ component WRITE setComponent NOTIFY componentChanged);
/// The URI to load the component from. Mutually exclusive to `component`.
/// The URI to load the component from. Mutually exclusive to @@component.
Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged);
Q_CLASSINFO("DefaultProperty", "component");
QML_ELEMENT;

937
src/core/logging.cpp Normal file
View file

@ -0,0 +1,937 @@
#include "logging.hpp"
#include <array>
#include <cerrno>
#include <cstdio>
#include <fcntl.h>
#include <qbytearrayview.h>
#include <qcoreapplication.h>
#include <qdatetime.h>
#include <qendian.h>
#include <qfilesystemwatcher.h>
#include <qhash.h>
#include <qhashfunctions.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qpair.h>
#include <qstring.h>
#include <qstringview.h>
#include <qsysinfo.h>
#include <qtenvironmentvariables.h>
#include <qtextstream.h>
#include <qthread.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <sys/mman.h>
#include <sys/sendfile.h>
#include "instanceinfo.hpp"
#include "logging_p.hpp"
#include "logging_qtprivate.cpp" // NOLINT
#include "paths.hpp"
#include "ringbuf.hpp"
Q_LOGGING_CATEGORY(logBare, "quickshell.bare");
namespace qs::log {
using namespace qt_logging_registry;
Q_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg);
bool LogMessage::operator==(const LogMessage& other) const {
// note: not including time
return this->type == other.type && this->category == other.category && this->body == other.body;
}
size_t qHash(const LogMessage& message) {
return qHash(message.type) ^ qHash(message.category) ^ qHash(message.body);
}
void LogMessage::formatMessage(
QTextStream& stream,
const LogMessage& msg,
bool color,
bool timestamp,
const QString& prefix
) {
if (!prefix.isEmpty()) {
if (color) stream << "\033[90m";
stream << '[' << prefix << ']';
if (timestamp) stream << ' ';
if (color) stream << "\033[0m";
}
if (timestamp) {
if (color) stream << "\033[90m";
stream << msg.time.toString("yyyy-MM-dd hh:mm:ss.zzz");
}
if (msg.category == "quickshell.bare") {
if (!prefix.isEmpty()) stream << ' ';
stream << msg.body;
} else {
if (color) {
switch (msg.type) {
case QtDebugMsg: stream << "\033[34m DEBUG"; break;
case QtInfoMsg: stream << "\033[32m INFO"; break;
case QtWarningMsg: stream << "\033[33m WARN"; break;
case QtCriticalMsg: stream << "\033[31m ERROR"; break;
case QtFatalMsg: stream << "\033[31m FATAL"; break;
}
} else {
switch (msg.type) {
case QtDebugMsg: stream << " DEBUG"; break;
case QtInfoMsg: stream << " INFO"; break;
case QtWarningMsg: stream << " WARN"; break;
case QtCriticalMsg: stream << " ERROR"; break;
case QtFatalMsg: stream << " FATAL"; break;
}
}
const auto isDefault = msg.category == "default";
if (color && !isDefault && msg.type != QtFatalMsg) stream << "\033[97m";
if (!isDefault) {
stream << ' ' << msg.category;
}
if (color && msg.type != QtFatalMsg) stream << "\033[0m";
stream << ": " << msg.body;
if (color && msg.type == QtFatalMsg) stream << "\033[0m";
}
}
bool CategoryFilter::shouldDisplay(QtMsgType type) const {
switch (type) {
case QtDebugMsg: return this->debug;
case QtInfoMsg: return this->info;
case QtWarningMsg: return this->warn;
case QtCriticalMsg: return this->critical;
default: return true;
}
}
void CategoryFilter::apply(QLoggingCategory* category) const {
category->setEnabled(QtDebugMsg, this->debug);
category->setEnabled(QtInfoMsg, this->info);
category->setEnabled(QtWarningMsg, this->warn);
category->setEnabled(QtCriticalMsg, this->critical);
}
void CategoryFilter::applyRule(
QLatin1StringView category,
const qt_logging_registry::QLoggingRule& rule
) {
auto filterpass = rule.pass(category, QtDebugMsg);
if (filterpass != 0) this->debug = filterpass > 0;
filterpass = rule.pass(category, QtInfoMsg);
if (filterpass != 0) this->info = filterpass > 0;
filterpass = rule.pass(category, QtWarningMsg);
if (filterpass != 0) this->warn = filterpass > 0;
filterpass = rule.pass(category, QtCriticalMsg);
if (filterpass != 0) this->critical = filterpass > 0;
}
LogManager::LogManager(): stdoutStream(stdout) {}
void LogManager::messageHandler(
QtMsgType type,
const QMessageLogContext& context,
const QString& msg
) {
auto message = LogMessage(type, QLatin1StringView(context.category), msg.toUtf8());
auto* self = LogManager::instance();
auto display = true;
const auto* key = static_cast<const void*>(context.category);
if (self->sparseFilters.contains(key)) {
display = self->sparseFilters.value(key).shouldDisplay(type);
}
if (display) {
LogMessage::formatMessage(
self->stdoutStream,
message,
self->colorLogs,
self->timestampLogs,
self->prefix
);
self->stdoutStream << Qt::endl;
}
emit self->logMessage(message, display);
}
void LogManager::filterCategory(QLoggingCategory* category) {
auto* instance = LogManager::instance();
auto categoryName = QLatin1StringView(category->categoryName());
auto isQs = categoryName.startsWith(QLatin1StringView("quickshell."));
if (instance->lastCategoryFilter) {
instance->lastCategoryFilter(category);
}
auto filter = CategoryFilter(category);
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;
}
for (const auto& rule: *instance->rules) {
filter.applyRule(categoryName, rule);
}
if (isQs && !instance->sparse) {
// We assume the category name pointer will always be the same and be comparable in the message handler.
instance->sparseFilters.insert(static_cast<const void*>(category->categoryName()), filter);
// all enabled by default
CategoryFilter().apply(category);
} else {
filter.apply(category);
}
instance->allFilters.insert(categoryName, filter);
}
LogManager* LogManager::instance() {
static auto* instance = new LogManager(); // NOLINT
return instance;
}
void LogManager::init(
bool color,
bool timestamp,
bool sparseOnly,
QtMsgType defaultLevel,
const QString& rules,
const QString& prefix
) {
auto* instance = LogManager::instance();
instance->colorLogs = color;
instance->timestampLogs = timestamp;
instance->sparse = sparseOnly;
instance->prefix = prefix;
instance->mDefaultLevel = defaultLevel;
instance->mRulesString = rules;
{
QLoggingSettingsParser parser;
parser.setContent(rules);
instance->rules = new QList(parser.rules());
}
qInstallMessageHandler(&LogManager::messageHandler);
instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory);
qCDebug(logLogging) << "Creating offthread logger...";
auto* thread = new QThread();
instance->threadProxy.moveToThread(thread);
thread->start();
QMetaObject::invokeMethod(
&instance->threadProxy,
&LoggingThreadProxy::initInThread,
Qt::BlockingQueuedConnection
);
qCDebug(logLogging) << "Logger initialized.";
}
void LogManager::initFs() {
QMetaObject::invokeMethod(
&LogManager::instance()->threadProxy,
"initFs",
Qt::BlockingQueuedConnection
);
}
QString LogManager::rulesString() const { return this->mRulesString; }
QtMsgType LogManager::defaultLevel() const { return this->mDefaultLevel; }
bool LogManager::isSparse() const { return this->sparse; }
CategoryFilter LogManager::getFilter(QLatin1StringView category) {
return this->allFilters.value(category);
}
void LoggingThreadProxy::initInThread() {
this->logging = new ThreadLogging(this);
this->logging->init();
}
void LoggingThreadProxy::initFs() { this->logging->initFs(); }
void ThreadLogging::init() {
auto logMfd = memfd_create("quickshell:logs", 0);
if (logMfd == -1) {
qCCritical(logLogging) << "Failed to create memfd for initial log storage"
<< qt_error_string(-1);
}
auto dlogMfd = memfd_create("quickshell:detailedlogs", 0);
if (dlogMfd == -1) {
qCCritical(logLogging) << "Failed to create memfd for initial detailed log storage"
<< qt_error_string(-1);
}
if (logMfd != -1) {
this->file = new QFile();
this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle);
this->fileStream.setDevice(this->file);
}
if (dlogMfd != -1) {
crash::CrashInfo::INSTANCE.logFd = dlogMfd;
this->detailedFile = new QFile();
// buffered by WriteBuffer
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;
}
}
// This connection is direct so it works while the event loop is destroyed between
// QCoreApplication delete and Q(Gui)Application launch.
QObject::connect(
LogManager::instance(),
&LogManager::logMessage,
this,
&ThreadLogging::onMessage,
Qt::DirectConnection
);
qCDebug(logLogging) << "Created memfd" << logMfd << "for early logs.";
qCDebug(logLogging) << "Created memfd" << dlogMfd << "for early detailed logs.";
}
void ThreadLogging::initFs() {
qCDebug(logLogging) << "Starting filesystem logging...";
auto* runDir = QsPaths::instance()->instanceRunDir();
if (!runDir) {
qCCritical(logLogging
) << "Could not start filesystem logging as the runtime directory could not be created.";
return;
}
auto path = runDir->filePath("log.log");
auto detailedPath = runDir->filePath("log.qslog");
auto* file = new QFile(path);
auto* detailedFile = new QFile(detailedPath);
if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
qCCritical(logLogging
) << "Could not start filesystem logger as the log file could not be created:"
<< path;
delete file;
file = nullptr;
} else {
qInfo() << "Saving logs to" << path;
}
// buffered by WriteBuffer
if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) {
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 {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0,
.l_pid = 0,
};
if (fcntl(detailedFile->handle(), F_SETLK, &lock) != 0) { // NOLINT
qCWarning(logLogging) << "Unable to set lock marker on detailed log file. --follow from "
"other instances will not work.";
}
qCInfo(logLogging) << "Saving detailed logs to" << path;
}
qCDebug(logLogging) << "Copying memfd logs to log file...";
if (file) {
auto* oldFile = this->file;
if (oldFile) {
oldFile->seek(0);
sendfile(file->handle(), oldFile->handle(), nullptr, oldFile->size());
}
this->file = file;
this->fileStream.setDevice(file);
delete oldFile;
}
if (detailedFile) {
auto* oldFile = this->detailedFile;
if (oldFile) {
oldFile->seek(0);
sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size());
}
crash::CrashInfo::INSTANCE.logFd = detailedFile->handle();
this->detailedFile = detailedFile;
this->detailedWriter.setDevice(detailedFile);
if (!oldFile) {
if (!this->detailedWriter.writeHeader()) {
qCCritical(logLogging) << "Could not write header for detailed logs.";
this->detailedWriter.setDevice(nullptr);
delete this->detailedFile;
this->detailedFile = nullptr;
}
}
delete oldFile;
}
qCDebug(logLogging) << "Switched logging to disk logs.";
auto* logManager = LogManager::instance();
QObject::disconnect(logManager, &LogManager::logMessage, this, &ThreadLogging::onMessage);
QObject::connect(
logManager,
&LogManager::logMessage,
this,
&ThreadLogging::onMessage,
Qt::QueuedConnection
);
qCDebug(logLogging) << "Switched threaded logger to queued eventloop connection.";
}
void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) {
if (showInSparse) {
if (this->fileStream.device() == nullptr) return;
LogMessage::formatMessage(this->fileStream, msg, false, true);
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.";
}
}
CompressedLogType compressedTypeOf(QtMsgType type) {
switch (type) {
case QtDebugMsg: return CompressedLogType::Debug;
case QtInfoMsg: return CompressedLogType::Info;
case QtWarningMsg: return CompressedLogType::Warn;
case QtCriticalMsg:
case QtFatalMsg: return CompressedLogType::Critical;
}
return CompressedLogType::Info; // unreachable under normal conditions
}
QtMsgType typeOfCompressed(CompressedLogType type) {
switch (type) {
case CompressedLogType::Debug: return QtDebugMsg;
case CompressedLogType::Info: return QtInfoMsg;
case CompressedLogType::Warn: return QtWarningMsg;
case CompressedLogType::Critical: return QtCriticalMsg;
}
return QtInfoMsg; // unreachable under normal conditions
}
void WriteBuffer::setDevice(QIODevice* device) { this->device = device; }
bool WriteBuffer::hasDevice() const { return this->device; }
bool WriteBuffer::flush() {
auto written = this->device->write(this->buffer);
auto success = written == this->buffer.length();
this->buffer.clear();
return success;
}
void WriteBuffer::writeBytes(const char* data, qsizetype length) {
this->buffer.append(data, length);
}
void WriteBuffer::writeU8(quint8 data) { this->writeBytes(reinterpret_cast<char*>(&data), 1); }
void WriteBuffer::writeU16(quint16 data) {
data = qToLittleEndian(data);
this->writeBytes(reinterpret_cast<char*>(&data), 2);
}
void WriteBuffer::writeU32(quint32 data) {
data = qToLittleEndian(data);
this->writeBytes(reinterpret_cast<char*>(&data), 4);
}
void WriteBuffer::writeU64(quint64 data) {
data = qToLittleEndian(data);
this->writeBytes(reinterpret_cast<char*>(&data), 8);
}
void DeviceReader::setDevice(QIODevice* device) { this->device = device; }
bool DeviceReader::hasDevice() const { return this->device; }
bool DeviceReader::readBytes(char* data, qsizetype length) {
return this->device->read(data, length) == length;
}
qsizetype DeviceReader::peekBytes(char* data, qsizetype length) {
return this->device->peek(data, length);
}
bool DeviceReader::skip(qsizetype length) { return this->device->skip(length) == length; }
bool DeviceReader::readU8(quint8* data) {
return this->readBytes(reinterpret_cast<char*>(data), 1);
}
bool DeviceReader::readU16(quint16* data) {
return this->readBytes(reinterpret_cast<char*>(data), 2);
}
bool DeviceReader::readU32(quint32* data) {
return this->readBytes(reinterpret_cast<char*>(data), 4);
}
bool DeviceReader::readU64(quint64* data) {
return this->readBytes(reinterpret_cast<char*>(data), 8);
}
void EncodedLogWriter::setDevice(QIODevice* target) { this->buffer.setDevice(target); }
void EncodedLogReader::setDevice(QIODevice* source) { this->reader.setDevice(source); }
constexpr quint8 LOG_VERSION = 2;
bool EncodedLogWriter::writeHeader() {
this->buffer.writeU8(LOG_VERSION);
return this->buffer.flush();
}
bool EncodedLogReader::readHeader(bool* success, quint8* version, quint8* readerVersion) {
if (!this->reader.readU8(version)) return false;
*success = *version == LOG_VERSION;
*readerVersion = LOG_VERSION;
return true;
}
bool EncodedLogWriter::write(const LogMessage& message) {
if (!this->buffer.hasDevice()) return false;
LogMessage* prevMessage = nullptr;
auto index = this->recentMessages.indexOf(message, &prevMessage);
// If its a dupe, save memory by reusing the buffer of the first message and letting
// the new one be deallocated.
auto body = prevMessage ? prevMessage->body : message.body;
this->recentMessages.emplace(message.type, message.category, body, message.time);
if (index != -1) {
auto secondDelta = this->lastMessageTime.secsTo(message.time);
if (secondDelta < 16 && index < 16) {
this->writeOp(EncodedLogOpcode::RecentMessageShort);
this->buffer.writeU8(index | (secondDelta << 4));
} else {
this->writeOp(EncodedLogOpcode::RecentMessageLong);
this->buffer.writeU8(index);
this->writeVarInt(secondDelta);
}
goto finish;
} else {
auto categoryId = this->getOrCreateCategory(message.category);
this->writeVarInt(categoryId);
auto writeFullTimestamp = [this, &message]() {
this->buffer.writeU64(message.time.toSecsSinceEpoch());
};
if (message.type == QtFatalMsg) {
this->buffer.writeU8(0xff);
writeFullTimestamp();
} else {
quint8 field = compressedTypeOf(message.type);
auto secondDelta = this->lastMessageTime.secsTo(message.time);
if (secondDelta >= 0x1d) {
// 0x1d = followed by delta int
// 0x1e = followed by epoch delta int
field |= (secondDelta < 0xffff ? 0x1d : 0x1e) << 3;
} else {
field |= secondDelta << 3;
}
this->buffer.writeU8(field);
if (secondDelta >= 0x1d) {
if (secondDelta > 0xffff) {
writeFullTimestamp();
} else {
this->writeVarInt(secondDelta);
}
}
}
this->writeString(message.body);
}
finish:
// copy with second precision
this->lastMessageTime = QDateTime::fromSecsSinceEpoch(message.time.toSecsSinceEpoch());
return this->buffer.flush();
}
bool EncodedLogReader::read(LogMessage* slot) {
start:
quint32 next = 0;
if (!this->readVarInt(&next)) return false;
if (next < EncodedLogOpcode::BeginCategories) {
if (next == EncodedLogOpcode::RegisterCategory) {
if (!this->registerCategory()) return false;
goto start;
} else if (next == EncodedLogOpcode::RecentMessageShort
|| next == EncodedLogOpcode::RecentMessageLong)
{
quint8 index = 0;
quint32 secondDelta = 0;
if (next == EncodedLogOpcode::RecentMessageShort) {
quint8 field = 0;
if (!this->reader.readU8(&field)) return false;
index = field & 0xf;
secondDelta = field >> 4;
} else {
if (!this->reader.readU8(&index)) return false;
if (!this->readVarInt(&secondDelta)) return false;
}
if (index >= this->recentMessages.size()) return false;
*slot = this->recentMessages.at(index);
this->lastMessageTime = this->lastMessageTime.addSecs(static_cast<qint64>(secondDelta));
slot->time = this->lastMessageTime;
}
} else {
auto categoryId = next - EncodedLogOpcode::BeginCategories;
auto category = this->categories.value(categoryId);
quint8 field = 0;
if (!this->reader.readU8(&field)) return false;
auto msgType = QtDebugMsg;
quint64 secondDelta = 0;
auto needsTimeRead = false;
if (field == 0xff) {
msgType = QtFatalMsg;
needsTimeRead = true;
} else {
msgType = typeOfCompressed(static_cast<CompressedLogType>(field & 0x07));
secondDelta = field >> 3;
if (secondDelta == 0x1d) {
quint32 slot = 0;
if (!this->readVarInt(&slot)) return false;
secondDelta = slot;
} else if (secondDelta == 0x1e) {
needsTimeRead = true;
}
}
if (needsTimeRead) {
if (!this->reader.readU64(&secondDelta)) return false;
}
this->lastMessageTime = this->lastMessageTime.addSecs(static_cast<qint64>(secondDelta));
QByteArray body;
if (!this->readString(&body)) return false;
*slot = LogMessage(msgType, QLatin1StringView(category.first), body, this->lastMessageTime);
slot->readCategoryId = categoryId;
}
this->recentMessages.emplace(*slot);
return true;
}
CategoryFilter EncodedLogReader::categoryFilterById(quint16 id) {
return this->categories.value(id).second;
}
void EncodedLogWriter::writeOp(EncodedLogOpcode opcode) { this->buffer.writeU8(opcode); }
void EncodedLogWriter::writeVarInt(quint32 n) {
if (n < 0xff) {
this->buffer.writeU8(n);
} else if (n < 0xffff) {
this->buffer.writeU8(0xff);
this->buffer.writeU16(n);
} else {
this->buffer.writeU8(0xff);
this->buffer.writeU16(0xffff);
this->buffer.writeU32(n);
}
}
bool EncodedLogReader::readVarInt(quint32* slot) {
auto bytes = std::array<quint8, 7>();
auto readLength = this->reader.peekBytes(reinterpret_cast<char*>(bytes.data()), 7);
if (bytes[0] != 0xff && readLength >= 1) {
auto n = *reinterpret_cast<quint8*>(bytes.data());
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);
if (!this->reader.skip(3)) return false;
*slot = qFromLittleEndian(n);
} else if (readLength == 7) {
auto n = *reinterpret_cast<quint32*>(bytes.data() + 3);
if (!this->reader.skip(7)) return false;
*slot = qFromLittleEndian(n);
} else return false;
return true;
}
void EncodedLogWriter::writeString(QByteArrayView bytes) {
this->writeVarInt(bytes.length());
this->buffer.writeBytes(bytes.constData(), bytes.length());
}
bool EncodedLogReader::readString(QByteArray* slot) {
quint32 length = 0;
if (!this->readVarInt(&length)) return false;
*slot = QByteArray(length, Qt::Uninitialized);
auto r = this->reader.readBytes(slot->data(), slot->size());
return r;
}
quint16 EncodedLogWriter::getOrCreateCategory(QLatin1StringView category) {
if (this->categories.contains(category)) {
return this->categories.value(category);
} else {
this->writeOp(EncodedLogOpcode::RegisterCategory);
// id is implicitly the next available id
this->writeString(category);
auto id = this->nextCategory++;
this->categories.insert(category, id);
auto filter = LogManager::instance()->getFilter(category);
quint8 flags = 0;
flags |= filter.debug << 0;
flags |= filter.info << 1;
flags |= filter.warn << 2;
flags |= filter.critical << 3;
this->buffer.writeU8(flags);
return id;
}
}
bool EncodedLogReader::registerCategory() {
QByteArray name;
quint8 flags = 0;
if (!this->readString(&name)) return false;
if (!this->reader.readU8(&flags)) return false;
CategoryFilter filter;
filter.debug = (flags >> 0) & 1;
filter.info = (flags >> 1) & 1;
filter.warn = (flags >> 2) & 1;
filter.critical = (flags >> 3) & 1;
this->categories.append(qMakePair(name, filter));
return true;
}
bool LogReader::initialize() {
this->reader.setDevice(this->file);
bool readable = false;
quint8 logVersion = 0;
quint8 readerVersion = 0;
if (!this->reader.readHeader(&readable, &logVersion, &readerVersion)) {
qCritical() << "Failed to read log header.";
return false;
}
if (!readable) {
qCritical() << "This log was encoded with version" << logVersion
<< "of the quickshell log encoder, which cannot be decoded by the current "
"version of quickshell, with log version"
<< readerVersion;
return false;
}
return true;
}
bool LogReader::continueReading() {
auto color = LogManager::instance()->colorLogs;
auto tailRing = RingBuffer<LogMessage>(this->remainingTail);
LogMessage message;
auto stream = QTextStream(stdout);
auto readCursor = this->file->pos();
while (this->reader.read(&message)) {
readCursor = this->file->pos();
CategoryFilter filter;
if (this->filters.contains(message.readCategoryId)) {
filter = this->filters.value(message.readCategoryId);
} else {
filter = this->reader.categoryFilterById(message.readCategoryId);
for (const auto& rule: this->rules) {
filter.applyRule(message.category, rule);
}
this->filters.insert(message.readCategoryId, filter);
}
if (filter.shouldDisplay(message.type)) {
if (this->remainingTail == 0) {
LogMessage::formatMessage(stream, message, color, this->timestamps);
stream << '\n';
} else {
tailRing.emplace(message);
}
}
}
if (this->remainingTail != 0) {
for (auto i = tailRing.size() - 1; i != -1; i--) {
auto& message = tailRing.at(i);
LogMessage::formatMessage(stream, message, color, this->timestamps);
stream << '\n';
}
}
stream << Qt::flush;
if (this->file->pos() != readCursor) {
qCritical() << "An error occurred parsing the end of this log file.";
qCritical() << "Remaining data:" << this->file->readAll();
return false;
}
return true;
}
void LogFollower::FcntlWaitThread::run() {
auto lock = flock {
.l_type = F_RDLCK, // won't block other read locks when we take it
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0,
.l_pid = 0,
};
auto r = fcntl(this->follower->reader->file->handle(), F_SETLKW, &lock); // NOLINT
if (r != 0) {
qCWarning(logLogging).nospace()
<< "Failed to wait for write locks to be removed from log file with error code " << errno
<< ": " << qt_error_string();
}
}
bool LogFollower::follow() {
QObject::connect(&this->waitThread, &QThread::finished, this, &LogFollower::onFileLocked);
QObject::connect(
&this->fileWatcher,
&QFileSystemWatcher::fileChanged,
this,
&LogFollower::onFileChanged
);
this->fileWatcher.addPath(this->path);
this->waitThread.start();
auto r = QCoreApplication::exec();
return r == 0;
}
void LogFollower::onFileChanged() {
if (!this->reader->continueReading()) {
QCoreApplication::exit(1);
}
}
void LogFollower::onFileLocked() {
if (!this->reader->continueReading()) {
QCoreApplication::exit(1);
} else {
QCoreApplication::exit(0);
}
}
bool readEncodedLogs(
QFile* file,
const QString& path,
bool timestamps,
int tail,
bool follow,
const QString& rulespec
) {
QList<QLoggingRule> rules;
{
QLoggingSettingsParser parser;
parser.setContent(rulespec);
rules = parser.rules();
}
auto reader = LogReader(file, timestamps, tail, rules);
if (!reader.initialize()) return false;
if (!reader.continueReading()) return false;
if (follow) {
auto follower = LogFollower(&reader, path);
return follower.follow();
}
return true;
}
} // namespace qs::log

148
src/core/logging.hpp Normal file
View file

@ -0,0 +1,148 @@
#pragma once
#include <utility>
#include <qcontainerfwd.h>
#include <qdatetime.h>
#include <qfile.h>
#include <qhash.h>
#include <qlatin1stringview.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtmetamacros.h>
Q_DECLARE_LOGGING_CATEGORY(logBare);
namespace qs::log {
struct LogMessage {
explicit LogMessage() = default;
explicit LogMessage(
QtMsgType type,
QLatin1StringView category,
QByteArray body,
QDateTime time = QDateTime::currentDateTime()
)
: type(type)
, time(std::move(time))
, category(category)
, body(std::move(body)) {}
bool operator==(const LogMessage& other) const;
QtMsgType type = QtDebugMsg;
QDateTime time;
QLatin1StringView category;
QByteArray body;
quint16 readCategoryId = 0;
static void formatMessage(
QTextStream& stream,
const LogMessage& msg,
bool color,
bool timestamp,
const QString& prefix = ""
);
};
size_t qHash(const LogMessage& message);
class ThreadLogging;
class LoggingThreadProxy: public QObject {
Q_OBJECT;
public:
explicit LoggingThreadProxy() = default;
public slots:
void initInThread();
void initFs();
private:
ThreadLogging* logging = nullptr;
};
namespace qt_logging_registry {
class QLoggingRule;
}
struct CategoryFilter {
explicit CategoryFilter() = default;
explicit CategoryFilter(QLoggingCategory* category)
: debug(category->isDebugEnabled())
, info(category->isInfoEnabled())
, warn(category->isWarningEnabled())
, critical(category->isCriticalEnabled()) {}
[[nodiscard]] bool shouldDisplay(QtMsgType type) const;
void apply(QLoggingCategory* category) const;
void applyRule(QLatin1StringView category, const qt_logging_registry::QLoggingRule& rule);
bool debug = true;
bool info = true;
bool warn = true;
bool critical = true;
};
class LogManager: public QObject {
Q_OBJECT;
public:
static void init(
bool color,
bool timestamp,
bool sparseOnly,
QtMsgType defaultLevel,
const QString& rules,
const QString& prefix = ""
);
static void initFs();
static LogManager* instance();
bool colorLogs = true;
bool timestampLogs = false;
[[nodiscard]] QString rulesString() const;
[[nodiscard]] QtMsgType defaultLevel() const;
[[nodiscard]] bool isSparse() const;
[[nodiscard]] CategoryFilter getFilter(QLatin1StringView category);
signals:
void logMessage(LogMessage msg, bool showInSparse);
private:
explicit LogManager();
static void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg);
static void filterCategory(QLoggingCategory* category);
QLoggingCategory::CategoryFilter lastCategoryFilter = nullptr;
bool sparse = false;
QString prefix;
QString mRulesString;
QList<qt_logging_registry::QLoggingRule>* rules = nullptr;
QtMsgType mDefaultLevel = QtWarningMsg;
QHash<const void*, CategoryFilter> sparseFilters;
QHash<QLatin1StringView, CategoryFilter> allFilters;
QTextStream stdoutStream;
LoggingThreadProxy threadProxy;
};
bool readEncodedLogs(
QFile* file,
const QString& path,
bool timestamps,
int tail,
bool follow,
const QString& rulespec
);
} // namespace qs::log
using LogManager = qs::log::LogManager;

190
src/core/logging_p.hpp Normal file
View file

@ -0,0 +1,190 @@
#pragma once
#include <utility>
#include <qbytearrayview.h>
#include <qcontainerfwd.h>
#include <qfile.h>
#include <qfilesystemwatcher.h>
#include <qlogging.h>
#include <qobject.h>
#include <qthread.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "logging.hpp"
#include "logging_qtprivate.hpp"
#include "ringbuf.hpp"
namespace qs::log {
enum EncodedLogOpcode : quint8 {
RegisterCategory = 0,
RecentMessageShort,
RecentMessageLong,
BeginCategories,
};
enum CompressedLogType : quint8 {
Debug = 0,
Info = 1,
Warn = 2,
Critical = 3,
};
CompressedLogType compressedTypeOf(QtMsgType type);
QtMsgType typeOfCompressed(CompressedLogType type);
class WriteBuffer {
public:
void setDevice(QIODevice* device);
[[nodiscard]] bool hasDevice() const;
[[nodiscard]] bool flush();
void writeBytes(const char* data, qsizetype length);
void writeU8(quint8 data);
void writeU16(quint16 data);
void writeU32(quint32 data);
void writeU64(quint64 data);
private:
QIODevice* device = nullptr;
QByteArray buffer;
};
class DeviceReader {
public:
void setDevice(QIODevice* device);
[[nodiscard]] bool hasDevice() const;
[[nodiscard]] bool readBytes(char* data, qsizetype length);
// peek UP TO length
[[nodiscard]] qsizetype peekBytes(char* data, qsizetype length);
[[nodiscard]] bool skip(qsizetype length);
[[nodiscard]] bool readU8(quint8* data);
[[nodiscard]] bool readU16(quint16* data);
[[nodiscard]] bool readU32(quint32* data);
[[nodiscard]] bool readU64(quint64* data);
private:
QIODevice* device = nullptr;
};
class EncodedLogWriter {
public:
void setDevice(QIODevice* target);
[[nodiscard]] bool writeHeader();
[[nodiscard]] bool write(const LogMessage& message);
private:
void writeOp(EncodedLogOpcode opcode);
void writeVarInt(quint32 n);
void writeString(QByteArrayView bytes);
quint16 getOrCreateCategory(QLatin1StringView category);
WriteBuffer buffer;
QHash<QLatin1StringView, quint16> categories;
quint16 nextCategory = EncodedLogOpcode::BeginCategories;
QDateTime lastMessageTime = QDateTime::fromSecsSinceEpoch(0);
HashBuffer<LogMessage> recentMessages {256};
};
class EncodedLogReader {
public:
void setDevice(QIODevice* source);
[[nodiscard]] bool readHeader(bool* success, quint8* logVersion, quint8* readerVersion);
// WARNING: log messages written to the given slot are invalidated when the log reader is destroyed.
[[nodiscard]] bool read(LogMessage* slot);
[[nodiscard]] CategoryFilter categoryFilterById(quint16 id);
private:
[[nodiscard]] bool readVarInt(quint32* slot);
[[nodiscard]] bool readString(QByteArray* slot);
[[nodiscard]] bool registerCategory();
DeviceReader reader;
QVector<QPair<QByteArray, CategoryFilter>> categories;
QDateTime lastMessageTime = QDateTime::fromSecsSinceEpoch(0);
RingBuffer<LogMessage> recentMessages {256};
};
class ThreadLogging: public QObject {
Q_OBJECT;
public:
explicit ThreadLogging(QObject* parent): QObject(parent) {}
void init();
void initFs();
void setupFileLogging();
private slots:
void onMessage(const LogMessage& msg, bool showInSparse);
private:
QFile* file = nullptr;
QTextStream fileStream;
QFile* detailedFile = nullptr;
EncodedLogWriter detailedWriter;
};
class LogFollower;
class LogReader {
public:
explicit LogReader(
QFile* file,
bool timestamps,
int tail,
QList<qt_logging_registry::QLoggingRule> rules
)
: file(file)
, timestamps(timestamps)
, remainingTail(tail)
, rules(std::move(rules)) {}
bool initialize();
bool continueReading();
private:
QFile* file;
EncodedLogReader reader;
bool timestamps;
int remainingTail;
QHash<quint16, CategoryFilter> filters;
QList<qt_logging_registry::QLoggingRule> rules;
friend class LogFollower;
};
class LogFollower: public QObject {
Q_OBJECT;
public:
explicit LogFollower(LogReader* reader, QString path): reader(reader), path(std::move(path)) {}
bool follow();
private slots:
void onFileChanged();
void onFileLocked();
private:
LogReader* reader;
QString path;
QFileSystemWatcher fileWatcher;
class FcntlWaitThread: public QThread {
public:
explicit FcntlWaitThread(LogFollower* follower): follower(follower) {}
protected:
void run() override;
private:
LogFollower* follower;
};
FcntlWaitThread waitThread {this};
};
} // namespace qs::log

View file

@ -0,0 +1,138 @@
// The logging rule parser from qloggingregistry_p.h and qloggingregistry.cpp.
// Was unable to properly link the functions when directly using the headers (which we depend
// on anyway), so below is a slightly stripped down copy. Making the originals link would
// be preferable.
#include <utility>
#include <qbytearrayview.h>
#include <qchar.h>
#include <qflags.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qstringtokenizer.h>
#include <qstringview.h>
#include <qtypes.h>
#include "logging_qtprivate.hpp"
namespace qs::log {
Q_DECLARE_LOGGING_CATEGORY(logLogging);
namespace qt_logging_registry {
class QLoggingSettingsParser {
public:
void setContent(QStringView content);
[[nodiscard]] QList<QLoggingRule> rules() const { return this->mRules; }
private:
void parseNextLine(QStringView line);
private:
QList<QLoggingRule> mRules;
};
void QLoggingSettingsParser::setContent(QStringView content) {
this->mRules.clear();
for (auto line: qTokenize(content, u';')) this->parseNextLine(line);
}
void QLoggingSettingsParser::parseNextLine(QStringView line) {
// Remove whitespace at start and end of line:
line = line.trimmed();
const qsizetype equalPos = line.indexOf(u'=');
if (equalPos != -1) {
if (line.lastIndexOf(u'=') == equalPos) {
const auto key = line.left(equalPos).trimmed();
const QStringView pattern = key;
const auto valueStr = line.mid(equalPos + 1).trimmed();
int value = -1;
if (valueStr == QString("true")) value = 1;
else if (valueStr == QString("false")) value = 0;
QLoggingRule rule(pattern, (value == 1));
if (rule.flags != 0 && (value != -1)) this->mRules.append(std::move(rule));
else
qCWarning(logLogging, "Ignoring malformed logging rule: '%s'", line.toUtf8().constData());
} else {
qCWarning(logLogging, "Ignoring malformed logging rule: '%s'", line.toUtf8().constData());
}
}
}
QLoggingRule::QLoggingRule(QStringView pattern, bool enabled): messageType(-1), enabled(enabled) {
this->parse(pattern);
}
void QLoggingRule::parse(QStringView pattern) {
QStringView p;
// strip trailing ".messagetype"
if (pattern.endsWith(QString(".debug"))) {
p = pattern.chopped(6); // strlen(".debug")
this->messageType = QtDebugMsg;
} else if (pattern.endsWith(QString(".info"))) {
p = pattern.chopped(5); // strlen(".info")
this->messageType = QtInfoMsg;
} else if (pattern.endsWith(QString(".warning"))) {
p = pattern.chopped(8); // strlen(".warning")
this->messageType = QtWarningMsg;
} else if (pattern.endsWith(QString(".critical"))) {
p = pattern.chopped(9); // strlen(".critical")
this->messageType = QtCriticalMsg;
} else {
p = pattern;
}
const QChar asterisk = u'*';
if (!p.contains(asterisk)) {
this->flags = FullText;
} else {
if (p.endsWith(asterisk)) {
this->flags |= LeftFilter;
p = p.chopped(1);
}
if (p.startsWith(asterisk)) {
this->flags |= RightFilter;
p = p.mid(1);
}
if (p.contains(asterisk)) // '*' only supported at start/end
this->flags = PatternFlags();
}
this->category = p.toString();
}
int QLoggingRule::pass(QLatin1StringView cat, QtMsgType msgType) const {
// check message type
if (this->messageType > -1 && this->messageType != msgType) return 0;
if (this->flags == FullText) {
// full match
if (this->category == cat) return (this->enabled ? 1 : -1);
else return 0;
}
const qsizetype idx = cat.indexOf(this->category);
if (idx >= 0) {
if (this->flags == MidFilter) {
// matches somewhere
return (this->enabled ? 1 : -1);
} else if (this->flags == LeftFilter) {
// matches left
if (idx == 0) return (this->enabled ? 1 : -1);
} else if (this->flags == RightFilter) {
// matches right
if (idx == (cat.size() - this->category.size())) return (this->enabled ? 1 : -1);
}
}
return 0;
}
} // namespace qt_logging_registry
} // namespace qs::log

View file

@ -0,0 +1,45 @@
#pragma once
// The logging rule parser from qloggingregistry_p.h and qloggingregistry.cpp.
// Was unable to properly link the functions when directly using the headers (which we depend
// on anyway), so below is a slightly stripped down copy. Making the originals link would
// be preferable.
#include <qflags.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qstringview.h>
#include <qtypes.h>
namespace qs::log {
Q_DECLARE_LOGGING_CATEGORY(logLogging);
namespace qt_logging_registry {
class QLoggingRule {
public:
QLoggingRule();
QLoggingRule(QStringView pattern, bool enabled);
[[nodiscard]] int pass(QLatin1StringView categoryName, QtMsgType type) const;
enum PatternFlag : quint8 {
FullText = 0x1,
LeftFilter = 0x2,
RightFilter = 0x4,
MidFilter = LeftFilter | RightFilter
};
Q_DECLARE_FLAGS(PatternFlags, PatternFlag)
QString category;
int messageType;
PatternFlags flags;
bool enabled;
private:
void parse(QStringView pattern);
};
} // namespace qt_logging_registry
} // namespace qs::log

View file

@ -1,331 +0,0 @@
#include "main.hpp"
#include <iostream>
#include <qapplication.h>
#include <qcommandlineoption.h>
#include <qcommandlineparser.h>
#include <qcoreapplication.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qguiapplication.h>
#include <qhash.h>
#include <qlogging.h>
#include <qquickwindow.h>
#include <qstandardpaths.h>
#include <qstring.h>
#include <qtenvironmentvariables.h>
#include <qtextstream.h>
#include <qtpreprocessorsupport.h>
#include "plugin.hpp"
#include "rootwrapper.hpp"
int qs_main(int argc, char** argv) {
QString configFilePath;
QString workingDirectory;
auto useQApplication = false;
auto nativeTextRendering = false;
auto desktopSettingsAware = true;
QHash<QString, QString> envOverrides;
{
const auto app = QCoreApplication(argc, argv);
QCoreApplication::setApplicationName("quickshell");
QCoreApplication::setApplicationVersion("0.1.0 (" GIT_REVISION ")");
QCommandLineParser parser;
parser.addHelpOption();
parser.addVersionOption();
// clang-format off
auto currentOption = QCommandLineOption("current", "Print information about the manifest and defaults.");
auto manifestOption = QCommandLineOption({"m", "manifest"}, "Path to a configuration manifest.", "path");
auto configOption = QCommandLineOption({"c", "config"}, "Name of a configuration in the manifest.", "name");
auto pathOption = QCommandLineOption({"p", "path"}, "Path to a configuration file.", "path");
auto workdirOption = QCommandLineOption({"d", "workdir"}, "Initial working directory.", "path");
// clang-format on
parser.addOption(currentOption);
parser.addOption(manifestOption);
parser.addOption(configOption);
parser.addOption(pathOption);
parser.addOption(workdirOption);
parser.process(app);
{
auto printCurrent = parser.isSet(currentOption);
// NOLINTBEGIN
#define CHECK(rname, name, level, label, expr) \
QString name = expr; \
if (rname.isEmpty() && !name.isEmpty()) { \
rname = name; \
rname##Level = level; \
if (!printCurrent) goto label; \
}
#define OPTSTR(name) (name.isEmpty() ? "(unset)" : name.toStdString())
// NOLINTEND
QString basePath;
int basePathLevel = 0;
Q_UNUSED(basePathLevel);
{
// NOLINTBEGIN
// clang-format off
CHECK(basePath, envBasePath, 0, foundbase, qEnvironmentVariable("QS_BASE_PATH"));
CHECK(basePath, defaultBasePath, 0, foundbase, QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)).filePath("quickshell"));
// clang-format on
// NOLINTEND
if (printCurrent) {
// clang-format off
std::cout << "Base path: " << OPTSTR(basePath) << "\n";
std::cout << " - Environment (QS_BASE_PATH): " << OPTSTR(envBasePath) << "\n";
std::cout << " - Default: " << OPTSTR(defaultBasePath) << "\n";
// clang-format on
}
}
foundbase:;
QString configPath;
int configPathLevel = 10;
{
// NOLINTBEGIN
CHECK(configPath, optionConfigPath, 0, foundpath, parser.value(pathOption));
CHECK(configPath, envConfigPath, 1, foundpath, qEnvironmentVariable("QS_CONFIG_PATH"));
// NOLINTEND
if (printCurrent) {
// clang-format off
std::cout << "\nConfig path: " << OPTSTR(configPath) << "\n";
std::cout << " - Option: " << OPTSTR(optionConfigPath) << "\n";
std::cout << " - Environment (QS_CONFIG_PATH): " << OPTSTR(envConfigPath) << "\n";
// clang-format on
}
}
foundpath:;
QString manifestPath;
int manifestPathLevel = 10;
{
// NOLINTBEGIN
// clang-format off
CHECK(manifestPath, optionManifestPath, 0, foundmf, parser.value(manifestOption));
CHECK(manifestPath, envManifestPath, 1, foundmf, qEnvironmentVariable("QS_MANIFEST"));
CHECK(manifestPath, defaultManifestPath, 2, foundmf, QDir(basePath).filePath("manifest.conf"));
// clang-format on
// NOLINTEND
if (printCurrent) {
// clang-format off
std::cout << "\nManifest path: " << OPTSTR(manifestPath) << "\n";
std::cout << " - Option: " << OPTSTR(optionManifestPath) << "\n";
std::cout << " - Environment (QS_MANIFEST): " << OPTSTR(envManifestPath) << "\n";
std::cout << " - Default: " << OPTSTR(defaultManifestPath) << "\n";
// clang-format on
}
}
foundmf:;
QString configName;
int configNameLevel = 10;
{
// NOLINTBEGIN
CHECK(configName, optionConfigName, 0, foundname, parser.value(configOption));
CHECK(configName, envConfigName, 1, foundname, qEnvironmentVariable("QS_CONFIG_NAME"));
// NOLINTEND
if (printCurrent) {
// clang-format off
std::cout << "\nConfig name: " << OPTSTR(configName) << "\n";
std::cout << " - Option: " << OPTSTR(optionConfigName) << "\n";
std::cout << " - Environment (QS_CONFIG_NAME): " << OPTSTR(envConfigName) << "\n\n";
// clang-format on
}
}
foundname:;
if (configPathLevel == 0 && configNameLevel == 0) {
qCritical() << "Pass only one of --path or --config";
return -1;
}
if (!configPath.isEmpty() && configPathLevel <= configNameLevel) {
configFilePath = configPath;
} else if (!configName.isEmpty()) {
if (!manifestPath.isEmpty()) {
auto file = QFile(manifestPath);
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
auto stream = QTextStream(&file);
while (!stream.atEnd()) {
auto line = stream.readLine();
if (line.trimmed().startsWith("#")) continue;
if (line.trimmed().isEmpty()) continue;
auto split = line.split('=');
if (split.length() != 2) {
qCritical() << "manifest line not in expected format 'name = relativepath':"
<< line;
return -1;
}
if (split[0].trimmed() == configName) {
configFilePath = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed());
goto haspath; // NOLINT
}
}
qCritical() << "configuration" << configName << "not found in manifest" << manifestPath;
return -1;
} else if (manifestPathLevel < 2) {
qCritical() << "cannot open config manifest at" << manifestPath;
return -1;
}
}
{
auto basePathInfo = QFileInfo(basePath);
if (!basePathInfo.exists()) {
qCritical() << "base path does not exist:" << basePath;
return -1;
} else if (!QFileInfo(basePathInfo.canonicalFilePath()).isDir()) {
qCritical() << "base path is not a directory" << basePath;
return -1;
}
auto dir = QDir(basePath);
for (auto& entry: dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) {
if (entry == configName) {
configFilePath = dir.filePath(entry);
goto haspath; // NOLINT
}
}
qCritical() << "no directory named " << configName << "found in base path" << basePath;
return -1;
}
haspath:;
} else {
configFilePath = basePath;
}
auto configFile = QFileInfo(configFilePath);
if (!configFile.exists()) {
qCritical() << "config path does not exist:" << configFilePath;
return -1;
}
if (configFile.isDir()) {
configFilePath = QDir(configFilePath).filePath("shell.qml");
}
configFile = QFileInfo(configFilePath);
if (!configFile.exists()) {
qCritical() << "no shell.qml found in config path:" << configFilePath;
return -1;
} else if (configFile.isDir()) {
qCritical() << "shell.qml is a directory:" << configFilePath;
return -1;
}
configFilePath = QFileInfo(configFilePath).canonicalFilePath();
configFile = QFileInfo(configFilePath);
if (!configFile.exists()) {
qCritical() << "config file does not exist:" << configFilePath;
return -1;
} else if (configFile.isDir()) {
qCritical() << "config file is a directory:" << configFilePath;
return -1;
}
#undef CHECK
#undef OPTSTR
qInfo() << "config file path:" << configFilePath;
if (printCurrent) return 0;
}
if (!QFile(configFilePath).exists()) {
qCritical() << "config file does not exist";
return -1;
}
if (parser.isSet(workdirOption)) {
workingDirectory = parser.value(workdirOption);
}
auto file = QFile(configFilePath);
if (!file.open(QFile::ReadOnly | QFile::Text)) {
qCritical() << "could not open config file";
return -1;
}
auto stream = QTextStream(&file);
while (!stream.atEnd()) {
auto line = stream.readLine().trimmed();
if (line.startsWith("//@ pragma ")) {
auto pragma = line.sliced(11).trimmed();
if (pragma == "UseQApplication") useQApplication = true;
else if (pragma == "NativeTextRendering") nativeTextRendering = true;
else if (pragma == "IgnoreSystemSettings") desktopSettingsAware = false;
else if (pragma.startsWith("Env ")) {
auto envPragma = pragma.sliced(4);
auto splitIdx = envPragma.indexOf('=');
if (splitIdx == -1) {
qCritical() << "Env pragma" << pragma << "not in the form 'VAR = VALUE'";
return -1;
}
auto var = envPragma.sliced(0, splitIdx).trimmed();
auto val = envPragma.sliced(splitIdx + 1).trimmed();
envOverrides.insert(var, val);
} else {
qCritical() << "Unrecognized pragma" << pragma;
return -1;
}
} else if (line.startsWith("import")) break;
}
file.close();
}
for (auto [var, val]: envOverrides.asKeyValueRange()) {
qputenv(var.toUtf8(), val.toUtf8());
}
QGuiApplication::setDesktopSettingsAware(desktopSettingsAware);
QGuiApplication* app = nullptr;
if (useQApplication) {
app = new QApplication(argc, argv);
} else {
app = new QGuiApplication(argc, argv);
}
if (!workingDirectory.isEmpty()) {
QDir::setCurrent(workingDirectory);
}
QuickshellPlugin::initPlugins();
// Base window transparency appears to be additive.
// Use a fully transparent window with a colored rect.
QQuickWindow::setDefaultAlphaBuffer(true);
if (nativeTextRendering) {
QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering);
}
auto root = RootWrapper(configFilePath);
QGuiApplication::setQuitOnLastWindowClosed(false);
auto code = QGuiApplication::exec();
delete app;
return code;
}

View file

@ -1,3 +0,0 @@
#pragma once
int qs_main(int argc, char** argv); // NOLINT

98
src/core/model.cpp Normal file
View file

@ -0,0 +1,98 @@
#include "model.hpp"
#include <qabstractitemmodel.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"}};
}
QQmlListProperty<QObject> UntypedObjectModel::values() {
return QQmlListProperty<QObject>(
this,
nullptr,
&UntypedObjectModel::valuesCount,
&UntypedObjectModel::valueAt
);
}
qsizetype UntypedObjectModel::valuesCount(QQmlListProperty<QObject>* property) {
return static_cast<UntypedObjectModel*>(property->object)->valuesList.count(); // NOLINT
}
QObject* UntypedObjectModel::valueAt(QQmlListProperty<QObject>* property, qsizetype index) {
return static_cast<UntypedObjectModel*>(property->object)->valuesList.at(index); // NOLINT
}
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
return instance;
}

111
src/core/model.hpp Normal file
View file

@ -0,0 +1,111 @@
#pragma once
#include <bit>
#include <qabstractitemmodel.h>
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
#include "doc.hpp"
///! View into a list of objets
/// Typed view into a list of objects.
///
/// An ObjectModel works as a QML [Data Model], allowing efficient interaction with
/// components that act on models. It has a single role named `modelData`, to match the
/// behavior of lists.
/// The same information contained in the list model is available as a normal list
/// via the `values` property.
///
/// #### Differences from a list
/// Unlike with a list, the following property binding will never be updated when `model[3]` changes.
/// ```qml
/// // will not update reactively
/// property var foo: model[3]
/// ```
///
/// You can work around this limitation using the @@values property of the model to view it as a list.
/// ```qml
/// // will update reactively
/// property var foo: model.values[3]
/// ```
///
/// [Data Model]: https://doc.qt.io/qt-6/qtquick-modelviewsdata-modelview.html#qml-data-models
class UntypedObjectModel: public QAbstractListModel {
QSDOC_CNAME(ObjectModel);
Q_OBJECT;
/// The content of the object model, as a QML list.
/// The values of this property will always be of the type of the model.
Q_PROPERTY(QQmlListProperty<QObject> values READ values NOTIFY valuesChanged);
QML_NAMED_ELEMENT(ObjectModel);
QML_UNCREATABLE("ObjectModels cannot be created directly.");
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]] QQmlListProperty<QObject> values();
void removeAt(qsizetype index);
Q_INVOKABLE qsizetype indexOf(QObject* object);
static UntypedObjectModel* emptyInstance();
signals:
void valuesChanged();
/// Sent immediately before an object is inserted into the list.
void objectInsertedPre(QObject* object, qsizetype index);
/// Sent immediately after an object is inserted into the list.
void objectInsertedPost(QObject* object, qsizetype index);
/// Sent immediately before an object is removed from the list.
void objectRemovedPre(QObject* object, qsizetype index);
/// 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);
};
template <typename T>
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);
}
void insertObject(T* object, qsizetype index = -1) {
this->UntypedObjectModel::insertObject(object, index);
}
void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); }
// Assumes only one instance of a specific value
void diffUpdate(const QVector<T*>& newValues) {
this->UntypedObjectModel::diffUpdate(*std::bit_cast<const QVector<QObject*>*>(&newValues));
}
static ObjectModel<T>* emptyInstance() {
return static_cast<ObjectModel<T>*>(UntypedObjectModel::emptyInstance());
}
};

View file

@ -7,16 +7,26 @@ headers = [
"shell.hpp",
"variants.hpp",
"region.hpp",
"proxywindow.hpp",
"../window/proxywindow.hpp",
"persistentprops.hpp",
"windowinterface.hpp",
"panelinterface.hpp",
"floatingwindow.hpp",
"popupwindow.hpp",
"../window/windowinterface.hpp",
"../window/panelinterface.hpp",
"../window/floatingwindow.hpp",
"../window/popupwindow.hpp",
"singleton.hpp",
"lazyloader.hpp",
"easingcurve.hpp",
"transformwatcher.hpp",
"boundcomponent.hpp",
"model.hpp",
"elapsedtimer.hpp",
"desktopentry.hpp",
"objectrepeater.hpp",
"qsmenu.hpp",
"retainable.hpp",
"popupanchor.hpp",
"types.hpp",
"qsmenuanchor.hpp",
"clock.hpp",
]
-----

190
src/core/objectrepeater.cpp Normal file
View file

@ -0,0 +1,190 @@
#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;
}

View file

@ -0,0 +1,85 @@
#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;
};

308
src/core/paths.cpp Normal file
View file

@ -0,0 +1,308 @@
#include "paths.hpp"
#include <cerrno>
#include <cstdio>
#include <utility>
#include <fcntl.h>
#include <qcontainerfwd.h>
#include <qdatastream.h>
#include <qdir.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qstandardpaths.h>
#include <qtenvironmentvariables.h>
#include <unistd.h>
#include "instanceinfo.hpp"
Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg);
QsPaths* QsPaths::instance() {
static auto* instance = new QsPaths(); // NOLINT
return instance;
}
void QsPaths::init(QString shellId, QString pathId) {
auto* instance = QsPaths::instance();
instance->shellId = std::move(shellId);
instance->pathId = std::move(pathId);
}
QDir QsPaths::crashDir(const QString& id) {
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
dir = QDir(dir.filePath("crashes"));
dir = QDir(dir.filePath(id));
return dir;
}
QString QsPaths::basePath(const QString& id) {
auto path = QsPaths::instance()->baseRunDir()->filePath("by-id");
path = QDir(path).filePath(id);
return path;
}
QString QsPaths::ipcPath(const QString& id) {
return QDir(QsPaths::basePath(id)).filePath("ipc.sock");
}
QDir* QsPaths::cacheDir() {
if (this->cacheState == DirState::Unknown) {
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
dir = QDir(dir.filePath(this->shellId));
this->mCacheDir = dir;
qCDebug(logPaths) << "Initialized cache path:" << dir.path();
if (!dir.mkpath(".")) {
qCCritical(logPaths) << "Could not create cache directory at" << dir.path();
this->cacheState = DirState::Failed;
} else {
this->cacheState = DirState::Ready;
}
}
if (this->cacheState == DirState::Failed) return nullptr;
else return &this->mCacheDir;
}
QDir* QsPaths::baseRunDir() {
if (this->baseRunState == DirState::Unknown) {
auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR");
if (runtimeDir.isEmpty()) {
runtimeDir = QString("/run/user/$1").arg(getuid());
qCInfo(logPaths) << "XDG_RUNTIME_DIR was not set, defaulting to" << runtimeDir;
}
this->mBaseRunDir = QDir(runtimeDir);
this->mBaseRunDir = QDir(this->mBaseRunDir.filePath("quickshell"));
qCDebug(logPaths) << "Initialized base runtime path:" << this->mBaseRunDir.path();
if (!this->mBaseRunDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create base runtime directory at"
<< this->mBaseRunDir.path();
this->baseRunState = DirState::Failed;
} else {
this->baseRunState = DirState::Ready;
}
}
if (this->baseRunState == DirState::Failed) return nullptr;
else return &this->mBaseRunDir;
}
QDir* QsPaths::shellRunDir() {
if (this->shellRunState == DirState::Unknown) {
if (auto* baseRunDir = this->baseRunDir()) {
this->mShellRunDir = QDir(baseRunDir->filePath("by-shell"));
this->mShellRunDir = QDir(this->mShellRunDir.filePath(this->shellId));
qCDebug(logPaths) << "Initialized runtime path:" << this->mShellRunDir.path();
if (!this->mShellRunDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create runtime directory at"
<< this->mShellRunDir.path();
this->shellRunState = DirState::Failed;
} else {
this->shellRunState = DirState::Ready;
}
} else {
qCCritical(logPaths) << "Could not create shell runtime path as it was not possible to "
"create the base runtime path.";
this->shellRunState = DirState::Failed;
}
}
if (this->shellRunState == DirState::Failed) return nullptr;
else return &this->mShellRunDir;
}
QDir* QsPaths::instanceRunDir() {
if (this->instanceRunState == DirState::Unknown) {
auto* runDir = this->baseRunDir();
if (!runDir) {
qCCritical(logPaths) << "Cannot create instance runtime directory as main runtim directory "
"could not be created.";
this->instanceRunState = DirState::Failed;
} else {
auto byIdDir = QDir(runDir->filePath("by-id"));
this->mInstanceRunDir = byIdDir.filePath(InstanceInfo::CURRENT.instanceId);
qCDebug(logPaths) << "Initialized instance runtime path:" << this->mInstanceRunDir.path();
if (!this->mInstanceRunDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create instance runtime directory at"
<< this->mInstanceRunDir.path();
this->instanceRunState = DirState::Failed;
} else {
this->instanceRunState = DirState::Ready;
}
}
}
if (this->shellRunState == DirState::Failed) return nullptr;
else return &this->mInstanceRunDir;
}
void QsPaths::linkRunDir() {
if (auto* runDir = this->instanceRunDir()) {
auto pidDir = QDir(this->baseRunDir()->filePath("by-pid"));
auto* shellDir = this->shellRunDir();
if (!shellDir) {
qCCritical(logPaths
) << "Could not create by-id symlink as the shell runtime path could not be created.";
} else {
auto shellPath = shellDir->filePath(runDir->dirName());
QFile::remove(shellPath);
auto r =
symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, shellPath.toStdString().c_str());
if (r != 0) {
qCCritical(logPaths).nospace()
<< "Could not create id symlink to " << runDir->path() << " at " << shellPath
<< " with error code " << errno << ": " << qt_error_string();
} else {
qCDebug(logPaths) << "Created shellid symlink" << shellPath << "to instance runtime path"
<< runDir->path();
}
}
if (!pidDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create PID symlink directory.";
} else {
auto pidPath = pidDir.filePath(QString::number(getpid()));
QFile::remove(pidPath);
auto r =
symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, pidPath.toStdString().c_str());
if (r != 0) {
qCCritical(logPaths).nospace()
<< "Could not create PID symlink to " << runDir->path() << " at " << pidPath
<< " with error code " << errno << ": " << qt_error_string();
} else {
qCDebug(logPaths) << "Created PID symlink" << pidPath << "to instance runtime path"
<< runDir->path();
}
}
} else {
qCCritical(logPaths) << "Could not create PID symlink to runtime directory, as the runtime "
"directory could not be created.";
}
}
void QsPaths::linkPathDir() {
if (auto* runDir = this->shellRunDir()) {
auto pathDir = QDir(this->baseRunDir()->filePath("by-path"));
if (!pathDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create path symlink directory.";
return;
}
auto linkPath = pathDir.filePath(this->pathId);
QFile::remove(linkPath);
auto r =
symlinkat(runDir->filesystemCanonicalPath().c_str(), 0, linkPath.toStdString().c_str());
if (r != 0) {
qCCritical(logPaths).nospace()
<< "Could not create path symlink to " << runDir->path() << " at " << linkPath
<< " with error code " << errno << ": " << qt_error_string();
} else {
qCDebug(logPaths) << "Created path symlink" << linkPath << "to shell runtime path"
<< runDir->path();
}
} else {
qCCritical(logPaths) << "Could not create path symlink to shell runtime directory, as the "
"shell runtime directory could not be created.";
}
}
void QsPaths::createLock() {
if (auto* runDir = this->instanceRunDir()) {
auto path = runDir->filePath("instance.lock");
auto* file = new QFile(path); // leaked
if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
qCCritical(logPaths) << "Could not create instance lock at" << path;
return;
}
auto lock = flock {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0,
.l_pid = 0,
};
if (fcntl(file->handle(), F_SETLK, &lock) != 0) { // NOLINT
qCCritical(logPaths).nospace() << "Could not lock instance lock at " << path
<< " with error code " << errno << ": " << qt_error_string();
} else {
auto stream = QDataStream(file);
stream << InstanceInfo::CURRENT;
file->flush();
qCDebug(logPaths) << "Created instance lock at" << path;
}
} else {
qCCritical(logPaths
) << "Could not create instance lock, as the instance runtime directory could not be created.";
}
}
bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info) {
auto file = QFile(QDir(path).filePath("instance.lock"));
if (!file.open(QFile::ReadOnly)) return false;
auto lock = flock {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0,
.l_pid = 0,
};
fcntl(file.handle(), F_GETLK, &lock); // NOLINT
if (lock.l_type == F_UNLCK) return false;
if (info) {
info->pid = lock.l_pid;
auto stream = QDataStream(&file);
stream >> info->instance;
}
return true;
}
QVector<InstanceLockInfo> QsPaths::collectInstances(const QString& path) {
qCDebug(logPaths) << "Collecting instances from" << path;
auto instances = 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)) {
qCDebug(logPaths).nospace() << "Found live instance " << info.instance.instanceId << " (pid "
<< info.pid << ") at " << path;
instances.push_back(info);
} else {
qCDebug(logPaths) << "Skipped dead instance at" << path;
}
}
return instances;
}

51
src/core/paths.hpp Normal file
View file

@ -0,0 +1,51 @@
#pragma once
#include <qdatetime.h>
#include <qdir.h>
#include <qtypes.h>
#include "instanceinfo.hpp"
struct InstanceLockInfo {
pid_t pid = -1;
InstanceInfo instance;
};
QDataStream& operator<<(QDataStream& stream, const InstanceLockInfo& info);
QDataStream& operator>>(QDataStream& stream, InstanceLockInfo& info);
class QsPaths {
public:
static QsPaths* instance();
static void init(QString shellId, QString pathId);
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);
static QVector<InstanceLockInfo> collectInstances(const QString& path);
QDir* cacheDir();
QDir* baseRunDir();
QDir* shellRunDir();
QDir* instanceRunDir();
void linkRunDir();
void linkPathDir();
void createLock();
private:
enum class DirState : quint8 {
Unknown = 0,
Ready = 1,
Failed = 2,
};
QString shellId;
QString pathId;
QDir mCacheDir;
QDir mBaseRunDir;
QDir mShellRunDir;
QDir mInstanceRunDir;
DirState cacheState = DirState::Unknown;
DirState baseRunState = DirState::Unknown;
DirState shellRunState = DirState::Unknown;
DirState instanceRunState = DirState::Unknown;
};

324
src/core/platformmenu.cpp Normal file
View file

@ -0,0 +1,324 @@
#include "platformmenu.hpp"
#include <functional>
#include <utility>
#include <qaction.h>
#include <qactiongroup.h>
#include <qapplication.h>
#include <qcontainerfwd.h>
#include <qcoreapplication.h>
#include <qicon.h>
#include <qlogging.h>
#include <qmenu.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qpoint.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qwindow.h>
#include "../window/proxywindow.hpp"
#include "../window/windowinterface.hpp"
#include "iconprovider.hpp"
#include "model.hpp"
#include "platformmenu_p.hpp"
#include "popupanchor.hpp"
#include "qsmenu.hpp"
namespace qs::menu::platform {
namespace {
QVector<std::function<void(PlatformMenuQMenu*)>> CREATION_HOOKS; // NOLINT
PlatformMenuQMenu* ACTIVE_MENU = nullptr; // NOLINT
} // namespace
PlatformMenuQMenu::~PlatformMenuQMenu() {
if (this == ACTIVE_MENU) {
ACTIVE_MENU = nullptr;
}
}
void PlatformMenuQMenu::setVisible(bool visible) {
if (visible) {
for (auto& hook: CREATION_HOOKS) {
hook(this);
}
} else {
if (this == ACTIVE_MENU) {
ACTIVE_MENU = nullptr;
}
}
this->QMenu::setVisible(visible);
}
PlatformMenuEntry::PlatformMenuEntry(QsMenuEntry* menu): QObject(menu), menu(menu) {
this->relayout();
// clang-format off
QObject::connect(menu, &QsMenuEntry::enabledChanged, this, &PlatformMenuEntry::onEnabledChanged);
QObject::connect(menu, &QsMenuEntry::textChanged, this, &PlatformMenuEntry::onTextChanged);
QObject::connect(menu, &QsMenuEntry::iconChanged, this, &PlatformMenuEntry::onIconChanged);
QObject::connect(menu, &QsMenuEntry::buttonTypeChanged, this, &PlatformMenuEntry::onButtonTypeChanged);
QObject::connect(menu, &QsMenuEntry::checkStateChanged, this, &PlatformMenuEntry::onCheckStateChanged);
QObject::connect(menu, &QsMenuEntry::hasChildrenChanged, this, &PlatformMenuEntry::relayoutParent);
QObject::connect(menu->children(), &UntypedObjectModel::valuesChanged, this, &PlatformMenuEntry::relayout);
// clang-format on
}
PlatformMenuEntry::~PlatformMenuEntry() {
this->clearChildren();
delete this->qaction;
delete this->qmenu;
}
void PlatformMenuEntry::registerCreationHook(std::function<void(PlatformMenuQMenu*)> hook) {
CREATION_HOOKS.push_back(std::move(hook));
}
bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) {
QWindow* window = nullptr;
if (qobject_cast<QApplication*>(QCoreApplication::instance()) == nullptr) {
qCritical() << "Cannot display PlatformMenuEntry as quickshell was not started in "
"QApplication mode.";
qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your "
"root QML file and restart quickshell.";
return false;
} else if (this->qmenu == nullptr) {
qCritical() << "Cannot display PlatformMenuEntry as it is not a menu.";
return false;
} else if (parentWindow == nullptr) {
qCritical() << "Cannot display PlatformMenuEntry with null parent window.";
return false;
} else if (auto* proxy = qobject_cast<ProxyWindowBase*>(parentWindow)) {
window = proxy->backingWindow();
} else if (auto* interface = qobject_cast<WindowInterface*>(parentWindow)) {
window = interface->proxyWindow()->backingWindow();
} else {
qCritical() << "PlatformMenuEntry.display() must be called with a window.";
return false;
}
if (window == nullptr) {
qCritical() << "Cannot display PlatformMenuEntry from a parent window that is not visible.";
return false;
}
if (ACTIVE_MENU && this->qmenu != ACTIVE_MENU) {
ACTIVE_MENU->close();
}
ACTIVE_MENU = this->qmenu;
auto point = window->mapToGlobal(QPoint(relativeX, relativeY));
this->qmenu->createWinId();
this->qmenu->windowHandle()->setTransientParent(window);
// Skips screen edge repositioning so it can be left to the compositor on wayland.
this->qmenu->targetPosition = point;
this->qmenu->popup(point);
return true;
}
bool PlatformMenuEntry::display(PopupAnchor* anchor) {
if (qobject_cast<QApplication*>(QCoreApplication::instance()) == nullptr) {
qCritical() << "Cannot display PlatformMenuEntry as quickshell was not started in "
"QApplication mode.";
qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your "
"root QML file and restart quickshell.";
return false;
} else if (!anchor->backingWindow() || !anchor->backingWindow()->isVisible()) {
qCritical() << "Cannot display PlatformMenuEntry on anchor without visible window.";
return false;
}
if (ACTIVE_MENU && this->qmenu != ACTIVE_MENU) {
ACTIVE_MENU->close();
}
ACTIVE_MENU = this->qmenu;
this->qmenu->createWinId();
this->qmenu->windowHandle()->setTransientParent(anchor->backingWindow());
// Update the window geometry to the menu's actual dimensions so reposition
// can accurately adjust it if applicable for the current platform.
this->qmenu->windowHandle()->setGeometry({{0, 0}, this->qmenu->sizeHint()});
PopupPositioner::instance()->reposition(anchor, this->qmenu->windowHandle(), false);
// Open the menu at the position determined by the popup positioner.
this->qmenu->popup(this->qmenu->windowHandle()->position());
return true;
}
void PlatformMenuEntry::relayout() {
if (qobject_cast<QApplication*>(QCoreApplication::instance()) == nullptr) {
return;
}
if (this->menu->hasChildren()) {
delete this->qaction;
this->qaction = nullptr;
if (this->qmenu == nullptr) {
this->qmenu = new PlatformMenuQMenu();
QObject::connect(this->qmenu, &QMenu::aboutToShow, this, &PlatformMenuEntry::onAboutToShow);
QObject::connect(this->qmenu, &QMenu::aboutToHide, this, &PlatformMenuEntry::onAboutToHide);
} else {
this->clearChildren();
}
this->qmenu->setTitle(this->menu->text());
auto icon = this->menu->icon();
if (!icon.isEmpty()) {
this->qmenu->setIcon(getCurrentEngineImageAsIcon(icon));
}
const auto& children = this->menu->children()->valueList();
auto len = children.count();
for (auto i = 0; i < len; i++) {
auto* child = children.at(i);
auto* instance = new PlatformMenuEntry(child);
QObject::connect(instance, &QObject::destroyed, this, &PlatformMenuEntry::onChildDestroyed);
QObject::connect(
instance,
&PlatformMenuEntry::relayoutParent,
this,
&PlatformMenuEntry::relayout
);
this->childEntries.push_back(instance);
instance->addToQMenu(this->qmenu);
}
} else if (!this->menu->isSeparator()) {
this->clearChildren();
delete this->qmenu;
this->qmenu = nullptr;
if (this->qaction == nullptr) {
this->qaction = new QAction(this);
QObject::connect(
this->qaction,
&QAction::triggered,
this,
&PlatformMenuEntry::onActionTriggered
);
}
this->qaction->setText(this->menu->text());
auto icon = this->menu->icon();
if (!icon.isEmpty()) {
this->qaction->setIcon(getCurrentEngineImageAsIcon(icon));
}
this->qaction->setEnabled(this->menu->enabled());
this->qaction->setCheckable(this->menu->buttonType() != QsMenuButtonType::None);
if (this->menu->buttonType() == QsMenuButtonType::RadioButton) {
if (!this->qactiongroup) this->qactiongroup = new QActionGroup(this);
this->qaction->setActionGroup(this->qactiongroup);
}
this->qaction->setChecked(this->menu->checkState() != Qt::Unchecked);
} else {
delete this->qmenu;
delete this->qaction;
this->qmenu = nullptr;
this->qaction = nullptr;
}
}
void PlatformMenuEntry::onAboutToShow() { this->menu->ref(); }
void PlatformMenuEntry::onAboutToHide() {
this->menu->unref();
emit this->closed();
}
void PlatformMenuEntry::onActionTriggered() {
auto* action = qobject_cast<PlatformMenuEntry*>(this->sender()->parent());
emit action->menu->triggered();
}
void PlatformMenuEntry::onChildDestroyed() { this->childEntries.removeOne(this->sender()); }
void PlatformMenuEntry::onEnabledChanged() {
if (this->qaction != nullptr) {
this->qaction->setEnabled(this->menu->enabled());
}
}
void PlatformMenuEntry::onTextChanged() {
if (this->qmenu != nullptr) {
this->qmenu->setTitle(this->menu->text());
} else if (this->qaction != nullptr) {
this->qaction->setText(this->menu->text());
}
}
void PlatformMenuEntry::onIconChanged() {
if (this->qmenu == nullptr && this->qaction == nullptr) return;
auto iconName = this->menu->icon();
QIcon icon;
if (!iconName.isEmpty()) {
icon = getCurrentEngineImageAsIcon(iconName);
}
if (this->qmenu != nullptr) {
this->qmenu->setIcon(icon);
} else if (this->qaction != nullptr) {
this->qaction->setIcon(icon);
}
}
void PlatformMenuEntry::onButtonTypeChanged() {
if (this->qaction != nullptr) {
QActionGroup* group = nullptr;
if (this->menu->buttonType() == QsMenuButtonType::RadioButton) {
if (!this->qactiongroup) this->qactiongroup = new QActionGroup(this);
group = this->qactiongroup;
}
this->qaction->setActionGroup(group);
}
}
void PlatformMenuEntry::onCheckStateChanged() {
if (this->qaction != nullptr) {
this->qaction->setChecked(this->menu->checkState() != Qt::Unchecked);
}
}
void PlatformMenuEntry::clearChildren() {
for (auto* child: this->childEntries) {
delete child;
}
this->childEntries.clear();
}
void PlatformMenuEntry::addToQMenu(PlatformMenuQMenu* menu) {
if (this->qmenu != nullptr) {
menu->addMenu(this->qmenu);
this->qmenu->containingMenu = menu;
} else if (this->qaction != nullptr) {
menu->addAction(this->qaction);
} else {
menu->addSeparator();
}
}
} // namespace qs::menu::platform

63
src/core/platformmenu.hpp Normal file
View file

@ -0,0 +1,63 @@
#pragma once
#include <functional>
#include <qaction.h>
#include <qactiongroup.h>
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include "../core/popupanchor.hpp"
#include "qsmenu.hpp"
namespace qs::menu::platform {
class PlatformMenuQMenu;
class PlatformMenuEntry: public QObject {
Q_OBJECT;
public:
explicit PlatformMenuEntry(QsMenuEntry* menu);
~PlatformMenuEntry() override;
Q_DISABLE_COPY_MOVE(PlatformMenuEntry);
bool display(QObject* parentWindow, int relativeX, int relativeY);
bool display(PopupAnchor* anchor);
static void registerCreationHook(std::function<void(PlatformMenuQMenu*)> hook);
signals:
void closed();
void relayoutParent();
public slots:
void relayout();
private slots:
void onAboutToShow();
void onAboutToHide();
void onActionTriggered();
void onChildDestroyed();
void onEnabledChanged();
void onTextChanged();
void onIconChanged();
void onButtonTypeChanged();
void onCheckStateChanged();
private:
void clearChildren();
void addToQMenu(PlatformMenuQMenu* menu);
QsMenuEntry* menu;
PlatformMenuQMenu* qmenu = nullptr;
QAction* qaction = nullptr;
QActionGroup* qactiongroup = nullptr;
QVector<PlatformMenuEntry*> childEntries;
};
} // namespace qs::menu::platform

View file

@ -0,0 +1,19 @@
#pragma once
#include <qmenu.h>
#include <qpoint.h>
namespace qs::menu::platform {
class PlatformMenuQMenu: public QMenu {
public:
explicit PlatformMenuQMenu() = default;
~PlatformMenuQMenu() override;
Q_DISABLE_COPY_MOVE(PlatformMenuQMenu);
void setVisible(bool visible) override;
PlatformMenuQMenu* containingMenu = nullptr;
QPoint targetPosition;
};
} // namespace qs::menu::platform

View file

@ -5,37 +5,41 @@
#include "generation.hpp"
static QVector<QuickshellPlugin*> plugins; // NOLINT
static QVector<QsEnginePlugin*> plugins; // NOLINT
void QuickshellPlugin::registerPlugin(QuickshellPlugin& plugin) { plugins.push_back(&plugin); }
void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); }
void QuickshellPlugin::initPlugins() {
void QsEnginePlugin::initPlugins() {
plugins.erase(
std::remove_if(
plugins.begin(),
plugins.end(),
[](QuickshellPlugin* plugin) { return !plugin->applies(); }
[](QsEnginePlugin* plugin) { return !plugin->applies(); }
),
plugins.end()
);
for (QuickshellPlugin* plugin: plugins) {
std::sort(plugins.begin(), plugins.end(), [](QsEnginePlugin* a, QsEnginePlugin* b) {
return b->dependencies().contains(a->name());
});
for (QsEnginePlugin* plugin: plugins) {
plugin->init();
}
for (QuickshellPlugin* plugin: plugins) {
for (QsEnginePlugin* plugin: plugins) {
plugin->registerTypes();
}
}
void QuickshellPlugin::runConstructGeneration(EngineGeneration& generation) {
for (QuickshellPlugin* plugin: plugins) {
void QsEnginePlugin::runConstructGeneration(EngineGeneration& generation) {
for (QsEnginePlugin* plugin: plugins) {
plugin->constructGeneration(generation);
}
}
void QuickshellPlugin::runOnReload() {
for (QuickshellPlugin* plugin: plugins) {
void QsEnginePlugin::runOnReload() {
for (QsEnginePlugin* plugin: plugins) {
plugin->onReload();
}
}

View file

@ -2,25 +2,28 @@
#include <qcontainerfwd.h>
#include <qfunctionpointer.h>
#include <qlist.h>
class EngineGeneration;
class QuickshellPlugin {
class QsEnginePlugin {
public:
QuickshellPlugin() = default;
virtual ~QuickshellPlugin() = default;
QuickshellPlugin(QuickshellPlugin&&) = delete;
QuickshellPlugin(const QuickshellPlugin&) = delete;
void operator=(QuickshellPlugin&&) = delete;
void operator=(const QuickshellPlugin&) = delete;
QsEnginePlugin() = default;
virtual ~QsEnginePlugin() = default;
QsEnginePlugin(QsEnginePlugin&&) = delete;
QsEnginePlugin(const QsEnginePlugin&) = delete;
void operator=(QsEnginePlugin&&) = delete;
void operator=(const QsEnginePlugin&) = delete;
virtual QString name() { return QString(); }
virtual QList<QString> dependencies() { return {}; }
virtual bool applies() { return true; }
virtual void init() {}
virtual void registerTypes() {}
virtual void constructGeneration(EngineGeneration& /*unused*/) {} // NOLINT
virtual void onReload() {}
static void registerPlugin(QuickshellPlugin& plugin);
static void registerPlugin(QsEnginePlugin& plugin);
static void initPlugins();
static void runConstructGeneration(EngineGeneration& generation);
static void runOnReload();
@ -30,6 +33,6 @@ public:
#define QS_REGISTER_PLUGIN(clazz) \
[[gnu::constructor]] void qsInitPlugin() { \
static clazz plugin; \
QuickshellPlugin::registerPlugin(plugin); \
QsEnginePlugin::registerPlugin(plugin); \
}
// NOLINTEND

322
src/core/popupanchor.cpp Normal file
View file

@ -0,0 +1,322 @@
#include "popupanchor.hpp"
#include <qcontainerfwd.h>
#include <qlogging.h>
#include <qobject.h>
#include <qsize.h>
#include <qtmetamacros.h>
#include <qwindow.h>
#include "../window/proxywindow.hpp"
#include "../window/windowinterface.hpp"
#include "types.hpp"
bool PopupAnchorState::operator==(const PopupAnchorState& other) const {
return this->rect == other.rect && this->edges == other.edges && this->gravity == other.gravity
&& this->adjustment == other.adjustment && this->anchorpoint == other.anchorpoint
&& this->size == other.size;
}
bool PopupAnchor::isDirty() const {
return !this->lastState.has_value() || this->state != this->lastState.value();
}
void PopupAnchor::markClean() { this->lastState = this->state; }
void PopupAnchor::markDirty() { this->lastState.reset(); }
QObject* PopupAnchor::window() const { return this->mWindow; }
ProxyWindowBase* PopupAnchor::proxyWindow() const { return this->mProxyWindow; }
QWindow* PopupAnchor::backingWindow() const {
return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr;
}
void PopupAnchor::setWindow(QObject* window) {
if (window == this->mWindow) return;
if (this->mWindow) {
QObject::disconnect(this->mWindow, nullptr, this, nullptr);
QObject::disconnect(this->mProxyWindow, nullptr, this, nullptr);
}
if (window) {
if (auto* proxy = qobject_cast<ProxyWindowBase*>(window)) {
this->mProxyWindow = proxy;
} else if (auto* interface = qobject_cast<WindowInterface*>(window)) {
this->mProxyWindow = interface->proxyWindow();
} else {
qWarning() << "Tried to set popup anchor window to" << window
<< "which is not a quickshell window.";
goto setnull;
}
this->mWindow = window;
QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed);
QObject::connect(
this->mProxyWindow,
&ProxyWindowBase::backerVisibilityChanged,
this,
&PopupAnchor::backingWindowVisibilityChanged
);
emit this->windowChanged();
emit this->backingWindowVisibilityChanged();
return;
}
setnull:
if (this->mWindow) {
this->mWindow = nullptr;
this->mProxyWindow = nullptr;
emit this->windowChanged();
emit this->backingWindowVisibilityChanged();
}
}
void PopupAnchor::onWindowDestroyed() {
this->mWindow = nullptr;
this->mProxyWindow = nullptr;
emit this->windowChanged();
emit this->backingWindowVisibilityChanged();
}
Box PopupAnchor::rect() const { return this->state.rect; }
void PopupAnchor::setRect(Box rect) {
if (rect == this->state.rect) return;
if (rect.w <= 0) rect.w = 1;
if (rect.h <= 0) rect.h = 1;
this->state.rect = rect;
emit this->rectChanged();
}
Edges::Flags PopupAnchor::edges() const { return this->state.edges; }
void PopupAnchor::setEdges(Edges::Flags edges) {
if (edges == this->state.edges) return;
if (Edges::isOpposing(edges)) {
qWarning() << "Cannot set opposing edges for anchor edges. Tried to set" << edges;
return;
}
this->state.edges = edges;
emit this->edgesChanged();
}
Edges::Flags PopupAnchor::gravity() const { return this->state.gravity; }
void PopupAnchor::setGravity(Edges::Flags gravity) {
if (gravity == this->state.gravity) return;
if (Edges::isOpposing(gravity)) {
qWarning() << "Cannot set opposing edges for anchor gravity. Tried to set" << gravity;
return;
}
this->state.gravity = gravity;
emit this->gravityChanged();
}
PopupAdjustment::Flags PopupAnchor::adjustment() const { return this->state.adjustment; }
void PopupAnchor::setAdjustment(PopupAdjustment::Flags adjustment) {
if (adjustment == this->state.adjustment) return;
this->state.adjustment = adjustment;
emit this->adjustmentChanged();
}
void PopupAnchor::updatePlacement(const QPoint& anchorpoint, const QSize& size) {
this->state.anchorpoint = anchorpoint;
this->state.size = size;
}
static PopupPositioner* POSITIONER = nullptr; // NOLINT
void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty) {
auto* parentWindow = window->transientParent();
if (!parentWindow) {
qFatal() << "Cannot reposition popup that does not have a transient parent.";
}
auto parentGeometry = parentWindow->geometry();
auto windowGeometry = window->geometry();
emit anchor->anchoring();
anchor->updatePlacement(parentGeometry.topLeft(), windowGeometry.size());
if (onlyIfDirty && !anchor->isDirty()) return;
anchor->markClean();
auto adjustment = anchor->adjustment();
auto screenGeometry = parentWindow->screen()->geometry();
auto anchorRectGeometry = anchor->rect().qrect().translated(parentGeometry.topLeft());
auto anchorEdges = anchor->edges();
auto anchorGravity = anchor->gravity();
auto width = windowGeometry.width();
auto height = windowGeometry.height();
auto anchorX = anchorEdges.testFlag(Edges::Left) ? anchorRectGeometry.left()
: anchorEdges.testFlag(Edges::Right) ? anchorRectGeometry.right()
: anchorRectGeometry.center().x();
auto anchorY = anchorEdges.testFlag(Edges::Top) ? anchorRectGeometry.top()
: anchorEdges.testFlag(Edges::Bottom) ? anchorRectGeometry.bottom()
: anchorRectGeometry.center().y();
auto calcEffectiveX = [&](Edges::Flags anchorGravity, int anchorX) {
auto ex = anchorGravity.testFlag(Edges::Left) ? anchorX - windowGeometry.width()
: anchorGravity.testFlag(Edges::Right) ? anchorX - 1
: anchorX - windowGeometry.width() / 2;
return ex + 1;
};
auto calcEffectiveY = [&](Edges::Flags anchorGravity, int anchorY) {
auto ey = anchorGravity.testFlag(Edges::Top) ? anchorY - windowGeometry.height()
: anchorGravity.testFlag(Edges::Bottom) ? anchorY - 1
: anchorY - windowGeometry.height() / 2;
return ey + 1;
};
auto calcRemainingWidth = [&](int effectiveX) {
auto width = windowGeometry.width();
if (effectiveX < screenGeometry.left()) {
auto diff = screenGeometry.left() - effectiveX;
effectiveX = screenGeometry.left();
width -= diff;
}
auto effectiveX2 = effectiveX + width;
if (effectiveX2 > screenGeometry.right()) {
width -= effectiveX2 - screenGeometry.right() - 1;
}
return QPair<int, int>(effectiveX, width);
};
auto calcRemainingHeight = [&](int effectiveY) {
auto height = windowGeometry.height();
if (effectiveY < screenGeometry.left()) {
auto diff = screenGeometry.top() - effectiveY;
effectiveY = screenGeometry.top();
height -= diff;
}
auto effectiveY2 = effectiveY + height;
if (effectiveY2 > screenGeometry.bottom()) {
height -= effectiveY2 - screenGeometry.bottom() - 1;
}
return QPair<int, int>(effectiveY, height);
};
auto effectiveX = calcEffectiveX(anchorGravity, anchorX);
auto effectiveY = calcEffectiveY(anchorGravity, anchorY);
if (adjustment.testFlag(PopupAdjustment::FlipX)) {
const bool flip = (anchorGravity.testFlag(Edges::Left) && effectiveX < screenGeometry.left())
|| (anchorGravity.testFlag(Edges::Right)
&& effectiveX + windowGeometry.width() > screenGeometry.right());
if (flip) {
auto newAnchorGravity = anchorGravity ^ (Edges::Left | Edges::Right);
auto newAnchorX = anchorEdges.testFlags(Edges::Left) ? anchorRectGeometry.right()
: anchorEdges.testFlags(Edges::Right) ? anchorRectGeometry.left()
: anchorX;
auto newEffectiveX = calcEffectiveX(newAnchorGravity, newAnchorX);
// TODO IN HL: pick constraint monitor based on anchor rect position in window
// if the available width when flipped is more than the available width without flipping then flip
if (calcRemainingWidth(newEffectiveX).second > calcRemainingWidth(effectiveX).second) {
anchorGravity = newAnchorGravity;
anchorX = newAnchorX;
effectiveX = newEffectiveX;
}
}
}
if (adjustment.testFlag(PopupAdjustment::FlipY)) {
const bool flip = (anchorGravity.testFlag(Edges::Top) && effectiveY < screenGeometry.top())
|| (anchorGravity.testFlag(Edges::Bottom)
&& effectiveY + windowGeometry.height() > screenGeometry.bottom());
if (flip) {
auto newAnchorGravity = anchorGravity ^ (Edges::Top | Edges::Bottom);
auto newAnchorY = anchorEdges.testFlags(Edges::Top) ? anchorRectGeometry.bottom()
: anchorEdges.testFlags(Edges::Bottom) ? anchorRectGeometry.top()
: anchorY;
auto newEffectiveY = calcEffectiveY(newAnchorGravity, newAnchorY);
// if the available width when flipped is more than the available width without flipping then flip
if (calcRemainingHeight(newEffectiveY).second > calcRemainingHeight(effectiveY).second) {
anchorGravity = newAnchorGravity;
anchorY = newAnchorY;
effectiveY = newEffectiveY;
}
}
}
// Slide order is important for the case where the window is too large to fit on screen.
if (adjustment.testFlag(PopupAdjustment::SlideX)) {
if (effectiveX + windowGeometry.width() > screenGeometry.right()) {
effectiveX = screenGeometry.right() - windowGeometry.width() + 1;
}
if (effectiveX < screenGeometry.left()) {
effectiveX = screenGeometry.left();
}
}
if (adjustment.testFlag(PopupAdjustment::SlideY)) {
if (effectiveY + windowGeometry.height() > screenGeometry.bottom()) {
effectiveY = screenGeometry.bottom() - windowGeometry.height() + 1;
}
if (effectiveY < screenGeometry.top()) {
effectiveY = screenGeometry.top();
}
}
if (adjustment.testFlag(PopupAdjustment::ResizeX)) {
auto [newX, newWidth] = calcRemainingWidth(effectiveX);
effectiveX = newX;
width = newWidth;
}
if (adjustment.testFlag(PopupAdjustment::ResizeY)) {
auto [newY, newHeight] = calcRemainingHeight(effectiveY);
effectiveY = newY;
height = newHeight;
}
window->setGeometry({effectiveX, effectiveY, width, height});
}
bool PopupPositioner::shouldRepositionOnMove() const { return true; }
PopupPositioner* PopupPositioner::instance() {
if (POSITIONER == nullptr) {
POSITIONER = new PopupPositioner();
}
return POSITIONER;
}
void PopupPositioner::setInstance(PopupPositioner* instance) {
delete POSITIONER;
POSITIONER = instance;
}

174
src/core/popupanchor.hpp Normal file
View file

@ -0,0 +1,174 @@
#pragma once
#include <optional>
#include <qflags.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qpoint.h>
#include <qqmlintegration.h>
#include <qsize.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwindow.h>
#include "../window/proxywindow.hpp"
#include "doc.hpp"
#include "types.hpp"
///! Adjustment strategy for popups that do not fit on screen.
/// Adjustment strategy for popups. See @@PopupAnchor.adjustment.
///
/// Adjustment flags can be combined with the `|` operator.
///
/// `Flip` will be applied first, then `Slide`, then `Resize`.
namespace PopupAdjustment { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum : quint8 {
None = 0,
/// If the X axis is constrained, the popup will slide along the X axis until it fits onscreen.
SlideX = 1,
/// If the Y axis is constrained, the popup will slide along the Y axis until it fits onscreen.
SlideY = 2,
/// Alias for `SlideX | SlideY`.
Slide = SlideX | SlideY,
/// If the X axis is constrained, the popup will invert its horizontal gravity if any.
FlipX = 4,
/// If the Y axis is constrained, the popup will invert its vertical gravity if any.
FlipY = 8,
/// Alias for `FlipX | FlipY`.
Flip = FlipX | FlipY,
/// If the X axis is constrained, the width of the popup will be reduced to fit on screen.
ResizeX = 16,
/// If the Y axis is constrained, the height of the popup will be reduced to fit on screen.
ResizeY = 32,
/// Alias for `ResizeX | ResizeY`
Resize = ResizeX | ResizeY,
/// Alias for `Flip | Slide | Resize`.
All = Slide | Flip | Resize,
};
Q_ENUM_NS(Enum);
Q_DECLARE_FLAGS(Flags, Enum);
} // namespace PopupAdjustment
Q_DECLARE_OPERATORS_FOR_FLAGS(PopupAdjustment::Flags);
struct PopupAnchorState {
bool operator==(const PopupAnchorState& other) const;
Box rect = {0, 0, 1, 1};
Edges::Flags edges = Edges::Top | Edges::Left;
Edges::Flags gravity = Edges::Bottom | Edges::Right;
PopupAdjustment::Flags adjustment = PopupAdjustment::Slide;
QPoint anchorpoint;
QSize size;
};
///! Anchorpoint or positioner for popup windows.
class PopupAnchor: public QObject {
Q_OBJECT;
// clang-format off
/// The window to anchor / attach the popup to.
Q_PROPERTY(QObject* window READ window WRITE setWindow NOTIFY windowChanged);
/// The anchorpoints the popup will attach to. Which anchors will be used is
/// determined by the @@edges, @@gravity, and @@adjustment.
///
/// If you leave @@edges, @@gravity and @@adjustment at their default values,
/// setting more than `x` and `y` does not matter. The anchor rect cannot
/// be smaller than 1x1 pixels.
///
/// > [!INFO] To position a popup relative to an item inside a window,
/// > you can use [coordinate mapping functions] (note the warning below).
///
/// > [!WARNING] Using [coordinate mapping functions] in a binding to
/// > this property will position the anchor incorrectly.
/// > If you want to use them, do so in @@anchoring(s), or use
/// > @@TransformWatcher if you need real-time updates to mapped coordinates.
///
/// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method
Q_PROPERTY(Box rect READ rect WRITE setRect NOTIFY rectChanged);
/// The point on the anchor rectangle the popup should anchor to.
/// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed.
///
/// Defaults to `Edges.Top | Edges.Left`.
Q_PROPERTY(Edges::Flags edges READ edges WRITE setEdges NOTIFY edgesChanged);
/// The direction the popup should expand towards, relative to the anchorpoint.
/// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed.
///
/// Defaults to `Edges.Bottom | Edges.Right`.
Q_PROPERTY(Edges::Flags gravity READ gravity WRITE setGravity NOTIFY gravityChanged);
/// The strategy used to adjust the popup's position if it would otherwise not fit on screen,
/// based on the anchor @@rect, preferred @@edges, and @@gravity.
///
/// See the documentation for @@PopupAdjustment for details.
Q_PROPERTY(PopupAdjustment::Flags adjustment READ adjustment WRITE setAdjustment NOTIFY adjustmentChanged);
// clang-format on
QML_ELEMENT;
QML_UNCREATABLE("");
public:
explicit PopupAnchor(QObject* parent): QObject(parent) {}
[[nodiscard]] bool isDirty() const;
void markClean();
void markDirty();
[[nodiscard]] QObject* window() const;
[[nodiscard]] ProxyWindowBase* proxyWindow() const;
[[nodiscard]] QWindow* backingWindow() const;
void setWindow(QObject* window);
[[nodiscard]] Box rect() const;
void setRect(Box rect);
[[nodiscard]] Edges::Flags edges() const;
void setEdges(Edges::Flags edges);
[[nodiscard]] Edges::Flags gravity() const;
void setGravity(Edges::Flags gravity);
[[nodiscard]] PopupAdjustment::Flags adjustment() const;
void setAdjustment(PopupAdjustment::Flags adjustment);
void updatePlacement(const QPoint& anchorpoint, const QSize& size);
signals:
/// Emitted when this anchor is about to be used. Mostly useful for modifying
/// the anchor @@rect using [coordinate mapping functions], which are not reactive.
///
/// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method
void anchoring();
void windowChanged();
QSDOC_HIDE void backingWindowVisibilityChanged();
void rectChanged();
void edgesChanged();
void gravityChanged();
void adjustmentChanged();
private slots:
void onWindowDestroyed();
private:
QObject* mWindow = nullptr;
ProxyWindowBase* mProxyWindow = nullptr;
PopupAnchorState state;
std::optional<PopupAnchorState> lastState;
};
class PopupPositioner {
public:
explicit PopupPositioner() = default;
virtual ~PopupPositioner() = default;
Q_DISABLE_COPY_MOVE(PopupPositioner);
virtual void reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty = true);
[[nodiscard]] virtual bool shouldRepositionOnMove() const;
static PopupPositioner* instance();
static void setInstance(PopupPositioner* instance);
};

View file

@ -1,161 +0,0 @@
#include "popupwindow.hpp"
#include <qlogging.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qquickwindow.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "proxywindow.hpp"
#include "qmlscreen.hpp"
#include "windowinterface.hpp"
ProxyPopupWindow::ProxyPopupWindow(QObject* parent): ProxyWindowBase(parent) {
this->mVisible = false;
}
void ProxyPopupWindow::completeWindow() {
this->ProxyWindowBase::completeWindow();
this->window->setFlag(Qt::ToolTip);
this->updateTransientParent();
}
void ProxyPopupWindow::postCompleteWindow() { this->ProxyWindowBase::setVisible(this->mVisible); }
bool ProxyPopupWindow::deleteOnInvisible() const {
// Currently crashes in normal mode, do not have the time to debug it now.
return true;
}
qint32 ProxyPopupWindow::x() const {
// QTBUG-121550
auto basepos = this->mParentProxyWindow == nullptr ? 0 : this->mParentProxyWindow->x();
return basepos + this->mRelativeX;
}
void ProxyPopupWindow::setParentWindow(QObject* parent) {
if (parent == this->mParentWindow) return;
if (this->mParentWindow != nullptr) {
QObject::disconnect(this->mParentWindow, nullptr, this, nullptr);
QObject::disconnect(this->mParentProxyWindow, nullptr, this, nullptr);
}
if (parent == nullptr) {
this->mParentWindow = nullptr;
this->mParentProxyWindow = nullptr;
} else {
if (auto* proxy = qobject_cast<ProxyWindowBase*>(parent)) {
this->mParentProxyWindow = proxy;
} else if (auto* interface = qobject_cast<WindowInterface*>(parent)) {
this->mParentProxyWindow = interface->proxyWindow();
} else {
qWarning() << "Tried to set popup parent window to something that is not a quickshell window:"
<< parent;
this->mParentWindow = nullptr;
this->mParentProxyWindow = nullptr;
this->updateTransientParent();
return;
}
this->mParentWindow = parent;
// clang-format off
QObject::connect(this->mParentWindow, &QObject::destroyed, this, &ProxyPopupWindow::onParentDestroyed);
QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::xChanged, this, &ProxyPopupWindow::updateX);
QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::yChanged, this, &ProxyPopupWindow::updateY);
QObject::connect(this->mParentProxyWindow, &ProxyWindowBase::backerVisibilityChanged, this, &ProxyPopupWindow::onParentUpdated);
// clang-format on
}
this->updateTransientParent();
}
QObject* ProxyPopupWindow::parentWindow() const { return this->mParentWindow; }
void ProxyPopupWindow::updateTransientParent() {
this->updateX();
this->updateY();
if (this->window != nullptr) {
this->window->setTransientParent(
this->mParentProxyWindow == nullptr ? nullptr : this->mParentProxyWindow->backingWindow()
);
}
this->updateVisible();
}
void ProxyPopupWindow::onParentUpdated() { this->updateTransientParent(); }
void ProxyPopupWindow::onParentDestroyed() {
this->mParentWindow = nullptr;
this->mParentProxyWindow = nullptr;
this->updateVisible();
emit this->parentWindowChanged();
}
void ProxyPopupWindow::setScreen(QuickshellScreenInfo* /*unused*/) {
qWarning() << "Cannot set screen of popup window, as that is controlled by the parent window";
}
void ProxyPopupWindow::setVisible(bool visible) {
if (visible == this->wantsVisible) return;
this->wantsVisible = visible;
this->updateVisible();
}
void ProxyPopupWindow::updateVisible() {
auto target = this->wantsVisible && this->mParentWindow != nullptr
&& this->mParentProxyWindow->isVisibleDirect();
if (target && this->window != nullptr && !this->window->isVisible()) {
this->updateX(); // QTBUG-121550
}
this->ProxyWindowBase::setVisible(target);
}
void ProxyPopupWindow::setRelativeX(qint32 x) {
if (x == this->mRelativeX) return;
this->mRelativeX = x;
this->updateX();
}
qint32 ProxyPopupWindow::relativeX() const { return this->mRelativeX; }
void ProxyPopupWindow::setRelativeY(qint32 y) {
if (y == this->mRelativeY) return;
this->mRelativeY = y;
this->updateY();
}
qint32 ProxyPopupWindow::relativeY() const { return this->mRelativeY; }
void ProxyPopupWindow::updateX() {
if (this->mParentWindow == nullptr || this->window == nullptr) return;
auto target = this->x() - 1; // QTBUG-121550
auto reshow = this->isVisibleDirect() && (this->window->x() != target && this->x() != target);
if (reshow) this->setVisibleDirect(false);
if (this->window != nullptr) this->window->setX(target);
if (reshow && this->wantsVisible) this->setVisibleDirect(true);
}
void ProxyPopupWindow::updateY() {
if (this->mParentWindow == nullptr || this->window == nullptr) return;
auto target = this->mParentProxyWindow->y() + this->relativeY();
auto reshow = this->isVisibleDirect() && this->window->y() != target;
if (reshow) {
this->setVisibleDirect(false);
this->updateX(); // QTBUG-121550
}
if (this->window != nullptr) this->window->setY(target);
if (reshow && this->wantsVisible) this->setVisibleDirect(true);
}

View file

@ -5,6 +5,7 @@
#include <qcoreapplication.h>
#include <qdir.h>
#include <qguiapplication.h>
#include <qicon.h>
#include <qjsengine.h>
#include <qlogging.h>
#include <qobject.h>
@ -19,6 +20,7 @@
#include <unistd.h>
#include "generation.hpp"
#include "iconimageprovider.hpp"
#include "qmlscreen.hpp"
#include "rootwrapper.hpp"
@ -165,6 +167,12 @@ 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();
}
@ -187,3 +195,27 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT
return qEnvironmentVariable(vstr.data());
}
QString QuickshellGlobal::iconPath(const QString& icon) {
return IconImageProvider::requestString(icon);
}
QString QuickshellGlobal::iconPath(const QString& icon, bool check) {
if (check && QIcon::fromTheme(icon).isNull()) return "";
return IconImageProvider::requestString(icon);
}
QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback) {
return IconImageProvider::requestString(icon, "", fallback);
}
QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) {
auto* qsg = new QuickshellGlobal();
auto* generation = EngineGeneration::findEngineGeneration(engine);
if (generation->qsgInstance == nullptr) {
generation->qsgInstance = qsg;
}
return qsg;
}

View file

@ -98,6 +98,11 @@ class QuickshellGlobal: public QObject {
/// This creates an instance of your window once on every screen.
/// As screens are added or removed your window will be created or destroyed on those screens.
Q_PROPERTY(QQmlListProperty<QuickshellScreenInfo> screens READ screens NOTIFY screensChanged);
/// The full path to the root directory of your shell.
///
/// The root directory is the folder containing the entrypoint to your shell, often referred
/// to as `shell.qml`.
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);
/// If true then the configuration will be reloaded whenever any files change.
@ -110,40 +115,62 @@ class QuickshellGlobal: public QObject {
public:
[[nodiscard]] qint32 processId() const;
QuickshellGlobal(QObject* parent = nullptr);
QQmlListProperty<QuickshellScreenInfo> screens();
/// Reload the shell from the [ShellRoot].
/// Reload the shell.
///
/// `hard` - perform a hard reload. If this is false, Quickshell will attempt to reuse windows
/// that already exist. If true windows will be recreated.
///
/// See [Reloadable] for more information on what can be reloaded and how.
///
/// [Reloadable]: ../reloadable
/// See @@Reloadable for more information on what can be reloaded and how.
Q_INVOKABLE void reload(bool hard);
/// Returns the string value of an environment variable or null if it is not set.
Q_INVOKABLE QVariant env(const QString& variable);
/// 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,
/// > which means they should match with all other qt applications on your system.
/// >
/// > If you want to use a different icon theme, you can put `//@ pragma IconTheme <name>`
/// > at the top of your root config file or set the `QS_ICON_THEME` variable to the name
/// > of your icon theme.
Q_INVOKABLE static QString iconPath(const QString& icon);
/// Setting the `check` parameter of `iconPath` to true will return an empty string
/// if the icon does not exist, instead of an image showing a missing texture.
Q_INVOKABLE static QString iconPath(const QString& icon, bool check);
/// 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);
[[nodiscard]] QString shellRoot() const;
[[nodiscard]] QString workingDirectory() const;
void setWorkingDirectory(QString workingDirectory);
[[nodiscard]] bool watchFiles() const;
void setWatchFiles(bool watchFiles);
static QuickshellGlobal* create(QQmlEngine* engine, QJSEngine* /*unused*/);
signals:
/// Sent when the last window is closed.
///
/// To make the application exit when the last window is closed run `Qt.quit()`.
void lastWindowClosed();
/// The reload sequence has completed successfully.
void reloadCompleted();
/// The reload sequence has failed.
void reloadFailed(QString errorString);
void screensChanged();
void workingDirectoryChanged();
void watchFilesChanged();
private:
QuickshellGlobal(QObject* parent = nullptr);
static qsizetype screensCount(QQmlListProperty<QuickshellScreenInfo>* prop);
static QuickshellScreenInfo* screenAt(QQmlListProperty<QuickshellScreenInfo>* prop, qsizetype i);
};

View file

@ -12,17 +12,14 @@
// unfortunately QQuickScreenInfo is private.
/// Monitor object useful for setting the monitor for a [ShellWindow]
/// Monitor object useful for setting the monitor for a @@QsWindow
/// or querying information about the monitor.
///
/// > [!WARNING] If the monitor is disconnected than any stored copies of its ShellMonitor will
/// > be marked as dangling and all properties will return default values.
/// > Reconnecting the monitor will not reconnect it to the ShellMonitor object.
///
/// Due to some technical limitations, it was not possible to reuse the native qml [Screen] type.
///
/// [ShellWindow]: ../shellwindow
/// [Screen]: https://doc.qt.io/qt-6/qml-qtquick-screen.html
/// Due to some technical limitations, it was not possible to reuse the native qml @@QtQuick.Screen type.
class QuickshellScreenInfo: public QObject {
Q_OBJECT;
QML_NAMED_ELEMENT(ShellScreen);

View file

@ -16,7 +16,22 @@
Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg);
QUrl QsUrlInterceptor::intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) {
QUrl QsUrlInterceptor::intercept(
const QUrl& originalUrl,
QQmlAbstractUrlInterceptor::DataType type
) {
auto url = originalUrl;
if (url.scheme() == "root") {
url.setScheme("qsintercept");
auto path = url.path();
if (path.startsWith('/')) path = path.sliced(1);
url.setPath(this->configRoot.filePath(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") {

View file

@ -1,5 +1,6 @@
#pragma once
#include <qdir.h>
#include <qhash.h>
#include <qloggingcategory.h>
#include <qnetworkaccessmanager.h>
@ -13,7 +14,12 @@ Q_DECLARE_LOGGING_CATEGORY(logQsIntercept);
class QsUrlInterceptor: public QQmlAbstractUrlInterceptor {
public:
QUrl intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) override;
explicit QsUrlInterceptor(const QDir& configRoot): configRoot(configRoot) {}
QUrl intercept(const QUrl& originalUrl, QQmlAbstractUrlInterceptor::DataType type) override;
private:
QDir configRoot;
};
class QsInterceptDataReply: public QNetworkReply {

110
src/core/qsmenu.cpp Normal file
View file

@ -0,0 +1,110 @@
#include "qsmenu.hpp"
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include "model.hpp"
#include "platformmenu.hpp"
using namespace qs::menu::platform;
namespace qs::menu {
QString QsMenuButtonType::toString(QsMenuButtonType::Enum value) {
switch (value) {
case QsMenuButtonType::None: return "None";
case QsMenuButtonType::CheckBox: return "CheckBox";
case QsMenuButtonType::RadioButton: return "RadioButton";
default: return "Invalid button type";
}
}
QsMenuEntry* QsMenuEntry::menu() { return this; }
void QsMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) {
auto* platform = new PlatformMenuEntry(this);
QObject::connect(platform, &PlatformMenuEntry::closed, platform, [=]() {
platform->deleteLater();
});
auto success = platform->display(parentWindow, relativeX, relativeY);
if (!success) delete platform;
}
void QsMenuEntry::ref() {
this->refcount++;
if (this->refcount == 1) emit this->opened();
}
void QsMenuEntry::unref() {
this->refcount--;
if (this->refcount == 0) emit this->closed();
}
ObjectModel<QsMenuEntry>* QsMenuEntry::children() {
return ObjectModel<QsMenuEntry>::emptyInstance();
}
QsMenuOpener::~QsMenuOpener() {
if (this->mMenu) {
if (this->mMenu->menu()) this->mMenu->menu()->unref();
this->mMenu->unrefHandle();
}
}
QsMenuHandle* QsMenuOpener::menu() const { return this->mMenu; }
void QsMenuOpener::setMenu(QsMenuHandle* menu) {
if (menu == this->mMenu) return;
if (this->mMenu != nullptr) {
QObject::disconnect(this->mMenu, nullptr, this, nullptr);
if (this->mMenu->menu()) {
QObject::disconnect(this->mMenu->menu(), nullptr, this, nullptr);
this->mMenu->menu()->unref();
}
this->mMenu->unrefHandle();
}
this->mMenu = menu;
if (menu != nullptr) {
auto onMenuChanged = [this, menu]() {
if (menu->menu()) {
menu->menu()->ref();
}
emit this->childrenChanged();
};
QObject::connect(menu, &QObject::destroyed, this, &QsMenuOpener::onMenuDestroyed);
QObject::connect(menu, &QsMenuHandle::menuChanged, this, onMenuChanged);
if (menu->menu()) onMenuChanged();
menu->refHandle();
}
emit this->menuChanged();
emit this->childrenChanged();
}
void QsMenuOpener::onMenuDestroyed() {
this->mMenu = nullptr;
emit this->menuChanged();
emit this->childrenChanged();
}
ObjectModel<QsMenuEntry>* QsMenuOpener::children() {
if (this->mMenu && this->mMenu->menu()) {
return this->mMenu->menu()->children();
} else {
return ObjectModel<QsMenuEntry>::emptyInstance();
}
}
} // namespace qs::menu

163
src/core/qsmenu.hpp Normal file
View file

@ -0,0 +1,163 @@
#pragma once
#include <qcontainerfwd.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "doc.hpp"
#include "model.hpp"
namespace qs::menu {
///! Button type associated with a QsMenuEntry.
/// See @@QsMenuEntry.buttonType.
class QsMenuButtonType: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum : quint8 {
/// This menu item does not have a checkbox or a radiobutton associated with it.
None = 0,
/// This menu item should draw a checkbox.
CheckBox = 1,
/// This menu item should draw a radiobutton.
RadioButton = 2,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(qs::menu::QsMenuButtonType::Enum value);
};
class QsMenuEntry;
///! Menu handle for QsMenuOpener
/// See @@QsMenuOpener.
class QsMenuHandle: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_UNCREATABLE("");
public:
explicit QsMenuHandle(QObject* parent): QObject(parent) {}
virtual void refHandle() {};
virtual void unrefHandle() {};
[[nodiscard]] virtual QsMenuEntry* menu() = 0;
signals:
void menuChanged();
};
class QsMenuEntry: public QsMenuHandle {
Q_OBJECT;
/// If this menu item should be rendered as a separator between other items.
///
/// No other properties have a meaningful value when @@isSeparator is true.
Q_PROPERTY(bool isSeparator READ isSeparator NOTIFY isSeparatorChanged);
Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged);
/// Text of the menu item.
Q_PROPERTY(QString text READ text NOTIFY textChanged);
/// Url of the menu item's icon or `""` if it doesn't have one.
///
/// This can be passed to [Image.source](https://doc.qt.io/qt-6/qml-qtquick-image.html#source-prop)
/// as shown below.
///
/// ```qml
/// Image {
/// source: menuItem.icon
/// // To get the best image quality, set the image source size to the same size
/// // as the rendered image.
/// sourceSize.width: width
/// sourceSize.height: height
/// }
/// ```
Q_PROPERTY(QString icon READ icon NOTIFY iconChanged);
/// If this menu item has an associated checkbox or radiobutton.
Q_PROPERTY(qs::menu::QsMenuButtonType::Enum buttonType READ buttonType NOTIFY buttonTypeChanged);
/// The check state of the checkbox or radiobutton if applicable, as a
/// [Qt.CheckState](https://doc.qt.io/qt-6/qt.html#CheckState-enum).
Q_PROPERTY(Qt::CheckState checkState READ checkState NOTIFY checkStateChanged);
/// If this menu item has children that can be accessed through a @@QsMenuOpener$.
Q_PROPERTY(bool hasChildren READ hasChildren NOTIFY hasChildrenChanged);
QML_ELEMENT;
QML_UNCREATABLE("QsMenuEntry cannot be directly created");
public:
explicit QsMenuEntry(QObject* parent): QsMenuHandle(parent) {}
[[nodiscard]] QsMenuEntry* menu() override;
/// Display a platform menu at the given location relative to the parent window.
Q_INVOKABLE void display(QObject* parentWindow, qint32 relativeX, qint32 relativeY);
[[nodiscard]] virtual bool isSeparator() const { return false; }
[[nodiscard]] virtual bool enabled() const { return true; }
[[nodiscard]] virtual QString text() const { return ""; }
[[nodiscard]] virtual QString icon() const { return ""; }
[[nodiscard]] virtual QsMenuButtonType::Enum buttonType() const { return QsMenuButtonType::None; }
[[nodiscard]] virtual Qt::CheckState checkState() const { return Qt::Unchecked; }
[[nodiscard]] virtual bool hasChildren() const { return false; }
void ref();
void unref();
[[nodiscard]] virtual ObjectModel<QsMenuEntry>* children();
signals:
/// Send a trigger/click signal to the menu entry.
void triggered();
QSDOC_HIDE void opened();
QSDOC_HIDE void closed();
void isSeparatorChanged();
void enabledChanged();
void textChanged();
void iconChanged();
void buttonTypeChanged();
void checkStateChanged();
void hasChildrenChanged();
private:
qsizetype refcount = 0;
};
///! Provides access to children of a QsMenuEntry
class QsMenuOpener: public QObject {
Q_OBJECT;
/// The menu to retrieve children from.
Q_PROPERTY(qs::menu::QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged);
/// The children of the given menu.
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::menu::QsMenuEntry>*);
Q_PROPERTY(UntypedObjectModel* children READ children NOTIFY childrenChanged);
QML_ELEMENT;
public:
explicit QsMenuOpener(QObject* parent = nullptr): QObject(parent) {}
~QsMenuOpener() override;
Q_DISABLE_COPY_MOVE(QsMenuOpener);
[[nodiscard]] QsMenuHandle* menu() const;
void setMenu(QsMenuHandle* menu);
[[nodiscard]] ObjectModel<QsMenuEntry>* children();
signals:
void menuChanged();
void childrenChanged();
private slots:
void onMenuDestroyed();
private:
QsMenuHandle* mMenu = nullptr;
};
} // namespace qs::menu

125
src/core/qsmenuanchor.cpp Normal file
View file

@ -0,0 +1,125 @@
#include "qsmenuanchor.hpp"
#include <qapplication.h>
#include <qcoreapplication.h>
#include <qlogging.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include "platformmenu.hpp"
#include "popupanchor.hpp"
#include "qsmenu.hpp"
using qs::menu::platform::PlatformMenuEntry;
namespace qs::menu {
QsMenuAnchor::~QsMenuAnchor() { this->onClosed(); }
void QsMenuAnchor::open() {
if (qobject_cast<QApplication*>(QCoreApplication::instance()) == nullptr) {
qCritical() << "Cannot call QsMenuAnchor.open() as quickshell was not started in "
"QApplication mode.";
qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your "
"root QML file and restart quickshell.";
return;
}
if (this->mOpen) {
qCritical() << "Cannot call QsMenuAnchor.open() as it is already open.";
return;
}
if (!this->mMenu) {
qCritical() << "Cannot open QsMenuAnchor with no menu attached.";
return;
}
this->mOpen = true;
if (this->mMenu->menu()) this->onMenuChanged();
QObject::connect(this->mMenu, &QsMenuHandle::menuChanged, this, &QsMenuAnchor::onMenuChanged);
this->mMenu->refHandle();
emit this->visibleChanged();
}
void QsMenuAnchor::onMenuChanged() {
// close menu if the path changes
if (this->platformMenu || !this->mMenu->menu()) {
this->onClosed();
return;
}
this->platformMenu = new PlatformMenuEntry(this->mMenu->menu());
QObject::connect(this->platformMenu, &PlatformMenuEntry::closed, this, &QsMenuAnchor::onClosed);
auto success = this->platformMenu->display(&this->mAnchor);
if (!success) this->onClosed();
else emit this->opened();
}
void QsMenuAnchor::close() {
if (!this->mOpen) {
qCritical() << "Cannot close QsMenuAnchor as it isn't open.";
return;
}
this->onClosed();
}
void QsMenuAnchor::onClosed() {
if (!this->mOpen) return;
this->mOpen = false;
if (this->platformMenu) {
this->platformMenu->deleteLater();
this->platformMenu = nullptr;
}
if (this->mMenu) {
QObject::disconnect(
this->mMenu,
&QsMenuHandle::menuChanged,
this,
&QsMenuAnchor::onMenuChanged
);
this->mMenu->unrefHandle();
}
emit this->closed();
emit this->visibleChanged();
}
PopupAnchor* QsMenuAnchor::anchor() { return &this->mAnchor; }
QsMenuHandle* QsMenuAnchor::menu() const { return this->mMenu; }
void QsMenuAnchor::setMenu(QsMenuHandle* menu) {
if (menu == this->mMenu) return;
if (this->mMenu != nullptr) {
if (this->platformMenu != nullptr) this->platformMenu->deleteLater();
QObject::disconnect(this->mMenu, nullptr, this, nullptr);
}
this->mMenu = menu;
if (menu != nullptr) {
QObject::connect(menu, &QObject::destroyed, this, &QsMenuAnchor::onMenuDestroyed);
}
emit this->menuChanged();
}
bool QsMenuAnchor::isVisible() const { return this->mOpen; }
void QsMenuAnchor::onMenuDestroyed() {
this->mMenu = nullptr;
this->onClosed();
emit this->menuChanged();
}
} // namespace qs::menu

86
src/core/qsmenuanchor.hpp Normal file
View file

@ -0,0 +1,86 @@
#pragma once
#include <qqmlintegration.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include "platformmenu.hpp"
#include "popupanchor.hpp"
#include "qsmenu.hpp"
namespace qs::menu {
///! Display anchor for platform menus.
class QsMenuAnchor: public QObject {
Q_OBJECT;
/// The menu's anchor / positioner relative to another window. The menu will not be
/// shown until it has a valid anchor.
///
/// > [!INFO] *The following is subject to change and NOT a guarantee of future behavior.*
/// >
/// > A snapshot of the anchor at the time @@opened(s) is emitted will be
/// > used to position the menu. Additional changes to the anchor after this point
/// > will not affect the placement of the menu.
///
/// You can set properties of the anchor like so:
/// ```qml
/// QsMenuAnchor {
/// anchor.window: parentwindow
/// // or
/// anchor {
/// window: parentwindow
/// }
/// }
/// ```
Q_PROPERTY(PopupAnchor* anchor READ anchor CONSTANT);
/// The menu that should be displayed on this anchor.
///
/// See also: @@Quickshell.Services.SystemTray.SystemTrayItem.menu.
Q_PROPERTY(qs::menu::QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged);
/// If the menu is currently open and visible.
///
/// See also: @@open(), @@close().
Q_PROPERTY(bool visible READ isVisible NOTIFY visibleChanged);
QML_ELEMENT;
public:
explicit QsMenuAnchor(QObject* parent = nullptr): QObject(parent) {}
~QsMenuAnchor() override;
Q_DISABLE_COPY_MOVE(QsMenuAnchor);
/// Open the given menu on this menu Requires that @@anchor is valid.
Q_INVOKABLE void open();
/// Close the open menu.
Q_INVOKABLE void close();
[[nodiscard]] PopupAnchor* anchor();
[[nodiscard]] QsMenuHandle* menu() const;
void setMenu(QsMenuHandle* menu);
[[nodiscard]] bool isVisible() const;
signals:
/// Sent when the menu is displayed onscreen which may be after @@visible
/// becomes true.
void opened();
/// Sent when the menu is closed.
void closed();
void menuChanged();
void visibleChanged();
private slots:
void onMenuChanged();
void onMenuDestroyed();
private:
void onClosed();
PopupAnchor mAnchor {this};
QsMenuHandle* mMenu = nullptr;
bool mOpen = false;
platform::PlatformMenuEntry* platformMenu = nullptr;
};
} // namespace qs::menu

View file

@ -9,12 +9,13 @@
#include <qtmetamacros.h>
#include <qtypes.h>
/// Shape of a Region.
///! Shape of a Region.
/// See @@Region.shape.
namespace RegionShape { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum {
enum Enum : quint8 {
Rect = 0,
Ellipse = 1,
};
@ -23,11 +24,12 @@ Q_ENUM_NS(Enum);
} // namespace RegionShape
///! Intersection strategy for Regions.
/// See @@Region.intersection.
namespace Intersection { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum {
enum Enum : quint8 {
/// Combine this region, leaving a union of this and the other region. (opposite of `Subtract`)
Combine = 0,
/// Subtract this region, cutting this region out of the other. (opposite of `Combine`)
@ -44,6 +46,7 @@ Q_ENUM_NS(Enum);
} // namespace Intersection
///! A composable region used as a mask.
/// See @@QsWindow.mask.
class PendingRegion: public QObject {
Q_OBJECT;
/// Defaults to `Rect`.
@ -52,16 +55,16 @@ class PendingRegion: public QObject {
Q_PROPERTY(Intersection::Enum intersection MEMBER mIntersection NOTIFY intersectionChanged);
/// The item that determines the geometry of the region.
/// `item` overrides `x`, `y`, `width` and `height`.
/// `item` overrides @@x, @@y, @@width and @@height.
Q_PROPERTY(QQuickItem* item MEMBER mItem WRITE setItem NOTIFY itemChanged);
/// Defaults to 0. Does nothing if `item` is set.
/// Defaults to 0. Does nothing if @@item is set.
Q_PROPERTY(qint32 x MEMBER mX NOTIFY xChanged);
/// Defaults to 0. Does nothing if `item` is set.
/// Defaults to 0. Does nothing if @@item is set.
Q_PROPERTY(qint32 y MEMBER mY NOTIFY yChanged);
/// Defaults to 0. Does nothing if `item` is set.
/// Defaults to 0. Does nothing if @@item is set.
Q_PROPERTY(qint32 width MEMBER mWidth NOTIFY widthChanged);
/// Defaults to 0. Does nothing if `item` is set.
/// Defaults to 0. Does nothing if @@item is set.
Q_PROPERTY(qint32 height MEMBER mHeight NOTIFY heightChanged);
/// Regions to apply on top of this region.

View file

@ -3,6 +3,7 @@
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qtimer.h>
#include "generation.hpp"
@ -12,8 +13,19 @@ void Reloadable::componentComplete() {
if (this->engineGeneration != nullptr) {
// When called this way there is no chance a reload will have old data,
// but this will at least help prevent weird behaviors due to never getting a reload.
if (this->engineGeneration->reloadComplete) this->reload();
else {
if (this->engineGeneration->reloadComplete) {
// Delayed due to Component.onCompleted running after QQmlParserStatus::componentComplete.
QTimer::singleShot(0, this, &Reloadable::onReloadFinished);
// This only matters for preventing the above timer from UAFing the generation,
// so it isn't connected anywhere else.
QObject::connect(
this->engineGeneration,
&QObject::destroyed,
this,
&Reloadable::onGenerationDestroyed
);
} else {
QObject::connect(
this->engineGeneration,
&EngineGeneration::reloadFinished,
@ -40,6 +52,7 @@ void Reloadable::reload(QObject* oldInstance) {
}
void Reloadable::onReloadFinished() { this->reload(nullptr); }
void Reloadable::onGenerationDestroyed() { this->engineGeneration = nullptr; }
void ReloadPropagator::onReload(QObject* oldInstance) {
auto* old = qobject_cast<ReloadPropagator*>(oldInstance);
@ -86,7 +99,7 @@ void Reloadable::reloadRecursive(QObject* newObj, QObject* oldRoot) {
// pass handling to the child's onReload, which should call back into reloadRecursive,
// with its oldInstance becoming the new oldRoot.
reloadable->onReload(oldInstance);
reloadable->reload(oldInstance);
} else if (newObj != nullptr) {
Reloadable::reloadChildrenRecursive(newObj, oldRoot);
}

View file

@ -11,10 +11,7 @@ class EngineGeneration;
///! The base class of all types that can be reloaded.
/// Reloadables will attempt to take specific state from previous config revisions if possible.
/// Some examples are [ProxyWindowBase] and [PersistentProperties]
///
/// [ProxyWindowBase]: ../proxywindowbase
/// [PersistentProperties]: ../persistentproperties
/// Some examples are @@ProxyWindowBase and @@PersistentProperties
class Reloadable
: public QObject
, public QQmlParserStatus {
@ -28,7 +25,7 @@ class Reloadable
/// this object in the current revision, and facilitate smoother reloading.
///
/// Note that identifiers are scoped, and will try to do the right thing in context.
/// For example if you have a `Variants` wrapping an object with an identified element inside,
/// For example if you have a @@Variants wrapping an object with an identified element inside,
/// a scope is created at the variant level.
///
/// ```qml
@ -74,6 +71,7 @@ public:
private slots:
void onReloadFinished();
void onGenerationDestroyed();
protected:
// Called unconditionally in the reload phase, with nullptr if no source could be determined.
@ -86,10 +84,9 @@ private:
};
///! Scope that propagates reloads to child items in order.
/// Convenience type equivalent to setting `reloadableId` on properties in a
/// QtObject instance.
/// Convenience type equivalent to setting @@Reloadable.reloadableId for all children.
///
/// Note that this does not work for visible `Item`s (all widgets).
/// Note that this does not work for visible @@QtQuick.Item$s (all widgets).
///
/// ```qml
/// ShellRoot {

163
src/core/retainable.cpp Normal file
View file

@ -0,0 +1,163 @@
#include "retainable.hpp"
#include <qlogging.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qvariant.h>
RetainableHook* RetainableHook::getHook(QObject* object, bool create) {
auto v = object->property("__qs_retainable");
if (v.canConvert<RetainableHook*>()) {
return v.value<RetainableHook*>();
} else if (create) {
auto* retainable = dynamic_cast<Retainable*>(object);
if (!retainable) return nullptr;
auto* hook = new RetainableHook(object);
hook->retainableFacet = retainable;
retainable->hook = hook;
object->setProperty("__qs_retainable", QVariant::fromValue(hook));
return hook;
} else return nullptr;
}
RetainableHook* RetainableHook::qmlAttachedProperties(QObject* object) {
return RetainableHook::getHook(object, true);
}
void RetainableHook::ref() { this->refcount++; }
void RetainableHook::unref() {
this->refcount--;
if (this->refcount == 0) this->unlocked();
}
void RetainableHook::lock() {
this->explicitRefcount++;
this->ref();
}
void RetainableHook::unlock() {
if (this->explicitRefcount < 1) {
qWarning() << "Retainable object" << this->parent()
<< "unlocked more times than it was locked!";
} else {
this->explicitRefcount--;
this->unref();
}
}
void RetainableHook::forceUnlock() { this->unlocked(); }
bool RetainableHook::isRetained() const { return !this->inactive; }
void RetainableHook::unlocked() {
if (this->inactive) return;
emit this->aboutToDestroy();
this->retainableFacet->retainFinished();
}
void Retainable::retainedDestroy() {
this->retaining = true;
auto* hook = RetainableHook::getHook(dynamic_cast<QObject*>(this), false);
if (hook) {
// let all signal handlers run before acting on changes
emit hook->dropped();
hook->inactive = false;
if (hook->refcount == 0) hook->unlocked();
else emit hook->retainedChanged();
} else {
this->retainFinished();
}
}
bool Retainable::isRetained() const { return this->retaining; }
void Retainable::retainFinished() {
// a normal delete tends to cause deref errors in a listview.
dynamic_cast<QObject*>(this)->deleteLater();
}
RetainableLock::~RetainableLock() {
if (this->mEnabled && this->mObject) {
this->hook->unref();
}
}
QObject* RetainableLock::object() const { return this->mObject; }
void RetainableLock::setObject(QObject* object) {
if (object == this->mObject) return;
if (this->mObject) {
QObject::disconnect(this->mObject, nullptr, this, nullptr);
if (this->hook->isRetained()) emit this->retainedChanged();
this->hook->unref();
}
this->mObject = nullptr;
this->hook = nullptr;
if (object) {
if (auto* hook = RetainableHook::getHook(object, true)) {
this->mObject = object;
this->hook = hook;
QObject::connect(object, &QObject::destroyed, this, &RetainableLock::onObjectDestroyed);
QObject::connect(hook, &RetainableHook::dropped, this, &RetainableLock::dropped);
QObject::connect(
hook,
&RetainableHook::aboutToDestroy,
this,
&RetainableLock::aboutToDestroy
);
QObject::connect(
hook,
&RetainableHook::retainedChanged,
this,
&RetainableLock::retainedChanged
);
if (hook->isRetained()) emit this->retainedChanged();
hook->ref();
} else {
qCritical() << "Tried to set non retainable object" << object << "as the target of" << this;
}
}
emit this->objectChanged();
}
void RetainableLock::onObjectDestroyed() {
this->mObject = nullptr;
this->hook = nullptr;
emit this->objectChanged();
}
bool RetainableLock::locked() const { return this->mEnabled; }
void RetainableLock::setLocked(bool locked) {
if (locked == this->mEnabled) return;
this->mEnabled = locked;
if (this->mObject) {
if (locked) this->hook->ref();
else {
if (this->hook->isRetained()) emit this->retainedChanged();
this->hook->unref();
}
}
emit this->lockedChanged();
}
bool RetainableLock::isRetained() const { return this->mObject && this->hook->isRetained(); }

162
src/core/retainable.hpp Normal file
View file

@ -0,0 +1,162 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
class Retainable;
///! Attached object for types that can have delayed destruction.
/// Retainable works as an attached property that allows objects to be
/// kept around (retained) after they would normally be destroyed, which
/// is especially useful for things like exit transitions.
///
/// An object that is retainable will have @@Retainable as an attached property.
/// All retainable objects will say that they are retainable on their respective
/// typeinfo pages.
///
/// > [!INFO] Working directly with @@Retainable is often overly complicated and
/// > error prone. For this reason @@RetainableLock should
/// > usually be used instead.
class RetainableHook: public QObject {
Q_OBJECT;
/// If the object is currently in a retained state.
Q_PROPERTY(bool retained READ isRetained NOTIFY retainedChanged);
QML_ATTACHED(RetainableHook);
QML_NAMED_ELEMENT(Retainable);
QML_UNCREATABLE("Retainable can only be used as an attached object.");
public:
static RetainableHook* getHook(QObject* object, bool create = false);
void destroyOnRelease();
void ref();
void unref();
/// Hold a lock on the object so it cannot be destroyed.
///
/// A counter is used to ensure you can lock the object from multiple places
/// and it will not be unlocked until the same number of unlocks as locks have occurred.
///
/// > [!WARNING] It is easy to forget to unlock a locked object.
/// > Doing so will create what is effectively a memory leak.
/// >
/// > Using @@RetainableLock is recommended as it will help
/// > avoid this scenario and make misuse more obvious.
Q_INVOKABLE void lock();
/// Remove a lock on the object. See @@lock() for more information.
Q_INVOKABLE void unlock();
/// Forcibly remove all locks, destroying the object.
///
/// @@unlock() should usually be preferred.
Q_INVOKABLE void forceUnlock();
[[nodiscard]] bool isRetained() const;
static RetainableHook* qmlAttachedProperties(QObject* object);
signals:
/// This signal is sent when the object would normally be destroyed.
///
/// If all signal handlers return and no locks are in place, the object will be destroyed.
/// If at least one lock is present the object will be retained until all are removed.
void dropped();
/// This signal is sent immediately before the object is destroyed.
/// At this point destruction cannot be interrupted.
void aboutToDestroy();
void retainedChanged();
private:
explicit RetainableHook(QObject* parent): QObject(parent) {}
void unlocked();
uint refcount = 0;
// tracked separately so a warning can be given when unlock is called too many times,
// without affecting other lock sources such as RetainableLock.
uint explicitRefcount = 0;
Retainable* retainableFacet = nullptr;
bool inactive = true;
friend class Retainable;
};
class Retainable {
public:
Retainable() = default;
virtual ~Retainable() = default;
Q_DISABLE_COPY_MOVE(Retainable);
void retainedDestroy();
[[nodiscard]] bool isRetained() const;
protected:
virtual void retainFinished();
private:
RetainableHook* hook = nullptr;
bool retaining = false;
friend class RetainableHook;
};
///! A helper for easily using Retainable.
/// A RetainableLock provides extra safety and ease of use for locking
/// @@Retainable objects. A retainable object can be locked by multiple
/// locks at once, and each lock re-exposes relevant properties
/// of the retained objects.
///
/// #### Example
/// The code below will keep a retainable object alive for as long as the
/// RetainableLock exists.
///
/// ```qml
/// RetainableLock {
/// object: aRetainableObject
/// locked: true
/// }
/// ```
class RetainableLock: public QObject {
Q_OBJECT;
/// The object to lock. Must be @@Retainable.
Q_PROPERTY(QObject* object READ object WRITE setObject NOTIFY objectChanged);
/// If the object should be locked.
Q_PROPERTY(bool locked READ locked WRITE setLocked NOTIFY lockedChanged);
/// If the object is currently in a retained state.
Q_PROPERTY(bool retained READ isRetained NOTIFY retainedChanged);
QML_ELEMENT;
public:
explicit RetainableLock(QObject* parent = nullptr): QObject(parent) {}
~RetainableLock() override;
Q_DISABLE_COPY_MOVE(RetainableLock);
[[nodiscard]] QObject* object() const;
void setObject(QObject* object);
[[nodiscard]] bool locked() const;
void setLocked(bool locked);
[[nodiscard]] bool isRetained() const;
signals:
/// Rebroadcast of the object's @@Retainable.dropped(s).
void dropped();
/// Rebroadcast of the object's @@Retainable.aboutToDestroy(s).
void aboutToDestroy();
void retainedChanged();
void objectChanged();
void lockedChanged();
private slots:
void onObjectDestroyed();
private:
QObject* mObject = nullptr;
RetainableHook* hook = nullptr;
bool mEnabled = false;
};

169
src/core/ringbuf.hpp Normal file
View file

@ -0,0 +1,169 @@
#pragma once
#include <new>
#include <tuple>
#include <utility>
#include <qcontainerfwd.h>
#include <qhashfunctions.h>
#include <qtclasshelpermacros.h>
#include <qtypes.h>
// NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic)
// capacity 0 buffer cannot be inserted into, only replaced with =
// this is NOT exception safe for constructors
template <typename T>
class RingBuffer {
public:
explicit RingBuffer() = default;
explicit RingBuffer(qsizetype capacity): mCapacity(capacity) {
if (capacity > 0) this->createData();
}
~RingBuffer() { this->deleteData(); }
Q_DISABLE_COPY(RingBuffer);
explicit RingBuffer(RingBuffer&& other) noexcept { *this = std::move(other); }
RingBuffer& operator=(RingBuffer&& other) noexcept {
this->deleteData();
this->data = other.data;
this->head = other.head;
this->mSize = other.mSize;
this->mCapacity = other.mCapacity;
other.data = nullptr;
other.head = -1;
return *this;
}
// undefined if capacity is 0
template <typename... Args>
T& emplace(Args&&... args) {
auto i = (this->head + 1) % this->mCapacity;
if (this->indexIsAllocated(i)) {
this->data[i].~T();
}
auto* slot = &this->data[i];
new (&this->data[i]) T(std::forward<Args>(args)...);
this->head = i;
if (this->mSize != this->mCapacity) this->mSize = i + 1;
return *slot;
}
void clear() {
if (this->head == -1) return;
auto i = this->head;
do {
i = (i + 1) % this->mSize;
this->data[i].~T();
} while (i != this->head);
this->mSize = 0;
this->head = -1;
}
// negative indexes and >size indexes are undefined
[[nodiscard]] T& at(qsizetype i) {
auto bufferI = (this->head - i) % this->mCapacity;
if (bufferI < 0) bufferI += this->mCapacity;
return this->data[bufferI];
}
[[nodiscard]] const T& at(qsizetype i) const {
return const_cast<RingBuffer<T>*>(this)->at(i); // NOLINT
}
[[nodiscard]] qsizetype size() const { return this->mSize; }
[[nodiscard]] qsizetype capacity() const { return this->mCapacity; }
private:
void createData() {
if (this->data != nullptr) return;
this->data =
static_cast<T*>(::operator new(this->mCapacity * sizeof(T), std::align_val_t {alignof(T)}));
}
void deleteData() {
this->clear();
::operator delete(this->data, std::align_val_t {alignof(T)});
this->data = nullptr;
}
bool indexIsAllocated(qsizetype index) {
return this->mSize == this->mCapacity || index <= this->head;
}
T* data = nullptr;
qsizetype mCapacity = 0;
qsizetype head = -1;
qsizetype mSize = 0;
};
// ring buffer with the ability to look up elements by hash (single bucket)
template <typename T>
class HashBuffer {
public:
explicit HashBuffer() = default;
explicit HashBuffer(qsizetype capacity): ring(capacity) {}
~HashBuffer() = default;
Q_DISABLE_COPY(HashBuffer);
explicit HashBuffer(HashBuffer&& other) noexcept: ring(other.ring) {}
HashBuffer& operator=(HashBuffer&& other) noexcept {
this->ring = other.ring;
return *this;
}
// returns the index of the given value or -1 if missing
[[nodiscard]] qsizetype indexOf(const T& value, T** slot = nullptr) {
auto hash = qHash(value);
for (auto i = 0; i < this->size(); i++) {
auto& v = this->ring.at(i);
if (hash == v.first && value == v.second) {
if (slot != nullptr) *slot = &v.second;
return i;
}
}
return -1;
}
[[nodiscard]] qsizetype indexOf(const T& value, T const** slot = nullptr) const {
return const_cast<HashBuffer<T>*>(this)->indexOf(value, slot); // NOLINT
}
template <typename... Args>
T& emplace(Args&&... args) {
auto& entry = this->ring.emplace(
std::piecewise_construct,
std::forward_as_tuple(0),
std::forward_as_tuple(std::forward<Args>(args)...)
);
entry.first = qHash(entry.second);
return entry.second;
}
void clear() { this->ring.clear(); }
// negative indexes and >size indexes are undefined
[[nodiscard]] T& at(qsizetype i) { return this->ring.at(i).second; }
[[nodiscard]] const T& at(qsizetype i) const { return this->ring.at(i).second; }
[[nodiscard]] qsizetype size() const { return this->ring.size(); }
[[nodiscard]] qsizetype capacity() const { return this->ring.capacity(); }
private:
RingBuffer<std::pair<size_t, T>> ring;
};
// NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic)

View file

@ -8,16 +8,19 @@
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlengine.h>
#include <qquickitem.h>
#include <qtmetamacros.h>
#include <qurl.h>
#include "../window/floatingwindow.hpp"
#include "generation.hpp"
#include "qmlglobal.hpp"
#include "scan.hpp"
#include "shell.hpp"
RootWrapper::RootWrapper(QString rootPath)
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);
@ -34,18 +37,16 @@ RootWrapper::RootWrapper(QString rootPath)
RootWrapper::~RootWrapper() {
// event loop may no longer be running so deleteLater is not an option
if (this->generation != nullptr) {
delete this->generation->root;
this->generation->root = nullptr;
this->generation->shutdown();
}
delete this->generation;
}
void RootWrapper::reloadGraph(bool hard) {
auto scanner = QmlScanner();
auto rootPath = QFileInfo(this->rootPath).dir();
auto scanner = QmlScanner(rootPath);
scanner.scanQmlFile(this->rootPath);
auto* generation = new EngineGeneration(std::move(scanner));
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
generation->wrapper = this;
// todo: move into EngineGeneration
@ -60,33 +61,49 @@ void RootWrapper::reloadGraph(bool hard) {
url.setScheme("qsintercept");
auto component = QQmlComponent(generation->engine, url);
auto* obj = component.beginCreate(generation->engine->rootContext());
auto* newRoot = component.beginCreate(generation->engine->rootContext());
if (newRoot == nullptr) {
const QString error = "failed to create root component\n" + component.errorString();
qWarning().noquote() << error;
generation->destroy();
if (this->generation != nullptr && this->generation->qsgInstance != nullptr) {
emit this->generation->qsgInstance->reloadFailed(error);
}
if (obj == nullptr) {
qWarning() << component.errorString().toStdString().c_str();
qWarning() << "failed to create root component";
delete generation;
return;
}
auto* newRoot = qobject_cast<ShellRoot*>(obj);
if (newRoot == nullptr) {
qWarning() << "root component was not a Quickshell.ShellRoot";
delete obj;
delete generation;
return;
if (auto* item = qobject_cast<QQuickItem*>(newRoot)) {
auto* window = new FloatingWindowInterface();
item->setParent(window);
item->setParentItem(window->contentItem());
window->setWidth(static_cast<int>(item->width()));
window->setHeight(static_cast<int>(item->height()));
newRoot = window;
}
generation->root = newRoot;
component.completeCreate();
if (this->generation) {
QObject::disconnect(this->generation, nullptr, this, nullptr);
}
auto isReload = this->generation != nullptr;
generation->onReload(hard ? nullptr : this->generation);
if (hard) delete this->generation;
if (hard && this->generation) {
this->generation->destroy();
}
this->generation = generation;
qInfo() << "Configuration Loaded";
QObject::connect(this->generation, &QObject::destroyed, this, &RootWrapper::generationDestroyed);
QObject::connect(
this->generation,
&EngineGeneration::filesChanged,
@ -95,8 +112,14 @@ void RootWrapper::reloadGraph(bool hard) {
);
this->onWatchFilesChanged();
if (isReload && this->generation->qsgInstance != nullptr) {
emit this->generation->qsgInstance->reloadCompleted();
}
}
void RootWrapper::generationDestroyed() { this->generation = nullptr; }
void RootWrapper::onWatchFilesChanged() {
auto watchFiles = QuickshellSettings::instance()->watchFiles();
if (this->generation != nullptr) {

View file

@ -12,18 +12,20 @@ class RootWrapper: public QObject {
Q_OBJECT;
public:
explicit RootWrapper(QString rootPath);
explicit RootWrapper(QString rootPath, QString shellId);
~RootWrapper() override;
Q_DISABLE_COPY_MOVE(RootWrapper);
void reloadGraph(bool hard);
private slots:
void generationDestroyed();
void onWatchFilesChanged();
void onWatchedFilesChanged();
private:
QString rootPath;
QString shellId;
EngineGeneration* generation = nullptr;
QString originalWorkingDirectory;
};

View file

@ -103,7 +103,15 @@ bool QmlScanner::scanQmlFile(const QString& path) {
this->scanDir(currentdir.path());
for (auto& import: imports) {
auto ipath = currentdir.filePath(import);
QString ipath;
if (import.startsWith("root:")) {
auto path = import.sliced(5);
if (path.startsWith('/')) path = path.sliced(1);
ipath = this->rootPath.filePath(path);
} else {
ipath = currentdir.filePath(import);
}
auto cpath = QFileInfo(ipath).canonicalFilePath();
if (cpath.isEmpty()) {

View file

@ -1,6 +1,7 @@
#pragma once
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qhash.h>
#include <qloggingcategory.h>
#include <qvector.h>
@ -10,6 +11,8 @@ Q_DECLARE_LOGGING_CATEGORY(logQmlScanner);
// expects canonical paths
class QmlScanner {
public:
QmlScanner(const QDir& rootPath): rootPath(rootPath) {}
void scanDir(const QString& path);
// returns if the file has a singleton
bool scanQmlFile(const QString& path);
@ -17,4 +20,7 @@ public:
QVector<QString> scannedDirs;
QVector<QString> scannedFiles;
QHash<QString, QString> qmldirIntercepts;
private:
QDir rootPath;
};

133
src/core/scriptmodel.cpp Normal file
View file

@ -0,0 +1,133 @@
#include "scriptmodel.hpp"
#include <algorithm>
#include <iterator>
#include <qabstractitemmodel.h>
#include <qcontainerfwd.h>
#include <qlist.h>
#include <qnamespace.h>
#include <qtmetamacros.h>
#include <qtversionchecks.h>
#include <qtypes.h>
#include <qvariant.h>
void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
this->mValues.reserve(newValues.size());
auto iter = this->mValues.begin();
auto newIter = newValues.begin();
while (true) {
if (newIter == newValues.end()) {
if (iter == this->mValues.end()) break;
auto startIndex = static_cast<qint32>(newValues.length());
auto endIndex = static_cast<qint32>(this->mValues.length() - 1);
this->beginRemoveRows(QModelIndex(), startIndex, endIndex);
this->mValues.erase(iter, this->mValues.end());
this->endRemoveRows();
break;
} else if (iter == this->mValues.end()) {
// Prior branch ensures length is at least 1.
auto startIndex = static_cast<qint32>(this->mValues.length());
auto endIndex = static_cast<qint32>(newValues.length() - 1);
this->beginInsertRows(QModelIndex(), startIndex, endIndex);
this->mValues.append(newValues.sliced(startIndex));
this->endInsertRows();
break;
} else if (*newIter != *iter) {
auto oldIter = std::find(iter, this->mValues.end(), *newIter);
if (oldIter != this->mValues.end()) {
if (std::find(newIter, newValues.end(), *iter) == newValues.end()) {
// Remove any entries we would otherwise move around that aren't in the new list.
auto startIter = iter;
do {
++iter;
} while (iter != this->mValues.end()
&& std::find(newIter, newValues.end(), *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));
this->beginRemoveRows(QModelIndex(), startIndex, index - 1);
iter = this->mValues.erase(startIter, iter);
this->endRemoveRows();
} else {
// Advance iters to capture a whole move sequence as a single operation if possible.
auto oldStartIter = oldIter;
do {
++oldIter;
++newIter;
} while (oldIter != this->mValues.end() && newIter != newValues.end()
&& *oldIter == *newIter);
auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
auto oldStartIndex =
static_cast<qint32>(std::distance(this->mValues.begin(), oldStartIter));
auto oldIndex = static_cast<qint32>(std::distance(this->mValues.begin(), oldIter));
auto len = oldIndex - oldStartIndex;
this->beginMoveRows(QModelIndex(), oldStartIndex, oldIndex - 1, QModelIndex(), index);
// While it is possible to optimize this further, it is currently not worth the time.
for (auto i = 0; i != len; i++) {
this->mValues.move(oldStartIndex + i, index + i);
}
iter = this->mValues.begin() + (index + len);
this->endMoveRows();
}
} else {
auto startNewIter = newIter;
do {
newIter++;
} while (newIter != newValues.end()
&& std::find(iter, this->mValues.end(), *newIter) == this->mValues.end());
auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
auto newIndex = static_cast<qint32>(std::distance(newValues.begin(), newIter));
auto startNewIndex = static_cast<qint32>(std::distance(newValues.begin(), startNewIter));
auto len = newIndex - startNewIndex;
this->beginInsertRows(QModelIndex(), index, index + len - 1);
#if QT_VERSION <= QT_VERSION_CHECK(6, 8, 0)
this->mValues.resize(this->mValues.length() + len);
#else
this->mValues.resizeForOverwrite(this->mValues.length() + len);
#endif
iter = this->mValues.begin() + index; // invalidated
std::move_backward(iter, this->mValues.end() - len, this->mValues.end());
iter = std::copy(startNewIter, newIter, iter);
this->endInsertRows();
}
} else {
++iter;
++newIter;
}
}
}
void ScriptModel::setValues(const QVariantList& newValues) {
if (newValues == this->mValues) return;
this->updateValuesUnique(newValues);
emit this->valuesChanged();
}
qint32 ScriptModel::rowCount(const QModelIndex& parent) const {
if (parent != QModelIndex()) return 0;
return static_cast<qint32>(this->mValues.length());
}
QVariant ScriptModel::data(const QModelIndex& index, qint32 role) const {
if (role != Qt::UserRole) return QVariant();
return this->mValues.at(index.row());
}
QHash<int, QByteArray> ScriptModel::roleNames() const { return {{Qt::UserRole, "modelData"}}; }

74
src/core/scriptmodel.hpp Normal file
View file

@ -0,0 +1,74 @@
#pragma once
#include <qabstractitemmodel.h>
#include <qcontainerfwd.h>
#include <qproperty.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
///! QML model reflecting a javascript expression
/// ScriptModel is a QML [Data Model] that generates model operations based on changes
/// to a javascript expression attached to @@values.
///
/// ### When should I use this
/// ScriptModel should be used when you would otherwise use a javascript expression as a model,
/// [QAbstractItemModel] is accepted, and the data is likely to change over the lifetime of the program.
///
/// When directly using a javascript expression as a model, types like @@QtQuick.Repeater or @@QtQuick.ListView
/// will destroy all created delegates, and re-create the entire list. In the case of @@QtQuick.ListView this
/// will also prevent animations from working. If you wrap your expression with ScriptModel, only new items
/// will be created, and ListView animations will work as expected.
///
/// ### Example
/// ```qml
/// // Will cause all delegates to be re-created every time filterText changes.
/// @@QtQuick.Repeater {
/// model: myList.filter(entry => entry.name.startsWith(filterText))
/// delegate: // ...
/// }
///
/// // Will add and remove delegates only when required.
/// @@QtQuick.Repeater {
/// model: ScriptModel {
/// values: myList.filter(entry => entry.name.startsWith(filterText))
/// }
///
/// delegate: // ...
/// }
/// ```
class ScriptModel: public QAbstractListModel {
Q_OBJECT;
/// The list of values to reflect in the model.
/// > [!WARNING] ScriptModel currently only works with lists of *unique* values.
/// > There must not be any duplicates in the given list, or behavior of the model is undefined.
///
/// > [!TIP] @@ObjectModel$s supplied by Quickshell types will only contain unique values,
/// > and can be used like so:
/// >
/// > ```qml
/// > ScriptModel {
/// > values: DesktopEntries.applications.values.filter(...)
/// > }
/// > ```
/// >
/// > Note that we are using @@DesktopEntries.values because it will cause @@ScriptModel.values
/// > to receive an update on change.
Q_PROPERTY(QVariantList values READ values WRITE setValues NOTIFY valuesChanged);
QML_ELEMENT;
public:
[[nodiscard]] const QVariantList& values() const { return this->mValues; }
void setValues(const QVariantList& newValues);
[[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;
signals:
void valuesChanged();
private:
QVariantList mValues;
void updateValuesUnique(const QVariantList& newValues);
};

View file

@ -8,7 +8,7 @@
#include "qmlglobal.hpp"
#include "reload.hpp"
///! Root config element
///! Optional root config element, allowing some settings to be specified inline.
class ShellRoot: public ReloadPropagator {
Q_OBJECT;
Q_PROPERTY(QuickshellSettings* settings READ settings CONSTANT);

View file

@ -1,8 +1,9 @@
function (qs_test name)
add_executable(${name} ${ARGN})
target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test quickshell-core)
target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window)
add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $<TARGET_FILE:${name}>)
endfunction()
qs_test(popupwindow popupwindow.cpp)
qs_test(transformwatcher transformwatcher.cpp)
qs_test(ringbuffer ringbuf.cpp)
qs_test(scriptmodel scriptmodel.cpp)

View file

@ -1,18 +0,0 @@
#pragma once
#include <qobject.h>
#include <qtmetamacros.h>
class TestPopupWindow: public QObject {
Q_OBJECT;
private slots:
void initiallyVisible();
void reloadReparent();
void reloadUnparent();
void invisibleWithoutParent();
void moveWithParent();
void attachParentLate();
void reparentLate();
void xMigrationFix();
};

125
src/core/test/ringbuf.cpp Normal file
View file

@ -0,0 +1,125 @@
#include "ringbuf.hpp"
#include <utility>
#include <qlogging.h>
#include <qtest.h>
#include <qtestcase.h>
#include <qtypes.h>
#include "../ringbuf.hpp"
TestObject::TestObject(quint32* count): count(count) {
(*this->count)++;
qDebug() << "Created TestObject" << this << "- count is now" << *this->count;
}
TestObject::~TestObject() {
(*this->count)--;
qDebug() << "Destroyed TestObject" << this << "- count is now" << *this->count;
}
void TestRingBuffer::fill() {
quint32 counter = 0;
auto rb = RingBuffer<TestObject>(3);
QCOMPARE(rb.capacity(), 3);
qInfo() << "adding test objects";
auto* n1 = &rb.emplace(&counter);
auto* n2 = &rb.emplace(&counter);
auto* n3 = &rb.emplace(&counter);
QCOMPARE(counter, 3);
QCOMPARE(rb.size(), 3);
QCOMPARE(&rb.at(0), n3);
QCOMPARE(&rb.at(1), n2);
QCOMPARE(&rb.at(2), n1);
qInfo() << "replacing last object with new one";
auto* n4 = &rb.emplace(&counter);
QCOMPARE(counter, 3);
QCOMPARE(rb.size(), 3);
QCOMPARE(&rb.at(0), n4);
QCOMPARE(&rb.at(1), n3);
QCOMPARE(&rb.at(2), n2);
qInfo() << "replacing the rest";
auto* n5 = &rb.emplace(&counter);
auto* n6 = &rb.emplace(&counter);
QCOMPARE(counter, 3);
QCOMPARE(rb.size(), 3);
QCOMPARE(&rb.at(0), n6);
QCOMPARE(&rb.at(1), n5);
QCOMPARE(&rb.at(2), n4);
qInfo() << "clearing buffer";
rb.clear();
QCOMPARE(counter, 0);
QCOMPARE(rb.size(), 0);
}
void TestRingBuffer::clearPartial() {
quint32 counter = 0;
auto rb = RingBuffer<TestObject>(2);
qInfo() << "adding object to buffer";
auto* n1 = &rb.emplace(&counter);
QCOMPARE(counter, 1);
QCOMPARE(rb.size(), 1);
QCOMPARE(&rb.at(0), n1);
qInfo() << "clearing buffer";
rb.clear();
QCOMPARE(counter, 0);
QCOMPARE(rb.size(), 0);
}
void TestRingBuffer::move() {
quint32 counter = 0;
{
auto rb1 = RingBuffer<TestObject>(1);
qInfo() << "adding object to first buffer";
auto* n1 = &rb1.emplace(&counter);
QCOMPARE(counter, 1);
QCOMPARE(rb1.size(), 1);
QCOMPARE(&rb1.at(0), n1);
qInfo() << "move constructing new buffer";
auto rb2 = RingBuffer<TestObject>(std::move(rb1));
QCOMPARE(counter, 1);
QCOMPARE(rb2.size(), 1);
QCOMPARE(&rb2.at(0), n1);
qInfo() << "move assigning new buffer";
auto rb3 = RingBuffer<TestObject>();
rb3 = std::move(rb2);
QCOMPARE(counter, 1);
QCOMPARE(rb3.size(), 1);
QCOMPARE(&rb3.at(0), n1);
}
QCOMPARE(counter, 0);
}
void TestRingBuffer::hashLookup() {
auto hb = HashBuffer<int>(3);
qInfo() << "inserting 1,2,3 into HashBuffer";
hb.emplace(1);
hb.emplace(2);
hb.emplace(3);
qInfo() << "checking lookups";
QCOMPARE(hb.indexOf(3), 0);
QCOMPARE(hb.indexOf(2), 1);
QCOMPARE(hb.indexOf(1), 2);
qInfo() << "adding 4";
hb.emplace(4);
QCOMPARE(hb.indexOf(4), 0);
QCOMPARE(hb.indexOf(3), 1);
QCOMPARE(hb.indexOf(2), 2);
QCOMPARE(hb.indexOf(1), -1);
}
QTEST_MAIN(TestRingBuffer);

27
src/core/test/ringbuf.hpp Normal file
View file

@ -0,0 +1,27 @@
#pragma once
#include <qobject.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include <qtypes.h>
class TestObject {
public:
explicit TestObject(quint32* count);
~TestObject();
Q_DISABLE_COPY_MOVE(TestObject);
private:
quint32* count;
};
class TestRingBuffer: public QObject {
Q_OBJECT;
private slots:
static void fill();
static void clearPartial();
static void move();
static void hashLookup();
};

View file

@ -0,0 +1,179 @@
#include "scriptmodel.hpp"
#include <qabstractitemmodel.h>
#include <qabstractitemmodeltester.h>
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qlist.h>
#include <qlogging.h>
#include <qobject.h>
#include <qsignalspy.h>
#include <qstring.h>
#include <qtest.h>
#include <qtestcase.h>
#include <qtypes.h>
#include "../scriptmodel.hpp"
using OpList = QList<ModelOperation>;
bool ModelOperation::operator==(const ModelOperation& other) const {
return other.operation == this->operation && other.index == this->index
&& other.length == this->length && other.destIndex == this->destIndex;
}
QDebug& operator<<(QDebug& debug, const ModelOperation& op) {
auto saver = QDebugStateSaver(debug);
debug.nospace();
switch (op.operation) {
case ModelOperation::Insert: debug << "Insert"; break;
case ModelOperation::Remove: debug << "Remove"; break;
case ModelOperation::Move: debug << "Move"; break;
}
debug << "(i: " << op.index << ", l: " << op.length;
if (op.destIndex != -1) {
debug << ", d: " << op.destIndex;
}
debug << ')';
return debug;
}
QDebug& operator<<(QDebug& debug, const QVariantList& list) {
auto str = QString();
for (const auto& var: list) {
if (var.canConvert<QChar>()) {
str += var.value<QChar>();
} else {
qFatal() << "QVariantList debug overridden in test";
}
}
debug << str;
return debug;
}
void TestScriptModel::unique_data() {
QTest::addColumn<QString>("oldstr");
QTest::addColumn<QString>("newstr");
QTest::addColumn<OpList>("operations");
QTest::addRow("append") << "ABCD" << "ABCDEFG" << OpList({{ModelOperation::Insert, 4, 3}});
QTest::addRow("prepend") << "EFG" << "ABCDEFG" << OpList({{ModelOperation::Insert, 0, 4}});
QTest::addRow("insert") << "ABFG" << "ABCDEFG" << OpList({{ModelOperation::Insert, 2, 3}});
QTest::addRow("chop") << "ABCDEFG" << "ABCD" << OpList({{ModelOperation::Remove, 4, 3}});
QTest::addRow("slice") << "ABCDEFG" << "DEFG" << OpList({{ModelOperation::Remove, 0, 3}});
QTest::addRow("remove_mid") << "ABCDEFG" << "ABFG" << OpList({{ModelOperation::Remove, 2, 3}});
QTest::addRow("move_single") << "ABCDEFG" << "AFBCDEG"
<< OpList({{ModelOperation::Move, 5, 1, 1}});
QTest::addRow("move_range") << "ABCDEFG" << "ADEFBCG"
<< OpList({{ModelOperation::Move, 3, 3, 1}});
// beginning to end is the same operation
QTest::addRow("move_end_to_beginning")
<< "ABCDEFG" << "EFGABCD" << OpList({{ModelOperation::Move, 4, 3, 0}});
QTest::addRow("move_overlapping")
<< "ABCDEFG" << "ABDEFCG" << OpList({{ModelOperation::Move, 3, 3, 2}});
// Ensure iterators arent skipping anything at the end of operations by performing
// multiple back to back.
QTest::addRow("insert_state_ok") << "ABCDEFG" << "ABXXEFG"
<< OpList({
{ModelOperation::Insert, 2, 2}, // ABXXCDEFG
{ModelOperation::Remove, 4, 2}, // ABXXEFG
});
QTest::addRow("remove_state_ok") << "ABCDEFG" << "ABFGE"
<< OpList({
{ModelOperation::Remove, 2, 2}, // ABEFG
{ModelOperation::Move, 3, 2, 2}, // ABFGE
});
QTest::addRow("move_state_ok") << "ABCDEFG" << "ABEFXYCDG"
<< OpList({
{ModelOperation::Move, 4, 2, 2}, // ABEFCDG
{ModelOperation::Insert, 4, 2}, // ABEFXYCDG
});
}
void TestScriptModel::unique() {
QFETCH(const QString, oldstr);
QFETCH(const QString, newstr);
QFETCH(OpList, operations);
auto strToVariantList = [](const QString& str) -> QVariantList {
QVariantList list;
for (auto c: str) {
list.emplace_back(c);
}
return list;
};
auto oldlist = strToVariantList(oldstr);
auto newlist = strToVariantList(newstr);
auto model = ScriptModel();
auto modelTester = QAbstractItemModelTester(&model);
OpList actualOperations;
auto onInsert = [&](const QModelIndex& parent, int first, int last) {
QCOMPARE(parent, QModelIndex());
actualOperations << ModelOperation(ModelOperation::Insert, first, last - first + 1);
};
auto onRemove = [&](const QModelIndex& parent, int first, int last) {
QCOMPARE(parent, QModelIndex());
actualOperations << ModelOperation(ModelOperation::Remove, first, last - first + 1);
};
auto onMove = [&](const QModelIndex& sourceParent,
int sourceStart,
int sourceEnd,
const QModelIndex& destParent,
int destStart) {
QCOMPARE(sourceParent, QModelIndex());
QCOMPARE(destParent, QModelIndex());
actualOperations << ModelOperation(
ModelOperation::Move,
sourceStart,
sourceEnd - sourceStart + 1,
destStart
);
};
QObject::connect(&model, &QAbstractItemModel::rowsInserted, &model, onInsert);
QObject::connect(&model, &QAbstractItemModel::rowsRemoved, &model, onRemove);
QObject::connect(&model, &QAbstractItemModel::rowsMoved, &model, onMove);
model.setValues(oldlist);
QCOMPARE_EQ(model.values(), oldlist);
QCOMPARE_EQ(
actualOperations,
OpList({{ModelOperation::Insert, 0, static_cast<qint32>(oldlist.length())}})
);
actualOperations.clear();
model.setValues(newlist);
QCOMPARE_EQ(model.values(), newlist);
QCOMPARE_EQ(actualOperations, operations);
}
QTEST_MAIN(TestScriptModel);

View file

@ -0,0 +1,37 @@
#pragma once
#include <qdebug.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qtypes.h>
struct ModelOperation {
enum Enum : quint8 {
Insert,
Remove,
Move,
};
ModelOperation(Enum operation, qint32 index, qint32 length, qint32 destIndex = -1)
: operation(operation)
, index(index)
, length(length)
, destIndex(destIndex) {}
Enum operation;
qint32 index = 0;
qint32 length = 0;
qint32 destIndex = -1;
[[nodiscard]] bool operator==(const ModelOperation& other) const;
};
QDebug& operator<<(QDebug& debug, const ModelOperation& op);
class TestScriptModel: public QObject {
Q_OBJECT;
private slots:
static void unique_data(); // NOLINT
static void unique();
};

View file

@ -7,6 +7,7 @@
#include <qobject.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qtmetamacros.h>
void TransformWatcher::resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent) {
if (a == nullptr || b == nullptr) return;
@ -82,7 +83,10 @@ void TransformWatcher::linkItem(QQuickItem* item) const {
QObject::connect(item, &QQuickItem::parentChanged, this, &TransformWatcher::recalcChains);
QObject::connect(item, &QQuickItem::windowChanged, this, &TransformWatcher::recalcChains);
QObject::connect(item, &QObject::destroyed, this, &TransformWatcher::recalcChains);
if (item != this->mA && item != this->mB) {
QObject::connect(item, &QObject::destroyed, this, &TransformWatcher::itemDestroyed);
}
}
void TransformWatcher::linkChains() {
@ -103,6 +107,18 @@ void TransformWatcher::unlinkChains() {
for (auto* item: this->childChain) {
QObject::disconnect(item, nullptr, this, nullptr);
}
// relink a and b destruction notifications
if (this->mA != nullptr) {
QObject::connect(this->mA, &QObject::destroyed, this, &TransformWatcher::aDestroyed);
}
if (this->mB != nullptr) {
QObject::connect(this->mB, &QObject::destroyed, this, &TransformWatcher::bDestroyed);
}
this->parentChain.clear();
this->childChain.clear();
}
void TransformWatcher::recalcChains() {
@ -111,26 +127,57 @@ void TransformWatcher::recalcChains() {
this->linkChains();
}
void TransformWatcher::itemDestroyed() {
auto destroyed =
this->parentChain.removeOne(this->sender()) || this->childChain.removeOne(this->sender());
if (destroyed) this->recalcChains();
}
QQuickItem* TransformWatcher::a() const { return this->mA; }
void TransformWatcher::setA(QQuickItem* a) {
if (this->mA == a) return;
if (this->mA != nullptr) QObject::disconnect(this->mA, nullptr, this, nullptr);
this->mA = a;
if (this->mA != nullptr) {
QObject::connect(this->mA, &QObject::destroyed, this, &TransformWatcher::aDestroyed);
}
this->recalcChains();
}
void TransformWatcher::aDestroyed() {
this->mA = nullptr;
this->unlinkChains();
emit this->aChanged();
}
QQuickItem* TransformWatcher::b() const { return this->mB; }
void TransformWatcher::setB(QQuickItem* b) {
if (this->mB == b) return;
if (this->mB != nullptr) QObject::disconnect(this->mB, nullptr, this, nullptr);
this->mB = b;
if (this->mB != nullptr) {
QObject::connect(this->mB, &QObject::destroyed, this, &TransformWatcher::bDestroyed);
}
this->recalcChains();
}
void TransformWatcher::bDestroyed() {
this->mB = nullptr;
this->unlinkChains();
emit this->bChanged();
}
QQuickItem* TransformWatcher::commonParent() const { return this->mCommonParent; }
void TransformWatcher::setCommonParent(QQuickItem* commonParent) {
if (this->mCommonParent == commonParent) return;
this->mCommonParent = commonParent;
this->resolveChains();
this->recalcChains();
}

View file

@ -13,7 +13,7 @@ class TestTransformWatcher;
///! Monitor of all geometry changes between two objects.
/// The TransformWatcher monitors all properties that affect the geometry
/// of two `Item`s relative to eachother.
/// of two @@QtQuick.Item$s relative to eachother.
///
/// > [!INFO] The algorithm responsible for determining the relationship
/// > between `a` and `b` is biased towards `a` being a parent of `b`,
@ -60,6 +60,9 @@ signals:
private slots:
void recalcChains();
void itemDestroyed();
void aDestroyed();
void bDestroyed();
private:
void resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent);

Some files were not shown because too many files have changed in this diff Show more