Compare commits

...

120 commits

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

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

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

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

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

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

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

Using QList breaks the default property usage.
2025-07-14 02:56:34 -07:00
281 changed files with 13064 additions and 2448 deletions

View file

@ -1,6 +1,6 @@
AlignArrayOfStructures: None AlignArrayOfStructures: None
AlignAfterOpenBracket: BlockIndent AlignAfterOpenBracket: BlockIndent
AllowShortBlocksOnASingleLine: Always AllowShortBlocksOnASingleLine: Empty
AllowShortCaseLabelsOnASingleLine: true AllowShortCaseLabelsOnASingleLine: true
AllowShortEnumsOnASingleLine: true AllowShortEnumsOnASingleLine: true
AllowShortFunctionsOnASingleLine: All AllowShortFunctionsOnASingleLine: All

View file

@ -20,6 +20,7 @@ Checks: >
-cppcoreguidelines-avoid-do-while, -cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-pro-type-reinterpret-cast, -cppcoreguidelines-pro-type-reinterpret-cast,
-cppcoreguidelines-pro-type-vararg, -cppcoreguidelines-pro-type-vararg,
-cppcoreguidelines-use-enum-class,
google-global-names-in-headers, google-global-names-in-headers,
google-readability-casting, google-readability-casting,
google-runtime-int, google-runtime-int,
@ -63,6 +64,8 @@ CheckOptions:
readability-identifier-naming.ParameterCase: camelBack readability-identifier-naming.ParameterCase: camelBack
readability-identifier-naming.VariableCase: camelBack readability-identifier-naming.VariableCase: camelBack
misc-const-correctness.WarnPointersAsPointers: false
# does not appear to work # does not appear to work
readability-operators-representation.BinaryOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^=' readability-operators-representation.BinaryOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^='
readability-operators-representation.OverloadedOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^=' readability-operators-representation.OverloadedOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^='

View file

@ -1,4 +1,4 @@
name: Crash Report name: Crash Report (v1)
description: Quickshell has crashed description: Quickshell has crashed
labels: ["bug", "crash"] labels: ["bug", "crash"]
body: body:

49
.github/ISSUE_TEMPLATE/crash2.yml vendored Normal file
View file

@ -0,0 +1,49 @@
name: Crash Report (v2)
description: Quickshell has crashed
labels: ["bug", "crash"]
body:
- type: textarea
id: userinfo
attributes:
label: What caused the crash
description: |
Any information likely to help debug the crash. What were you doing when the crash occurred,
what changes did you make, can you get it to happen again?
- type: textarea
id: report
attributes:
label: Report file
description: Attach `report.txt` here.
validations:
required: true
- type: textarea
id: logs
attributes:
label: Log file
description: |
Attach `log.qslog.log` here. If it is too big to upload, compress it.
You can preview the log if you'd like using `quickshell read-log <path-to-log>`.
validations:
required: true
- type: textarea
id: config
attributes:
label: Configuration
description: |
Attach your configuration here, preferrably in full (not just one file).
Compress it into a zip, tar, etc.
This will help us reproduce the crash ourselves.
- type: textarea
id: bt
attributes:
label: Backtrace
description: |
GDB usually produces better stacktraces than quickshell can. Consider attaching a gdb backtrace
following the instructions below.
1. Run `coredumpctl debug <pid>` where `pid` is the number shown after "Crashed process ID"
in the crash reporter.
2. Once it loads, type `bt -full` (then enter)
3. Copy the output and attach it as a file or in a spoiler.

View file

@ -6,17 +6,23 @@ jobs:
name: Nix name: Nix
strategy: strategy:
matrix: matrix:
qtver: [qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0] qtver: [qt6.10.1, qt6.10.0, qt6.9.2, qt6.9.1, qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0]
compiler: [clang, gcc] compiler: [clang, gcc]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# Use cachix action over detsys for testing with act. # Use cachix action over detsys for testing with act.
# - uses: cachix/install-nix-action@v27 # - uses: cachix/install-nix-action@v27
- uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
with:
use-flakehub: false
- name: Download Dependencies - name: Download Dependencies
run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).inputDerivation' run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).unwrapped.inputDerivation'
- name: Build - name: Build
run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }' run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }'
@ -44,13 +50,16 @@ jobs:
wayland-protocols \ wayland-protocols \
wayland \ wayland \
libdrm \ libdrm \
vulkan-headers \
libxcb \ libxcb \
libpipewire \ libpipewire \
cli11 \ cli11 \
jemalloc polkit \
jemalloc \
libunwind \
git # for cpptrace clone
- name: Build - name: Build
# breakpad is annoying to build in ci due to makepkg not running as root
run: | run: |
cmake -GNinja -B build -DCRASH_REPORTER=OFF cmake -GNinja -B build -DVENDOR_CPPTRACE=ON
cmake --build build cmake --build build

View file

@ -5,11 +5,17 @@ jobs:
lint: lint:
name: Lint name: Lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# Use cachix action over detsys for testing with act. # Use cachix action over detsys for testing with act.
# - uses: cachix/install-nix-action@v27 # - uses: cachix/install-nix-action@v27
- uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
with:
use-flakehub: false
- uses: nicknovitski/nix-develop@v1 - uses: nicknovitski/nix-develop@v1
- name: Check formatting - name: Check formatting

View file

@ -15,15 +15,7 @@ Please make this descriptive enough to identify your specific package, for examp
- `Nixpkgs` - `Nixpkgs`
- `Fedora COPR (errornointernet/quickshell)` - `Fedora COPR (errornointernet/quickshell)`
`-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=YES/NO` Please leave at least symbol names attached to the binary for debugging purposes.
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 ### QML Module dir
Currently all QML modules are statically linked to quickshell, but this is where Currently all QML modules are statically linked to quickshell, but this is where
@ -55,7 +47,7 @@ On some distros, private Qt headers are in separate packages which you may have
We currently require private headers for the following libraries: We currently require private headers for the following libraries:
- `qt6declarative` - `qt6declarative`
- `qt6wayland` - `qt6wayland` (for Qt versions prior to 6.10)
We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and
svg icons will not work, including system ones. svg icons will not work, including system ones.
@ -64,14 +56,18 @@ At least Qt 6.6 is required.
All features are enabled by default and some have their own dependencies. All features are enabled by default and some have their own dependencies.
### Crash Reporter ### Crash Handler
The crash reporter catches crashes, restarts quickshell when it crashes, The crash reporter catches crashes, restarts Quickshell when it crashes,
and collects useful crash information in one place. Leaving this enabled will and collects useful crash information in one place. Leaving this enabled will
enable us to fix bugs far more easily. enable us to fix bugs far more easily.
To disable: `-DCRASH_REPORTER=OFF` To disable: `-DCRASH_HANDLER=OFF`
Dependencies: `google-breakpad` (static library) Dependencies: `cpptrace`
Note: `-DVENDOR_CPPTRACE=ON` can be set to vendor cpptrace using FetchContent.
When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the package manager or fetched with FetchContent.
### Jemalloc ### Jemalloc
We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused
@ -104,7 +100,7 @@ Currently supported Qt versions: `6.6`, `6.7`.
To disable: `-DWAYLAND=OFF` To disable: `-DWAYLAND=OFF`
Dependencies: Dependencies:
- `qt6wayland` - `qt6wayland` (for Qt versions prior to 6.10)
- `wayland` (libwayland-client) - `wayland` (libwayland-client)
- `wayland-scanner` (build time) - `wayland-scanner` (build time)
- `wayland-protocols` (static library) - `wayland-protocols` (static library)
@ -146,6 +142,7 @@ To disable: `-DSCREENCOPY=OFF`
Dependencies: Dependencies:
- `libdrm` - `libdrm`
- `libgbm` - `libgbm`
- `vulkan-headers` (build-time)
Specific protocols can also be disabled: Specific protocols can also be disabled:
- `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1] - `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1]
@ -192,6 +189,13 @@ To disable: `-DSERVICE_PAM=OFF`
Dependencies: `pam` Dependencies: `pam`
### Polkit
This feature enables creating Polkit agents that can prompt user for authentication.
To disable: `-DSERVICE_POLKIT=OFF`
Dependencies: `polkit`, `glib`
### Hyprland ### Hyprland
This feature enables hyprland specific integrations. It requires wayland support This feature enables hyprland specific integrations. It requires wayland support
but has no extra dependencies. but has no extra dependencies.

View file

@ -1,5 +1,7 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
project(quickshell VERSION "0.1.0" LANGUAGES CXX C) project(quickshell VERSION "0.2.1" LANGUAGES CXX C)
set(UNRELEASED_FEATURES)
set(QT_MIN_VERSION "6.6.0") set(QT_MIN_VERSION "6.6.0")
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
@ -38,14 +40,17 @@ string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}")
message(STATUS "Quickshell configuration") message(STATUS "Quickshell configuration")
message(STATUS " Distributor: ${DISTRIBUTOR}") message(STATUS " Distributor: ${DISTRIBUTOR}")
boption(DISTRIBUTOR_DEBUGINFO_AVAILABLE "Distributor provided debuginfo" NO)
boption(NO_PCH "Disable precompild headers (dev)" OFF) boption(NO_PCH "Disable precompild headers (dev)" OFF)
boption(BUILD_TESTING "Build tests (dev)" OFF) boption(BUILD_TESTING "Build tests (dev)" OFF)
boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang
boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN}) boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN})
boption(CRASH_REPORTER "Crash Handling" ON) if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
boption(USE_JEMALLOC "Use jemalloc" ON) boption(USE_JEMALLOC "Use jemalloc" OFF)
else()
boption(USE_JEMALLOC "Use jemalloc" ON)
endif()
boption(CRASH_HANDLER "Crash Handling" ON)
boption(SOCKETS "Unix Sockets" ON) boption(SOCKETS "Unix Sockets" ON)
boption(WAYLAND "Wayland" ON) boption(WAYLAND "Wayland" ON)
boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND) boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND)
@ -67,10 +72,12 @@ boption(SERVICE_STATUS_NOTIFIER "System Tray" ON)
boption(SERVICE_PIPEWIRE "PipeWire" ON) boption(SERVICE_PIPEWIRE "PipeWire" ON)
boption(SERVICE_MPRIS "Mpris" ON) boption(SERVICE_MPRIS "Mpris" ON)
boption(SERVICE_PAM "Pam" ON) boption(SERVICE_PAM "Pam" ON)
boption(SERVICE_POLKIT "Polkit" ON)
boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_GREETD "Greetd" ON)
boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_UPOWER "UPower" ON)
boption(SERVICE_NOTIFICATIONS "Notifications" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON)
boption(BLUETOOTH "Bluetooth" ON) boption(BLUETOOTH "Bluetooth" ON)
boption(NETWORK "Network" ON)
include(cmake/install-qml-module.cmake) include(cmake/install-qml-module.cmake)
include(cmake/util.cmake) include(cmake/util.cmake)
@ -100,6 +107,7 @@ if (NOT CMAKE_BUILD_TYPE)
endif() endif()
set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools) set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools)
set(QT_PRIVDEPS QuickPrivate)
include(cmake/pch.cmake) include(cmake/pch.cmake)
@ -115,9 +123,10 @@ endif()
if (WAYLAND) if (WAYLAND)
list(APPEND QT_FPDEPS WaylandClient) list(APPEND QT_FPDEPS WaylandClient)
list(APPEND QT_PRIVDEPS WaylandClientPrivate)
endif() endif()
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH) if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH OR NETWORK)
set(DBUS ON) set(DBUS ON)
endif() endif()
@ -127,6 +136,13 @@ endif()
find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS}) find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS})
# In Qt 6.10, private dependencies are required to be explicit,
# but they could not be explicitly depended on prior to 6.9.
if (Qt6_VERSION VERSION_GREATER_EQUAL "6.9.0")
set(QT_NO_PRIVATE_MODULE_WARNING ON)
find_package(Qt6 REQUIRED COMPONENTS ${QT_PRIVDEPS})
endif()
set(CMAKE_AUTOUIC OFF) set(CMAKE_AUTOUIC OFF)
qt_standard_project_setup(REQUIRES 6.6) qt_standard_project_setup(REQUIRES 6.6)
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules) set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules)

View file

@ -12,6 +12,9 @@ lint-ci:
lint-changed: lint-changed:
git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
lint-staged:
git diff --staged --name-only --diff-filter=d HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }}
configure target='debug' *FLAGS='': configure target='debug' *FLAGS='':
cmake -GNinja -B {{builddir}} \ cmake -GNinja -B {{builddir}} \
-DCMAKE_BUILD_TYPE={{ if target == "debug" { "Debug" } else { "RelWithDebInfo" } }} \ -DCMAKE_BUILD_TYPE={{ if target == "debug" { "Debug" } else { "RelWithDebInfo" } }} \

60
changelog/next.md Normal file
View file

@ -0,0 +1,60 @@
## Breaking Changes
### Config paths are no longer canonicalized
This fixes nix configs changing shell-ids on rebuild as the shell id is now derived from
the symlink path. Configs with a symlink in their path will have a different shell id.
Shell ids are used to derive the default config / state / cache folders, so those files
will need to be manually moved if using a config behind a symlinked path without an explicitly
set shell id.
## New Features
- Added support for creating Polkit agents.
- Added support for creating wayland idle inhibitors.
- Added support for wayland idle timeouts.
- Added support for inhibiting wayland compositor shortcuts for focused windows.
- Added the ability to override Quickshell.cacheDir with a custom path.
- Added minimized, maximized, and fullscreen properties to FloatingWindow.
- Added the ability to handle move and resize events to FloatingWindow.
- Pipewire service now reconnects if pipewire dies or a protocol error occurs.
- Added pipewire audio peak detection.
- Added initial support for network management.
- Added support for grabbing focus from popup windows.
- Added support for IPC signal listeners.
- Added Quickshell version checking and version gated preprocessing.
- Added a way to detect if an icon is from the system icon theme or not.
- Added vulkan support to screencopy.
## Other Changes
- FreeBSD is now partially supported.
- IPC operations filter available instances to the current display connection by default.
- PwNodeLinkTracker ignores sound level monitoring programs.
- Replaced breakpad with cpptrace.
## Bug Fixes
- Fixed volume control breaking with pipewire pro audio mode.
- Fixed volume control breaking with bluez streams and potentially others.
- Fixed volume control breaking for devices without route definitions.
- Fixed escape sequence handling in desktop entries.
- Fixed volumes not initializing if a pipewire device was already loaded before its node.
- Fixed hyprland active toplevel not resetting after window closes.
- Fixed hyprland ipc window names and titles being reversed.
- Fixed missing signals for system tray item title and description updates.
- Fixed asynchronous loaders not working after reload.
- Fixed asynchronous loaders not working before window creation.
- Fixed memory leak in IPC handlers.
- Fixed ClippingRectangle related crashes.
- Fixed crashes when monitors are unplugged.
- Fixed crashes when default pipewire devices are lost.
- Desktop action order is now preserved.
## Packaging Changes
- `glib` and `polkit` have been added as dependencies when compiling with polkit agent support.
- `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support).
- `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore.
- `DISTRIBUTOR_DEBUGINFO_AVAILABLE` was removed as it is no longer important without breakpad.

1
changelog/v0.1.0.md Normal file
View file

@ -0,0 +1 @@
Initial release

84
changelog/v0.2.0.md Normal file
View file

@ -0,0 +1,84 @@
## Breaking Changes
- Files outside of the shell directory can no longer be referenced with relative paths, e.g. '../../foo.png'.
- PanelWindow's Automatic exclusion mode now adds an exclusion zone for panels with a single anchor.
- `QT_QUICK_CONTROLS_STYLE` and `QT_STYLE_OVERRIDE` are ignored unless `//@ pragma RespectSystemStyle` is set.
## New Features
### Root-Relative Imports
Quickshell 0.2 comes with a new method to import QML modules which is supported by QMLLS.
This replaces "root:/" imports for QML modules.
The new syntax is `import qs.path.to.module`, where `path/to/module` is the path to
a module/subdirectory relative to the config root (`qs`).
### Better LSP support
LSP support for Singletons and Root-Relative imports can be enabled by creating a file named
`.qmlls.ini` in the shell root directory. Quickshell will detect this file and automatically
populate it with an LSP configuration. This file should be gitignored in your configuration,
as it is system dependent.
The generated configuration also includes QML import paths available to Quickshell, meaning
QMLLS no longer requires the `-E` flag.
### Bluetooth Module
Quickshell can now manage your bluetooth devices through BlueZ. While authenticated pairing
has not landed in 0.2, support for connecting and disconnecting devices, basic device information,
and non-authenticated pairing are now supported.
### Other Features
- Added `HyprlandToplevel` and related toplevel/window management APIs in the Hyprland module.
- Added `Quickshell.execDetached()`, which spawns a detached process without a `Process` object.
- Added `Process.exec()` for easier reconfiguration of process commands when starting them.
- Added `FloatingWindow.title`, which allows changing the title of a floating window.
- Added `signal QsWindow.closed()`, fired when a window is closed externally.
- Added support for inline replies in notifications, when supported by applications.
- Added `DesktopEntry.startupWmClass` and `DesktopEntry.heuristicLookup()` to better identify toplevels.
- Added `DesktopEntry.command` which can be run as an alternative to `DesktopEntry.execute()`.
- Added `//@ pragma Internal`, which makes a QML component impossible to import outside of its module.
- Added dead instance selection for some subcommands, such as `qs log` and `qs list`.
## Other Changes
- `Quickshell.shellRoot` has been renamed to `Quickshell.shellDir`.
- PanelWindow margins opposite the window's anchorpoint are now added to exclusion zone.
- stdout/stderr or detached processes and executed desktop entries are now hidden by default.
- Various warnings caused by other applications Quickshell communicates with over D-BUS have been hidden in logs.
- Quickshell's new logo is now shown in any floating windows.
## Bug Fixes
- Fixed pipewire device volume and mute states not updating before the device has been used.
- Fixed a crash when changing the volume of any pipewire device on a sound card another removed device was using.
- Fixed a crash when accessing a removed previous default pipewire node from the default sink/source changed signals.
- Fixed session locks crashing if all monitors are disconnected.
- Fixed session locks crashing if unsupported by the compositor.
- Fixed a crash when creating a session lock and destroying it before acknowledged by the compositor.
- Fixed window input masks not updating after a reload.
- Fixed PanelWindows being unconfigurable unless `screen` was set under X11.
- Fixed a crash when anchoring a popup to a zero sized `Item`.
- Fixed `FileView` crashing if `watchChanges` was used.
- Fixed `SocketServer` sockets disappearing after a reload.
- Fixed `ScreencopyView` having incorrect rotation when displaying a rotated monitor.
- Fixed `MarginWrapperManager` breaking pixel alignment of child items when centering.
- Fixed `IpcHandler`, `NotificationServer` and `GlobalShortcut` not activating with certain QML structures.
- Fixed tracking of QML incubator destruction and deregistration, which occasionally caused crashes.
- Fixed FloatingWindows being constrained to the smallest window manager supported size unless max size was set.
- Fixed `MprisPlayer.lengthSupported` not updating reactively.
- Fixed normal tray icon being ignored when status is `NeedsAttention` and no attention icon is provided.
- Fixed `HyprlandWorkspace.activate()` sending invalid commands to Hyprland for named or special workspaces.
- Fixed file watcher occasionally breaking when using VSCode to edit QML files.
- Fixed crashes when screencopy buffer creation fails.
- Fixed a crash when wayland layer surfaces are recreated for the same window.
- Fixed the `QsWindow` attached object not working when using `WlrLayershell` directly.
- Fixed a crash when attempting to create a window without available VRAM.
- Fixed OOM crash when failing to write to detailed log file.
- Prevented distro logging configurations for Qt from interfering with Quickshell commands.
- Removed the "QProcess destroyed for running process" warning when destroying `Process` objects.
- Fixed `ColorQuantizer` printing a pointer to an error message instead of an error message.
- Fixed notification pixmap rowstride warning showing for correct rowstrides.

17
changelog/v0.2.1.md Normal file
View file

@ -0,0 +1,17 @@
## New Features
- Changes to desktop entries are now tracked in real time.
## Other Changes
- Added support for Qt 6.10
## Bug Fixes
- Fixed volumes getting stuck on change for pipewire devices with few volume steps.
- Fixed a crash when running out of disk space to write log files.
- Fixed a rare crash when disconnecting a monitor.
- Fixed build issues preventing cross compilation from working.
- Fixed dekstop entries with lower priority than a hidden entry not being hidden.
- Fixed desktop entry keys with mismatched modifier or country not being discarded.
- Fixed greetd hanging when authenticating with a fingerprint.

View file

@ -2,7 +2,10 @@
qtver, qtver,
compiler, compiler,
}: let }: let
nixpkgs = (import ./nix-checkouts.nix).${builtins.replaceStrings ["."] ["_"] qtver}; checkouts = import ./nix-checkouts.nix;
nixpkgs = checkouts.${builtins.replaceStrings ["."] ["_"] qtver};
compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler}; compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler};
pkg = (nixpkgs.callPackage ../default.nix {}).override compilerOverride; pkg = (nixpkgs.callPackage ../default.nix {}).override (compilerOverride // {
wayland-protocols = checkouts.latest.wayland-protocols;
});
in pkg in pkg

View file

@ -7,9 +7,28 @@ let
url = "https://github.com/nixos/nixpkgs/archive/${commit}.tar.gz"; url = "https://github.com/nixos/nixpkgs/archive/${commit}.tar.gz";
inherit sha256; inherit sha256;
}) {}; }) {};
in { in rec {
# For old qt versions, grab the commit before the version bump that has all the patches latest = qt6_10_0;
# instead of the bumped version.
qt6_10_1 = byCommit {
commit = "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38";
sha256 = "0fvbizl7j5rv2rf8j76yw0xb3d9l06hahkjys2a7k1yraznvnafm";
};
qt6_10_0 = byCommit {
commit = "c5ae371f1a6a7fd27823bc500d9390b38c05fa55";
sha256 = "18g0f8cb9m8mxnz9cf48sks0hib79b282iajl2nysyszph993yp0";
};
qt6_9_2 = byCommit {
commit = "e9f00bd893984bc8ce46c895c3bf7cac95331127";
sha256 = "0s2mhbrgzxlgkg2yxb0q0hpk8lby1a7w67dxvfmaz4gsmc0bnvfj";
};
qt6_9_1 = byCommit {
commit = "4c202d26483c5ccf3cb95e0053163facde9f047e";
sha256 = "06l2w4bcgfw7dfanpzpjcf25ydf84in240yplqsss82qx405y9di";
};
qt6_9_0 = byCommit { qt6_9_0 = byCommit {
commit = "546c545bd0594809a28ab7e869b5f80dd7243ef6"; commit = "546c545bd0594809a28ab7e869b5f80dd7243ef6";

View file

@ -2,6 +2,6 @@
clangStdenv, clangStdenv,
gccStdenv, gccStdenv,
}: { }: {
clang = { buildStdenv = clangStdenv; }; clang = { stdenv = clangStdenv; };
gcc = { buildStdenv = gccStdenv; }; gcc = { stdenv = gccStdenv; };
} }

View file

@ -2,25 +2,31 @@
lib, lib,
nix-gitignore, nix-gitignore,
pkgs, pkgs,
stdenv,
keepDebugInfo, keepDebugInfo,
buildStdenv ? pkgs.clangStdenv,
pkg-config, pkg-config,
cmake, cmake,
ninja, ninja,
spirv-tools, spirv-tools,
qt6, qt6,
breakpad, cpptrace ? null,
libunwind,
libdwarf,
jemalloc, jemalloc,
cli11, cli11,
wayland, wayland,
wayland-protocols, wayland-protocols,
wayland-scanner, wayland-scanner,
xorg, xorg,
libxcb ? xorg.libxcb,
libdrm, libdrm,
libgbm ? null, libgbm ? null,
vulkan-headers,
pipewire, pipewire,
pam, pam,
polkit,
glib,
gitRev ? (let gitRev ? (let
headExists = builtins.pathExists ./.git/HEAD; headExists = builtins.pathExists ./.git/HEAD;
@ -43,64 +49,106 @@
withPam ? true, withPam ? true,
withHyprland ? true, withHyprland ? true,
withI3 ? true, withI3 ? true,
}: buildStdenv.mkDerivation { withPolkit ? true,
pname = "quickshell${lib.optionalString debug "-debug"}"; withNetworkManager ? true,
version = "0.1.0"; }: let
src = nix-gitignore.gitignoreSource [] ./.; withCrashHandler = withCrashReporter && cpptrace != null && lib.strings.compareVersions cpptrace.version "0.7.2" >= 0;
nativeBuildInputs = [ unwrapped = stdenv.mkDerivation {
cmake pname = "quickshell${lib.optionalString debug "-debug"}";
ninja version = "0.2.1";
qt6.qtshadertools src = nix-gitignore.gitignoreSource "/default.nix\n" ./.;
spirv-tools
qt6.wrapQtAppsHook
pkg-config
]
++ lib.optional withWayland wayland-scanner;
buildInputs = [ dontWrapQtApps = true; # see wrappers
qt6.qtbase
qt6.qtdeclarative
cli11
]
++ lib.optional withQtSvg qt6.qtsvg
++ lib.optional withCrashReporter breakpad
++ lib.optional withJemalloc jemalloc
++ lib.optionals withWayland [ qt6.qtwayland wayland wayland-protocols ]
++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ]
++ lib.optional withX11 xorg.libxcb
++ lib.optional withPam pam
++ lib.optional withPipewire pipewire;
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; nativeBuildInputs = [
cmake
ninja
spirv-tools
pkg-config
]
++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
++ lib.optionals withWayland [
qt6.qtwayland # qtwaylandscanner required at build time
wayland-scanner
];
cmakeFlags = [ buildInputs = [
(lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake") qt6.qtbase
(lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix) qt6.qtdeclarative
(lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true) cli11
(lib.cmakeFeature "GIT_REVISION" gitRev) ]
(lib.cmakeBool "CRASH_REPORTER" withCrashReporter) ++ lib.optional withQtSvg qt6.qtsvg
(lib.cmakeBool "USE_JEMALLOC" withJemalloc) ++ lib.optional withCrashHandler (cpptrace.overrideAttrs (prev: {
(lib.cmakeBool "WAYLAND" withWayland) cmakeFlags = prev.cmakeFlags ++ [
(lib.cmakeBool "SCREENCOPY" (libgbm != null)) "-DCPPTRACE_UNWIND_WITH_LIBUNWIND=TRUE"
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) ];
(lib.cmakeBool "SERVICE_PAM" withPam) buildInputs = prev.buildInputs ++ [ libunwind ];
(lib.cmakeBool "HYPRLAND" withHyprland) }))
(lib.cmakeBool "I3" withI3) ++ lib.optional withJemalloc jemalloc
]; ++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland
++ lib.optionals withWayland [ wayland wayland-protocols ]
++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm vulkan-headers ]
++ lib.optional withX11 libxcb
++ lib.optional withPam pam
++ lib.optional withPipewire pipewire
++ lib.optionals withPolkit [ polkit glib ];
# How to get debuginfo in gdb from a release build: cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
# 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; { cmakeFlags = [
homepage = "https://quickshell.outfoxxed.me"; (lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake")
description = "Flexbile QtQuick based desktop shell toolkit"; (lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
license = licenses.lgpl3Only; (lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true)
platforms = platforms.linux; (lib.cmakeFeature "GIT_REVISION" gitRev)
mainProgram = "quickshell"; (lib.cmakeBool "CRASH_HANDLER" withCrashHandler)
(lib.cmakeBool "USE_JEMALLOC" withJemalloc)
(lib.cmakeBool "WAYLAND" withWayland)
(lib.cmakeBool "SCREENCOPY" (libgbm != null))
(lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire)
(lib.cmakeBool "SERVICE_PAM" withPam)
(lib.cmakeBool "SERVICE_NETWORKMANAGER" withNetworkManager)
(lib.cmakeBool "SERVICE_POLKIT" withPolkit)
(lib.cmakeBool "HYPRLAND" withHyprland)
(lib.cmakeBool "I3" withI3)
];
# How to get debuginfo in gdb from a release build:
# 1. build `quickshell.debug`
# 2. set NIX_DEBUG_INFO_DIRS="<quickshell.debug store path>/lib/debug"
# 3. launch gdb / coredumpctl and debuginfo will work
separateDebugInfo = !debug;
dontStrip = debug;
meta = with lib; {
homepage = "https://quickshell.org";
description = "Flexbile QtQuick based desktop shell toolkit";
license = licenses.lgpl3Only;
platforms = platforms.linux;
mainProgram = "quickshell";
};
}; };
}
wrapper = unwrapped.stdenv.mkDerivation {
inherit (unwrapped) version meta buildInputs;
pname = "${unwrapped.pname}-wrapped";
nativeBuildInputs = unwrapped.nativeBuildInputs ++ [ qt6.wrapQtAppsHook ];
dontUnpack = true;
dontConfigure = true;
dontBuild = true;
installPhase = ''
mkdir -p $out
cp -r ${unwrapped}/* $out
'';
passthru = {
unwrapped = unwrapped;
withModules = modules: wrapper.overrideAttrs (prev: {
buildInputs = prev.buildInputs ++ modules;
});
};
};
in wrapper

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1749285348, "lastModified": 1768127708,
"narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=", "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3e3afe5174c561dee0df6f2c2b2236990146329f", "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -4,23 +4,28 @@
}; };
outputs = { self, nixpkgs }: let outputs = { self, nixpkgs }: let
overlayPkgs = p: p.appendOverlays [ self.overlays.default ];
forEachSystem = fn: forEachSystem = fn:
nixpkgs.lib.genAttrs nixpkgs.lib.genAttrs
nixpkgs.lib.platforms.linux nixpkgs.lib.platforms.linux
(system: fn system nixpkgs.legacyPackages.${system}); (system: fn system (overlayPkgs nixpkgs.legacyPackages.${system}));
in { in {
packages = forEachSystem (system: pkgs: rec { overlays.default = import ./overlay.nix {
quickshell = pkgs.callPackage ./default.nix { rev = self.rev or self.dirtyRev;
gitRev = self.rev or self.dirtyRev; };
};
packages = forEachSystem (system: pkgs: rec {
quickshell = pkgs.quickshell;
default = quickshell; default = quickshell;
}); });
devShells = forEachSystem (system: pkgs: rec { devShells = forEachSystem (system: pkgs: rec {
default = import ./shell.nix { default = import ./shell.nix {
inherit pkgs; inherit pkgs;
inherit (self.packages.${system}) quickshell; quickshell = self.packages.${system}.quickshell.override {
stdenv = pkgs.clangStdenv;
};
}; };
}); });
}; };

5
overlay.nix Normal file
View file

@ -0,0 +1,5 @@
{ rev ? null }: (final: prev: {
quickshell = final.callPackage ./default.nix {
gitRev = rev;
};
})

View file

@ -42,6 +42,7 @@
libxcb libxcb
libxkbcommon libxkbcommon
linux-pam linux-pam
polkit
mesa mesa
pipewire pipewire
qtbase qtbase
@ -55,8 +56,7 @@
#~(list "-GNinja" #~(list "-GNinja"
"-DDISTRIBUTOR=\"In-tree Guix channel\"" "-DDISTRIBUTOR=\"In-tree Guix channel\""
"-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO" "-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO"
;; Breakpad is not currently packaged for Guix. "-DCRASH_HANDLER=OFF")
"-DCRASH_REPORTER=OFF")
#:phases #:phases
#~(modify-phases %standard-phases #~(modify-phases %standard-phases
(replace 'build (lambda _ (invoke "cmake" "--build" "."))) (replace 'build (lambda _ (invoke "cmake" "--build" ".")))

View file

@ -1,14 +1,15 @@
{ {
pkgs ? import <nixpkgs> {}, pkgs ? import <nixpkgs> {},
quickshell ? pkgs.callPackage ./default.nix {}, stdenv ? pkgs.clangStdenv, # faster compiles than gcc
quickshell ? pkgs.callPackage ./default.nix { inherit stdenv; },
... ...
}: let }: let
tidyfox = import (pkgs.fetchFromGitea { tidyfox = import (pkgs.fetchFromGitea {
domain = "git.outfoxxed.me"; domain = "git.outfoxxed.me";
owner = "outfoxxed"; owner = "outfoxxed";
repo = "tidyfox"; repo = "tidyfox";
rev = "1f062cc198d1112d13e5128fa1f2ee3dbffe613b"; rev = "9d85d7e7dea2602aa74ec3168955fee69967e92f";
sha256 = "kbt0Zc1qHE5fhqBkKz8iue+B+ZANjF1AR/RdgmX1r0I="; hash = "sha256-77ERiweF6lumonp2c/124rAoVG6/o9J+Aajhttwtu0w=";
}) { inherit pkgs; }; }) { inherit pkgs; };
in pkgs.mkShell.override { stdenv = quickshell.stdenv; } { in pkgs.mkShell.override { stdenv = quickshell.stdenv; } {
inputsFrom = [ quickshell ]; inputsFrom = [ quickshell ];

View file

@ -11,8 +11,9 @@ add_subdirectory(window)
add_subdirectory(io) add_subdirectory(io)
add_subdirectory(widgets) add_subdirectory(widgets)
add_subdirectory(ui) add_subdirectory(ui)
add_subdirectory(windowmanager)
if (CRASH_REPORTER) if (CRASH_HANDLER)
add_subdirectory(crash) add_subdirectory(crash)
endif() endif()
@ -33,3 +34,7 @@ add_subdirectory(services)
if (BLUETOOTH) if (BLUETOOTH)
add_subdirectory(bluetooth) add_subdirectory(bluetooth)
endif() endif()
if (NETWORK)
add_subdirectory(network)
endif()

View file

@ -9,7 +9,6 @@
#include <qlogging.h> #include <qlogging.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qstring.h> #include <qstring.h>
#include <qstringliteral.h>
#include <qtypes.h> #include <qtypes.h>
#include "../core/logcat.hpp" #include "../core/logcat.hpp"
@ -53,6 +52,12 @@ QString BluetoothAdapter::adapterId() const {
void BluetoothAdapter::setEnabled(bool enabled) { void BluetoothAdapter::setEnabled(bool enabled) {
if (enabled == this->bEnabled) return; if (enabled == this->bEnabled) return;
if (enabled && this->bState == BluetoothAdapterState::Blocked) {
qCCritical(logAdapter) << "Cannot enable adapter because it is blocked by rfkill.";
return;
}
this->bEnabled = enabled; this->bEnabled = enabled;
this->pEnabled.write(); this->pEnabled.write();
} }

View file

@ -8,7 +8,6 @@
#include <qlogging.h> #include <qlogging.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qstring.h> #include <qstring.h>
#include <qstringliteral.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h> #include <qtypes.h>

View file

@ -9,16 +9,10 @@ if (NOT DEFINED GIT_REVISION)
) )
endif() endif()
if (CRASH_REPORTER) if (CRASH_HANDLER)
set(CRASH_REPORTER_DEF 1) set(CRASH_HANDLER_DEF 1)
else() else()
set(CRASH_REPORTER_DEF 0) set(CRASH_HANDLER_DEF 0)
endif()
if (DISTRIBUTOR_DEBUGINFO_AVAILABLE)
set(DEBUGINFO_AVAILABLE 1)
else()
set(DEBUGINFO_AVAILABLE 0)
endif() endif()
configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES) configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES)

View file

@ -1,10 +1,14 @@
#pragma once #pragma once
// NOLINTBEGIN // NOLINTBEGIN
#define QS_VERSION "@quickshell_VERSION@"
#define QS_VERSION_MAJOR @quickshell_VERSION_MAJOR@
#define QS_VERSION_MINOR @quickshell_VERSION_MINOR@
#define QS_VERSION_PATCH @quickshell_VERSION_PATCH@
#define QS_UNRELEASED_FEATURES "@UNRELEASED_FEATURES@"
#define GIT_REVISION "@GIT_REVISION@" #define GIT_REVISION "@GIT_REVISION@"
#define DISTRIBUTOR "@DISTRIBUTOR@" #define DISTRIBUTOR "@DISTRIBUTOR@"
#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@ #define CRASH_HANDLER @CRASH_HANDLER_DEF@
#define CRASH_REPORTER @CRASH_REPORTER_DEF@
#define BUILD_TYPE "@CMAKE_BUILD_TYPE@" #define BUILD_TYPE "@CMAKE_BUILD_TYPE@"
#define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)" #define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)"
#define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@" #define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@"

View file

@ -12,6 +12,7 @@ qt_add_library(quickshell-core STATIC
singleton.cpp singleton.cpp
generation.cpp generation.cpp
scan.cpp scan.cpp
scanenv.cpp
qsintercept.cpp qsintercept.cpp
incubator.cpp incubator.cpp
lazyloader.cpp lazyloader.cpp
@ -23,7 +24,7 @@ qt_add_library(quickshell-core STATIC
model.cpp model.cpp
elapsedtimer.cpp elapsedtimer.cpp
desktopentry.cpp desktopentry.cpp
objectrepeater.cpp desktopentrymonitor.cpp
platformmenu.cpp platformmenu.cpp
qsmenu.cpp qsmenu.cpp
retainable.cpp retainable.cpp
@ -38,6 +39,7 @@ qt_add_library(quickshell-core STATIC
iconprovider.cpp iconprovider.cpp
scriptmodel.cpp scriptmodel.cpp
colorquantizer.cpp colorquantizer.cpp
toolsupport.cpp
) )
qt_add_qml_module(quickshell-core qt_add_qml_module(quickshell-core
@ -50,7 +52,7 @@ qt_add_qml_module(quickshell-core
install_qml_module(quickshell-core) install_qml_module(quickshell-core)
target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets) target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build)
qs_module_pch(quickshell-core SET large) qs_module_pch(quickshell-core SET large)

View file

@ -28,26 +28,28 @@ ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qrea
: source(source) : source(source)
, maxDepth(depth) , maxDepth(depth)
, rescaleSize(rescaleSize) { , rescaleSize(rescaleSize) {
setAutoDelete(false); this->setAutoDelete(false);
} }
void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) { void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) {
if (shouldCancel.loadAcquire() || source->isEmpty()) return; if (shouldCancel.loadAcquire() || this->source->isEmpty()) return;
colors.clear(); this->colors.clear();
auto image = QImage(source->toLocalFile()); auto image = QImage(this->source->toLocalFile());
if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) { if ((image.width() > this->rescaleSize || image.height() > this->rescaleSize)
&& this->rescaleSize > 0)
{
image = image.scaled( image = image.scaled(
static_cast<int>(rescaleSize), static_cast<int>(this->rescaleSize),
static_cast<int>(rescaleSize), static_cast<int>(this->rescaleSize),
Qt::KeepAspectRatio, Qt::KeepAspectRatio,
Qt::SmoothTransformation Qt::SmoothTransformation
); );
} }
if (image.isNull()) { if (image.isNull()) {
qCWarning(logColorQuantizer) << "Failed to load image from" << source->toString(); qCWarning(logColorQuantizer) << "Failed to load image from" << this->source->toString();
return; return;
} }
@ -63,7 +65,7 @@ void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCa
auto startTime = QDateTime::currentDateTime(); auto startTime = QDateTime::currentDateTime();
colors = quantization(pixels, 0); this->colors = this->quantization(pixels, 0);
auto endTime = QDateTime::currentDateTime(); auto endTime = QDateTime::currentDateTime();
auto milliseconds = startTime.msecsTo(endTime); auto milliseconds = startTime.msecsTo(endTime);
@ -77,7 +79,7 @@ QList<QColor> ColorQuantizerOperation::quantization(
) { ) {
if (shouldCancel.loadAcquire()) return QList<QColor>(); if (shouldCancel.loadAcquire()) return QList<QColor>();
if (depth >= maxDepth || rgbValues.isEmpty()) { if (depth >= this->maxDepth || rgbValues.isEmpty()) {
if (rgbValues.isEmpty()) return QList<QColor>(); if (rgbValues.isEmpty()) return QList<QColor>();
auto totalR = 0; auto totalR = 0;
@ -114,8 +116,8 @@ QList<QColor> ColorQuantizerOperation::quantization(
auto rightHalf = rgbValues.mid(mid); auto rightHalf = rgbValues.mid(mid);
QList<QColor> result; QList<QColor> result;
result.append(quantization(leftHalf, depth + 1)); result.append(this->quantization(leftHalf, depth + 1));
result.append(quantization(rightHalf, depth + 1)); result.append(this->quantization(rightHalf, depth + 1));
return result; return result;
} }
@ -159,7 +161,7 @@ void ColorQuantizerOperation::finishRun() {
} }
void ColorQuantizerOperation::finished() { void ColorQuantizerOperation::finished() {
emit this->done(colors); emit this->done(this->colors);
delete this; delete this;
} }
@ -178,39 +180,39 @@ void ColorQuantizerOperation::run() {
void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); } void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); }
void ColorQuantizer::componentComplete() { void ColorQuantizer::componentComplete() {
componentCompleted = true; this->componentCompleted = true;
if (!mSource.isEmpty()) quantizeAsync(); if (!this->mSource.isEmpty()) this->quantizeAsync();
} }
void ColorQuantizer::setSource(const QUrl& source) { void ColorQuantizer::setSource(const QUrl& source) {
if (mSource != source) { if (this->mSource != source) {
mSource = source; this->mSource = source;
emit this->sourceChanged(); emit this->sourceChanged();
if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync(); if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync();
} }
} }
void ColorQuantizer::setDepth(qreal depth) { void ColorQuantizer::setDepth(qreal depth) {
if (mDepth != depth) { if (this->mDepth != depth) {
mDepth = depth; this->mDepth = depth;
emit this->depthChanged(); emit this->depthChanged();
if (this->componentCompleted) quantizeAsync(); if (this->componentCompleted) this->quantizeAsync();
} }
} }
void ColorQuantizer::setRescaleSize(int rescaleSize) { void ColorQuantizer::setRescaleSize(int rescaleSize) {
if (mRescaleSize != rescaleSize) { if (this->mRescaleSize != rescaleSize) {
mRescaleSize = rescaleSize; this->mRescaleSize = rescaleSize;
emit this->rescaleSizeChanged(); emit this->rescaleSizeChanged();
if (this->componentCompleted) quantizeAsync(); if (this->componentCompleted) this->quantizeAsync();
} }
} }
void ColorQuantizer::operationFinished(const QList<QColor>& result) { void ColorQuantizer::operationFinished(const QList<QColor>& result) {
bColors = result; this->bColors = result;
this->liveOperation = nullptr; this->liveOperation = nullptr;
emit this->colorsChanged(); emit this->colorsChanged();
} }
@ -219,7 +221,8 @@ void ColorQuantizer::quantizeAsync() {
if (this->liveOperation) this->cancelAsync(); if (this->liveOperation) this->cancelAsync();
qCDebug(logColorQuantizer) << "Starting color quantization asynchronously"; qCDebug(logColorQuantizer) << "Starting color quantization asynchronously";
this->liveOperation = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize); this->liveOperation =
new ColorQuantizerOperation(&this->mSource, this->mDepth, this->mRescaleSize);
QObject::connect( QObject::connect(
this->liveOperation, this->liveOperation,

View file

@ -91,13 +91,13 @@ public:
[[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; } [[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; }
[[nodiscard]] QUrl source() const { return mSource; } [[nodiscard]] QUrl source() const { return this->mSource; }
void setSource(const QUrl& source); void setSource(const QUrl& source);
[[nodiscard]] qreal depth() const { return mDepth; } [[nodiscard]] qreal depth() const { return this->mDepth; }
void setDepth(qreal depth); void setDepth(qreal depth);
[[nodiscard]] qreal rescaleSize() const { return mRescaleSize; } [[nodiscard]] qreal rescaleSize() const { return this->mRescaleSize; }
void setRescaleSize(int rescaleSize); void setRescaleSize(int rescaleSize);
signals: signals:

View file

@ -1,21 +1,27 @@
#include "desktopentry.hpp" #include "desktopentry.hpp"
#include <algorithm>
#include <utility>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qdebug.h> #include <qdebug.h>
#include <qdir.h> #include <qdir.h>
#include <qfile.h>
#include <qfileinfo.h> #include <qfileinfo.h>
#include <qhash.h>
#include <qlist.h>
#include <qlogging.h> #include <qlogging.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qnamespace.h> #include <qnamespace.h>
#include <qobject.h> #include <qobject.h>
#include <qobjectdefs.h>
#include <qpair.h> #include <qpair.h>
#include <qstringview.h> #include <qproperty.h>
#include <qscopeguard.h>
#include <qtenvironmentvariables.h> #include <qtenvironmentvariables.h>
#include <qthreadpool.h>
#include <qtmetamacros.h>
#include <ranges> #include <ranges>
#include "../io/processcore.hpp" #include "../io/processcore.hpp"
#include "desktopentrymonitor.hpp"
#include "logcat.hpp" #include "logcat.hpp"
#include "model.hpp" #include "model.hpp"
#include "qmlglobal.hpp" #include "qmlglobal.hpp"
@ -55,12 +61,14 @@ struct Locale {
[[nodiscard]] int matchScore(const Locale& other) const { [[nodiscard]] int matchScore(const Locale& other) const {
if (this->language != other.language) return 0; if (this->language != other.language) return 0;
auto territoryMatches = !this->territory.isEmpty() && this->territory == other.territory;
auto modifierMatches = !this->modifier.isEmpty() && this->modifier == other.modifier; if (!other.modifier.isEmpty() && this->modifier != other.modifier) return 0;
if (!other.territory.isEmpty() && this->territory != other.territory) return 0;
auto score = 1; auto score = 1;
if (territoryMatches) score += 2;
if (modifierMatches) score += 1; if (!other.territory.isEmpty()) score += 2;
if (!other.modifier.isEmpty()) score += 1;
return score; return score;
} }
@ -86,56 +94,64 @@ struct Locale {
QDebug operator<<(QDebug debug, const Locale& locale) { QDebug operator<<(QDebug debug, const Locale& locale) {
auto saver = QDebugStateSaver(debug); auto saver = QDebugStateSaver(debug);
debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory
<< ", modifier" << locale.modifier << ')'; << ", modifier=" << locale.modifier << ')';
return debug; return debug;
} }
void DesktopEntry::parseEntry(const QString& text) { ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& text) {
ParsedDesktopEntryData data;
data.id = id;
const auto& system = Locale::system(); const auto& system = Locale::system();
auto groupName = QString(); auto groupName = QString();
auto entries = QHash<QString, QPair<Locale, QString>>(); auto entries = QHash<QString, QPair<Locale, QString>>();
auto finishCategory = [this, &groupName, &entries]() { auto actionOrder = QStringList();
auto pendingActions = QHash<QString, DesktopActionData>();
auto finishCategory = [&data, &groupName, &entries, &actionOrder, &pendingActions]() {
if (groupName == "Desktop Entry") { if (groupName == "Desktop Entry") {
if (entries["Type"].second != "Application") return; if (entries.value("Type").second != "Application") return;
if (entries.contains("Hidden") && entries["Hidden"].second == "true") return;
for (const auto& [key, pair]: entries.asKeyValueRange()) { for (const auto& [key, pair]: entries.asKeyValueRange()) {
auto& [_, value] = pair; auto& [_, value] = pair;
this->mEntries.insert(key, value); data.entries.insert(key, value);
if (key == "Name") this->mName = value; if (key == "Name") data.name = value;
else if (key == "GenericName") this->mGenericName = value; else if (key == "GenericName") data.genericName = value;
else if (key == "NoDisplay") this->mNoDisplay = value == "true"; else if (key == "StartupWMClass") data.startupClass = value;
else if (key == "Comment") this->mComment = value; else if (key == "NoDisplay") data.noDisplay = value == "true";
else if (key == "Icon") this->mIcon = value; else if (key == "Hidden") data.hidden = value == "true";
else if (key == "Comment") data.comment = value;
else if (key == "Icon") data.icon = value;
else if (key == "Exec") { else if (key == "Exec") {
this->mExecString = value; data.execString = value;
this->mCommand = DesktopEntry::parseExecString(value); data.command = DesktopEntry::parseExecString(value);
} else if (key == "Path") this->mWorkingDirectory = value; } else if (key == "Path") data.workingDirectory = value;
else if (key == "Terminal") this->mTerminal = value == "true"; else if (key == "Terminal") data.terminal = value == "true";
else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts); else if (key == "Categories") data.categories = value.split(u';', Qt::SkipEmptyParts);
else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts); else if (key == "Keywords") data.keywords = value.split(u';', Qt::SkipEmptyParts);
else if (key == "Actions") actionOrder = value.split(u';', Qt::SkipEmptyParts);
} }
} else if (groupName.startsWith("Desktop Action ")) { } else if (groupName.startsWith("Desktop Action ")) {
auto actionName = groupName.sliced(16); auto actionName = groupName.sliced(15);
auto* action = new DesktopAction(actionName, this); DesktopActionData action;
action.id = actionName;
for (const auto& [key, pair]: entries.asKeyValueRange()) { for (const auto& [key, pair]: entries.asKeyValueRange()) {
const auto& [_, value] = pair; const auto& [_, value] = pair;
action->mEntries.insert(key, value); action.entries.insert(key, value);
if (key == "Name") action->mName = value; if (key == "Name") action.name = value;
else if (key == "Icon") action->mIcon = value; else if (key == "Icon") action.icon = value;
else if (key == "Exec") { else if (key == "Exec") {
action->mExecString = value; action.execString = value;
action->mCommand = DesktopEntry::parseExecString(value); action.command = DesktopEntry::parseExecString(value);
} }
} }
this->mActions.insert(actionName, action); pendingActions.insert(actionName, action);
} }
entries.clear(); entries.clear();
@ -181,16 +197,73 @@ void DesktopEntry::parseEntry(const QString& text) {
} }
finishCategory(); finishCategory();
for (const auto& actionId: actionOrder) {
if (pendingActions.contains(actionId)) {
data.actions.append(pendingActions.value(actionId));
}
}
return data;
}
void DesktopEntry::updateState(const ParsedDesktopEntryData& newState) {
Qt::beginPropertyUpdateGroup();
this->bName = newState.name;
this->bGenericName = newState.genericName;
this->bStartupClass = newState.startupClass;
this->bNoDisplay = newState.noDisplay;
this->bComment = newState.comment;
this->bIcon = newState.icon;
this->bExecString = newState.execString;
this->bCommand = newState.command;
this->bWorkingDirectory = newState.workingDirectory;
this->bRunInTerminal = newState.terminal;
this->bCategories = newState.categories;
this->bKeywords = newState.keywords;
Qt::endPropertyUpdateGroup();
this->state = newState;
this->updateActions(newState.actions);
}
void DesktopEntry::updateActions(const QVector<DesktopActionData>& newActions) {
auto old = this->mActions;
this->mActions.clear();
for (const auto& d: newActions) {
DesktopAction* act = nullptr;
auto found = std::ranges::find(old, d.id, &DesktopAction::mId);
if (found != old.end()) {
act = *found;
old.erase(found);
} else {
act = new DesktopAction(d.id, this);
}
Qt::beginPropertyUpdateGroup();
act->bName = d.name;
act->bIcon = d.icon;
act->bExecString = d.execString;
act->bCommand = d.command;
Qt::endPropertyUpdateGroup();
act->mEntries = d.entries;
this->mActions.append(act);
}
for (auto* leftover: old) {
leftover->deleteLater();
}
} }
void DesktopEntry::execute() const { void DesktopEntry::execute() const {
DesktopEntry::doExec(this->mCommand, this->mWorkingDirectory); DesktopEntry::doExec(this->bCommand.value(), this->bWorkingDirectory.value());
} }
bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); } bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); }
bool DesktopEntry::noDisplay() const { return this->mNoDisplay; }
QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions.values(); } QVector<DesktopAction*> DesktopEntry::actions() const { return this->mActions; }
QVector<QString> DesktopEntry::parseExecString(const QString& execString) { QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
QVector<QString> arguments; QVector<QString> arguments;
@ -209,16 +282,22 @@ QVector<QString> DesktopEntry::parseExecString(const QString& execString) {
currentArgument += '\\'; currentArgument += '\\';
escape = 0; escape = 0;
} }
} else if (escape == 2) {
currentArgument += c;
escape = 0;
} else if (escape != 0) { } else if (escape != 0) {
if (escape != 2) { switch (c.unicode()) {
// Technically this is an illegal state, but the spec has a terrible double escape case 's': currentArgument += u' '; break;
// rule in strings for no discernable reason. Assuming someone might understandably case 'n': currentArgument += u'\n'; break;
// misunderstand it, treat it as a normal escape and log it. case 't': currentArgument += u'\t'; break;
case 'r': currentArgument += u'\r'; break;
case '\\': currentArgument += u'\\'; break;
default:
qCWarning(logDesktopEntry).noquote() qCWarning(logDesktopEntry).noquote()
<< "Illegal escape sequence in desktop entry exec string:" << execString; << "Illegal escape sequence in desktop entry exec string:" << execString;
currentArgument += c;
break;
} }
currentArgument += c;
escape = 0; escape = 0;
} else if (c == u'"' || c == u'\'') { } else if (c == u'"' || c == u'\'') {
parsingString = false; parsingString = false;
@ -264,59 +343,44 @@ void DesktopEntry::doExec(const QList<QString>& execString, const QString& worki
} }
void DesktopAction::execute() const { void DesktopAction::execute() const {
DesktopEntry::doExec(this->mCommand, this->entry->mWorkingDirectory); DesktopEntry::doExec(this->bCommand.value(), this->entry->bWorkingDirectory.value());
} }
DesktopEntryManager::DesktopEntryManager() { DesktopEntryScanner::DesktopEntryScanner(DesktopEntryManager* manager): manager(manager) {
this->scanDesktopEntries(); this->setAutoDelete(true);
this->populateApplications();
} }
void DesktopEntryManager::scanDesktopEntries() { void DesktopEntryScanner::run() {
QList<QString> dataPaths; const auto& desktopPaths = DesktopEntryManager::desktopPaths();
auto scanResults = QList<ParsedDesktopEntryData>();
if (qEnvironmentVariableIsSet("XDG_DATA_HOME")) { for (const auto& path: desktopPaths | std::views::reverse) {
dataPaths.push_back(qEnvironmentVariable("XDG_DATA_HOME")); auto file = QFileInfo(path);
} else if (qEnvironmentVariableIsSet("HOME")) { if (!file.isDir()) continue;
dataPaths.push_back(qEnvironmentVariable("HOME") + "/.local/share");
this->scanDirectory(QDir(path), QString(), scanResults);
} }
if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) { QMetaObject::invokeMethod(
auto var = qEnvironmentVariable("XDG_DATA_DIRS"); this->manager,
dataPaths += var.split(u':', Qt::SkipEmptyParts); "onScanCompleted",
} else { Qt::QueuedConnection,
dataPaths.push_back("/usr/local/share"); Q_ARG(QList<ParsedDesktopEntryData>, scanResults)
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() { void DesktopEntryScanner::scanDirectory(
for (auto& entry: this->desktopEntries.values()) { const QDir& dir,
if (!entry->noDisplay()) this->mApplications.insertObject(entry); const QString& idPrefix,
} QList<ParsedDesktopEntryData>& entries
} ) {
auto dirEntries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) { for (auto& entry: dirEntries) {
auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); if (entry.isDir()) {
auto subdirPrefix = idPrefix.isEmpty() ? entry.fileName() : idPrefix + '-' + entry.fileName();
for (auto& entry: entries) { this->scanDirectory(QDir(entry.absoluteFilePath()), subdirPrefix, entries);
if (entry.isDir()) this->scanPath(entry.absoluteFilePath(), prefix + dir.dirName() + "-"); } else if (entry.isFile()) {
else if (entry.isFile()) {
auto path = entry.filePath(); auto path = entry.filePath();
if (!path.endsWith(".desktop")) { if (!path.endsWith(".desktop")) {
qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension"; qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension";
@ -329,46 +393,42 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) {
continue; continue;
} }
auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8); auto basename = QFileInfo(entry.fileName()).completeBaseName();
auto lowerId = id.toLower(); auto id = idPrefix.isEmpty() ? basename : idPrefix + '-' + basename;
auto content = QString::fromUtf8(file.readAll());
auto text = QString::fromUtf8(file.readAll()); auto data = DesktopEntry::parseText(id, content);
auto* dentry = new DesktopEntry(id, this); entries.append(std::move(data));
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(): monitor(new DesktopEntryMonitor(this)) {
QObject::connect(
this->monitor,
&DesktopEntryMonitor::desktopEntriesChanged,
this,
&DesktopEntryManager::handleFileChanges
);
DesktopEntryScanner(this).run();
}
void DesktopEntryManager::scanDesktopEntries() {
qCDebug(logDesktopEntry) << "Starting desktop entry scan";
if (this->scanInProgress) {
qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan";
this->scanQueued = true;
return;
}
this->scanInProgress = true;
this->scanQueued = false;
auto* scanner = new DesktopEntryScanner(this);
QThreadPool::globalInstance()->start(scanner);
}
DesktopEntryManager* DesktopEntryManager::instance() { DesktopEntryManager* DesktopEntryManager::instance() {
static auto* instance = new DesktopEntryManager(); // NOLINT static auto* instance = new DesktopEntryManager(); // NOLINT
return instance; return instance;
@ -384,14 +444,167 @@ DesktopEntry* DesktopEntryManager::byId(const QString& id) {
} }
} }
DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) {
if (auto* entry = this->byId(name)) return entry;
auto list = this->desktopEntries.values();
auto iter = std::ranges::find_if(list, [&](DesktopEntry* entry) {
return name == entry->bStartupClass.value();
});
if (iter != list.end()) return *iter;
iter = std::ranges::find_if(list, [&](DesktopEntry* entry) {
return name.toLower() == entry->bStartupClass.value().toLower();
});
if (iter != list.end()) return *iter;
return nullptr;
}
ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; } ObjectModel<DesktopEntry>* DesktopEntryManager::applications() { return &this->mApplications; }
DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); } void DesktopEntryManager::handleFileChanges() {
qCDebug(logDesktopEntry) << "Directory change detected, performing full rescan";
if (this->scanInProgress) {
qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan";
this->scanQueued = true;
return;
}
this->scanInProgress = true;
this->scanQueued = false;
auto* scanner = new DesktopEntryScanner(this);
QThreadPool::globalInstance()->start(scanner);
}
const QStringList& DesktopEntryManager::desktopPaths() {
static const auto paths = []() {
auto dataPaths = QStringList();
auto dataHome = qEnvironmentVariable("XDG_DATA_HOME");
if (dataHome.isEmpty() && qEnvironmentVariableIsSet("HOME"))
dataHome = qEnvironmentVariable("HOME") + "/.local/share";
if (!dataHome.isEmpty()) dataPaths.append(dataHome + "/applications");
auto dataDirs = qEnvironmentVariable("XDG_DATA_DIRS");
if (dataDirs.isEmpty()) dataDirs = "/usr/local/share:/usr/share";
for (const auto& dir: dataDirs.split(':', Qt::SkipEmptyParts)) {
dataPaths.append(dir + "/applications");
}
return dataPaths;
}();
return paths;
}
void DesktopEntryManager::onScanCompleted(const QList<ParsedDesktopEntryData>& scanResults) {
auto guard = qScopeGuard([this] {
this->scanInProgress = false;
if (this->scanQueued) {
this->scanQueued = false;
this->scanDesktopEntries();
}
});
auto oldEntries = this->desktopEntries;
auto newEntries = QHash<QString, DesktopEntry*>();
auto newLowercaseEntries = QHash<QString, DesktopEntry*>();
for (const auto& data: scanResults) {
auto lowerId = data.id.toLower();
if (data.hidden) {
if (auto* victim = newEntries.take(data.id)) victim->deleteLater();
newLowercaseEntries.remove(lowerId);
if (auto it = oldEntries.find(data.id); it != oldEntries.end()) {
it.value()->deleteLater();
oldEntries.erase(it);
}
qCDebug(logDesktopEntry) << "Masking hidden desktop entry" << data.id;
continue;
}
DesktopEntry* dentry = nullptr;
if (auto it = oldEntries.find(data.id); it != oldEntries.end()) {
dentry = it.value();
oldEntries.erase(it);
dentry->updateState(data);
} else {
dentry = new DesktopEntry(data.id, this);
dentry->updateState(data);
}
if (!dentry->isValid()) {
qCDebug(logDesktopEntry) << "Skipping desktop entry" << data.id;
if (!oldEntries.contains(data.id)) {
dentry->deleteLater();
}
continue;
}
qCDebug(logDesktopEntry) << "Found desktop entry" << data.id;
auto conflictingId = newEntries.contains(data.id);
if (conflictingId) {
qCDebug(logDesktopEntry) << "Replacing old entry for" << data.id;
if (auto* victim = newEntries.take(data.id)) victim->deleteLater();
newLowercaseEntries.remove(lowerId);
}
newEntries.insert(data.id, dentry);
if (newLowercaseEntries.contains(lowerId)) {
qCInfo(logDesktopEntry).nospace()
<< "Multiple desktop entries have the same lowercased id " << lowerId
<< ". This can cause ambiguity when byId requests are not made with the correct case "
"already.";
newLowercaseEntries.remove(lowerId);
}
newLowercaseEntries.insert(lowerId, dentry);
}
this->desktopEntries = newEntries;
this->lowercaseDesktopEntries = newLowercaseEntries;
auto newApplications = QVector<DesktopEntry*>();
for (auto* entry: this->desktopEntries.values())
if (!entry->bNoDisplay) newApplications.append(entry);
this->mApplications.diffUpdate(newApplications);
emit this->applicationsChanged();
for (auto* e: oldEntries) e->deleteLater();
}
DesktopEntries::DesktopEntries() {
QObject::connect(
DesktopEntryManager::instance(),
&DesktopEntryManager::applicationsChanged,
this,
&DesktopEntries::applicationsChanged
);
}
DesktopEntry* DesktopEntries::byId(const QString& id) { DesktopEntry* DesktopEntries::byId(const QString& id) {
return DesktopEntryManager::instance()->byId(id); return DesktopEntryManager::instance()->byId(id);
} }
DesktopEntry* DesktopEntries::heuristicLookup(const QString& name) {
return DesktopEntryManager::instance()->heuristicLookup(name);
}
ObjectModel<DesktopEntry>* DesktopEntries::applications() { ObjectModel<DesktopEntry>* DesktopEntries::applications() {
return DesktopEntryManager::instance()->applications(); return DesktopEntryManager::instance()->applications();
} }

View file

@ -6,32 +6,68 @@
#include <qdir.h> #include <qdir.h>
#include <qhash.h> #include <qhash.h>
#include <qobject.h> #include <qobject.h>
#include <qproperty.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qrunnable.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include "desktopentrymonitor.hpp"
#include "doc.hpp" #include "doc.hpp"
#include "model.hpp" #include "model.hpp"
class DesktopAction; class DesktopAction;
class DesktopEntryMonitor;
struct DesktopActionData {
QString id;
QString name;
QString icon;
QString execString;
QVector<QString> command;
QHash<QString, QString> entries;
};
struct ParsedDesktopEntryData {
QString id;
QString name;
QString genericName;
QString startupClass;
bool noDisplay = false;
bool hidden = false;
QString comment;
QString icon;
QString execString;
QVector<QString> command;
QString workingDirectory;
bool terminal = false;
QVector<QString> categories;
QVector<QString> keywords;
QHash<QString, QString> entries;
QVector<DesktopActionData> actions;
};
/// A desktop entry. See @@DesktopEntries for details. /// A desktop entry. See @@DesktopEntries for details.
class DesktopEntry: public QObject { class DesktopEntry: public QObject {
Q_OBJECT; Q_OBJECT;
Q_PROPERTY(QString id MEMBER mId CONSTANT); Q_PROPERTY(QString id MEMBER mId CONSTANT);
/// Name of the specific application, such as "Firefox". /// Name of the specific application, such as "Firefox".
Q_PROPERTY(QString name MEMBER mName CONSTANT); // clang-format off
Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName);
/// Short description of the application, such as "Web Browser". May be empty. /// Short description of the application, such as "Web Browser". May be empty.
Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT); Q_PROPERTY(QString genericName READ default WRITE default NOTIFY genericNameChanged BINDABLE bindableGenericName);
/// Initial class or app id the app intends to use. May be useful for matching running apps
/// to desktop entries.
Q_PROPERTY(QString startupClass READ default WRITE default NOTIFY startupClassChanged BINDABLE bindableStartupClass);
/// If true, this application should not be displayed in menus and launchers. /// If true, this application should not be displayed in menus and launchers.
Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT); Q_PROPERTY(bool noDisplay READ default WRITE default NOTIFY noDisplayChanged BINDABLE bindableNoDisplay);
/// Long description of the application, such as "View websites on the internet". May be empty. /// Long description of the application, such as "View websites on the internet". May be empty.
Q_PROPERTY(QString comment MEMBER mComment CONSTANT); Q_PROPERTY(QString comment READ default WRITE default NOTIFY commentChanged BINDABLE bindableComment);
/// Name of the icon associated with this application. May be empty. /// Name of the icon associated with this application. May be empty.
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT); Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon);
/// The raw `Exec` string from the desktop entry. /// The raw `Exec` string from the desktop entry.
/// ///
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run. /// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString);
/// The parsed `Exec` command in the desktop entry. /// The parsed `Exec` command in the desktop entry.
/// ///
/// The entry can be run with @@execute(), or by using this command in /// The entry can be run with @@execute(), or by using this command in
@ -40,13 +76,14 @@ class DesktopEntry: public QObject {
/// the invoked process. See @@execute() for details. /// the invoked process. See @@execute() for details.
/// ///
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true. /// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
Q_PROPERTY(QVector<QString> command MEMBER mCommand CONSTANT); Q_PROPERTY(QVector<QString> command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand);
/// The working directory to execute from. /// The working directory to execute from.
Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT); Q_PROPERTY(QString workingDirectory READ default WRITE default NOTIFY workingDirectoryChanged BINDABLE bindableWorkingDirectory);
/// If the application should run in a terminal. /// If the application should run in a terminal.
Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT); Q_PROPERTY(bool runInTerminal READ default WRITE default NOTIFY runInTerminalChanged BINDABLE bindableRunInTerminal);
Q_PROPERTY(QVector<QString> categories MEMBER mCategories CONSTANT); Q_PROPERTY(QVector<QString> categories READ default WRITE default NOTIFY categoriesChanged BINDABLE bindableCategories);
Q_PROPERTY(QVector<QString> keywords MEMBER mKeywords CONSTANT); Q_PROPERTY(QVector<QString> keywords READ default WRITE default NOTIFY keywordsChanged BINDABLE bindableKeywords);
// clang-format on
Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT); Q_PROPERTY(QVector<DesktopAction*> actions READ actions CONSTANT);
QML_ELEMENT; QML_ELEMENT;
QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries"); QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries");
@ -54,7 +91,8 @@ class DesktopEntry: public QObject {
public: public:
explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {} explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {}
void parseEntry(const QString& text); static ParsedDesktopEntryData parseText(const QString& id, const QString& text);
void updateState(const ParsedDesktopEntryData& newState);
/// Run the application. Currently ignores @@runInTerminal and field codes. /// Run the application. Currently ignores @@runInTerminal and field codes.
/// ///
@ -70,30 +108,66 @@ public:
Q_INVOKABLE void execute() const; Q_INVOKABLE void execute() const;
[[nodiscard]] bool isValid() const; [[nodiscard]] bool isValid() const;
[[nodiscard]] bool noDisplay() const;
[[nodiscard]] QVector<DesktopAction*> actions() const; [[nodiscard]] QVector<DesktopAction*> actions() const;
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
[[nodiscard]] QBindable<QString> bindableGenericName() const { return &this->bGenericName; }
[[nodiscard]] QBindable<QString> bindableStartupClass() const { return &this->bStartupClass; }
[[nodiscard]] QBindable<bool> bindableNoDisplay() const { return &this->bNoDisplay; }
[[nodiscard]] QBindable<QString> bindableComment() const { return &this->bComment; }
[[nodiscard]] QBindable<QString> bindableIcon() const { return &this->bIcon; }
[[nodiscard]] QBindable<QString> bindableExecString() const { return &this->bExecString; }
[[nodiscard]] QBindable<QVector<QString>> bindableCommand() const { return &this->bCommand; }
[[nodiscard]] QBindable<QString> bindableWorkingDirectory() const {
return &this->bWorkingDirectory;
}
[[nodiscard]] QBindable<bool> bindableRunInTerminal() const { return &this->bRunInTerminal; }
[[nodiscard]] QBindable<QVector<QString>> bindableCategories() const {
return &this->bCategories;
}
[[nodiscard]] QBindable<QVector<QString>> bindableKeywords() const { return &this->bKeywords; }
// currently ignores all field codes. // currently ignores all field codes.
static QVector<QString> parseExecString(const QString& execString); static QVector<QString> parseExecString(const QString& execString);
static void doExec(const QList<QString>& execString, const QString& workingDirectory); static void doExec(const QList<QString>& execString, const QString& workingDirectory);
signals:
void nameChanged();
void genericNameChanged();
void startupClassChanged();
void noDisplayChanged();
void commentChanged();
void iconChanged();
void execStringChanged();
void commandChanged();
void workingDirectoryChanged();
void runInTerminalChanged();
void categoriesChanged();
void keywordsChanged();
public: public:
QString mId; QString mId;
QString mName;
QString mGenericName; // clang-format off
bool mNoDisplay = false; Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bName, &DesktopEntry::nameChanged);
QString mComment; Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bGenericName, &DesktopEntry::genericNameChanged);
QString mIcon; Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bStartupClass, &DesktopEntry::startupClassChanged);
QString mExecString; Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bNoDisplay, &DesktopEntry::noDisplayChanged);
QVector<QString> mCommand; Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bComment, &DesktopEntry::commentChanged);
QString mWorkingDirectory; Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bIcon, &DesktopEntry::iconChanged);
bool mTerminal = false; Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bExecString, &DesktopEntry::execStringChanged);
QVector<QString> mCategories; Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bCommand, &DesktopEntry::commandChanged);
QVector<QString> mKeywords; Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bWorkingDirectory, &DesktopEntry::workingDirectoryChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bRunInTerminal, &DesktopEntry::runInTerminalChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bCategories, &DesktopEntry::categoriesChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector<QString>, bKeywords, &DesktopEntry::keywordsChanged);
// clang-format on
private: private:
QHash<QString, QString> mEntries; void updateActions(const QVector<DesktopActionData>& newActions);
QHash<QString, DesktopAction*> mActions;
ParsedDesktopEntryData state;
QVector<DesktopAction*> mActions;
friend class DesktopAction; friend class DesktopAction;
}; };
@ -102,12 +176,13 @@ private:
class DesktopAction: public QObject { class DesktopAction: public QObject {
Q_OBJECT; Q_OBJECT;
Q_PROPERTY(QString id MEMBER mId CONSTANT); Q_PROPERTY(QString id MEMBER mId CONSTANT);
Q_PROPERTY(QString name MEMBER mName CONSTANT); // clang-format off
Q_PROPERTY(QString icon MEMBER mIcon CONSTANT); Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName);
Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon);
/// The raw `Exec` string from the action. /// The raw `Exec` string from the action.
/// ///
/// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run. /// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run.
Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString);
/// The parsed `Exec` command in the action. /// The parsed `Exec` command in the action.
/// ///
/// The entry can be run with @@execute(), or by using this command in /// The entry can be run with @@execute(), or by using this command in
@ -116,7 +191,8 @@ class DesktopAction: public QObject {
/// the invoked process. /// the invoked process.
/// ///
/// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true. /// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true.
Q_PROPERTY(QVector<QString> command MEMBER mCommand CONSTANT); Q_PROPERTY(QVector<QString> command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand);
// clang-format on
QML_ELEMENT; QML_ELEMENT;
QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry"); QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry");
@ -132,18 +208,47 @@ public:
/// and @@DesktopEntry.workingDirectory. /// and @@DesktopEntry.workingDirectory.
Q_INVOKABLE void execute() const; Q_INVOKABLE void execute() const;
[[nodiscard]] QBindable<QString> bindableName() const { return &this->bName; }
[[nodiscard]] QBindable<QString> bindableIcon() const { return &this->bIcon; }
[[nodiscard]] QBindable<QString> bindableExecString() const { return &this->bExecString; }
[[nodiscard]] QBindable<QVector<QString>> bindableCommand() const { return &this->bCommand; }
signals:
void nameChanged();
void iconChanged();
void execStringChanged();
void commandChanged();
private: private:
DesktopEntry* entry; DesktopEntry* entry;
QString mId; QString mId;
QString mName;
QString mIcon;
QString mExecString;
QVector<QString> mCommand;
QHash<QString, QString> mEntries; QHash<QString, QString> mEntries;
// clang-format off
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bName, &DesktopAction::nameChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bIcon, &DesktopAction::iconChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bExecString, &DesktopAction::execStringChanged);
Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QVector<QString>, bCommand, &DesktopAction::commandChanged);
// clang-format on
friend class DesktopEntry; friend class DesktopEntry;
}; };
class DesktopEntryManager;
class DesktopEntryScanner: public QRunnable {
public:
explicit DesktopEntryScanner(DesktopEntryManager* manager);
void run() override;
// clang-format off
void scanDirectory(const QDir& dir, const QString& idPrefix, QList<ParsedDesktopEntryData>& entries);
// clang-format on
private:
DesktopEntryManager* manager;
};
class DesktopEntryManager: public QObject { class DesktopEntryManager: public QObject {
Q_OBJECT; Q_OBJECT;
@ -151,20 +256,32 @@ public:
void scanDesktopEntries(); void scanDesktopEntries();
[[nodiscard]] DesktopEntry* byId(const QString& id); [[nodiscard]] DesktopEntry* byId(const QString& id);
[[nodiscard]] DesktopEntry* heuristicLookup(const QString& name);
[[nodiscard]] ObjectModel<DesktopEntry>* applications(); [[nodiscard]] ObjectModel<DesktopEntry>* applications();
static DesktopEntryManager* instance(); static DesktopEntryManager* instance();
static const QStringList& desktopPaths();
signals:
void applicationsChanged();
private slots:
void handleFileChanges();
void onScanCompleted(const QList<ParsedDesktopEntryData>& scanResults);
private: private:
explicit DesktopEntryManager(); explicit DesktopEntryManager();
void populateApplications();
void scanPath(const QDir& dir, const QString& prefix = QString());
QHash<QString, DesktopEntry*> desktopEntries; QHash<QString, DesktopEntry*> desktopEntries;
QHash<QString, DesktopEntry*> lowercaseDesktopEntries; QHash<QString, DesktopEntry*> lowercaseDesktopEntries;
ObjectModel<DesktopEntry> mApplications {this}; ObjectModel<DesktopEntry> mApplications {this};
DesktopEntryMonitor* monitor = nullptr;
bool scanInProgress = false;
bool scanQueued = false;
friend class DesktopEntryScanner;
}; };
///! Desktop entry index. ///! Desktop entry index.
@ -186,7 +303,17 @@ public:
explicit DesktopEntries(); explicit DesktopEntries();
/// Look up a desktop entry by name. Includes NoDisplay entries. May return null. /// Look up a desktop entry by name. Includes NoDisplay entries. May return null.
///
/// While this function requires an exact match, @@heuristicLookup() will correctly
/// find an entry more often and is generally more useful.
Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id); Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id);
/// Look up a desktop entry by name using heuristics. Unlike @@byId(),
/// if no exact matches are found this function will try to guess - potentially incorrectly.
/// May return null.
Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name);
[[nodiscard]] static ObjectModel<DesktopEntry>* applications(); [[nodiscard]] static ObjectModel<DesktopEntry>* applications();
signals:
void applicationsChanged();
}; };

View file

@ -0,0 +1,68 @@
#include "desktopentrymonitor.hpp"
#include <qdir.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qobject.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include "desktopentry.hpp"
namespace {
void addPathAndParents(QFileSystemWatcher& watcher, const QString& path) {
watcher.addPath(path);
auto p = QFileInfo(path).absolutePath();
while (!p.isEmpty()) {
watcher.addPath(p);
const auto parent = QFileInfo(p).dir().absolutePath();
if (parent == p) break;
p = parent;
}
}
} // namespace
DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) {
this->debounceTimer.setSingleShot(true);
this->debounceTimer.setInterval(100);
QObject::connect(
&this->watcher,
&QFileSystemWatcher::directoryChanged,
this,
&DesktopEntryMonitor::onDirectoryChanged
);
QObject::connect(
&this->debounceTimer,
&QTimer::timeout,
this,
&DesktopEntryMonitor::processChanges
);
this->startMonitoring();
}
void DesktopEntryMonitor::startMonitoring() {
for (const auto& path: DesktopEntryManager::desktopPaths()) {
if (!QDir(path).exists()) continue;
addPathAndParents(this->watcher, path);
this->scanAndWatch(path);
}
}
void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) {
auto dir = QDir(dirPath);
if (!dir.exists()) return;
this->watcher.addPath(dirPath);
auto subdirs = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
for (const auto& subdir: subdirs) this->watcher.addPath(subdir.absoluteFilePath());
}
void DesktopEntryMonitor::onDirectoryChanged(const QString& /*path*/) {
this->debounceTimer.start();
}
void DesktopEntryMonitor::processChanges() { emit this->desktopEntriesChanged(); }

View file

@ -0,0 +1,32 @@
#pragma once
#include <qfilesystemwatcher.h>
#include <qobject.h>
#include <qstringlist.h>
#include <qtimer.h>
class DesktopEntryMonitor: public QObject {
Q_OBJECT
public:
explicit DesktopEntryMonitor(QObject* parent = nullptr);
~DesktopEntryMonitor() override = default;
DesktopEntryMonitor(const DesktopEntryMonitor&) = delete;
DesktopEntryMonitor& operator=(const DesktopEntryMonitor&) = delete;
DesktopEntryMonitor(DesktopEntryMonitor&&) = delete;
DesktopEntryMonitor& operator=(DesktopEntryMonitor&&) = delete;
signals:
void desktopEntriesChanged();
private slots:
void onDirectoryChanged(const QString& path);
void processChanges();
private:
void startMonitoring();
void scanAndWatch(const QString& dirPath);
QFileSystemWatcher watcher;
QTimer debounceTimer;
};

View file

@ -11,12 +11,12 @@
#include <qlist.h> #include <qlist.h>
#include <qlogging.h> #include <qlogging.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlcontext.h> #include <qqmlcontext.h>
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qqmlerror.h> #include <qqmlerror.h>
#include <qqmlincubator.h> #include <qqmlincubator.h>
#include <qquickwindow.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include "iconimageprovider.hpp" #include "iconimageprovider.hpp"
@ -49,7 +49,8 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
this->engine->addImportPath("qs:@/"); this->engine->addImportPath("qs:@/");
this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory); this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory);
this->engine->setIncubationController(&this->delayedIncubationController); this->incubationController.initLoop();
this->engine->setIncubationController(&this->incubationController);
this->engine->addImageProvider("icon", new IconImageProvider()); this->engine->addImageProvider("icon", new IconImageProvider());
this->engine->addImageProvider("qsimage", new QsImageProvider()); this->engine->addImageProvider("qsimage", new QsImageProvider());
@ -134,7 +135,7 @@ void EngineGeneration::onReload(EngineGeneration* old) {
// new generation acquires it then incubators will hang intermittently // new generation acquires it then incubators will hang intermittently
qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old; qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old;
old->incubationControllersLocked = true; old->incubationControllersLocked = true;
old->assignIncubationController(); old->updateIncubationMode();
} }
QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit); QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit);
@ -161,8 +162,9 @@ void EngineGeneration::postReload() {
if (this->engine == nullptr || this->root == nullptr) return; if (this->engine == nullptr || this->root == nullptr) return;
QsEnginePlugin::runOnReload(); QsEnginePlugin::runOnReload();
PostReloadHook::postReloadTree(this->root);
this->singletonRegistry.onPostReload(); emit this->firePostReload();
QObject::disconnect(this, &EngineGeneration::firePostReload, nullptr, nullptr);
} }
void EngineGeneration::setWatchingFiles(bool watching) { void EngineGeneration::setWatchingFiles(bool watching) {
@ -222,6 +224,11 @@ void EngineGeneration::onFileChanged(const QString& name) {
if (!this->watcher->files().contains(name)) { if (!this->watcher->files().contains(name)) {
this->deletedWatchedFiles.push_back(name); this->deletedWatchedFiles.push_back(name);
} else { } else {
// some editors (e.g vscode) perform file saving in two steps: truncate + write
// ignore the first event (truncate) with size 0 to prevent incorrect live reloading
auto fileInfo = QFileInfo(name);
if (fileInfo.isFile() && fileInfo.size() == 0) return;
emit this->filesChanged(); emit this->filesChanged();
} }
} }
@ -236,90 +243,6 @@ void EngineGeneration::onDirectoryChanged() {
} }
} }
void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) {
// We only want controllers that we can swap out if destroyed.
// This happens if the window owning the active controller dies.
auto* obj = dynamic_cast<QObject*>(controller);
if (!obj) {
qCWarning(logIncubator) << "Could not register incubation controller as it is not a QObject"
<< controller;
return;
}
QObject::connect(
obj,
&QObject::destroyed,
this,
&EngineGeneration::incubationControllerDestroyed,
Qt::UniqueConnection
);
this->incubationControllers.push_back(obj);
qCDebug(logIncubator) << "Registered incubation controller" << obj << "to generation" << this;
// This function can run during destruction.
if (this->engine == nullptr) return;
if (this->engine->incubationController() == &this->delayedIncubationController) {
this->assignIncubationController();
}
}
// Multiple controllers may be destroyed at once. Dynamic casts must be performed before working
// with any controllers. The QQmlIncubationController destructor will already have run by the
// point QObject::destroyed is called, so we can't cast to that.
void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) {
auto* obj = dynamic_cast<QObject*>(controller);
if (!obj) {
qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, "
"however only QObject controllers should be registered.";
}
QObject::disconnect(obj, nullptr, this, nullptr);
if (this->incubationControllers.removeOne(obj)) {
qCDebug(logIncubator) << "Deregistered incubation controller" << obj << "from" << this;
} else {
qCCritical(logIncubator) << "Failed to deregister incubation controller" << obj << "from"
<< this << "as it was not registered to begin with";
qCCritical(logIncubator) << "Current registered incuabation controllers"
<< this->incubationControllers;
}
// This function can run during destruction.
if (this->engine == nullptr) return;
if (this->engine->incubationController() == controller) {
qCDebug(logIncubator
) << "Destroyed incubation controller was currently active, reassigning from pool";
this->assignIncubationController();
}
}
void EngineGeneration::incubationControllerDestroyed() {
auto* sender = this->sender();
if (this->incubationControllers.removeAll(sender) != 0) {
qCDebug(logIncubator) << "Destroyed incubation controller" << sender << "deregistered from"
<< this;
} else {
qCCritical(logIncubator) << "Destroyed incubation controller" << sender
<< "was not registered, but its destruction was observed by" << this;
return;
}
// This function can run during destruction.
if (this->engine == nullptr) return;
if (dynamic_cast<QObject*>(this->engine->incubationController()) == sender) {
qCDebug(logIncubator
) << "Destroyed incubation controller was currently active, reassigning from pool";
this->assignIncubationController();
}
}
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) { void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) {
for (const auto& error: warnings) { for (const auto& error: warnings) {
const auto& url = error.url(); const auto& url = error.url();
@ -361,20 +284,23 @@ void EngineGeneration::exit(int code) {
this->destroy(); this->destroy();
} }
void EngineGeneration::assignIncubationController() { void EngineGeneration::trackWindowIncubationController(QQuickWindow* window) {
QQmlIncubationController* controller = nullptr; if (this->trackedWindows.contains(window)) return;
if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) { QObject::connect(window, &QObject::destroyed, this, &EngineGeneration::onTrackedWindowDestroyed);
controller = &this->delayedIncubationController; this->trackedWindows.append(window);
} else { this->updateIncubationMode();
controller = dynamic_cast<QQmlIncubationController*>(this->incubationControllers.first()); }
}
qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation" void EngineGeneration::onTrackedWindowDestroyed(QObject* object) {
<< this this->trackedWindows.removeAll(static_cast<QQuickWindow*>(object)); // NOLINT
<< "fallback:" << (controller == &this->delayedIncubationController); this->updateIncubationMode();
}
this->engine->setIncubationController(controller); void EngineGeneration::updateIncubationMode() {
// If we're in a situation with only hidden but tracked windows this might be wrong,
// but it seems to at least work.
this->incubationController.setIncubationMode(!this->trackedWindows.empty());
} }
EngineGeneration* EngineGeneration::currentGeneration() { EngineGeneration* EngineGeneration::currentGeneration() {

View file

@ -9,6 +9,7 @@
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qqmlerror.h> #include <qqmlerror.h>
#include <qqmlincubator.h> #include <qqmlincubator.h>
#include <qquickwindow.h>
#include <qtclasshelpermacros.h> #include <qtclasshelpermacros.h>
#include "incubator.hpp" #include "incubator.hpp"
@ -40,8 +41,7 @@ public:
void setWatchingFiles(bool watching); void setWatchingFiles(bool watching);
bool setExtraWatchedFiles(const QVector<QString>& files); bool setExtraWatchedFiles(const QVector<QString>& files);
void registerIncubationController(QQmlIncubationController* controller); void trackWindowIncubationController(QQuickWindow* window);
void deregisterIncubationController(QQmlIncubationController* controller);
// takes ownership // takes ownership
void registerExtension(const void* key, EngineGenerationExt* extension); void registerExtension(const void* key, EngineGenerationExt* extension);
@ -65,7 +65,7 @@ public:
QFileSystemWatcher* watcher = nullptr; QFileSystemWatcher* watcher = nullptr;
QVector<QString> deletedWatchedFiles; QVector<QString> deletedWatchedFiles;
QVector<QString> extraWatchedFiles; QVector<QString> extraWatchedFiles;
DelayedQmlIncubationController delayedIncubationController; QsIncubationController incubationController;
bool reloadComplete = false; bool reloadComplete = false;
QuickshellGlobal* qsgInstance = nullptr; QuickshellGlobal* qsgInstance = nullptr;
@ -75,6 +75,7 @@ public:
signals: signals:
void filesChanged(); void filesChanged();
void reloadFinished(); void reloadFinished();
void firePostReload();
public slots: public slots:
void quit(); void quit();
@ -83,13 +84,13 @@ public slots:
private slots: private slots:
void onFileChanged(const QString& name); void onFileChanged(const QString& name);
void onDirectoryChanged(); void onDirectoryChanged();
void incubationControllerDestroyed(); void onTrackedWindowDestroyed(QObject* object);
static void onEngineWarnings(const QList<QQmlError>& warnings); static void onEngineWarnings(const QList<QQmlError>& warnings);
private: private:
void postReload(); void postReload();
void assignIncubationController(); void updateIncubationMode();
QVector<QObject*> incubationControllers; QVector<QQuickWindow*> trackedWindows;
bool incubationControllersLocked = false; bool incubationControllersLocked = false;
QHash<const void*, EngineGenerationExt*> extensions; QHash<const void*, EngineGenerationExt*> extensions;

View file

@ -19,8 +19,7 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
if (splitIdx != -1) { if (splitIdx != -1) {
iconName = id.sliced(0, splitIdx); iconName = id.sliced(0, splitIdx);
path = id.sliced(splitIdx + 6); path = id.sliced(splitIdx + 6);
qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for" path = QString("/%1/%2").arg(path, iconName.sliced(iconName.lastIndexOf('/') + 1));
<< id;
} else { } else {
splitIdx = id.indexOf("?fallback="); splitIdx = id.indexOf("?fallback=");
if (splitIdx != -1) { if (splitIdx != -1) {
@ -32,7 +31,8 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
} }
auto icon = QIcon::fromTheme(iconName); auto icon = QIcon::fromTheme(iconName);
if (icon.isNull()) icon = QIcon::fromTheme(fallbackName); if (icon.isNull() && !fallbackName.isEmpty()) icon = QIcon::fromTheme(fallbackName);
if (icon.isNull() && !path.isEmpty()) icon = QPixmap(path);
auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100); auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100);
if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2); if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2);

View file

@ -22,8 +22,8 @@ class PixmapCacheIconEngine: public QIconEngine {
QIcon::Mode /*unused*/, QIcon::Mode /*unused*/,
QIcon::State /*unused*/ QIcon::State /*unused*/
) override { ) override {
qFatal( qFatal()
) << "Unexpected icon paint request bypassed pixmap method. Please report this as a bug."; << "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 { QPixmap pixmap(const QSize& size, QIcon::Mode /*unused*/, QIcon::State /*unused*/) override {

View file

@ -1,7 +1,16 @@
#include "incubator.hpp" #include "incubator.hpp"
#include <private/qsgrenderloop_p.h>
#include <qabstractanimation.h>
#include <qguiapplication.h>
#include <qlogging.h> #include <qlogging.h>
#include <qloggingcategory.h>
#include <qminmax.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qqmlincubator.h> #include <qqmlincubator.h>
#include <qscreen.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include "logcat.hpp" #include "logcat.hpp"
@ -15,3 +24,112 @@ void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) {
default: break; default: break;
} }
} }
void QsIncubationController::initLoop() {
auto* app = static_cast<QGuiApplication*>(QGuiApplication::instance()); // NOLINT
this->renderLoop = QSGRenderLoop::instance();
QObject::connect(
app,
&QGuiApplication::screenAdded,
this,
&QsIncubationController::updateIncubationTime
);
QObject::connect(
app,
&QGuiApplication::screenRemoved,
this,
&QsIncubationController::updateIncubationTime
);
this->updateIncubationTime();
QObject::connect(
this->renderLoop,
&QSGRenderLoop::timeToIncubate,
this,
&QsIncubationController::incubate
);
QAnimationDriver* animationDriver = this->renderLoop->animationDriver();
if (animationDriver) {
QObject::connect(
animationDriver,
&QAnimationDriver::stopped,
this,
&QsIncubationController::animationStopped
);
} else {
qCInfo(logIncubator) << "Render loop does not have animation driver, animationStopped cannot "
"be used to trigger incubation.";
}
}
void QsIncubationController::setIncubationMode(bool render) {
if (render == this->followRenderloop) return;
this->followRenderloop = render;
if (render) {
qCDebug(logIncubator) << "Incubation mode changed: render loop driven";
} else {
qCDebug(logIncubator) << "Incubation mode changed: event loop driven";
}
if (!render && this->incubatingObjectCount()) this->incubateLater();
}
void QsIncubationController::timerEvent(QTimerEvent* /*event*/) {
this->killTimer(this->timerId);
this->timerId = 0;
this->incubate();
}
void QsIncubationController::incubateLater() {
if (this->followRenderloop) {
if (this->timerId != 0) {
this->killTimer(this->timerId);
this->timerId = 0;
}
// Incubate again at the end of the event processing queue
QMetaObject::invokeMethod(this, &QsIncubationController::incubate, Qt::QueuedConnection);
} else if (this->timerId == 0) {
// Wait for a while before processing the next batch. Using a
// timer to avoid starvation of system events.
this->timerId = this->startTimer(this->incubationTime);
}
}
void QsIncubationController::incubate() {
if ((!this->followRenderloop || this->renderLoop) && this->incubatingObjectCount()) {
if (!this->followRenderloop) {
this->incubateFor(10);
if (this->incubatingObjectCount()) this->incubateLater();
} else if (this->renderLoop->interleaveIncubation()) {
this->incubateFor(this->incubationTime);
} else {
this->incubateFor(this->incubationTime * 2);
if (this->incubatingObjectCount()) this->incubateLater();
}
}
}
void QsIncubationController::animationStopped() { this->incubate(); }
void QsIncubationController::incubatingObjectCountChanged(int count) {
if (count
&& (!this->followRenderloop
|| (this->renderLoop && !this->renderLoop->interleaveIncubation())))
{
this->incubateLater();
}
}
void QsIncubationController::updateIncubationTime() {
auto* screen = QGuiApplication::primaryScreen();
if (!screen) return;
// 1/3 frame on primary screen
this->incubationTime = qMax(1, static_cast<int>(1000 / screen->refreshRate() / 3));
}

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <qobject.h> #include <qobject.h>
#include <qpointer.h>
#include <qqmlincubator.h> #include <qqmlincubator.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
@ -25,7 +26,37 @@ signals:
void failed(); void failed();
}; };
class DelayedQmlIncubationController: public QQmlIncubationController { class QSGRenderLoop;
// Do nothing.
// This ensures lazy loaders don't start blocking before onReload creates windows. class QsIncubationController
: public QObject
, public QQmlIncubationController {
Q_OBJECT
public:
void initLoop();
void setIncubationMode(bool render);
void incubateLater();
protected:
void timerEvent(QTimerEvent* event) override;
public slots:
void incubate();
void animationStopped();
void updateIncubationTime();
protected:
void incubatingObjectCountChanged(int count) override;
private:
// QPointer did not work with forward declarations prior to 6.7
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
QPointer<QSGRenderLoop> renderLoop = nullptr;
#else
QSGRenderLoop* renderLoop = nullptr;
#endif
int incubationTime = 0;
int timerId = 0;
bool followRenderloop = false;
}; };

View file

@ -3,12 +3,14 @@
#include <qdatastream.h> #include <qdatastream.h>
QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) { QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) {
stream << info.instanceId << info.configPath << info.shellId << info.launchTime << info.pid; stream << info.instanceId << info.configPath << info.shellId << info.launchTime << info.pid
<< info.display;
return stream; return stream;
} }
QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) { QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) {
stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime >> info.pid; stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime >> info.pid
>> info.display;
return stream; return stream;
} }

View file

@ -11,6 +11,7 @@ struct InstanceInfo {
QString shellId; QString shellId;
QDateTime launchTime; QDateTime launchTime;
pid_t pid = -1; pid_t pid = -1;
QString display;
static InstanceInfo CURRENT; // NOLINT static InstanceInfo CURRENT; // NOLINT
}; };
@ -34,6 +35,8 @@ namespace qs::crash {
struct CrashInfo { struct CrashInfo {
int logFd = -1; int logFd = -1;
int traceFd = -1;
int infoFd = -1;
static CrashInfo INSTANCE; // NOLINT static CrashInfo INSTANCE; // NOLINT
}; };

View file

@ -82,9 +82,6 @@
/// > Notably, @@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 /// > loading, meaning using it inside a LazyLoader will block similarly to not
/// > having a loader to start with. /// > having a loader to start with.
///
/// > [!WARNING] LazyLoaders do not start loading before the first window is created,
/// > meaning if you create all windows inside of lazy loaders, none of them will ever load.
class LazyLoader: public Reloadable { class LazyLoader: public Reloadable {
Q_OBJECT; Q_OBJECT;
/// The fully loaded item if the loader is @@loading or @@active, or `null` /// The fully loaded item if the loader is @@loading or @@active, or `null`

View file

@ -27,7 +27,10 @@
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h> #include <qtypes.h>
#include <sys/mman.h> #include <sys/mman.h>
#ifdef __linux__
#include <sys/sendfile.h> #include <sys/sendfile.h>
#include <sys/types.h>
#endif
#include "instanceinfo.hpp" #include "instanceinfo.hpp"
#include "logcat.hpp" #include "logcat.hpp"
@ -43,6 +46,57 @@ using namespace qt_logging_registry;
QS_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg); QS_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg);
namespace {
bool copyFileData(int sourceFd, int destFd, qint64 size) {
auto usize = static_cast<size_t>(size);
#ifdef __linux__
off_t offset = 0;
auto remaining = usize;
while (remaining > 0) {
auto r = sendfile(destFd, sourceFd, &offset, remaining);
if (r == -1) {
if (errno == EINTR) continue;
return false;
}
if (r == 0) break;
remaining -= static_cast<size_t>(r);
}
return true;
#else
std::array<char, 64 * 1024> buffer = {};
auto remaining = totalTarget;
while (remaining > 0) {
auto chunk = std::min(remaining, buffer.size());
auto r = ::read(sourceFd, buffer.data(), chunk);
if (r == -1) {
if (errno == EINTR) continue;
return false;
}
if (r == 0) break;
auto readBytes = static_cast<size_t>(r);
size_t written = 0;
while (written < readBytes) {
auto w = ::write(destFd, buffer.data() + written, readBytes - written);
if (w == -1) {
if (errno == EINTR) continue;
return false;
}
written += static_cast<size_t>(w);
}
remaining -= readBytes;
}
return true;
#endif
}
} // namespace
bool LogMessage::operator==(const LogMessage& other) const { bool LogMessage::operator==(const LogMessage& other) const {
// note: not including time // note: not including time
return this->type == other.type && this->category == other.category && this->body == other.body; return this->type == other.type && this->category == other.category && this->body == other.body;
@ -313,8 +367,12 @@ void ThreadLogging::init() {
if (logMfd != -1) { if (logMfd != -1) {
this->file = new QFile(); this->file = new QFile();
this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle);
this->fileStream.setDevice(this->file); if (this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle)) {
this->fileStream.setDevice(this->file);
} else {
qCCritical(logLogging) << "Failed to open early logging memfd.";
}
} }
if (dlogMfd != -1) { if (dlogMfd != -1) {
@ -322,14 +380,19 @@ void ThreadLogging::init() {
this->detailedFile = new QFile(); this->detailedFile = new QFile();
// buffered by WriteBuffer // buffered by WriteBuffer
this->detailedFile->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle); if (this->detailedFile
this->detailedWriter.setDevice(this->detailedFile); ->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle))
{
this->detailedWriter.setDevice(this->detailedFile);
if (!this->detailedWriter.writeHeader()) { if (!this->detailedWriter.writeHeader()) {
qCCritical(logLogging) << "Could not write header for detailed logs."; qCCritical(logLogging) << "Could not write header for detailed logs.";
this->detailedWriter.setDevice(nullptr); this->detailedWriter.setDevice(nullptr);
delete this->detailedFile; delete this->detailedFile;
this->detailedFile = nullptr; this->detailedFile = nullptr;
}
} else {
qCCritical(logLogging) << "Failed to open early detailed logging memfd.";
} }
} }
@ -352,7 +415,8 @@ void ThreadLogging::initFs() {
auto* runDir = QsPaths::instance()->instanceRunDir(); auto* runDir = QsPaths::instance()->instanceRunDir();
if (!runDir) { if (!runDir) {
qCCritical(logLogging qCCritical(
logLogging
) << "Could not start filesystem logging as the runtime directory could not be created."; ) << "Could not start filesystem logging as the runtime directory could not be created.";
return; return;
} }
@ -363,7 +427,8 @@ void ThreadLogging::initFs() {
auto* detailedFile = new QFile(detailedPath); auto* detailedFile = new QFile(detailedPath);
if (!file->open(QFile::ReadWrite | QFile::Truncate)) { if (!file->open(QFile::ReadWrite | QFile::Truncate)) {
qCCritical(logLogging qCCritical(
logLogging
) << "Could not start filesystem logger as the log file could not be created:" ) << "Could not start filesystem logger as the log file could not be created:"
<< path; << path;
delete file; delete file;
@ -374,13 +439,14 @@ void ThreadLogging::initFs() {
// buffered by WriteBuffer // buffered by WriteBuffer
if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) { if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) {
qCCritical(logLogging qCCritical(
logLogging
) << "Could not start detailed filesystem logger as the log file could not be created:" ) << "Could not start detailed filesystem logger as the log file could not be created:"
<< detailedPath; << detailedPath;
delete detailedFile; delete detailedFile;
detailedFile = nullptr; detailedFile = nullptr;
} else { } else {
auto lock = flock { struct flock lock = {
.l_type = F_WRLCK, .l_type = F_WRLCK,
.l_whence = SEEK_SET, .l_whence = SEEK_SET,
.l_start = 0, .l_start = 0,
@ -402,7 +468,11 @@ void ThreadLogging::initFs() {
auto* oldFile = this->file; auto* oldFile = this->file;
if (oldFile) { if (oldFile) {
oldFile->seek(0); oldFile->seek(0);
sendfile(file->handle(), oldFile->handle(), nullptr, oldFile->size());
if (!copyFileData(oldFile->handle(), file->handle(), oldFile->size())) {
qCritical(logLogging) << "Failed to copy log from memfd with error code " << errno
<< qt_error_string(errno);
}
} }
this->file = file; this->file = file;
@ -414,7 +484,10 @@ void ThreadLogging::initFs() {
auto* oldFile = this->detailedFile; auto* oldFile = this->detailedFile;
if (oldFile) { if (oldFile) {
oldFile->seek(0); oldFile->seek(0);
sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size()); if (!copyFileData(oldFile->handle(), detailedFile->handle(), oldFile->size())) {
qCritical(logLogging) << "Failed to copy detailed log from memfd with error code " << errno
<< qt_error_string(errno);
}
} }
crash::CrashInfo::INSTANCE.logFd = detailedFile->handle(); crash::CrashInfo::INSTANCE.logFd = detailedFile->handle();
@ -457,10 +530,14 @@ void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) {
this->fileStream << Qt::endl; this->fileStream << Qt::endl;
} }
if (this->detailedWriter.write(msg)) { if (!this->detailedWriter.write(msg) || (this->detailedFile && !this->detailedFile->flush())) {
this->detailedFile->flush(); this->detailedWriter.setDevice(nullptr);
} else if (this->detailedFile != nullptr) {
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs."; if (this->detailedFile) {
this->detailedFile->close();
this->detailedFile = nullptr;
qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs.";
}
} }
} }
@ -733,11 +810,11 @@ bool EncodedLogReader::readVarInt(quint32* slot) {
if (!this->reader.skip(1)) return false; if (!this->reader.skip(1)) return false;
*slot = qFromLittleEndian(n); *slot = qFromLittleEndian(n);
} else if ((bytes[1] != 0xff || bytes[2] != 0xff) && readLength >= 3) { } else if ((bytes[1] != 0xff || bytes[2] != 0xff) && readLength >= 3) {
auto n = *reinterpret_cast<quint16*>(bytes.data() + 1); auto n = *reinterpret_cast<quint16*>(bytes.data() + 1); // NOLINT
if (!this->reader.skip(3)) return false; if (!this->reader.skip(3)) return false;
*slot = qFromLittleEndian(n); *slot = qFromLittleEndian(n);
} else if (readLength == 7) { } else if (readLength == 7) {
auto n = *reinterpret_cast<quint32*>(bytes.data() + 3); auto n = *reinterpret_cast<quint32*>(bytes.data() + 3); // NOLINT
if (!this->reader.skip(7)) return false; if (!this->reader.skip(7)) return false;
*slot = qFromLittleEndian(n); *slot = qFromLittleEndian(n);
} else return false; } else return false;
@ -873,7 +950,7 @@ bool LogReader::continueReading() {
} }
void LogFollower::FcntlWaitThread::run() { void LogFollower::FcntlWaitThread::run() {
auto lock = flock { struct flock lock = {
.l_type = F_RDLCK, // won't block other read locks when we take it .l_type = F_RDLCK, // won't block other read locks when we take it
.l_whence = SEEK_SET, .l_whence = SEEK_SET,
.l_start = 0, .l_start = 0,

View file

@ -1,81 +1,14 @@
#include "model.hpp" #include "model.hpp"
#include <qabstractitemmodel.h> #include <qbytearray.h>
#include <qhash.h> #include <qhash.h>
#include <qnamespace.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 { QHash<int, QByteArray> UntypedObjectModel::roleNames() const {
return {{Qt::UserRole, "modelData"}}; return {{Qt::UserRole, "modelData"}};
} }
void UntypedObjectModel::insertObject(QObject* object, qsizetype index) {
auto iindex = index == -1 ? this->valuesList.length() : index;
emit this->objectInsertedPre(object, iindex);
auto intIndex = static_cast<qint32>(iindex);
this->beginInsertRows(QModelIndex(), intIndex, intIndex);
this->valuesList.insert(iindex, object);
this->endInsertRows();
emit this->valuesChanged();
emit this->objectInsertedPost(object, iindex);
}
void UntypedObjectModel::removeAt(qsizetype index) {
auto* object = this->valuesList.at(index);
emit this->objectRemovedPre(object, index);
auto intIndex = static_cast<qint32>(index);
this->beginRemoveRows(QModelIndex(), intIndex, intIndex);
this->valuesList.removeAt(index);
this->endRemoveRows();
emit this->valuesChanged();
emit this->objectRemovedPost(object, index);
}
bool UntypedObjectModel::removeObject(const QObject* object) {
auto index = this->valuesList.indexOf(object);
if (index == -1) return false;
this->removeAt(index);
return true;
}
void UntypedObjectModel::diffUpdate(const QVector<QObject*>& newValues) {
for (qsizetype i = 0; i < this->valuesList.length();) {
if (newValues.contains(this->valuesList.at(i))) i++;
else this->removeAt(i);
}
qsizetype oi = 0;
for (auto* object: newValues) {
if (this->valuesList.length() == oi || this->valuesList.at(oi) != object) {
this->insertObject(object, oi);
}
oi++;
}
}
qsizetype UntypedObjectModel::indexOf(QObject* object) { return this->valuesList.indexOf(object); }
UntypedObjectModel* UntypedObjectModel::emptyInstance() { UntypedObjectModel* UntypedObjectModel::emptyInstance() {
static auto* instance = new UntypedObjectModel(nullptr); // NOLINT static auto* instance = new ObjectModel<void>(nullptr);
return instance; return instance;
} }

View file

@ -2,7 +2,7 @@
#include <functional> #include <functional>
#include <bit> #include <QtCore/qtmetamacros.h>
#include <qabstractitemmodel.h> #include <qabstractitemmodel.h>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qobject.h> #include <qobject.h>
@ -49,14 +49,11 @@ class UntypedObjectModel: public QAbstractListModel {
public: public:
explicit UntypedObjectModel(QObject* parent): QAbstractListModel(parent) {} 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]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] QList<QObject*> values() const { return this->valuesList; }; [[nodiscard]] virtual QList<QObject*> values() = 0;
void removeAt(qsizetype index);
Q_INVOKABLE qsizetype indexOf(QObject* object); Q_INVOKABLE virtual qsizetype indexOf(QObject* object) const = 0;
static UntypedObjectModel* emptyInstance(); static UntypedObjectModel* emptyInstance();
@ -71,15 +68,6 @@ signals:
/// Sent immediately after an object is removed from the list. /// Sent immediately after an object is removed from the list.
void objectRemovedPost(QObject* object, qsizetype index); 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: private:
static qsizetype valuesCount(QQmlListProperty<QObject>* property); static qsizetype valuesCount(QQmlListProperty<QObject>* property);
static QObject* valueAt(QQmlListProperty<QObject>* property, qsizetype index); static QObject* valueAt(QQmlListProperty<QObject>* property, qsizetype index);
@ -90,14 +78,20 @@ class ObjectModel: public UntypedObjectModel {
public: public:
explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {} explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {}
[[nodiscard]] QVector<T*>& valueList() { return *std::bit_cast<QVector<T*>*>(&this->valuesList); } [[nodiscard]] const QList<T*>& valueList() const { return this->mValuesList; }
[[nodiscard]] QList<T*>& valueList() { return this->mValuesList; }
[[nodiscard]] const QVector<T*>& valueList() const {
return *std::bit_cast<const QVector<T*>*>(&this->valuesList);
}
void insertObject(T* object, qsizetype index = -1) { void insertObject(T* object, qsizetype index = -1) {
this->UntypedObjectModel::insertObject(object, index); auto iindex = index == -1 ? this->mValuesList.length() : index;
emit this->objectInsertedPre(object, iindex);
auto intIndex = static_cast<qint32>(iindex);
this->beginInsertRows(QModelIndex(), intIndex, intIndex);
this->mValuesList.insert(iindex, object);
this->endInsertRows();
emit this->valuesChanged();
emit this->objectInsertedPost(object, iindex);
} }
void insertObjectSorted(T* object, const std::function<bool(T*, T*)>& compare) { void insertObjectSorted(T* object, const std::function<bool(T*, T*)>& compare) {
@ -110,17 +104,71 @@ public:
} }
auto idx = iter - list.begin(); auto idx = iter - list.begin();
this->UntypedObjectModel::insertObject(object, idx); this->insertObject(object, idx);
} }
void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); } bool removeObject(const T* object) {
auto index = this->mValuesList.indexOf(object);
if (index == -1) return false;
this->removeAt(index);
return true;
}
void removeAt(qsizetype index) {
auto* object = this->mValuesList.at(index);
emit this->objectRemovedPre(object, index);
auto intIndex = static_cast<qint32>(index);
this->beginRemoveRows(QModelIndex(), intIndex, intIndex);
this->mValuesList.removeAt(index);
this->endRemoveRows();
emit this->valuesChanged();
emit this->objectRemovedPost(object, index);
}
// Assumes only one instance of a specific value // Assumes only one instance of a specific value
void diffUpdate(const QVector<T*>& newValues) { void diffUpdate(const QList<T*>& newValues) {
this->UntypedObjectModel::diffUpdate(*std::bit_cast<const QVector<QObject*>*>(&newValues)); for (qsizetype i = 0; i < this->mValuesList.length();) {
if (newValues.contains(this->mValuesList.at(i))) i++;
else this->removeAt(i);
}
qsizetype oi = 0;
for (auto* object: newValues) {
if (this->mValuesList.length() == oi || this->mValuesList.at(oi) != object) {
this->insertObject(object, oi);
}
oi++;
}
} }
static ObjectModel<T>* emptyInstance() { static ObjectModel<T>* emptyInstance() {
return static_cast<ObjectModel<T>*>(UntypedObjectModel::emptyInstance()); return static_cast<ObjectModel<T>*>(UntypedObjectModel::emptyInstance());
} }
[[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override {
if (parent != QModelIndex()) return 0;
return static_cast<qint32>(this->mValuesList.length());
}
[[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override {
if (role != Qt::UserRole) return QVariant();
// Values must be QObject derived, but we can't assert that here without breaking forward decls,
// so no static_cast.
return QVariant::fromValue(reinterpret_cast<QObject*>(this->mValuesList.at(index.row())));
}
qsizetype indexOf(QObject* object) const override {
return this->mValuesList.indexOf(reinterpret_cast<T*>(object));
}
[[nodiscard]] QList<QObject*> values() override {
return *reinterpret_cast<QList<QObject*>*>(&this->mValuesList);
}
private:
QList<T*> mValuesList;
}; };

View file

@ -21,7 +21,6 @@ headers = [
"model.hpp", "model.hpp",
"elapsedtimer.hpp", "elapsedtimer.hpp",
"desktopentry.hpp", "desktopentry.hpp",
"objectrepeater.hpp",
"qsmenu.hpp", "qsmenu.hpp",
"retainable.hpp", "retainable.hpp",
"popupanchor.hpp", "popupanchor.hpp",

View file

@ -1,190 +0,0 @@
#include "objectrepeater.hpp"
#include <utility>
#include <qabstractitemmodel.h>
#include <qcontainerfwd.h>
#include <qhash.h>
#include <qlogging.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlcontext.h>
#include <qqmlengine.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
QVariant ObjectRepeater::model() const { return this->mModel; }
void ObjectRepeater::setModel(QVariant model) {
if (model == this->mModel) return;
if (this->itemModel != nullptr) {
QObject::disconnect(this->itemModel, nullptr, this, nullptr);
}
this->mModel = std::move(model);
emit this->modelChanged();
this->reloadElements();
}
void ObjectRepeater::onModelDestroyed() {
this->mModel.clear();
this->itemModel = nullptr;
emit this->modelChanged();
this->reloadElements();
}
QQmlComponent* ObjectRepeater::delegate() const { return this->mDelegate; }
void ObjectRepeater::setDelegate(QQmlComponent* delegate) {
if (delegate == this->mDelegate) return;
if (this->mDelegate != nullptr) {
QObject::disconnect(this->mDelegate, nullptr, this, nullptr);
}
this->mDelegate = delegate;
if (delegate != nullptr) {
QObject::connect(
this->mDelegate,
&QObject::destroyed,
this,
&ObjectRepeater::onDelegateDestroyed
);
}
emit this->delegateChanged();
this->reloadElements();
}
void ObjectRepeater::onDelegateDestroyed() {
this->mDelegate = nullptr;
emit this->delegateChanged();
this->reloadElements();
}
void ObjectRepeater::reloadElements() {
for (auto i = this->valuesList.length() - 1; i >= 0; i--) {
this->removeComponent(i);
}
if (this->mDelegate == nullptr || !this->mModel.isValid()) return;
if (this->mModel.canConvert<QAbstractItemModel*>()) {
auto* model = this->mModel.value<QAbstractItemModel*>();
this->itemModel = model;
this->insertModelElements(model, 0, model->rowCount() - 1); // -1 is fine
// clang-format off
QObject::connect(model, &QObject::destroyed, this, &ObjectRepeater::onModelDestroyed);
QObject::connect(model, &QAbstractItemModel::rowsInserted, this, &ObjectRepeater::onModelRowsInserted);
QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &ObjectRepeater::onModelRowsRemoved);
QObject::connect(model, &QAbstractItemModel::rowsMoved, this, &ObjectRepeater::onModelRowsMoved);
QObject::connect(model, &QAbstractItemModel::modelAboutToBeReset, this, &ObjectRepeater::onModelAboutToBeReset);
// clang-format on
} else if (this->mModel.canConvert<QQmlListReference>()) {
auto values = this->mModel.value<QQmlListReference>();
auto len = values.count();
for (auto i = 0; i != len; i++) {
this->insertComponent(i, {{"modelData", QVariant::fromValue(values.at(i))}});
}
} else if (this->mModel.canConvert<QVector<QVariant>>()) {
auto values = this->mModel.value<QVector<QVariant>>();
for (auto& value: values) {
this->insertComponent(this->valuesList.length(), {{"modelData", value}});
}
} else {
qCritical() << this
<< "Cannot create components as the model is not compatible:" << this->mModel;
}
}
void ObjectRepeater::insertModelElements(QAbstractItemModel* model, int first, int last) {
auto roles = model->roleNames();
auto roleDataVec = QVector<QModelRoleData>();
for (auto id: roles.keys()) {
roleDataVec.push_back(QModelRoleData(id));
}
auto values = QModelRoleDataSpan(roleDataVec);
auto props = QVariantMap();
for (auto i = first; i != last + 1; i++) {
auto index = model->index(i, 0);
model->multiData(index, values);
for (auto [id, name]: roles.asKeyValueRange()) {
props.insert(name, *values.dataForRole(id));
}
this->insertComponent(i, props);
props.clear();
}
}
void ObjectRepeater::onModelRowsInserted(const QModelIndex& parent, int first, int last) {
if (parent != QModelIndex()) return;
this->insertModelElements(this->itemModel, first, last);
}
void ObjectRepeater::onModelRowsRemoved(const QModelIndex& parent, int first, int last) {
if (parent != QModelIndex()) return;
for (auto i = last; i != first - 1; i--) {
this->removeComponent(i);
}
}
void ObjectRepeater::onModelRowsMoved(
const QModelIndex& sourceParent,
int sourceStart,
int sourceEnd,
const QModelIndex& destParent,
int destStart
) {
auto hasSource = sourceParent != QModelIndex();
auto hasDest = destParent != QModelIndex();
if (!hasSource && !hasDest) return;
if (hasSource) {
this->onModelRowsRemoved(sourceParent, sourceStart, sourceEnd);
}
if (hasDest) {
this->onModelRowsInserted(destParent, destStart, destStart + (sourceEnd - sourceStart));
}
}
void ObjectRepeater::onModelAboutToBeReset() {
auto last = static_cast<int>(this->valuesList.length() - 1);
this->onModelRowsRemoved(QModelIndex(), 0, last); // -1 is fine
}
void ObjectRepeater::insertComponent(qsizetype index, const QVariantMap& properties) {
auto* context = QQmlEngine::contextForObject(this);
auto* instance = this->mDelegate->createWithInitialProperties(properties, context);
if (instance == nullptr) {
qWarning().noquote() << this->mDelegate->errorString();
qWarning() << this << "failed to create object for model data" << properties;
} else {
QQmlEngine::setObjectOwnership(instance, QQmlEngine::CppOwnership);
instance->setParent(this);
}
this->insertObject(instance, index);
}
void ObjectRepeater::removeComponent(qsizetype index) {
auto* instance = this->valuesList.at(index);
this->removeAt(index);
delete instance;
}

View file

@ -1,85 +0,0 @@
#pragma once
#include <qabstractitemmodel.h>
#include <qobject.h>
#include <qqmlcomponent.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
#include "model.hpp"
///! A Repeater / for loop / map for non Item derived objects.
/// > [!ERROR] Removed in favor of @@QtQml.Models.Instantiator
///
/// The ObjectRepeater creates instances of the provided delegate for every entry in the
/// given model, similarly to a @@QtQuick.Repeater but for non visual types.
class ObjectRepeater: public ObjectModel<QObject> {
Q_OBJECT;
/// The model providing data to the ObjectRepeater.
///
/// Currently accepted model types are `list<T>` lists, javascript arrays,
/// and [QAbstractListModel] derived models, though only one column will be repeated
/// from the latter.
///
/// Note: @@ObjectModel is a [QAbstractListModel] with a single column.
///
/// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html
Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged);
/// The delegate component to repeat.
///
/// The delegate is given the same properties as in a Repeater, except `index` which
/// is not currently implemented.
///
/// If the model is a `list<T>` or javascript array, a `modelData` property will be
/// exposed containing the entry from the model. If the model is a [QAbstractListModel],
/// the roles from the model will be exposed.
///
/// Note: @@ObjectModel has a single role named `modelData` for compatibility with normal lists.
///
/// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html
Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged);
Q_CLASSINFO("DefaultProperty", "delegate");
QML_ELEMENT;
QML_UNCREATABLE("ObjectRepeater has been removed in favor of QtQml.Models.Instantiator.");
public:
explicit ObjectRepeater(QObject* parent = nullptr): ObjectModel(parent) {}
[[nodiscard]] QVariant model() const;
void setModel(QVariant model);
[[nodiscard]] QQmlComponent* delegate() const;
void setDelegate(QQmlComponent* delegate);
signals:
void modelChanged();
void delegateChanged();
private slots:
void onDelegateDestroyed();
void onModelDestroyed();
void onModelRowsInserted(const QModelIndex& parent, int first, int last);
void onModelRowsRemoved(const QModelIndex& parent, int first, int last);
void onModelRowsMoved(
const QModelIndex& sourceParent,
int sourceStart,
int sourceEnd,
const QModelIndex& destParent,
int destStart
);
void onModelAboutToBeReset();
private:
void reloadElements();
void insertModelElements(QAbstractItemModel* model, int first, int last);
void insertComponent(qsizetype index, const QVariantMap& properties);
void removeComponent(qsizetype index);
QVariant mModel;
QAbstractItemModel* itemModel = nullptr;
QQmlComponent* mDelegate = nullptr;
};

View file

@ -27,12 +27,19 @@ QsPaths* QsPaths::instance() {
return instance; return instance;
} }
void QsPaths::init(QString shellId, QString pathId, QString dataOverride, QString stateOverride) { void QsPaths::init(
QString shellId,
QString pathId,
QString dataOverride,
QString stateOverride,
QString cacheOverride
) {
auto* instance = QsPaths::instance(); auto* instance = QsPaths::instance();
instance->shellId = std::move(shellId); instance->shellId = std::move(shellId);
instance->pathId = std::move(pathId); instance->pathId = std::move(pathId);
instance->shellDataOverride = std::move(dataOverride); instance->shellDataOverride = std::move(dataOverride);
instance->shellStateOverride = std::move(stateOverride); instance->shellStateOverride = std::move(stateOverride);
instance->shellCacheOverride = std::move(cacheOverride);
} }
QDir QsPaths::crashDir(const QString& id) { QDir QsPaths::crashDir(const QString& id) {
@ -135,13 +142,41 @@ QDir* QsPaths::instanceRunDir() {
else return &this->mInstanceRunDir; else return &this->mInstanceRunDir;
} }
QDir* QsPaths::shellVfsDir() {
if (this->shellVfsState == DirState::Unknown) {
if (auto* baseRunDir = this->baseRunDir()) {
this->mShellVfsDir = QDir(baseRunDir->filePath("vfs"));
this->mShellVfsDir = QDir(this->mShellVfsDir.filePath(this->shellId));
qCDebug(logPaths) << "Initialized runtime vfs path:" << this->mShellVfsDir.path();
if (!this->mShellVfsDir.mkpath(".")) {
qCCritical(logPaths) << "Could not create runtime vfs directory at"
<< this->mShellVfsDir.path();
this->shellVfsState = DirState::Failed;
} else {
this->shellVfsState = DirState::Ready;
}
} else {
qCCritical(logPaths) << "Could not create shell runtime vfs path as it was not possible to "
"create the base runtime path.";
this->shellVfsState = DirState::Failed;
}
}
if (this->shellVfsState == DirState::Failed) return nullptr;
else return &this->mShellVfsDir;
}
void QsPaths::linkRunDir() { void QsPaths::linkRunDir() {
if (auto* runDir = this->instanceRunDir()) { if (auto* runDir = this->instanceRunDir()) {
auto pidDir = QDir(this->baseRunDir()->filePath("by-pid")); auto pidDir = QDir(this->baseRunDir()->filePath("by-pid"));
auto* shellDir = this->shellRunDir(); auto* shellDir = this->shellRunDir();
if (!shellDir) { if (!shellDir) {
qCCritical(logPaths qCCritical(
logPaths
) << "Could not create by-id symlink as the shell runtime path could not be created."; ) << "Could not create by-id symlink as the shell runtime path could not be created.";
} else { } else {
auto shellPath = shellDir->filePath(runDir->dirName()); auto shellPath = shellDir->filePath(runDir->dirName());
@ -289,9 +324,16 @@ QDir QsPaths::shellStateDir() {
QDir QsPaths::shellCacheDir() { QDir QsPaths::shellCacheDir() {
if (this->shellCacheState == DirState::Unknown) { if (this->shellCacheState == DirState::Unknown) {
auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); QDir dir;
dir = QDir(dir.filePath("by-shell")); if (this->shellCacheOverride.isEmpty()) {
dir = QDir(dir.filePath(this->shellId)); dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
dir = QDir(dir.filePath("by-shell"));
dir = QDir(dir.filePath(this->shellId));
} else {
auto basedir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
dir = QDir(this->shellCacheOverride.replace("$BASE", basedir));
}
this->mShellCacheDir = dir; this->mShellCacheDir = dir;
qCDebug(logPaths) << "Initialized cache path:" << dir.path(); qCDebug(logPaths) << "Initialized cache path:" << dir.path();
@ -319,7 +361,7 @@ void QsPaths::createLock() {
return; return;
} }
auto lock = flock { struct flock lock = {
.l_type = F_WRLCK, .l_type = F_WRLCK,
.l_whence = SEEK_SET, .l_whence = SEEK_SET,
.l_start = 0, .l_start = 0,
@ -337,7 +379,8 @@ void QsPaths::createLock() {
qCDebug(logPaths) << "Created instance lock at" << path; qCDebug(logPaths) << "Created instance lock at" << path;
} }
} else { } else {
qCCritical(logPaths qCCritical(
logPaths
) << "Could not create instance lock, as the instance runtime directory could not be created."; ) << "Could not create instance lock, as the instance runtime directory could not be created.";
} }
} }
@ -346,7 +389,7 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD
auto file = QFile(QDir(path).filePath("instance.lock")); auto file = QFile(QDir(path).filePath("instance.lock"));
if (!file.open(QFile::ReadOnly)) return false; if (!file.open(QFile::ReadOnly)) return false;
auto lock = flock { struct flock lock = {
.l_type = F_WRLCK, .l_type = F_WRLCK,
.l_whence = SEEK_SET, .l_whence = SEEK_SET,
.l_start = 0, .l_start = 0,
@ -370,7 +413,7 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD
} }
QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>> QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>>
QsPaths::collectInstances(const QString& path) { QsPaths::collectInstances(const QString& path, const QString& display) {
qCDebug(logPaths) << "Collecting instances from" << path; qCDebug(logPaths) << "Collecting instances from" << path;
auto liveInstances = QVector<InstanceLockInfo>(); auto liveInstances = QVector<InstanceLockInfo>();
auto deadInstances = QVector<InstanceLockInfo>(); auto deadInstances = QVector<InstanceLockInfo>();
@ -384,6 +427,11 @@ QsPaths::collectInstances(const QString& path) {
qCDebug(logPaths).nospace() << "Found instance " << info.instance.instanceId << " (pid " qCDebug(logPaths).nospace() << "Found instance " << info.instance.instanceId << " (pid "
<< info.pid << ") at " << path; << info.pid << ") at " << path;
if (!display.isEmpty() && info.instance.display != display) {
qCDebug(logPaths) << "Skipped instance with mismatched display at" << path;
continue;
}
if (info.pid == -1) { if (info.pid == -1) {
deadInstances.push_back(info); deadInstances.push_back(info);
} else { } else {

View file

@ -17,17 +17,24 @@ QDataStream& operator>>(QDataStream& stream, InstanceLockInfo& info);
class QsPaths { class QsPaths {
public: public:
static QsPaths* instance(); static QsPaths* instance();
static void init(QString shellId, QString pathId, QString dataOverride, QString stateOverride); static void init(
QString shellId,
QString pathId,
QString dataOverride,
QString stateOverride,
QString cacheOverride
);
static QDir crashDir(const QString& id); static QDir crashDir(const QString& id);
static QString basePath(const QString& id); static QString basePath(const QString& id);
static QString ipcPath(const QString& id); static QString ipcPath(const QString& id);
static bool static bool
checkLock(const QString& path, InstanceLockInfo* info = nullptr, bool allowDead = false); checkLock(const QString& path, InstanceLockInfo* info = nullptr, bool allowDead = false);
static QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>> static QPair<QVector<InstanceLockInfo>, QVector<InstanceLockInfo>>
collectInstances(const QString& path); collectInstances(const QString& path, const QString& display);
QDir* baseRunDir(); QDir* baseRunDir();
QDir* shellRunDir(); QDir* shellRunDir();
QDir* shellVfsDir();
QDir* instanceRunDir(); QDir* instanceRunDir();
void linkRunDir(); void linkRunDir();
void linkPathDir(); void linkPathDir();
@ -48,9 +55,11 @@ private:
QString pathId; QString pathId;
QDir mBaseRunDir; QDir mBaseRunDir;
QDir mShellRunDir; QDir mShellRunDir;
QDir mShellVfsDir;
QDir mInstanceRunDir; QDir mInstanceRunDir;
DirState baseRunState = DirState::Unknown; DirState baseRunState = DirState::Unknown;
DirState shellRunState = DirState::Unknown; DirState shellRunState = DirState::Unknown;
DirState shellVfsState = DirState::Unknown;
DirState instanceRunState = DirState::Unknown; DirState instanceRunState = DirState::Unknown;
QDir mShellDataDir; QDir mShellDataDir;
@ -62,4 +71,5 @@ private:
QString shellDataOverride; QString shellDataOverride;
QString shellStateOverride; QString shellStateOverride;
QString shellCacheOverride;
}; };

View file

@ -28,7 +28,7 @@ void PopupAnchor::markClean() { this->lastState = this->state; }
void PopupAnchor::markDirty() { this->lastState.reset(); } void PopupAnchor::markDirty() { this->lastState.reset(); }
QWindow* PopupAnchor::backingWindow() const { QWindow* PopupAnchor::backingWindow() const {
return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr; return this->bProxyWindow ? this->bProxyWindow->backingWindow() : nullptr;
} }
void PopupAnchor::setWindowInternal(QObject* window) { void PopupAnchor::setWindowInternal(QObject* window) {
@ -36,14 +36,14 @@ void PopupAnchor::setWindowInternal(QObject* window) {
if (this->mWindow) { if (this->mWindow) {
QObject::disconnect(this->mWindow, nullptr, this, nullptr); QObject::disconnect(this->mWindow, nullptr, this, nullptr);
QObject::disconnect(this->mProxyWindow, nullptr, this, nullptr); QObject::disconnect(this->bProxyWindow, nullptr, this, nullptr);
} }
if (window) { if (window) {
if (auto* proxy = qobject_cast<ProxyWindowBase*>(window)) { if (auto* proxy = qobject_cast<ProxyWindowBase*>(window)) {
this->mProxyWindow = proxy; this->bProxyWindow = proxy;
} else if (auto* interface = qobject_cast<WindowInterface*>(window)) { } else if (auto* interface = qobject_cast<WindowInterface*>(window)) {
this->mProxyWindow = interface->proxyWindow(); this->bProxyWindow = interface->proxyWindow();
} else { } else {
qWarning() << "Tried to set popup anchor window to" << window qWarning() << "Tried to set popup anchor window to" << window
<< "which is not a quickshell window."; << "which is not a quickshell window.";
@ -55,7 +55,7 @@ void PopupAnchor::setWindowInternal(QObject* window) {
QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed); QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed);
QObject::connect( QObject::connect(
this->mProxyWindow, this->bProxyWindow,
&ProxyWindowBase::backerVisibilityChanged, &ProxyWindowBase::backerVisibilityChanged,
this, this,
&PopupAnchor::backingWindowVisibilityChanged &PopupAnchor::backingWindowVisibilityChanged
@ -70,7 +70,7 @@ void PopupAnchor::setWindowInternal(QObject* window) {
setnull: setnull:
if (this->mWindow) { if (this->mWindow) {
this->mWindow = nullptr; this->mWindow = nullptr;
this->mProxyWindow = nullptr; this->bProxyWindow = nullptr;
emit this->windowChanged(); emit this->windowChanged();
emit this->backingWindowVisibilityChanged(); emit this->backingWindowVisibilityChanged();
@ -100,7 +100,7 @@ void PopupAnchor::setItem(QQuickItem* item) {
void PopupAnchor::onWindowDestroyed() { void PopupAnchor::onWindowDestroyed() {
this->mWindow = nullptr; this->mWindow = nullptr;
this->mProxyWindow = nullptr; this->bProxyWindow = nullptr;
emit this->windowChanged(); emit this->windowChanged();
emit this->backingWindowVisibilityChanged(); emit this->backingWindowVisibilityChanged();
} }
@ -186,11 +186,11 @@ void PopupAnchor::updatePlacement(const QPoint& anchorpoint, const QSize& size)
} }
void PopupAnchor::updateAnchor() { void PopupAnchor::updateAnchor() {
if (this->mItem && this->mProxyWindow) { if (this->mItem && this->bProxyWindow) {
auto baseRect = auto baseRect =
this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect(); this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect();
auto rect = this->mProxyWindow->contentItem()->mapFromItem( auto rect = this->bProxyWindow->contentItem()->mapFromItem(
this->mItem, this->mItem,
baseRect.marginsRemoved(this->mMargins.qmargins()) baseRect.marginsRemoved(this->mMargins.qmargins())
); );

View file

@ -6,6 +6,7 @@
#include <qnamespace.h> #include <qnamespace.h>
#include <qobject.h> #include <qobject.h>
#include <qpoint.h> #include <qpoint.h>
#include <qproperty.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qquickitem.h> #include <qquickitem.h>
#include <qsize.h> #include <qsize.h>
@ -139,7 +140,9 @@ public:
void markDirty(); void markDirty();
[[nodiscard]] QObject* window() const { return this->mWindow; } [[nodiscard]] QObject* window() const { return this->mWindow; }
[[nodiscard]] ProxyWindowBase* proxyWindow() const { return this->mProxyWindow; } [[nodiscard]] QBindable<ProxyWindowBase*> bindableProxyWindow() const {
return &this->bProxyWindow;
}
[[nodiscard]] QWindow* backingWindow() const; [[nodiscard]] QWindow* backingWindow() const;
void setWindowInternal(QObject* window); void setWindowInternal(QObject* window);
void setWindow(QObject* window); void setWindow(QObject* window);
@ -193,11 +196,12 @@ private slots:
private: private:
QObject* mWindow = nullptr; QObject* mWindow = nullptr;
QQuickItem* mItem = nullptr; QQuickItem* mItem = nullptr;
ProxyWindowBase* mProxyWindow = nullptr;
PopupAnchorState state; PopupAnchorState state;
Box mUserRect; Box mUserRect;
Margins mMargins; Margins mMargins;
std::optional<PopupAnchorState> lastState; std::optional<PopupAnchorState> lastState;
Q_OBJECT_BINDABLE_PROPERTY(PopupAnchor, ProxyWindowBase*, bProxyWindow);
}; };
class PopupPositioner { class PopupPositioner {

View file

@ -29,6 +29,7 @@
#include "paths.hpp" #include "paths.hpp"
#include "qmlscreen.hpp" #include "qmlscreen.hpp"
#include "rootwrapper.hpp" #include "rootwrapper.hpp"
#include "scanenv.hpp"
QuickshellSettings::QuickshellSettings() { QuickshellSettings::QuickshellSettings() {
QObject::connect( QObject::connect(
@ -210,10 +211,22 @@ void QuickshellGlobal::onClipboardChanged(QClipboard::Mode mode) {
if (mode == QClipboard::Clipboard) emit this->clipboardTextChanged(); if (mode == QClipboard::Clipboard) emit this->clipboardTextChanged();
} }
QString QuickshellGlobal::configDir() const { QString QuickshellGlobal::shellDir() const {
return EngineGeneration::findObjectGeneration(this)->rootPath.path(); return EngineGeneration::findObjectGeneration(this)->rootPath.path();
} }
QString QuickshellGlobal::configDir() const {
qWarning() << "Quickshell.configDir is deprecated and may be removed in a future release. Use "
"Quickshell.shellDir.";
return this->shellDir();
}
QString QuickshellGlobal::shellRoot() const {
qWarning() << "Quickshell.shellRoot is deprecated and may be removed in a future release. Use "
"Quickshell.shellDir.";
return this->shellDir();
}
QString QuickshellGlobal::dataDir() const { // NOLINT QString QuickshellGlobal::dataDir() const { // NOLINT
return QsPaths::instance()->shellDataDir().path(); return QsPaths::instance()->shellDataDir().path();
} }
@ -226,8 +239,14 @@ QString QuickshellGlobal::cacheDir() const { // NOLINT
return QsPaths::instance()->shellCacheDir().path(); return QsPaths::instance()->shellCacheDir().path();
} }
QString QuickshellGlobal::shellPath(const QString& path) const {
return this->shellDir() % '/' % path;
}
QString QuickshellGlobal::configPath(const QString& path) const { QString QuickshellGlobal::configPath(const QString& path) const {
return this->configDir() % '/' % path; qWarning() << "Quickshell.configPath() is deprecated and may be removed in a future release. Use "
"Quickshell.shellPath().";
return this->shellPath(path);
} }
QString QuickshellGlobal::dataPath(const QString& path) const { QString QuickshellGlobal::dataPath(const QString& path) const {
@ -295,6 +314,16 @@ QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback)
return IconImageProvider::requestString(icon, "", fallback); return IconImageProvider::requestString(icon, "", fallback);
} }
bool QuickshellGlobal::hasThemeIcon(const QString& icon) { return QIcon::hasThemeIcon(icon); }
bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor, const QStringList& features) {
return qs::scan::env::PreprocEnv::hasVersion(major, minor, features);
}
bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor) {
return QuickshellGlobal::hasVersion(major, minor, QStringList());
}
QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) { QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) {
auto* qsg = new QuickshellGlobal(); auto* qsg = new QuickshellGlobal();
auto* generation = EngineGeneration::findEngineGeneration(engine); auto* generation = EngineGeneration::findEngineGeneration(engine);

View file

@ -108,9 +108,11 @@ class QuickshellGlobal: public QObject {
/// ///
/// The root directory is the folder containing the entrypoint to your shell, often referred /// The root directory is the folder containing the entrypoint to your shell, often referred
/// to as `shell.qml`. /// to as `shell.qml`.
Q_PROPERTY(QString shellDir READ shellDir CONSTANT);
/// > [!WARNING] Deprecated: Renamed to @@shellDir for clarity.
Q_PROPERTY(QString configDir READ configDir CONSTANT); Q_PROPERTY(QString configDir READ configDir CONSTANT);
/// > [!WARNING] Deprecated: Returns @@configDir. /// > [!WARNING] Deprecated: Renamed to @@shellDir for consistency.
Q_PROPERTY(QString shellRoot READ configDir CONSTANT); Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT);
/// Quickshell's working directory. Defaults to whereever quickshell was launched from. /// Quickshell's working directory. Defaults to whereever quickshell was launched from.
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged); Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged);
/// If true then the configuration will be reloaded whenever any files change. /// If true then the configuration will be reloaded whenever any files change.
@ -125,18 +127,21 @@ class QuickshellGlobal: public QObject {
/// Usually `~/.local/share/quickshell/by-shell/<shell-id>` /// Usually `~/.local/share/quickshell/by-shell/<shell-id>`
/// ///
/// Can be overridden using `//@ pragma DataDir $BASE/path` in the root qml file, where `$BASE` /// Can be overridden using `//@ pragma DataDir $BASE/path` in the root qml file, where `$BASE`
/// corrosponds to `$XDG_DATA_HOME` (usually `~/.local/share`). /// corresponds to `$XDG_DATA_HOME` (usually `~/.local/share`).
Q_PROPERTY(QString dataDir READ dataDir CONSTANT); Q_PROPERTY(QString dataDir READ dataDir CONSTANT);
/// The per-shell state directory. /// The per-shell state directory.
/// ///
/// Usually `~/.local/state/quickshell/by-shell/<shell-id>` /// Usually `~/.local/state/quickshell/by-shell/<shell-id>`
/// ///
/// Can be overridden using `//@ pragma StateDir $BASE/path` in the root qml file, where `$BASE` /// Can be overridden using `//@ pragma StateDir $BASE/path` in the root qml file, where `$BASE`
/// corrosponds to `$XDG_STATE_HOME` (usually `~/.local/state`). /// corresponds to `$XDG_STATE_HOME` (usually `~/.local/state`).
Q_PROPERTY(QString stateDir READ stateDir CONSTANT); Q_PROPERTY(QString stateDir READ stateDir CONSTANT);
/// The per-shell cache directory. /// The per-shell cache directory.
/// ///
/// Usually `~/.cache/quickshell/by-shell/<shell-id>` /// Usually `~/.cache/quickshell/by-shell/<shell-id>`
///
/// Can be overridden using `//@ pragma CacheDir $BASE/path` in the root qml file, where `$BASE`
/// corresponds to `$XDG_CACHE_HOME` (usually `~/.cache`).
Q_PROPERTY(QString cacheDir READ cacheDir CONSTANT); Q_PROPERTY(QString cacheDir READ cacheDir CONSTANT);
// clang-format on // clang-format on
QML_SINGLETON; QML_SINGLETON;
@ -197,7 +202,11 @@ public:
/// Setting the `fallback` parameter of `iconPath` will attempt to load the fallback /// Setting the `fallback` parameter of `iconPath` will attempt to load the fallback
/// icon if the requested one could not be loaded. /// icon if the requested one could not be loaded.
Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback); Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback);
/// Check if specified icon has an available icon in your icon theme
Q_INVOKABLE static bool hasThemeIcon(const QString& icon);
/// Equivalent to `${Quickshell.configDir}/${path}` /// Equivalent to `${Quickshell.configDir}/${path}`
Q_INVOKABLE [[nodiscard]] QString shellPath(const QString& path) const;
/// > [!WARNING] Deprecated: Renamed to @@shellPath() for clarity.
Q_INVOKABLE [[nodiscard]] QString configPath(const QString& path) const; Q_INVOKABLE [[nodiscard]] QString configPath(const QString& path) const;
/// Equivalent to `${Quickshell.dataDir}/${path}` /// Equivalent to `${Quickshell.dataDir}/${path}`
Q_INVOKABLE [[nodiscard]] QString dataPath(const QString& path) const; Q_INVOKABLE [[nodiscard]] QString dataPath(const QString& path) const;
@ -210,11 +219,28 @@ public:
/// ///
/// The popup can also be blocked by setting `QS_NO_RELOAD_POPUP=1`. /// The popup can also be blocked by setting `QS_NO_RELOAD_POPUP=1`.
Q_INVOKABLE void inhibitReloadPopup() { this->mInhibitReloadPopup = true; } Q_INVOKABLE void inhibitReloadPopup() { this->mInhibitReloadPopup = true; }
/// Check if Quickshell's version is at least `major.minor` and the listed
/// unreleased features are available. If Quickshell is newer than the given version
/// it is assumed that all unreleased features are present. The unreleased feature list
/// may be omitted.
///
/// > [!NOTE] You can feature gate code blocks using Quickshell's preprocessor which
/// > has the same function available.
/// >
/// > ```qml
/// > //@ if hasVersion(0, 3, ["feature"])
/// > ...
/// > //@ endif
/// > ```
Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor, const QStringList& features);
Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor);
void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; } void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; }
[[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; } [[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; }
[[nodiscard]] QString shellDir() const;
[[nodiscard]] QString configDir() const; [[nodiscard]] QString configDir() const;
[[nodiscard]] QString shellRoot() const;
[[nodiscard]] QString workingDirectory() const; [[nodiscard]] QString workingDirectory() const;
void setWorkingDirectory(QString workingDirectory); void setWorkingDirectory(QString workingDirectory);

View file

@ -46,8 +46,8 @@ class QsMenuHandle: public QObject {
public: public:
explicit QsMenuHandle(QObject* parent): QObject(parent) {} explicit QsMenuHandle(QObject* parent): QObject(parent) {}
virtual void refHandle() {}; virtual void refHandle() {}
virtual void unrefHandle() {}; virtual void unrefHandle() {}
[[nodiscard]] virtual QsMenuEntry* menu() = 0; [[nodiscard]] virtual QsMenuEntry* menu() = 0;

View file

@ -1,13 +1,13 @@
#include "region.hpp" #include "region.hpp"
#include <cmath> #include <cmath>
#include <qlist.h>
#include <qobject.h> #include <qobject.h>
#include <qpoint.h> #include <qpoint.h>
#include <qqmllist.h> #include <qqmllist.h>
#include <qquickitem.h> #include <qquickitem.h>
#include <qregion.h> #include <qregion.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h>
#include <qvectornd.h> #include <qvectornd.h>
PendingRegion::PendingRegion(QObject* parent): QObject(parent) { PendingRegion::PendingRegion(QObject* parent): QObject(parent) {
@ -19,7 +19,6 @@ PendingRegion::PendingRegion(QObject* parent): QObject(parent) {
QObject::connect(this, &PendingRegion::widthChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::widthChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::heightChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::heightChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::childrenChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::childrenChanged, this, &PendingRegion::changed);
QObject::connect(this, &PendingRegion::regionsChanged, this, &PendingRegion::childrenChanged);
} }
void PendingRegion::setItem(QQuickItem* item) { void PendingRegion::setItem(QQuickItem* item) {
@ -42,33 +41,21 @@ void PendingRegion::setItem(QQuickItem* item) {
emit this->itemChanged(); emit this->itemChanged();
} }
void PendingRegion::onItemDestroyed() { void PendingRegion::onItemDestroyed() { this->mItem = nullptr; }
this->mItem = nullptr;
emit this->itemChanged();
}
void PendingRegion::onChildDestroyed() { void PendingRegion::onChildDestroyed() { this->mRegions.removeAll(this->sender()); }
this->mRegions.removeAll(this->sender());
emit this->regionsChanged();
}
const QList<PendingRegion*>& PendingRegion::regions() const { return this->mRegions; } QQmlListProperty<PendingRegion> PendingRegion::regions() {
return QQmlListProperty<PendingRegion>(
void PendingRegion::setRegions(const QList<PendingRegion*>& regions) { this,
if (regions == this->mRegions) return; nullptr,
&PendingRegion::regionsAppend,
for (auto* region: this->mRegions) { &PendingRegion::regionsCount,
QObject::disconnect(region, nullptr, this, nullptr); &PendingRegion::regionAt,
} &PendingRegion::regionsClear,
&PendingRegion::regionsReplace,
this->mRegions = regions; &PendingRegion::regionsRemoveLast
);
for (auto* region: regions) {
QObject::connect(region, &QObject::destroyed, this, &PendingRegion::onChildDestroyed);
QObject::connect(region, &PendingRegion::changed, this, &PendingRegion::childrenChanged);
}
emit this->regionsChanged();
} }
bool PendingRegion::empty() const { bool PendingRegion::empty() const {
@ -130,3 +117,58 @@ QRegion PendingRegion::applyTo(const QRect& rect) const {
return this->applyTo(baseRegion); return this->applyTo(baseRegion);
} }
} }
void PendingRegion::regionsAppend(QQmlListProperty<PendingRegion>* prop, PendingRegion* region) {
auto* self = static_cast<PendingRegion*>(prop->object); // NOLINT
if (!region) return;
QObject::connect(region, &QObject::destroyed, self, &PendingRegion::onChildDestroyed);
QObject::connect(region, &PendingRegion::changed, self, &PendingRegion::childrenChanged);
self->mRegions.append(region);
emit self->childrenChanged();
}
PendingRegion* PendingRegion::regionAt(QQmlListProperty<PendingRegion>* prop, qsizetype i) {
return static_cast<PendingRegion*>(prop->object)->mRegions.at(i); // NOLINT
}
void PendingRegion::regionsClear(QQmlListProperty<PendingRegion>* prop) {
auto* self = static_cast<PendingRegion*>(prop->object); // NOLINT
for (auto* region: self->mRegions) {
QObject::disconnect(region, nullptr, self, nullptr);
}
self->mRegions.clear(); // NOLINT
emit self->childrenChanged();
}
qsizetype PendingRegion::regionsCount(QQmlListProperty<PendingRegion>* prop) {
return static_cast<PendingRegion*>(prop->object)->mRegions.length(); // NOLINT
}
void PendingRegion::regionsRemoveLast(QQmlListProperty<PendingRegion>* prop) {
auto* self = static_cast<PendingRegion*>(prop->object); // NOLINT
auto* last = self->mRegions.last();
if (last != nullptr) QObject::disconnect(last, nullptr, self, nullptr);
self->mRegions.removeLast();
emit self->childrenChanged();
}
void PendingRegion::regionsReplace(
QQmlListProperty<PendingRegion>* prop,
qsizetype i,
PendingRegion* region
) {
auto* self = static_cast<PendingRegion*>(prop->object); // NOLINT
auto* old = self->mRegions.at(i);
if (old != nullptr) QObject::disconnect(old, nullptr, self, nullptr);
self->mRegions.replace(i, region);
emit self->childrenChanged();
}

View file

@ -82,7 +82,7 @@ class PendingRegion: public QObject {
/// } /// }
/// } /// }
/// ``` /// ```
Q_PROPERTY(QList<PendingRegion*> regions READ regions WRITE setRegions NOTIFY regionsChanged); Q_PROPERTY(QQmlListProperty<PendingRegion> regions READ regions);
Q_CLASSINFO("DefaultProperty", "regions"); Q_CLASSINFO("DefaultProperty", "regions");
QML_NAMED_ELEMENT(Region); QML_NAMED_ELEMENT(Region);
@ -91,8 +91,7 @@ public:
void setItem(QQuickItem* item); void setItem(QQuickItem* item);
[[nodiscard]] const QList<PendingRegion*>& regions() const; QQmlListProperty<PendingRegion> regions();
void setRegions(const QList<PendingRegion*>& regions);
[[nodiscard]] bool empty() const; [[nodiscard]] bool empty() const;
[[nodiscard]] QRegion build() const; [[nodiscard]] QRegion build() const;
@ -110,7 +109,6 @@ signals:
void yChanged(); void yChanged();
void widthChanged(); void widthChanged();
void heightChanged(); void heightChanged();
void regionsChanged();
void childrenChanged(); void childrenChanged();
/// Triggered when the region's geometry changes. /// Triggered when the region's geometry changes.
@ -124,6 +122,14 @@ private slots:
void onChildDestroyed(); void onChildDestroyed();
private: private:
static void regionsAppend(QQmlListProperty<PendingRegion>* prop, PendingRegion* region);
static PendingRegion* regionAt(QQmlListProperty<PendingRegion>* prop, qsizetype i);
static void regionsClear(QQmlListProperty<PendingRegion>* prop);
static qsizetype regionsCount(QQmlListProperty<PendingRegion>* prop);
static void regionsRemoveLast(QQmlListProperty<PendingRegion>* prop);
static void
regionsReplace(QQmlListProperty<PendingRegion>* prop, qsizetype i, PendingRegion* region);
QQuickItem* mItem = nullptr; QQuickItem* mItem = nullptr;
qint32 mX = 0; qint32 mX = 0;

View file

@ -129,14 +129,18 @@ QObject* Reloadable::getChildByReloadId(QObject* parent, const QString& reloadId
void PostReloadHook::componentComplete() { void PostReloadHook::componentComplete() {
auto* engineGeneration = EngineGeneration::findObjectGeneration(this); auto* engineGeneration = EngineGeneration::findObjectGeneration(this);
if (!engineGeneration || engineGeneration->reloadComplete) this->postReload(); if (!engineGeneration || engineGeneration->reloadComplete) this->postReload();
else {
// disconnected by EngineGeneration::postReload
QObject::connect(
engineGeneration,
&EngineGeneration::firePostReload,
this,
&PostReloadHook::postReload
);
}
} }
void PostReloadHook::postReload() { void PostReloadHook::postReload() {
this->isPostReload = true; this->isPostReload = true;
this->onPostReload(); this->onPostReload();
} }
void PostReloadHook::postReloadTree(QObject* root) {
for (auto* child: root->children()) PostReloadHook::postReloadTree(child);
if (auto* self = dynamic_cast<PostReloadHook*>(root)) self->postReload();
}

View file

@ -57,7 +57,7 @@ public:
void reload(QObject* oldInstance = nullptr); void reload(QObject* oldInstance = nullptr);
void classBegin() override {}; void classBegin() override {}
void componentComplete() override; void componentComplete() override;
// Reload objects in the parent->child graph recursively. // Reload objects in the parent->child graph recursively.
@ -122,15 +122,19 @@ private:
class PostReloadHook class PostReloadHook
: public QObject : public QObject
, public QQmlParserStatus { , public QQmlParserStatus {
Q_OBJECT;
QML_ANONYMOUS;
Q_INTERFACES(QQmlParserStatus);
public: public:
PostReloadHook(QObject* parent = nullptr): QObject(parent) {} PostReloadHook(QObject* parent = nullptr): QObject(parent) {}
void classBegin() override {} void classBegin() override {}
void componentComplete() override; void componentComplete() override;
void postReload();
virtual void onPostReload() = 0; virtual void onPostReload() = 0;
static void postReloadTree(QObject* root); public slots:
void postReload();
protected: protected:
bool isPostReload = false; bool isPostReload = false;

View file

@ -4,6 +4,7 @@
#include <qdir.h> #include <qdir.h>
#include <qfileinfo.h> #include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qlogging.h> #include <qlogging.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlcomponent.h> #include <qqmlcomponent.h>
@ -18,15 +19,26 @@
#include "instanceinfo.hpp" #include "instanceinfo.hpp"
#include "qmlglobal.hpp" #include "qmlglobal.hpp"
#include "scan.hpp" #include "scan.hpp"
#include "toolsupport.hpp"
RootWrapper::RootWrapper(QString rootPath, QString shellId) RootWrapper::RootWrapper(QString rootPath, QString shellId)
: QObject(nullptr) : QObject(nullptr)
, rootPath(std::move(rootPath)) , rootPath(std::move(rootPath))
, shellId(std::move(shellId)) , shellId(std::move(shellId))
, originalWorkingDirectory(QDir::current().absolutePath()) { , originalWorkingDirectory(QDir::current().absolutePath()) {
// clang-format off QObject::connect(
QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::watchFilesChanged, this, &RootWrapper::onWatchFilesChanged); QuickshellSettings::instance(),
// clang-format on &QuickshellSettings::watchFilesChanged,
this,
&RootWrapper::onWatchFilesChanged
);
QObject::connect(
&this->configDirWatcher,
&QFileSystemWatcher::directoryChanged,
this,
&RootWrapper::updateTooling
);
this->reloadGraph(true); this->reloadGraph(true);
@ -46,10 +58,10 @@ void RootWrapper::reloadGraph(bool hard) {
auto rootFile = QFileInfo(this->rootPath); auto rootFile = QFileInfo(this->rootPath);
auto rootPath = rootFile.dir(); auto rootPath = rootFile.dir();
auto scanner = QmlScanner(rootPath); auto scanner = QmlScanner(rootPath);
scanner.scanQmlFile(this->rootPath); scanner.scanQmlRoot(this->rootPath);
auto* generation = new EngineGeneration(rootPath, std::move(scanner)); qs::core::QmlToolingSupport::updateTooling(rootPath, scanner);
generation->wrapper = this; this->configDirWatcher.addPath(rootPath.path());
// todo: move into EngineGeneration // todo: move into EngineGeneration
if (this->generation != nullptr) { if (this->generation != nullptr) {
@ -59,6 +71,33 @@ void RootWrapper::reloadGraph(bool hard) {
QDir::setCurrent(this->originalWorkingDirectory); QDir::setCurrent(this->originalWorkingDirectory);
if (!scanner.scanErrors.isEmpty()) {
qCritical() << "Failed to load configuration";
QString errorString = "Failed to load configuration";
for (auto& error: scanner.scanErrors) {
const auto& file = error.file;
QString rel;
if (file.startsWith(rootPath.path() % '/')) {
rel = '@' % file.sliced(rootPath.path().length() + 1);
} else {
rel = file;
}
auto msg = " error in " % rel % '[' % QString::number(error.line) % ":0]: " % error.message;
errorString += '\n' % msg;
qCritical().noquote() << msg;
}
if (this->generation != nullptr && this->generation->qsgInstance != nullptr) {
emit this->generation->qsgInstance->reloadFailed(errorString);
}
return;
}
auto* generation = new EngineGeneration(rootPath, std::move(scanner));
generation->wrapper = this;
QUrl url; QUrl url;
url.setScheme("qs"); url.setScheme("qs");
url.setPath("@/qs/" % rootFile.fileName()); url.setPath("@/qs/" % rootFile.fileName());
@ -168,3 +207,9 @@ void RootWrapper::onWatchFilesChanged() {
} }
void RootWrapper::onWatchedFilesChanged() { this->reloadGraph(false); } void RootWrapper::onWatchedFilesChanged() { this->reloadGraph(false); }
void RootWrapper::updateTooling() {
if (!this->generation) return;
auto configDir = QFileInfo(this->rootPath).dir();
qs::core::QmlToolingSupport::updateTooling(configDir, this->generation->scanner);
}

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <qfilesystemwatcher.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qtclasshelpermacros.h> #include <qtclasshelpermacros.h>
@ -22,10 +23,12 @@ private slots:
void generationDestroyed(); void generationDestroyed();
void onWatchFilesChanged(); void onWatchFilesChanged();
void onWatchedFilesChanged(); void onWatchedFilesChanged();
void updateTooling();
private: private:
QString rootPath; QString rootPath;
QString shellId; QString shellId;
EngineGeneration* generation = nullptr; EngineGeneration* generation = nullptr;
QString originalWorkingDirectory; QString originalWorkingDirectory;
QFileSystemWatcher configDirWatcher;
}; };

View file

@ -1,9 +1,11 @@
#include "scan.hpp" #include "scan.hpp"
#include <cmath> #include <cmath>
#include <utility>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qdir.h> #include <qdir.h>
#include <qfileinfo.h> #include <qfileinfo.h>
#include <qjsengine.h>
#include <qjsonarray.h> #include <qjsonarray.h>
#include <qjsondocument.h> #include <qjsondocument.h>
#include <qjsonobject.h> #include <qjsonobject.h>
@ -12,44 +14,57 @@
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qpair.h> #include <qpair.h>
#include <qstring.h> #include <qstring.h>
#include <qstringliteral.h>
#include <qtextstream.h> #include <qtextstream.h>
#include "logcat.hpp" #include "logcat.hpp"
#include "scanenv.hpp"
QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg); QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg);
void QmlScanner::scanDir(const QString& path) { void QmlScanner::scanDir(const QDir& dir) {
if (this->scannedDirs.contains(path)) return; if (this->scannedDirs.contains(dir)) return;
this->scannedDirs.push_back(path); this->scannedDirs.push_back(dir);
const auto& path = dir.path();
qCDebug(logQmlScanner) << "Scanning directory" << path; qCDebug(logQmlScanner) << "Scanning directory" << path;
auto dir = QDir(path);
struct Entry {
QString name;
bool singleton = false;
bool internal = false;
};
bool seenQmldir = false; bool seenQmldir = false;
auto singletons = QVector<QString>(); auto entries = QVector<Entry>();
auto entries = QVector<QString>();
for (auto& entry: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) { for (auto& name: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) {
if (entry == "qmldir") { if (name == "qmldir") {
qCDebug(logQmlScanner qCDebug(
logQmlScanner
) << "Found qmldir file, qmldir synthesization will be disabled for directory" ) << "Found qmldir file, qmldir synthesization will be disabled for directory"
<< path; << path;
seenQmldir = true; seenQmldir = true;
} else if (entry.at(0).isUpper() && entry.endsWith(".qml")) { } else if (name.at(0).isUpper() && name.endsWith(".qml")) {
if (this->scanQmlFile(dir.filePath(entry))) { auto& entry = entries.emplaceBack();
singletons.push_back(entry);
if (this->scanQmlFile(dir.filePath(name), entry.singleton, entry.internal)) {
entry.name = name;
} else { } else {
entries.push_back(entry); entries.pop_back();
}
} else if (name.at(0).isUpper() && name.endsWith(".qml.json")) {
if (this->scanQmlJson(dir.filePath(name))) {
entries.push_back({
.name = name.first(name.length() - 5),
.singleton = true,
});
} }
} else if (entry.at(0).isUpper() && entry.endsWith(".qml.json")) {
this->scanQmlJson(dir.filePath(entry));
singletons.push_back(entry.first(entry.length() - 5));
} }
} }
if (!seenQmldir) { if (!seenQmldir) {
qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path << "singletons" qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path;
<< singletons;
QString qmldir; QString qmldir;
auto stream = QTextStream(&qmldir); auto stream = QTextStream(&qmldir);
@ -77,13 +92,10 @@ void QmlScanner::scanDir(const QString& path) {
qCWarning(logQmlScanner) << "Module path" << path << "is outside of the config folder."; qCWarning(logQmlScanner) << "Module path" << path << "is outside of the config folder.";
} }
for (auto& singleton: singletons) { for (const auto& entry: entries) {
stream << "singleton " << singleton.sliced(0, singleton.length() - 4) << " 1.0 " << singleton if (entry.internal) stream << "internal ";
<< "\n"; if (entry.singleton) stream << "singleton ";
} stream << entry.name.sliced(0, entry.name.length() - 4) << " 1.0 " << entry.name << '\n';
for (auto& entry: entries) {
stream << entry.sliced(0, entry.length() - 4) << " 1.0 " << entry << "\n";
} }
qCDebug(logQmlScanner) << "Synthesized qmldir for" << path << qPrintable("\n" + qmldir); qCDebug(logQmlScanner) << "Synthesized qmldir for" << path << qPrintable("\n" + qmldir);
@ -91,7 +103,7 @@ void QmlScanner::scanDir(const QString& path) {
} }
} }
bool QmlScanner::scanQmlFile(const QString& path) { bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& internal) {
if (this->scannedFiles.contains(path)) return false; if (this->scannedFiles.contains(path)) return false;
this->scannedFiles.push_back(path); this->scannedFiles.push_back(path);
@ -106,60 +118,121 @@ bool QmlScanner::scanQmlFile(const QString& path) {
auto stream = QTextStream(&file); auto stream = QTextStream(&file);
auto imports = QVector<QString>(); auto imports = QVector<QString>();
bool singleton = false; bool inHeader = true;
auto ifScopes = QVector<bool>();
bool sourceMasked = false;
int lineNum = 0;
QString overrideText;
bool isOverridden = false;
auto pragmaEngine = QJSEngine();
pragmaEngine.globalObject().setPrototype(
pragmaEngine.newQObject(new qs::scan::env::PreprocEnv())
);
auto postError = [&, this](QString error) {
this->scanErrors.append({.file = path, .message = std::move(error), .line = lineNum});
};
while (!stream.atEnd()) { while (!stream.atEnd()) {
auto line = stream.readLine().trimmed(); ++lineNum;
if (!singleton && line == "pragma Singleton") { bool hideMask = false;
qCDebug(logQmlScanner) << "Discovered singleton" << path; auto rawLine = stream.readLine();
singleton = true; auto line = rawLine.trimmed();
} else if (line.startsWith("import")) { if (!sourceMasked && inHeader) {
// we dont care about "import qs" as we always load the root folder if (!singleton && line == "pragma Singleton") {
if (auto importCursor = line.indexOf(" qs."); importCursor != -1) { singleton = true;
importCursor += 4; } else if (line.startsWith("import")) {
QString path; // we dont care about "import qs" as we always load the root folder
if (auto importCursor = line.indexOf(" qs."); importCursor != -1) {
importCursor += 4;
QString path;
while (importCursor != line.length()) { while (importCursor != line.length()) {
auto c = line.at(importCursor); auto c = line.at(importCursor);
if (c == '.') c = '/'; if (c == '.') c = '/';
else if (c == ' ') break; else if (c == ' ') break;
else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|| c == '_') || c == '_')
{ {
} else { } else {
qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line; qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line;
goto next; goto next;
}
path.append(c);
importCursor += 1;
} }
path.append(c); imports.append(this->rootPath.filePath(path));
importCursor += 1; } else if (auto startQuot = line.indexOf('"');
startQuot != -1 && line.length() >= startQuot + 3)
{
auto endQuot = line.indexOf('"', startQuot + 1);
if (endQuot == -1) continue;
auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1);
imports.push_back(name);
} }
} else if (!internal && line == "//@ pragma Internal") {
imports.append(this->rootPath.filePath(path)); internal = true;
} else if (auto startQuot = line.indexOf('"'); } else if (line.contains('{')) {
startQuot != -1 && line.length() >= startQuot + 3) inHeader = false;
{
auto endQuot = line.indexOf('"', startQuot + 1);
if (endQuot == -1) continue;
auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1);
imports.push_back(name);
} }
} else if (line.contains('{')) break; }
if (line.startsWith("//@ if ")) {
auto code = line.sliced(7);
auto value = pragmaEngine.evaluate(code, path, 1234);
bool mask = true;
if (value.isError()) {
postError(QString("Evaluating if: %0").arg(value.toString()));
} else if (!value.isBool()) {
postError(QString("If expression \"%0\" is not a boolean").arg(value.toString()));
} else if (value.toBool()) {
mask = false;
}
if (!sourceMasked && mask) hideMask = true;
mask = sourceMasked || mask; // cant unmask if a nested if passes
ifScopes.append(mask);
if (mask) isOverridden = true;
sourceMasked = mask;
} else if (line.startsWith("//@ endif")) {
if (ifScopes.isEmpty()) {
postError("endif without matching if");
} else {
ifScopes.pop_back();
if (ifScopes.isEmpty()) sourceMasked = false;
else sourceMasked = ifScopes.last();
}
}
if (!hideMask && sourceMasked) overrideText.append("// MASKED: " % rawLine % '\n');
else overrideText.append(rawLine % '\n');
next:; next:;
} }
if (!ifScopes.isEmpty()) {
postError("unclosed preprocessor if block");
}
file.close(); file.close();
if (isOverridden) {
this->fileIntercepts.insert(path, overrideText);
}
if (logQmlScanner().isDebugEnabled() && !imports.isEmpty()) { if (logQmlScanner().isDebugEnabled() && !imports.isEmpty()) {
qCDebug(logQmlScanner) << "Found imports" << imports; qCDebug(logQmlScanner) << "Found imports" << imports;
} }
auto currentdir = QDir(QFileInfo(path).canonicalPath()); auto currentdir = QDir(QFileInfo(path).absolutePath());
// the root can never be a singleton so it dosent matter if we skip it // the root can never be a singleton so it dosent matter if we skip it
this->scanDir(currentdir.path()); this->scanDir(currentdir);
for (auto& import: imports) { for (auto& import: imports) {
QString ipath; QString ipath;
@ -172,9 +245,9 @@ bool QmlScanner::scanQmlFile(const QString& path) {
} }
auto pathInfo = QFileInfo(ipath); auto pathInfo = QFileInfo(ipath);
auto cpath = pathInfo.canonicalFilePath(); auto cpath = pathInfo.absoluteFilePath();
if (cpath.isEmpty()) { if (!pathInfo.exists()) {
qCWarning(logQmlScanner) << "Ignoring unresolvable import" << ipath << "from" << path; qCWarning(logQmlScanner) << "Ignoring unresolvable import" << ipath << "from" << path;
continue; continue;
} }
@ -188,16 +261,22 @@ bool QmlScanner::scanQmlFile(const QString& path) {
else this->scanDir(cpath); else this->scanDir(cpath);
} }
return singleton; return true;
} }
void QmlScanner::scanQmlJson(const QString& path) { void QmlScanner::scanQmlRoot(const QString& path) {
bool singleton = false;
bool internal = false;
this->scanQmlFile(path, singleton, internal);
}
bool QmlScanner::scanQmlJson(const QString& path) {
qCDebug(logQmlScanner) << "Scanning qml.json file" << path; qCDebug(logQmlScanner) << "Scanning qml.json file" << path;
auto file = QFile(path); auto file = QFile(path);
if (!file.open(QFile::ReadOnly | QFile::Text)) { if (!file.open(QFile::ReadOnly | QFile::Text)) {
qCWarning(logQmlScanner) << "Failed to open file" << path; qCWarning(logQmlScanner) << "Failed to open file" << path;
return; return false;
} }
auto data = file.readAll(); auto data = file.readAll();
@ -209,7 +288,7 @@ void QmlScanner::scanQmlJson(const QString& path) {
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
qCCritical(logQmlScanner).nospace() qCCritical(logQmlScanner).nospace()
<< "Failed to parse qml.json file at " << path << ": " << error.errorString(); << "Failed to parse qml.json file at " << path << ": " << error.errorString();
return; return false;
} }
const QString body = const QString body =
@ -219,6 +298,7 @@ void QmlScanner::scanQmlJson(const QString& path) {
this->fileIntercepts.insert(path.first(path.length() - 5), body); this->fileIntercepts.insert(path.first(path.length() - 5), body);
this->scannedFiles.push_back(path); this->scannedFiles.push_back(path);
return true;
} }
QPair<QString, QString> QmlScanner::jsonToQml(const QJsonValue& value, int indent) { QPair<QString, QString> QmlScanner::jsonToQml(const QJsonValue& value, int indent) {

View file

@ -16,18 +16,25 @@ public:
QmlScanner() = default; QmlScanner() = default;
QmlScanner(const QDir& rootPath): rootPath(rootPath) {} QmlScanner(const QDir& rootPath): rootPath(rootPath) {}
// path must be canonical void scanDir(const QDir& dir);
void scanDir(const QString& path); void scanQmlRoot(const QString& path);
// returns if the file has a singleton
bool scanQmlFile(const QString& path);
QVector<QString> scannedDirs; QVector<QDir> scannedDirs;
QVector<QString> scannedFiles; QVector<QString> scannedFiles;
QHash<QString, QString> fileIntercepts; QHash<QString, QString> fileIntercepts;
struct ScanError {
QString file;
QString message;
int line;
};
QVector<ScanError> scanErrors;
private: private:
QDir rootPath; QDir rootPath;
void scanQmlJson(const QString& path); bool scanQmlFile(const QString& path, bool& singleton, bool& internal);
bool scanQmlJson(const QString& path);
[[nodiscard]] static QPair<QString, QString> jsonToQml(const QJsonValue& value, int indent = 0); [[nodiscard]] static QPair<QString, QString> jsonToQml(const QJsonValue& value, int indent = 0);
}; };

31
src/core/scanenv.cpp Normal file
View file

@ -0,0 +1,31 @@
#include "scanenv.hpp"
#include <qcontainerfwd.h>
#include <qtenvironmentvariables.h>
#include "build.hpp"
namespace qs::scan::env {
bool PreprocEnv::hasVersion(int major, int minor, const QStringList& features) {
if (QS_VERSION_MAJOR > major) return true;
if (QS_VERSION_MAJOR == major && QS_VERSION_MINOR > minor) return true;
auto availFeatures = QString(QS_UNRELEASED_FEATURES).split(';');
for (const auto& feature: features) {
if (!availFeatures.contains(feature)) return false;
}
return QS_VERSION_MAJOR == major && QS_VERSION_MINOR == minor;
}
QString PreprocEnv::env(const QString& variable) {
return qEnvironmentVariable(variable.toStdString().c_str());
}
bool PreprocEnv::isEnvSet(const QString& variable) {
return qEnvironmentVariableIsSet(variable.toStdString().c_str());
}
} // namespace qs::scan::env

20
src/core/scanenv.hpp Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qtmetamacros.h>
namespace qs::scan::env {
class PreprocEnv: public QObject {
Q_OBJECT;
public:
Q_INVOKABLE static bool
hasVersion(int major, int minor, const QStringList& features = QStringList());
Q_INVOKABLE static QString env(const QString& variable);
Q_INVOKABLE static bool isEnvSet(const QString& variable);
};
} // namespace qs::scan::env

View file

@ -19,7 +19,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
auto newIter = newValues.begin(); auto newIter = newValues.begin();
// TODO: cache this // TODO: cache this
auto getCmpKey = [&](const QVariant& v) { auto getCmpKey = [this](const QVariant& v) {
if (v.canConvert<QVariantMap>()) { if (v.canConvert<QVariantMap>()) {
auto vMap = v.value<QVariantMap>(); auto vMap = v.value<QVariantMap>();
if (vMap.contains(this->cmpKey)) { if (vMap.contains(this->cmpKey)) {
@ -30,7 +30,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
return v; return v;
}; };
auto variantCmp = [&](const QVariant& a, const QVariant& b) { auto variantCmp = [&, this](const QVariant& a, const QVariant& b) {
if (!this->cmpKey.isEmpty()) return getCmpKey(a) == getCmpKey(b); if (!this->cmpKey.isEmpty()) return getCmpKey(a) == getCmpKey(b);
else return a == b; else return a == b;
}; };
@ -72,8 +72,8 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
do { do {
++iter; ++iter;
} while (iter != this->mValues.end() } while (iter != this->mValues.end()
&& std::find_if(newIter, newValues.end(), eqPredicate(*iter)) == newValues.end() && std::find_if(newIter, newValues.end(), eqPredicate(*iter))
); == newValues.end());
auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter)); auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
auto startIndex = static_cast<qint32>(std::distance(this->mValues.begin(), startIter)); auto startIndex = static_cast<qint32>(std::distance(this->mValues.begin(), startIter));

View file

@ -51,9 +51,3 @@ void SingletonRegistry::onReload(SingletonRegistry* old) {
singleton->reload(old == nullptr ? nullptr : old->registry.value(url)); singleton->reload(old == nullptr ? nullptr : old->registry.value(url));
} }
} }
void SingletonRegistry::onPostReload() {
for (auto* singleton: this->registry.values()) {
PostReloadHook::postReloadTree(singleton);
}
}

View file

@ -26,7 +26,6 @@ public:
void registerSingleton(const QUrl& url, Singleton* singleton); void registerSingleton(const QUrl& url, Singleton* singleton);
void onReload(SingletonRegistry* old); void onReload(SingletonRegistry* old);
void onPostReload();
private: private:
QHash<QUrl, Singleton*> registry; QHash<QUrl, Singleton*> registry;

241
src/core/toolsupport.cpp Normal file
View file

@ -0,0 +1,241 @@
#include "toolsupport.hpp"
#include <cerrno>
#include <fcntl.h>
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qqmlengine.h>
#include <qtenvironmentvariables.h>
#include "logcat.hpp"
#include "paths.hpp"
#include "scan.hpp"
namespace qs::core {
namespace {
QS_LOGGING_CATEGORY(logTooling, "quickshell.tooling", QtWarningMsg);
}
bool QmlToolingSupport::updateTooling(const QDir& configRoot, QmlScanner& scanner) {
auto* vfs = QsPaths::instance()->shellVfsDir();
if (!vfs) {
qCCritical(logTooling) << "Tooling dir could not be created";
return false;
}
if (!QmlToolingSupport::lockTooling()) {
return false;
}
if (!QmlToolingSupport::updateQmllsConfig(configRoot, false)) {
QDir(vfs->filePath("qs")).removeRecursively();
return false;
}
QmlToolingSupport::updateToolingFs(scanner, configRoot, vfs->filePath("qs"));
return true;
}
bool QmlToolingSupport::lockTooling() {
if (QmlToolingSupport::toolingLock) return true;
auto lockPath = QsPaths::instance()->shellVfsDir()->filePath("tooling.lock");
auto* file = new QFile(lockPath);
if (!file->open(QFile::WriteOnly)) {
qCCritical(logTooling) << "Could not open tooling lock for write";
return false;
}
struct flock lock = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET, // NOLINT (fcntl.h??)
.l_start = 0,
.l_len = 0,
.l_pid = 0,
};
if (fcntl(file->handle(), F_SETLK, &lock) == 0) {
qCInfo(logTooling) << "Acquired tooling support lock";
QmlToolingSupport::toolingLock = file;
return true;
} else if (errno == EACCES || errno == EAGAIN) {
qCInfo(logTooling) << "Tooling support locked by another instance";
return false;
} else {
qCCritical(logTooling).nospace() << "Could not create tooling lock at " << lockPath
<< " with error code " << errno << ": " << qt_error_string();
return false;
}
}
QString QmlToolingSupport::getQmllsConfig() {
static auto config = []() {
// We can't replicate the algorithm used to create the import path list as it can have distro
// specific patches, e.g. nixos.
auto importPaths = QQmlEngine().importPathList();
importPaths.removeIf([](const QString& path) { return path.startsWith("qrc:"); });
auto vfsPath = QsPaths::instance()->shellVfsDir()->path();
auto importPathsStr = importPaths.join(u':');
QString qmllsConfig;
auto print = QDebug(&qmllsConfig).nospace();
print << "[General]\nno-cmake-calls=true\nbuildDir=" << vfsPath
<< "\nimportPaths=" << importPathsStr << '\n';
return qmllsConfig;
}();
return config;
}
bool QmlToolingSupport::updateQmllsConfig(const QDir& configRoot, bool create) {
auto shellConfigPath = configRoot.filePath(".qmlls.ini");
auto vfsConfigPath = QsPaths::instance()->shellVfsDir()->filePath(".qmlls.ini");
auto shellFileInfo = QFileInfo(shellConfigPath);
if (!create && !shellFileInfo.exists() && !shellFileInfo.isSymLink()) {
if (QmlToolingSupport::toolingEnabled) {
qInfo() << "QML tooling support disabled";
QmlToolingSupport::toolingEnabled = false;
} else {
qCInfo(logTooling) << "Not enabling QML tooling support, qmlls.ini is missing at path"
<< shellConfigPath;
}
QFile::remove(vfsConfigPath);
return false;
}
auto vfsFile = QFile(vfsConfigPath);
if (!vfsFile.open(QFile::ReadWrite | QFile::Text)) {
qCCritical(logTooling) << "Failed to create qmlls config in vfs";
return false;
}
auto config = QmlToolingSupport::getQmllsConfig();
if (vfsFile.readAll() != config) {
if (!vfsFile.resize(0) || !vfsFile.write(config.toUtf8())) {
qCCritical(logTooling) << "Failed to write qmlls config in vfs";
return false;
}
qCDebug(logTooling) << "Wrote qmlls config in vfs";
}
if (!shellFileInfo.isSymLink() || shellFileInfo.symLinkTarget() != vfsConfigPath) {
QFile::remove(shellConfigPath);
if (!QFile::link(vfsConfigPath, shellConfigPath)) {
qCCritical(logTooling) << "Failed to create qmlls config symlink";
return false;
}
qCDebug(logTooling) << "Created qmlls config symlink";
}
if (!QmlToolingSupport::toolingEnabled) {
qInfo() << "QML tooling support enabled";
QmlToolingSupport::toolingEnabled = true;
}
return true;
}
void QmlToolingSupport::updateToolingFs(
QmlScanner& scanner,
const QDir& scanDir,
const QDir& linkDir
) {
QList<QString> files;
QSet<QString> subdirs;
auto scanPath = scanDir.path();
linkDir.mkpath(".");
for (auto& path: scanner.scannedFiles) {
if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue;
auto name = path.sliced(scanPath.length() + 1);
if (name.contains('/')) {
auto dirname = name.first(name.indexOf('/'));
subdirs.insert(dirname);
continue;
}
auto fileInfo = QFileInfo(path);
if (!fileInfo.isFile()) continue;
auto spath = linkDir.filePath(name);
auto sFileInfo = QFileInfo(spath);
if (!sFileInfo.isSymLink() || sFileInfo.symLinkTarget() != path) {
QFile::remove(spath);
if (QFile::link(path, spath)) {
qCDebug(logTooling) << "Created symlink to" << path << "at" << spath;
files.append(spath);
} else {
qCCritical(logTooling) << "Could not create symlink to" << path << "at" << spath;
}
} else {
files.append(spath);
}
}
for (auto [path, text]: scanner.fileIntercepts.asKeyValueRange()) {
if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue;
auto name = path.sliced(scanPath.length() + 1);
if (name.contains('/')) {
auto dirname = name.first(name.indexOf('/'));
subdirs.insert(dirname);
continue;
}
auto spath = linkDir.filePath(name);
auto file = QFile(spath);
if (!file.open(QFile::ReadWrite | QFile::Text)) {
qCCritical(logTooling) << "Failed to open injected file" << spath;
continue;
}
if (file.readAll() == text) {
files.append(spath);
continue;
}
if (file.resize(0) && file.write(text.toUtf8())) {
files.append(spath);
qCDebug(logTooling) << "Wrote injected file" << spath;
} else {
qCCritical(logTooling) << "Failed to write injected file" << spath;
}
}
for (auto& name: linkDir.entryList(QDir::Files | QDir::System)) { // System = broken symlinks
auto path = linkDir.filePath(name);
if (!files.contains(path)) {
if (QFile::remove(path)) qCDebug(logTooling) << "Removed old file at" << path;
else qCWarning(logTooling) << "Failed to remove old file at" << path;
}
}
for (const auto& subdir: subdirs) {
QmlToolingSupport::updateToolingFs(scanner, scanDir.filePath(subdir), linkDir.filePath(subdir));
}
}
} // namespace qs::core

22
src/core/toolsupport.hpp Normal file
View file

@ -0,0 +1,22 @@
#pragma once
#include <qdir.h>
#include "scan.hpp"
namespace qs::core {
class QmlToolingSupport {
public:
static bool updateTooling(const QDir& configRoot, QmlScanner& scanner);
private:
static QString getQmllsConfig();
static bool lockTooling();
static bool updateQmllsConfig(const QDir& configRoot, bool create);
static void updateToolingFs(QmlScanner& scanner, const QDir& scanDir, const QDir& linkDir);
static inline bool toolingEnabled = false;
static inline QFile* toolingLock = nullptr;
};
} // namespace qs::core

View file

@ -29,7 +29,7 @@ struct StringLiteral16 {
} }
[[nodiscard]] constexpr const QChar* qCharPtr() const noexcept { [[nodiscard]] constexpr const QChar* qCharPtr() const noexcept {
return std::bit_cast<const QChar*>(&this->value); return std::bit_cast<const QChar*>(&this->value); // NOLINT
} }
[[nodiscard]] Q_ALWAYS_INLINE operator QString() const noexcept { [[nodiscard]] Q_ALWAYS_INLINE operator QString() const noexcept {
@ -251,37 +251,6 @@ public:
GuardedEmitBlocker block() { return GuardedEmitBlocker(&this->blocked); } GuardedEmitBlocker block() { return GuardedEmitBlocker(&this->blocked); }
}; };
template <auto member, auto destroyedSlot, auto changedSignal>
class SimpleObjectHandleOps {
using Traits = MemberPointerTraits<decltype(member)>;
public:
static bool setObject(Traits::Class* parent, Traits::Type value) {
if (value == parent->*member) return false;
if (parent->*member != nullptr) {
QObject::disconnect(parent->*member, &QObject::destroyed, parent, destroyedSlot);
}
parent->*member = value;
if (value != nullptr) {
QObject::connect(parent->*member, &QObject::destroyed, parent, destroyedSlot);
}
if constexpr (changedSignal != nullptr) {
emit(parent->*changedSignal)();
}
return true;
}
};
template <auto member, auto destroyedSlot, auto changedSignal = nullptr>
bool setSimpleObjectHandle(auto* parent, auto* value) {
return SimpleObjectHandleOps<member, destroyedSlot, changedSignal>::setObject(parent, value);
}
template <auto methodPtr> template <auto methodPtr>
class MethodFunctor { class MethodFunctor {
using PtrMeta = MemberPointerTraits<decltype(methodPtr)>; using PtrMeta = MemberPointerTraits<decltype(methodPtr)>;

View file

@ -6,12 +6,51 @@ qt_add_library(quickshell-crash STATIC
qs_pch(quickshell-crash SET large) qs_pch(quickshell-crash SET large)
find_package(PkgConfig REQUIRED) if (VENDOR_CPPTRACE)
pkg_check_modules(breakpad REQUIRED IMPORTED_TARGET breakpad) message(STATUS "Vendoring cpptrace...")
# only need client?? take only includes from pkg config todo include(FetchContent)
target_link_libraries(quickshell-crash PRIVATE PkgConfig::breakpad -lbreakpad_client)
# For use without internet access see: https://cmake.org/cmake/help/latest/module/FetchContent.html#variable:FETCHCONTENT_SOURCE_DIR_%3CuppercaseName%3E
FetchContent_Declare(
cpptrace
GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git
GIT_TAG v1.0.4
)
set(CPPTRACE_UNWIND_WITH_LIBUNWIND TRUE)
FetchContent_MakeAvailable(cpptrace)
else ()
find_package(cpptrace REQUIRED)
# useful for cross after you have already checked cpptrace is built correctly
if (NOT DO_NOT_CHECK_CPPTRACE_USABILITY)
try_run(CPPTRACE_SIGNAL_SAFE_UNWIND CPPTRACE_SIGNAL_SAFE_UNWIND_COMP
SOURCE_FROM_CONTENT check.cxx "
#include <cpptrace/basic.hpp>
int main() {
return cpptrace::can_signal_safe_unwind() ? 0 : 1;
}
"
LOG_DESCRIPTION "Checking ${CPPTRACE_SIGNAL_SAFE_UNWIND}"
LINK_LIBRARIES cpptrace::cpptrace
COMPILE_OUTPUT_VARIABLE CPPTRACE_SIGNAL_SAFE_UNWIND_LOG
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
)
if (NOT CPPTRACE_SIGNAL_SAFE_UNWIND_COMP)
message(STATUS "${CPPTRACE_SIGNAL_SAFE_UNWIND_LOG}")
message(FATAL_ERROR "Failed to compile cpptrace signal safe unwind tester.")
endif()
if (NOT CPPTRACE_SIGNAL_SAFE_UNWIND EQUAL 0)
message(STATUS "Cpptrace signal safe unwind test exited with: ${CPPTRACE_SIGNAL_SAFE_UNWIND}")
message(FATAL_ERROR "Cpptrace was built without CPPTRACE_UNWIND_WITH_LIBUNWIND set to true. Enable libunwind support in the package or set VENDOR_CPPTRACE to true when building Quickshell.")
endif()
endif ()
endif ()
# quick linked for pch compat # quick linked for pch compat
target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt::Quick Qt::Widgets) target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt::Quick Qt::Widgets cpptrace::cpptrace)
target_link_libraries(quickshell PRIVATE quickshell-crash) target_link_libraries(quickshell PRIVATE quickshell-crash)

View file

@ -1,12 +1,13 @@
#include "handler.hpp" #include "handler.hpp"
#include <algorithm>
#include <array> #include <array>
#include <cerrno>
#include <csignal>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <bits/types/sigset_t.h> #include <cpptrace/basic.hpp>
#include <breakpad/client/linux/handler/exception_handler.h> #include <cpptrace/forward.hpp>
#include <breakpad/client/linux/handler/minidump_descriptor.h>
#include <breakpad/common/linux/linux_libc_support.h>
#include <qdatastream.h> #include <qdatastream.h>
#include <qfile.h> #include <qfile.h>
#include <qlogging.h> #include <qlogging.h>
@ -19,91 +20,70 @@
extern char** environ; // NOLINT extern char** environ; // NOLINT
using namespace google_breakpad;
namespace qs::crash { namespace qs::crash {
namespace { namespace {
QS_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg); QS_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg);
}
struct CrashHandlerPrivate { void writeEnvInt(char* buf, const char* name, int value) {
ExceptionHandler* exceptionHandler = nullptr; // NOLINTBEGIN (cppcoreguidelines-pro-bounds-pointer-arithmetic)
int minidumpFd = -1; while (*name != '\0') *buf++ = *name++;
int infoFd = -1; *buf++ = '=';
static bool minidumpCallback(const MinidumpDescriptor& descriptor, void* context, bool succeeded); if (value < 0) {
}; *buf++ = '-';
value = -value;
CrashHandler::CrashHandler(): d(new CrashHandlerPrivate()) {}
void CrashHandler::init() {
// MinidumpDescriptor has no move constructor and the copy constructor breaks fds.
auto createHandler = [this](const MinidumpDescriptor& desc) {
this->d->exceptionHandler = new ExceptionHandler(
desc,
nullptr,
&CrashHandlerPrivate::minidumpCallback,
this->d,
true,
-1
);
};
qCDebug(logCrashHandler) << "Starting crash handler...";
this->d->minidumpFd = memfd_create("quickshell:minidump", MFD_CLOEXEC);
if (this->d->minidumpFd == -1) {
qCCritical(logCrashHandler
) << "Failed to allocate minidump memfd, minidumps will be saved in the working directory.";
createHandler(MinidumpDescriptor("."));
} else {
qCDebug(logCrashHandler) << "Created memfd" << this->d->minidumpFd
<< "for holding possible minidumps.";
createHandler(MinidumpDescriptor(this->d->minidumpFd));
} }
qCInfo(logCrashHandler) << "Crash handler initialized."; if (value == 0) {
} *buf++ = '0';
*buf = '\0';
void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) {
this->d->infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC);
if (this->d->infoFd == -1) {
qCCritical(logCrashHandler
) << "Failed to allocate instance info memfd, crash recovery will not work.";
return; return;
} }
QFile file; auto* start = buf;
file.open(this->d->infoFd, QFile::ReadWrite); while (value > 0) {
*buf++ = static_cast<char>('0' + (value % 10));
QDataStream ds(&file); value /= 10;
ds << info;
file.flush();
qCDebug(logCrashHandler) << "Stored instance info in memfd" << this->d->infoFd;
}
CrashHandler::~CrashHandler() {
delete this->d->exceptionHandler;
delete this->d;
}
bool CrashHandlerPrivate::minidumpCallback(
const MinidumpDescriptor& /*descriptor*/,
void* context,
bool /*success*/
) {
// A fork that just dies to ensure the coredump is caught by the system.
auto coredumpPid = fork();
if (coredumpPid == 0) {
return false;
} }
auto* self = static_cast<CrashHandlerPrivate*>(context); *buf = '\0';
std::reverse(start, buf);
// NOLINTEND
}
void signalHandler(
int sig,
siginfo_t* /*info*/, // NOLINT (misc-include-cleaner)
void* /*context*/
) {
if (CrashInfo::INSTANCE.traceFd != -1) {
auto traceBuffer = std::array<cpptrace::frame_ptr, 1024>();
auto frameCount = cpptrace::safe_generate_raw_trace(traceBuffer.data(), traceBuffer.size(), 1);
for (size_t i = 0; i < static_cast<size_t>(frameCount); i++) {
auto frame = cpptrace::safe_object_frame();
cpptrace::get_safe_object_frame(traceBuffer[i], &frame);
auto* wptr = reinterpret_cast<char*>(&frame);
auto* end = wptr + sizeof(cpptrace::safe_object_frame); // NOLINT
while (wptr != end) {
auto r = write(CrashInfo::INSTANCE.traceFd, &frame, sizeof(cpptrace::safe_object_frame));
if (r < 0 && errno == EINTR) continue;
if (r <= 0) goto fail;
wptr += r; // NOLINT
}
}
fail:;
}
auto coredumpPid = fork();
if (coredumpPid == 0) {
raise(sig);
_exit(-1);
}
auto exe = std::array<char, 4096>(); auto exe = std::array<char, 4096>();
if (readlink("/proc/self/exe", exe.data(), exe.size() - 1) == -1) { if (readlink("/proc/self/exe", exe.data(), exe.size() - 1) == -1) {
@ -116,17 +96,19 @@ bool CrashHandlerPrivate::minidumpCallback(
auto env = std::array<char*, 4096>(); auto env = std::array<char*, 4096>();
auto envi = 0; auto envi = 0;
auto infoFd = dup(self->infoFd); // dup to remove CLOEXEC
auto infoFdStr = std::array<char, 38>(); auto infoFdStr = std::array<char, 48>();
memcpy(infoFdStr.data(), "__QUICKSHELL_CRASH_INFO_FD=-1" /*\0*/, 30); writeEnvInt(infoFdStr.data(), "__QUICKSHELL_CRASH_INFO_FD", dup(CrashInfo::INSTANCE.infoFd));
if (infoFd != -1) my_uitos(&infoFdStr[27], infoFd, 10);
env[envi++] = infoFdStr.data(); env[envi++] = infoFdStr.data();
auto corePidStr = std::array<char, 39>(); auto corePidStr = std::array<char, 48>();
memcpy(corePidStr.data(), "__QUICKSHELL_CRASH_DUMP_PID=-1" /*\0*/, 31); writeEnvInt(corePidStr.data(), "__QUICKSHELL_CRASH_DUMP_PID", coredumpPid);
if (coredumpPid != -1) my_uitos(&corePidStr[28], coredumpPid, 10);
env[envi++] = corePidStr.data(); env[envi++] = corePidStr.data();
auto sigStr = std::array<char, 48>();
writeEnvInt(sigStr.data(), "__QUICKSHELL_CRASH_SIGNAL", sig);
env[envi++] = sigStr.data();
auto populateEnv = [&]() { auto populateEnv = [&]() {
auto senvi = 0; auto senvi = 0;
while (envi != 4095) { while (envi != 4095) {
@ -138,30 +120,18 @@ bool CrashHandlerPrivate::minidumpCallback(
env[envi] = nullptr; env[envi] = nullptr;
}; };
sigset_t sigset;
sigemptyset(&sigset); // NOLINT (include)
sigprocmask(SIG_SETMASK, &sigset, nullptr); // NOLINT
auto pid = fork(); auto pid = fork();
if (pid == -1) { if (pid == -1) {
perror("Failed to fork and launch crash reporter.\n"); perror("Failed to fork and launch crash reporter.\n");
return false; _exit(-1);
} else if (pid == 0) { } else if (pid == 0) {
// dup to remove CLOEXEC // dup to remove CLOEXEC
// if already -1 will return -1 auto dumpFdStr = std::array<char, 48>();
auto dumpFd = dup(self->minidumpFd); auto logFdStr = std::array<char, 48>();
auto logFd = dup(CrashInfo::INSTANCE.logFd); writeEnvInt(dumpFdStr.data(), "__QUICKSHELL_CRASH_DUMP_FD", dup(CrashInfo::INSTANCE.traceFd));
writeEnvInt(logFdStr.data(), "__QUICKSHELL_CRASH_LOG_FD", dup(CrashInfo::INSTANCE.logFd));
// allow up to 10 digits, which should never happen
auto dumpFdStr = std::array<char, 38>();
auto logFdStr = std::array<char, 37>();
memcpy(dumpFdStr.data(), "__QUICKSHELL_CRASH_DUMP_FD=-1" /*\0*/, 30);
memcpy(logFdStr.data(), "__QUICKSHELL_CRASH_LOG_FD=-1" /*\0*/, 29);
if (dumpFd != -1) my_uitos(&dumpFdStr[27], dumpFd, 10);
if (logFd != -1) my_uitos(&logFdStr[26], logFd, 10);
env[envi++] = dumpFdStr.data(); env[envi++] = dumpFdStr.data();
env[envi++] = logFdStr.data(); env[envi++] = logFdStr.data();
@ -178,8 +148,82 @@ bool CrashHandlerPrivate::minidumpCallback(
perror("Failed to relaunch quickshell.\n"); perror("Failed to relaunch quickshell.\n");
_exit(-1); _exit(-1);
} }
}
return false; // should make sure it hits the system coredump handler } // namespace
void CrashHandler::init() {
qCDebug(logCrashHandler) << "Starting crash handler...";
CrashInfo::INSTANCE.traceFd = memfd_create("quickshell:trace", MFD_CLOEXEC);
if (CrashInfo::INSTANCE.traceFd == -1) {
qCCritical(logCrashHandler) << "Failed to allocate trace memfd, stack traces will not be "
"available in crash reports.";
} else {
qCDebug(logCrashHandler) << "Created memfd" << CrashInfo::INSTANCE.traceFd
<< "for holding possible stack traces.";
}
{
// Preload anything dynamically linked to avoid malloc etc in the dynamic loader.
// See cpptrace documentation for more information.
auto buffer = std::array<cpptrace::frame_ptr, 10>();
cpptrace::safe_generate_raw_trace(buffer.data(), buffer.size());
auto frame = cpptrace::safe_object_frame();
cpptrace::get_safe_object_frame(buffer[0], &frame);
}
// NOLINTBEGIN (misc-include-cleaner)
// Set up alternate signal stack for stack overflow handling
auto ss = stack_t();
ss.ss_sp = new char[SIGSTKSZ];
ss.ss_size = SIGSTKSZ;
ss.ss_flags = 0;
sigaltstack(&ss, nullptr);
// Install signal handlers
struct sigaction sa {};
sa.sa_sigaction = &signalHandler;
sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESETHAND;
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, nullptr);
sigaction(SIGABRT, &sa, nullptr);
sigaction(SIGFPE, &sa, nullptr);
sigaction(SIGILL, &sa, nullptr);
sigaction(SIGBUS, &sa, nullptr);
sigaction(SIGTRAP, &sa, nullptr);
// NOLINTEND (misc-include-cleaner)
qCInfo(logCrashHandler) << "Crash handler initialized.";
}
void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) {
CrashInfo::INSTANCE.infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC);
if (CrashInfo::INSTANCE.infoFd == -1) {
qCCritical(
logCrashHandler
) << "Failed to allocate instance info memfd, crash recovery will not work.";
return;
}
QFile file;
if (!file.open(CrashInfo::INSTANCE.infoFd, QFile::ReadWrite)) {
qCCritical(
logCrashHandler
) << "Failed to open instance info memfd, crash recovery will not work.";
}
QDataStream ds(&file);
ds << info;
file.flush();
qCDebug(logCrashHandler) << "Stored instance info in memfd" << CrashInfo::INSTANCE.infoFd;
} }
} // namespace qs::crash } // namespace qs::crash

View file

@ -5,19 +5,10 @@
#include "../core/instanceinfo.hpp" #include "../core/instanceinfo.hpp"
namespace qs::crash { namespace qs::crash {
struct CrashHandlerPrivate;
class CrashHandler { class CrashHandler {
public: public:
explicit CrashHandler(); static void init();
~CrashHandler(); static void setRelaunchInfo(const RelaunchInfo& info);
Q_DISABLE_COPY_MOVE(CrashHandler);
void init();
void setRelaunchInfo(const RelaunchInfo& info);
private:
CrashHandlerPrivate* d;
}; };
} // namespace qs::crash } // namespace qs::crash

View file

@ -66,7 +66,8 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid)
mainLayout->addSpacing(textHeight); mainLayout->addSpacing(textHeight);
if (qtVersionMatches) { if (qtVersionMatches) {
mainLayout->addWidget(new QLabel("Please open a bug report for this issue via github or email.") mainLayout->addWidget(
new QLabel("Please open a bug report for this issue via github or email.")
); );
} else { } else {
mainLayout->addWidget(new QLabel( mainLayout->addWidget(new QLabel(
@ -77,7 +78,7 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid)
mainLayout->addWidget(new ReportLabel( mainLayout->addWidget(new ReportLabel(
"Github:", "Github:",
"https://github.com/quickshell-mirror/quickshell/issues/new?template=crash.yml", "https://github.com/quickshell-mirror/quickshell/issues/new?template=crash2.yml",
this this
)); ));
@ -113,7 +114,7 @@ void CrashReporterGui::openFolder() {
void CrashReporterGui::openReportUrl() { void CrashReporterGui::openReportUrl() {
QDesktopServices::openUrl( QDesktopServices::openUrl(
QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash.yml") QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml")
); );
} }

View file

@ -1,7 +1,10 @@
#include "main.hpp" #include "main.hpp"
#include <cerrno> #include <cerrno>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include <cpptrace/basic.hpp>
#include <cpptrace/formatting.hpp>
#include <qapplication.h> #include <qapplication.h>
#include <qconfig.h> #include <qconfig.h>
#include <qcoreapplication.h> #include <qcoreapplication.h>
@ -13,13 +16,17 @@
#include <qtenvironmentvariables.h> #include <qtenvironmentvariables.h>
#include <qtextstream.h> #include <qtextstream.h>
#include <qtversion.h> #include <qtversion.h>
#include <qtypes.h>
#include <sys/sendfile.h> #include <sys/sendfile.h>
#include <sys/types.h> #include <sys/types.h>
#include <unistd.h>
#include "../core/instanceinfo.hpp" #include "../core/instanceinfo.hpp"
#include "../core/logcat.hpp" #include "../core/logcat.hpp"
#include "../core/logging.hpp" #include "../core/logging.hpp"
#include "../core/logging_p.hpp"
#include "../core/paths.hpp" #include "../core/paths.hpp"
#include "../core/ringbuf.hpp"
#include "build.hpp" #include "build.hpp"
#include "interface.hpp" #include "interface.hpp"
@ -61,6 +68,76 @@ int tryDup(int fd, const QString& path) {
return 0; return 0;
} }
QString readRecentLogs(int logFd, int maxLines, qint64 maxAgeSecs) {
QFile file;
if (!file.open(logFd, QFile::ReadOnly, QFile::AutoCloseHandle)) {
return QStringLiteral("(failed to open log fd)\n");
}
file.seek(0);
qs::log::EncodedLogReader reader;
reader.setDevice(&file);
bool readable = false;
quint8 logVersion = 0;
quint8 readerVersion = 0;
if (!reader.readHeader(&readable, &logVersion, &readerVersion) || !readable) {
return QStringLiteral("(failed to read log header)\n");
}
// Read all messages, keeping last maxLines in a ring buffer
auto tail = RingBuffer<qs::log::LogMessage>(maxLines);
qs::log::LogMessage message;
while (reader.read(&message)) {
tail.emplace(message);
}
if (tail.size() == 0) {
return QStringLiteral("(no logs)\n");
}
// Filter to only messages within maxAgeSecs of the newest message
auto cutoff = tail.at(0).time.addSecs(-maxAgeSecs);
QString result;
auto stream = QTextStream(&result);
for (auto i = tail.size() - 1; i != -1; i--) {
if (tail.at(i).time < cutoff) continue;
qs::log::LogMessage::formatMessage(stream, tail.at(i), false, true);
stream << '\n';
}
if (result.isEmpty()) {
return QStringLiteral("(no recent logs)\n");
}
return result;
}
cpptrace::stacktrace resolveStacktrace(int dumpFd) {
QFile sourceFile;
if (!sourceFile.open(dumpFd, QFile::ReadOnly, QFile::AutoCloseHandle)) {
qCCritical(logCrashReporter) << "Failed to open trace memfd.";
return {};
}
sourceFile.seek(0);
auto data = sourceFile.readAll();
auto frameCount = static_cast<size_t>(data.size()) / sizeof(cpptrace::safe_object_frame);
if (frameCount == 0) return {};
const auto* frames = reinterpret_cast<const cpptrace::safe_object_frame*>(data.constData());
cpptrace::object_trace objectTrace;
for (size_t i = 0; i < frameCount; i++) {
objectTrace.frames.push_back(frames[i].resolve()); // NOLINT
}
return objectTrace.resolve();
}
void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) {
qCDebug(logCrashReporter) << "Recording crash information at" << crashDir.path(); qCDebug(logCrashReporter) << "Recording crash information at" << crashDir.path();
@ -71,32 +148,25 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) {
} }
auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt(); auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt();
auto crashSignal = qEnvironmentVariable("__QUICKSHELL_CRASH_SIGNAL").toInt();
auto dumpFd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD").toInt(); auto dumpFd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD").toInt();
auto logFd = qEnvironmentVariable("__QUICKSHELL_CRASH_LOG_FD").toInt(); auto logFd = qEnvironmentVariable("__QUICKSHELL_CRASH_LOG_FD").toInt();
qCDebug(logCrashReporter) << "Saving minidump from fd" << dumpFd; qCDebug(logCrashReporter) << "Resolving stacktrace from fd" << dumpFd;
auto dumpDupStatus = tryDup(dumpFd, crashDir.filePath("minidump.dmp.log")); auto stacktrace = resolveStacktrace(dumpFd);
if (dumpDupStatus != 0) {
qCCritical(logCrashReporter) << "Failed to write minidump:" << dumpDupStatus;
}
qCDebug(logCrashReporter) << "Saving log from fd" << logFd; qCDebug(logCrashReporter) << "Reading recent log lines from fd" << logFd;
auto logDupStatus = tryDup(logFd, crashDir.filePath("log.qslog.log")); auto logDupFd = dup(logFd);
auto recentLogs = readRecentLogs(logFd, 100, 10);
qCDebug(logCrashReporter) << "Saving log from fd" << logDupFd;
auto logDupStatus = tryDup(logDupFd, crashDir.filePath("log.qslog.log"));
if (logDupStatus != 0) { if (logDupStatus != 0) {
qCCritical(logCrashReporter) << "Failed to save log:" << logDupStatus; qCCritical(logCrashReporter) << "Failed to save log:" << logDupStatus;
} }
auto copyBinStatus = 0;
if (!DISTRIBUTOR_DEBUGINFO_AVAILABLE) {
qCDebug(logCrashReporter) << "Copying binary to crash folder";
if (!QFile(QCoreApplication::applicationFilePath()).copy(crashDir.filePath("executable.txt"))) {
copyBinStatus = 1;
qCCritical(logCrashReporter) << "Failed to copy binary.";
}
}
{ {
auto extraInfoFile = QFile(crashDir.filePath("info.txt")); auto extraInfoFile = QFile(crashDir.filePath("report.txt"));
if (!extraInfoFile.open(QFile::WriteOnly)) { if (!extraInfoFile.open(QFile::WriteOnly)) {
qCCritical(logCrashReporter) << "Failed to open crash info file for writing."; qCCritical(logCrashReporter) << "Failed to open crash info file for writing.";
} else { } else {
@ -111,16 +181,12 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) {
stream << "\n===== Runtime Information =====\n"; stream << "\n===== Runtime Information =====\n";
stream << "Runtime Qt Version: " << qVersion() << '\n'; stream << "Runtime Qt Version: " << qVersion() << '\n';
stream << "Signal: " << strsignal(crashSignal) << " (" << crashSignal << ")\n"; // NOLINT
stream << "Crashed process ID: " << crashProc << '\n'; stream << "Crashed process ID: " << crashProc << '\n';
stream << "Run ID: " << instance.instanceId << '\n'; stream << "Run ID: " << instance.instanceId << '\n';
stream << "Shell ID: " << instance.shellId << '\n'; stream << "Shell ID: " << instance.shellId << '\n';
stream << "Config Path: " << instance.configPath << '\n'; stream << "Config Path: " << instance.configPath << '\n';
stream << "\n===== Report Integrity =====\n";
stream << "Minidump save status: " << dumpDupStatus << '\n';
stream << "Log save status: " << logDupStatus << '\n';
stream << "Binary copy status: " << copyBinStatus << '\n';
stream << "\n===== System Information =====\n\n"; stream << "\n===== System Information =====\n\n";
stream << "/etc/os-release:"; stream << "/etc/os-release:";
auto osReleaseFile = QFile("/etc/os-release"); auto osReleaseFile = QFile("/etc/os-release");
@ -140,6 +206,18 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) {
stream << "FAILED TO OPEN\n"; stream << "FAILED TO OPEN\n";
} }
stream << "\n===== Stacktrace =====\n";
if (stacktrace.empty()) {
stream << "(no trace available)\n";
} else {
auto formatter = cpptrace::formatter().header(std::string());
auto traceStr = formatter.format(stacktrace);
stream << QString::fromStdString(traceStr) << '\n';
}
stream << "\n===== Log Tail =====\n";
stream << recentLogs;
extraInfoFile.close(); extraInfoFile.close();
} }
} }
@ -161,7 +239,10 @@ void qsCheckCrash(int argc, char** argv) {
auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt(); auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt();
QFile file; QFile file;
file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle); if (!file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle)) {
qFatal() << "Failed to open instance info fd.";
}
file.seek(0); file.seek(0);
auto ds = QDataStream(&file); auto ds = QDataStream(&file);

View file

@ -183,7 +183,7 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString
} }
} else if (removed.isEmpty() || removed.contains("icon-data")) { } else if (removed.isEmpty() || removed.contains("icon-data")) {
imageChanged = this->image.hasData(); imageChanged = this->image.hasData();
image.data.clear(); this->image.data.clear();
} }
auto type = properties.value("type"); auto type = properties.value("type");
@ -312,8 +312,8 @@ void DBusMenu::prepareToShow(qint32 item, qint32 depth) {
auto responseCallback = [this, item, depth](QDBusPendingCallWatcher* call) { auto responseCallback = [this, item, depth](QDBusPendingCallWatcher* call) {
const QDBusPendingReply<bool> reply = *call; const QDBusPendingReply<bool> reply = *call;
if (reply.isError()) { if (reply.isError()) {
qCWarning(logDbusMenu) << "Error in AboutToShow, but showing anyway for menu" << item << "of" qCDebug(logDbusMenu) << "Error in AboutToShow, but showing anyway for menu" << item << "of"
<< this << reply.error(); << this << reply.error();
} }
this->updateLayout(item, depth); this->updateLayout(item, depth);

View file

@ -36,7 +36,7 @@ class DBusMenuPngImage: public QsIndexedImageHandle {
public: public:
explicit DBusMenuPngImage(): QsIndexedImageHandle(QQuickImageProvider::Image) {} explicit DBusMenuPngImage(): QsIndexedImageHandle(QQuickImageProvider::Image) {}
[[nodiscard]] bool hasData() const { return !data.isEmpty(); } [[nodiscard]] bool hasData() const { return !this->data.isEmpty(); }
QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override;
QByteArray data; QByteArray data;

View file

@ -214,8 +214,10 @@ void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool co
} }
} }
void DBusPropertyGroup::tryUpdateProperty(DBusPropertyCore* property, const QVariant& variant) void DBusPropertyGroup::tryUpdateProperty(
const { DBusPropertyCore* property,
const QVariant& variant
) const {
property->mExists = true; property->mExists = true;
auto error = property->store(variant); auto error = property->store(variant);
@ -246,8 +248,13 @@ void DBusPropertyGroup::requestPropertyUpdate(DBusPropertyCore* property) {
const QDBusPendingReply<QDBusVariant> reply = *call; const QDBusPendingReply<QDBusVariant> reply = *call;
if (reply.isError()) { if (reply.isError()) {
qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr; if (!property->isRequired() && reply.error().type() == QDBusError::InvalidArgs) {
qCWarning(logDbusProperties) << reply.error(); qCDebug(logDbusProperties) << "Error updating non-required property" << propStr;
qCDebug(logDbusProperties) << reply.error();
} else {
qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr;
qCWarning(logDbusProperties) << reply.error();
}
} else { } else {
this->tryUpdateProperty(property, reply.value().variant()); this->tryUpdateProperty(property, reply.value().variant());
} }

View file

@ -168,9 +168,9 @@ class DBusBindableProperty: public DBusPropertyCore {
public: public:
explicit DBusBindableProperty() { this->group()->attachProperty(this); } explicit DBusBindableProperty() { this->group()->attachProperty(this); }
[[nodiscard]] QString name() const override { return Name; }; [[nodiscard]] QString name() const override { return Name; }
[[nodiscard]] QStringView nameRef() const override { return Name; }; [[nodiscard]] QStringView nameRef() const override { return Name; }
[[nodiscard]] bool isRequired() const override { return required; }; [[nodiscard]] bool isRequired() const override { return required; }
[[nodiscard]] QString valueString() override { [[nodiscard]] QString valueString() override {
QString str; QString str;
@ -217,7 +217,7 @@ protected:
private: private:
[[nodiscard]] constexpr Owner* owner() const { [[nodiscard]] constexpr Owner* owner() const {
auto* self = std::bit_cast<char*>(this); auto* self = std::bit_cast<char*>(this); // NOLINT
return std::bit_cast<Owner*>(self - offset()); // NOLINT return std::bit_cast<Owner*>(self - offset()); // NOLINT
} }

View file

@ -9,7 +9,7 @@
#include <qqmlinfo.h> #include <qqmlinfo.h>
#include <qquickitem.h> #include <qquickitem.h>
#include <qquickwindow.h> #include <qquickwindow.h>
#include <qstringliteral.h> #include <qstring.h>
#include "../core/logcat.hpp" #include "../core/logcat.hpp"

View file

@ -21,9 +21,10 @@ qt_add_qml_module(quickshell-io
FileView.qml FileView.qml
) )
qs_add_module_deps_light(quickshell-io Quickshell)
install_qml_module(quickshell-io) install_qml_module(quickshell-io)
target_link_libraries(quickshell-io PRIVATE Qt::Quick) target_link_libraries(quickshell-io PRIVATE Qt::Quick quickshell-ipc)
target_link_libraries(quickshell PRIVATE quickshell-ioplugin) target_link_libraries(quickshell PRIVATE quickshell-ioplugin)
qs_module_pch(quickshell-io) qs_module_pch(quickshell-io)

View file

@ -55,7 +55,7 @@ public:
// the buffer will be sent in both slots if there is data remaining from a previous parser // the buffer will be sent in both slots if there is data remaining from a previous parser
virtual void parseBytes(QByteArray& incoming, QByteArray& buffer) = 0; virtual void parseBytes(QByteArray& incoming, QByteArray& buffer) = 0;
virtual void streamEnded(QByteArray& /*buffer*/) {}; virtual void streamEnded(QByteArray& /*buffer*/) {}
signals: signals:
/// Emitted when data is read from the stream. /// Emitted when data is read from the stream.
@ -63,7 +63,7 @@ signals:
}; };
///! DataStreamParser for delimited data streams. ///! DataStreamParser for delimited data streams.
/// DataStreamParser for delimited data streams. @@read() is emitted once per delimited chunk of the stream. /// DataStreamParser for delimited data streams. @@DataStreamParser.read(s) is emitted once per delimited chunk of the stream.
class SplitParser: public DataStreamParser { class SplitParser: public DataStreamParser {
Q_OBJECT; Q_OBJECT;
/// The delimiter for parsed data. May be multiple characters. Defaults to `\n`. /// The delimiter for parsed data. May be multiple characters. Defaults to `\n`.

View file

@ -93,7 +93,8 @@ void FileViewReader::run() {
FileViewReader::read(this->owner, this->state, this->doStringConversion, this->shouldCancel); FileViewReader::read(this->owner, this->state, this->doStringConversion, this->shouldCancel);
if (this->shouldCancel.loadAcquire()) { if (this->shouldCancel.loadAcquire()) {
qCDebug(logFileView) << "Read" << this << "of" << state.path << "canceled for" << this->owner; qCDebug(logFileView) << "Read" << this << "of" << this->state.path << "canceled for"
<< this->owner;
} }
} }
@ -206,7 +207,7 @@ void FileViewWriter::run() {
FileViewWriter::write(this->owner, this->state, this->doAtomicWrite, this->shouldCancel); FileViewWriter::write(this->owner, this->state, this->doAtomicWrite, this->shouldCancel);
if (this->shouldCancel.loadAcquire()) { if (this->shouldCancel.loadAcquire()) {
qCDebug(logFileView) << "Write" << this << "of" << state.path << "canceled for" qCDebug(logFileView) << "Write" << this << "of" << this->state.path << "canceled for"
<< this->owner; << this->owner;
} }
} }

View file

@ -190,6 +190,14 @@ QString WirePropertyDefinition::toString() const {
return "property " % this->name % ": " % this->type; return "property " % this->name % ": " % this->type;
} }
QString WireSignalDefinition::toString() const {
if (this->rettype.isEmpty()) {
return "signal " % this->name % "()";
} else {
return "signal " % this->name % "(" % this->retname % ": " % this->rettype % ')';
}
}
QString WireTargetDefinition::toString() const { QString WireTargetDefinition::toString() const {
QString accum = "target " % this->name; QString accum = "target " % this->name;
@ -201,6 +209,10 @@ QString WireTargetDefinition::toString() const {
accum += "\n " % prop.toString(); accum += "\n " % prop.toString();
} }
for (const auto& sig: this->signalFunctions) {
accum += "\n " % sig.toString();
}
return accum; return accum;
} }

View file

@ -146,14 +146,31 @@ struct WirePropertyDefinition {
DEFINE_SIMPLE_DATASTREAM_OPS(WirePropertyDefinition, data.name, data.type); DEFINE_SIMPLE_DATASTREAM_OPS(WirePropertyDefinition, data.name, data.type);
struct WireTargetDefinition { struct WireSignalDefinition {
QString name; QString name;
QVector<WireFunctionDefinition> functions; QString retname;
QVector<WirePropertyDefinition> properties; QString rettype;
[[nodiscard]] QString toString() const; [[nodiscard]] QString toString() const;
}; };
DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions, data.properties); DEFINE_SIMPLE_DATASTREAM_OPS(WireSignalDefinition, data.name, data.retname, data.rettype);
struct WireTargetDefinition {
QString name;
QVector<WireFunctionDefinition> functions;
QVector<WirePropertyDefinition> properties;
QVector<WireSignalDefinition> signalFunctions;
[[nodiscard]] QString toString() const;
};
DEFINE_SIMPLE_DATASTREAM_OPS(
WireTargetDefinition,
data.name,
data.functions,
data.properties,
data.signalFunctions
);
} // namespace qs::io::ipc } // namespace qs::io::ipc

View file

@ -1,10 +1,11 @@
#include "ipccomm.hpp" #include "ipccomm.hpp"
#include <cstdio> #include <utility>
#include <variant> #include <variant>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qlogging.h> #include <qlogging.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qobject.h>
#include <qtextstream.h> #include <qtextstream.h>
#include <qtypes.h> #include <qtypes.h>
@ -19,10 +20,6 @@ using namespace qs::ipc;
namespace qs::io::ipc::comm { namespace qs::io::ipc::comm {
struct NoCurrentGeneration: std::monostate {};
struct TargetNotFound: std::monostate {};
struct EntryNotFound: std::monostate {};
using QueryResponse = std::variant< using QueryResponse = std::variant<
std::monostate, std::monostate,
NoCurrentGeneration, NoCurrentGeneration,
@ -314,4 +311,106 @@ int getProperty(IpcClient* client, const QString& target, const QString& propert
return -1; return -1;
} }
int listenToSignal(IpcClient* client, const QString& target, const QString& signal, bool once) {
if (target.isEmpty()) {
qCCritical(logBare) << "Target required to listen for signals.";
return -1;
} else if (signal.isEmpty()) {
qCCritical(logBare) << "Signal required to listen.";
return -1;
}
client->sendMessage(IpcCommand(SignalListenCommand {.target = target, .signal = signal}));
while (true) {
SignalListenResponse slot;
if (!client->waitForResponse(slot)) return -1;
if (std::holds_alternative<SignalResponse>(slot)) {
auto& result = std::get<SignalResponse>(slot);
QTextStream(stdout) << result.response << Qt::endl;
if (once) return 0;
else continue;
} else if (std::holds_alternative<TargetNotFound>(slot)) {
qCCritical(logBare) << "Target not found.";
} else if (std::holds_alternative<EntryNotFound>(slot)) {
qCCritical(logBare) << "Signal not found.";
} else if (std::holds_alternative<NoCurrentGeneration>(slot)) {
qCCritical(logBare) << "Not ready to accept queries yet.";
} else {
qCCritical(logIpc) << "Received invalid IPC response from" << client;
}
break;
}
return -1;
}
void SignalListenCommand::exec(qs::ipc::IpcServerConnection* conn) {
auto resp = conn->responseStream<SignalListenResponse>();
if (auto* generation = EngineGeneration::currentGeneration()) {
auto* registry = IpcHandlerRegistry::forGeneration(generation);
auto* handler = registry->findHandler(this->target);
if (!handler) {
resp << TargetNotFound();
return;
}
auto* signal = handler->findSignal(this->signal);
if (!signal) {
resp << EntryNotFound();
return;
}
new RemoteSignalListener(conn, *this);
} else {
conn->respond(SignalListenResponse(NoCurrentGeneration()));
}
}
RemoteSignalListener::RemoteSignalListener(
qs::ipc::IpcServerConnection* conn,
SignalListenCommand command
)
: conn(conn)
, command(std::move(command)) {
conn->setParent(this);
QObject::connect(
IpcSignalRemoteListener::instance(),
&IpcSignalRemoteListener::triggered,
this,
&RemoteSignalListener::onSignal
);
QObject::connect(
conn,
&qs::ipc::IpcServerConnection::destroyed,
this,
&RemoteSignalListener::onConnDestroyed
);
qCDebug(logIpc) << "Remote listener created for" << this->command.target << this->command.signal
<< ":" << this;
}
RemoteSignalListener::~RemoteSignalListener() {
qCDebug(logIpc) << "Destroying remote listener" << this;
}
void RemoteSignalListener::onSignal(
const QString& target,
const QString& signal,
const QString& value
) {
if (target != this->command.target || signal != this->command.signal) return;
qCDebug(logIpc) << "Remote signal" << signal << "triggered on" << target << "with value" << value;
this->conn->respond(SignalListenResponse(SignalResponse {.response = value}));
}
void RemoteSignalListener::onConnDestroyed() { this->deleteLater(); }
} // namespace qs::io::ipc::comm } // namespace qs::io::ipc::comm

View file

@ -2,6 +2,8 @@
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qflags.h> #include <qflags.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qtypes.h> #include <qtypes.h>
#include "../ipc/ipc.hpp" #include "../ipc/ipc.hpp"
@ -48,4 +50,52 @@ DEFINE_SIMPLE_DATASTREAM_OPS(StringPropReadCommand, data.target, data.property);
int getProperty(qs::ipc::IpcClient* client, const QString& target, const QString& property); int getProperty(qs::ipc::IpcClient* client, const QString& target, const QString& property);
struct SignalListenCommand {
QString target;
QString signal;
void exec(qs::ipc::IpcServerConnection* conn);
};
DEFINE_SIMPLE_DATASTREAM_OPS(SignalListenCommand, data.target, data.signal);
int listenToSignal(
qs::ipc::IpcClient* client,
const QString& target,
const QString& signal,
bool once
);
struct NoCurrentGeneration: std::monostate {};
struct TargetNotFound: std::monostate {};
struct EntryNotFound: std::monostate {};
struct SignalResponse {
QString response;
};
DEFINE_SIMPLE_DATASTREAM_OPS(SignalResponse, data.response);
using SignalListenResponse = std::
variant<std::monostate, NoCurrentGeneration, TargetNotFound, EntryNotFound, SignalResponse>;
class RemoteSignalListener: public QObject {
Q_OBJECT;
public:
explicit RemoteSignalListener(qs::ipc::IpcServerConnection* conn, SignalListenCommand command);
~RemoteSignalListener() override;
Q_DISABLE_COPY_MOVE(RemoteSignalListener);
private slots:
void onSignal(const QString& target, const QString& signal, const QString& value);
void onConnDestroyed();
private:
qs::ipc::IpcServerConnection* conn;
SignalListenCommand command;
};
} // namespace qs::io::ipc::comm } // namespace qs::io::ipc::comm

View file

@ -1,5 +1,7 @@
#include "ipchandler.hpp" #include "ipchandler.hpp"
#include <cstddef> #include <cstddef>
#include <memory>
#include <utility>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qdebug.h> #include <qdebug.h>
@ -139,6 +141,75 @@ WirePropertyDefinition IpcProperty::wireDef() const {
return wire; return wire;
} }
WireSignalDefinition IpcSignal::wireDef() const {
WireSignalDefinition wire;
wire.name = this->signal.name();
if (this->targetSlot != IpcSignalListener::SLOT_VOID) {
wire.retname = this->signal.parameterNames().value(0);
if (this->targetSlot == IpcSignalListener::SLOT_STRING) wire.rettype = "string";
else if (this->targetSlot == IpcSignalListener::SLOT_INT) wire.rettype = "int";
else if (this->targetSlot == IpcSignalListener::SLOT_BOOL) wire.rettype = "bool";
else if (this->targetSlot == IpcSignalListener::SLOT_REAL) wire.rettype = "real";
else if (this->targetSlot == IpcSignalListener::SLOT_COLOR) wire.rettype = "color";
}
return wire;
}
// NOLINTBEGIN (cppcoreguidelines-interfaces-global-init)
// clang-format off
const int IpcSignalListener::SLOT_VOID = IpcSignalListener::staticMetaObject.indexOfSlot("invokeVoid()");
const int IpcSignalListener::SLOT_STRING = IpcSignalListener::staticMetaObject.indexOfSlot("invokeString(QString)");
const int IpcSignalListener::SLOT_INT = IpcSignalListener::staticMetaObject.indexOfSlot("invokeInt(int)");
const int IpcSignalListener::SLOT_BOOL = IpcSignalListener::staticMetaObject.indexOfSlot("invokeBool(bool)");
const int IpcSignalListener::SLOT_REAL = IpcSignalListener::staticMetaObject.indexOfSlot("invokeReal(double)");
const int IpcSignalListener::SLOT_COLOR = IpcSignalListener::staticMetaObject.indexOfSlot("invokeColor(QColor)");
// clang-format on
// NOLINTEND
bool IpcSignal::resolve(QString& error) {
if (this->signal.parameterCount() > 1) {
error = "Due to technical limitations, IPC signals can have at most one argument.";
return false;
}
auto slot = IpcSignalListener::SLOT_VOID;
if (this->signal.parameterCount() == 1) {
auto paramType = this->signal.parameterType(0);
if (paramType == QMetaType::QString) slot = IpcSignalListener::SLOT_STRING;
else if (paramType == QMetaType::Int) slot = IpcSignalListener::SLOT_INT;
else if (paramType == QMetaType::Bool) slot = IpcSignalListener::SLOT_BOOL;
else if (paramType == QMetaType::Double) slot = IpcSignalListener::SLOT_REAL;
else if (paramType == QMetaType::QColor) slot = IpcSignalListener::SLOT_COLOR;
else {
error = QString("Type of argument (%2: %3) cannot be used across IPC.")
.arg(this->signal.parameterNames().value(0))
.arg(QMetaType(paramType).name());
return false;
}
}
this->targetSlot = slot;
return true;
}
void IpcSignal::connectListener(IpcHandler* handler) {
if (this->targetSlot == -1) {
qFatal() << "Tried to connect unresolved IPC signal";
}
this->listener = std::make_shared<IpcSignalListener>(this->signal.name());
QMetaObject::connect(handler, this->signal.methodIndex(), this->listener.get(), this->targetSlot);
QObject::connect(
this->listener.get(),
&IpcSignalListener::triggered,
handler,
&IpcHandler::onSignalTriggered
);
}
IpcCallStorage::IpcCallStorage(const IpcFunction& function): returnSlot(function.returnType) { IpcCallStorage::IpcCallStorage(const IpcFunction& function): returnSlot(function.returnType) {
for (const auto& arg: function.argumentTypes) { for (const auto& arg: function.argumentTypes) {
this->argumentSlots.emplace_back(arg); this->argumentSlots.emplace_back(arg);
@ -172,16 +243,28 @@ void IpcHandler::onPostReload() {
// which should handle inheritance on the qml side. // which should handle inheritance on the qml side.
for (auto i = smeta.methodCount(); i != meta->methodCount(); i++) { for (auto i = smeta.methodCount(); i != meta->methodCount(); i++) {
const auto& method = meta->method(i); const auto& method = meta->method(i);
if (method.methodType() != QMetaMethod::Slot) continue; if (method.methodType() == QMetaMethod::Slot) {
auto ipcFunc = IpcFunction(method);
QString error;
auto ipcFunc = IpcFunction(method); if (!ipcFunc.resolve(error)) {
QString error; qmlWarning(this).nospace().noquote()
<< "Error parsing function \"" << method.name() << "\": " << error;
} else {
this->functionMap.insert(method.name(), ipcFunc);
}
} else if (method.methodType() == QMetaMethod::Signal) {
qmlDebug(this) << "Signal detected: " << method.name();
auto ipcSig = IpcSignal(method);
QString error;
if (!ipcFunc.resolve(error)) { if (!ipcSig.resolve(error)) {
qmlWarning(this).nospace().noquote() qmlWarning(this).nospace().noquote()
<< "Error parsing function \"" << method.name() << "\": " << error; << "Error parsing signal \"" << method.name() << "\": " << error;
} else { } else {
this->functionMap.insert(method.name(), ipcFunc); ipcSig.connectListener(this);
this->signalMap.emplace(method.name(), std::move(ipcSig));
}
} }
} }
@ -222,6 +305,11 @@ IpcHandlerRegistry* IpcHandlerRegistry::forGeneration(EngineGeneration* generati
return dynamic_cast<IpcHandlerRegistry*>(ext); return dynamic_cast<IpcHandlerRegistry*>(ext);
} }
void IpcHandler::onSignalTriggered(const QString& signal, const QString& value) const {
emit IpcSignalRemoteListener::instance()
-> triggered(this->registeredState.target, signal, value);
}
void IpcHandler::updateRegistration(bool destroying) { void IpcHandler::updateRegistration(bool destroying) {
if (!this->complete) return; if (!this->complete) return;
@ -324,6 +412,10 @@ WireTargetDefinition IpcHandler::wireDef() const {
wire.properties += prop.wireDef(); wire.properties += prop.wireDef();
} }
for (const auto& sig: this->signalMap.values()) {
wire.signalFunctions += sig.wireDef();
}
return wire; return wire;
} }
@ -368,6 +460,13 @@ IpcProperty* IpcHandler::findProperty(const QString& name) {
else return &*itr; else return &*itr;
} }
IpcSignal* IpcHandler::findSignal(const QString& name) {
auto itr = this->signalMap.find(name);
if (itr == this->signalMap.end()) return nullptr;
else return &*itr;
}
IpcHandler* IpcHandlerRegistry::findHandler(const QString& target) { IpcHandler* IpcHandlerRegistry::findHandler(const QString& target) {
return this->handlers.value(target); return this->handlers.value(target);
} }
@ -382,4 +481,9 @@ QVector<WireTargetDefinition> IpcHandlerRegistry::wireTargets() const {
return wire; return wire;
} }
IpcSignalRemoteListener* IpcSignalRemoteListener::instance() {
static auto* instance = new IpcSignalRemoteListener();
return instance;
}
} // namespace qs::io::ipc } // namespace qs::io::ipc

View file

@ -1,8 +1,10 @@
#pragma once #pragma once
#include <cstddef> #include <cstddef>
#include <memory>
#include <vector> #include <vector>
#include <qcolor.h>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qdebug.h> #include <qdebug.h>
#include <qhash.h> #include <qhash.h>
@ -67,6 +69,54 @@ public:
const IpcType* type = nullptr; const IpcType* type = nullptr;
}; };
class IpcSignalListener: public QObject {
Q_OBJECT;
public:
IpcSignalListener(QString signal): signal(std::move(signal)) {}
static const int SLOT_VOID;
static const int SLOT_STRING;
static const int SLOT_INT;
static const int SLOT_BOOL;
static const int SLOT_REAL;
static const int SLOT_COLOR;
signals:
void triggered(const QString& signal, const QString& value);
private slots:
void invokeVoid() { this->triggered(this->signal, "void"); }
void invokeString(const QString& value) { this->triggered(this->signal, value); }
void invokeInt(int value) { this->triggered(this->signal, QString::number(value)); }
void invokeBool(bool value) { this->triggered(this->signal, value ? "true" : "false"); }
void invokeReal(double value) { this->triggered(this->signal, QString::number(value)); }
void invokeColor(QColor value) { this->triggered(this->signal, value.name(QColor::HexArgb)); }
private:
QString signal;
};
class IpcHandler;
class IpcSignal {
public:
explicit IpcSignal(QMetaMethod signal): signal(signal) {}
bool resolve(QString& error);
[[nodiscard]] WireSignalDefinition wireDef() const;
QMetaMethod signal;
int targetSlot = -1;
void connectListener(IpcHandler* handler);
private:
void connectListener(QObject* handler, IpcSignalListener* listener) const;
std::shared_ptr<IpcSignalListener> listener;
};
class IpcHandlerRegistry; class IpcHandlerRegistry;
///! Handler for IPC message calls. ///! Handler for IPC message calls.
@ -100,6 +150,11 @@ class IpcHandlerRegistry;
/// - `real` will be converted to a string and returned. /// - `real` will be converted to a string and returned.
/// - `color` will be converted to a hex string in the form `#AARRGGBB` and returned. /// - `color` will be converted to a hex string in the form `#AARRGGBB` and returned.
/// ///
/// #### Signals
/// IPC handler signals can be observed remotely using `qs ipc wait` (one call)
/// and `qs ipc listen` (many calls). IPC signals may have zero or one argument, where
/// the argument is one of the types listed above, or no arguments for void.
///
/// #### Example /// #### Example
/// The following example creates ipc functions to control and retrieve the appearance /// The following example creates ipc functions to control and retrieve the appearance
/// of a Rectangle. /// of a Rectangle.
@ -119,10 +174,18 @@ class IpcHandlerRegistry;
/// ///
/// function setColor(color: color): void { rect.color = color; } /// function setColor(color: color): void { rect.color = color; }
/// function getColor(): color { return rect.color; } /// function getColor(): color { return rect.color; }
///
/// function setAngle(angle: real): void { rect.rotation = angle; } /// function setAngle(angle: real): void { rect.rotation = angle; }
/// function getAngle(): real { return rect.rotation; } /// function getAngle(): real { return rect.rotation; }
/// function setRadius(radius: int): void { rect.radius = radius; } ///
/// function setRadius(radius: int): void {
/// rect.radius = radius;
/// this.radiusChanged(radius);
/// }
///
/// function getRadius(): int { return rect.radius; } /// function getRadius(): int { return rect.radius; }
///
/// signal radiusChanged(newRadius: int);
/// } /// }
/// } /// }
/// ``` /// ```
@ -136,6 +199,7 @@ class IpcHandlerRegistry;
/// function getAngle(): real /// function getAngle(): real
/// function setRadius(radius: int): void /// function setRadius(radius: int): void
/// function getRadius(): int /// function getRadius(): int
/// signal radiusChanged(newRadius: int)
/// ``` /// ```
/// ///
/// and then invoked using `qs ipc call`. /// and then invoked using `qs ipc call`.
@ -164,7 +228,7 @@ class IpcHandler: public PostReloadHook {
QML_ELEMENT; QML_ELEMENT;
public: public:
explicit IpcHandler(QObject* parent = nullptr): PostReloadHook(parent) {}; explicit IpcHandler(QObject* parent = nullptr): PostReloadHook(parent) {}
~IpcHandler() override; ~IpcHandler() override;
Q_DISABLE_COPY_MOVE(IpcHandler); Q_DISABLE_COPY_MOVE(IpcHandler);
@ -179,14 +243,15 @@ public:
QString listMembers(qsizetype indent); QString listMembers(qsizetype indent);
[[nodiscard]] IpcFunction* findFunction(const QString& name); [[nodiscard]] IpcFunction* findFunction(const QString& name);
[[nodiscard]] IpcProperty* findProperty(const QString& name); [[nodiscard]] IpcProperty* findProperty(const QString& name);
[[nodiscard]] IpcSignal* findSignal(const QString& name);
[[nodiscard]] WireTargetDefinition wireDef() const; [[nodiscard]] WireTargetDefinition wireDef() const;
signals: signals:
void enabledChanged(); void enabledChanged();
void targetChanged(); void targetChanged();
private slots: public slots:
//void handleIpcPropertyChange(); void onSignalTriggered(const QString& signal, const QString& value) const;
private: private:
void updateRegistration(bool destroying = false); void updateRegistration(bool destroying = false);
@ -204,6 +269,7 @@ private:
QHash<QString, IpcFunction> functionMap; QHash<QString, IpcFunction> functionMap;
QHash<QString, IpcProperty> propertyMap; QHash<QString, IpcProperty> propertyMap;
QHash<QString, IpcSignal> signalMap;
friend class IpcHandlerRegistry; friend class IpcHandlerRegistry;
}; };
@ -227,4 +293,14 @@ private:
QHash<QString, QVector<IpcHandler*>> knownHandlers; QHash<QString, QVector<IpcHandler*>> knownHandlers;
}; };
class IpcSignalRemoteListener: public QObject {
Q_OBJECT;
public:
static IpcSignalRemoteListener* instance();
signals:
void triggered(const QString& target, const QString& signal, const QString& value);
};
} // namespace qs::io::ipc } // namespace qs::io::ipc

View file

@ -44,7 +44,7 @@ void JsonAdapter::deserializeAdapter(const QByteArray& data) {
this->deserializeRec(json.object(), this, &JsonAdapter::staticMetaObject); this->deserializeRec(json.object(), this, &JsonAdapter::staticMetaObject);
for (auto* object: oldCreatedObjects) { for (auto* object: this->oldCreatedObjects) {
delete object; // FIXME: QMetaType::destroy? delete object; // FIXME: QMetaType::destroy?
} }
@ -56,7 +56,7 @@ void JsonAdapter::deserializeAdapter(const QByteArray& data) {
void JsonAdapter::connectNotifiers() { void JsonAdapter::connectNotifiers() {
auto notifySlot = JsonAdapter::staticMetaObject.indexOfSlot("onPropertyChanged()"); auto notifySlot = JsonAdapter::staticMetaObject.indexOfSlot("onPropertyChanged()");
connectNotifiersRec(notifySlot, this, &JsonAdapter::staticMetaObject); this->connectNotifiersRec(notifySlot, this, &JsonAdapter::staticMetaObject);
} }
void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaObject* base) { void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaObject* base) {
@ -71,7 +71,7 @@ void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaO
auto val = prop.read(obj); auto val = prop.read(obj);
if (val.canView<JsonObject*>()) { if (val.canView<JsonObject*>()) {
auto* pobj = prop.read(obj).view<JsonObject*>(); auto* pobj = prop.read(obj).view<JsonObject*>();
if (pobj) connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject); if (pobj) this->connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject);
} else if (val.canConvert<QQmlListProperty<JsonObject>>()) { } else if (val.canConvert<QQmlListProperty<JsonObject>>()) {
auto listVal = val.value<QQmlListProperty<JsonObject>>(); auto listVal = val.value<QQmlListProperty<JsonObject>>();
@ -79,7 +79,7 @@ void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaO
for (auto i = 0; i != len; i++) { for (auto i = 0; i != len; i++) {
auto* pobj = listVal.at(&listVal, i); auto* pobj = listVal.at(&listVal, i);
if (pobj) connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject); if (pobj) this->connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject);
} }
} }
} }
@ -111,7 +111,7 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas
auto* pobj = val.view<JsonObject*>(); auto* pobj = val.view<JsonObject*>();
if (pobj) { if (pobj) {
json.insert(prop.name(), serializeRec(pobj, &JsonObject::staticMetaObject)); json.insert(prop.name(), this->serializeRec(pobj, &JsonObject::staticMetaObject));
} else { } else {
json.insert(prop.name(), QJsonValue::Null); json.insert(prop.name(), QJsonValue::Null);
} }
@ -124,7 +124,7 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas
auto* pobj = listVal.at(&listVal, i); auto* pobj = listVal.at(&listVal, i);
if (pobj) { if (pobj) {
array.push_back(serializeRec(pobj, &JsonObject::staticMetaObject)); array.push_back(this->serializeRec(pobj, &JsonObject::staticMetaObject));
} else { } else {
array.push_back(QJsonValue::Null); array.push_back(QJsonValue::Null);
} }
@ -178,8 +178,8 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM
currentValue->setParent(this); currentValue->setParent(this);
this->createdObjects.push_back(currentValue); this->createdObjects.push_back(currentValue);
} else if (oldCreatedObjects.removeOne(currentValue)) { } else if (this->oldCreatedObjects.removeOne(currentValue)) {
createdObjects.push_back(currentValue); this->createdObjects.push_back(currentValue);
} }
this->deserializeRec(jval.toObject(), currentValue, &JsonObject::staticMetaObject); this->deserializeRec(jval.toObject(), currentValue, &JsonObject::staticMetaObject);
@ -212,8 +212,8 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM
if (jsonValue.isObject()) { if (jsonValue.isObject()) {
if (isNew) { if (isNew) {
currentValue = lp.at(&lp, i); currentValue = lp.at(&lp, i);
if (oldCreatedObjects.removeOne(currentValue)) { if (this->oldCreatedObjects.removeOne(currentValue)) {
createdObjects.push_back(currentValue); this->createdObjects.push_back(currentValue);
} }
} else { } else {
// FIXME: should be the type inside the QQmlListProperty but how can we get that? // FIXME: should be the type inside the QQmlListProperty but how can we get that?

View file

@ -91,6 +91,7 @@ class JsonAdapter
, public QQmlParserStatus { , public QQmlParserStatus {
Q_OBJECT; Q_OBJECT;
QML_ELEMENT; QML_ELEMENT;
Q_INTERFACES(QQmlParserStatus);
public: public:
void classBegin() override {} void classBegin() override {}

View file

@ -102,7 +102,7 @@ class Process: public PostReloadHook {
/// If the process is already running changing this property will affect the next /// If the process is already running changing this property will affect the next
/// started process. If the property has been changed after starting a process it will /// started process. If the property has been changed after starting a process it will
/// return the new value, not the one for the currently running process. /// return the new value, not the one for the currently running process.
Q_PROPERTY(QHash<QString, QVariant> environment READ environment WRITE setEnvironment NOTIFY environmentChanged); Q_PROPERTY(QVariantHash environment READ environment WRITE setEnvironment NOTIFY environmentChanged);
/// If the process's environment should be cleared prior to applying @@environment. /// If the process's environment should be cleared prior to applying @@environment.
/// Defaults to false. /// Defaults to false.
/// ///

View file

@ -13,7 +13,7 @@ namespace qs::io::process {
class ProcessContext { class ProcessContext {
Q_PROPERTY(QList<QString> command MEMBER command WRITE setCommand); Q_PROPERTY(QList<QString> command MEMBER command WRITE setCommand);
Q_PROPERTY(QHash<QString, QVariant> environment MEMBER environment WRITE setEnvironment); Q_PROPERTY(QVariantHash environment MEMBER environment WRITE setEnvironment);
Q_PROPERTY(bool clearEnvironment MEMBER clearEnvironment WRITE setClearEnvironment); Q_PROPERTY(bool clearEnvironment MEMBER clearEnvironment WRITE setClearEnvironment);
Q_PROPERTY(QString workingDirectory MEMBER workingDirectory WRITE setWorkingDirectory); Q_PROPERTY(QString workingDirectory MEMBER workingDirectory WRITE setWorkingDirectory);
Q_PROPERTY(bool unbindStdout MEMBER unbindStdout WRITE setUnbindStdout); Q_PROPERTY(bool unbindStdout MEMBER unbindStdout WRITE setUnbindStdout);

View file

@ -3,6 +3,7 @@
#include <variant> #include <variant>
#include <qbuffer.h> #include <qbuffer.h>
#include <qcoreapplication.h>
#include <qlocalserver.h> #include <qlocalserver.h>
#include <qlocalsocket.h> #include <qlocalsocket.h>
#include <qlogging.h> #include <qlogging.h>
@ -36,7 +37,8 @@ void IpcServer::start() {
auto path = run->filePath("ipc.sock"); auto path = run->filePath("ipc.sock");
new IpcServer(path); new IpcServer(path);
} else { } else {
qCCritical(logIpc qCCritical(
logIpc
) << "Could not start IPC server as the instance runtime path could not be created."; ) << "Could not start IPC server as the instance runtime path could not be created.";
} }
} }
@ -60,6 +62,7 @@ IpcServerConnection::IpcServerConnection(QLocalSocket* socket, IpcServer* server
void IpcServerConnection::onDisconnected() { void IpcServerConnection::onDisconnected() {
qCInfo(logIpc) << "IPC connection disconnected" << this; qCInfo(logIpc) << "IPC connection disconnected" << this;
this->deleteLater();
} }
void IpcServerConnection::onReadyRead() { void IpcServerConnection::onReadyRead() {
@ -83,6 +86,11 @@ void IpcServerConnection::onReadyRead() {
); );
if (!this->stream.commitTransaction()) return; if (!this->stream.commitTransaction()) return;
// async connections reparent
if (dynamic_cast<IpcServer*>(this->parent()) != nullptr) {
this->deleteLater();
}
} }
IpcClient::IpcClient(const QString& path) { IpcClient::IpcClient(const QString& path) {
@ -120,7 +128,9 @@ int IpcClient::connect(const QString& id, const std::function<void(IpcClient& cl
void IpcKillCommand::exec(IpcServerConnection* /*unused*/) { void IpcKillCommand::exec(IpcServerConnection* /*unused*/) {
qInfo() << "Exiting due to IPC request."; qInfo() << "Exiting due to IPC request.";
EngineGeneration::currentGeneration()->quit(); auto* generation = EngineGeneration::currentGeneration();
if (generation) generation->quit();
else QCoreApplication::exit(0);
} }
} // namespace qs::ipc } // namespace qs::ipc

View file

@ -16,6 +16,7 @@ using IpcCommand = std::variant<
IpcKillCommand, IpcKillCommand,
qs::io::ipc::comm::QueryMetadataCommand, qs::io::ipc::comm::QueryMetadataCommand,
qs::io::ipc::comm::StringCallCommand, qs::io::ipc::comm::StringCallCommand,
qs::io::ipc::comm::SignalListenCommand,
qs::io::ipc::comm::StringPropReadCommand>; qs::io::ipc::comm::StringPropReadCommand>;
} // namespace qs::ipc } // namespace qs::ipc

View file

@ -1,7 +1,6 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <cerrno> #include <cerrno>
#include <cstdio>
#include <cstring> #include <cstring>
#include <utility> #include <utility>
@ -13,6 +12,7 @@
#include <qdebug.h> #include <qdebug.h>
#include <qdir.h> #include <qdir.h>
#include <qfileinfo.h> #include <qfileinfo.h>
#include <qguiapplication.h>
#include <qjsonarray.h> #include <qjsonarray.h>
#include <qjsondocument.h> #include <qjsondocument.h>
#include <qjsonobject.h> #include <qjsonobject.h>
@ -89,9 +89,9 @@ int locateConfigFile(CommandState& cmd, QString& path) {
} }
if (!manifestPath.isEmpty()) { if (!manifestPath.isEmpty()) {
qWarning( qWarning()
) << "Config manifests (manifest.conf) are deprecated and will be removed in a future " << "Config manifests (manifest.conf) are deprecated and will be removed in a future "
"release."; "release.";
qWarning() << "Consider using symlinks to a subfolder of quickshell's XDG config dirs."; qWarning() << "Consider using symlinks to a subfolder of quickshell's XDG config dirs.";
auto file = QFile(manifestPath); auto file = QFile(manifestPath);
@ -109,7 +109,7 @@ int locateConfigFile(CommandState& cmd, QString& path) {
} }
if (split[0].trimmed() == *cmd.config.name) { if (split[0].trimmed() == *cmd.config.name) {
path = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed()); path = QDir(QFileInfo(file).absolutePath()).filePath(split[1].trimmed());
break; break;
} }
} }
@ -129,7 +129,8 @@ int locateConfigFile(CommandState& cmd, QString& path) {
if (path.isEmpty()) { if (path.isEmpty()) {
if (name == "default") { if (name == "default") {
qCCritical(logBare qCCritical(
logBare
) << "Could not find \"default\" config directory or shell.qml in any valid config path."; ) << "Could not find \"default\" config directory or shell.qml in any valid config path.";
} else { } else {
qCCritical(logBare) << "Could not find" << name qCCritical(logBare) << "Could not find" << name
@ -139,8 +140,7 @@ int locateConfigFile(CommandState& cmd, QString& path) {
return -1; return -1;
} }
path = QFileInfo(path).canonicalFilePath(); goto rpath;
return 0;
} }
} }
@ -153,7 +153,8 @@ int locateConfigFile(CommandState& cmd, QString& path) {
return -1; return -1;
} }
path = QFileInfo(path).canonicalFilePath(); rpath:
path = QFileInfo(path).absoluteFilePath();
return 0; return 0;
} }
@ -178,7 +179,8 @@ int selectInstance(CommandState& cmd, InstanceLockInfo* instance, bool deadFallb
} }
} else if (!cmd.instance.id->isEmpty()) { } else if (!cmd.instance.id->isEmpty()) {
path = basePath->filePath("by-pid"); path = basePath->filePath("by-pid");
auto [liveInstances, deadInstances] = QsPaths::collectInstances(path); auto [liveInstances, deadInstances] =
QsPaths::collectInstances(path, cmd.config.anyDisplay ? "" : getDisplayConnection());
liveInstances.removeIf([&](const InstanceLockInfo& info) { liveInstances.removeIf([&](const InstanceLockInfo& info) {
return !info.instance.instanceId.startsWith(*cmd.instance.id); return !info.instance.instanceId.startsWith(*cmd.instance.id);
@ -228,7 +230,8 @@ int selectInstance(CommandState& cmd, InstanceLockInfo* instance, bool deadFallb
path = QDir(basePath->filePath("by-path")).filePath(pathId); path = QDir(basePath->filePath("by-path")).filePath(pathId);
auto [liveInstances, deadInstances] = QsPaths::collectInstances(path); auto [liveInstances, deadInstances] =
QsPaths::collectInstances(path, cmd.config.anyDisplay ? "" : getDisplayConnection());
auto instances = liveInstances; auto instances = liveInstances;
if (instances.isEmpty() && deadFallback) { if (instances.isEmpty() && deadFallback) {
@ -311,7 +314,10 @@ int listInstances(CommandState& cmd) {
path = QDir(basePath->filePath("by-path")).filePath(pathId); path = QDir(basePath->filePath("by-path")).filePath(pathId);
} }
auto [liveInstances, deadInstances] = QsPaths::collectInstances(path); auto [liveInstances, deadInstances] = QsPaths::collectInstances(
path,
cmd.config.anyDisplay || cmd.instance.all ? "" : getDisplayConnection()
);
sortInstances(liveInstances, cmd.config.newest); sortInstances(liveInstances, cmd.config.newest);
@ -373,6 +379,7 @@ int listInstances(CommandState& cmd) {
<< " Process ID: " << instance.instance.pid << '\n' << " Process ID: " << instance.instance.pid << '\n'
<< " Shell ID: " << instance.instance.shellId << '\n' << " Shell ID: " << instance.instance.shellId << '\n'
<< " Config path: " << instance.instance.configPath << '\n' << " Config path: " << instance.instance.configPath << '\n'
<< " Display connection: " << instance.instance.display << '\n'
<< " Launch time: " << launchTimeStr << " Launch time: " << launchTimeStr
<< (isDead ? "" : " (running for " + runtimeStr + ")") << '\n' << (isDead ? "" : " (running for " + runtimeStr + ")") << '\n'
<< (gray ? "\033[0m" : ""); << (gray ? "\033[0m" : "");
@ -404,6 +411,10 @@ int ipcCommand(CommandState& cmd) {
return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.name); return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.name);
} else if (*cmd.ipc.getprop) { } else if (*cmd.ipc.getprop) {
return qs::io::ipc::comm::getProperty(&client, *cmd.ipc.target, *cmd.ipc.name); return qs::io::ipc::comm::getProperty(&client, *cmd.ipc.target, *cmd.ipc.name);
} else if (*cmd.ipc.wait) {
return qs::io::ipc::comm::listenToSignal(&client, *cmd.ipc.target, *cmd.ipc.name, true);
} else if (*cmd.ipc.listen) {
return qs::io::ipc::comm::listenToSignal(&client, *cmd.ipc.target, *cmd.ipc.name, false);
} else { } else {
QVector<QString> arguments; QVector<QString> arguments;
for (auto& arg: cmd.ipc.arguments) { for (auto& arg: cmd.ipc.arguments) {
@ -453,7 +464,7 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
QTextStream(stdout) << "\033[31mCOMPATIBILITY WARNING: Quickshell was built against Qt " QTextStream(stdout) << "\033[31mCOMPATIBILITY WARNING: Quickshell was built against Qt "
<< QT_VERSION_STR << " but the system has updated to Qt " << qVersion() << QT_VERSION_STR << " but the system has updated to Qt " << qVersion()
<< " without rebuilding the package. This is likely to cause crashes, so " << " without rebuilding the package. This is likely to cause crashes, so "
"you must rebuild the quickshell package.\n"; "you must rebuild the quickshell package.\n\033[0m";
return 1; return 1;
} }
@ -508,8 +519,8 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
} }
if (state.misc.printVersion) { if (state.misc.printVersion) {
qCInfo(logBare).noquote().nospace() qCInfo(logBare).noquote().nospace() << "quickshell " << QS_VERSION << ", revision "
<< "quickshell 0.1.0, revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR; << GIT_REVISION << ", distributed by: " << DISTRIBUTOR;
if (state.log.verbosity > 1) { if (state.log.verbosity > 1) {
qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR;
@ -545,4 +556,18 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) {
return 0; return 0;
} }
QString getDisplayConnection() {
auto platform = qEnvironmentVariable("QT_QPA_PLATFORM");
auto wlDisplay = qEnvironmentVariable("WAYLAND_DISPLAY");
auto xDisplay = qEnvironmentVariable("DISPLAY");
if (platform == "wayland" || (platform.isEmpty() && !wlDisplay.isEmpty())) {
return "wayland," + wlDisplay;
} else if (platform == "xcb" || (platform.isEmpty() && !xDisplay.isEmpty())) {
return "x11," + xDisplay;
} else {
return "unk," + QGuiApplication::platformName();
}
}
} // namespace qs::launch } // namespace qs::launch

View file

@ -27,7 +27,7 @@
#include "build.hpp" #include "build.hpp"
#include "launch_p.hpp" #include "launch_p.hpp"
#if CRASH_REPORTER #if CRASH_HANDLER
#include "../crash/handler.hpp" #include "../crash/handler.hpp"
#endif #endif
@ -73,10 +73,12 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio
bool useQApplication = false; bool useQApplication = false;
bool nativeTextRendering = false; bool nativeTextRendering = false;
bool desktopSettingsAware = true; bool desktopSettingsAware = true;
bool useSystemStyle = false;
QString iconTheme = qEnvironmentVariable("QS_ICON_THEME"); QString iconTheme = qEnvironmentVariable("QS_ICON_THEME");
QHash<QString, QString> envOverrides; QHash<QString, QString> envOverrides;
QString dataDir; QString dataDir;
QString stateDir; QString stateDir;
QString cacheDir;
} pragmas; } pragmas;
auto stream = QTextStream(&file); auto stream = QTextStream(&file);
@ -88,6 +90,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio
if (pragma == "UseQApplication") pragmas.useQApplication = true; if (pragma == "UseQApplication") pragmas.useQApplication = true;
else if (pragma == "NativeTextRendering") pragmas.nativeTextRendering = true; else if (pragma == "NativeTextRendering") pragmas.nativeTextRendering = true;
else if (pragma == "IgnoreSystemSettings") pragmas.desktopSettingsAware = false; else if (pragma == "IgnoreSystemSettings") pragmas.desktopSettingsAware = false;
else if (pragma == "RespectSystemStyle") pragmas.useSystemStyle = true;
else if (pragma.startsWith("IconTheme ")) pragmas.iconTheme = pragma.sliced(10); else if (pragma.startsWith("IconTheme ")) pragmas.iconTheme = pragma.sliced(10);
else if (pragma.startsWith("Env ")) { else if (pragma.startsWith("Env ")) {
auto envPragma = pragma.sliced(4); auto envPragma = pragma.sliced(4);
@ -107,6 +110,8 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio
pragmas.dataDir = pragma.sliced(8).trimmed(); pragmas.dataDir = pragma.sliced(8).trimmed();
} else if (pragma.startsWith("StateDir ")) { } else if (pragma.startsWith("StateDir ")) {
pragmas.stateDir = pragma.sliced(9).trimmed(); pragmas.stateDir = pragma.sliced(9).trimmed();
} else if (pragma.startsWith("CacheDir ")) {
pragmas.cacheDir = pragma.sliced(9).trimmed();
} else { } else {
qCritical() << "Unrecognized pragma" << pragma; qCritical() << "Unrecognized pragma" << pragma;
return -1; return -1;
@ -129,15 +134,15 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio
.shellId = shellId, .shellId = shellId,
.launchTime = qs::Common::LAUNCH_TIME, .launchTime = qs::Common::LAUNCH_TIME,
.pid = getpid(), .pid = getpid(),
.display = getDisplayConnection(),
}; };
#if CRASH_REPORTER #if CRASH_HANDLER
auto crashHandler = crash::CrashHandler(); crash::CrashHandler::init();
crashHandler.init();
{ {
auto* log = LogManager::instance(); auto* log = LogManager::instance();
crashHandler.setRelaunchInfo({ crash::CrashHandler::setRelaunchInfo({
.instance = InstanceInfo::CURRENT, .instance = InstanceInfo::CURRENT,
.noColor = !log->colorLogs, .noColor = !log->colorLogs,
.timestamp = log->timestampLogs, .timestamp = log->timestampLogs,
@ -148,13 +153,18 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio
} }
#endif #endif
QsPaths::init(shellId, pathId, pragmas.dataDir, pragmas.stateDir); QsPaths::init(shellId, pathId, pragmas.dataDir, pragmas.stateDir, pragmas.cacheDir);
QsPaths::instance()->linkRunDir(); QsPaths::instance()->linkRunDir();
QsPaths::instance()->linkPathDir(); QsPaths::instance()->linkPathDir();
LogManager::initFs(); LogManager::initFs();
Common::INITIAL_ENVIRONMENT = QProcessEnvironment::systemEnvironment(); Common::INITIAL_ENVIRONMENT = QProcessEnvironment::systemEnvironment();
if (!pragmas.useSystemStyle) {
qunsetenv("QT_STYLE_OVERRIDE");
qputenv("QT_QUICK_CONTROLS_STYLE", "Fusion");
}
for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) { for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) {
qputenv(var.toUtf8(), val.toUtf8()); qputenv(var.toUtf8(), val.toUtf8());
} }

View file

@ -50,6 +50,7 @@ struct CommandState {
QStringOption manifest; QStringOption manifest;
QStringOption name; QStringOption name;
bool newest = false; bool newest = false;
bool anyDisplay = false;
} config; } config;
struct { struct {
@ -73,6 +74,8 @@ struct CommandState {
CLI::App* show = nullptr; CLI::App* show = nullptr;
CLI::App* call = nullptr; CLI::App* call = nullptr;
CLI::App* getprop = nullptr; CLI::App* getprop = nullptr;
CLI::App* wait = nullptr;
CLI::App* listen = nullptr;
bool showOld = false; bool showOld = false;
QStringOption target; QStringOption target;
QStringOption name; QStringOption name;
@ -106,6 +109,8 @@ void exitDaemon(int code);
int parseCommand(int argc, char** argv, CommandState& state); int parseCommand(int argc, char** argv, CommandState& state);
int runCommand(int argc, char** argv, QCoreApplication* coreApplication); int runCommand(int argc, char** argv, QCoreApplication* coreApplication);
QString getDisplayConnection();
int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication); int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication);
} // namespace qs::launch } // namespace qs::launch

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