Compare commits

...

243 commits

Author SHA1 Message Date
b41882c169
maybe do import?
Some checks failed
Build Hyprland / Build Hyprland (Arch) (push) Has been cancelled
Build Hyprland / Code Style (push) Has been cancelled
Nix / update-inputs (push) Has been cancelled
Nix / hyprland (push) Has been cancelled
Nix / test (push) Has been cancelled
Security Checks / Flawfinder Checks (push) Has been cancelled
Nix / xdph (push) Has been cancelled
2026-03-07 15:45:46 +10:00
bcab43b181
bump glaze version
Some checks are pending
Build Hyprland / Build Hyprland (Arch) (push) Waiting to run
Build Hyprland / Code Style (push) Waiting to run
Nix / update-inputs (push) Waiting to run
Nix / hyprland (push) Waiting to run
Nix / xdph (push) Blocked by required conditions
Nix / test (push) Waiting to run
Security Checks / Flawfinder Checks (push) Waiting to run
2026-03-07 14:59:09 +10:00
08e215c9bf
temp
Some checks are pending
Build Hyprland / Build Hyprland (Arch) (push) Waiting to run
Build Hyprland / Code Style (push) Waiting to run
Nix / update-inputs (push) Waiting to run
Nix / hyprland (push) Waiting to run
Nix / xdph (push) Blocked by required conditions
Nix / test (push) Waiting to run
Security Checks / Flawfinder Checks (push) Waiting to run
2026-03-07 14:51:29 +10:00
UjinT34
4152ac76d0
renderer: refactor Texture, Framebuffer and Renderbuffer (#13437)
Some checks are pending
Build Hyprland / Build Hyprland (Arch) (push) Waiting to run
Build Hyprland / Code Style (push) Waiting to run
Nix / update-inputs (push) Waiting to run
Nix / hyprland (push) Waiting to run
Nix / xdph (push) Blocked by required conditions
Nix / test (push) Waiting to run
Security Checks / Flawfinder Checks (push) Waiting to run
Part 1 of the renderer refactors
2026-03-06 21:44:10 +00:00
UjinT34
a5858018d8
renderer: shader variants refactor (#13434)
Part 0 of renderer reworks.
2026-03-06 21:05:10 +00:00
Mikhail
8685fd7b0c
dwindle: add rotatesplit layoutmsg and tests (#13235) 2026-03-06 20:47:48 +00:00
Logan Collins
1fa157cf6d
compositor: fix missing recheckWorkArea to prevent CReservedArea assert failure (#13590) 2026-03-06 20:47:39 +00:00
Virt
e0c5710059
layerrules: add dynamically registered rules for plugins (#13331)
* layerrules: add dynamically registered rules for plugins

* be gone

* layerrules: add layer tests with waybar

* fix: use kitty layers instead of waybar
2026-03-06 20:11:42 +00:00
Nikolai Nechaev
42f0a6005b
keybinds: Remove removed keybinds (#13605)
There seems to be no reason for them to remain.

But if they are kept, no notification appears to warn
a user that a dispatcher used in their config is no
longer valid. The config remains valid, but the bindings
do not work anymore.
2026-03-06 16:33:08 +00:00
JaSha256
ae9ca17b40
pointer: fix hardware cursor rendering on rotated/flipped monitors (#13574)
Replace the broken cairo_matrix_rotate() approach with explicit
per-transform pattern matrices for all 8 wl_output_transform values.
2026-03-05 15:14:23 +00:00
Vaxry
972f23efe8
screencopy: fix isOutputBeingSSd (#13586)
use sessions instead of pending frames
2026-03-05 15:14:13 +00:00
justin4046
4c60d9df70
desktop/rules: fix empty workspace handling (#13544) 2026-03-05 15:14:05 +00:00
Vaxry
b7dfb47566
config/descriptions: add missing desc entry 2026-03-05 14:10:22 +00:00
Thedudeman
3284dd729b
algo/scrolling: add config options for focus and swapcol wrapping (#13518) 2026-03-05 14:08:40 +00:00
Ikalco
803e81ac39
screenshare: improve destroy logic of objects (#13554) 2026-03-05 14:06:55 +00:00
Vũ Xuân Trường
34c7cc7d38
i18n: update Vietnamese translations (#13489) 2026-03-04 20:02:04 +00:00
Ikalco
c47ae950f4
screencopy: fix minor crash (#13566) 2026-03-04 20:01:37 +00:00
Harsh Narayan Jha
3f169ee5de
socket2: emit kill event (hyprctl kill) (#13104) 2026-03-04 20:00:00 +00:00
Vaxry
10754745a9
render/cm: add ICC profile pipeline (#12711)
Adds an ICC profile pipeline, loading via config and applying via 3D LUTs.
2026-03-04 19:50:28 +00:00
Florian "sp1rit
8271cfc97b
core: fix i586 build (#13550)
Signed-off-by: Florian "sp1rit"​ <sp1rit@disroot.org>
2026-03-04 11:33:44 +00:00
Vaxry
c11cadd8d6
desktop/window: don't group modals 2026-03-03 21:00:33 +00:00
Vaxry
dc4b082ee8
algo/scrolling: fix rare crash 2026-03-03 20:59:18 +00:00
André Silva
edf7098345
desktop/window: fix floating windows being auto-grouped (#13475)
---------

Co-authored-by: Aqa-Ib <16420574+Aqa-Ib@users.noreply.github.com>
2026-03-03 20:56:02 +00:00
Vaxry
7299a3b0d5
hyprctl: fix workspace dynamic effect reloading (#13537)
ref https://github.com/hyprwm/Hyprland/discussions/12806
2026-03-03 13:03:47 +00:00
Vaxry
b06a4b5e13
layout/windowTarget: override maximized box status in updateGeom (#13535)
ref https://github.com/hyprwm/Hyprland/discussions/13525
2026-03-03 12:33:46 +00:00
Vaxry
3faddf40d0
algo/dwindle: don't crash on empty swapsplit (#13533)
ref https://github.com/hyprwm/Hyprland/discussions/13530
2026-03-03 11:55:57 +00:00
Vaxry
a6e3a2478c
tests/workspace: fix one test case failing 2026-03-03 11:27:16 +00:00
Vaxry
ff0b706ea3
renderer: fix crash on mirrored outputs needing recalc (#13534)
ref https://github.com/hyprwm/Hyprland/discussions/13517
2026-03-03 11:25:58 +00:00
UjinT34
4f44df7b17
algo/master: fix crash after dpms (#13522) 2026-03-03 11:06:32 +00:00
Vaxry
be03497b82
layout/algos: use binds:window_direction_monitor_fallback for moves (#13508)
ref https://github.com/hyprwm/Hyprland/discussions/13473
2026-03-02 21:39:06 +00:00
Vaxry
ff20cbf89c
algo/scrolling: fix offset on removeTarget (#13515) 2026-03-02 21:23:24 +00:00
André Silva
fe0a202137
desktop/group: respect direction when moving window out of group (#13490) 2026-03-02 21:12:27 +00:00
Vaxry
75a815fbf2
algo/dwindle: use focal point correctly for x-ws moves (#13514) 2026-03-02 21:10:21 +00:00
Vaxry
3b7401b065
algo/scroll: improve directional moves (#13423) 2026-03-02 19:31:33 +00:00
Vaxry
d4d17d5d52
compositor: damage monitors on workspace attachment updates
ref https://github.com/hyprwm/Hyprland/discussions/13386
2026-03-02 19:26:06 +00:00
Mihai Fufezan
52ece2b017
treewide: alejandra -> nixfmt 2026-03-02 21:03:44 +02:00
Vaxry
d98f7ffaf5
layout: store and preserve size and pos after fullscreen (#13500)
ref https://github.com/hyprwm/Hyprland/discussions/13401
2026-03-02 18:57:09 +00:00
Vaxry
5f650f8ed9
layout/windowTarget: don't use swar on maximized (#13501) 2026-03-02 16:54:33 +00:00
Vaxry
9f98f7440b
algo/dwindle: add back splitratio (#13498) 2026-03-02 16:21:20 +00:00
Vaxry
5cb1281035
layout/windowTarget: damage before and after moves (#13496) 2026-03-02 12:52:22 +00:00
Thedudeman
743dffd638
layout/scroll: fix configuredWidths not setting properly on new workspaces (#13476) 2026-03-02 12:51:56 +00:00
Vaxry
5c370c3333
hyprpm: fix url sanitization in add
this could've been used to exec additional commands with hyprpm
2026-03-01 21:55:12 +00:00
Vaxry
cf0d256c13
layout/windowTarget: fix size_limits_tiled (#13445)
fucks up on scroll, also, wtf
2026-03-01 19:21:53 +00:00
Yujon Pradhananga
6ebafcf107
layout/scrolling: fix size_t underflow in idxForHeight (#13465) 2026-03-01 18:19:02 +00:00
Vaxry
8ad96a95d6
screencopy: fix nullptr deref if shm format is weird 2026-03-01 15:31:22 +00:00
Vaxry
f41e3c2203
scroll: clamp column widths properly
ref https://github.com/hyprwm/Hyprland/discussions/13458
2026-03-01 10:15:22 +00:00
Vaxry
f0a80ce5e0
keybinds: fixup changegroupactive 2026-03-01 10:12:15 +00:00
Vaxry
2928d6af0a
layouts: fix crash on missed relayout updates (#13444) 2026-02-28 23:06:27 +00:00
vaxerski
93aacfc0dc [gha] Nix: update inputs 2026-02-28 22:55:48 +00:00
Vaxry
19c263e53c
screencopy: scale window region for toplevel export (#13442) 2026-02-28 22:54:10 +00:00
Vaxry
a032090098
monitor: damage old special monitor on change
ref https://github.com/hyprwm/Hyprland/discussions/13419
2026-02-28 22:51:33 +00:00
Vaxry
0b55c55f4a
monitor: update pinned window states properly on changeWorkspace (#13441)
ref https://github.com/hyprwm/Hyprland/discussions/13440
2026-02-28 22:42:06 +00:00
Vaxry
85c2764f5e
deco/border: fix damageEntire 2026-02-28 22:03:14 +00:00
Zynix
c2bed4103c
monitor: keep workspace monitor bindings on full reconnect (#13384)
When all monitors disconnect, non-active workspaces could migrate to the wrong output after reconnect. Preserve workspace ownership and re-apply assigned monitor bindings on connect.
2026-02-28 21:49:47 +00:00
LionHeartP
82729db330
build: fix build on gcc 16.x after #6b2c08d (#13429) 2026-02-28 21:45:16 +00:00
Vaxry
f12904e641
layout/algo: fix swar on removing a target (#13427)
ref https://github.com/hyprwm/Hyprland/discussions/13422
2026-02-28 18:53:36 +00:00
Vaxry
b90c61c04f
compositor: fix focus edge detection (#13425)
fixes edge detection, making it more relaxed and intuitive
2026-02-28 18:53:26 +00:00
André Silva
e333a330c0
desktop/group: fix movegroupwindow not following focus (#13426) 2026-02-28 18:19:29 +00:00
Vaxry
1c64ef06d9
desktop/window: fix idealBB reserved (#13421)
ref https://github.com/hyprwm/Hyprland/discussions/12766\#discussioncomment-15955638
2026-02-28 16:55:34 +00:00
Vaxry
6b2c08d3e8
pointer: damage entire buffer in begin of rendering hw
ref #13391
2026-02-28 15:55:30 +00:00
Christian Fredrik Johnsen
db8509dfe2
build: remove auto-generated hyprctl/hw-protocols/ files during make clear (#13399) 2026-02-28 15:51:24 +00:00
Tom Englund
d2b9957fab
format: safeguard drmGetFormat functions (#13416)
they can return null if not found.
2026-02-28 15:29:22 +00:00
Vaxry
362ea7b0f3
hyprctl: fix buffer overflowing writes to the socket 2026-02-28 15:06:10 +00:00
Vaxry
f7114016c6
desktop/rule: fix matching for content type by str 2026-02-28 15:03:49 +00:00
Vaxry
0002f148c9
version: bump to 0.54.0
Some checks failed
Build Hyprland / Build Hyprland (Arch) (push) Has been cancelled
Build Hyprland / Code Style (push) Has been cancelled
Nix / update-inputs (push) Has been cancelled
Nix / hyprland (push) Has been cancelled
Nix / test (push) Has been cancelled
Security Checks / Flawfinder Checks (push) Has been cancelled
Nix / xdph (push) Has been cancelled
2026-02-27 18:03:19 +00:00
ItsOhen
ffec41c426
desktop/rules: fix border colors not resetting. (#13382) 2026-02-27 17:59:47 +00:00
Vaxry
f624449c12
start: add --force-nixgl and check /run/opengl-driver (#13385) 2026-02-27 17:57:04 +00:00
ItsOhen
70cdd819e4
desktop/rules: use pid for exec rules (#13374) 2026-02-26 18:13:49 +00:00
Skidam
cc14dd1baf
xwayland: validate size hints before floating (#13361) 2026-02-26 16:42:49 +00:00
UjinT34
c71fbd854d
renderer: better sdr eotf settings (#12812) 2026-02-26 12:01:59 +00:00
Vaxry
0e9196867b
algo/dwindle: fix focal point not being properly used in movedTarget (#13373) 2026-02-26 12:00:05 +00:00
Vaxry
1e06ab464f
algo/master: fix orientation cycling (#13372) 2026-02-25 23:54:13 +00:00
Vaxry
623185170b
desktop/popup: avoid crash on null popup child in rechecking
ref #13352
2026-02-25 23:16:16 +00:00
Vaxry
d0583e1761
compositor: fix calculating x11 work area (#13347)
in a multimon scenario, due to our positioning hacks, and due to the fact work area is a rect anyways, likely wont make sense
2026-02-25 22:44:35 +00:00
fazzi
5b2efe54b1
input: use fresh cursor pos when sending motion events (#13366) 2026-02-25 22:41:50 +00:00
Skidam
457617b5a3
xwayland: normalize OR geometry to logical coords with force_zero_scaling (#13359)
Fixes X11 popups, tooltips, and menus showing black boxes on scaled monitors with xwayland:force_zero_scaling = 1 #13334
2026-02-25 12:29:12 +00:00
Vaxry
c60b3cb2ed
target: fix geometry for x11 floats 2026-02-24 19:01:51 +00:00
Vaxry
fbf67ef050
algo/scrolling: adjust focus callbacks to be more intuitive 2026-02-24 12:27:00 +00:00
Vaxry
be893a81b4
algo/master: fix master:orientation being a noop 2026-02-24 11:36:51 +00:00
Vaxry
5a80bc120a
algo/scrolling: fix crashes on destroying ws
ref #13324
2026-02-24 11:33:21 +00:00
vaxerski
a248805132 [gha] Nix: update inputs 2026-02-24 11:22:10 +00:00
Tom Englund
8ab4d1dc06
popup: check for expired weak ptr (#13352)
onCommit can destroy popups while the vector CPY still holds a weak ptr
to it, check if the weak ptr is still valid
2026-02-24 11:20:29 +00:00
Skidam
ae82a55400
view: send wl_surface.enter to subsurfaces of popups (#13353) 2026-02-24 11:20:04 +00:00
jmanc3
bc09504ea5
desktop/popup: fix use after free in Popup (#13335)
m_alpha was freed by fullyDestroy, but was then being touched because setCallbackOnEnd is activated by tick, which is the same function that updates animating variables

---------

Co-authored-by: Vaxry <vaxry@vaxry.net>
2026-02-23 16:58:06 +00:00
ssareta
f4bc8c3a64
keybinds: fix unguarded member access in moveWindowOrGroup (#13337) 2026-02-23 16:29:44 +00:00
Vaxry
b88813c7ef
event: refactor HookSystem into a typed event bus (#13333)
Refactors the old HookSystem into a typed event bus with clear
separation,
discovery and types.
2026-02-22 23:30:10 +00:00
Ikalco
b4ee4674f9
protocols: implement image-capture-source-v1 and image-copy-capture-v1 (#11709)
Implements the new screencopy protocols
2026-02-22 12:30:11 +00:00
Vaxry
93dbf88426
pointermgr: revert "damage only the surface size (#13284)"
This reverts commit 13dab66b1d.
2026-02-22 12:23:27 +00:00
Vaxry
0eb4755a3e
example: fixup config for togglesplit 2026-02-21 21:35:11 +00:00
Vaxry
723870337f
layout: rethonk layouts from the ground up (#12890)
Rewrites layouts to be much smaller, and deal with much less annoying
BS. Improves the overall architecture, unifies handling of pseudotiling,
and various other improvements.
2026-02-21 21:30:39 +00:00
Vaxry
51f8849e54
github: add ai policy to mr template 2026-02-21 20:37:34 +00:00
Vaxry
b9b1eda2ef
hyprctl: adjust json case
should be camel
2026-02-21 20:30:16 +00:00
Tom Englund
13dab66b1d
pointermgr: damage only the surface size (#13284)
* pointermgr: damage only the surface size

CWaylandOutput returns a vector2d with -1, -1 set as a "no limit",
passing that down into beginSimple and the renderer it hits pixman bug
of invalid sizes and wrong rectangles gets created.

causing bunch of
*** BUG ***
In pixman_region32_init_rect: Invalid rectangle passed

set the damage either to cursor plane size or fallback to 256x256.

* pointermgr: dedup if hw cursorsize checks

dedup a bit if else casing
2026-02-21 20:29:00 +00:00
Tom Englund
9f59ed7868
multigpu: fix multi gpu checking (#13277)
* multigpu: fix multi gpu checking

drmFD() from allocators is not always equal, because we reopen them
inside AQ for refcounting, meaning they get duplicated and become their
own fds, so checking if fd1 == fd2 ends up wrong.

introduce sameGpu in MiscFunctions that checks the actual drmDevice
meaning we can now even check if a rendernode is the same gpu as a
display node if we want.

* multigpu: move sameGpu to DRM namespace

move sameGpu out of MiscFunctions to DRM namespace.
2026-02-21 20:27:59 +00:00
Vaxry
a20142bcce
xwayland/xwm: fix window closing when props race
we need to recheck before closing, ideally on change but that's later
2026-02-21 14:40:36 +00:00
Vaxry
d91952c555
wayland/output: return all bound wl_output instances in outputResourceFrom (#13315)
ref https://github.com/hyprwm/Hyprland/discussions/13301
2026-02-20 22:31:59 +00:00
Murat65536
8b17a7404b
config/descriptions: fix use_cpu_buffer (#13285) 2026-02-20 17:57:08 +00:00
Vaxry
9ea6d0e15f
desktop/popup: only remove reserved for window popups 2026-02-19 17:41:17 +00:00
Vaxry
a1e62dcb12
welcome: skip in safe mode 2026-02-19 01:12:48 +00:00
Vaxry
7a566942d5
versionKeeper: ignore minor rev version
no point in firing the update screen when no breaking changes happen on point releases
2026-02-19 01:05:28 +00:00
Vaxry
68456a5d9a
desktop/window: add stable id and use it for foreign 2026-02-19 00:49:43 +00:00
Skidam
184af52f24
config: support no_vrr rule on vrr 1 (#13250) 2026-02-18 14:48:56 +00:00
Tom Englund
1af260ecbe
compositor: dont unlock all states on empty commits (#13303)
we cant unlock all states on empty commits, at best tryProcess them.

for example if a state is locked and waiting for a fence to become readable,
and another commit comes in we cant unlock it until the fence has actually signaled.
2026-02-18 14:29:35 +00:00
Dominick DiMaggio
0de216e783
cm: block DS for scRGB in HDR mode (#13262) 2026-02-17 13:57:46 +00:00
Mihai Fufezan
661314e134 CI/c-f check: adapt jidicula script 2026-02-16 15:45:10 +02:00
ssareta
17fc159ae2
desktop/windowRule: use content rule as enum directly (#13275) 2026-02-16 12:15:50 +00:00
bea4dev
6716b8a0e3
xwayland: fix size mismatch for no scaling (#13263) 2026-02-15 17:50:03 +00:00
flyingpeakock
59f19e465b
nix: fix evaluation warnings, the xorg package set has been deprecated (#13231)
Co-authored-by: Philip Johansson <philip@phlipphlop.me>
2026-02-15 17:53:28 +02:00
Mihai Fufezan
e6ca141364
CI/c-f check: set older clang ver 2026-02-15 00:53:57 +02:00
Kamikadze
48176160ab
commit-timing: avoid use-after-free in timer callback (#13271) 2026-02-14 13:09:25 +00:00
Vaxry
e5a2b9e5b0
hyprctl: bump hyprpaper protocol to rev 2 (#12838) 2026-02-14 13:08:13 +00:00
garypippi
e80f705d76
compositor: guard null view() in getWindowFromSurface (#13255) 2026-02-13 23:52:15 +00:00
Tom Englund
1bf410e1fc
renderer: fix dgpu directscanout explicit sync (#13229)
* directscanout: fix dgpu directscanout explicit sync

without setting an infence, AQ doesnt explicit sync, nor recreate the
dgpu fence for the blit work. and as such attemptdirectscanout path
artifacts and breaks. create a dummy CEGLSync even tho we dont really
have any pending glwork to get a proper fence, and set it.

* monitor: dont use new scheduling if direct scanout

using the new_render_scheduling makes no sense in the direct scanout
path, add a if guard against it.
2026-02-13 23:52:00 +00:00
vaxerski
1c767de9da [gha] Nix: update inputs 2026-02-13 23:42:49 +00:00
Kirill Unitsaev
a8a8929bb4
i18n: update russian translation (#13247) 2026-02-13 23:40:57 +00:00
Yingjie Wang
eb0d3f9f01
cmake: use OpenGL::GLES3 when OpenGL::GL does not exist (#13260)
This will allow to build on some systems without X.
2026-02-13 23:40:30 +00:00
Kamikadze
81a029e504 hyprpm: exclude glaze from all targets during fetch 2026-02-12 00:12:42 +02:00
Mihai Fufezan
380d14998e nix: remove glaze patch
Signed-off-by: Tyr Heimdal <tyr.heimdal@warning.no>
2026-02-12 00:01:33 +02:00
Tyr Heimdal
fd48d102e1 Reapply "hyprpm: bump glaze version"
This reverts commit e92b20292b.
2026-02-12 00:01:33 +02:00
EvilLary
531fc43203
cmake: bump wayland-server version to 1.22.91 (#13242) 2026-02-11 12:05:58 +02:00
Vaxry
5b6c42ca70
dynamicPermManager: fix c+p fail 2026-02-10 15:13:25 +00:00
Vaxry
857a78ce4e
hyprpm: add full nix integration (#13189)
Adds nix integration to hyprpm: hyprpm will now detect nix'd hyprland and use nix develop instead

---------

Co-authored-by: Mihai Fufezan <mihai@fufexan.net>
2026-02-10 15:12:43 +00:00
Vaxry
339661229d
desktop/reserved: fix a possible reserved crash (#13207) 2026-02-10 14:59:21 +00:00
UjinT34
ff061d177e
protocols: commit and presentation timing fixes (#13174)
* move commit timing fields to surface state

* fix toTimespec init

* update sendQueued api

* update onPresented api

* set zero copy flag

* send clock id

* move presented calcs inside condition

* use only CLOCK_MONOTONIC for commit/presentation timings

* fix setSetTimestamp

* do not wait for commit timing while tearing

* proto config

* fix config defaults
2026-02-10 14:55:21 +00:00
ItsOhen
407a623801
hyprctl: add error messages to hyprctl hyprpaper wallpaper (#13234) 2026-02-10 14:52:31 +00:00
bea4dev
171ad7d338
input: fix kinetic scroll (#13233) 2026-02-10 14:51:51 +00:00
vaxerski
f16ebef003 [gha] Nix: update inputs 2026-02-09 12:53:15 +00:00
Aurelle
6507445787
layershell: restore focus to layer shell surface after popup is destroyed (#13225) 2026-02-09 12:51:25 +00:00
G36maid
f68ac7ef75
i18n: add Traditional Chinese (zh_TW) translations (#13210)
* i18n: add Traditional Chinese (zh_TW) translations

* i18n: add missing Simplified Chinese (zh_CN) translations
2026-02-07 13:27:10 +00:00
UjinT34
60f1c61323
protocols/dmabuf: fix DMA-BUF checks and events (#12965) 2026-02-07 12:40:08 +00:00
UjinT34
9f9dbb0dc5
renderer: allow tearing with DS with invisible cursors (#13155) 2026-02-07 12:38:01 +00:00
Vaxry
cfbbfb591a
popup: reposition with reserved taken into account
ref https://github.com/hyprwm/Hyprland/discussions/13194
2026-02-07 11:11:45 +00:00
Tom Englund
63eb6b3bda
opengl: add debug:gl_debugging (#13183)
add debug:gl_debugging so we can disable gl debugging entirerly,
both glGetError and enabling EGL_KHR_debug has its cost, we still have
EXT_create_context_robustness and glGetGraphicsResetStatus that should
catch context loss, and is generally cheap to call it only checks a flag
set.

glGetError might cause a implicit flush to get any pending calls sent to
the gpu.

however to get EGL_KHR_debug back enabled we now require a restart of
the compositor after changing debug:gl_debugging
2026-02-06 21:02:20 +00:00
EvilLary
8606bc255b
proto/shm: update wl_shm to v2 (#13187) 2026-02-06 17:08:30 +00:00
Yash Dodwani
562171ab66
i18n: add bengali translations (#13185) 2026-02-05 22:35:59 +00:00
Vaxry
9ce9ef2705
decorations/border: fix damage scheduling after #12665 2026-02-05 18:07:03 +00:00
UjinT34
02ff413002
monitor: fix DS deactivation (#13188) 2026-02-04 12:42:43 +00:00
UjinT34
1bc857b12c
fifo: miscellaneous fifo fixes (#13136)
* LOGM: clang-tidy fix

* fix fifo state and scheduling

* disable fifo_pending_workaround by default

* fix tearing

* fix "empty" commit skipping
2026-02-04 00:27:48 +00:00
EvilLary
cd7bdc7a43
hyprerror: add padding & adjust for scale when reserving area (#13158) 2026-02-03 20:44:41 +00:00
EvilLary
e123fd3e66
monitor: revert "remove disconnected monitor before unsafe state #12544" (#13154) 2026-02-03 20:44:18 +00:00
Łukasz Rek
30756d8718
gestures/fs: remove unneeded floating state switch (#13127) 2026-02-03 00:49:05 +00:00
UjinT34
9433060760
renderer: fix screen export back to srgb (#13148) 2026-02-02 20:33:03 +00:00
Vũ Xuân Trường
a0ec2e4daf
i18n: add Vietnamese translation (#13163) 2026-02-01 16:59:15 +00:00
Luke Barkess
d9d9d9358f
gestures: add cursor zoom (#13033) 2026-02-01 14:32:47 +00:00
ekhadley
95c8f8b299
input: fix edge grab resize logic for gaps_out > 0 (#13144) 2026-02-01 14:29:35 +00:00
Tom Englund
beeca9dacb
xwayland: ensure NO_XWAYLAND builds (#13160)
add , using xcb_atom_t = uint32_t;
2026-02-01 14:27:37 +00:00
Tom Englund
47f9035601
time: ensure type correctness and calculate nsec correctly (#13167)
use auto for nsecdur, assigning system_tp into steady_tp compiles but is
not correct. just change it to auto.

use {} initialization for timespec structs and returning std::pair.

in timediff, fromTimespec and toTimespec the else case was calculating
wrong. we need to correctly handle the borrow when the nanoseconds of
the first time are smaller than the second, by adding TIMESPEC_NSEC_PER_SEC
and decrementing the seconds.
2026-02-01 14:18:06 +00:00
jmanc3
db6114c6c5
renderer/pass: fix surface opaque region bounds used in occluding (#13124) 2026-01-31 13:39:22 +00:00
Szwagi
cbeb6984e7
renderer: fix mouse motion in VRR (#12665) 2026-01-31 13:37:01 +00:00
Tom Englund
4330b49a84
buffer: add move constructor and operator to CHLBufferReference (#13157)
add missing move constructor and operator, a lot of churn was done on
always copying CHLBufferReference, also add a self copy check.
2026-01-31 13:35:39 +00:00
Tom Englund
2ad7f6edd4
xwayland/xwm: get supported props on constructing surface (#13156)
not all clients supports WM_DELETE_WINDOW like glxgears, so get
supported props in constuctor of surface, and if not supported
forcefully kill the client.
2026-01-31 13:35:06 +00:00
Tom Englund
ec120d5732
opengl: set EGL_CONTEXT_RELEASE_BEHAVIOR_KHR if supported (#13114)
EGL_CONTEXT_RELEASE_BEHAVIOR_KHR determines what happends with implicit
flushes when context changes, on multigpu scenario we change context
frequently when blitting content. while we still rely on explicit sync
fences, the flush is destroying driver optimisations.

setting it to EGL_CONTEXT_RELEASE_BEHAVIOR_NONE_KHR essentially mean
just swap context and continue processing on the next context.
2026-01-30 19:42:01 +00:00
Tom Englund
fe6c213024
xwayland/xwm: fix _NET_WM_STATE_MAXIMIZED_VERT type (#13151)
add _ infront of the atom name. as it should be.
2026-01-30 19:35:52 +00:00
Zynix
b8fc0def97
xwayland/xwm: handle INCR clipboard transfer chunks correctly (#13125)
Handle XCB_PROPERTY_NEW_VALUE events for incremental selection transfers.
Previously only DELETE was handled, causing INCR transfers to fail after
the first chunk.
2026-01-30 14:14:17 +00:00
Zynix
c92fb5e85f
xwayland/xwm: prevent onWrite infinite loop and clean orphan transfers (#13122)
Fixes #11411

- Add return 0 after erasing completed non-incremental transfer to stop event source polling
- Add removeTransfer() helper to SXSelection for cleaning transfers by window ID
- Add removeTransfersForWindow() helper to CXWM for cleaning all selections at once
- Clean orphan transfers in handleDestroy before surface removal
- Clean orphan transfers in handlePropertyNotify on missing window or failed reply
- Add m_dndSelection to handleSelectionPropertyNotify cleanup loop
- Initialize SXTransfer members with safe defaults to prevent undefined behavior
- Fix race condition in getTransferData by using window ID lookup instead of index
2026-01-29 13:50:17 +00:00
Mihai Fufezan
7d209b2941
Nix: apply glaze patch 2026-01-29 13:16:59 +02:00
Mihai Fufezan
e92b20292b
Revert "hyprpm: bump glaze version"
This reverts commit 116537b494.

Re-apply when glaze 7.0.0 lands in Arch repos.

Relevant discussion: https://github.com/hyprwm/Hyprland/discussions/13043#discussioncomment-15636089
2026-01-29 13:14:05 +02:00
Mihai Fufezan
22e53345ba
flake.lock: update 2026-01-29 01:38:41 +02:00
Mihai Fufezan
116537b494
hyprpm: bump glaze version 2026-01-29 01:37:09 +02:00
jmanc3
c8b5023bb0
opengl: allow texture filter to be changed (#13078)
* opengl: allow texture filter to be changed

* format

* correct filter

* Moved from OpenGL.hpp to Texture.hpp

* Shortened names
2026-01-27 21:21:53 +00:00
ItsOhen
bcb34275ea
hyprctl: fix layerrules not being applied dynamically with hyprctl (#13080) 2026-01-27 12:13:29 +00:00
Tom Englund
21325f9385
eventLoop: various eventloopmgr fixes (#13091)
* eventloopmgr: use unordered_map for readableWaiters

use an unordered_map with the raw ptr as key, avoids any risk of
dangling ptrs.

* eventloopmgr: read the timerfd fd

the manpage for timerfd_create and read states this.

timefd_create creates a new timer object, and returns a file descriptor
that can be used to read the number of expirations that have occurred.

The FD becomes readable when the timer expires.

read removes the “readable” state from the FD.

so most likely it has somewhat worked because of the scheduleRecalc()
function.

* eventloopmgr: avoid unneeded std::function copy

move the idle functions instead of copying.

* eventloopmgr: remove event source before calling fn

if fn causes a dispatch/reentry its gonna cause UB inside libwayland
itself, remove the event source before calling the fn() avoids that
entirerly. even if a new dispatch occurs.

* eventloopmgr: check if timer fd is readable

check if timerfd is readable before calling read on it, so we dont end
up blocking on an accident, log an error if its not readable.

* eventloopmgr: revert unordered_map change

my mistake, the address wasnt changing on reallocations of the heap
object. the only issue i was triggering was the reentry path in fn()
2026-01-27 12:11:54 +00:00
Viorel Ciobotaru
50454c6d17
i18n: add Romanian translations (#13075) 2026-01-27 00:20:56 +02:00
Vaxry
c65c7614bc
hyprpm: fix build step execution 2026-01-24 20:00:56 +00:00
Naufal Hisyam Muzakki
891e029ba3
hyprctl: add overFullscreen field in hyprctl window debug (#13066) 2026-01-24 19:53:40 +00:00
Michał Minarowski
b1d1c9843f
hyprctl: remove trailing comma from json object (#13042) 2026-01-23 20:40:50 +00:00
Tom Englund
2a2c2b0e28
opengl/fb: use GL_DEPTH24_STENCIL8 instead of GL_STENCIL_INDEX8 (#13067)
older drivers lack support for GL_STENCIL_INDEX8 so use
GL_DEPTH24_STENCIL8 but explicitly disable the depth.
2026-01-23 20:09:39 +00:00
Vaxry
64db62d7e2
hyprpm: use provided pkgconf env if available
this is required for hyprpm to work under nix develop
2026-01-22 20:33:36 +00:00
UjinT34
82de66a030
renderer: fix frame sync (#13061)
* fix commit timing timer

* fix surface state lock/unlock

* debug state sync todos

* debug solitary vrr
2026-01-22 15:16:52 +00:00
Florent Charpentier
22fc8136a2
desktop/windowRule: allow expression in min_size/max_size (#12977) 2026-01-21 15:56:51 +00:00
Naufal Hisyam Muzakki
e7985ca4c4
desktop: restore invisible floating window alpha/opacity when focused over fullscreen (#12994) 2026-01-21 15:55:12 +00:00
UjinT34
6c3ebed76e
renderer: add surface shader variants with less branching and uniforms (#13030)
* shader variant features

* getSurfaceShader variant with feats

* split surface shaders by features

* cleanup old shaders
2026-01-21 15:54:14 +00:00
William Wernert
f9fb24577a
animation: reset tick state on session activation (#13024)
After suspend/wake, the animation tick timer state (m_lastTickValid,
m_tickScheduled) could be stale, causing framerate drops when blur is
enabled. This was introduced in 2b0fd417 which changed the animation
tick timing mechanism.

Reset the tick state when the session becomes active to ensure a clean
state for the animation system.
2026-01-21 15:54:02 +00:00
UjinT34
55f40ecc95
renderer: fix non shader cm reset (#13027) 2026-01-21 14:03:37 +00:00
Vaxry
441a8714c7
hyprpm: fix clang-format 2026-01-21 13:58:09 +00:00
Luke Barkess
57e6a57e6b
hyprerror: clear reserved area on destroy (#13046) 2026-01-21 13:57:36 +00:00
ArchSav
c44292c723
protocols/toplevelExport: Support transparency in toplevel export (#12824) 2026-01-20 15:32:32 +01:00
Mihai Fufezan
f0b6714539
Nix: re-enable hyprpm 2026-01-20 13:34:25 +02:00
Mihai Fufezan
8f547c6fa0
hyprpm: drop meson dep 2026-01-20 13:34:25 +02:00
Mihai Fufezan
d6e2ae0247
hyprpm,Makefile: drop cmake ninja build 2026-01-20 13:34:25 +02:00
Nikolai Nechaev
eb0480ba0d
tests: Test the no_focus_on_activate window rule (#13015) 2026-01-18 15:22:33 +01:00
Tom Englund
0896775f1b
pointermgr: remove onRenderBufferDestroy (#13008)
set the damage to cursor plane size instead of INT16_MAX and remove
onRenderbufferDestroy, renderbuffer already have a listener that
destroys when buffer is destroyed.
2026-01-18 13:51:14 +01:00
Tom Englund
c99eb23869
renderer: optimise shader usage further, split shaders and add more caching (#12992)
* shader: split CM rgba/rgbx into discard ones

make it branchless if we have no discards.

* shader: ensure we dont stall on vbo uv buffer

if we render a new texture before the previous was done gpu wise its
going to stall until done, call glBufferData to orphan the data.
this allows the driver to return a new memory block immediately
if the GPU is still reading from the previous one

* protocols: ensure we reset GL_PACK_ALIGNMENT

reset GL_PACK_ALIGNMENT back to the default initial value of 4

* shader: use unsigned short in VAO

loose a tiny bit of precision but gain massive bandwidth reductions.
use GL_UNSIGNED_SHORT and set it as normalized. clamp and round the UV
for uint16_t in customUv.

* shader: interleave vertex buffers

use std::array for fullverts, use a single interleaved buffer for
position and uv, should in theory improve cache locality. and also remove
the need to have two buffers around.

* shader: revert precision drop

we need the float precision because we might have 1.01 or similiar
floats entering CM shader maths, and rounding/clamping those means the
maths turns out wrong. so revert back to float, sadly higher bandwidth
usage.

* update doColorManagement api

* convert primaries to XYZ on cpu

* remove unused primaries uniform

---------

Co-authored-by: UjinT34 <ujint34@mail.ru>
2026-01-17 15:31:19 +01:00
Tom Englund
92a3b91999
anr: remove window on closewindow (#13007)
m_data was never cleaned and continously built up the m_data, remove
the entry on closeWindow.
2026-01-17 10:23:09 +01:00
Vaxry
36aa465a21
cmakelists: add fno-omit-frame-pointer for tracy builds 2026-01-16 16:59:46 +01:00
Vaxry
fec17e5e79
desktop/ruleApplicator: fix typo in border color rule parsing (#12995)
ref https://github.com/hyprwm/Hyprland/discussions/12746
2026-01-16 16:43:25 +01:00
Vaxry
eff484b96c
core: optimize some common branches 2026-01-16 16:40:48 +01:00
Vaxry
2e697ce2bf
cmakelists: don't require debug for tracy 2026-01-16 16:26:58 +01:00
Vaxry
0b13d398fe
desktop/window: avoid uaf on instant removal of a window
ref https://github.com/hyprwm/Hyprland/discussions/12999
2026-01-16 09:11:21 +01:00
Vaxry
ac9df44788
desktop/workspaceHistory: fix tracking for multiple monitors (#12979) 2026-01-15 17:00:47 +01:00
UjinT34
e0cf88809d
protocols/cm: Fix image description info events (#12781)
* fix image description info events

* always send some target primaries

* set edid values as target primaries and luminances

* init monitor image description

* set default luminances for tf

* fix BT1886 luminances

* fix mastering values and overrides

* set maxCLL & maxFALL

* typo

* add FALL & CLL to preferred HDR image description

* fix ref luminances
2026-01-13 16:44:36 +01:00
Tom Englund
e43f949f8a
shm: ensure we use right gl unpack alignment (#12975)
gl defaults to 4 and not all formats is divisible with 4 meaning its
going to pad out ouf bounds and cause issues. check if the stride is
divisible with 4 otherwise set it to 1, aka disable it.

GL_UNPACK_ALIGNMENT only takes 1,2,4,8 but formats like RGB888 has
bytesPerBlock 3.
2026-01-13 16:42:31 +01:00
Chris Naporlee
8d03fcc8d7
protocols/syncobj: fix DRM sync obj support logging (#12946) 2026-01-12 18:28:08 +01:00
Tom Englund
5e18111121
renderer: shader code refactor (#12926)
* shader: begin the shader refactor

make SShader a class and rename it to CShader, move createprogram,
compileshader, logshadererror to CShader.

* shader: move uniform creation to CShader

move uniform creation to CShader, reduces tons of duplicated effort,
however forcing uniform names to be same in all shaders.

* shader: move to array based frag handling

use an array with an enum so it gets easier dealing with multiple
shaders, move creating program to a for loop and array, reduces line of
code a lot.

* shader: use shared ptr for frags

with smart pointers we can now rename useProgram to useShader and return
the shader directly, means only place we have to decide the shader frag
is when calling useShader. easier for future shader splitting to reduce
branching.

* shader: move unneded public members to private

move structs and uniforms to private add a get/set for initialtime
and add a getUniformLocation to make the code tell what its doing,
instead of direct array getting when all we wanted to get was its value,
also limits the setting of uniformLocations to the createProgram as it should
be.

* shader: fix style nits

set first enum member to 0 , remove extra {}

* shader: dont show a failed notif on success

the logic got inverted in the refactor here.

* shader: split CM shader to rgba/rgbx variants

split shader to rgba/rgbx variants, use bool, and reduce branching.

* shader: split up blurprepare CM and non CM

split up blurprepare, remove skipcm, move gain to gain.glsl.
remove ternary operator and reduce branching by using step() and mix()
use vec3 for gain, make brightness a cheap mulitplication with max.

* shader: split up border to CM/noncm variants

splitup border shader to CM/noncm variant, move common used things to
border.glsl , there is room for optimisations here but its a complex
shader im putting it for future PR.

* shader: touchup blurfinish

make brightness a cheap multiplication instead of branching.
mod is redundant, fract in hash already returns a value in [0.0, 1.0]
2026-01-12 18:27:16 +01:00
Vaxry
fbf421df88
LICENSE: update year 2026-01-11 16:13:52 +01:00
zacoons
8f8b31e7a6
decoration: take desiredExtents on all sides into account (#12935) 2026-01-10 20:53:57 +01:00
Austin Horstman
81e7498ec2 nix: add hyprland-uwsm to passthru.providedSessions
Fix issue with displayManager `defaultSession` not accepting the
stringsince it's missing from the lookup it does
with`passthru.providedSessions`.
2026-01-10 01:09:41 +02:00
John Mylchreest
fa41c8229d
desktop/window: track explicit workspace assignments to prevent X11 configure overwrites (#12850)
* fix: track explicit workspace assignments to prevent X11 configure overwrites

Instead of only checking for special workspaces, track when workspaces are
explicitly assigned via window rules or user actions (movetoworkspace).
This prevents onX11ConfigureRequest from overwriting any explicit workspace
assignment based on window position.

Changes:
- Add m_workspaceExplicitlyAssigned flag to CWindow
- Set flag when window rules assign workspace
- Set flag when user moves window via dispatcher
- Check flag in onX11ConfigureRequest instead of just special workspace
- Add debug logging for explicit workspace assignments

* fix: simplify X11 configure request handling for special workspaces

X11 apps send configure requests with positions based on XWayland's
monitor layout, which could incorrectly move windows off special
workspaces.

Skip workspace reassignment when the window is on a special workspace
or staying on the same monitor, but always run z-order, fullscreen flag,
and damage logic since the configure request may include geometry changes.
2026-01-09 19:25:37 +01:00
John Mylchreest
5b1b79c29c
fix: handle fullscreen windows on special workspaces (#12851)
* fix: handle fullscreen windows on special workspaces

inFullscreenMode() only checked m_activeWorkspace, missing fullscreen
windows on special workspaces. This caused crashes and incorrect
behavior when fullscreen windows were on special workspaces.

Changes:
- inFullscreenMode() now checks special workspace first since it
  renders on top of regular workspaces
- Added getFullscreenWindow() helper to safely get fullscreen window
  from either active or special workspace
- Updated callers (shouldSkipScheduleFrameOnMouseEvent, Renderer,
  getFSImageDescription) to use the new helper
- Reset m_aboveFullscreen for layer surfaces when opening, closing,
  or stealing special workspaces between monitors

* test: add special workspace fullscreen detection tests

Add tests for the new special workspace fullscreen handling introduced
in the previous commit. The tests cover:

1. Fullscreen detection on special workspace - verifies that a window
   made fullscreen on a special workspace is correctly detected

2. Special workspace fullscreen precedence - verifies that when both
   regular and special workspaces have fullscreen windows, the special
   workspace window can be focused when the special workspace is opened

3. Toggle special workspace behavior - verifies that toggling the
   special workspace off properly hides it and returns focus to the
   regular workspace's fullscreen window

These tests exercise the key code paths modified in the fix:
- inFullscreenMode() checking special workspace first
- getFullscreenWindow() helper returning correct window
- Layer surface m_aboveFullscreen reset on special workspace toggle
2026-01-08 22:27:00 +01:00
Vaxry
eb623bd91d
animationMgr: avoid uaf in ::tick() if handleUpdate destroys AV
ref https://github.com/hyprwm/Hyprland/discussions/12840
2026-01-08 22:22:58 +01:00
Vaxry
3dcaadbdf5
desktop/ls: fix invalid clamp 2026-01-08 22:22:58 +01:00
Aaron Blasko
a649dbe4c4
main: add watchdog-fd and safe-mode options to help message (#12922)
Additionally, don't print the "you're not using start-hyprland" warning when using `--verify-config`
2026-01-08 17:50:11 +01:00
Vaxry
f767782e3f
desktop/reservedArea: clamp dynamic types to 0
ref https://github.com/hyprwm/Hyprland/discussions/12880
2026-01-08 12:25:39 +01:00
Vaxry
f54dd4da4a
desktop/reservedArea: clamp to 0
ref https://github.com/hyprwm/Hyprland/discussions/12880
2026-01-08 12:24:29 +01:00
Vaxry
3aa4e02720
config: don't crash on permission with a config check
ref #12872
2026-01-08 12:19:22 +01:00
Vaxry
8368566044
start: use nixGL if Hyprland is nix but not NixOS (#12845)
---------

Co-authored-by: Mihai Fufezan <mihai@fufexan.net>
2026-01-08 10:57:56 +01:00
Tom Englund
918e2bb9be
renderer/gl: add internal gl formats and reduce internal driver format conversions (#12879)
* format: add internal formats for drm formats

cross referenced with weston and added internal formats and types for a
lot of missing ones. also added a isFormatYUV helper.

* framebuffer: ensure we use right internalformat

ensure we use the right internal format to avoid internal driver
blitting, also since we only attach the GL_STENCIL_ATTACHMENT we might
just aswell only use the GL_STENCIL_INDEX8 to not confuse drivers that
we want a depth aswell.

* texture: use external on yuv or non linear mods

using external makes us use the gpu's internal detiler.
and this is makes intel a lot happier then having to format convert it
to a linear format internally.

* shaders: add external support to CM frag

add external support to CM frag, and correct ext.frag typo.

* formats: remove duplicates and fix a typo in cm.frag

remove duplicate formats and a typo in cm.frag

* formats: add swizzle logic to all formats

add swizzle logic from weston for all formats and use it in shm texture
paths.

* format: more format changes

use monitor drm format instead of forcing something different.

* shader: remove external from cm.frag

drivers want this resolved at compiletime cant use both
samplerExternalOES and sampler2d and then runtime branch it.

* screencopy: swizzle textures in screencopy

swizzle textures in screencopy, to get the right colors when copying.

* screencopy: restore old behaviour

try restore old behaviour before the gles3 format changes.
glReadPixels had the wrong format, so i went to far trying to mitigate
it. should be like before now.
2026-01-07 19:53:42 +01:00
wbg
a383ca1866
groupbar: added group:groupbar:text_padding (#12818)
Co-authored-by: Roman Weinberger // ACL <roman.weinberger@acl.at>
2026-01-07 16:52:02 +01:00
Hiroki Tagato
f1652b2951
start: add parent-death handling for BSDs (#12863)
* Add parent-death handling for BSDs

prctl() is a system call specific to Linux. So we cannot use it on BSDs.

FreeBSD has a system call procctl() which is similar to prctl(). We can
use it with PROC_PDEATHSIG_CTL.

OpenBSD, NetBSD, and DragonFly BSD do not appear to have a similar
mechanism. So intead of relying on a system call, we need to manually
poll ppid to see if the parent process has died.

With the changes, the spawned Hyprland process is terminated when the
launcher process exits, matching Linux behavior as closely as possible
on BSD platforms.

* Remove ppid polling on OpenBSD, NetBSD, and DragonFly BSD
2026-01-06 14:38:25 +01:00
EvilLary
cbfbd9712a
anr: open anr dialog on parent's workspace (#12509) 2026-01-06 14:29:17 +01:00
ItsOhen
9817553c66
config: return windowrulev2 layerrulev2 error messages (#12847) 2026-01-06 00:00:14 +01:00
Vaxry
6fce2d7288
renderer/opengl: invalidate intermediate FBs post render, avoid stencil if possible (#12848) 2026-01-05 22:37:54 +01:00
Vaxry
107275238c desktop/ls: clamp layer from protocol 2026-01-05 18:53:29 +01:00
Vaxry
3b77c784e2 protocols/contentType: fix missing destroy 2026-01-05 18:53:29 +01:00
Vaxry
d46df728fd protocols/contentType: fix typo in already constructed check 2026-01-05 18:53:29 +01:00
Vaxry
8eb3ecc755 input/TI: avoid UAF in destroy 2026-01-05 18:53:29 +01:00
Vaxry
97c8a2f1cf protocolMgr: remove IME / virtual input protocols from sandbox whitelist 2026-01-05 18:53:29 +01:00
Vaxry
a492fa3866 desktop/window: catch bad any cast tokens 2026-01-05 18:53:29 +01:00
Vaxry
e165f84184 core/compositor: immediately do readable if adding waiter fails for scheduling state 2026-01-05 18:53:29 +01:00
Vaxry
686eda9d48 eventLoop: remove failed readable waiters 2026-01-05 18:53:29 +01:00
Vaxry
70c5fe5cd8 systemd/sdDaemon: fix incorrect strnlen 2026-01-05 18:53:29 +01:00
Vaxry
32978176b1 systemd/sdDaemon: initialize sockaddr_un 2026-01-05 18:53:29 +01:00
Vaxry
1761909bca mainLoopExecutor: fix incorrect pipe check 2026-01-05 18:53:29 +01:00
Hiroki Tagato
7d8f57083e
testers: add missing #include <unistd.h> (#12862)
FreeBSD clang needs the header to be included for read(), write(),
pipe(), close(), etc.
2026-01-05 16:42:35 +01:00
Vaxry
a3c8533d74
subprojects: bump tracy 2026-01-05 12:57:40 +01:00
Tom Englund
0b3b012817
framebuffer: revert viewport (#12842)
to much stuff are relying on the viewport being set like this, just
revert it to not regress further. this needs a overhaul.
2026-01-04 11:44:19 +01:00
vaxerski
583c4074a5 [gha] Nix: update inputs 2026-01-03 21:12:46 +00:00
Virt
922e53c68c
pluginsystem: fix crash when unloading plugin hyprctl commands (#12821) 2026-01-03 22:11:05 +01:00
Tom Englund
17bc3b83db
renderer/fb: dont forget to set m_drmFormat (#12833)
fab3370 accidently removed the setting of m_drmFormat, causing it to
think format changed on each alloc.
2026-01-03 16:48:43 +01:00
Tom Englund
fab3370254
renderer: minor framebuffer and renderbuffer changes (#12831)
* framebuffer: dont release if format or size changes

we dont have to release and recreate both the texture and framebuffer if
size or format changes, we can just bind the texture and call glTexImage2D
with the new format and size.

* framebuffer: set the alloced viewport size

if monitor size mismatch with the allocated m_size its going to set a
mismatched viewport and cause rendering issues. and if they are
mismatching there is a missing alloc call.

* renderbuffer: cleanup unneded binds

the renderbuffer is attached to the fbo and trying to rebind it in
bind() is causing unnecessery state changes, just bind the fbo.

add safeguard in the destructor, the constructor can return early on
failure and leave m_rbo empty or m_image as EGL_NO_IMAGE_KHR.
2026-01-03 15:13:01 +01:00
Dmytro Budnyk
ee67278038
hyprerror: fix horizontal overflow and damage box (#12719)
* hyprerror: fix horizontal overflow and damage box

* hyprerror: remove redundant m_queued preservation logic

The logic to save and restore m_queued into a temporary string 'q' was redundant because m_queued is explicitly cleared at the end of createQueued() (line 164). Restoring it to a non-empty state would cause createQueued() to be called every frame in draw(), which is not the intended behavior for the static error bar.

* Fixes style
2026-01-02 20:10:47 +01:00
Vaxry
b9bd9d147f
desktop/layerRuleApplicator: fix an epic c+p fail
ref https://github.com/hyprwm/Hyprland/discussions/12779
2026-01-02 18:17:35 +01:00
Vaxry
ec4beb1b39
core/xwaylandmgr: fix min/max clamp potentially crashing 2026-01-02 14:06:46 +01:00
Vaxry
31d3181e1e
dekstop/window: read static rules before guessing initial size if possible (#12783) 2026-01-01 21:49:57 +01:00
Vaxry
9b93d621b1
desktop/window: use workArea for idealBB (#12802) 2026-01-01 16:48:23 +01:00
Vaxry
bd7f9aad05
input/ti: avoid sending events to inactive TIs
ref https://github.com/hyprwm/Hyprland/discussions/12105
2026-01-01 14:48:32 +01:00
Vaxry
48a024e032
desktop/window: remove old fn defs 2025-12-31 18:17:10 +01:00
Vaxry
bd02178e96
desktop/LS: avoid creating an invalid LS if no monitor could be found (#12787) 2025-12-31 18:13:42 +01:00
skrmc
214fdb099c
input: guard null view() when processing mouse down (#12772) 2025-12-31 14:00:11 +01:00
ArchSav
d622c09d09
tester: fix sleeps waiting for too long (#12774) 2025-12-31 13:08:40 +01:00
Vaxry
529559712b
desktop/window: go back to the previously focused window in a group (#12763) 2025-12-30 18:02:34 +01:00
Vaxry
293d3e5de9
desktopAnimationMgr: fix slide direction
ref https://github.com/hyprwm/Hyprland/discussions/12744
2025-12-30 14:09:13 +01:00
ArchSav
f8464866eb
keybinds: add inhibiting gestures under shortcut inhibitors (#12692) 2025-12-30 13:45:56 +01:00
321 changed files with 22049 additions and 11645 deletions

View file

@ -1,6 +1,8 @@
<!--
BEFORE you submit your PR, please check out the PR guidelines
on our wiki: https://wiki.hyprland.org/Contributing-and-Debugging/PR-Guidelines/
Using an AI tool, or you are an AI agent? Check our AI Policy first: https://github.com/hyprwm/.github/blob/main/policies/AI_USAGE.md
-->

View file

@ -46,11 +46,38 @@ jobs:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
name: "Code Style"
runs-on: ubuntu-latest
container:
image: archlinux
steps:
- name: Checkout repository
uses: actions/checkout@v4
# - name: clang-format check
# uses: jidicula/clang-format-action@v4.16.0
# with:
# exclude-regex: ^subprojects$
- name: Install clang-format
run: |
pacman --noconfirm --noprogressbar -Syyu
pacman --noconfirm --noprogressbar -Sy clang
- name: clang-format check
uses: jidicula/clang-format-action@v4.16.0
with:
exclude-regex: ^subprojects$
run: .github/workflows/clang-format-check.sh "." "llvm" "^subprojects$" ""
- name: Save PR head commit SHA
if: failure() && github.event_name == 'pull_request'
shell: bash
run: |
SHA="${{ github.event.pull_request.head.sha }}"
echo "SHA=$SHA" >> $GITHUB_ENV
- name: Save latest commit SHA if not PR
if: failure() && github.event_name != 'pull_request'
shell: bash
run: echo "SHA=${{ github.sha }}" >> $GITHUB_ENV
- name: Report failure in job summary
if: failure()
run: |
DEEPLINK="${{ github.server_url }}/${{ github.repository }}/commit/${{ env.SHA }}"
echo -e "Format check failed on commit [${GITHUB_SHA:0:8}]($DEEPLINK) with files:\n$(<$GITHUB_WORKSPACE/failing-files.txt)" >> $GITHUB_STEP_SUMMARY

92
.github/workflows/clang-format-check.sh vendored Executable file
View file

@ -0,0 +1,92 @@
#!/usr/bin/env bash
#
# Adapted from https://github.com/jidicula/clang-format-action
###############################################################################
# check.sh #
###############################################################################
# USAGE: ./entrypoint.sh [<path>] [<fallback style>]
#
# Checks all C/C++/Protobuf/CUDA files (.h, .H, .hpp, .hh, .h++, .hxx and .c,
# .C, .cpp, .cc, .c++, .cxx, .proto, .cu) in the provided GitHub repository path
# (arg1) for conforming to clang-format. If no path is provided or provided path
# is not a directory, all C/C++/Protobuf/CUDA files are checked. If any files
# are incorrectly formatted, the script lists them and exits with 1.
#
# Define your own formatting rules in a .clang-format file at your repository
# root. Otherwise, the provided style guide (arg2) is used as a fallback.
# format_diff function
# Accepts a filepath argument. The filepath passed to this function must point
# to a C/C++/Protobuf/CUDA file.
format_diff() {
local filepath="$1"
# Invoke clang-format with dry run and formatting error output
local_format="$(clang-format \
--dry-run \
--Werror \
--style=file \
--fallback-style="$FALLBACK_STYLE" \
"${filepath}")"
local format_status="$?"
if [[ ${format_status} -ne 0 ]]; then
# Append Markdown-bulleted monospaced filepath of failing file to
# summary file.
echo "* \`$filepath\`" >>failing-files.txt
echo "Failed on file: $filepath" >&2
echo "$local_format" >&2
exit_code=1 # flip the global exit code
return "${format_status}"
fi
return 0
}
CHECK_PATH="$1"
FALLBACK_STYLE="$2"
EXCLUDE_REGEX="$3"
INCLUDE_REGEX="$4"
# Set the regex to an empty string regex if nothing was provided
if [[ -z $EXCLUDE_REGEX ]]; then
EXCLUDE_REGEX="^$"
fi
# Set the filetype regex if nothing was provided.
# Find all C/C++/Protobuf/CUDA files:
# h, H, hpp, hh, h++, hxx
# c, C, cpp, cc, c++, cxx
# ino, pde
# proto
# cu
if [[ -z $INCLUDE_REGEX ]]; then
INCLUDE_REGEX='^.*\.((((c|C)(c|pp|xx|\+\+)?$)|((h|H)h?(pp|xx|\+\+)?$))|(ino|pde|proto|cu))$'
fi
cd "$GITHUB_WORKSPACE" || exit 2
if [[ ! -d $CHECK_PATH ]]; then
echo "Not a directory in the workspace, fallback to all files." >&2
CHECK_PATH="."
fi
# initialize exit code
exit_code=0
# All files improperly formatted will be printed to the output.
src_files=$(find "$CHECK_PATH" -name .git -prune -o -regextype posix-egrep -regex "$INCLUDE_REGEX" -print)
# check formatting in each source file
IFS=$'\n' # Loop below should separate on new lines, not spaces.
for file in $src_files; do
# Only check formatting if the path doesn't match the regex
if ! [[ ${file} =~ $EXCLUDE_REGEX ]]; then
format_diff "${file}"
fi
done
# global exit code is flipped to nonzero if any invocation of `format_diff` has
# a formatting difference.
exit "$exit_code"

View file

@ -110,6 +110,7 @@ add_compile_options(
-Wno-narrowing
-Wno-pointer-arith
-Wno-clobbered
-frtti
-fmacro-prefix-map=${CMAKE_SOURCE_DIR}/=)
# disable lto as it may break plugins
@ -124,6 +125,7 @@ find_package(Threads REQUIRED)
set(GLES_VERSION "GLES3")
find_package(OpenGL REQUIRED COMPONENTS ${GLES_VERSION})
find_package(glslang CONFIG REQUIRED)
set(AQUAMARINE_MINIMUM_VERSION 0.9.3)
set(HYPRLANG_MINIMUM_VERSION 0.6.7)
@ -243,7 +245,7 @@ configure_file(
set_source_files_properties(${CMAKE_SOURCE_DIR}/src/version.h PROPERTIES GENERATED TRUE)
set(XKBCOMMON_MINIMUM_VERSION 1.11.0)
set(WAYLAND_SERVER_MINIMUM_VERSION 1.22.90)
set(WAYLAND_SERVER_MINIMUM_VERSION 1.22.91)
set(WAYLAND_SERVER_PROTOCOLS_MINIMUM_VERSION 1.45)
set(LIBINPUT_MINIMUM_VERSION 1.28)
@ -265,7 +267,8 @@ pkg_check_modules(
gbm
gio-2.0
re2
muparser)
muparser
lcms2)
find_package(hyprwayland-scanner 0.3.10 REQUIRED)
@ -298,21 +301,6 @@ if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
target_compile_options(hyprland_lib PUBLIC -fsanitize=address)
endif()
if(USE_TRACY)
message(STATUS "Tracy is turned on")
option(TRACY_ENABLE "" ON)
option(TRACY_ON_DEMAND "" ON)
add_subdirectory(subprojects/tracy)
target_link_libraries(hyprland_lib PUBLIC Tracy::TracyClient)
if(USE_TRACY_GPU)
message(STATUS "Tracy GPU Profiling is turned on")
add_compile_definitions(USE_TRACY_GPU)
endif()
endif()
add_compile_options(-fno-pie -fno-builtin)
add_link_options(-no-pie -fno-builtin)
if(USE_GPROF)
@ -321,6 +309,23 @@ if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
endif()
endif()
if(USE_TRACY)
message(STATUS "Tracy is turned on")
option(TRACY_ENABLE "" ON)
option(TRACY_ON_DEMAND "" ON)
add_subdirectory(subprojects/tracy)
add_compile_options(-fno-omit-frame-pointer)
target_link_libraries(hyprland_lib PUBLIC Tracy::TracyClient)
if(USE_TRACY_GPU)
message(STATUS "Tracy GPU Profiling is turned on")
add_compile_definitions(USE_TRACY_GPU)
endif()
endif()
if(BUILT_WITH_NIX)
add_compile_definitions(BUILT_WITH_NIX)
endif()
@ -474,7 +479,11 @@ function(protocolWayland)
set(PROTOCOL_SOURCES "${PROTOCOL_SOURCES}" PARENT_SCOPE)
endfunction()
target_link_libraries(hyprland_lib PUBLIC OpenGL::EGL OpenGL::GL Threads::Threads)
if(TARGET OpenGL::GL)
target_link_libraries(hyprland_lib PUBLIC OpenGL::EGL OpenGL::GL glslang::glslang glslang::glslang-default-resource-limits glslang::SPIRV Threads::Threads)
else()
target_link_libraries(hyprland_lib PUBLIC OpenGL::EGL OpenGL::GLES3 glslang::glslang glslang::glslang-default-resource-limits glslang::SPIRV Threads::Threads)
endif()
pkg_check_modules(hyprland_protocols_dep hyprland-protocols>=0.6.4)
if(hyprland_protocols_dep_FOUND)
@ -549,6 +558,8 @@ protocolnew("staging/ext-data-control" "ext-data-control-v1" false)
protocolnew("staging/pointer-warp" "pointer-warp-v1" false)
protocolnew("staging/fifo" "fifo-v1" false)
protocolnew("staging/commit-timing" "commit-timing-v1" false)
protocolnew("staging/ext-image-capture-source" "ext-image-capture-source-v1" false)
protocolnew("staging/ext-image-copy-capture" "ext-image-copy-capture-v1" false)
protocolwayland()

View file

@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) 2022-2025, vaxerski
Copyright (c) 2022-2026, vaxerski
All rights reserved.
Redistribution and use in source and binary forms, with or without

View file

@ -18,6 +18,7 @@ nopch:
clear:
rm -rf build
rm -f ./protocols/*.h ./protocols/*.c ./protocols/*.cpp ./protocols/*.hpp
rm -f ./hyprctl/hw-protocols/*.cpp ./hyprctl/hw-protocols/*.hpp
all:
$(MAKE) clear
@ -87,7 +88,7 @@ asan:
@echo "Wayland done"
patch -p1 < ./scripts/hyprlandStaticAsan.diff
cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Debug -DWITH_ASAN:STRING=True -DUSE_TRACY:STRING=False -DUSE_TRACY_GPU:STRING=False -S . -B ./build -G Ninja
cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Debug -DWITH_ASAN:STRING=True -DUSE_TRACY:STRING=False -DUSE_TRACY_GPU:STRING=False -S . -B ./build
cmake --build ./build --config Debug --target all
@echo "Hyprland done"

View file

@ -1 +1 @@
0.53.0
0.54.0

View file

@ -242,7 +242,7 @@ bind = $mainMod, E, exec, $fileManager
bind = $mainMod, V, togglefloating,
bind = $mainMod, R, exec, $menu
bind = $mainMod, P, pseudo, # dwindle
bind = $mainMod, J, togglesplit, # dwindle
bind = $mainMod, J, layoutmsg, togglesplit # dwindle
# Move focus with mainMod + arrow keys
bind = $mainMod, left, movefocus, l

64
flake.lock generated
View file

@ -16,11 +16,11 @@
]
},
"locked": {
"lastModified": 1765900596,
"narHash": "sha256-+hn8v9jkkLP9m+o0Nm5SiEq10W0iWDSotH2XfjU45fA=",
"lastModified": 1772292445,
"narHash": "sha256-4F1Q7U313TKUDDovCC96m/Za4wZcJ3yqtu4eSrj8lk8=",
"owner": "hyprwm",
"repo": "aquamarine",
"rev": "d83c97f8f5c0aae553c1489c7d9eff3eadcadace",
"rev": "1dbbba659c1cef0b0202ce92cadfe13bae550e8f",
"type": "github"
},
"original": {
@ -32,15 +32,15 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1761588595,
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
"owner": "edolstra",
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "edolstra",
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
@ -105,11 +105,11 @@
]
},
"locked": {
"lastModified": 1763733840,
"narHash": "sha256-JnET78yl5RvpGuDQy3rCycOCkiKoLr5DN1fPhRNNMco=",
"lastModified": 1770511807,
"narHash": "sha256-suKmSbSk34uPOJDTg/GbPrKEJutzK08vj0VoTvAFBCA=",
"owner": "hyprwm",
"repo": "hyprgraphics",
"rev": "8f1bec691b2d198c60cccabca7a94add2df4ed1a",
"rev": "7c75487edd43a71b61adb01cae8326d277aab683",
"type": "github"
},
"original": {
@ -144,11 +144,11 @@
]
},
"locked": {
"lastModified": 1765643131,
"narHash": "sha256-CCGohW5EBIRy4B7vTyBMqPgsNcaNenVad/wszfddET0=",
"lastModified": 1767023960,
"narHash": "sha256-R2HgtVS1G3KSIKAQ77aOZ+Q0HituOmPgXW9nBNkpp3Q=",
"owner": "hyprwm",
"repo": "hyprland-guiutils",
"rev": "e50ae912813bdfa8372d62daf454f48d6df02297",
"rev": "c2e906261142f5dd1ee0bfc44abba23e2754c660",
"type": "github"
},
"original": {
@ -193,11 +193,11 @@
]
},
"locked": {
"lastModified": 1764612430,
"narHash": "sha256-54ltTSbI6W+qYGMchAgCR6QnC1kOdKXN6X6pJhOWxFg=",
"lastModified": 1771866172,
"narHash": "sha256-fYFoXhQLrm1rD8vSFKQBOEX4OGCuJdLt1amKfHd5GAw=",
"owner": "hyprwm",
"repo": "hyprlang",
"rev": "0d00dc118981531aa731150b6ea551ef037acddd",
"rev": "0b219224910e7642eb0ed49f0db5ec3d008e3e41",
"type": "github"
},
"original": {
@ -261,11 +261,11 @@
]
},
"locked": {
"lastModified": 1766160771,
"narHash": "sha256-roINUGikWRqqgKrD4iotKbGj3ZKJl3hjMz5l/SyKrHw=",
"lastModified": 1771271487,
"narHash": "sha256-41gEiUS0Pyw3L/ge1l8MXn61cK14VAhgWB/JV8s/oNI=",
"owner": "hyprwm",
"repo": "hyprutils",
"rev": "5ac060bfcf2f12b3a6381156ebbc13826a05b09f",
"rev": "340a792e3b3d482c4ae5f66d27a9096bdee6d76d",
"type": "github"
},
"original": {
@ -284,11 +284,11 @@
]
},
"locked": {
"lastModified": 1763640274,
"narHash": "sha256-Uan1Nl9i4TF/kyFoHnTq1bd/rsWh4GAK/9/jDqLbY5A=",
"lastModified": 1770501770,
"narHash": "sha256-NWRM6+YxTRv+bT9yvlhhJ2iLae1B1pNH3mAL5wi2rlQ=",
"owner": "hyprwm",
"repo": "hyprwayland-scanner",
"rev": "f6cf414ca0e16a4d30198fd670ec86df3c89f671",
"rev": "0bd8b6cde9ec27d48aad9e5b4deefb3746909d40",
"type": "github"
},
"original": {
@ -310,11 +310,11 @@
]
},
"locked": {
"lastModified": 1766253200,
"narHash": "sha256-26qPwrd3od+xoYVywSB7hC2cz9ivN46VPLlrsXyGxvE=",
"lastModified": 1771606233,
"narHash": "sha256-F3PLUqQ/TwgR70U+UeOqJnihJZ2EuunzojYC4g5xHr0=",
"owner": "hyprwm",
"repo": "hyprwire",
"rev": "1079777525b30a947c8d657fac158e00ae85de9d",
"rev": "06c7f1f8c4194786c8400653c4efc49dc14c0f3a",
"type": "github"
},
"original": {
@ -325,11 +325,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1766070988,
"narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=",
"lastModified": 1772198003,
"narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c6245e83d836d0433170a16eb185cefe0572f8b8",
"rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61",
"type": "github"
},
"original": {
@ -348,11 +348,11 @@
]
},
"locked": {
"lastModified": 1765911976,
"narHash": "sha256-t3T/xm8zstHRLx+pIHxVpQTiySbKqcQbK+r+01XVKc0=",
"lastModified": 1772024342,
"narHash": "sha256-+eXlIc4/7dE6EcPs9a2DaSY3fTA9AE526hGqkNID3Wg=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "b68b780b69702a090c8bb1b973bab13756cc7a27",
"rev": "6e34e97ed9788b17796ee43ccdbaf871a5c2b476",
"type": "github"
},
"original": {

206
flake.nix
View file

@ -88,108 +88,122 @@
};
};
outputs = inputs @ {
self,
nixpkgs,
systems,
...
}: let
inherit (nixpkgs) lib;
eachSystem = lib.genAttrs (import systems);
pkgsFor = eachSystem (system:
import nixpkgs {
localSystem = system;
overlays = with self.overlays; [
hyprland-packages
hyprland-extras
];
});
pkgsCrossFor = eachSystem (system: crossSystem:
import nixpkgs {
localSystem = system;
inherit crossSystem;
overlays = with self.overlays; [
hyprland-packages
hyprland-extras
];
});
pkgsDebugFor = eachSystem (system:
import nixpkgs {
localSystem = system;
overlays = with self.overlays; [
hyprland-debug
];
});
pkgsDebugCrossFor = eachSystem (system: crossSystem:
import nixpkgs {
localSystem = system;
inherit crossSystem;
overlays = with self.overlays; [
hyprland-debug
];
});
in {
overlays = import ./nix/overlays.nix {inherit self lib inputs;};
outputs =
inputs@{
self,
nixpkgs,
systems,
...
}:
let
inherit (nixpkgs) lib;
eachSystem = lib.genAttrs (import systems);
pkgsFor = eachSystem (
system:
import nixpkgs {
localSystem = system;
overlays = with self.overlays; [
hyprland-packages
hyprland-extras
];
}
);
pkgsCrossFor = eachSystem (
system: crossSystem:
import nixpkgs {
localSystem = system;
inherit crossSystem;
overlays = with self.overlays; [
hyprland-packages
hyprland-extras
];
}
);
pkgsDebugFor = eachSystem (
system:
import nixpkgs {
localSystem = system;
overlays = with self.overlays; [
hyprland-debug
];
}
);
pkgsDebugCrossFor = eachSystem (
system: crossSystem:
import nixpkgs {
localSystem = system;
inherit crossSystem;
overlays = with self.overlays; [
hyprland-debug
];
}
);
in
{
overlays = import ./nix/overlays.nix { inherit self lib inputs; };
checks = eachSystem (system:
(lib.filterAttrs
(n: _: (lib.hasPrefix "hyprland" n) && !(lib.hasSuffix "debug" n))
self.packages.${system})
// {
inherit (self.packages.${system}) xdg-desktop-portal-hyprland;
pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run {
src = ./.;
hooks = {
hyprland-treewide-formatter = {
enable = true;
entry = "${self.formatter.${system}}/bin/hyprland-treewide-formatter";
pass_filenames = false;
excludes = ["subprojects"];
always_run = true;
checks = eachSystem (
system:
(lib.filterAttrs (
n: _: (lib.hasPrefix "hyprland" n) && !(lib.hasSuffix "debug" n)
) self.packages.${system})
// {
inherit (self.packages.${system}) xdg-desktop-portal-hyprland;
pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run {
src = ./.;
hooks = {
hyprland-treewide-formatter = {
enable = true;
entry = "${self.formatter.${system}}/bin/hyprland-treewide-formatter";
pass_filenames = false;
excludes = [ "subprojects" ];
always_run = true;
};
};
};
};
}
// (import ./nix/tests inputs pkgsFor.${system}));
}
// (import ./nix/tests inputs pkgsFor.${system})
);
packages = eachSystem (system: {
default = self.packages.${system}.hyprland;
inherit
(pkgsFor.${system})
# hyprland-packages
hyprland
hyprland-unwrapped
hyprland-with-tests
# hyprland-extras
xdg-desktop-portal-hyprland
;
inherit (pkgsDebugFor.${system}) hyprland-debug;
hyprland-cross = (pkgsCrossFor.${system} "aarch64-linux").hyprland;
hyprland-debug-cross = (pkgsDebugCrossFor.${system} "aarch64-linux").hyprland-debug;
});
packages = eachSystem (system: {
default = self.packages.${system}.hyprland;
inherit (pkgsFor.${system})
# hyprland-packages
hyprland
hyprland-unwrapped
hyprland-with-tests
# hyprland-extras
xdg-desktop-portal-hyprland
;
inherit (pkgsDebugFor.${system}) hyprland-debug;
hyprland-cross = (pkgsCrossFor.${system} "aarch64-linux").hyprland;
hyprland-debug-cross = (pkgsDebugCrossFor.${system} "aarch64-linux").hyprland-debug;
});
devShells = eachSystem (system: {
default =
pkgsFor.${system}.mkShell.override {
inherit (self.packages.${system}.default) stdenv;
} {
name = "hyprland-shell";
hardeningDisable = ["fortify"];
inputsFrom = [pkgsFor.${system}.hyprland];
packages = [pkgsFor.${system}.clang-tools];
inherit (self.checks.${system}.pre-commit-check) shellHook;
};
});
devShells = eachSystem (system: {
default =
pkgsFor.${system}.mkShell.override
{
inherit (self.packages.${system}.default) stdenv;
}
{
name = "hyprland-shell";
hardeningDisable = [ "fortify" ];
inputsFrom = [ pkgsFor.${system}.hyprland ];
packages = [ pkgsFor.${system}.clang-tools ];
inherit (self.checks.${system}.pre-commit-check) shellHook;
};
});
formatter = eachSystem (system: pkgsFor.${system}.callPackage ./nix/formatter.nix {});
formatter = eachSystem (system: pkgsFor.${system}.callPackage ./nix/formatter.nix { });
nixosModules.default = import ./nix/module.nix inputs;
homeManagerModules.default = import ./nix/hm-module.nix self;
nixosModules.default = import ./nix/module.nix inputs;
homeManagerModules.default = import ./nix/hm-module.nix self;
# Hydra build jobs
# Recent versions of Hydra can aggregate jobsets from 'hydraJobs' instead of a release.nix
# or similar. Remember to filter large or incompatible attributes here. More eval jobs can
# be added by merging, e.g., self.packages // self.devShells.
hydraJobs = self.packages;
};
# Hydra build jobs
# Recent versions of Hydra can aggregate jobsets from 'hydraJobs' instead of a release.nix
# or similar. Remember to filter large or incompatible attributes here. More eval jobs can
# be added by merging, e.g., self.packages // self.devShells.
hydraJobs = self.packages;
};
}

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="hyprpaper_core" version="1">
<protocol name="hyprpaper_core" version="2">
<copyright>
BSD 3-Clause License
@ -31,7 +31,7 @@
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
</copyright>
<object name="hyprpaper_core_manager" version="1">
<object name="hyprpaper_core_manager" version="2">
<description summary="manager object">
This is the core manager object for hyprpaper operations
</description>
@ -62,6 +62,13 @@
Destroys this object. Children remain alive until destroyed.
</description>
</c2s>
<c2s name="get_status_object" since="2">
<description summary="Get a status object">
Creates a status object
</description>
<returns iface="hyprpaper_status"/>
</c2s>
</object>
<enum name="wallpaper_fit_mode">
@ -141,4 +148,25 @@
</description>
</c2s>
</object>
<object name="hyprpaper_status" version="2">
<description summary="status object">
This is an object which will emit various status updates.
</description>
<s2c name="active_wallpaper">
<description summary="Active wallpaper state">
Sends the active wallpaper for a given monitor. This will be emitted
immediately after binding, and then every time the path changes.
</description>
<arg name="monitor" type="varchar" summary="monitor name"/>
<arg name="path" type="varchar" summary="wallpaper path"/>
</s2c>
<c2s name="destroy" destructor="true">
<description summary="Destroy this object">
Destroys this object.
</description>
</c2s>
</object>
</protocol>

View file

@ -4,6 +4,7 @@
#include <optional>
#include <format>
#include <filesystem>
#include <print>
#include <hyprpaper_core-client.hpp>
@ -15,7 +16,7 @@ using namespace std::string_literals;
constexpr const char* SOCKET_NAME = ".hyprpaper.sock";
static SP<CCHyprpaperCoreImpl> g_coreImpl;
constexpr const uint32_t PROTOCOL_VERSION_SUPPORTED = 1;
constexpr const uint32_t PROTOCOL_VERSION_SUPPORTED = 2;
//
static hyprpaperCoreWallpaperFitMode fitFromString(const std::string_view& sv) {
@ -53,21 +54,7 @@ static std::expected<std::string, std::string> getFullPath(const std::string_vie
return resolvePath(sv);
}
std::expected<void, std::string> Hyprpaper::makeHyprpaperRequest(const std::string_view& rq) {
if (!rq.contains(' '))
return std::unexpected("Invalid request");
if (!rq.starts_with("/hyprpaper "))
return std::unexpected("Invalid request");
std::string_view LHS, RHS;
auto spacePos = rq.find(' ', 12);
LHS = rq.substr(11, spacePos - 11);
RHS = rq.substr(spacePos + 1);
if (LHS != "wallpaper")
return std::unexpected("Unknown hyprpaper request");
static std::expected<void, std::string> doWallpaper(const std::string_view& RHS) {
CVarList2 args(std::string{RHS}, 0, ',');
const std::string MONITOR = std::string{args[0]};
@ -99,7 +86,7 @@ std::expected<void, std::string> Hyprpaper::makeHyprpaperRequest(const std::stri
if (!socket)
return std::unexpected("can't send: failed to connect to hyprpaper (is it running?)");
g_coreImpl = makeShared<CCHyprpaperCoreImpl>(1);
g_coreImpl = makeShared<CCHyprpaperCoreImpl>(PROTOCOL_VERSION_SUPPORTED);
socket->addImplementation(g_coreImpl);
@ -111,7 +98,7 @@ std::expected<void, std::string> Hyprpaper::makeHyprpaperRequest(const std::stri
if (!spec)
return std::unexpected("can't send: hyprpaper doesn't have the spec?!");
auto manager = makeShared<CCHyprpaperCoreManagerObject>(socket->bindProtocol(g_coreImpl->protocol(), PROTOCOL_VERSION_SUPPORTED));
auto manager = makeShared<CCHyprpaperCoreManagerObject>(socket->bindProtocol(g_coreImpl->protocol(), std::min(PROTOCOL_VERSION_SUPPORTED, spec->specVer())));
if (!manager)
return std::unexpected("wire error: couldn't create manager");
@ -126,7 +113,11 @@ std::expected<void, std::string> Hyprpaper::makeHyprpaperRequest(const std::stri
wallpaper->setFailed([&canExit, &err](uint32_t code) {
canExit = true;
err = std::format("failed to set wallpaper, code {}", code);
switch (code) {
case HYPRPAPER_CORE_APPLYING_ERROR_INVALID_PATH: err = std::format("failed to set wallpaper: Invalid path", code); break;
case HYPRPAPER_CORE_APPLYING_ERROR_INVALID_MONITOR: err = std::format("failed to set wallpaper: Invalid monitor", code); break;
default: err = std::format("failed to set wallpaper: unknown error, code {}", code); break;
}
});
wallpaper->setSuccess([&canExit]() { canExit = true; });
@ -145,4 +136,73 @@ std::expected<void, std::string> Hyprpaper::makeHyprpaperRequest(const std::stri
return std::unexpected(*err);
return {};
}
}
static std::expected<void, std::string> doListActive() {
const auto RTDIR = getenv("XDG_RUNTIME_DIR");
if (!RTDIR || RTDIR[0] == '\0')
return std::unexpected("can't send: no XDG_RUNTIME_DIR");
const auto HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE");
if (!HIS || HIS[0] == '\0')
return std::unexpected("can't send: no HYPRLAND_INSTANCE_SIGNATURE (not running under hyprland)");
auto socketPath = RTDIR + "/hypr/"s + HIS + "/"s + SOCKET_NAME;
auto socket = Hyprwire::IClientSocket::open(socketPath);
if (!socket)
return std::unexpected("can't send: failed to connect to hyprpaper (is it running?)");
g_coreImpl = makeShared<CCHyprpaperCoreImpl>(PROTOCOL_VERSION_SUPPORTED);
socket->addImplementation(g_coreImpl);
if (!socket->waitForHandshake())
return std::unexpected("can't send: wire handshake failed");
auto spec = socket->getSpec(g_coreImpl->protocol()->specName());
if (!spec)
return std::unexpected("can't send: hyprpaper doesn't have the spec?!");
if (spec->specVer() < 2)
return std::unexpected("can't send: hyprpaper protocol version too low (hyprpaper too old)");
auto manager = makeShared<CCHyprpaperCoreManagerObject>(socket->bindProtocol(g_coreImpl->protocol(), std::min(PROTOCOL_VERSION_SUPPORTED, spec->specVer())));
if (!manager)
return std::unexpected("wire error: couldn't create manager");
auto status = makeShared<CCHyprpaperStatusObject>(manager->sendGetStatusObject());
status->setActiveWallpaper([](const char* mon, const char* wp) { std::println("{}: {}", mon, wp); });
socket->roundtrip();
return {};
}
std::expected<void, std::string> Hyprpaper::makeHyprpaperRequest(const std::string_view& rq) {
if (!rq.contains(' '))
return std::unexpected("Invalid request");
if (!rq.starts_with("/hyprpaper "))
return std::unexpected("Invalid request");
std::string_view LHS, RHS;
auto spacePos = rq.find(' ', 12);
LHS = rq.substr(11, spacePos - 11);
RHS = rq.substr(spacePos + 1);
if (LHS == "wallpaper")
return doWallpaper(RHS);
else if (LHS == "listactive")
return doListActive();
else
return std::unexpected("invalid hyprpaper request");
return {};
}

View file

@ -228,23 +228,23 @@ int request(std::string_view arg, int minArgs = 0, bool needRoll = false) {
constexpr size_t BUFFER_SIZE = 8192;
char buffer[BUFFER_SIZE] = {0};
sizeWritten = read(SERVERSOCKET, buffer, BUFFER_SIZE);
if (sizeWritten < 0) {
if (errno == EWOULDBLOCK)
log("Hyprland IPC didn't respond in time\n");
log("Couldn't read (6)");
return 6;
}
reply += std::string(buffer, sizeWritten);
while (sizeWritten == BUFFER_SIZE) {
// read all data until server closes the connection
// this handles partial writes on the server side under high load
while (true) {
sizeWritten = read(SERVERSOCKET, buffer, BUFFER_SIZE);
if (sizeWritten < 0) {
if (errno == EWOULDBLOCK)
log("Hyprland IPC didn't respond in time\n");
log("Couldn't read (6)");
return 6;
}
if (sizeWritten == 0) {
// server closed connection, we're done
break;
}
reply += std::string(buffer, sizeWritten);
}

View file

@ -11,9 +11,9 @@ set(CMAKE_CXX_STANDARD 23)
pkg_check_modules(hyprpm_deps REQUIRED IMPORTED_TARGET tomlplusplus hyprutils>=0.7.0)
find_package(glaze 6.0.0 QUIET)
find_package(glaze 6.0.1 QUIET)
if (NOT glaze_FOUND)
set(GLAZE_VERSION v6.1.0)
set(GLAZE_VERSION v6.0.1)
message(STATUS "glaze dependency not found, retrieving ${GLAZE_VERSION} with FetchContent")
include(FetchContent)
FetchContent_Declare(
@ -21,6 +21,7 @@ if (NOT glaze_FOUND)
GIT_REPOSITORY https://github.com/stephenberry/glaze.git
GIT_TAG ${GLAZE_VERSION}
GIT_SHALLOW TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(glaze)
endif()

View file

@ -94,15 +94,18 @@ SHyprlandVersion CPluginManager::getHyprlandVersion(bool running) {
auto hldate = (*jsonQuery)["commit_date"].get_string();
auto hlcommits = (*jsonQuery)["commits"].get_string();
auto flags = (*jsonQuery)["flags"].get_array();
bool isNix = std::ranges::any_of(flags, [](const auto& f) { return f.is_string() && f.get_string() == std::string_view{"nix"}; });
size_t commits = 0;
try {
commits = std::stoull(hlcommits);
} catch (...) { ; }
if (m_bVerbose)
std::println("{}", verboseString("parsed commit {} at branch {} on {}, commits {}", hlcommit, hlbranch, hldate, commits));
std::println("{}", verboseString("parsed commit {} at branch {} on {}, commits {}, nix: {}", hlcommit, hlbranch, hldate, commits, isNix));
auto ver = SHyprlandVersion{hlbranch, hlcommit, hldate, abiHash, commits};
auto ver = SHyprlandVersion{hlbranch, hlcommit, hldate, abiHash, commits, isNix};
if (running)
verRunning = ver;
@ -128,12 +131,20 @@ bool CPluginManager::createSafeDirectory(const std::string& path) {
return true;
}
bool CPluginManager::validArg(const std::string& s) {
return !s.contains("'") && !s.ends_with("\\") && !s.starts_with("\\");
}
bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string& rev) {
const auto HLVER = getHyprlandVersion();
if (!validArg(url) || !validArg(rev)) {
std::println(stderr, "\n{}", failureString("url or rev invalid"));
return false;
}
if (!hasDeps()) {
std::println(stderr, "\n{}",
failureString("Could not clone the plugin repository. Dependencies not satisfied. Hyprpm requires: cmake, meson, cpio, pkg-config, git, g++, gcc"));
std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. Dependencies not satisfied. Hyprpm requires: cmake, cpio, pkg-config, git, g++, gcc"));
return false;
}
@ -196,7 +207,7 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
progress.printMessageAbove(infoString("Cloning {}", url));
std::string ret = execAndGet(std::format("cd {} && git clone --recursive {} {}", getTempRoot(), url, USERNAME));
std::string ret = execAndGet(std::format("cd {} && git clone --recursive '{}' {}", getTempRoot(), url, USERNAME));
if (!std::filesystem::exists(m_szWorkingPluginDirectory + "/.git")) {
std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. shell returned:\n{}", ret));
@ -303,8 +314,14 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
progress.printMessageAbove(infoString("Building {}", p.name));
for (auto const& bs : p.buildSteps) {
const std::string& cmd = std::format("cd {} && PKG_CONFIG_PATH=\"{}/share/pkgconfig\" {}", m_szWorkingPluginDirectory, DataState::getHeadersPath(), bs);
out += " -> " + cmd + "\n" + execAndGet(cmd) + "\n";
const auto CMD_RAW = nixDevelopIfNeeded(std::format("cd {} && PKG_CONFIG_PATH=\"{}\" {}", m_szWorkingPluginDirectory, getPkgConfigPath(), bs), HLVER);
if (!CMD_RAW) {
progress.printMessageAbove(failureString("Failed to build {}: {}", p.name, CMD_RAW.error()));
break;
}
out += " -> " + *CMD_RAW + "\n" + execAndGet(*CMD_RAW) + "\n";
}
if (m_bVerbose)
@ -389,7 +406,7 @@ eHeadersErrors CPluginManager::headersValid() {
return HEADERS_MISSING;
// find headers commit
const std::string& cmd = std::format("PKG_CONFIG_PATH=\"{}/share/pkgconfig\" pkgconf --cflags --keep-system-cflags hyprland", DataState::getHeadersPath());
const std::string& cmd = std::format("PKG_CONFIG_PATH=\"{}\" pkgconf --cflags --keep-system-cflags hyprland", getPkgConfigPath());
auto headers = execAndGet(cmd);
if (!headers.contains("-I/"))
@ -453,7 +470,7 @@ bool CPluginManager::updateHeaders(bool force) {
const auto HLVER = getHyprlandVersion(false);
if (!hasDeps()) {
std::println("\n{}", failureString("Could not update. Dependencies not satisfied. Hyprpm requires: cmake, meson, cpio, pkg-config, git, g++, gcc"));
std::println("\n{}", failureString("Could not update. Dependencies not satisfied. Hyprpm requires: cmake, cpio, pkg-config, git, g++, gcc"));
return false;
}
@ -495,11 +512,11 @@ bool CPluginManager::updateHeaders(bool force) {
progress.printMessageAbove(verboseString("will shallow since: {}", SHALLOW_DATE));
std::string ret =
execAndGet(std::format("cd {} && git clone --recursive {} hyprland-{}{}", getTempRoot(), HL_URL, USERNAME, (bShallow ? " --shallow-since='" + SHALLOW_DATE + "'" : "")));
execAndGet(std::format("cd {} && git clone --recursive '{}' hyprland-{}{}", getTempRoot(), HL_URL, USERNAME, (bShallow ? " --shallow-since='" + SHALLOW_DATE + "'" : "")));
if (!std::filesystem::exists(WORKINGDIR)) {
progress.printMessageAbove(failureString("Clone failed. Retrying without shallow."));
ret = execAndGet(std::format("cd {} && git clone --recursive {} hyprland-{}", getTempRoot(), HL_URL, USERNAME));
ret = execAndGet(std::format("cd {} && git clone --recursive '{}' hyprland-{}", getTempRoot(), HL_URL, USERNAME));
}
if (!std::filesystem::exists(WORKINGDIR + "/.git")) {
@ -542,8 +559,17 @@ bool CPluginManager::updateHeaders(bool force) {
if (m_bVerbose)
progress.printMessageAbove(verboseString("setting PREFIX for cmake to {}", DataState::getHeadersPath()));
ret = execAndGet(std::format("cd {} && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:STRING=\"{}\" -S . -B ./build -G Ninja", WORKINGDIR,
DataState::getHeadersPath()));
const auto CONFIGURE_CMD =
nixDevelopIfNeeded(std::format("cd {} && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:STRING=\"{}\" -S . -B ./build", WORKINGDIR,
DataState::getHeadersPath()),
HLVER);
if (!CONFIGURE_CMD) {
std::println(stderr, "\n{}", failureString("Could not configure hyprland: {}", CONFIGURE_CMD.error()));
return false;
}
ret = execAndGet(*CONFIGURE_CMD);
if (m_bVerbose)
progress.printMessageAbove(verboseString("cmake returned: {}", ret));
@ -631,7 +657,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
const auto HLVER = getHyprlandVersion(false);
CProgressBar progress;
progress.m_iMaxSteps = REPOS.size() * 2 + 2;
progress.m_iMaxSteps = (REPOS.size() * 2) + 2;
progress.m_iSteps = 0;
progress.m_szCurrentMessage = "Updating repositories";
progress.print();
@ -652,7 +678,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
progress.printMessageAbove(infoString("Cloning {}", repo.url));
std::string ret = execAndGet(std::format("cd {} && git clone --recursive {} {}", getTempRoot(), repo.url, USERNAME));
std::string ret = execAndGet(std::format("cd {} && git clone --recursive '{}' {}", getTempRoot(), repo.url, USERNAME));
if (!std::filesystem::exists(m_szWorkingPluginDirectory + "/.git")) {
std::println("{}", failureString("could not clone repo: shell returned: {}", ret));
@ -662,7 +688,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
if (!repo.rev.empty()) {
progress.printMessageAbove(infoString("Plugin has revision set, resetting: {}", repo.rev));
std::string ret = execAndGet("git -C " + m_szWorkingPluginDirectory + " reset --hard --recurse-submodules " + repo.rev);
std::string ret = execAndGet("git -C " + m_szWorkingPluginDirectory + " reset --hard --recurse-submodules \'" + repo.rev + "\'");
if (ret.compare(0, 6, "fatal:") == 0) {
std::println(stderr, "\n{}", failureString("could not check out revision {}: shell returned:\n{}", repo.rev, ret));
@ -741,8 +767,14 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
progress.printMessageAbove(infoString("Building {}", p.name));
for (auto const& bs : p.buildSteps) {
const std::string& cmd = std::format("cd {} && PKG_CONFIG_PATH=\"{}/share/pkgconfig\" {}", m_szWorkingPluginDirectory, DataState::getHeadersPath(), bs);
out += " -> " + cmd + "\n" + execAndGet(cmd) + "\n";
const auto CMD_RAW = nixDevelopIfNeeded(std::format("cd {} && PKG_CONFIG_PATH=\"{}\" {}", m_szWorkingPluginDirectory, getPkgConfigPath(), bs), HLVER);
if (!CMD_RAW) {
progress.printMessageAbove(failureString("Failed to build {}: {}", p.name, CMD_RAW.error()));
break;
}
out += " -> " + *CMD_RAW + "\n" + execAndGet(*CMD_RAW) + "\n";
}
if (m_bVerbose)
@ -772,8 +804,8 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
repohash.pop_back();
newrepo.hash = repohash;
for (auto const& p : pManifest->m_plugins) {
const auto OLDPLUGINIT = std::find_if(repo.plugins.begin(), repo.plugins.end(), [&](const auto& other) { return other.name == p.name; });
newrepo.plugins.push_back(SPlugin{p.name, m_szWorkingPluginDirectory + "/" + p.output, OLDPLUGINIT != repo.plugins.end() ? OLDPLUGINIT->enabled : false});
const auto OLDPLUGINIT = std::ranges::find_if(repo.plugins, [&](const auto& other) { return other.name == p.name; });
newrepo.plugins.emplace_back(SPlugin{p.name, m_szWorkingPluginDirectory + "/" + p.output, OLDPLUGINIT != repo.plugins.end() ? OLDPLUGINIT->enabled : false});
}
DataState::removePluginRepo(SPluginRepoIdentifier::fromName(newrepo.name));
DataState::addNewPluginRepo(newrepo);
@ -899,7 +931,7 @@ ePluginLoadStateReturn CPluginManager::ensurePluginsLoadState(bool forceReload)
if (!p.enabled)
continue;
if (!forceReload && std::find_if(loadedPlugins.begin(), loadedPlugins.end(), [&](const auto& other) { return other == p.name; }) != loadedPlugins.end())
if (!forceReload && std::ranges::find_if(loadedPlugins, [&](const auto& other) { return other == p.name; }) != loadedPlugins.end())
continue;
if (!loadUnloadPlugin(HYPRPMPATH / repoForName(p.name) / p.filename, true)) {
@ -987,8 +1019,11 @@ std::string CPluginManager::headerErrorShort(const eHeadersErrors err) {
}
bool CPluginManager::hasDeps() {
if (!m_bNoNix && getHyprlandVersion().isNix)
return true; // dep check not needed if we are on nix
bool hasAllDeps = true;
std::vector<std::string> deps = {"meson", "cpio", "cmake", "pkg-config", "g++", "gcc", "git"};
std::vector<std::string> deps = {"cpio", "cmake", "pkg-config", "g++", "gcc", "git"};
for (auto const& d : deps) {
if (!execAndGet("command -v " + d).contains("/")) {
@ -999,3 +1034,92 @@ bool CPluginManager::hasDeps() {
return hasAllDeps;
}
const std::string& CPluginManager::getPkgConfigPath() {
static const auto str = std::format("{}/share/pkgconfig:$PKG_CONFIG_PATH", DataState::getHeadersPath());
return str;
}
static std::expected<std::string, std::string> getNixDevelopFromPath(const std::string& argv0) {
std::string fullStorePath;
if (argv0.starts_with("/")) {
// we can use this directly
fullStorePath = argv0;
} else {
// use hyprpm, find in path
auto exe = NSys::findInPath("hyprpm");
if (!exe)
return std::unexpected("hyprpm not found in PATH");
fullStorePath = *exe;
}
if (fullStorePath.empty() || !fullStorePath.ends_with("/bin/hyprpm"))
return std::unexpected("couldn't get a real path for hyprpm (1)");
// canonicalize to get the real nix-store path
std::error_code ec;
fullStorePath = std::filesystem::canonical(fullStorePath, ec);
if (ec || fullStorePath.empty() || !fullStorePath.starts_with("/nix"))
return std::unexpected("couldn't get a real path for hyprpm");
fullStorePath = fullStorePath.substr(0, fullStorePath.length() - std::string_view{"/bin/hyprpm"}.length());
auto deriver = trim(execAndGet(std::format("echo \"$(nix-store --query --deriver '{}')\"", fullStorePath)));
if (deriver.starts_with("unknown"))
return std::unexpected("couldn't nix deriver");
return deriver;
}
static std::expected<std::string, std::string> getNixDevelopFromProfile() {
const auto NIX_PROFILE_STR = execAndGet("nix profile list --json");
auto rawJson = glz::read_json<glz::generic>(NIX_PROFILE_STR);
if (!rawJson)
return std::unexpected("failed to parse nix profile list --json");
auto& json = *rawJson;
if (!json.contains("elements") || !json["elements"].is_object())
return std::unexpected("nix profile list --json returned a wonky json");
if (!json["elements"].contains("hyprland") && !json["elements"].contains("Hyprland"))
return std::unexpected("nix profile list --json doesn't contain Hyprland (did you uninstall?)");
auto& hyprlandJson = json["elements"].contains("hyprland") ? json["elements"]["hyprland"] : json["elements"]["Hyprland"];
if (!hyprlandJson.contains("originalUrl"))
return std::unexpected("nix profile list --json's hyprland doesn't contain originalUrl?");
return hyprlandJson["originalUrl"].get_string();
}
std::expected<std::string, std::string> CPluginManager::nixDevelopIfNeeded(const std::string& cmd, const SHyprlandVersion& ver) {
if (m_bNoNix || !ver.isNix)
return cmd;
// Escape single quotes
std::string newCmd = cmd;
replaceInString(newCmd, "'", "\\'");
auto NIX_DEVELOP = getNixDevelopFromPath(m_szArgv0);
if (NIX_DEVELOP)
return std::format("nix develop '{}' --command bash -c $'{}'", *NIX_DEVELOP, newCmd);
else if (m_bVerbose)
std::println("{}", verboseString("Failed nix from path: {}", NIX_DEVELOP.error()));
NIX_DEVELOP = getNixDevelopFromProfile();
if (NIX_DEVELOP)
return std::format("nix develop '{}' --command bash -c $'{}'", *NIX_DEVELOP, newCmd);
else if (m_bVerbose)
std::println("{}", verboseString("Failed nix from profile: {}", NIX_DEVELOP.error()));
return std::unexpected("hyprland is nix, but hyprpm failed to obtain a nix develop shell for build cmd");
}

View file

@ -4,7 +4,7 @@
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <expected>
#include "Plugin.hpp"
enum eHeadersErrors {
@ -41,6 +41,7 @@ struct SHyprlandVersion {
std::string date;
std::string abiHash;
int commits = 0;
bool isNix = false;
};
class CPluginManager {
@ -65,20 +66,26 @@ class CPluginManager {
void notify(const eNotifyIcons icon, uint32_t color, int durationMs, const std::string& message);
const std::string& getPkgConfigPath();
bool hasDeps();
bool m_bVerbose = false;
bool m_bNoShallow = false;
std::string m_szCustomHlUrl, m_szUsername;
bool m_bNoNix = false;
std::string m_szCustomHlUrl, m_szUsername, m_szArgv0;
// will delete recursively if exists!!
bool createSafeDirectory(const std::string& path);
private:
std::string headerError(const eHeadersErrors err);
std::string headerErrorShort(const eHeadersErrors err);
std::string headerError(const eHeadersErrors err);
std::string headerErrorShort(const eHeadersErrors err);
bool validArg(const std::string& s);
std::string m_szWorkingPluginDirectory;
std::expected<std::string, std::string> nixDevelopIfNeeded(const std::string& cmd, const SHyprlandVersion& ver);
std::string m_szWorkingPluginDirectory;
};
inline std::unique_ptr<CPluginManager> g_pPluginManager;

View file

@ -35,9 +35,13 @@ static std::string validSubinsAsStr() {
}
static bool executableExistsInPath(const std::string& exe) {
return NSys::findInPath(exe).has_value();
}
std::optional<std::string> NSys::findInPath(const std::string& exe) {
const char* PATHENV = std::getenv("PATH");
if (!PATHENV)
return false;
return std::nullopt;
CVarList paths(PATHENV, 0, ':', true);
std::error_code ec;
@ -52,10 +56,10 @@ static bool executableExistsInPath(const std::string& exe) {
if (ec)
continue;
if ((perms & std::filesystem::perms::others_exec) != std::filesystem::perms::none)
return true;
return candidate.string();
}
return false;
return std::nullopt;
}
static std::string subin() {

View file

@ -1,11 +1,13 @@
#pragma once
#include <string>
#include <optional>
namespace NSys {
bool isSuperuser();
int getUID();
int getEUID();
bool isSuperuser();
int getUID();
int getEUID();
std::optional<std::string> findInPath(const std::string& exe);
// NOLINTNEXTLINE
namespace root {
@ -20,4 +22,4 @@ namespace NSys {
// Do not use this unless absolutely necessary!
std::string runAsSuperuserUnsafe(const std::string& cmd);
};
};
};

View file

@ -25,6 +25,7 @@ constexpr std::string_view HELP = R"#(┏ hyprpm, a Hyprland Plugin Manager
Flags:
--no-nix | Disable `nix develop` for build commands, even if Hyprland is nix.
--notify | -n Send a hyprland notification confirming successful plugin load.
Warnings/Errors trigger notifications regardless of this flag.
--help | -h Show this menu.
@ -47,7 +48,7 @@ int main(int argc, char** argv, char** envp) {
}
std::vector<std::string> command;
bool notify = false, verbose = false, force = false, noShallow = false;
bool notify = false, verbose = false, force = false, noShallow = false, noNix = false;
std::string customHlUrl;
for (int i = 1; i < argc; ++i) {
@ -63,6 +64,8 @@ int main(int argc, char** argv, char** envp) {
g_pPluginManager->notify(ICON_INFO, 0, 10000, "[hyprpm] -n flag is deprecated, see hyprpm --help.");
} else if (ARGS[i] == "--verbose" || ARGS[i] == "-v") {
verbose = true;
} else if (ARGS[i] == "--no-nix") {
noNix = true;
} else if (ARGS[i] == "--no-shallow" || ARGS[i] == "-s") {
noShallow = true;
} else if (ARGS[i] == "--hl-url") {
@ -91,7 +94,9 @@ int main(int argc, char** argv, char** envp) {
g_pPluginManager = std::make_unique<CPluginManager>();
g_pPluginManager->m_bVerbose = verbose;
g_pPluginManager->m_bNoShallow = noShallow;
g_pPluginManager->m_bNoNix = noNix;
g_pPluginManager->m_szCustomHlUrl = customHlUrl;
g_pPluginManager->m_szArgv0 = argv[0];
if (command[0] == "add") {
if (command.size() < 2) {

View file

@ -96,7 +96,9 @@ endfunction()
protocolnew("staging/pointer-warp" "pointer-warp-v1" false)
protocolnew("stable/xdg-shell" "xdg-shell" false)
protocolnew("unstable/keyboard-shortcuts-inhibit" "keyboard-shortcuts-inhibit-unstable-v1" false)
clientNew("pointer-warp" PROTOS "pointer-warp-v1" "xdg-shell")
clientNew("pointer-scroll" PROTOS "xdg-shell")
clientNew("child-window" PROTOS "xdg-shell")
clientNew("child-window" PROTOS "xdg-shell")
clientNew("shortcut-inhibitor" PROTOS "xdg-shell" "keyboard-shortcuts-inhibit-unstable-v1")

View file

@ -3,6 +3,7 @@
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <wayland-client.h>
#include <wayland.hpp>
@ -332,4 +333,4 @@ int main(int argc, char** argv) {
wl_display_disconnect(display);
return 0;
}
}

View file

@ -2,6 +2,7 @@
#include <sys/poll.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <print>
#include <format>
#include <string>

View file

@ -2,6 +2,7 @@
#include <sys/poll.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <print>
#include <format>
#include <string>

View file

@ -0,0 +1,297 @@
#include <sys/poll.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <print>
#include <wayland-client.h>
#include <wayland.hpp>
#include <xdg-shell.hpp>
#include <keyboard-shortcuts-inhibit-unstable-v1.hpp>
#include <hyprutils/memory/SharedPtr.hpp>
#include <hyprutils/math/Vector2D.hpp>
using Hyprutils::Math::Vector2D;
using namespace Hyprutils::Memory;
struct SWlState {
wl_display* display;
CSharedPointer<CCWlRegistry> registry;
// protocols
CSharedPointer<CCWlCompositor> wlCompositor;
CSharedPointer<CCWlSeat> wlSeat;
CSharedPointer<CCWlShm> wlShm;
CSharedPointer<CCXdgWmBase> xdgShell;
CSharedPointer<CCZwpKeyboardShortcutsInhibitManagerV1> inhibitManager;
// shm/buffer stuff
CSharedPointer<CCWlShmPool> shmPool;
CSharedPointer<CCWlBuffer> shmBuf;
int shmFd;
size_t shmBufSize;
bool xrgb8888_support = false;
// surface/toplevel stuff
CSharedPointer<CCWlSurface> surf;
CSharedPointer<CCXdgSurface> xdgSurf;
CSharedPointer<CCXdgToplevel> xdgToplevel;
Vector2D geom;
// pointer
CSharedPointer<CCWlPointer> pointer;
uint32_t enterSerial;
// shortcut inhibiting
CSharedPointer<CCZwpKeyboardShortcutsInhibitorV1> inhibitor;
};
static bool debug, started, shouldExit;
template <typename... Args>
//NOLINTNEXTLINE
static void clientLog(std::format_string<Args...> fmt, Args&&... args) {
std::string text = std::vformat(fmt.get(), std::make_format_args(args...));
std::println("{}", text);
std::fflush(stdout);
}
template <typename... Args>
//NOLINTNEXTLINE
static void debugLog(std::format_string<Args...> fmt, Args&&... args) {
std::string text = std::vformat(fmt.get(), std::make_format_args(args...));
if (!debug)
return;
std::println("{}", text);
std::fflush(stdout);
}
static bool bindRegistry(SWlState& state) {
state.registry = makeShared<CCWlRegistry>((wl_proxy*)wl_display_get_registry(state.display));
state.registry->setGlobal([&](CCWlRegistry* r, uint32_t id, const char* name, uint32_t version) {
const std::string NAME = name;
if (NAME == "wl_compositor") {
debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
state.wlCompositor = makeShared<CCWlCompositor>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wl_compositor_interface, 6));
} else if (NAME == "wl_shm") {
debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
state.wlShm = makeShared<CCWlShm>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wl_shm_interface, 1));
} else if (NAME == "wl_seat") {
debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
state.wlSeat = makeShared<CCWlSeat>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wl_seat_interface, 9));
} else if (NAME == "xdg_wm_base") {
debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
state.xdgShell = makeShared<CCXdgWmBase>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &xdg_wm_base_interface, 1));
} else if (NAME == "zwp_keyboard_shortcuts_inhibit_manager_v1") {
debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
state.inhibitManager = makeShared<CCZwpKeyboardShortcutsInhibitManagerV1>(
(wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &zwp_keyboard_shortcuts_inhibit_manager_v1_interface, 1));
}
});
state.registry->setGlobalRemove([](CCWlRegistry* r, uint32_t id) { debugLog("Global {} removed", id); });
wl_display_roundtrip(state.display);
if (!state.wlCompositor || !state.wlShm || !state.wlSeat || !state.xdgShell || !state.inhibitManager) {
clientLog("Failed to get protocols from Hyprland");
return false;
}
return true;
}
static bool createShm(SWlState& state, Vector2D geom) {
if (!state.xrgb8888_support)
return false;
size_t stride = geom.x * 4;
size_t size = geom.y * stride;
if (!state.shmPool) {
const char* name = "/wl-shm-shortcut-inhibitor";
state.shmFd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600);
if (state.shmFd < 0)
return false;
if (shm_unlink(name) < 0 || ftruncate(state.shmFd, size) < 0) {
close(state.shmFd);
return false;
}
state.shmPool = makeShared<CCWlShmPool>(state.wlShm->sendCreatePool(state.shmFd, size));
if (!state.shmPool->resource()) {
close(state.shmFd);
state.shmFd = -1;
state.shmPool.reset();
return false;
}
state.shmBufSize = size;
} else if (size > state.shmBufSize) {
if (ftruncate(state.shmFd, size) < 0) {
close(state.shmFd);
state.shmFd = -1;
state.shmPool.reset();
return false;
}
state.shmPool->sendResize(size);
state.shmBufSize = size;
}
auto buf = makeShared<CCWlBuffer>(state.shmPool->sendCreateBuffer(0, geom.x, geom.y, stride, WL_SHM_FORMAT_XRGB8888));
if (!buf->resource())
return false;
if (state.shmBuf) {
state.shmBuf->sendDestroy();
state.shmBuf.reset();
}
state.shmBuf = buf;
return true;
}
static bool setupToplevel(SWlState& state) {
state.wlShm->setFormat([&](CCWlShm* p, uint32_t format) {
if (format == WL_SHM_FORMAT_XRGB8888)
state.xrgb8888_support = true;
});
state.xdgShell->setPing([&](CCXdgWmBase* p, uint32_t serial) { state.xdgShell->sendPong(serial); });
state.surf = makeShared<CCWlSurface>(state.wlCompositor->sendCreateSurface());
if (!state.surf->resource())
return false;
state.xdgSurf = makeShared<CCXdgSurface>(state.xdgShell->sendGetXdgSurface(state.surf->resource()));
if (!state.xdgSurf->resource())
return false;
state.xdgToplevel = makeShared<CCXdgToplevel>(state.xdgSurf->sendGetToplevel());
if (!state.xdgToplevel->resource())
return false;
state.xdgToplevel->setClose([&](CCXdgToplevel* p) { exit(0); });
state.xdgToplevel->setConfigure([&](CCXdgToplevel* p, int32_t w, int32_t h, wl_array* arr) {
state.geom = {1280, 720};
if (!createShm(state, state.geom))
exit(-1);
});
state.xdgSurf->setConfigure([&](CCXdgSurface* p, uint32_t serial) {
if (!state.shmBuf)
debugLog("xdgSurf configure but no buf made yet?");
state.xdgSurf->sendSetWindowGeometry(0, 0, state.geom.x, state.geom.y);
state.surf->sendAttach(state.shmBuf.get(), 0, 0);
state.surf->sendCommit();
state.xdgSurf->sendAckConfigure(serial);
if (!started) {
started = true;
clientLog("started");
}
});
state.xdgToplevel->sendSetTitle("shortcut-inhibitor test client");
state.xdgToplevel->sendSetAppId("shortcut-inhibitor");
state.surf->sendAttach(nullptr, 0, 0);
state.surf->sendCommit();
return true;
}
static bool setupSeat(SWlState& state) {
state.pointer = makeShared<CCWlPointer>(state.wlSeat->sendGetPointer());
if (!state.pointer->resource())
return false;
state.pointer->setEnter([&](CCWlPointer* p, uint32_t serial, wl_proxy* surf, wl_fixed_t x, wl_fixed_t y) {
debugLog("Got pointer enter event, serial {}, x {}, y {}", serial, x, y);
state.enterSerial = serial;
});
state.pointer->setLeave([&](CCWlPointer* p, uint32_t serial, wl_proxy* surf) { debugLog("Got pointer leave event, serial {}", serial); });
state.pointer->setMotion([&](CCWlPointer* p, uint32_t serial, wl_fixed_t x, wl_fixed_t y) { debugLog("Got pointer motion event, serial {}, x {}, y {}", serial, x, y); });
return true;
}
static void parseRequest(SWlState& state, std::string req) {
if (req.starts_with("on")) {
state.inhibitor = makeShared<CCZwpKeyboardShortcutsInhibitorV1>(state.inhibitManager->sendInhibitShortcuts(state.surf->resource(), state.wlSeat->resource()));
state.inhibitor->setActive([&](CCZwpKeyboardShortcutsInhibitorV1* inhibitorV1) { clientLog("inhibiting"); });
state.inhibitor->setInactive([&](CCZwpKeyboardShortcutsInhibitorV1* inhibitorV1) { clientLog("inhibit disabled by compositor"); });
} else if (req.starts_with("off")) {
state.inhibitor->sendDestroy();
state.inhibitor.reset();
shouldExit = true;
clientLog("inhibit disabled by request");
}
}
int main(int argc, char** argv) {
if (argc != 1 && argc != 2)
clientLog("Only the \"--debug\" switch is allowed, it turns on debug logs.");
if (argc == 2 && std::string{argv[1]} == "--debug")
debug = true;
SWlState state;
// WAYLAND_DISPLAY env should be set to the correct one
state.display = wl_display_connect(nullptr);
if (!state.display) {
clientLog("Failed to connect to wayland display");
return -1;
}
if (!bindRegistry(state) || !setupSeat(state) || !setupToplevel(state))
return -1;
std::array<char, 1024> readBuf;
readBuf.fill(0);
wl_display_flush(state.display);
struct pollfd fds[2] = {{.fd = wl_display_get_fd(state.display), .events = POLLIN | POLLOUT}, {.fd = STDIN_FILENO, .events = POLLIN}};
while (!shouldExit && poll(fds, 2, 0) != -1) {
if (fds[0].revents & POLLIN) {
wl_display_flush(state.display);
if (wl_display_prepare_read(state.display) == 0) {
wl_display_read_events(state.display);
wl_display_dispatch_pending(state.display);
} else
wl_display_dispatch(state.display);
int ret = 0;
do {
ret = wl_display_dispatch_pending(state.display);
wl_display_flush(state.display);
} while (ret > 0);
}
if (fds[1].revents & POLLIN) {
ssize_t bytesRead = read(fds[1].fd, readBuf.data(), 1023);
if (bytesRead == -1)
continue;
readBuf[bytesRead] = 0;
parseRequest(state, std::string{readBuf.data()});
}
}
wl_display* display = state.display;
state = {};
wl_display_disconnect(display);
return 0;
}

View file

@ -6,15 +6,16 @@
#define private public
#include <src/config/ConfigManager.hpp>
#include <src/config/ConfigDescriptions.hpp>
#include <src/layout/IHyprLayout.hpp>
#include <src/managers/LayoutManager.hpp>
#include <src/managers/input/InputManager.hpp>
#include <src/managers/PointerManager.hpp>
#include <src/managers/input/trackpad/TrackpadGestures.hpp>
#include <src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp>
#include <src/desktop/rule/layerRule/LayerRuleEffectContainer.hpp>
#include <src/desktop/rule/windowRule/WindowRuleApplicator.hpp>
#include <src/desktop/view/LayerSurface.hpp>
#include <src/Compositor.hpp>
#include <src/desktop/state/FocusState.hpp>
#include <src/layout/LayoutManager.hpp>
#undef private
#include <hyprutils/utils/ScopeGuard.hpp>
@ -53,8 +54,9 @@ static SDispatchResult snapMove(std::string in) {
Vector2D pos = PLASTWINDOW->m_realPosition->goal();
Vector2D size = PLASTWINDOW->m_realSize->goal();
g_pLayoutManager->getCurrentLayout()->performSnap(pos, size, PLASTWINDOW, MBIND_MOVE, -1, size);
*PLASTWINDOW->m_realPosition = pos.round();
g_layoutManager->performSnap(pos, size, PLASTWINDOW->layoutTarget(), MBIND_MOVE, -1, size);
PLASTWINDOW->layoutTarget()->setPositionGlobal(CBox{pos, size});
return {};
}
@ -270,32 +272,85 @@ static SDispatchResult keybind(std::string in) {
return {};
}
static Desktop::Rule::CWindowRuleEffectContainer::storageType ruleIDX = 0;
static Desktop::Rule::CWindowRuleEffectContainer::storageType windowRuleIDX = 0;
//
static SDispatchResult addRule(std::string in) {
ruleIDX = Desktop::Rule::windowEffects()->registerEffect("plugin_rule");
static SDispatchResult addWindowRule(std::string in) {
windowRuleIDX = Desktop::Rule::windowEffects()->registerEffect("plugin_rule");
if (Desktop::Rule::windowEffects()->registerEffect("plugin_rule") != ruleIDX)
if (Desktop::Rule::windowEffects()->registerEffect("plugin_rule") != windowRuleIDX)
return {.success = false, .error = "re-registering returned a different id?"};
return {};
}
static SDispatchResult checkRule(std::string in) {
static SDispatchResult checkWindowRule(std::string in) {
const auto PLASTWINDOW = Desktop::focusState()->window();
if (!PLASTWINDOW)
return {.success = false, .error = "No window"};
if (!PLASTWINDOW->m_ruleApplicator->m_otherProps.props.contains(ruleIDX))
if (!PLASTWINDOW->m_ruleApplicator->m_otherProps.props.contains(windowRuleIDX))
return {.success = false, .error = "No rule"};
if (PLASTWINDOW->m_ruleApplicator->m_otherProps.props[ruleIDX]->effect != "effect")
if (PLASTWINDOW->m_ruleApplicator->m_otherProps.props[windowRuleIDX]->effect != "effect")
return {.success = false, .error = "Effect isn't \"effect\""};
return {};
}
static Desktop::Rule::CLayerRuleEffectContainer::storageType layerRuleIDX = 0;
static SDispatchResult addLayerRule(std::string in) {
layerRuleIDX = Desktop::Rule::layerEffects()->registerEffect("plugin_rule");
if (Desktop::Rule::layerEffects()->registerEffect("plugin_rule") != layerRuleIDX)
return {.success = false, .error = "re-registering returned a different id?"};
return {};
}
static SDispatchResult checkLayerRule(std::string in) {
if (g_pCompositor->m_layers.size() != 3)
return {.success = false, .error = "Layers under test not here"};
for (const auto& layer : g_pCompositor->m_layers) {
if (layer->m_namespace == "rule-layer") {
if (!layer->m_ruleApplicator->m_otherProps.props.contains(layerRuleIDX))
return {.success = false, .error = "No rule"};
if (layer->m_ruleApplicator->m_otherProps.props[layerRuleIDX]->effect != "effect")
return {.success = false, .error = "Effect isn't \"effect\""};
} else if (layer->m_namespace == "norule-layer") {
if (layer->m_ruleApplicator->m_otherProps.props.contains(layerRuleIDX))
return {.success = false, .error = "Rule even though it shouldn't"};
} else
return {.success = false, .error = "Unrecognized layer"};
}
return {};
}
static SDispatchResult floatingFocusOnFullscreen(std::string in) {
const auto PLASTWINDOW = Desktop::focusState()->window();
if (!PLASTWINDOW)
return {.success = false, .error = "No window"};
if (!PLASTWINDOW->m_isFloating)
return {.success = false, .error = "Window must be floating"};
if (PLASTWINDOW->m_alpha != 1.f)
return {.success = false, .error = "floating window doesnt restore it opacity when focused on fullscreen workspace"};
if (!PLASTWINDOW->m_createdOverFullscreen)
return {.success = false, .error = "floating window doesnt get flagged as createdOverFullscreen"};
return {};
}
APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
PHANDLE = handle;
@ -307,8 +362,11 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:scroll", ::scroll);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:click", ::click);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:keybind", ::keybind);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_rule", ::addRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_rule", ::checkRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_window_rule", ::addWindowRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_window_rule", ::checkWindowRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_layer_rule", ::addLayerRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_layer_rule", ::checkLayerRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:floating_focus_on_fullscreen", ::floatingFocusOnFullscreen);
// init mouse
g_mouse = CTestMouse::create(false);

View file

@ -39,6 +39,16 @@ namespace Colors {
TESTS_PASSED++; \
}
#define EXPECT_NOT(expr, val) \
if (const auto RESULT = expr; RESULT == (val)) { \
NLog::log("{}Failed: {}{}, expected not {}, got {}. Source: {}@{}.", Colors::RED, Colors::RESET, #expr, val, RESULT, __FILE__, __LINE__); \
ret = 1; \
TESTS_FAILED++; \
} else { \
NLog::log("{}Passed: {}{}. Got {}", Colors::GREEN, Colors::RESET, #expr, val); \
TESTS_PASSED++; \
}
#define EXPECT_VECTOR2D(expr, val) \
do { \
const auto& RESULT = expr; \

View file

@ -8,6 +8,7 @@
#include <hyprutils/os/Process.hpp>
#include <sys/poll.h>
#include <unistd.h>
#include <csignal>
#include <thread>
@ -117,7 +118,34 @@ static bool test() {
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
// test that child windows (shouldBeFloated) are not auto-grouped
NLog::log("{}Test child windows are not auto-grouped", Colors::GREEN);
auto kitty = Tests::spawnKitty();
if (!kitty) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
// create group and enable auto-grouping
OK(getFromSocket("/dispatch togglegroup"));
OK(getFromSocket("/keyword group:auto_group true"));
SClient client2;
if (!startClient(client2))
return false;
EXPECT(Tests::windowCount(), 2);
createChild(client2);
EXPECT(Tests::windowCount(), 3);
// child has set_parent so shouldBeFloated returns true, it should not be auto-grouped
EXPECT_COUNT_STRING(getFromSocket("/clients"), "grouped: 0", 1);
stopClient(client2);
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
return !ret;
}
REGISTER_CLIENT_TEST_FN(test);
REGISTER_CLIENT_TEST_FN(test);

View file

@ -8,6 +8,7 @@
#include <hyprutils/os/Process.hpp>
#include <sys/poll.h>
#include <unistd.h>
#include <csignal>
#include <thread>
@ -42,6 +43,7 @@ static bool startClient(SClient& client) {
client.readFd = CFileDescriptor(pipeFds2[0]);
client.proc->setStdoutFD(pipeFds2[1]);
const int COUNT_BEFORE = Tests::windowCount();
client.proc->runAsync();
close(pipeFds1[0]);
@ -62,7 +64,16 @@ static bool startClient(SClient& client) {
}
// wait for window to appear
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
int counter = 0;
while (Tests::processAlive(client.proc->pid()) && Tests::windowCount() == COUNT_BEFORE) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (counter > 50) {
NLog::log("{}pointer-scroll client took too long to open", Colors::RED);
return false;
}
}
if (getFromSocket(std::format("/dispatch setprop pid:{} no_anim 1", client.proc->pid())) != "ok") {
NLog::log("{}Failed to disable animations for client window", Colors::RED, ret);

View file

@ -8,6 +8,7 @@
#include <hyprutils/os/Process.hpp>
#include <sys/poll.h>
#include <unistd.h>
#include <csignal>
#include <thread>
@ -42,6 +43,7 @@ static bool startClient(SClient& client) {
client.readFd = CFileDescriptor(pipeFds2[0]);
client.proc->setStdoutFD(pipeFds2[1]);
const int COUNT_BEFORE = Tests::windowCount();
client.proc->runAsync();
close(pipeFds1[0]);
@ -62,7 +64,16 @@ static bool startClient(SClient& client) {
}
// wait for window to appear
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
int counter = 0;
while (Tests::processAlive(client.proc->pid()) && Tests::windowCount() == COUNT_BEFORE) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (counter > 50) {
NLog::log("{}pointer-warp client took too long to open", Colors::RED);
return false;
}
}
if (getFromSocket(std::format("/dispatch setprop pid:{} no_anim 1", client.proc->pid())) != "ok") {
NLog::log("{}Failed to disable animations for client window", Colors::RED, ret);

View file

@ -0,0 +1,180 @@
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "../shared.hpp"
#include "tests.hpp"
#include "build.hpp"
#include <hyprutils/os/FileDescriptor.hpp>
#include <hyprutils/os/Process.hpp>
#include <sys/poll.h>
#include <csignal>
#include <thread>
#include <filesystem>
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
#define SP CSharedPointer
struct SClient {
SP<CProcess> proc;
std::array<char, 1024> readBuf;
CFileDescriptor readFd, writeFd;
struct pollfd fds;
};
static int ret = 0;
static bool startClient(SClient& client) {
Tests::killAllWindows();
client.proc = makeShared<CProcess>(binaryDir + "/shortcut-inhibitor", std::vector<std::string>{});
client.proc->addEnv("WAYLAND_DISPLAY", WLDISPLAY);
int pipeFds1[2], pipeFds2[2];
if (pipe(pipeFds1) != 0 || pipe(pipeFds2) != 0) {
NLog::log("{}Unable to open pipe to client", Colors::RED);
return false;
}
client.writeFd = CFileDescriptor(pipeFds1[1]);
client.proc->setStdinFD(pipeFds1[0]);
client.readFd = CFileDescriptor(pipeFds2[0]);
client.proc->setStdoutFD(pipeFds2[1]);
const int COUNT_BEFORE = Tests::windowCount();
client.proc->runAsync();
close(pipeFds1[0]);
close(pipeFds2[1]);
client.fds = {.fd = client.readFd.get(), .events = POLLIN};
if (poll(&client.fds, 1, 1000) != 1 || !(client.fds.revents & POLLIN)) {
NLog::log("{}shortcut-inhibitor client failed poll", Colors::RED);
return false;
}
client.readBuf.fill(0);
if (read(client.readFd.get(), client.readBuf.data(), client.readBuf.size() - 1) == -1) {
NLog::log("{}shortcut-inhibitor client read failed", Colors::RED);
return false;
}
std::string ret = std::string{client.readBuf.data()};
if (ret.find("started") == std::string::npos) {
NLog::log("{}Failed to start shortcut-inhibitor client, read {}", Colors::RED, ret);
return false;
}
// wait for window to appear
int counter = 0;
while (Tests::processAlive(client.proc->pid()) && Tests::windowCount() == COUNT_BEFORE) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (counter > 50) {
NLog::log("{}shortcut-inhibitor client took too long to open", Colors::RED);
return false;
}
}
if (!Tests::processAlive(client.proc->pid())) {
NLog::log("{}shortcut-inhibitor client not alive", Colors::RED);
return false;
}
if (getFromSocket(std::format("/dispatch focuswindow pid:{}", client.proc->pid())) != "ok") {
NLog::log("{}Failed to focus shortcut-inhibitor client", Colors::RED, ret);
return false;
}
std::string command = "on\n";
if (write(client.writeFd.get(), command.c_str(), command.length()) == -1) {
NLog::log("{}shortcut-inhibitor client write failed", Colors::RED);
return false;
}
client.readBuf.fill(0);
if (read(client.readFd.get(), client.readBuf.data(), client.readBuf.size() - 1) == -1)
return false;
ret = std::string{client.readBuf.data()};
if (ret.find("inhibiting") == std::string::npos) {
NLog::log("{}shortcut-inhibitor client didn't return inhibiting", Colors::RED);
return false;
}
NLog::log("{}Started shortcut-inhibitor client", Colors::YELLOW);
return true;
}
static void stopClient(SClient& client) {
std::string cmd = "off\n";
write(client.writeFd.get(), cmd.c_str(), cmd.length());
kill(client.proc->pid(), SIGKILL);
client.proc.reset();
}
static std::string flagFile = "/tmp/hyprtester-keybinds.txt";
static bool checkFlag() {
bool exists = std::filesystem::exists(flagFile);
std::filesystem::remove(flagFile);
return exists;
}
static bool attemptCheckFlag(int attempts, int intervalMs) {
for (int i = 0; i < attempts; i++) {
if (checkFlag())
return true;
std::this_thread::sleep_for(std::chrono::milliseconds(intervalMs));
}
return false;
}
static bool test() {
SClient client;
if (!startClient(client))
return false;
NLog::log("{}Testing keybinds", Colors::GREEN);
//basic keybind test
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword bind SUPER,Y,exec,touch " + flagFile), "ok");
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
EXPECT(attemptCheckFlag(20, 50), false);
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
//keybind bypass flag test
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword bindp SUPER,Y,exec,touch " + flagFile), "ok");
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
EXPECT(attemptCheckFlag(20, 50), true);
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
NLog::log("{}Testing gestures", Colors::GREEN);
//basic gesture test
OK(getFromSocket("/dispatch plugin:test:gesture right,3"));
EXPECT_NOT_CONTAINS(getFromSocket("/activewindow"), "floating: 1");
//gesture bypass flag test
OK(getFromSocket("/dispatch plugin:test:gesture right,2"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "floating: 1");
stopClient(client);
NLog::log("{}Reloading the config", Colors::YELLOW);
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_CLIENT_TEST_FN(test);

View file

@ -34,6 +34,197 @@ static void testFloatClamp() {
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
OK(getFromSocket("/reload"));
}
static void test13349() {
// Test if dwindle properly uses a focal point to place a new window.
// exposed by #13349 as a regression from #12890
for (auto const& win : {"a", "b", "c"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
OK(getFromSocket("/dispatch focuswindow class:c"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 967,547");
EXPECT_CONTAINS(str, "size: 931,511");
}
OK(getFromSocket("/dispatch movewindow l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 497,22");
EXPECT_CONTAINS(str, "size: 456,1036");
}
OK(getFromSocket("/dispatch movewindow r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 967,22");
EXPECT_CONTAINS(str, "size: 456,1036");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static void testSplit() {
// Test various split methods
Tests::spawnKitty("a");
// these must not crash
EXPECT_NOT(getFromSocket("/dispatch layoutmsg swapsplit"), "ok");
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio 1 exact"), "ok");
Tests::spawnKitty("b");
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg splitratio -0.2"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 743,1036");
}
OK(getFromSocket("/dispatch layoutmsg splitratio 1.6 exact"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1495,1036");
}
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio fhne exact"), "ok");
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio exact"), "ok");
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio -....9"), "ok");
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio ..9"), "ok");
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio"), "ok");
OK(getFromSocket("/dispatch layoutmsg togglesplit"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,823");
}
OK(getFromSocket("/dispatch layoutmsg swapsplit"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,859");
EXPECT_CONTAINS(str, "size: 1876,199");
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static void testRotatesplit() {
OK(getFromSocket("r/keyword general:gaps_in 0"));
OK(getFromSocket("r/keyword general:gaps_out 0"));
OK(getFromSocket("r/keyword general:border_size 0"));
for (auto const& win : {"a", "b"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
// test 4 repeated rotations by 90 degrees
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 1920,540");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 960,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,540");
EXPECT_CONTAINS(str, "size: 1920,540");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
// test different angles
OK(getFromSocket("/dispatch layoutmsg rotatesplit 180"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 960,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit 270"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,540");
EXPECT_CONTAINS(str, "size: 1920,540");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit 360"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 1920,540");
}
// test negative angles
OK(getFromSocket("/dispatch layoutmsg rotatesplit -90"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit -180"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 960,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
OK(getFromSocket("/reload"));
}
static bool test() {
@ -43,6 +234,15 @@ static bool test() {
NLog::log("{}Testing float clamp", Colors::GREEN);
testFloatClamp();
NLog::log("{}Testing #13349", Colors::GREEN);
test13349();
NLog::log("{}Testing splits", Colors::GREEN);
testSplit();
NLog::log("{}Testing rotatesplit", Colors::GREEN);
testRotatesplit();
// clean up
NLog::log("Cleaning up", Colors::YELLOW);
getFromSocket("/dispatch workspace 1");

View file

@ -2,6 +2,7 @@
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include <chrono>
#include <format>
#include <thread>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
@ -15,40 +16,46 @@ using namespace Hyprutils::Memory;
#define UP CUniquePointer
#define SP CSharedPointer
static bool test() {
const static auto SLEEP_DURATIONS = std::array{1, 10};
static bool test() {
NLog::log("{}Testing process spawning", Colors::GREEN);
// Note: POSIX sleep does not support fractional seconds, so
// can't sleep for less than 1 second.
OK(getFromSocket("/dispatch exec sleep 1"));
for (const auto duration : SLEEP_DURATIONS) {
// Note: POSIX sleep does not support fractional seconds, so
// can't sleep for less than 1 second.
OK(getFromSocket(std::format("/dispatch exec sleep {}", duration)));
// Ensure that sleep is our child
const std::string sleepPidS = Tests::execAndGet("pgrep sleep");
pid_t sleepPid;
try {
sleepPid = std::stoull(sleepPidS);
} catch (...) {
NLog::log("{}Sleep was not spawned or several sleeps are running: pgrep returned '{}'", Colors::RED, sleepPidS);
return false;
// Ensure that sleep is our child
const std::string sleepPidS = Tests::execAndGet("pgrep sleep");
pid_t sleepPid;
try {
sleepPid = std::stoull(sleepPidS);
} catch (...) {
NLog::log("{}Sleep was not spawned or several sleeps are running: pgrep returned '{}'", Colors::RED, sleepPidS);
continue;
}
const std::string sleepParentComm = Tests::execAndGet("cat \"/proc/$(ps -o ppid:1= -p " + sleepPidS + ")/comm\"");
NLog::log("{}Expecting that sleep's parent is Hyprland", Colors::YELLOW);
EXPECT_CONTAINS(sleepParentComm, "Hyprland");
std::this_thread::sleep_for(std::chrono::seconds(duration));
// Ensure that sleep did not become a zombie
EXPECT(Tests::processAlive(sleepPid), false);
// kill all
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
return !ret;
}
const std::string sleepParentComm = Tests::execAndGet("cat \"/proc/$(ps -o ppid:1= -p " + sleepPidS + ")/comm\"");
NLog::log("{}Expecting that sleep's parent is Hyprland", Colors::YELLOW);
EXPECT_CONTAINS(sleepParentComm, "Hyprland");
std::this_thread::sleep_for(std::chrono::seconds(1));
// Ensure that sleep did not become a zombie
EXPECT(Tests::processAlive(sleepPid), false);
// kill all
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
return !ret;
return false;
}
REGISTER_TEST_FN(test)

View file

@ -127,6 +127,34 @@ static bool test() {
ret = 1;
}
// test movegroupwindow: focus should follow the moved window
NLog::log("{}Test movegroupwindow focus follows window", Colors::YELLOW);
try {
auto str = getFromSocket("/activewindow");
auto activeBeforeMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16);
OK(getFromSocket("/dispatch movegroupwindow f"));
str = getFromSocket("/activewindow");
auto activeAfterMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16);
EXPECT(activeAfterMove, activeBeforeMove);
} catch (...) {
NLog::log("{}Fail at getting prop", Colors::RED);
ret = 1;
}
// and backwards
NLog::log("{}Test movegroupwindow backwards", Colors::YELLOW);
try {
auto str = getFromSocket("/activewindow");
auto activeBeforeMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16);
OK(getFromSocket("/dispatch movegroupwindow b"));
str = getFromSocket("/activewindow");
auto activeAfterMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16);
EXPECT(activeAfterMove, activeBeforeMove);
} catch (...) {
NLog::log("{}Fail at getting prop", Colors::RED);
ret = 1;
}
NLog::log("{}Disable autogrouping", Colors::YELLOW);
OK(getFromSocket("/keyword group:auto_group false"));
@ -173,6 +201,99 @@ static bool test() {
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
// test movewindoworgroup: direction should be respected when extracting from group
NLog::log("{}Test movewindoworgroup respects direction out of group", Colors::YELLOW);
OK(getFromSocket("/keyword group:groupbar:enabled 0"));
{
auto kittyE = Tests::spawnKitty();
if (!kittyE) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
// group kitty, and new windows should be auto-grouped
OK(getFromSocket("/dispatch togglegroup"));
auto kittyF = Tests::spawnKitty();
if (!kittyF) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
EXPECT(Tests::windowCount(), 2);
// both windows should be grouped at the same position
{
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 2);
}
// move active window out of group to the right
NLog::log("{}Test movewindoworgroup r", Colors::YELLOW);
OK(getFromSocket("/dispatch movewindoworgroup r"));
// the group should stay at x=22, the extracted window should be to the right
{
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 1);
}
// move it back into the group
OK(getFromSocket("/dispatch moveintogroup l"));
// move active window out of group downward
NLog::log("{}Test movewindoworgroup d", Colors::YELLOW);
OK(getFromSocket("/dispatch movewindoworgroup d"));
// the group should stay at y=22, the extracted window should be below
{
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 1);
}
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
}
// test that we deny a floated window getting auto-grouped into a tiled group.
NLog::log("{}Test that we deny a floated window getting auto-grouped into a tiled group.", Colors::GREEN);
OK(getFromSocket("/keyword windowrule[kitty-tiled]:match:class kitty_tiled"));
OK(getFromSocket("/keyword windowrule[kitty-tiled]:tile yes"));
auto kittyProcE = Tests::spawnKitty("kitty_tiled");
if (!kittyProcE) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
OK(getFromSocket("/dispatch togglegroup"));
OK(getFromSocket("/keyword windowrule[kitty-floated]:match:class kitty_floated"));
OK(getFromSocket("/keyword windowrule[kitty-floated]:float yes"));
auto kittyProcF = Tests::spawnKitty("kitty_floated");
if (!kittyProcF) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
EXPECT(Tests::windowCount(), 2);
{
auto clients = getFromSocket("/clients");
auto classPos = clients.find("class: kitty_floated");
if (classPos == std::string::npos) {
NLog::log("{}Could not find kitty_floated in clients output", Colors::RED);
ret = 1;
} else {
auto entryStart = clients.rfind("Window ", classPos);
auto entryEnd = clients.find("\n\n", classPos);
auto windowEntry = clients.substr(entryStart, entryEnd - entryStart);
EXPECT_CONTAINS(windowEntry, "floating: 1");
EXPECT_CONTAINS(windowEntry, "grouped: 0");
}
}
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
return !ret;
}

View file

@ -77,6 +77,16 @@ static bool testGetprop() {
EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size"), "100 50");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size -j"), R"({"min_size": [100,50]})");
// expr-based min/max _size
getFromSocket("/dispatch setfloating class:kitty"); // need to set floating for tests below
getFromSocket("/dispatch setprop class:kitty max_size 90+10 25*2"); // set max to the same as min above, forcing window to 100*50
EXPECT(getCommandStdOut("hyprctl getprop class:kitty max_size"), "100 50");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty max_size -j"), R"({"max_size": [100,50]})");
getFromSocket("/dispatch setprop class:kitty min_size window_w*0.5 window_h-10");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size"), "50 40");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size -j"), R"({"min_size": [50,40]})");
getFromSocket("/dispatch settiled class:kitty"); // go back to tiled for consistency
// opacity
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity"), "1");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity -j"), R"({"opacity": 1})");

View file

@ -0,0 +1,53 @@
#include "../../Log.hpp"
#include "../shared.hpp"
#include "tests.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
static int ret = 0;
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
static bool spawnLayer(const std::string& namespace_) {
NLog::log("{}Spawning kitty layer {}", Colors::YELLOW, namespace_);
if (!Tests::spawnLayerKitty(namespace_)) {
NLog::log("{}Error: {} layer did not spawn", Colors::RED, namespace_);
return false;
}
return true;
}
static bool test() {
NLog::log("{}Testing plugin layerrules", Colors::GREEN);
if (!spawnLayer("rule-layer"))
return false;
OK(getFromSocket("/dispatch plugin:test:add_layer_rule"));
OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword layerrule match:namespace rule-layer, plugin_rule effect"));
if (!spawnLayer("rule-layer"))
return false;
if (!spawnLayer("norule-layer"))
return false;
OK(getFromSocket("/dispatch plugin:test:check_layer_rule"));
OK(getFromSocket("/reload"));
NLog::log("{}Killing all layers", Colors::YELLOW);
Tests::killAllLayers();
NLog::log("{}Expecting 0 layers", Colors::YELLOW);
EXPECT(Tests::layerCount(), 0);
return !ret;
}
REGISTER_TEST_FN(test)

View file

@ -0,0 +1,127 @@
#include "../shared.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "tests.hpp"
static int ret = 0;
static void swar() {
OK(getFromSocket("/keyword layout:single_window_aspect_ratio 1 1"));
Tests::spawnKitty();
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 442,22");
EXPECT_CONTAINS(str, "size: 1036,1036");
}
Tests::spawnKitty();
OK(getFromSocket("/dispatch killwindow activewindow"));
Tests::waitUntilWindowsN(1);
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 442,22");
EXPECT_CONTAINS(str, "size: 1036,1036");
}
// don't use swar on maximized
OK(getFromSocket("/dispatch fullscreen 1"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,1036");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
// Don't crash when focus after global geometry changes
static void testCrashOnGeomUpdate() {
Tests::spawnKitty();
Tests::spawnKitty();
Tests::spawnKitty();
// move the layout
OK(getFromSocket("/keyword monitor HEADLESS-2,1920x1080@60,1000x0,1"));
// shouldnt crash
OK(getFromSocket("/dispatch movefocus r"));
OK(getFromSocket("/reload"));
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
// Test if size + pos is preserved after fs cycle
static void testPosPreserve() {
Tests::spawnKitty();
OK(getFromSocket("/dispatch setfloating class:kitty"));
OK(getFromSocket("/dispatch resizewindowpixel exact 1337 69, class:kitty"));
OK(getFromSocket("/dispatch movewindowpixel exact 420 420, class:kitty"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 420,420");
EXPECT_CONTAINS(str, "size: 1337,69");
}
OK(getFromSocket("/dispatch fullscreen"));
OK(getFromSocket("/dispatch fullscreen"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "size: 1337,69");
}
OK(getFromSocket("/dispatch movewindow r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 581,420");
EXPECT_CONTAINS(str, "size: 1337,69");
}
OK(getFromSocket("/dispatch fullscreen"));
OK(getFromSocket("/dispatch fullscreen"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 581,420");
EXPECT_CONTAINS(str, "size: 1337,69");
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static bool test() {
NLog::log("{}Testing layout generic", Colors::GREEN);
// setup
OK(getFromSocket("/dispatch workspace 10"));
// test
NLog::log("{}Testing `single_window_aspect_ratio`", Colors::GREEN);
swar();
testCrashOnGeomUpdate();
testPosPreserve();
// clean up
NLog::log("Cleaning up", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace 1"));
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_TEST_FN(test);

View file

@ -3,7 +3,53 @@
#include "../../hyprctlCompat.hpp"
#include "tests.hpp"
static int ret = 0;
static int ret = 0;
// reqs 1 master 3 slaves
static void testOrientations() {
OK(getFromSocket("/keyword master:orientation top"));
// top
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876");
}
// cycle = top, right, bottom, center, left
// right
OK(getFromSocket("/dispatch layoutmsg orientationnext"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 873,22");
EXPECT_CONTAINS(str, "size: 1025,1036");
}
// bottom
OK(getFromSocket("/dispatch layoutmsg orientationnext"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,495");
EXPECT_CONTAINS(str, "size: 1876");
}
// center
OK(getFromSocket("/dispatch layoutmsg orientationnext"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 450,22");
EXPECT_CONTAINS(str, "size: 1020,1036");
}
// left
OK(getFromSocket("/dispatch layoutmsg orientationnext"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1025,1036");
}
}
static void focusMasterPrevious() {
// setup
@ -44,11 +90,74 @@ static void focusMasterPrevious() {
OK(getFromSocket("/dispatch layoutmsg focusmaster previous"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: master");
testOrientations();
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static void testFsBehavior() {
// Master will re-send data to fullscreen / maximized windows, which can interfere with misc:on_focus_under_fullscreen
// check that it doesn't.
for (auto const& win : {"master", "slave1", "slave2"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
OK(getFromSocket("/dispatch focuswindow class:master"));
OK(getFromSocket("/dispatch fullscreen 1"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,1036");
EXPECT_CONTAINS(str, "class: master");
}
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 1"));
Tests::spawnKitty("new_master");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,1036");
EXPECT_CONTAINS(str, "class: new_master");
EXPECT_CONTAINS(str, "fullscreen: 1");
}
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 0"));
Tests::spawnKitty("ignored");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,1036");
EXPECT_CONTAINS(str, "class: new_master");
EXPECT_CONTAINS(str, "fullscreen: 1");
}
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 2"));
Tests::spawnKitty("vaxwashere");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: vaxwashere");
EXPECT_CONTAINS(str, "fullscreen: 0");
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static bool test() {
NLog::log("{}Testing Master layout", Colors::GREEN);
@ -60,6 +169,9 @@ static bool test() {
NLog::log("{}Testing `focusmaster previous` layoutmsg", Colors::GREEN);
focusMasterPrevious();
NLog::log("{}Testing fs behavior", Colors::GREEN);
testFsBehavior();
// clean up
NLog::log("Cleaning up", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace 1"));

View file

@ -18,6 +18,121 @@ using namespace Hyprutils::Memory;
#define UP CUniquePointer
#define SP CSharedPointer
// Uncomment once test vm can run hyprland-dialog
// static void testAnrDialogs() {
// NLog::log("{}Testing ANR dialogs", Colors::YELLOW);
//
// OK(getFromSocket("/keyword misc:enable_anr_dialog true"));
// OK(getFromSocket("/keyword misc:anr_missed_pings 1"));
//
// NLog::log("{}ANR dialog: regular workspaces", Colors::YELLOW);
// {
// OK(getFromSocket("/dispatch workspace 2"));
//
// auto kitty = Tests::spawnKitty("bad_kitty");
//
// if (!kitty) {
// ret = 1;
// return;
// }
//
// {
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "workspace: 2");
// }
//
// OK(getFromSocket("/dispatch workspace 1"));
//
// ::kill(kitty->pid(), SIGSTOP);
// Tests::waitUntilWindowsN(2);
//
// {
// auto str = getFromSocket("/activeworkspace");
// EXPECT_CONTAINS(str, "windows: 0");
// }
//
// {
// OK(getFromSocket("/dispatch focuswindow class:hyprland-dialog"))
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "workspace: 2");
// }
// }
//
// Tests::killAllWindows();
//
// NLog::log("{}ANR dialog: named workspaces", Colors::YELLOW);
// {
// OK(getFromSocket("/dispatch workspace name:yummy"));
//
// auto kitty = Tests::spawnKitty("bad_kitty");
//
// if (!kitty) {
// ret = 1;
// return;
// }
//
// {
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "yummy");
// }
//
// OK(getFromSocket("/dispatch workspace 1"));
//
// ::kill(kitty->pid(), SIGSTOP);
// Tests::waitUntilWindowsN(2);
//
// {
// auto str = getFromSocket("/activeworkspace");
// EXPECT_CONTAINS(str, "windows: 0");
// }
//
// {
// OK(getFromSocket("/dispatch focuswindow class:hyprland-dialog"))
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "yummy");
// }
// }
//
// Tests::killAllWindows();
//
// NLog::log("{}ANR dialog: special workspaces", Colors::YELLOW);
// {
// OK(getFromSocket("/dispatch workspace special:apple"));
//
// auto kitty = Tests::spawnKitty("bad_kitty");
//
// if (!kitty) {
// ret = 1;
// return;
// }
//
// {
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "special:apple");
// }
//
// OK(getFromSocket("/dispatch togglespecialworkspace apple"));
// OK(getFromSocket("/dispatch workspace 1"));
//
// ::kill(kitty->pid(), SIGSTOP);
// Tests::waitUntilWindowsN(2);
//
// {
// auto str = getFromSocket("/activeworkspace");
// EXPECT_CONTAINS(str, "windows: 0");
// }
//
// {
// OK(getFromSocket("/dispatch focuswindow class:hyprland-dialog"))
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "special:apple");
// }
// }
//
// OK(getFromSocket("/reload"));
// Tests::killAllWindows();
// }
static bool test() {
NLog::log("{}Testing config: misc:", Colors::GREEN);

View file

@ -0,0 +1,228 @@
#include "../shared.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "tests.hpp"
static int ret = 0;
static void testFocusCycling() {
for (auto const& win : {"a", "b", "c", "d"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
OK(getFromSocket("/dispatch movewindow l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
OK(getFromSocket("/dispatch movefocus u"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static void testFocusWrapping() {
for (auto const& win : {"a", "b", "c", "d"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
// set wrap_focus to true
OK(getFromSocket("/keyword scrolling:wrap_focus true"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: a");
}
// set wrap_focus to false
OK(getFromSocket("/keyword scrolling:wrap_focus false"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: a");
}
OK(getFromSocket("/dispatch focuswindow class:d"));
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static void testSwapcolWrapping() {
for (auto const& win : {"a", "b", "c", "d"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
// set wrap_swapcol to true
OK(getFromSocket("/keyword scrolling:wrap_swapcol true"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg swapcol l"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
for (auto const& win : {"a", "b", "c", "d"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
OK(getFromSocket("/dispatch focuswindow class:d"));
OK(getFromSocket("/dispatch layoutmsg swapcol r"));
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
for (auto const& win : {"a", "b", "c", "d"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
// set wrap_swapcol to false
OK(getFromSocket("/keyword scrolling:wrap_swapcol false"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg swapcol l"));
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
OK(getFromSocket("/dispatch focuswindow class:d"));
OK(getFromSocket("/dispatch layoutmsg swapcol r"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static bool test() {
NLog::log("{}Testing Scroll layout", Colors::GREEN);
// setup
OK(getFromSocket("/dispatch workspace name:scroll"));
OK(getFromSocket("/keyword general:layout scrolling"));
// test
NLog::log("{}Testing focus cycling", Colors::GREEN);
testFocusCycling();
// test
NLog::log("{}Testing focus wrap", Colors::GREEN);
testFocusWrapping();
// test
NLog::log("{}Testing swapcol wrap", Colors::GREEN);
testSwapcolWrapping();
// clean up
NLog::log("Cleaning up", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace 1"));
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_TEST_FN(test);

View file

@ -1,3 +1,4 @@
#include <unistd.h>
#include <cmath>
#include <chrono>
#include <cstdlib>
@ -24,6 +25,28 @@ static bool spawnKitty(const std::string& class_, const std::vector<std::string>
return true;
}
/// Spawns a kitty and creates a file and returns its name. The removal of the file triggers
/// activation of the spawned kitty window.
///
/// On failure, returns an empty string, possibly leaving a temporary file.
static std::string spawnKittyActivating(const std::string& class_ = "kitty_activating") {
// `XXXXXX` is what `mkstemp` expects to find in the string
std::string tmpFilename = (std::filesystem::temp_directory_path() / "XXXXXX").string();
int fd = mkstemp(tmpFilename.data());
if (fd < 0) {
NLog::log("{}Error: could not create tmp file: errno {}", Colors::RED, errno);
return "";
}
(void)close(fd);
bool ok =
spawnKitty(class_, {"-o", "allow_remote_control=yes", "--", "/bin/sh", "-c", "while [ -f \"" + tmpFilename + "\" ]; do :; done; kitten @ focus-window; sleep infinity"});
if (!ok) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return "";
}
return tmpFilename;
}
static std::string getWindowAttribute(const std::string& winInfo, const std::string& attr) {
auto pos = winInfo.find(attr);
if (pos == std::string::npos) {
@ -70,9 +93,9 @@ static void testSwapWindow() {
{
getFromSocket("/dispatch focuswindow class:kitty_A");
auto pos = getWindowAttribute(getFromSocket("/activewindow"), "at:");
NLog::log("{}Testing kitty_A {}, swapwindow with direction 'l'", Colors::YELLOW, pos);
NLog::log("{}Testing kitty_A {}, swapwindow with direction 'r'", Colors::YELLOW, pos);
OK(getFromSocket("/dispatch swapwindow l"));
OK(getFromSocket("/dispatch swapwindow r"));
OK(getFromSocket("/dispatch focuswindow class:kitty_B"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", pos));
@ -197,7 +220,7 @@ static void testGroupRules() {
Tests::killAllWindows();
}
static bool isActiveWindow(const std::string& class_, char fullscreen, bool log = true) {
static bool isActiveWindow(const std::string& class_, char fullscreen = '0', bool log = true) {
std::string activeWin = getFromSocket("/activewindow");
auto winClass = getWindowAttribute(activeWin, "class:");
auto winFullscreen = getWindowAttribute(activeWin, "fullscreen:").back();
@ -210,13 +233,13 @@ static bool isActiveWindow(const std::string& class_, char fullscreen, bool log
}
}
static bool waitForActiveWindow(const std::string& class_, char fullscreen, int maxTries = 50) {
static bool waitForActiveWindow(const std::string& class_, char fullscreen = '0', bool logLastCheck = true, int maxTries = 50) {
int cnt = 0;
while (!isActiveWindow(class_, fullscreen, false)) {
++cnt;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (cnt > maxTries) {
return isActiveWindow(class_, fullscreen, true);
return isActiveWindow(class_, fullscreen, logLastCheck);
}
}
return true;
@ -232,24 +255,6 @@ static bool testWindowFocusOnFullscreenConflict() {
OK(getFromSocket("/keyword misc:focus_on_activate true"));
auto spawnKittyActivating = [] -> std::string {
// `XXXXXX` is what `mkstemp` expects to find in the string
std::string tmpFilename = (std::filesystem::temp_directory_path() / "XXXXXX").string();
int fd = mkstemp(tmpFilename.data());
if (fd < 0) {
NLog::log("{}Error: could not create tmp file: errno {}", Colors::RED, errno);
return "";
}
(void)close(fd);
bool ok = spawnKitty("kitty_activating",
{"-o", "allow_remote_control=yes", "--", "/bin/sh", "-c", "while [ -f \"" + tmpFilename + "\" ]; do :; done; kitten @ focus-window; sleep infinity"});
if (!ok) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return "";
}
return tmpFilename;
};
// Unfullscreen on conflict
{
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 2"));
@ -373,6 +378,61 @@ static void testMaximizeSize() {
EXPECT(Tests::windowCount(), 0);
}
static void testFloatingFocusOnFullscreen() {
NLog::log("{}Testing floating focus on fullscreen", Colors::GREEN);
EXPECT(spawnKitty("kitty_A"), true);
OK(getFromSocket("/dispatch togglefloating"));
EXPECT(spawnKitty("kitty_B"), true);
OK(getFromSocket("/dispatch fullscreen 1"));
OK(getFromSocket("/dispatch cyclenext"));
OK(getFromSocket("/dispatch plugin:test:floating_focus_on_fullscreen"));
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
}
static void testGroupFallbackFocus() {
NLog::log("{}Testing group fallback focus", Colors::GREEN);
EXPECT(spawnKitty("kitty_A"), true);
OK(getFromSocket("/dispatch togglegroup"));
EXPECT(spawnKitty("kitty_B"), true);
EXPECT(spawnKitty("kitty_C"), true);
EXPECT(spawnKitty("kitty_D"), true);
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("class: kitty_D"), true);
}
OK(getFromSocket("/dispatch focuswindow class:kitty_B"));
OK(getFromSocket("/dispatch focuswindow class:kitty_D"));
OK(getFromSocket("/dispatch killactive"));
Tests::waitUntilWindowsN(3);
// Focus must return to the last focus, in this case B.
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("class: kitty_B"), true);
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
}
static void testBringActiveToTopMouseMovement() {
NLog::log("{}Testing bringactivetotop mouse movement", Colors::GREEN);
@ -412,6 +472,192 @@ static void testBringActiveToTopMouseMovement() {
Tests::killAllWindows();
}
static void testInitialFloatSize() {
NLog::log("{}Testing initial float size", Colors::GREEN);
Tests::killAllWindows();
OK(getFromSocket("/keyword windowrule match:class kitty, float yes"));
OK(getFromSocket("/keyword input:float_switch_override_focus 0"));
EXPECT(spawnKitty("kitty"), true);
{
// Kitty by default opens as 640x400, if this changes this test will break
auto str = getFromSocket("/clients");
EXPECT(str.contains("size: 640,400"), true);
}
OK(getFromSocket("/reload"));
Tests::killAllWindows();
OK(getFromSocket("/dispatch exec [float yes]kitty"));
Tests::waitUntilWindowsN(1);
{
// Kitty by default opens as 640x400, if this changes this test will break
auto str = getFromSocket("/clients");
EXPECT(str.contains("size: 640,400"), true);
EXPECT(str.contains("floating: 1"), true);
}
Tests::killAllWindows();
}
/// Tests that the `focus_on_activate` effect of window rules always overrides
/// the `misc:focus_on_activate` variable.
static bool testWindowRuleFocusOnActivate() {
OK(getFromSocket("/reload"));
if (!spawnKitty("kitty_default")) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return false;
}
// Do not focus anyone automatically
///////////OK(getFromSocket("/keyword windowrule match:class .*, no_initial_focus true"));
// `focus_on_activate off` takes over
{
OK(getFromSocket("/keyword misc:focus_on_activate true"));
OK(getFromSocket("/keyword windowrule match:class kitty_antifocus, focus_on_activate off"));
const std::string removeToActivate = spawnKittyActivating("kitty_antifocus");
if (removeToActivate.empty()) {
return false;
}
EXPECT(waitForActiveWindow("kitty_antifocus"), true);
OK(getFromSocket("/dispatch focuswindow class:kitty_default"));
EXPECT(isActiveWindow("kitty_default"), true);
std::filesystem::remove(removeToActivate);
// The focus should NOT transition, since the window rule explicitly forbids that
EXPECT(waitForActiveWindow("kitty_antifocus", '0', false), false);
}
// `focus_on_activate on` takes over
{
OK(getFromSocket("/keyword misc:focus_on_activate false"));
OK(getFromSocket("/keyword windowrule match:class kitty_superfocus, focus_on_activate on"));
const std::string removeToActivate = spawnKittyActivating("kitty_superfocus");
if (removeToActivate.empty()) {
return false;
}
EXPECT(waitForActiveWindow("kitty_superfocus"), true);
OK(getFromSocket("/dispatch focuswindow class:kitty_default"));
EXPECT(isActiveWindow("kitty_default"), true);
std::filesystem::remove(removeToActivate);
// Now that we requested activation, the focus SHOULD transition to kitty_superfocus, according to the window rule
EXPECT(waitForActiveWindow("kitty_superfocus"), true);
}
NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload"));
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
return true;
}
// tests if a pinned window contains the valid workspace after change
static bool testPinnedWorkspacesValid() {
OK(getFromSocket("/reload"));
getFromSocket("/dispatch workspace 1337");
if (!spawnKitty("kitty")) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return false;
}
OK(getFromSocket("/dispatch setfloating class:kitty"));
OK(getFromSocket("/dispatch pin class:kitty"));
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 1337"), true);
EXPECT(str.contains("pinned: 1"), true);
}
getFromSocket("/dispatch workspace 1338");
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 1338"), true);
EXPECT(str.contains("pinned: 1"), true);
}
OK(getFromSocket("/dispatch settiled class:kitty"))
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 1338"), true);
EXPECT(str.contains("pinned: 0"), true);
}
NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload"));
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
return true;
}
static bool testWindowRuleWorkspaceEmpty() {
NLog::log("{}Testing windowrule workspace empty", Colors::YELLOW);
OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword windowrule match:class kitty_A, workspace empty"));
OK(getFromSocket("/keyword windowrule match:class kitty_B, workspace emptyn"));
getFromSocket("/dispatch workspace 3");
if (!spawnKitty("kitty")) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return false;
}
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 3"), true);
}
if (!spawnKitty("kitty_A")) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return false;
}
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 1"), true);
}
getFromSocket("/dispatch workspace 3");
if (!spawnKitty("kitty_B")) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return false;
}
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 4"), true);
}
Tests::killAllWindows();
return true;
}
static bool test() {
NLog::log("{}Testing windows", Colors::GREEN);
@ -525,14 +771,14 @@ static bool test() {
EXPECT(Tests::windowCount(), 3);
NLog::log("{}Checking props of xeyes", Colors::YELLOW);
// check some window props of xeyes, try to tile them
// check some window props of xeyes, try to float it
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "floating: 1");
getFromSocket("/dispatch settiled class:XEyes");
EXPECT_NOT_CONTAINS(str, "floating: 1");
getFromSocket("/dispatch setfloating class:XEyes");
std::this_thread::sleep_for(std::chrono::milliseconds(200));
str = getFromSocket("/clients");
EXPECT_NOT_CONTAINS(str, "floating: 1");
EXPECT_CONTAINS(str, "floating: 1");
}
// kill all
@ -716,6 +962,23 @@ static bool test() {
Tests::killAllWindows();
OK(getFromSocket("/keyword windowrule[border-magic-kitty]:match:class border_kitty"));
OK(getFromSocket("/keyword windowrule[border-magic-kitty]:border_color rgba(c6ff00ff) rgba(ff0000ee) 45deg"));
if (!spawnKitty("border_kitty"))
return false;
OK(getFromSocket("/dispatch focuswindow class:border_kitty"));
{
auto str = getFromSocket("/getprop active active_border_color");
EXPECT_CONTAINS(str, "ffc6ff00");
EXPECT_CONTAINS(str, "eeff0000");
EXPECT_CONTAINS(str, "45deg");
}
Tests::killAllWindows();
if (!spawnKitty("tag_kitty"))
return false;
@ -799,7 +1062,8 @@ static bool test() {
Tests::killAllWindows();
// test expression rules
OK(getFromSocket("/keyword windowrule match:class expr_kitty, float yes, size monitor_w*0.5 monitor_h*0.5, move 20+(monitor_w*0.1) monitor_h*0.5"));
OK(getFromSocket("/keyword windowrule match:class expr_kitty, float yes, size monitor_w*0.5 monitor_h*0.5, min_size monitor_w*0.25 monitor_h*0.25, "
"max_size monitor_w*0.75 monitor_h*0.75, move 20+(monitor_w*0.1) monitor_h*0.5"));
if (!spawnKitty("expr_kitty"))
return false;
@ -809,12 +1073,20 @@ static bool test() {
EXPECT_CONTAINS(str, "floating: 1");
EXPECT_CONTAINS(str, "at: 212,540");
EXPECT_CONTAINS(str, "size: 960,540");
auto min = getFromSocket("/getprop active min_size");
EXPECT_CONTAINS(min, "480");
EXPECT_CONTAINS(min, "270");
auto max = getFromSocket("/getprop active max_size");
EXPECT_CONTAINS(max, "1440");
EXPECT_CONTAINS(max, "810");
}
OK(getFromSocket("/reload"));
Tests::killAllWindows();
OK(getFromSocket("/dispatch plugin:test:add_rule"));
OK(getFromSocket("/dispatch plugin:test:add_window_rule"));
OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword windowrule match:class plugin_kitty, plugin_rule effect"));
@ -822,12 +1094,12 @@ static bool test() {
if (!spawnKitty("plugin_kitty"))
return false;
OK(getFromSocket("/dispatch plugin:test:check_rule"));
OK(getFromSocket("/dispatch plugin:test:check_window_rule"));
OK(getFromSocket("/reload"));
Tests::killAllWindows();
OK(getFromSocket("/dispatch plugin:test:add_rule"));
OK(getFromSocket("/dispatch plugin:test:add_window_rule"));
OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword windowrule[test-plugin-rule]:match:class plugin_kitty"));
@ -836,16 +1108,20 @@ static bool test() {
if (!spawnKitty("plugin_kitty"))
return false;
OK(getFromSocket("/dispatch plugin:test:check_rule"));
OK(getFromSocket("/dispatch plugin:test:check_window_rule"));
OK(getFromSocket("/reload"));
Tests::killAllWindows();
testGroupRules();
testMaximizeSize();
testFloatingFocusOnFullscreen();
testBringActiveToTopMouseMovement();
testGroupFallbackFocus();
testInitialFloatSize();
testWindowRuleFocusOnActivate();
testPinnedWorkspacesValid();
testWindowRuleWorkspaceEmpty();
NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload"));

View file

@ -20,6 +20,91 @@ using namespace Hyprutils::Utils;
#define UP CUniquePointer
#define SP CSharedPointer
static bool testSpecialWorkspaceFullscreen() {
NLog::log("{}Testing special workspace fullscreen detection", Colors::YELLOW);
CScopeGuard guard = {[&]() {
NLog::log("{}Cleaning up special workspace fullscreen test", Colors::YELLOW);
// Close special workspace if open
auto monitors = getFromSocket("/monitors");
if (monitors.contains("(special:test_fs_special)") && !monitors.contains("special workspace: 0 ()"))
getFromSocket("/dispatch togglespecialworkspace test_fs_special");
Tests::killAllWindows();
OK(getFromSocket("/reload"));
}};
getFromSocket("/dispatch workspace 1");
EXPECT(Tests::windowCount(), 0);
NLog::log("{}Test 1: Fullscreen detection on special workspace", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace special:test_fs_special"));
if (!Tests::spawnKitty("kitty_special"))
return false;
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: kitty_special");
EXPECT_CONTAINS(str, "(special:test_fs_special)");
}
OK(getFromSocket("/dispatch fullscreen 0"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
}
{
auto str = getFromSocket("/monitors");
EXPECT_CONTAINS(str, "(special:test_fs_special)");
}
NLog::log("{}Test 2: Special workspace fullscreen precedence", Colors::YELLOW);
// Close special workspace before spawning on regular workspace
OK(getFromSocket("/dispatch togglespecialworkspace test_fs_special"));
getFromSocket("/dispatch workspace 1");
if (!Tests::spawnKitty("kitty_regular"))
return false;
OK(getFromSocket("/dispatch fullscreen 0"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: kitty_regular");
EXPECT_CONTAINS(str, "fullscreen: 2");
}
OK(getFromSocket("/dispatch togglespecialworkspace test_fs_special"));
OK(getFromSocket("/dispatch focuswindow class:kitty_special"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: kitty_special");
}
NLog::log("{}Test 3: Toggle special workspace hides it", Colors::YELLOW);
OK(getFromSocket("/dispatch togglespecialworkspace test_fs_special"));
OK(getFromSocket("/dispatch focuswindow class:kitty_regular"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: kitty_regular");
EXPECT_CONTAINS(str, "fullscreen: 2");
}
{
auto str = getFromSocket("/monitors");
EXPECT_CONTAINS(str, "special workspace: 0 ()");
}
return true;
}
static bool testAsymmetricGaps() {
NLog::log("{}Testing asymmetric gap splits", Colors::YELLOW);
{
@ -108,6 +193,206 @@ static bool testAsymmetricGaps() {
return true;
}
static void testMultimonBAF() {
NLog::log("{}Testing multimon back and forth", Colors::YELLOW);
OK(getFromSocket("/keyword binds:workspace_back_and_forth 1"));
OK(getFromSocket("/dispatch focusmonitor HEADLESS-2"));
OK(getFromSocket("/dispatch workspace 1"));
Tests::spawnKitty();
OK(getFromSocket("/dispatch workspace 2"));
Tests::spawnKitty();
OK(getFromSocket("/dispatch focusmonitor HEADLESS-3"));
OK(getFromSocket("/dispatch workspace 3"));
Tests::spawnKitty();
OK(getFromSocket("/dispatch workspace 3"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 2 ");
}
OK(getFromSocket("/dispatch workspace 4"));
OK(getFromSocket("/dispatch workspace 4"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 2 ");
}
OK(getFromSocket("/dispatch workspace 2"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 4 ");
}
OK(getFromSocket("/dispatch workspace 3"));
OK(getFromSocket("/dispatch workspace 3"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 4 ");
}
OK(getFromSocket("/dispatch workspace 2"));
OK(getFromSocket("/dispatch workspace 3"));
OK(getFromSocket("/dispatch workspace 1"));
OK(getFromSocket("/dispatch workspace 1"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 3 ");
}
Tests::killAllWindows();
}
static void testMultimonFocus() {
NLog::log("{}Testing multimon focus and move", Colors::YELLOW);
OK(getFromSocket("/keyword input:follow_mouse 0"));
OK(getFromSocket("/dispatch focusmonitor HEADLESS-3"));
OK(getFromSocket("/dispatch workspace 8"));
OK(getFromSocket("/dispatch focusmonitor HEADLESS-2"));
OK(getFromSocket("/dispatch workspace 7"));
for (auto const& win : {"a", "b"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 7 ");
}
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
Tests::spawnKitty("c");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movefocus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 7 ");
}
OK(getFromSocket("/dispatch movewindow r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movefocus l"));
OK(getFromSocket("/keyword general:no_focus_fallback true"));
OK(getFromSocket("/keyword binds:window_direction_monitor_fallback false"));
EXPECT_NOT(getFromSocket("/dispatch movefocus l"), "ok");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movewindow l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/reload"));
Tests::killAllWindows();
}
static void testDynamicWsEffects() {
// test dynamic workspace effects, they shouldn't lag
OK(getFromSocket("/dispatch workspace 69"));
Tests::spawnKitty("bitch");
OK(getFromSocket("r/keyword workspace 69,bordersize:20"));
OK(getFromSocket("r/keyword workspace 69,rounding:false"));
EXPECT(getFromSocket("/getprop class:bitch border_size"), "20");
EXPECT(getFromSocket("/getprop class:bitch rounding"), "0");
OK(getFromSocket("/reload"));
Tests::killAllWindows();
}
static bool test() {
NLog::log("{}Testing workspaces", Colors::GREEN);
@ -442,14 +727,19 @@ static bool test() {
EXPECT_CONTAINS(str, "class: kitty_B");
}
// destroy the headless output
OK(getFromSocket("/output remove HEADLESS-3"));
// kill all
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
testMultimonBAF();
testMultimonFocus();
// destroy the headless output
OK(getFromSocket("/output remove HEADLESS-3"));
testSpecialWorkspaceFullscreen();
testAsymmetricGaps();
testDynamicWsEffects();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);

View file

@ -3,6 +3,7 @@
#include <cerrno>
#include <thread>
#include <print>
#include <fstream>
#include "../shared.hpp"
#include "../hyprctlCompat.hpp"
@ -39,6 +40,38 @@ CUniquePointer<CProcess> Tests::spawnKitty(const std::string& class_, const std:
return kitty;
}
CUniquePointer<CProcess> Tests::spawnLayerKitty(const std::string& namespace_, const std::vector<std::string> args) {
std::vector<std::string> programArgs = args;
if (!namespace_.empty()) {
programArgs.insert(programArgs.begin(), "--class");
programArgs.insert(programArgs.begin() + 1, namespace_);
}
programArgs.insert(programArgs.begin(), "+kitten");
programArgs.insert(programArgs.begin() + 1, "panel");
CUniquePointer<CProcess> kitty = makeUnique<CProcess>("kitty", programArgs);
kitty->addEnv("WAYLAND_DISPLAY", WLDISPLAY);
kitty->runAsync();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// wait while the layer spawns
int counter = 0;
while (processAlive(kitty->pid()) && countOccurrences(getFromSocket("/layers"), std::format("pid: {}", kitty->pid())) == 0) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (counter > 50)
return nullptr;
}
if (!processAlive(kitty->pid()))
return nullptr;
return kitty;
}
bool Tests::processAlive(pid_t pid) {
errno = 0;
int ret = kill(pid, 0);
@ -96,6 +129,38 @@ void Tests::waitUntilWindowsN(int n) {
}
}
int Tests::layerCount() {
return countOccurrences(getFromSocket("/layers"), "namespace: ");
}
bool Tests::killAllLayers() {
auto str = getFromSocket("/layers");
auto pos = str.find("pid: ");
while (pos != std::string::npos) {
auto pid = stoi(str.substr(pos + 5, str.find('\n', pos)));
kill(pid, 15);
// we need to wait for a bit because for some reason otherwise we'll end up
// with layers with pid -1 if they are all removed at the same time
std::this_thread::sleep_for(std::chrono::milliseconds(100));
pos = str.find("pid: ", pos + 5);
}
int counter = 0;
while (Tests::layerCount() != 0) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (counter > 50) {
std::println("{}Timed out waiting for layers to close", Colors::RED);
return false;
}
}
return true;
}
std::string Tests::execAndGet(const std::string& cmd) {
CProcess proc("/bin/sh", {"-c", cmd});
@ -105,3 +170,14 @@ std::string Tests::execAndGet(const std::string& cmd) {
return proc.stdOut();
}
bool Tests::writeFile(const std::string& name, const std::string& contents) {
std::ofstream of(name, std::ios::trunc);
if (!of.good())
return false;
of << contents;
of.close();
return true;
}

View file

@ -9,10 +9,14 @@
//NOLINTNEXTLINE
namespace Tests {
Hyprutils::Memory::CUniquePointer<Hyprutils::OS::CProcess> spawnKitty(const std::string& class_ = "", const std::vector<std::string> args = {});
Hyprutils::Memory::CUniquePointer<Hyprutils::OS::CProcess> spawnLayerKitty(const std::string& namespace_ = "", const std::vector<std::string> args = {});
bool processAlive(pid_t pid);
int windowCount();
int countOccurrences(const std::string& in, const std::string& what);
bool killAllWindows();
void waitUntilWindowsN(int n);
int layerCount();
bool killAllLayers();
std::string execAndGet(const std::string& cmd);
bool writeFile(const std::string& name, const std::string& contents);
};

View file

@ -179,6 +179,17 @@ master {
new_status = master
}
scrolling {
fullscreen_on_one_column = true
column_width = 0.5
focus_fit_method = 1
follow_focus = true
follow_min_visible = 1
explicit_column_widths = 0.25, 0.333, 0.5, 0.667, 0.75, 1.0
wrap_focus = true
wrap_swapcol = true
}
# https://wiki.hyprland.org/Configuring/Variables/#misc
misc {
force_default_wallpaper = -1 # Set to 0 or 1 to disable the anime mascot wallpapers
@ -239,7 +250,7 @@ bind = $mainMod, E, exec, $fileManager
bind = $mainMod, V, togglefloating,
bind = $mainMod, R, exec, $menu
bind = $mainMod, P, pseudo, # dwindle
bind = $mainMod, J, togglesplit, # dwindle
bind = $mainMod, J, layoutmsg, togglesplit, # dwindle
# Move focus with mainMod + arrow keys
bind = $mainMod, left, movefocus, l
@ -398,3 +409,5 @@ gesture = 5, left, dispatcher, sendshortcut, , i, activewindow
gesture = 5, right, dispatcher, sendshortcut, , t, activewindow
gesture = 4, right, dispatcher, sendshortcut, , return, activewindow
gesture = 4, left, dispatcher, movecursortocorner, 1
gesturep = 2, right, float

View file

@ -12,6 +12,7 @@
epoll-shim,
git,
glaze-hyprland,
glslang,
gtest,
hyprcursor,
hyprgraphics,
@ -21,10 +22,17 @@
hyprutils,
hyprwayland-scanner,
hyprwire,
lcms2,
libGL,
libdrm,
libexecinfo,
libinput,
libxcb,
libxcb-errors,
libxcb-render-util,
libxcb-wm,
libxdmcp,
libxcursor,
libxkbcommon,
libuuid,
libgbm,
@ -38,7 +46,6 @@
wayland,
wayland-protocols,
wayland-scanner,
xorg,
xwayland,
debug ? false,
withTests ? false,
@ -59,8 +66,20 @@
inherit (builtins) foldl' readFile;
inherit (lib.asserts) assertMsg;
inherit (lib.attrsets) mapAttrsToList;
inherit (lib.lists) flatten concatLists optional optionals;
inherit (lib.strings) makeBinPath optionalString cmakeBool trim;
inherit
(lib.lists)
flatten
concatLists
optional
optionals
;
inherit
(lib.strings)
makeBinPath
optionalString
cmakeBool
trim
;
fs = lib.fileset;
adapters = flatten [
@ -72,7 +91,8 @@
in
assert assertMsg (!nvidiaPatches) "The option `nvidiaPatches` has been removed.";
assert assertMsg (!enableNvidiaPatches) "The option `enableNvidiaPatches` has been removed.";
assert assertMsg (!hidpiXWayland) "The option `hidpiXWayland` has been removed. Please refer https://wiki.hypr.land/Configuring/XWayland";
assert assertMsg (!hidpiXWayland)
"The option `hidpiXWayland` has been removed. Please refer https://wiki.hypr.land/Configuring/XWayland";
assert assertMsg (!legacyRenderer) "The option `legacyRenderer` has been removed. Legacy renderer is no longer supported.";
assert assertMsg (!withHyprtester) "The option `withHyprtester` has been removed. Hyprtester is always built now.";
customStdenv.mkDerivation (finalAttrs: {
@ -85,23 +105,29 @@ in
fs.intersection
# allows non-flake builds to only include files tracked by git
(fs.gitTracked ../.)
(fs.unions (flatten [
../assets/hyprland-portals.conf
../assets/install
../hyprctl
../hyprland.pc.in
../LICENSE
../protocols
../src
../start
../systemd
../VERSION
(fs.fileFilter (file: file.hasExt "1") ../docs)
(fs.fileFilter (file: file.hasExt "conf" || file.hasExt "in") ../example)
(fs.fileFilter (file: file.hasExt "sh") ../scripts)
(fs.fileFilter (file: file.name == "CMakeLists.txt") ../.)
(optional withTests [../tests ../hyprtester])
]));
(
fs.unions (flatten [
../assets/hyprland-portals.conf
../assets/install
../hyprctl
../hyprland.pc.in
../hyprpm
../LICENSE
../protocols
../src
../start
../systemd
../VERSION
(fs.fileFilter (file: file.hasExt "1") ../docs)
(fs.fileFilter (file: file.hasExt "conf" || file.hasExt "in") ../example)
(fs.fileFilter (file: file.hasExt "sh") ../scripts)
(fs.fileFilter (file: file.name == "CMakeLists.txt") ../.)
(optional withTests [
../tests
../hyprtester
])
])
);
};
postPatch = ''
@ -117,7 +143,10 @@ in
GIT_COMMITS = revCount;
GIT_COMMIT_DATE = date;
GIT_COMMIT_HASH = commit;
GIT_DIRTY = if (commit == "") then "clean" else "dirty";
GIT_DIRTY =
if (commit == "")
then "clean"
else "dirty";
GIT_TAG = "v${trim (readFile "${finalAttrs.src}/VERSION")}";
};
@ -145,6 +174,7 @@ in
cairo
git
glaze-hyprland
glslang
gtest
hyprcursor
hyprgraphics
@ -152,12 +182,14 @@ in
hyprlang
hyprutils
hyprwire
lcms2
libdrm
libgbm
libGL
libinput
libuuid
libxcursor
libxkbcommon
libgbm
muparser
pango
pciutils
@ -167,16 +199,15 @@ in
wayland
wayland-protocols
wayland-scanner
xorg.libXcursor
]
(optionals customStdenv.hostPlatform.isBSD [epoll-shim])
(optionals customStdenv.hostPlatform.isMusl [libexecinfo])
(optionals enableXWayland [
xorg.libxcb
xorg.libXdmcp
xorg.xcbutilerrors
xorg.xcbutilrenderutil
xorg.xcbutilwm
libxcb
libxcb-errors
libxcb-render-util
libxcb-wm
libxdmcp
xwayland
])
(optional withSystemd systemd)
@ -199,7 +230,6 @@ in
"NO_SYSTEMD" = !withSystemd;
"CMAKE_DISABLE_PRECOMPILE_HEADERS" = true;
"NO_UWSM" = !withSystemd;
"NO_HYPRPM" = true;
"TRACY_ENABLE" = false;
"WITH_TESTS" = withTests;
};
@ -213,23 +243,26 @@ in
postInstall = ''
${optionalString wrapRuntimeDeps ''
wrapProgram $out/bin/Hyprland \
--suffix PATH : ${makeBinPath [
binutils
hyprland-guiutils
pciutils
pkgconf
]}
--suffix PATH : ${
makeBinPath [
binutils
hyprland-guiutils
pciutils
pkgconf
]
}
''}
${optionalString withTests ''
install hyprtester/pointer-warp -t $out/bin
install hyprtester/pointer-scroll -t $out/bin
install hyprtester/shortcut-inhibitor -t $out/bin
install hyprland_gtests -t $out/bin
install hyprtester/child-window -t $out/bin
''}
'';
passthru.providedSessions = ["hyprland"];
passthru.providedSessions = ["hyprland"] ++ optionals withSystemd ["hyprland-uwsm"];
meta = {
homepage = "https://github.com/hyprwm/Hyprland";

View file

@ -2,7 +2,7 @@
writeShellApplication,
deadnix,
statix,
alejandra,
nixfmt,
llvmPackages_19,
fd,
}:
@ -11,7 +11,7 @@ writeShellApplication {
runtimeInputs = [
deadnix
statix
alejandra
nixfmt
llvmPackages_19.clang-tools
fd
];
@ -24,14 +24,14 @@ writeShellApplication {
nix_format() {
if [ "$*" = 0 ]; then
fd '.*\.nix' . -E "$excludes" -x statix fix -- {} \;
fd '.*\.nix' . -E "$excludes" -X deadnix -e -- {} \; -X alejandra {} \;
fd '.*\.nix' . -E "$excludes" -X deadnix -e -- {} \; -X nixfmt {} \;
elif [ -d "$1" ]; then
fd '.*\.nix' "$1" -E "$excludes" -i -x statix fix -- {} \;
fd '.*\.nix' "$1" -E "$excludes" -i -X deadnix -e -- {} \; -X alejandra {} \;
fd '.*\.nix' "$1" -E "$excludes" -i -X deadnix -e -- {} \; -X nixfmt {} \;
else
statix fix -- "$1"
deadnix -e "$1"
alejandra "$1"
nixfmt "$1"
fi
}

View file

@ -1,13 +1,15 @@
self: {
config,
self:
{
lib,
pkgs,
...
}: let
}:
let
inherit (pkgs.stdenv.hostPlatform) system;
package = self.packages.${system}.default;
in {
in
{
config = {
wayland.windowManager.hyprland.package = lib.mkDefault package;
};

View file

@ -1,4 +1,5 @@
lib: let
lib:
let
inherit (lib)
attrNames
filterAttrs
@ -17,7 +18,7 @@ lib: let
This function takes a nested attribute set and converts it into Hyprland-compatible
configuration syntax, supporting top, bottom, and regular command sections.
Commands are flattened using the `flattenAttrs` function, and attributes are formatted as
`key = value` pairs. Lists are expanded as duplicate keys to match Hyprland's expected format.
@ -81,44 +82,51 @@ lib: let
:::
*/
toHyprlang = {
topCommandsPrefixes ? ["$" "bezier"],
bottomCommandsPrefixes ? [],
}: attrs: let
toHyprlang' = attrs: let
# Specially configured `toKeyValue` generator with support for duplicate keys
# and a legible key-value separator.
mkCommands = generators.toKeyValue {
mkKeyValue = generators.mkKeyValueDefault {} " = ";
listsAsDuplicateKeys = true;
indent = ""; # No indent, since we don't have nesting
};
toHyprlang =
{
topCommandsPrefixes ? [
"$"
"bezier"
],
bottomCommandsPrefixes ? [ ],
}:
attrs:
let
toHyprlang' =
attrs:
let
# Specially configured `toKeyValue` generator with support for duplicate keys
# and a legible key-value separator.
mkCommands = generators.toKeyValue {
mkKeyValue = generators.mkKeyValueDefault { } " = ";
listsAsDuplicateKeys = true;
indent = ""; # No indent, since we don't have nesting
};
# Flatten the attrset, combining keys in a "path" like `"a:b:c" = "x"`.
# Uses `flattenAttrs` with a colon separator.
commands = flattenAttrs (p: k: "${p}:${k}") attrs;
# Flatten the attrset, combining keys in a "path" like `"a:b:c" = "x"`.
# Uses `flattenAttrs` with a colon separator.
commands = flattenAttrs (p: k: "${p}:${k}") attrs;
# General filtering function to check if a key starts with any prefix in a given list.
filterCommands = list: n:
foldl (acc: prefix: acc || hasPrefix prefix n) false list;
# General filtering function to check if a key starts with any prefix in a given list.
filterCommands = list: n: foldl (acc: prefix: acc || hasPrefix prefix n) false list;
# Partition keys into top commands and the rest
result = partition (filterCommands topCommandsPrefixes) (attrNames commands);
topCommands = filterAttrs (n: _: builtins.elem n result.right) commands;
remainingCommands = removeAttrs commands result.right;
# Partition keys into top commands and the rest
result = partition (filterCommands topCommandsPrefixes) (attrNames commands);
topCommands = filterAttrs (n: _: builtins.elem n result.right) commands;
remainingCommands = removeAttrs commands result.right;
# Partition remaining commands into bottom commands and regular commands
result2 = partition (filterCommands bottomCommandsPrefixes) result.wrong;
bottomCommands = filterAttrs (n: _: builtins.elem n result2.right) remainingCommands;
regularCommands = removeAttrs remainingCommands result2.right;
# Partition remaining commands into bottom commands and regular commands
result2 = partition (filterCommands bottomCommandsPrefixes) result.wrong;
bottomCommands = filterAttrs (n: _: builtins.elem n result2.right) remainingCommands;
regularCommands = removeAttrs remainingCommands result2.right;
in
# Concatenate strings from mapping `mkCommands` over top, regular, and bottom commands.
concatMapStrings mkCommands [
topCommands
regularCommands
bottomCommands
];
in
# Concatenate strings from mapping `mkCommands` over top, regular, and bottom commands.
concatMapStrings mkCommands [
topCommands
regularCommands
bottomCommands
];
in
toHyprlang' attrs;
/**
@ -131,7 +139,7 @@ lib: let
Configuration:
* `pred` - A function `(string -> string -> string)` defining how keys should be concatenated.
# Inputs
Structured function argument:
@ -139,7 +147,7 @@ lib: let
: pred (required)
: A function that determines how parent and child keys should be combined into a single key.
It takes a `prefix` (parent key) and `key` (current key) and returns the joined key.
Value:
: The nested attribute set to be flattened.
@ -174,26 +182,21 @@ lib: let
```
:::
*/
flattenAttrs = pred: attrs: let
flattenAttrs' = prefix: attrs:
builtins.foldl' (
acc: key: let
value = attrs.${key};
newKey =
if prefix == ""
then key
else pred prefix key;
in
acc
// (
if builtins.isAttrs value
then flattenAttrs' newKey value
else {"${newKey}" = value;}
)
) {} (builtins.attrNames attrs);
in
flattenAttrs =
pred: attrs:
let
flattenAttrs' =
prefix: attrs:
builtins.foldl' (
acc: key:
let
value = attrs.${key};
newKey = if prefix == "" then key else pred prefix key;
in
acc // (if builtins.isAttrs value then flattenAttrs' newKey value else { "${newKey}" = value; })
) { } (builtins.attrNames attrs);
in
flattenAttrs' "" attrs;
in
{

View file

@ -1,18 +1,21 @@
inputs: {
inputs:
{
config,
lib,
pkgs,
...
}: let
}:
let
inherit (pkgs.stdenv.hostPlatform) system;
selflib = import ./lib.nix lib;
cfg = config.programs.hyprland;
in {
in
{
options = {
programs.hyprland = {
plugins = lib.mkOption {
type = with lib.types; listOf (either package path);
default = [];
default = [ ];
description = ''
List of Hyprland plugins to use. Can either be packages or
absolute plugin paths.
@ -20,23 +23,25 @@ in {
};
settings = lib.mkOption {
type = with lib.types; let
valueType =
nullOr (oneOf [
bool
int
float
str
path
(attrsOf valueType)
(listOf valueType)
])
// {
description = "Hyprland configuration value";
};
in
type =
with lib.types;
let
valueType =
nullOr (oneOf [
bool
int
float
str
path
(attrsOf valueType)
(listOf valueType)
])
// {
description = "Hyprland configuration value";
};
in
valueType;
default = {};
default = { };
description = ''
Hyprland configuration written in Nix. Entries with the same key
should be written as lists. Variables' and colors' names should be
@ -92,8 +97,15 @@ in {
topPrefixes = lib.mkOption {
type = with lib.types; listOf str;
default = ["$" "bezier"];
example = ["$" "bezier" "source"];
default = [
"$"
"bezier"
];
example = [
"$"
"bezier"
"source"
];
description = ''
List of prefix of attributes to put at the top of the config.
'';
@ -101,8 +113,8 @@ in {
bottomPrefixes = lib.mkOption {
type = with lib.types; listOf str;
default = [];
example = ["source"];
default = [ ];
example = [ "source" ];
description = ''
List of prefix of attributes to put at the bottom of the config.
'';
@ -117,35 +129,36 @@ in {
};
}
(lib.mkIf cfg.enable {
environment.etc."xdg/hypr/hyprland.conf" = let
shouldGenerate = cfg.extraConfig != "" || cfg.settings != {} || cfg.plugins != [];
environment.etc."xdg/hypr/hyprland.conf" =
let
shouldGenerate = cfg.extraConfig != "" || cfg.settings != { } || cfg.plugins != [ ];
pluginsToHyprlang = plugins:
selflib.toHyprlang {
topCommandsPrefixes = cfg.topPrefixes;
bottomCommandsPrefixes = cfg.bottomPrefixes;
}
{
"exec-once" = let
mkEntry = entry:
if lib.types.package.check entry
then "${entry}/lib/lib${entry.pname}.so"
else entry;
hyprctl = lib.getExe' config.programs.hyprland.package "hyprctl";
in
map (p: "${hyprctl} plugin load ${mkEntry p}") cfg.plugins;
};
in
lib.mkIf shouldGenerate {
text =
lib.optionalString (cfg.plugins != [])
(pluginsToHyprlang cfg.plugins)
+ lib.optionalString (cfg.settings != {})
(selflib.toHyprlang {
pluginsToHyprlang =
_plugins:
selflib.toHyprlang
{
topCommandsPrefixes = cfg.topPrefixes;
bottomCommandsPrefixes = cfg.bottomPrefixes;
}
cfg.settings)
{
"exec-once" =
let
mkEntry =
entry: if lib.types.package.check entry then "${entry}/lib/lib${entry.pname}.so" else entry;
hyprctl = lib.getExe' config.programs.hyprland.package "hyprctl";
in
map (p: "${hyprctl} plugin load ${mkEntry p}") cfg.plugins;
};
in
lib.mkIf shouldGenerate {
text =
lib.optionalString (cfg.plugins != [ ]) (pluginsToHyprlang cfg.plugins)
+ lib.optionalString (cfg.settings != { }) (
selflib.toHyprlang {
topCommandsPrefixes = cfg.topPrefixes;
bottomCommandsPrefixes = cfg.bottomPrefixes;
} cfg.settings
)
+ lib.optionalString (cfg.extraConfig != "") cfg.extraConfig;
};
})

View file

@ -2,20 +2,27 @@
self,
lib,
inputs,
}: let
mkDate = longDate: (lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
}:
let
mkDate =
longDate:
(lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
ver = lib.removeSuffix "\n" (builtins.readFile ../VERSION);
in {
in
{
# Contains what a user is most likely to care about:
# Hyprland itself, XDPH and the Share Picker.
default = lib.composeManyExtensions (with self.overlays; [
hyprland-packages
hyprland-extras
]);
default = lib.composeManyExtensions (
with self.overlays;
[
hyprland-packages
hyprland-extras
]
);
# Packages for variations of Hyprland, dependencies included.
hyprland-packages = lib.composeManyExtensions [
@ -33,49 +40,45 @@ in {
self.overlays.glaze
# Hyprland packages themselves
(final: _prev: let
date = mkDate (self.lastModifiedDate or "19700101");
version = "${ver}+date=${date}_${self.shortRev or "dirty"}";
in {
hyprland = final.callPackage ./default.nix {
stdenv = final.gcc15Stdenv;
commit = self.rev or "";
revCount = self.sourceInfo.revCount or "";
inherit date version;
};
hyprland-unwrapped = final.hyprland.override {wrapRuntimeDeps = false;};
(
final: _prev:
let
date = mkDate (self.lastModifiedDate or "19700101");
version = "${ver}+date=${date}_${self.shortRev or "dirty"}";
in
{
hyprland = final.callPackage ./default.nix {
stdenv = final.gcc15Stdenv;
commit = self.rev or "";
revCount = self.sourceInfo.revCount or "";
inherit date version;
};
hyprland-unwrapped = final.hyprland.override { wrapRuntimeDeps = false; };
hyprland-with-tests = final.hyprland.override {withTests = true;};
hyprland-with-tests = final.hyprland.override { withTests = true; };
hyprland-with-hyprtester =
builtins.trace ''
hyprland-with-hyprtester = builtins.trace ''
hyprland-with-hyprtester was removed. Please use the hyprland package.
Hyprtester is always built now.
''
final.hyprland;
'' final.hyprland;
# deprecated packages
hyprland-legacy-renderer =
builtins.trace ''
# deprecated packages
hyprland-legacy-renderer = builtins.trace ''
hyprland-legacy-renderer was removed. Please use the hyprland package.
Legacy renderer is no longer supported.
''
final.hyprland;
'' final.hyprland;
hyprland-nvidia =
builtins.trace ''
hyprland-nvidia = builtins.trace ''
hyprland-nvidia was removed. Please use the hyprland package.
Nvidia patches are no longer needed.
''
final.hyprland;
'' final.hyprland;
hyprland-hidpi =
builtins.trace ''
hyprland-hidpi = builtins.trace ''
hyprland-hidpi was removed. Please use the hyprland package.
For more information, refer to https://wiki.hypr.land/Configuring/XWayland.
''
final.hyprland;
})
'' final.hyprland;
}
)
];
# Debug
@ -83,10 +86,10 @@ in {
# Dependencies
self.overlays.hyprland-packages
(final: prev: {
aquamarine = prev.aquamarine.override {debug = true;};
hyprutils = prev.hyprutils.override {debug = true;};
hyprland-debug = prev.hyprland.override {debug = true;};
(_final: prev: {
aquamarine = prev.aquamarine.override { debug = true; };
hyprutils = prev.hyprutils.override { debug = true; };
hyprland-debug = prev.hyprland.override { debug = true; };
})
];
@ -100,21 +103,23 @@ in {
# this version is the one used in the git submodule, and allows us to
# fetch the source without '?submodules=1'
udis86 = final: prev: {
udis86-hyprland = prev.udis86.overrideAttrs (_self: _super: {
src = final.fetchFromGitHub {
owner = "canihavesomecoffee";
repo = "udis86";
rev = "5336633af70f3917760a6d441ff02d93477b0c86";
hash = "sha256-HifdUQPGsKQKQprByeIznvRLONdOXeolOsU5nkwIv3g=";
};
udis86-hyprland = prev.udis86.overrideAttrs (
_self: _super: {
src = final.fetchFromGitHub {
owner = "canihavesomecoffee";
repo = "udis86";
rev = "5336633af70f3917760a6d441ff02d93477b0c86";
hash = "sha256-HifdUQPGsKQKQprByeIznvRLONdOXeolOsU5nkwIv3g=";
};
patches = [];
});
patches = [ ];
}
);
};
# Even though glaze itself disables it by default, nixpkgs sets ENABLE_SSL set to true.
# Since we don't include openssl, the build failes without the `enableSSL = false;` override
glaze = final: prev: {
glaze = _final: prev: {
glaze-hyprland = prev.glaze.override {
enableSSL = false;
enableInterop = false;

View file

@ -1,68 +1,75 @@
inputs: pkgs: let
inputs: pkgs:
let
flake = inputs.self.packages.${pkgs.stdenv.hostPlatform.system};
hyprland = flake.hyprland-with-tests;
in {
in
{
tests = pkgs.testers.runNixOSTest {
name = "hyprland-tests";
nodes.machine = {pkgs, ...}: {
environment.systemPackages = with pkgs; [
# Programs needed for tests
jq
kitty
wl-clipboard
xorg.xeyes
];
nodes.machine =
{ pkgs, ... }:
{
environment.systemPackages = with pkgs; [
# Programs needed for tests
jq
kitty
wl-clipboard
xeyes
];
# Enabled by default for some reason
services.speechd.enable = false;
# Enabled by default for some reason
services.speechd.enable = false;
environment.variables = {
"AQ_TRACE" = "1";
"HYPRLAND_TRACE" = "1";
"XDG_RUNTIME_DIR" = "/tmp";
"XDG_CACHE_HOME" = "/tmp";
"KITTY_CONFIG_DIRECTORY" = "/etc/kitty";
};
environment.etc."kitty/kitty.conf".text = ''
confirm_os_window_close 0
'';
programs.hyprland = {
enable = true;
package = hyprland;
# We don't need portals in this test, so we don't set portalPackage
};
# Test configuration
environment.etc."test.conf".source = "${hyprland}/share/hypr/test.conf";
# Disable portals
xdg.portal.enable = pkgs.lib.mkForce false;
# Autologin root into tty
services.getty.autologinUser = "alice";
system.stateVersion = "24.11";
users.users.alice = {
isNormalUser = true;
};
virtualisation = {
cores = 4;
# Might crash with less
memorySize = 8192;
resolution = {
x = 1920;
y = 1080;
environment.variables = {
"AQ_TRACE" = "1";
"HYPRLAND_TRACE" = "1";
"XDG_RUNTIME_DIR" = "/tmp";
"XDG_CACHE_HOME" = "/tmp";
"KITTY_CONFIG_DIRECTORY" = "/etc/kitty";
};
# Doesn't seem to do much, thought it would fix XWayland crashing
qemu.options = ["-vga none -device virtio-gpu-pci"];
environment.etc."kitty/kitty.conf".text = ''
confirm_os_window_close 0
remember_window_size no
initial_window_width 640
initial_window_height 400
'';
programs.hyprland = {
enable = true;
package = hyprland;
# We don't need portals in this test, so we don't set portalPackage
};
# Test configuration
environment.etc."test.conf".source = "${hyprland}/share/hypr/test.conf";
# Disable portals
xdg.portal.enable = pkgs.lib.mkForce false;
# Autologin root into tty
services.getty.autologinUser = "alice";
system.stateVersion = "24.11";
users.users.alice = {
isNormalUser = true;
};
virtualisation = {
cores = 4;
# Might crash with less
memorySize = 8192;
resolution = {
x = 1920;
y = 1080;
};
# Doesn't seem to do much, thought it would fix XWayland crashing
qemu.options = [ "-vga none -device virtio-gpu-pci" ];
};
};
};
testScript = ''
# Wait for tty to be up

View file

@ -15,7 +15,7 @@ echo 'static const std::map<std::string, std::string> SHADERS = {' >> ./src/rend
for filename in `ls ${SHADERS_SRC}`; do
echo "-- ${filename}"
{ echo 'R"#('; cat ${SHADERS_SRC}/${filename}; echo ')#"'; } > ./src/render/shaders/${filename}.inc
{ echo -n 'R"#('; cat ${SHADERS_SRC}/${filename}; echo ')#"'; } > ./src/render/shaders/${filename}.inc
echo "{\"${filename}\"," >> ./src/render/shaders/Shaders.hpp
echo "#include \"./${filename}.inc\"" >> ./src/render/shaders/Shaders.hpp
echo "}," >> ./src/render/shaders/Shaders.hpp

View file

@ -7,6 +7,7 @@
#include "desktop/state/FocusState.hpp"
#include "desktop/history/WindowHistoryTracker.hpp"
#include "desktop/history/WorkspaceHistoryTracker.hpp"
#include "desktop/view/Group.hpp"
#include "helpers/Splashes.hpp"
#include "config/ConfigValue.hpp"
#include "config/ConfigWatcher.hpp"
@ -19,6 +20,7 @@
#include "managers/ANRManager.hpp"
#include "managers/eventLoop/EventLoopManager.hpp"
#include "managers/permissions/DynamicPermissionManager.hpp"
#include "managers/screenshare/ScreenshareManager.hpp"
#include <algorithm>
#include <aquamarine/output/Output.hpp>
#include <bit>
@ -45,6 +47,7 @@
#include "protocols/core/Compositor.hpp"
#include "protocols/core/Subcompositor.hpp"
#include "desktop/view/LayerSurface.hpp"
#include "layout/space/Space.hpp"
#include "render/Renderer.hpp"
#include "xwayland/XWayland.hpp"
#include "helpers/ByteOperations.hpp"
@ -60,9 +63,7 @@
#include "managers/animation/AnimationManager.hpp"
#include "managers/animation/DesktopAnimationManager.hpp"
#include "managers/EventManager.hpp"
#include "managers/HookSystemManager.hpp"
#include "managers/ProtocolManager.hpp"
#include "managers/LayoutManager.hpp"
#include "managers/WelcomeManager.hpp"
#include "render/AsyncResourceGatherer.hpp"
#include "plugins/PluginSystem.hpp"
@ -71,6 +72,9 @@
#include "debug/HyprDebugOverlay.hpp"
#include "helpers/MonitorFrameScheduler.hpp"
#include "i18n/Engine.hpp"
#include "layout/LayoutManager.hpp"
#include "layout/target/WindowTarget.hpp"
#include "event/EventBus.hpp"
#include <hyprutils/string/String.hpp>
#include <aquamarine/input/Input.hpp>
@ -103,11 +107,6 @@ static void handleUnrecoverableSignal(int sig) {
signal(SIGABRT, SIG_DFL);
signal(SIGSEGV, SIG_DFL);
if (g_pHookSystem && g_pHookSystem->m_currentEventPlugin) {
longjmp(g_pHookSystem->m_hookFaultJumpBuf, 1);
return;
}
// Kill the program if the crash-reporter is caught in a deadlock.
signal(SIGALRM, [](int _) {
char const* msg = "\nCrashReporter exceeded timeout, forcefully exiting\n";
@ -283,7 +282,6 @@ static bool filterGlobals(const wl_client* client, const wl_global* global, void
//
void CCompositor::initServer(std::string socketName, int socketFd) {
if (m_onlyConfigVerification) {
g_pHookSystem = makeUnique<CHookSystemManager>();
g_pKeybindManager = makeUnique<CKeybindManager>();
g_pAnimationManager = makeUnique<CHyprAnimationManager>();
g_pConfigManager = makeUnique<CConfigManager>();
@ -372,11 +370,11 @@ void CCompositor::initServer(std::string socketName, int socketFd) {
return ret == 0 && cap != 0;
};
if ((m_drm.syncobjSupport = syncObjSupport(m_drm.fd)))
Log::logger->log(Log::DEBUG, "DRM DisplayNode syncobj timeline support: {}", m_drm.syncobjSupport ? "yes" : "no");
m_drm.syncobjSupport = syncObjSupport(m_drm.fd);
Log::logger->log(Log::DEBUG, "DRM DisplayNode syncobj timeline support: {}", m_drm.syncobjSupport ? "yes" : "no");
if ((m_drmRenderNode.syncObjSupport = syncObjSupport(m_drmRenderNode.fd)))
Log::logger->log(Log::DEBUG, "DRM RenderNode syncobj timeline support: {}", m_drmRenderNode.syncObjSupport ? "yes" : "no");
m_drmRenderNode.syncObjSupport = syncObjSupport(m_drmRenderNode.fd);
Log::logger->log(Log::DEBUG, "DRM RenderNode syncobj timeline support: {}", m_drmRenderNode.syncObjSupport ? "yes" : "no");
if (!m_drm.syncobjSupport && !m_drmRenderNode.syncObjSupport)
Log::logger->log(Log::DEBUG, "DRM no syncobj support, disabling explicit sync");
@ -483,6 +481,10 @@ void CCompositor::initAllSignals() {
m_sessionActive = true;
// Reset animation tick state to avoid stale timer issues after suspend/wake
if (g_pAnimationManager)
g_pAnimationManager->resetTickState();
for (auto const& m : m_monitors) {
scheduleFrameForMonitor(m);
m->applyMonitorRule(&m->m_activeMonitorRule, true);
@ -586,11 +588,10 @@ void CCompositor::cleanup() {
g_pHyprRenderer.reset();
g_pHyprOpenGL.reset();
g_pConfigManager.reset();
g_pLayoutManager.reset();
g_layoutManager.reset();
g_pHyprError.reset();
g_pConfigManager.reset();
g_pKeybindManager.reset();
g_pHookSystem.reset();
g_pXWaylandManager.reset();
g_pPointerManager.reset();
g_pSeatManager.reset();
@ -619,9 +620,6 @@ void CCompositor::initManagers(eManagersInitStage stage) {
Log::logger->log(Log::DEBUG, "Creating the EventLoopManager!");
g_pEventLoopManager = makeUnique<CEventLoopManager>(m_wlDisplay, m_wlEventLoop);
Log::logger->log(Log::DEBUG, "Creating the HookSystem!");
g_pHookSystem = makeUnique<CHookSystemManager>();
Log::logger->log(Log::DEBUG, "Creating the KeybindManager!");
g_pKeybindManager = makeUnique<CKeybindManager>();
@ -638,7 +636,7 @@ void CCompositor::initManagers(eManagersInitStage stage) {
g_pHyprError = makeUnique<CHyprError>();
Log::logger->log(Log::DEBUG, "Creating the LayoutManager!");
g_pLayoutManager = makeUnique<CLayoutManager>();
g_layoutManager = makeUnique<Layout::CLayoutManager>();
Log::logger->log(Log::DEBUG, "Creating the TokenManager!");
g_pTokenManager = makeUnique<CTokenManager>();
@ -792,7 +790,8 @@ void CCompositor::startCompositor() {
createLockFile();
EMIT_HOOK_EVENT("ready", nullptr);
Event::bus()->m_events.ready.emit();
if (m_watchdogWriteFd.isValid())
write(m_watchdogWriteFd.get(), "vax", 3);
@ -872,7 +871,7 @@ PHLMONITOR CCompositor::getMonitorFromVector(const Vector2D& point) {
void CCompositor::removeWindowFromVectorSafe(PHLWINDOW pWindow) {
if (!pWindow->m_fadingOut) {
EMIT_HOOK_EVENT("destroyWindow", pWindow);
Event::bus()->m_events.window.destroy.emit(pWindow);
std::erase_if(m_windows, [&](SP<Desktop::View::CWindow>& el) { return el == pWindow; });
std::erase_if(m_windowsFadingOut, [&](PHLWINDOWREF el) { return el.lock() == pWindow; });
@ -1034,6 +1033,8 @@ PHLWINDOW CCompositor::vectorToWindowUnified(const Vector2D& pos, uint8_t proper
if (!w->m_isFloating && w->m_isMapped && w->workspaceID() == WSPID && !w->isHidden() && !w->m_X11ShouldntFocus && !w->m_ruleApplicator->noFocus().valueOrDefault() &&
w != pIgnoreWindow && !isShadowedByModal(w)) {
CBox box = (properties & Desktop::View::USE_PROP_TILED) ? w->getWindowBoxUnified(properties) : CBox{w->m_position, w->m_size};
if ((properties & Desktop::View::INPUT_EXTENTS) && BORDER_GRAB_AREA > 0 && !w->isX11OverrideRedirect())
box.expand(BORDER_GRAB_AREA);
if (box.containsPoint(pos))
return w;
}
@ -1179,7 +1180,7 @@ PHLWINDOW CCompositor::getWindowFromSurface(SP<CWLSurfaceResource> pSurface) {
const auto VIEW = pSurface->m_hlSurface->view();
if (VIEW->type() != Desktop::View::VIEW_TYPE_WINDOW)
if (!VIEW || VIEW->type() != Desktop::View::VIEW_TYPE_WINDOW)
return nullptr;
return dynamicPointerCast<Desktop::View::CWindow>(VIEW);
@ -1371,8 +1372,8 @@ void CCompositor::addToFadingOutSafe(PHLWINDOW pWindow) {
m_windowsFadingOut.emplace_back(pWindow);
}
PHLWINDOW CCompositor::getWindowInDirection(PHLWINDOW pWindow, char dir) {
if (!isDirection(dir))
PHLWINDOW CCompositor::getWindowInDirection(PHLWINDOW pWindow, Math::eDirection dir) {
if (dir == Math::DIRECTION_DEFAULT)
return nullptr;
const auto PMONITOR = pWindow->m_monitor.lock();
@ -1389,8 +1390,8 @@ PHLWINDOW CCompositor::getWindowInDirection(PHLWINDOW pWindow, char dir) {
return getWindowInDirection(WINDOWIDEALBB, PWORKSPACE, dir, pWindow, pWindow->m_isFloating);
}
PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorkspace, char dir, PHLWINDOW ignoreWindow, bool useVectorAngles) {
if (!isDirection(dir))
PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorkspace, Math::eDirection dir, PHLWINDOW ignoreWindow, bool useVectorAngles) {
if (dir == Math::DIRECTION_DEFAULT)
return nullptr;
// 0 -> history, 1 -> shared length
@ -1404,6 +1405,35 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks
PHLWINDOW leaderWindow = nullptr;
if (!useVectorAngles) {
// helper to check if two rectangles are adjacent along an axis, considering slight overlaps.
// returns true if: STICKS (delta <= 2) OR rectangles overlap but no more than 50% of the smaller dimension.
static auto isAdjacent = [](const double aMin, const double aMax, const double bMin, const double bMax) -> bool {
constexpr double STICK_THRESHOLD = 2.0;
constexpr double MAX_OVERLAP_RATIO = 0.5;
const double aEdge = aMin;
const double bEdge = bMax;
const double delta = aEdge - bEdge;
// old STICKS check for 2px
if (std::abs(delta) < STICK_THRESHOLD)
return true;
if (delta >= 0)
return false;
const double overlap = -delta;
const double sizeA = aMax - aMin;
const double sizeB = bMax - bMin;
// reject if one rectangle fully contains the other
if ((bMin <= aMin && bMax >= aMax) || (aMin <= bMin && aMax >= bMax))
return false;
// accept if overlap is at most 50% of the smaller dimension
return overlap <= std::min(sizeA, sizeB) * MAX_OVERLAP_RATIO;
};
for (auto const& w : m_windows) {
if (w == ignoreWindow || !w->m_workspace || !w->m_isMapped || w->isHidden() || (!w->isFullscreen() && w->m_isFloating) || !w->m_workspace->isVisible())
continue;
@ -1425,28 +1455,23 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks
double intersectLength = -1;
switch (dir) {
case 'l':
if (STICKS(POSA.x, POSB.x + SIZEB.x)) {
case Math::DIRECTION_LEFT:
if (isAdjacent(POSA.x, POSA.x + SIZEA.x, POSB.x, POSB.x + SIZEB.x))
intersectLength = std::max(0.0, std::min(POSA.y + SIZEA.y, POSB.y + SIZEB.y) - std::max(POSA.y, POSB.y));
}
break;
case 'r':
if (STICKS(POSA.x + SIZEA.x, POSB.x)) {
case Math::DIRECTION_RIGHT:
if (isAdjacent(POSB.x, POSB.x + SIZEB.x, POSA.x, POSA.x + SIZEA.x))
intersectLength = std::max(0.0, std::min(POSA.y + SIZEA.y, POSB.y + SIZEB.y) - std::max(POSA.y, POSB.y));
}
break;
case 't':
case 'u':
if (STICKS(POSA.y, POSB.y + SIZEB.y)) {
case Math::DIRECTION_UP:
if (isAdjacent(POSA.y, POSA.y + SIZEA.y, POSB.y, POSB.y + SIZEB.y))
intersectLength = std::max(0.0, std::min(POSA.x + SIZEA.x, POSB.x + SIZEB.x) - std::max(POSA.x, POSB.x));
}
break;
case 'b':
case 'd':
if (STICKS(POSA.y + SIZEA.y, POSB.y)) {
case Math::DIRECTION_DOWN:
if (isAdjacent(POSB.y, POSB.y + SIZEB.y, POSA.y, POSA.y + SIZEA.y))
intersectLength = std::max(0.0, std::min(POSA.x + SIZEA.x, POSB.x + SIZEB.x) - std::max(POSA.x, POSB.x));
}
break;
default: break;
}
if (*PMETHOD == 0 /* history */) {
@ -1475,12 +1500,8 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks
}
}
} else {
if (dir == 'u')
dir = 't';
if (dir == 'd')
dir = 'b';
static const std::unordered_map<char, Vector2D> VECTORS = {{'r', {1, 0}}, {'t', {0, -1}}, {'b', {0, 1}}, {'l', {-1, 0}}};
static const std::unordered_map<Math::eDirection, Vector2D> VECTORS = {
{Math::DIRECTION_RIGHT, {1, 0}}, {Math::DIRECTION_UP, {0, -1}}, {Math::DIRECTION_DOWN, {0, 1}}, {Math::DIRECTION_LEFT, {-1, 0}}};
//
auto vectorAngles = [](const Vector2D& a, const Vector2D& b) -> double {
@ -1632,38 +1653,28 @@ bool CCompositor::isPointOnReservedArea(const Vector2D& point, const PHLMONITOR
return VECNOTINRECT(point, box.x, box.y, box.x + box.w, box.y + box.h);
}
CBox CCompositor::calculateX11WorkArea() {
std::optional<CBox> CCompositor::calculateX11WorkArea() {
static auto PXWLFORCESCALEZERO = CConfigValue<Hyprlang::INT>("xwayland:force_zero_scaling");
CBox workbox = {0, 0, 0, 0};
bool firstMonitor = true;
// We more than likely won't be able to calculate one
// and even if we could this is minor
if (m_monitors.size() > 1 || m_monitors.empty())
return std::nullopt;
for (const auto& monitor : m_monitors) {
// we ignore monitor->m_position on purpose
CBox box = monitor->logicalBoxMinusReserved().translate(-monitor->m_position);
if ((*PXWLFORCESCALEZERO))
box.scale(monitor->m_scale);
const auto M = m_monitors.front();
if (firstMonitor) {
firstMonitor = false;
workbox = box;
} else {
// if this monitor creates a different workbox than previous monitor, we remove the _NET_WORKAREA property all together
if ((std::abs(box.x - workbox.x) > 3) || (std::abs(box.y - workbox.y) > 3) || (std::abs(box.w - workbox.w) > 3) || (std::abs(box.h - workbox.h) > 3)) {
workbox = {0, 0, 0, 0};
break;
}
}
}
// we ignore monitor->m_position on purpose
CBox box = M->logicalBoxMinusReserved().translate(-M->m_position);
if ((*PXWLFORCESCALEZERO))
box.scale(M->m_scale);
// returning 0, 0 will remove the _NET_WORKAREA property
return workbox;
return box.translate(M->m_xwaylandPosition);
}
PHLMONITOR CCompositor::getMonitorInDirection(const char& dir) {
PHLMONITOR CCompositor::getMonitorInDirection(Math::eDirection dir) {
return getMonitorInDirection(Desktop::focusState()->monitor(), dir);
}
PHLMONITOR CCompositor::getMonitorInDirection(PHLMONITOR pSourceMonitor, const char& dir) {
PHLMONITOR CCompositor::getMonitorInDirection(PHLMONITOR pSourceMonitor, Math::eDirection dir) {
if (!pSourceMonitor)
return nullptr;
@ -1680,7 +1691,7 @@ PHLMONITOR CCompositor::getMonitorInDirection(PHLMONITOR pSourceMonitor, const c
const auto POSB = m->m_position;
const auto SIZEB = m->m_size;
switch (dir) {
case 'l':
case Math::DIRECTION_LEFT:
if (STICKS(POSA.x, POSB.x + SIZEB.x)) {
const auto INTERSECTLEN = std::max(0.0, std::min(POSA.y + SIZEA.y, POSB.y + SIZEB.y) - std::max(POSA.y, POSB.y));
if (INTERSECTLEN > longestIntersect) {
@ -1689,7 +1700,7 @@ PHLMONITOR CCompositor::getMonitorInDirection(PHLMONITOR pSourceMonitor, const c
}
}
break;
case 'r':
case Math::DIRECTION_RIGHT:
if (STICKS(POSA.x + SIZEA.x, POSB.x)) {
const auto INTERSECTLEN = std::max(0.0, std::min(POSA.y + SIZEA.y, POSB.y + SIZEB.y) - std::max(POSA.y, POSB.y));
if (INTERSECTLEN > longestIntersect) {
@ -1698,8 +1709,7 @@ PHLMONITOR CCompositor::getMonitorInDirection(PHLMONITOR pSourceMonitor, const c
}
}
break;
case 't':
case 'u':
case Math::DIRECTION_UP:
if (STICKS(POSA.y, POSB.y + SIZEB.y)) {
const auto INTERSECTLEN = std::max(0.0, std::min(POSA.x + SIZEA.x, POSB.x + SIZEB.x) - std::max(POSA.x, POSB.x));
if (INTERSECTLEN > longestIntersect) {
@ -1708,8 +1718,7 @@ PHLMONITOR CCompositor::getMonitorInDirection(PHLMONITOR pSourceMonitor, const c
}
}
break;
case 'b':
case 'd':
case Math::DIRECTION_DOWN:
if (STICKS(POSA.y + SIZEA.y, POSB.y)) {
const auto INTERSECTLEN = std::max(0.0, std::min(POSA.x + SIZEA.x, POSB.x + SIZEB.x) - std::max(POSA.x, POSB.x));
if (INTERSECTLEN > longestIntersect) {
@ -1718,6 +1727,7 @@ PHLMONITOR CCompositor::getMonitorInDirection(PHLMONITOR pSourceMonitor, const c
}
}
break;
default: break;
}
}
@ -1773,7 +1783,7 @@ void CCompositor::swapActiveWorkspaces(PHLMONITOR pMonitorA, PHLMONITOR pMonitor
// additionally, move floating and fs windows manually
if (w->m_isFloating)
*w->m_realPosition = w->m_realPosition->goal() - pMonitorA->m_position + pMonitorB->m_position;
w->layoutTarget()->setPositionGlobal(w->layoutTarget()->position().translate(-pMonitorA->m_position + pMonitorB->m_position));
if (w->isFullscreen()) {
*w->m_realPosition = pMonitorB->m_position;
@ -1798,7 +1808,7 @@ void CCompositor::swapActiveWorkspaces(PHLMONITOR pMonitorA, PHLMONITOR pMonitor
// additionally, move floating and fs windows manually
if (w->m_isFloating)
*w->m_realPosition = w->m_realPosition->goal() - pMonitorB->m_position + pMonitorA->m_position;
w->layoutTarget()->setPositionGlobal(w->layoutTarget()->position().translate(-pMonitorB->m_position + pMonitorA->m_position));
if (w->isFullscreen()) {
*w->m_realPosition = pMonitorA->m_position;
@ -1812,8 +1822,11 @@ void CCompositor::swapActiveWorkspaces(PHLMONITOR pMonitorA, PHLMONITOR pMonitor
pMonitorA->m_activeWorkspace = PWORKSPACEB;
pMonitorB->m_activeWorkspace = PWORKSPACEA;
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(pMonitorA->m_id);
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(pMonitorB->m_id);
g_layoutManager->recalculateMonitor(pMonitorA);
g_layoutManager->recalculateMonitor(pMonitorB);
g_pHyprRenderer->damageMonitor(pMonitorB);
g_pHyprRenderer->damageMonitor(pMonitorA);
g_pDesktopAnimationManager->setFullscreenFadeAnimation(
PWORKSPACEB, PWORKSPACEB->m_hasFullscreenWindow ? CDesktopAnimationManager::ANIMATION_TYPE_IN : CDesktopAnimationManager::ANIMATION_TYPE_OUT);
@ -1825,29 +1838,30 @@ void CCompositor::swapActiveWorkspaces(PHLMONITOR pMonitorA, PHLMONITOR pMonitor
Desktop::focusState()->fullWindowFocus(
LASTWIN ? LASTWIN :
(g_pCompositor->vectorToWindowUnified(g_pInputManager->getMouseCoordsInternal(),
Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING)));
Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING)),
Desktop::FOCUS_REASON_DESKTOP_STATE_CHANGE);
const auto PNEWWORKSPACE = pMonitorA->m_id == Desktop::focusState()->monitor()->m_id ? PWORKSPACEB : PWORKSPACEA;
g_pEventManager->postEvent(SHyprIPCEvent{.event = "workspace", .data = PNEWWORKSPACE->m_name});
g_pEventManager->postEvent(SHyprIPCEvent{.event = "workspacev2", .data = std::format("{},{}", PNEWWORKSPACE->m_id, PNEWWORKSPACE->m_name)});
EMIT_HOOK_EVENT("workspace", PNEWWORKSPACE);
Event::bus()->m_events.workspace.active.emit(PNEWWORKSPACE);
}
// event
// events
g_pEventManager->postEvent(SHyprIPCEvent{.event = "moveworkspace", .data = PWORKSPACEA->m_name + "," + pMonitorB->m_name});
g_pEventManager->postEvent(SHyprIPCEvent{.event = "moveworkspacev2", .data = std::format("{},{},{}", PWORKSPACEA->m_id, PWORKSPACEA->m_name, pMonitorB->m_name)});
EMIT_HOOK_EVENT("moveWorkspace", (std::vector<std::any>{PWORKSPACEA, pMonitorB}));
g_pEventManager->postEvent(SHyprIPCEvent{.event = "moveworkspace", .data = PWORKSPACEB->m_name + "," + pMonitorA->m_name});
g_pEventManager->postEvent(SHyprIPCEvent{.event = "moveworkspacev2", .data = std::format("{},{},{}", PWORKSPACEB->m_id, PWORKSPACEB->m_name, pMonitorA->m_name)});
EMIT_HOOK_EVENT("moveWorkspace", (std::vector<std::any>{PWORKSPACEB, pMonitorA}));
Event::bus()->m_events.workspace.moveToMonitor.emit(PWORKSPACEA, pMonitorB);
Event::bus()->m_events.workspace.moveToMonitor.emit(PWORKSPACEB, pMonitorA);
}
PHLMONITOR CCompositor::getMonitorFromString(const std::string& name) {
if (name == "current")
return Desktop::focusState()->monitor();
else if (isDirection(name))
return getMonitorInDirection(name[0]);
return getMonitorInDirection(Math::fromChar(name[0]));
else if (name[0] == '+' || name[0] == '-') {
// relative
@ -1968,6 +1982,7 @@ void CCompositor::moveWorkspaceToMonitor(PHLWORKSPACE pWorkspace, PHLMONITOR pMo
// move the workspace
pWorkspace->m_monitor = pMonitor;
pWorkspace->m_space->recheckWorkArea();
pWorkspace->m_events.monitorChanged.emit();
for (auto const& w : m_windows) {
@ -1983,17 +1998,18 @@ void CCompositor::moveWorkspaceToMonitor(PHLWORKSPACE pWorkspace, PHLMONITOR pMo
if (w->m_isMapped && !w->isHidden()) {
if (POLDMON) {
if (w->m_isFloating)
*w->m_realPosition = w->m_realPosition->goal() - POLDMON->m_position + pMonitor->m_position;
w->layoutTarget()->setPositionGlobal(w->layoutTarget()->position().translate(-POLDMON->m_position + pMonitor->m_position));
if (w->isFullscreen()) {
*w->m_realPosition = pMonitor->m_position;
*w->m_realSize = pMonitor->m_size;
}
} else
*w->m_realPosition = Vector2D{
(pMonitor->m_size.x != 0) ? sc<int>(w->m_realPosition->goal().x) % sc<int>(pMonitor->m_size.x) : 0,
(pMonitor->m_size.y != 0) ? sc<int>(w->m_realPosition->goal().y) % sc<int>(pMonitor->m_size.y) : 0,
};
w->layoutTarget()->setPositionGlobal(CBox{Vector2D{
(pMonitor->m_size.x != 0) ? sc<int>(w->m_realPosition->goal().x) % sc<int>(pMonitor->m_size.x) : 0,
(pMonitor->m_size.y != 0) ? sc<int>(w->m_realPosition->goal().y) % sc<int>(pMonitor->m_size.y) : 0,
},
w->layoutTarget()->position().size()});
}
w->updateToplevel();
@ -2021,7 +2037,8 @@ void CCompositor::moveWorkspaceToMonitor(PHLWORKSPACE pWorkspace, PHLMONITOR pMo
pWorkspace->m_events.activeChanged.emit();
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(pMonitor->m_id);
g_layoutManager->recalculateMonitor(pMonitor);
g_pHyprRenderer->damageMonitor(pMonitor);
g_pDesktopAnimationManager->startAnimation(pWorkspace, CDesktopAnimationManager::ANIMATION_TYPE_IN, true, true);
pWorkspace->m_visible = true;
@ -2034,7 +2051,7 @@ void CCompositor::moveWorkspaceToMonitor(PHLWORKSPACE pWorkspace, PHLMONITOR pMo
// finalize
if (POLDMON) {
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(POLDMON->m_id);
g_layoutManager->recalculateMonitor(POLDMON);
if (valid(POLDMON->m_activeWorkspace))
g_pDesktopAnimationManager->setFullscreenFadeAnimation(POLDMON->m_activeWorkspace,
POLDMON->m_activeWorkspace->m_hasFullscreenWindow ? CDesktopAnimationManager::ANIMATION_TYPE_IN :
@ -2049,7 +2066,8 @@ void CCompositor::moveWorkspaceToMonitor(PHLWORKSPACE pWorkspace, PHLMONITOR pMo
// event
g_pEventManager->postEvent(SHyprIPCEvent{.event = "moveworkspace", .data = pWorkspace->m_name + "," + pMonitor->m_name});
g_pEventManager->postEvent(SHyprIPCEvent{.event = "moveworkspacev2", .data = std::format("{},{},{}", pWorkspace->m_id, pWorkspace->m_name, pMonitor->m_name)});
EMIT_HOOK_EVENT("moveWorkspace", (std::vector<std::any>{pWorkspace, pMonitor}));
Event::bus()->m_events.workspace.moveToMonitor.emit(pWorkspace, pMonitor);
}
bool CCompositor::workspaceIDOutOfBounds(const WORKSPACEID& id) {
@ -2132,24 +2150,25 @@ void CCompositor::setWindowFullscreenState(const PHLWINDOW PWINDOW, Desktop::Vie
PWINDOW->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_FULLSCREEN | Desktop::Rule::RULE_PROP_FULLSCREENSTATE_CLIENT |
Desktop::Rule::RULE_PROP_FULLSCREENSTATE_INTERNAL | Desktop::Rule::RULE_PROP_ON_WORKSPACE);
PWINDOW->updateDecorationValues();
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(PWINDOW->monitorID());
g_layoutManager->recalculateMonitor(PMONITOR);
return;
}
g_pLayoutManager->getCurrentLayout()->fullscreenRequestForWindow(PWINDOW, CURRENT_EFFECTIVE_MODE, EFFECTIVE_MODE);
PWORKSPACE->m_fullscreenMode = EFFECTIVE_MODE;
PWORKSPACE->m_hasFullscreenWindow = EFFECTIVE_MODE != FSMODE_NONE;
g_layoutManager->fullscreenRequestForTarget(PWINDOW->layoutTarget(), CURRENT_EFFECTIVE_MODE, EFFECTIVE_MODE);
PWINDOW->m_fullscreenState.internal = state.internal;
PWORKSPACE->m_fullscreenMode = EFFECTIVE_MODE;
PWORKSPACE->m_hasFullscreenWindow = EFFECTIVE_MODE != FSMODE_NONE;
g_pEventManager->postEvent(SHyprIPCEvent{.event = "fullscreen", .data = std::to_string(sc<int>(EFFECTIVE_MODE) != FSMODE_NONE)});
EMIT_HOOK_EVENT("fullscreen", PWINDOW);
Event::bus()->m_events.window.fullscreen.emit(PWINDOW);
PWINDOW->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_FULLSCREEN | Desktop::Rule::RULE_PROP_FULLSCREENSTATE_CLIENT |
Desktop::Rule::RULE_PROP_FULLSCREENSTATE_INTERNAL | Desktop::Rule::RULE_PROP_ON_WORKSPACE);
PWINDOW->updateDecorationValues();
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(PWINDOW->monitorID());
g_layoutManager->recalculateMonitor(PMONITOR);
// make all windows and layers on the same workspace under the fullscreen window
for (auto const& w : m_windows) {
@ -2262,7 +2281,7 @@ PHLWINDOW CCompositor::getWindowByRegex(const std::string& regexp_) {
}
for (auto const& w : g_pCompositor->m_windows) {
if (!w->m_isMapped || (w->isHidden() && !g_pLayoutManager->getCurrentLayout()->isWindowReachable(w)))
if (!w->m_isMapped)
continue;
switch (mode) {
@ -2567,59 +2586,30 @@ void CCompositor::moveWindowToWorkspaceSafe(PHLWINDOW pWindow, PHLWORKSPACE pWor
setWindowFullscreenInternal(pWindow, FSMODE_NONE);
const PHLWINDOW pFirstWindowOnWorkspace = pWorkspace->getFirstWindow();
const int visibleWindowsOnWorkspace = pWorkspace->getWindows(std::nullopt, true);
const int visibleWindowsOnWorkspace = pWorkspace->getWindows(true, std::nullopt, true);
const auto POSTOMON = pWindow->m_realPosition->goal() - (pWindow->m_monitor ? pWindow->m_monitor->m_position : Vector2D{});
const auto PWORKSPACEMONITOR = pWorkspace->m_monitor.lock();
if (!pWindow->m_isFloating)
g_pLayoutManager->getCurrentLayout()->onWindowRemovedTiling(pWindow);
pWindow->moveToWorkspace(pWorkspace);
pWindow->m_monitor = pWorkspace->m_monitor;
static auto PGROUPONMOVETOWORKSPACE = CConfigValue<Hyprlang::INT>("group:group_on_movetoworkspace");
if (*PGROUPONMOVETOWORKSPACE && visibleWindowsOnWorkspace == 1 && pFirstWindowOnWorkspace && pFirstWindowOnWorkspace != pWindow &&
pFirstWindowOnWorkspace->m_groupData.pNextWindow.lock() && pWindow->canBeGroupedInto(pFirstWindowOnWorkspace)) {
pWindow->m_isFloating = pFirstWindowOnWorkspace->m_isFloating; // match the floating state. Needed to group tiled into floated and vice versa.
if (!pWindow->m_groupData.pNextWindow.expired()) {
PHLWINDOW next = pWindow->m_groupData.pNextWindow.lock();
while (next != pWindow) {
next->m_isFloating = pFirstWindowOnWorkspace->m_isFloating; // match the floating state of group members
next = next->m_groupData.pNextWindow.lock();
}
}
static auto USECURRPOS = CConfigValue<Hyprlang::INT>("group:insert_after_current");
(*USECURRPOS ? pFirstWindowOnWorkspace : pFirstWindowOnWorkspace->getGroupTail())->insertWindowToGroup(pWindow);
pFirstWindowOnWorkspace->setGroupCurrent(pWindow);
pWindow->updateWindowDecos();
g_pLayoutManager->getCurrentLayout()->recalculateWindow(pWindow);
if (!pWindow->getDecorationByType(DECORATION_GROUPBAR))
pWindow->addWindowDeco(makeUnique<CHyprGroupBarDecoration>(pWindow));
if (*PGROUPONMOVETOWORKSPACE && visibleWindowsOnWorkspace == 1 && pFirstWindowOnWorkspace && pFirstWindowOnWorkspace != pWindow && pFirstWindowOnWorkspace->m_group &&
pWindow->canBeGroupedInto(pFirstWindowOnWorkspace->m_group)) {
pFirstWindowOnWorkspace->m_group->add(pWindow);
} else {
if (!pWindow->m_isFloating)
g_pLayoutManager->getCurrentLayout()->onWindowCreatedTiling(pWindow);
if (pWindow->m_isFloating)
*pWindow->m_realPosition = POSTOMON + PWORKSPACEMONITOR->m_position;
pWindow->layoutTarget()->setPositionGlobal(CBox{POSTOMON + PWORKSPACEMONITOR->m_position, pWindow->layoutTarget()->position().size()});
}
pWindow->updateToplevel();
pWindow->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_ON_WORKSPACE);
pWindow->uncacheWindowDecos();
pWindow->updateGroupOutputs();
if (!pWindow->m_groupData.pNextWindow.expired()) {
PHLWINDOW next = pWindow->m_groupData.pNextWindow.lock();
while (next != pWindow) {
next->updateToplevel();
next = next->m_groupData.pNextWindow.lock();
}
}
if (pWindow->m_group)
pWindow->m_group->updateWorkspace(pWorkspace);
g_layoutManager->newTarget(pWindow->layoutTarget(), pWorkspace->m_space);
if (FULLSCREEN)
setWindowFullscreenInternal(pWindow, FULLSCREENMODE);
@ -2788,12 +2778,17 @@ void CCompositor::arrangeMonitors() {
}
PROTO::xdgOutput->updateAllOutputs();
Event::bus()->m_events.monitor.layoutChanged.emit();
#ifndef NO_XWAYLAND
CBox box = g_pCompositor->calculateX11WorkArea();
if (!g_pXWayland || !g_pXWayland->m_wm)
return;
g_pXWayland->m_wm->updateWorkArea(box.x, box.y, box.w, box.h);
const auto box = g_pCompositor->calculateX11WorkArea();
if (g_pXWayland && g_pXWayland->m_wm) {
if (box)
g_pXWayland->m_wm->updateWorkArea(box->x, box->y, box->w, box->h);
else
g_pXWayland->m_wm->updateWorkArea(0, 0, 0, 0);
}
#endif
}
@ -2920,7 +2915,7 @@ void CCompositor::onNewMonitor(SP<Aquamarine::IOutput> output) {
PNEWMONITOR->m_id = FALLBACK ? MONITOR_INVALID : g_pCompositor->getNextAvailableMonitorID(output->name);
PNEWMONITOR->m_isUnsafeFallback = FALLBACK;
EMIT_HOOK_EVENT("newMonitor", PNEWMONITOR);
Event::bus()->m_events.monitor.newMon.emit(PNEWMONITOR);
if (!FALLBACK)
PNEWMONITOR->onConnect(false);
@ -2973,13 +2968,16 @@ PImageDescription CCompositor::getHDRImageDescription() {
}
return m_monitors.size() == 1 && m_monitors[0]->m_output && m_monitors[0]->m_output->parsedEDID.hdrMetadata.has_value() ?
CImageDescription::from(SImageDescription{.transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_ST2084_PQ,
.primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_BT2020,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_BT2020),
.luminances = {.min = m_monitors[0]->m_output->parsedEDID.hdrMetadata->desiredContentMinLuminance,
.max = m_monitors[0]->m_output->parsedEDID.hdrMetadata->desiredContentMaxLuminance,
.reference = m_monitors[0]->m_output->parsedEDID.hdrMetadata->desiredMaxFrameAverageLuminance}}) :
CImageDescription::from(SImageDescription{
.transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_ST2084_PQ,
.primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_BT2020,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_BT2020),
.masteringPrimaries = m_monitors[0]->getMasteringPrimaries(),
.luminances = {.min = m_monitors[0]->minLuminance(HDR_MIN_LUMINANCE), .max = m_monitors[0]->maxLuminance(HDR_MAX_LUMINANCE), .reference = HDR_REF_LUMINANCE},
.masteringLuminances = m_monitors[0]->getMasteringLuminances(),
.maxCLL = m_monitors[0]->maxCLL(),
.maxFALL = m_monitors[0]->maxFALL()}) :
DEFAULT_HDR_IMAGE_DESCRIPTION;
}
@ -3076,6 +3074,27 @@ void CCompositor::ensurePersistentWorkspacesPresent(const std::vector<SWorkspace
}
}
void CCompositor::ensureWorkspacesOnAssignedMonitors() {
for (auto const& ws : getWorkspacesCopy()) {
if (!valid(ws) || ws->m_isSpecialWorkspace)
continue;
const auto RULE = g_pConfigManager->getWorkspaceRuleFor(ws);
if (RULE.monitor.empty())
continue;
const auto PMONITOR = getMonitorFromString(RULE.monitor);
if (!PMONITOR)
continue;
if (ws->m_monitor == PMONITOR)
continue;
Log::logger->log(Log::DEBUG, "ensureWorkspacesOnAssignedMonitors: moving workspace {} to {}", ws->m_name, PMONITOR->m_name);
moveWorkspaceToMonitor(ws, PMONITOR, true);
}
}
std::optional<unsigned int> CCompositor::getVTNr() {
if (!m_aqBackend->hasSession())
return std::nullopt;
@ -3096,3 +3115,7 @@ std::optional<unsigned int> CCompositor::getVTNr() {
return ttynum;
}
bool CCompositor::isVRRActiveOnAnyMonitor() const {
return std::ranges::any_of(m_monitors, [](const PHLMONITOR& m) { return m->m_vrrActive; });
}

View file

@ -4,11 +4,12 @@
#include <ranges>
#include "helpers/math/Direction.hpp"
#include "managers/XWaylandManager.hpp"
#include "managers/KeybindManager.hpp"
#include "managers/SessionLockManager.hpp"
#include "desktop/view/Window.hpp"
#include "protocols/types/ColorManagement.hpp"
#include "helpers/cm/ColorManagement.hpp"
#include <aquamarine/backend/Backend.hpp>
#include <aquamarine/output/Output.hpp>
@ -113,16 +114,16 @@ class CCompositor {
bool isWindowActive(PHLWINDOW);
void changeWindowZOrder(PHLWINDOW, bool);
void cleanupFadingOut(const MONITORID& monid);
PHLWINDOW getWindowInDirection(PHLWINDOW, char);
PHLWINDOW getWindowInDirection(const CBox& box, PHLWORKSPACE pWorkspace, char dir, PHLWINDOW ignoreWindow = nullptr, bool useVectorAngles = false);
PHLWINDOW getWindowInDirection(PHLWINDOW, Math::eDirection);
PHLWINDOW getWindowInDirection(const CBox& box, PHLWORKSPACE pWorkspace, Math::eDirection dir, PHLWINDOW ignoreWindow = nullptr, bool useVectorAngles = false);
PHLWINDOW getWindowCycle(PHLWINDOW cur, bool focusableOnly = false, std::optional<bool> floating = std::nullopt, bool visible = false, bool prev = false);
PHLWINDOW getWindowCycleHist(PHLWINDOWREF cur, bool focusableOnly = false, std::optional<bool> floating = std::nullopt, bool visible = false, bool next = false);
WORKSPACEID getNextAvailableNamedWorkspace();
bool isPointOnAnyMonitor(const Vector2D&);
bool isPointOnReservedArea(const Vector2D& point, const PHLMONITOR monitor = nullptr);
CBox calculateX11WorkArea();
PHLMONITOR getMonitorInDirection(const char&);
PHLMONITOR getMonitorInDirection(PHLMONITOR, const char&);
std::optional<CBox> calculateX11WorkArea();
PHLMONITOR getMonitorInDirection(Math::eDirection);
PHLMONITOR getMonitorInDirection(PHLMONITOR, Math::eDirection);
void updateAllWindowsAnimatedDecorationValues();
MONITORID getNextAvailableMonitorID(std::string const& name);
void moveWorkspaceToMonitor(PHLWORKSPACE, PHLMONITOR, bool noWarpCursor = false);
@ -160,7 +161,9 @@ class CCompositor {
void updateSuspendedStates();
void onNewMonitor(SP<Aquamarine::IOutput> output);
void ensurePersistentWorkspacesPresent(const std::vector<SWorkspaceRule>& rules, PHLWORKSPACE pWorkspace = nullptr);
void ensureWorkspacesOnAssignedMonitors();
std::optional<unsigned int> getVTNr();
bool isVRRActiveOnAnyMonitor() const;
NColorManagement::PImageDescription getPreferredImageDescription();
NColorManagement::PImageDescription getHDRImageDescription();

View file

@ -38,10 +38,6 @@ enum eInputType : uint8_t {
INPUT_TYPE_MOTION
};
struct SCallbackInfo {
bool cancelled = false; /* on cancellable events, will cancel the event. */
};
enum eHyprCtlOutputFormat : uint8_t {
FORMAT_NORMAL = 0,
FORMAT_JSON
@ -62,5 +58,3 @@ struct SDispatchResult {
using WINDOWID = int64_t;
using MONITORID = int64_t;
using WORKSPACEID = int64_t;
using HOOK_CALLBACK_FN = std::function<void(void*, SCallbackInfo&, std::any)>;

View file

@ -1115,6 +1115,12 @@ inline static const std::vector<SConfigOptionDescription> CONFIG_OPTIONS = {
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SRangeData{0, -20, 20},
},
SConfigOptionDescription{
.value = "group:groupbar:text_padding",
.description = "set horizontal padding for a text",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SRangeData{0, 0, 22},
},
SConfigOptionDescription{
.value = "group:groupbar:blur",
.description = "enable background blur for groupbars",
@ -1562,10 +1568,28 @@ inline static const std::vector<SConfigOptionDescription> CONFIG_OPTIONS = {
},
SConfigOptionDescription{
.value = "render:cm_sdr_eotf",
.description = "Default transfer function for displaying SDR apps. 0 - Use default value (Gamma 2.2), 1 - Treat unspecified as Gamma 2.2, 2 - Treat "
"unspecified and sRGB as Gamma 2.2, 3 - Treat unspecified as sRGB",
.type = CONFIG_OPTION_CHOICE,
.data = SConfigOptionDescription::SChoiceData{0, "default,gamma22,gamma22force,srgb"},
.description = "Default transfer function for displaying SDR apps. default - Use default value (Gamma 2.2), gamma22 - Treat unspecified as Gamma 2.2, gamma22force - Treat "
"unspecified and sRGB as Gamma 2.2, srgb - Treat unspecified as sRGB",
.type = CONFIG_OPTION_STRING_SHORT,
.data = SConfigOptionDescription::SStringData{"default"},
},
SConfigOptionDescription{
.value = "render:commit_timing_enabled",
.description = "Enable commit timing proto. Requires restart",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{true},
},
SConfigOptionDescription{
.value = "render:icc_vcgt_enabled",
.description = "Enable sending VCGT ramps to KMS with ICC profiles",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{true},
},
{
.value = "render:use_shader_blur_blend",
.description = "Use experimental blurred bg blending",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{false},
},
/*
@ -1690,9 +1714,9 @@ inline static const std::vector<SConfigOptionDescription> CONFIG_OPTIONS = {
},
SConfigOptionDescription{
.value = "cursor:use_cpu_buffer",
.description = "Makes HW cursors use a CPU buffer. Required on Nvidia to have HW cursors. Experimental",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{false},
.description = "Makes HW cursors use a CPU buffer. Required on Nvidia to have HW cursors. 0 - off, 1 - on, 2 - auto (nvidia only)",
.type = CONFIG_OPTION_INT,
.data = SConfigOptionDescription::SRangeData{.value = 2, .min = 0, .max = 2},
},
SConfigOptionDescription{
.value = "cursor:sync_gsettings_theme",
@ -1746,6 +1770,12 @@ inline static const std::vector<SConfigOptionDescription> CONFIG_OPTIONS = {
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{false},
},
SConfigOptionDescription{
.value = "debug:gl_debugging",
.description = "enable OpenGL debugging and error checking, they hurt performance.",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{false},
},
SConfigOptionDescription{
.value = "debug:disable_logs",
.description = "disable logging to a file",
@ -1824,6 +1854,46 @@ inline static const std::vector<SConfigOptionDescription> CONFIG_OPTIONS = {
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{false},
},
SConfigOptionDescription{
.value = "debug:ds_handle_same_buffer",
.description = "Special case for DS with unmodified buffer",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{true},
},
SConfigOptionDescription{
.value = "debug:ds_handle_same_buffer_fifo",
.description = "Special case for DS with unmodified buffer unlocks fifo",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{true},
},
SConfigOptionDescription{
.value = "debug:fifo_pending_workaround",
.description = "Fifo workaround for empty pending list",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{false},
},
SConfigOptionDescription{
.value = "debug:render_solitary_wo_damage",
.description = "Render solitary window with empty damage",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{false},
},
/*
* layout:
*/
SConfigOptionDescription{
.value = "layout:single_window_aspect_ratio",
.description = "If specified, whenever only a single window is open, it will be coerced into the specified aspect ratio. Ignored if the y-value is zero.",
.type = CONFIG_OPTION_VECTOR,
.data = SConfigOptionDescription::SVectorData{{0, 0}, {0, 0}, {1000., 1000.}},
},
SConfigOptionDescription{
.value = "layout:single_window_aspect_ratio_tolerance",
.description = "Minimum distance for single_window_aspect_ratio to take effect, in fractions of the monitor's size.",
.type = CONFIG_OPTION_FLOAT,
.data = SConfigOptionDescription::SFloatData{0.1f, 0.f, 1.f},
},
/*
* dwindle:
@ -1904,18 +1974,6 @@ inline static const std::vector<SConfigOptionDescription> CONFIG_OPTIONS = {
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{false},
},
SConfigOptionDescription{
.value = "dwindle:single_window_aspect_ratio",
.description = "If specified, whenever only a single window is open, it will be coerced into the specified aspect ratio. Ignored if the y-value is zero.",
.type = CONFIG_OPTION_VECTOR,
.data = SConfigOptionDescription::SVectorData{{0, 0}, {0, 0}, {1000., 1000.}},
},
SConfigOptionDescription{
.value = "dwindle:single_window_aspect_ratio_tolerance",
.description = "Minimum distance for single_window_aspect_ratio to take effect, in fractions of the monitor's size.",
.type = CONFIG_OPTION_FLOAT,
.data = SConfigOptionDescription::SFloatData{0.1f, 0.f, 1.f},
},
/*
* master:
@ -2001,6 +2059,65 @@ inline static const std::vector<SConfigOptionDescription> CONFIG_OPTIONS = {
.data = SConfigOptionDescription::SBoolData{false},
},
/*
* scrolling:
*/
SConfigOptionDescription{
.value = "scrolling:fullscreen_on_one_column",
.description = "when enabled, a single column on a workspace will always span the entire screen.",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{true},
},
SConfigOptionDescription{
.value = "scrolling:column_width",
.description = "the default width of a column, [0.1 - 1.0].",
.type = CONFIG_OPTION_FLOAT,
.data = SConfigOptionDescription::SFloatData{.value = 0.5, .min = 0.1, .max = 1.0},
},
SConfigOptionDescription{
.value = "scrolling:focus_fit_method",
.description = "When a column is focused, what method should be used to bring it into view",
.type = CONFIG_OPTION_CHOICE,
.data = SConfigOptionDescription::SChoiceData{.firstIndex = 0, .choices = "center,fit"},
},
SConfigOptionDescription{
.value = "scrolling:follow_focus",
.description = "when a window is focused, should the layout move to bring it into view automatically",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{.value = true},
},
SConfigOptionDescription{
.value = "scrolling:follow_min_visible",
.description = "when a window is focused, require that at least a given fraction of it is visible for focus to follow",
.type = CONFIG_OPTION_FLOAT,
.data = SConfigOptionDescription::SFloatData{.value = 0.4, .min = 0.0, .max = 1.0},
},
SConfigOptionDescription{
.value = "scrolling:explicit_column_widths",
.description = "A comma-separated list of preconfigured widths for colresize +conf/-conf",
.type = CONFIG_OPTION_STRING_SHORT,
.data = SConfigOptionDescription::SStringData{"0.333, 0.5, 0.667, 1.0"},
},
SConfigOptionDescription{
.value = "scrolling:direction",
.description = "Direction in which new windows appear and the layout scrolls",
.type = CONFIG_OPTION_CHOICE,
.data = SConfigOptionDescription::SChoiceData{.firstIndex = 0, .choices = "right,left,down,up"},
},
SConfigOptionDescription{
.value = "scrolling:wrap_focus",
.description = "Determines if column focus wraps around when going before the first column or past the last column",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{.value = true},
},
SConfigOptionDescription{
.value = "scrolling:wrap_swapcol",
.description = "Determines if column movement wraps around when moving to before the first column or past the last column",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{.value = true},
},
/*
* Quirks
*/
@ -2011,5 +2128,11 @@ inline static const std::vector<SConfigOptionDescription> CONFIG_OPTIONS = {
.type = CONFIG_OPTION_INT,
.data = SConfigOptionDescription::SRangeData{.value = 0, .min = 0, .max = 2},
},
SConfigOptionDescription{
.value = "quirks:skip_non_kms_dmabuf_formats",
.description = "Do not report dmabuf formats which cannot be imported into KMS",
.type = CONFIG_OPTION_BOOL,
.data = SConfigOptionDescription::SBoolData{true},
},
};

View file

@ -18,13 +18,14 @@
#include "../desktop/rule/layerRule/LayerRule.hpp"
#include "../debug/HyprCtl.hpp"
#include "../desktop/state/FocusState.hpp"
#include "../layout/space/Space.hpp"
#include "../layout/supplementary/WorkspaceAlgoMatcher.hpp"
#include "defaultConfig.hpp"
#include "../render/Renderer.hpp"
#include "../hyprerror/HyprError.hpp"
#include "../managers/input/InputManager.hpp"
#include "../managers/eventLoop/EventLoopManager.hpp"
#include "../managers/LayoutManager.hpp"
#include "../managers/EventManager.hpp"
#include "../managers/permissions/DynamicPermissionManager.hpp"
#include "../debug/HyprNotificationOverlay.hpp"
@ -39,8 +40,10 @@
#include "../managers/input/trackpad/gestures/CloseGesture.hpp"
#include "../managers/input/trackpad/gestures/FloatGesture.hpp"
#include "../managers/input/trackpad/gestures/FullscreenGesture.hpp"
#include "../managers/input/trackpad/gestures/CursorZoomGesture.hpp"
#include "../event/EventBus.hpp"
#include "../managers/HookSystemManager.hpp"
#include "../protocols/types/ContentType.hpp"
#include <cstddef>
#include <cstdint>
@ -399,6 +402,12 @@ static Hyprlang::CParseResult handleWindowrule(const char* c, const char* v) {
return result;
}
static Hyprlang::CParseResult handleWindowrulev2(const char* c, const char* v) {
Hyprlang::CParseResult res;
res.setError("windowrulev2 is deprecated. Correct syntax can be found on the wiki.");
return res;
}
static Hyprlang::CParseResult handleLayerrule(const char* c, const char* v) {
const std::string VALUE = v;
const std::string COMMAND = c;
@ -411,6 +420,12 @@ static Hyprlang::CParseResult handleLayerrule(const char* c, const char* v) {
return result;
}
static Hyprlang::CParseResult handleLayerrulev2(const char* c, const char* v) {
Hyprlang::CParseResult res;
res.setError("layerrulev2 doesn't exist. Correct syntax can be found on the wiki.");
return res;
}
void CConfigManager::registerConfigVar(const char* name, const Hyprlang::INT& val) {
m_configValueNumber++;
m_config->addConfigValue(name, val);
@ -541,12 +556,14 @@ CConfigManager::CConfigManager() {
registerConfigVar("group:groupbar:gaps_in", Hyprlang::INT{2});
registerConfigVar("group:groupbar:keep_upper_gap", Hyprlang::INT{1});
registerConfigVar("group:groupbar:text_offset", Hyprlang::INT{0});
registerConfigVar("group:groupbar:text_padding", Hyprlang::INT{0});
registerConfigVar("group:groupbar:blur", Hyprlang::INT{0});
registerConfigVar("debug:log_damage", Hyprlang::INT{0});
registerConfigVar("debug:overlay", Hyprlang::INT{0});
registerConfigVar("debug:damage_blink", Hyprlang::INT{0});
registerConfigVar("debug:pass", Hyprlang::INT{0});
registerConfigVar("debug:gl_debugging", Hyprlang::INT{0});
registerConfigVar("debug:disable_logs", Hyprlang::INT{1});
registerConfigVar("debug:disable_time", Hyprlang::INT{1});
registerConfigVar("debug:enable_stdout_logs", Hyprlang::INT{0});
@ -558,6 +575,10 @@ CConfigManager::CConfigManager() {
registerConfigVar("debug:disable_scale_checks", Hyprlang::INT{0});
registerConfigVar("debug:colored_stdout_logs", Hyprlang::INT{1});
registerConfigVar("debug:full_cm_proto", Hyprlang::INT{0});
registerConfigVar("debug:ds_handle_same_buffer", Hyprlang::INT{1});
registerConfigVar("debug:ds_handle_same_buffer_fifo", Hyprlang::INT{1});
registerConfigVar("debug:fifo_pending_workaround", Hyprlang::INT{0});
registerConfigVar("debug:render_solitary_wo_damage", Hyprlang::INT{0});
registerConfigVar("decoration:rounding", Hyprlang::INT{0});
registerConfigVar("decoration:rounding_power", {2.F});
@ -597,6 +618,9 @@ CConfigManager::CConfigManager() {
registerConfigVar("decoration:screen_shader", {STRVAL_EMPTY});
registerConfigVar("decoration:border_part_of_window", Hyprlang::INT{1});
registerConfigVar("layout:single_window_aspect_ratio", Hyprlang::VEC2{0, 0});
registerConfigVar("layout:single_window_aspect_ratio_tolerance", {0.1f});
registerConfigVar("dwindle:pseudotile", Hyprlang::INT{0});
registerConfigVar("dwindle:force_split", Hyprlang::INT{0});
registerConfigVar("dwindle:permanent_direction_override", Hyprlang::INT{0});
@ -609,8 +633,6 @@ CConfigManager::CConfigManager() {
registerConfigVar("dwindle:smart_split", Hyprlang::INT{0});
registerConfigVar("dwindle:smart_resizing", Hyprlang::INT{1});
registerConfigVar("dwindle:precise_mouse_move", Hyprlang::INT{0});
registerConfigVar("dwindle:single_window_aspect_ratio", Hyprlang::VEC2{0, 0});
registerConfigVar("dwindle:single_window_aspect_ratio_tolerance", {0.1f});
registerConfigVar("master:special_scale_factor", {1.f});
registerConfigVar("master:mfact", {0.55f});
@ -626,6 +648,16 @@ CConfigManager::CConfigManager() {
registerConfigVar("master:drop_at_cursor", Hyprlang::INT{1});
registerConfigVar("master:always_keep_position", Hyprlang::INT{0});
registerConfigVar("scrolling:fullscreen_on_one_column", Hyprlang::INT{1});
registerConfigVar("scrolling:column_width", Hyprlang::FLOAT{0.5F});
registerConfigVar("scrolling:focus_fit_method", Hyprlang::INT{1});
registerConfigVar("scrolling:follow_focus", Hyprlang::INT{1});
registerConfigVar("scrolling:follow_min_visible", Hyprlang::FLOAT{0.4});
registerConfigVar("scrolling:explicit_column_widths", Hyprlang::STRING{"0.333, 0.5, 0.667, 1.0"});
registerConfigVar("scrolling:direction", Hyprlang::STRING{"right"});
registerConfigVar("scrolling:wrap_focus", Hyprlang::INT{1});
registerConfigVar("scrolling:wrap_swapcol", Hyprlang::INT{1});
registerConfigVar("animations:enabled", Hyprlang::INT{1});
registerConfigVar("animations:workspace_wraparound", Hyprlang::INT{0});
@ -696,9 +728,9 @@ CConfigManager::CConfigManager() {
registerConfigVar("binds:movefocus_cycles_fullscreen", Hyprlang::INT{0});
registerConfigVar("binds:movefocus_cycles_groupfirst", Hyprlang::INT{0});
registerConfigVar("binds:disable_keybind_grabbing", Hyprlang::INT{0});
registerConfigVar("binds:window_direction_monitor_fallback", Hyprlang::INT{1});
registerConfigVar("binds:allow_pin_fullscreen", Hyprlang::INT{0});
registerConfigVar("binds:drag_threshold", Hyprlang::INT{0});
registerConfigVar("binds:window_direction_monitor_fallback", Hyprlang::INT{1});
registerConfigVar("gestures:workspace_swipe_distance", Hyprlang::INT{300});
registerConfigVar("gestures:workspace_swipe_invert", Hyprlang::INT{1});
@ -765,13 +797,17 @@ CConfigManager::CConfigManager() {
registerConfigVar("render:cm_auto_hdr", Hyprlang::INT{1});
registerConfigVar("render:new_render_scheduling", Hyprlang::INT{0});
registerConfigVar("render:non_shader_cm", Hyprlang::INT{3});
registerConfigVar("render:cm_sdr_eotf", Hyprlang::INT{0});
registerConfigVar("render:cm_sdr_eotf", {"default"});
registerConfigVar("render:commit_timing_enabled", Hyprlang::INT{1});
registerConfigVar("render:icc_vcgt_enabled", Hyprlang::INT{1});
registerConfigVar("render:use_shader_blur_blend", Hyprlang::INT{0});
registerConfigVar("ecosystem:no_update_news", Hyprlang::INT{0});
registerConfigVar("ecosystem:no_donation_nag", Hyprlang::INT{0});
registerConfigVar("ecosystem:enforce_permissions", Hyprlang::INT{0});
registerConfigVar("quirks:prefer_hdr", Hyprlang::INT{0});
registerConfigVar("quirks:skip_non_kms_dmabuf_formats", Hyprlang::INT{0});
// devices
m_config->addSpecialCategory("device", {"name"});
@ -827,7 +863,7 @@ CConfigManager::CConfigManager() {
m_config->addSpecialConfigValue("monitorv2", "mirror", {STRVAL_EMPTY});
m_config->addSpecialConfigValue("monitorv2", "bitdepth", {STRVAL_EMPTY}); // TODO use correct type
m_config->addSpecialConfigValue("monitorv2", "cm", {"auto"});
m_config->addSpecialConfigValue("monitorv2", "sdr_eotf", Hyprlang::INT{0});
m_config->addSpecialConfigValue("monitorv2", "sdr_eotf", {"default"});
m_config->addSpecialConfigValue("monitorv2", "sdrbrightness", Hyprlang::FLOAT{1.0});
m_config->addSpecialConfigValue("monitorv2", "sdrsaturation", Hyprlang::FLOAT{1.0});
m_config->addSpecialConfigValue("monitorv2", "vrr", Hyprlang::INT{0});
@ -839,6 +875,7 @@ CConfigManager::CConfigManager() {
m_config->addSpecialConfigValue("monitorv2", "min_luminance", Hyprlang::FLOAT{-1.0});
m_config->addSpecialConfigValue("monitorv2", "max_luminance", Hyprlang::INT{-1});
m_config->addSpecialConfigValue("monitorv2", "max_avg_luminance", Hyprlang::INT{-1});
m_config->addSpecialConfigValue("monitorv2", "icc", Hyprlang::STRING{""});
// windowrule v3
m_config->addSpecialCategory("windowrule", {.key = "name"});
@ -868,9 +905,13 @@ CConfigManager::CConfigManager() {
m_config->registerHandler(&::handleSubmap, "submap", {false});
m_config->registerHandler(&::handlePlugin, "plugin", {false});
m_config->registerHandler(&::handlePermission, "permission", {false});
m_config->registerHandler(&::handleGesture, "gesture", {false});
m_config->registerHandler(&::handleGesture, "gesture", {true});
m_config->registerHandler(&::handleEnv, "env", {true});
// windowrulev2 and layerrulev2 errors
m_config->registerHandler(&::handleWindowrulev2, "windowrulev2", {false});
m_config->registerHandler(&::handleLayerrulev2, "layerrulev2", {false});
// pluginza
m_config->addSpecialCategory("plugin", {nullptr, true});
@ -1031,7 +1072,7 @@ static void clearHlVersionVars() {
}
void CConfigManager::reload() {
EMIT_HOOK_EVENT("preConfigReload", nullptr);
Event::bus()->m_events.config.preReload.emit();
setDefaultAnimationVars();
resetHLConfig();
m_configCurrentPath = getMainConfigPath();
@ -1173,8 +1214,18 @@ std::optional<std::string> CConfigManager::handleMonitorv2(const std::string& ou
if (VAL && VAL->m_bSetByUser)
parser.parseCM(std::any_cast<Hyprlang::STRING>(VAL->getValue()));
VAL = m_config->getSpecialConfigValuePtr("monitorv2", "sdr_eotf", output.c_str());
if (VAL && VAL->m_bSetByUser)
parser.rule().sdrEotf = std::any_cast<Hyprlang::INT>(VAL->getValue());
if (VAL && VAL->m_bSetByUser) {
const std::string value = std::any_cast<Hyprlang::STRING>(VAL->getValue());
// remap legacy
if (value == "0")
parser.rule().sdrEotf = NTransferFunction::TF_AUTO;
else if (value == "1")
parser.rule().sdrEotf = NTransferFunction::TF_SRGB;
else if (value == "2")
parser.rule().sdrEotf = NTransferFunction::TF_GAMMA22;
else
parser.rule().sdrEotf = NTransferFunction::fromString(value);
}
VAL = m_config->getSpecialConfigValuePtr("monitorv2", "sdrbrightness", output.c_str());
if (VAL && VAL->m_bSetByUser)
parser.rule().sdrBrightness = std::any_cast<Hyprlang::FLOAT>(VAL->getValue());
@ -1211,6 +1262,10 @@ std::optional<std::string> CConfigManager::handleMonitorv2(const std::string& ou
if (VAL && VAL->m_bSetByUser)
parser.rule().maxAvgLuminance = std::any_cast<Hyprlang::INT>(VAL->getValue());
VAL = m_config->getSpecialConfigValuePtr("monitorv2", "icc", output.c_str());
if (VAL && VAL->m_bSetByUser)
parser.rule().iccFile = std::any_cast<Hyprlang::STRING>(VAL->getValue());
auto newrule = parser.rule();
std::erase_if(m_monitorRules, [&](const auto& other) { return other.name == newrule.name; });
@ -1313,7 +1368,8 @@ void CConfigManager::postConfigReload(const Hyprlang::CParseResult& result) {
static auto PZOOMFACTOR = CConfigValue<Hyprlang::FLOAT>("cursor:zoom_factor");
for (auto const& m : g_pCompositor->m_monitors) {
*(m->m_cursorZoom) = *PZOOMFACTOR;
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(m->m_id);
if (m->m_activeWorkspace)
m->m_activeWorkspace->m_space->recalculate();
}
// Update the keyboard layout to the cfg'd one if this is not the first launch
@ -1381,9 +1437,6 @@ void CConfigManager::postConfigReload(const Hyprlang::CParseResult& result) {
// Update window border colors
g_pCompositor->updateAllWindowsAnimatedDecorationValues();
// update layout
g_pLayoutManager->switchToLayout(std::any_cast<Hyprlang::STRING>(m_config->getConfigValue("general:layout")));
// manual crash
if (std::any_cast<Hyprlang::INT>(m_config->getConfigValue("debug:manual_crash")) && !m_manualCrashInitiated) {
m_manualCrashInitiated = true;
@ -1422,7 +1475,10 @@ void CConfigManager::postConfigReload(const Hyprlang::CParseResult& result) {
if (!m_isFirstLaunch)
ensurePersistentWorkspacesPresent();
EMIT_HOOK_EVENT("configReloaded", nullptr);
// update layouts
Layout::Supplementary::algoMatcher()->updateWorkspaceLayouts();
Event::bus()->m_events.config.reloaded.emit();
if (g_pEventManager)
g_pEventManager->postEvent(SHyprIPCEvent{"configreloaded", ""});
}
@ -1444,8 +1500,9 @@ std::string CConfigManager::parseKeyword(const std::string& COMMAND, const std::
// invalidate layouts if they changed
if (COMMAND == "monitor" || COMMAND.contains("gaps_") || COMMAND.starts_with("dwindle:") || COMMAND.starts_with("master:")) {
for (auto const& m : g_pCompositor->m_monitors)
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(m->m_id);
for (auto const& m : g_pCompositor->m_monitors) {
g_layoutManager->recalculateMonitor(m);
}
}
// Update window border colors
@ -1617,6 +1674,8 @@ SWorkspaceRule CConfigManager::mergeWorkspaceRules(const SWorkspaceRule& rule1,
mergedRule.onCreatedEmptyRunCmd = rule2.onCreatedEmptyRunCmd;
if (rule2.defaultName.has_value())
mergedRule.defaultName = rule2.defaultName;
if (rule2.layout.has_value())
mergedRule.layout = rule2.layout;
if (!rule2.layoutopts.empty()) {
for (const auto& layoutopt : rule2.layoutopts) {
mergedRule.layoutopts[layoutopt.first] = layoutopt.second;
@ -1708,7 +1767,7 @@ void CConfigManager::performMonitorReload() {
m_wantsMonitorReload = false;
EMIT_HOOK_EVENT("monitorLayoutChanged", nullptr);
Event::bus()->m_events.monitor.layoutChanged.emit();
}
void* const* CConfigManager::getConfigValuePtr(const std::string& val) {
@ -1766,24 +1825,41 @@ void CConfigManager::ensureVRR(PHLMONITOR pMonitor) {
}
m->m_vrrActive = false;
return;
} else if (USEVRR == 1) {
if (!m->m_vrrActive) {
m->m_output->state->resetExplicitFences();
m->m_output->state->setAdaptiveSync(true);
}
if (!m->m_state.test()) {
Log::logger->log(Log::DEBUG, "Pending output {} does not accept VRR.", m->m_output->name);
m->m_output->state->setAdaptiveSync(false);
const auto PWORKSPACE = m->m_activeWorkspace;
if (USEVRR == 1) {
bool wantVRR = true;
if (PWORKSPACE && PWORKSPACE->m_hasFullscreenWindow && (PWORKSPACE->m_fullscreenMode & FSMODE_FULLSCREEN))
wantVRR = !PWORKSPACE->getFullscreenWindow()->m_ruleApplicator->noVRR().valueOrDefault();
if (wantVRR) {
if (!m->m_vrrActive) {
m->m_output->state->resetExplicitFences();
m->m_output->state->setAdaptiveSync(true);
if (!m->m_state.test()) {
Log::logger->log(Log::DEBUG, "Pending output {} does not accept VRR.", m->m_output->name);
m->m_output->state->setAdaptiveSync(false);
}
if (!m->m_state.commit())
Log::logger->log(Log::ERR, "Couldn't commit output {} in ensureVRR -> true", m->m_output->name);
}
m->m_vrrActive = true;
} else {
if (m->m_vrrActive) {
m->m_output->state->resetExplicitFences();
m->m_output->state->setAdaptiveSync(false);
if (!m->m_state.commit())
Log::logger->log(Log::ERR, "Couldn't commit output {} in ensureVRR -> true", m->m_output->name);
if (!m->m_state.commit())
Log::logger->log(Log::ERR, "Couldn't commit output {} in ensureVRR -> false", m->m_output->name);
}
m->m_vrrActive = false;
}
m->m_vrrActive = true;
return;
} else if (USEVRR == 2 || USEVRR == 3) {
const auto PWORKSPACE = m->m_activeWorkspace;
if (!PWORKSPACE)
return; // ???
@ -2208,6 +2284,15 @@ bool CMonitorRuleParser::parseVRR(const std::string& value) {
return true;
}
bool CMonitorRuleParser::parseICC(const std::string& val) {
if (val.empty()) {
m_error += "invalid icc ";
return false;
}
m_rule.iccFile = val;
return true;
}
void CMonitorRuleParser::setDisabled() {
m_rule.disabled = true;
}
@ -2302,6 +2387,9 @@ std::optional<std::string> CConfigManager::handleMonitor(const std::string& comm
} else if (ARGS[argno] == "vrr") {
parser.parseVRR(std::string(ARGS[argno + 1]));
argno++;
} else if (ARGS[argno] == "icc") {
parser.parseICC(std::string(ARGS[argno + 1]));
argno++;
} else if (ARGS[argno] == "workspace") {
const auto& [id, name, isAutoID] = getWorkspaceIDNameFromString(std::string(ARGS[argno + 1]));
@ -2668,6 +2756,9 @@ std::optional<std::string> CConfigManager::handleWorkspaceRules(const std::strin
opt = opt.substr(0, opt.find(':'));
wsRule.layoutopts[opt] = val;
} else if ((delim = rule.find("layout:")) != std::string::npos) {
std::string layout = rule.substr(delim + 7);
wsRule.layout = std::move(layout);
}
return {};
@ -2804,6 +2895,8 @@ std::optional<std::string> CConfigManager::handlePermission(const std::string& c
if (data[1] == "screencopy")
type = PERMISSION_TYPE_SCREENCOPY;
else if (data[1] == "cursorpos")
type = PERMISSION_TYPE_CURSOR_POS;
else if (data[1] == "plugin")
type = PERMISSION_TYPE_PLUGIN;
else if (data[1] == "keyboard" || data[1] == "keeb")
@ -2821,7 +2914,7 @@ std::optional<std::string> CConfigManager::handlePermission(const std::string& c
if (mode == PERMISSION_RULE_ALLOW_MODE_UNKNOWN)
return "unknown permission allow mode";
if (m_isFirstLaunch)
if (m_isFirstLaunch && g_pDynamicPermissionManager)
g_pDynamicPermissionManager->addConfigPermissionRule(std::string(data[0]), type, mode);
return {};
@ -2845,9 +2938,17 @@ std::optional<std::string> CConfigManager::handleGesture(const std::string& comm
if (direction == TRACKPAD_GESTURE_DIR_NONE)
return std::format("Invalid direction: {}", data[1]);
int startDataIdx = 2;
uint32_t modMask = 0;
float deltaScale = 1.F;
int startDataIdx = 2;
uint32_t modMask = 0;
float deltaScale = 1.F;
bool disableInhibit = false;
for (const auto arg : command.substr(7)) {
switch (arg) {
case 'p': disableInhibit = true; break;
default: return "gesture: invalid flag";
}
}
while (true) {
@ -2870,23 +2971,29 @@ std::optional<std::string> CConfigManager::handleGesture(const std::string& comm
if (data[startDataIdx] == "dispatcher")
result = g_pTrackpadGestures->addGesture(makeUnique<CDispatcherTrackpadGesture>(std::string{data[startDataIdx + 1]}, data.join(",", startDataIdx + 2)), fingerCount,
direction, modMask, deltaScale);
direction, modMask, deltaScale, disableInhibit);
else if (data[startDataIdx] == "workspace")
result = g_pTrackpadGestures->addGesture(makeUnique<CWorkspaceSwipeGesture>(), fingerCount, direction, modMask, deltaScale);
result = g_pTrackpadGestures->addGesture(makeUnique<CWorkspaceSwipeGesture>(), fingerCount, direction, modMask, deltaScale, disableInhibit);
else if (data[startDataIdx] == "resize")
result = g_pTrackpadGestures->addGesture(makeUnique<CResizeTrackpadGesture>(), fingerCount, direction, modMask, deltaScale);
result = g_pTrackpadGestures->addGesture(makeUnique<CResizeTrackpadGesture>(), fingerCount, direction, modMask, deltaScale, disableInhibit);
else if (data[startDataIdx] == "move")
result = g_pTrackpadGestures->addGesture(makeUnique<CMoveTrackpadGesture>(), fingerCount, direction, modMask, deltaScale);
result = g_pTrackpadGestures->addGesture(makeUnique<CMoveTrackpadGesture>(), fingerCount, direction, modMask, deltaScale, disableInhibit);
else if (data[startDataIdx] == "special")
result = g_pTrackpadGestures->addGesture(makeUnique<CSpecialWorkspaceGesture>(std::string{data[startDataIdx + 1]}), fingerCount, direction, modMask, deltaScale);
result =
g_pTrackpadGestures->addGesture(makeUnique<CSpecialWorkspaceGesture>(std::string{data[startDataIdx + 1]}), fingerCount, direction, modMask, deltaScale, disableInhibit);
else if (data[startDataIdx] == "close")
result = g_pTrackpadGestures->addGesture(makeUnique<CCloseTrackpadGesture>(), fingerCount, direction, modMask, deltaScale);
result = g_pTrackpadGestures->addGesture(makeUnique<CCloseTrackpadGesture>(), fingerCount, direction, modMask, deltaScale, disableInhibit);
else if (data[startDataIdx] == "float")
result = g_pTrackpadGestures->addGesture(makeUnique<CFloatTrackpadGesture>(std::string{data[startDataIdx + 1]}), fingerCount, direction, modMask, deltaScale);
result =
g_pTrackpadGestures->addGesture(makeUnique<CFloatTrackpadGesture>(std::string{data[startDataIdx + 1]}), fingerCount, direction, modMask, deltaScale, disableInhibit);
else if (data[startDataIdx] == "fullscreen")
result = g_pTrackpadGestures->addGesture(makeUnique<CFullscreenTrackpadGesture>(std::string{data[startDataIdx + 1]}), fingerCount, direction, modMask, deltaScale);
else if (data[startDataIdx] == "unset")
result = g_pTrackpadGestures->removeGesture(fingerCount, direction, modMask, deltaScale);
result = g_pTrackpadGestures->addGesture(makeUnique<CFullscreenTrackpadGesture>(std::string{data[startDataIdx + 1]}), fingerCount, direction, modMask, deltaScale,
disableInhibit);
else if (data[startDataIdx] == "cursorZoom") {
result = g_pTrackpadGestures->addGesture(makeUnique<CCursorZoomTrackpadGesture>(std::string{data[startDataIdx + 1]}, std::string{data[startDataIdx + 2]}), fingerCount,
direction, modMask, deltaScale, disableInhibit);
} else if (data[startDataIdx] == "unset")
result = g_pTrackpadGestures->removeGesture(fingerCount, direction, modMask, deltaScale, disableInhibit);
else
return std::format("Invalid gesture: {}", data[startDataIdx]);
@ -2989,7 +3096,7 @@ bool CConfigManager::shouldUseSoftwareCursors(PHLMONITOR pMonitor) {
switch (*PNOHW) {
case 0: return false;
case 1: return true;
case 2: return g_pHyprRenderer->isNvidia() && g_pHyprRenderer->isMgpu();
case 2: return g_pHyprRenderer->isNvidia() && (g_pHyprRenderer->isMgpu() || g_pCompositor->isVRRActiveOnAnyMonitor());
default: break;
}
@ -3016,20 +3123,20 @@ std::string SConfigOptionDescription::jsonify() const {
else if (typeid(Hyprlang::FLOAT) == std::type_index(CONFIGVALUE.type()))
currentValue = std::format("{:.2f}", std::any_cast<Hyprlang::FLOAT>(CONFIGVALUE));
else if (typeid(Hyprlang::STRING) == std::type_index(CONFIGVALUE.type()))
currentValue = std::any_cast<Hyprlang::STRING>(CONFIGVALUE);
currentValue = std::format("\"{}\"", std::any_cast<Hyprlang::STRING>(CONFIGVALUE));
else if (typeid(Hyprlang::VEC2) == std::type_index(CONFIGVALUE.type())) {
const auto V = std::any_cast<Hyprlang::VEC2>(CONFIGVALUE);
currentValue = std::format("{}, {}", V.x, V.y);
currentValue = std::format("\"{}, {}\"", V.x, V.y);
} else if (typeid(void*) == std::type_index(CONFIGVALUE.type())) {
const auto DATA = sc<ICustomConfigValueData*>(std::any_cast<void*>(CONFIGVALUE));
currentValue = DATA->toString();
currentValue = std::format("\"{}\"", DATA->toString());
}
try {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, SStringData>) {
return std::format(R"#( "value": "{}",
"current": "{}",
"current": {},
"explicit": {})#",
val.value, currentValue, EXPLICIT);
} else if constexpr (std::is_same_v<T, SRangeData>) {
@ -3048,7 +3155,7 @@ std::string SConfigOptionDescription::jsonify() const {
val.value, val.min, val.max, currentValue, EXPLICIT);
} else if constexpr (std::is_same_v<T, SColorData>) {
return std::format(R"#( "value": "{}",
"current": "{}",
"current": {},
"explicit": {})#",
val.color.getAsHex(), currentValue, EXPLICIT);
} else if constexpr (std::is_same_v<T, SBoolData>) {
@ -3069,12 +3176,12 @@ std::string SConfigOptionDescription::jsonify() const {
"min_y": {},
"max_x": {},
"max_y": {},
"current": "{}",
"current": {},
"explicit": {})#",
val.vec.x, val.vec.y, val.min.x, val.min.y, val.max.x, val.max.y, currentValue, EXPLICIT);
} else if constexpr (std::is_same_v<T, SGradientData>) {
return std::format(R"#( "value": "{}",
"current": "{}",
"current": {},
"explicit": {})#",
val.gradient, currentValue, EXPLICIT);
}

View file

@ -46,6 +46,7 @@ struct SWorkspaceRule {
std::optional<bool> noShadow;
std::optional<std::string> onCreatedEmptyRunCmd;
std::optional<std::string> defaultName;
std::optional<std::string> layout;
std::map<std::string, std::string> layoutopts;
};
@ -176,6 +177,7 @@ class CMonitorRuleParser {
bool parseSDRBrightness(const std::string& value);
bool parseSDRSaturation(const std::string& value);
bool parseVRR(const std::string& value);
bool parseICC(const std::string& value);
void setDisabled();
void setMirror(const std::string& value);

View file

@ -44,6 +44,7 @@ using namespace Hyprutils::OS;
#include "config/ConfigManager.hpp"
#include "helpers/MiscFunctions.hpp"
#include "../desktop/view/LayerSurface.hpp"
#include "../desktop/view/Group.hpp"
#include "../desktop/rule/Engine.hpp"
#include "../desktop/history/WindowHistoryTracker.hpp"
#include "../desktop/state/FocusState.hpp"
@ -52,12 +53,15 @@ using namespace Hyprutils::OS;
#include "../Compositor.hpp"
#include "../managers/input/InputManager.hpp"
#include "../managers/XWaylandManager.hpp"
#include "../managers/LayoutManager.hpp"
#include "../plugins/PluginSystem.hpp"
#include "../managers/animation/AnimationManager.hpp"
#include "../debug/HyprNotificationOverlay.hpp"
#include "../render/Renderer.hpp"
#include "../render/OpenGL.hpp"
#include "../layout/space/Space.hpp"
#include "../layout/algorithm/Algorithm.hpp"
#include "../layout/algorithm/TiledAlgorithm.hpp"
#include "../layout/supplementary/WorkspaceAlgoMatcher.hpp"
#if defined(__DragonFly__) || defined(__FreeBSD__)
#include <sys/ucred.h>
@ -144,13 +148,13 @@ std::string CHyprCtl::getSolitaryBlockedReason(Hyprutils::Memory::CSharedPointer
}
const std::array<const char*, CMonitor::DS_CHECKS_COUNT> DS_REASONS_JSON = {
"\"UNKNOWN\"", "\"USER\"", "\"WINDOWED\"", "\"CONTENT\"", "\"MIRROR\"", "\"RECORD\"", "\"SW\"",
"\"CANDIDATE\"", "\"SURFACE\"", "\"TRANSFORM\"", "\"DMA\"", "\"TEARING\"", "\"FAILED\"", "\"CM\"",
"\"UNKNOWN\"", "\"USER\"", "\"WINDOWED\"", "\"CONTENT\"", "\"MIRROR\"", "\"RECORD\"", "\"SW\"",
"\"CANDIDATE\"", "\"SURFACE\"", "\"TRANSFORM\"", "\"DMA\"", "\"FAILED\"", "\"CM\"",
};
const std::array<const char*, CMonitor::DS_CHECKS_COUNT> DS_REASONS_TEXT = {
"unknown reason", "user settings", "windowed mode", "content type", "monitor mirrors", "screen record/screenshot", "software renders/cursors",
"missing candidate", "invalid surface", "surface transformations", "invalid buffer", "tearing", "activation failed", "color management",
"unknown reason", "user settings", "windowed mode", "content type", "monitor mirrors", "screen record/screenshot", "software renders/cursors",
"missing candidate", "invalid surface", "surface transformations", "invalid buffer", "activation failed", "color management",
};
std::string CHyprCtl::getDSBlockedReason(Hyprutils::Memory::CSharedPointer<CMonitor> m, eHyprCtlOutputFormat format) {
@ -173,14 +177,13 @@ std::string CHyprCtl::getDSBlockedReason(Hyprutils::Memory::CSharedPointer<CMoni
}
const std::array<const char*, CMonitor::TC_CHECKS_COUNT> TEARING_REASONS_JSON = {
"\"UNKNOWN\"", "\"NOT_TORN\"", "\"USER\"", "\"ZOOM\"", "\"SUPPORT\"", "\"CANDIDATE\"", "\"WINDOW\"",
"\"UNKNOWN\"", "\"NOT_TORN\"", "\"USER\"", "\"ZOOM\"", "\"SUPPORT\"", "\"CANDIDATE\"", "\"WINDOW\"", "\"HW_CURSOR\"",
};
const std::array<const char*, CMonitor::TC_CHECKS_COUNT> TEARING_REASONS_TEXT = {
"unknown reason", "next frame is not torn", "user settings", "zoom", "not supported by monitor", "missing candidate", "window settings",
};
const std::array<const char*, CMonitor::TC_CHECKS_COUNT> TEARING_REASONS_TEXT = {"unknown reason", "next frame is not torn", "user settings", "zoom",
"not supported by monitor", "missing candidate", "window settings", "hw cursor"};
std::string CHyprCtl::getTearingBlockedReason(Hyprutils::Memory::CSharedPointer<CMonitor> m, eHyprCtlOutputFormat format) {
std::string CHyprCtl::getTearingBlockedReason(Hyprutils::Memory::CSharedPointer<CMonitor> m, eHyprCtlOutputFormat format) {
const auto reasons = m->isTearingBlocked(true);
if (!reasons || (reasons == CMonitor::TC_NOT_TORN && m->m_tearingState.activelyTearing))
return "null";
@ -331,23 +334,19 @@ static std::string getTagsData(PHLWINDOW w, eHyprCtlOutputFormat format) {
static std::string getGroupedData(PHLWINDOW w, eHyprCtlOutputFormat format) {
const bool isJson = format == eHyprCtlOutputFormat::FORMAT_JSON;
if (w->m_groupData.pNextWindow.expired())
if (!w->m_group)
return isJson ? "" : "0";
std::ostringstream result;
PHLWINDOW head = w->getGroupHead();
PHLWINDOW curr = head;
while (true) {
for (const auto& curr : w->m_group->windows()) {
if (isJson)
result << std::format("\"0x{:x}\"", rc<uintptr_t>(curr.get()));
else
result << std::format("{:x}", rc<uintptr_t>(curr.get()));
curr = curr->m_groupData.pNextWindow.lock();
// We've wrapped around to the start, break out without trailing comma
if (curr == head)
break;
result << (isJson ? ", " : ",");
if (curr != w->m_group->windows().back())
result << (isJson ? ", " : ",");
}
return result.str();
@ -376,7 +375,6 @@ std::string CHyprCtl::getWindowData(PHLWINDOW w, eHyprCtlOutputFormat format) {
"name": "{}"
}},
"floating": {},
"pseudo": {},
"monitor": {},
"class": "{}",
"title": "{}",
@ -387,6 +385,7 @@ std::string CHyprCtl::getWindowData(PHLWINDOW w, eHyprCtlOutputFormat format) {
"pinned": {},
"fullscreen": {},
"fullscreenClient": {},
"overFullscreen": {},
"grouped": [{}],
"tags": [{}],
"swallowing": "0x{:x}",
@ -394,29 +393,31 @@ std::string CHyprCtl::getWindowData(PHLWINDOW w, eHyprCtlOutputFormat format) {
"inhibitingIdle": {},
"xdgTag": "{}",
"xdgDescription": "{}",
"contentType": "{}"
"contentType": "{}",
"stableId": "{:x}"
}},)#",
rc<uintptr_t>(w.get()), (w->m_isMapped ? "true" : "false"), (w->isHidden() ? "true" : "false"), sc<int>(w->m_realPosition->goal().x),
sc<int>(w->m_realPosition->goal().y), sc<int>(w->m_realSize->goal().x), sc<int>(w->m_realSize->goal().y), w->m_workspace ? w->workspaceID() : WORKSPACE_INVALID,
escapeJSONStrings(!w->m_workspace ? "" : w->m_workspace->m_name), (sc<int>(w->m_isFloating) == 1 ? "true" : "false"), (w->m_isPseudotiled ? "true" : "false"),
w->monitorID(), escapeJSONStrings(w->m_class), escapeJSONStrings(w->m_title), escapeJSONStrings(w->m_initialClass), escapeJSONStrings(w->m_initialTitle), w->getPID(),
(sc<int>(w->m_isX11) == 1 ? "true" : "false"), (w->m_pinned ? "true" : "false"), sc<uint8_t>(w->m_fullscreenState.internal), sc<uint8_t>(w->m_fullscreenState.client),
escapeJSONStrings(!w->m_workspace ? "" : w->m_workspace->m_name), (sc<int>(w->m_isFloating) == 1 ? "true" : "false"), w->monitorID(), escapeJSONStrings(w->m_class),
escapeJSONStrings(w->m_title), escapeJSONStrings(w->m_initialClass), escapeJSONStrings(w->m_initialTitle), w->getPID(), (sc<int>(w->m_isX11) == 1 ? "true" : "false"),
(w->m_pinned ? "true" : "false"), sc<uint8_t>(w->m_fullscreenState.internal), sc<uint8_t>(w->m_fullscreenState.client), (w->m_createdOverFullscreen ? "true" : "false"),
getGroupedData(w, format), getTagsData(w, format), rc<uintptr_t>(w->m_swallowed.get()), getFocusHistoryID(w),
(g_pInputManager->isWindowInhibiting(w, false) ? "true" : "false"), escapeJSONStrings(w->xdgTag().value_or("")), escapeJSONStrings(w->xdgDescription().value_or("")),
escapeJSONStrings(NContentType::toString(w->getContentType())));
escapeJSONStrings(NContentType::toString(w->getContentType())), w->m_stableID);
} else {
return std::format(
"Window {:x} -> {}:\n\tmapped: {}\n\thidden: {}\n\tat: {},{}\n\tsize: {},{}\n\tworkspace: {} ({})\n\tfloating: {}\n\tpseudo: {}\n\tmonitor: {}\n\tclass: {}\n\ttitle: "
"Window {:x} -> {}:\n\tmapped: {}\n\thidden: {}\n\tat: {},{}\n\tsize: {},{}\n\tworkspace: {} ({})\n\tfloating: {}\n\tmonitor: {}\n\tclass: {}\n\ttitle: "
"{}\n\tinitialClass: {}\n\tinitialTitle: {}\n\tpid: "
"{}\n\txwayland: {}\n\tpinned: "
"{}\n\tfullscreen: {}\n\tfullscreenClient: {}\n\tgrouped: {}\n\ttags: {}\n\tswallowing: {:x}\n\tfocusHistoryID: {}\n\tinhibitingIdle: {}\n\txdgTag: "
"{}\n\txdgDescription: {}\n\tcontentType: {}\n\n",
"{}\n\tfullscreen: {}\n\tfullscreenClient: {}\n\toverFullscreen: {}\n\tgrouped: {}\n\ttags: {}\n\tswallowing: {:x}\n\tfocusHistoryID: {}\n\tinhibitingIdle: "
"{}\n\txdgTag: "
"{}\n\txdgDescription: {}\n\tcontentType: {}\n\tstableID: {:x}\n\n",
rc<uintptr_t>(w.get()), w->m_title, sc<int>(w->m_isMapped), sc<int>(w->isHidden()), sc<int>(w->m_realPosition->goal().x), sc<int>(w->m_realPosition->goal().y),
sc<int>(w->m_realSize->goal().x), sc<int>(w->m_realSize->goal().y), w->m_workspace ? w->workspaceID() : WORKSPACE_INVALID,
(!w->m_workspace ? "" : w->m_workspace->m_name), sc<int>(w->m_isFloating), sc<int>(w->m_isPseudotiled), w->monitorID(), w->m_class, w->m_title, w->m_initialClass,
w->m_initialTitle, w->getPID(), sc<int>(w->m_isX11), sc<int>(w->m_pinned), sc<uint8_t>(w->m_fullscreenState.internal), sc<uint8_t>(w->m_fullscreenState.client),
(!w->m_workspace ? "" : w->m_workspace->m_name), sc<int>(w->m_isFloating), w->monitorID(), w->m_class, w->m_title, w->m_initialClass, w->m_initialTitle, w->getPID(),
sc<int>(w->m_isX11), sc<int>(w->m_pinned), sc<uint8_t>(w->m_fullscreenState.internal), sc<uint8_t>(w->m_fullscreenState.client), sc<int>(w->m_createdOverFullscreen),
getGroupedData(w, format), getTagsData(w, format), rc<uintptr_t>(w->m_swallowed.get()), getFocusHistoryID(w), sc<int>(g_pInputManager->isWindowInhibiting(w, false)),
w->xdgTag().value_or(""), w->xdgDescription().value_or(""), NContentType::toString(w->getContentType()));
w->xdgTag().value_or(""), w->xdgDescription().value_or(""), NContentType::toString(w->getContentType()), w->m_stableID);
}
}
@ -450,8 +451,15 @@ static std::string clientsRequest(eHyprCtlOutputFormat format, std::string reque
}
std::string CHyprCtl::getWorkspaceData(PHLWORKSPACE w, eHyprCtlOutputFormat format) {
const auto PLASTW = w->getLastFocusedWindow();
const auto PMONITOR = w->m_monitor.lock();
const auto PLASTW = w->getLastFocusedWindow();
const auto PMONITOR = w->m_monitor.lock();
std::string layoutName = "unknown";
if (w->m_space && w->m_space->algorithm() && w->m_space->algorithm()->tiledAlgo()) {
const auto& TILED_ALGO = w->m_space->algorithm()->tiledAlgo();
layoutName = Layout::Supplementary::algoMatcher()->getNameForTiledAlgo(&typeid(*TILED_ALGO.get()));
}
if (format == eHyprCtlOutputFormat::FORMAT_JSON) {
return std::format(R"#({{
"id": {},
@ -462,16 +470,17 @@ std::string CHyprCtl::getWorkspaceData(PHLWORKSPACE w, eHyprCtlOutputFormat form
"hasfullscreen": {},
"lastwindow": "0x{:x}",
"lastwindowtitle": "{}",
"ispersistent": {}
"ispersistent": {},
"tiledLayout": "{}"
}})#",
w->m_id, escapeJSONStrings(w->m_name), escapeJSONStrings(PMONITOR ? PMONITOR->m_name : "?"),
escapeJSONStrings(PMONITOR ? std::to_string(PMONITOR->m_id) : "null"), w->getWindows(), w->m_hasFullscreenWindow ? "true" : "false",
rc<uintptr_t>(PLASTW.get()), PLASTW ? escapeJSONStrings(PLASTW->m_title) : "", w->isPersistent() ? "true" : "false");
rc<uintptr_t>(PLASTW.get()), PLASTW ? escapeJSONStrings(PLASTW->m_title) : "", w->isPersistent() ? "true" : "false", escapeJSONStrings(layoutName));
} else {
return std::format(
"workspace ID {} ({}) on monitor {}:\n\tmonitorID: {}\n\twindows: {}\n\thasfullscreen: {}\n\tlastwindow: 0x{:x}\n\tlastwindowtitle: {}\n\tispersistent: {}\n\n",
w->m_id, w->m_name, PMONITOR ? PMONITOR->m_name : "?", PMONITOR ? std::to_string(PMONITOR->m_id) : "null", w->getWindows(), sc<int>(w->m_hasFullscreenWindow),
rc<uintptr_t>(PLASTW.get()), PLASTW ? PLASTW->m_title : "", sc<int>(w->isPersistent()));
return std::format("workspace ID {} ({}) on monitor {}:\n\tmonitorID: {}\n\twindows: {}\n\thasfullscreen: {}\n\tlastwindow: 0x{:x}\n\tlastwindowtitle: {}\n\tispersistent: "
"{}\n\ttiledLayout: {}\n\n",
w->m_id, w->m_name, PMONITOR ? PMONITOR->m_name : "?", PMONITOR ? std::to_string(PMONITOR->m_id) : "null", w->getWindows(),
sc<int>(w->m_hasFullscreenWindow), rc<uintptr_t>(PLASTW.get()), PLASTW ? PLASTW->m_title : "", sc<int>(w->isPersistent()), layoutName);
}
}
@ -670,28 +679,6 @@ static std::string layersRequest(eHyprCtlOutputFormat format, std::string reques
return result;
}
static std::string layoutsRequest(eHyprCtlOutputFormat format, std::string request) {
std::string result = "";
if (format == eHyprCtlOutputFormat::FORMAT_JSON) {
result += "[";
for (auto const& m : g_pLayoutManager->getAllLayoutNames()) {
result += std::format(
R"#(
"{}",)#",
m);
}
trimTrailingComma(result);
result += "\n]\n";
} else {
for (auto const& m : g_pLayoutManager->getAllLayoutNames()) {
result += std::format("{}\n", m);
}
}
return result;
}
static std::string configErrorsRequest(eHyprCtlOutputFormat format, std::string request) {
std::string result = "";
std::string currErrors = g_pConfigManager->getErrors();
@ -802,7 +789,7 @@ static std::string devicesRequest(eHyprCtlOutputFormat format, std::string reque
result += std::format(
R"#( {{
"address": "0x{:x}",
"type": "tabletTool",
"type": "tabletTool"
}},)#",
rc<uintptr_t>(d.get()));
}
@ -1294,8 +1281,13 @@ static std::string dispatchKeyword(eHyprCtlOutputFormat format, std::string in)
g_pHyprCtl->m_currentRequestParams.isDynamicKeyword = false;
if (COMMAND == "source") {
g_pConfigManager->m_wantsMonitorReload = true;
g_pEventLoopManager->doLater([] { g_pConfigManager->reloadRules(); });
}
// if we are executing a dynamic source we have to reload everything, so every if will have a check for source.
if (COMMAND == "monitor" || COMMAND == "source")
if (COMMAND == "monitor")
g_pConfigManager->m_wantsMonitorReload = true; // for monitor keywords
if (COMMAND.contains("monitorv2"))
@ -1308,10 +1300,8 @@ static std::string dispatchKeyword(eHyprCtlOutputFormat format, std::string in)
g_pInputManager->setTabletConfigs(); // update tablets
}
static auto PLAYOUT = CConfigValue<std::string>("general:layout");
if (COMMAND.contains("general:layout"))
g_pLayoutManager->switchToLayout(*PLAYOUT); // update layout
if (COMMAND.contains("general:layout") || (COMMAND.contains("workspace") && VALUE.contains("layout:")))
Layout::Supplementary::algoMatcher()->updateWorkspaceLayouts();
if (COMMAND.contains("decoration:screen_shader") || COMMAND == "source")
g_pHyprOpenGL->m_reloadScreenShader = true;
@ -1331,13 +1321,22 @@ static std::string dispatchKeyword(eHyprCtlOutputFormat format, std::string in)
for (auto const& m : g_pCompositor->m_monitors) {
*(m->m_cursorZoom) = *PZOOMFACTOR;
g_pHyprRenderer->damageMonitor(m);
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(m->m_id);
if (m->m_activeWorkspace)
m->m_activeWorkspace->m_space->recalculate();
}
}
if (COMMAND.contains("windowrule ") || COMMAND.contains("windowrule["))
g_pConfigManager->reloadRules();
if (COMMAND.contains("layerrule") || COMMAND.contains("layerrule[")) {
g_pConfigManager->reloadRules();
// Damage all monitors to redraw static layers.
for (auto const& m : g_pCompositor->m_monitors) {
g_pHyprRenderer->damageMonitor(m);
}
}
if (COMMAND.contains("workspace"))
g_pConfigManager->ensurePersistentWorkspacesPresent();
@ -1597,7 +1596,7 @@ static std::string dispatchGetProp(eHyprCtlOutputFormat format, std::string requ
static auto PGROUPACTIVELOCKEDCOL = CConfigValue<Hyprlang::CUSTOMTYPE>("group:col.border_locked_active");
static auto PGROUPINACTIVELOCKEDCOL = CConfigValue<Hyprlang::CUSTOMTYPE>("group:col.border_locked_inactive");
const bool GROUPLOCKED = PWINDOW->m_groupData.pNextWindow.lock() ? PWINDOW->getGroupHead()->m_groupData.locked : false;
const bool GROUPLOCKED = PWINDOW->m_group ? PWINDOW->m_group->locked() : false;
if (active) {
auto* const ACTIVECOL = (CGradientValueData*)(PACTIVECOL.ptr())->getData();
@ -1605,7 +1604,7 @@ static std::string dispatchGetProp(eHyprCtlOutputFormat format, std::string requ
auto* const GROUPACTIVECOL = (CGradientValueData*)(PGROUPACTIVECOL.ptr())->getData();
auto* const GROUPACTIVELOCKEDCOL = (CGradientValueData*)(PGROUPACTIVELOCKEDCOL.ptr())->getData();
const auto* const ACTIVECOLOR =
!PWINDOW->m_groupData.pNextWindow.lock() ? (!PWINDOW->m_groupData.deny ? ACTIVECOL : NOGROUPACTIVECOL) : (GROUPLOCKED ? GROUPACTIVELOCKEDCOL : GROUPACTIVECOL);
!PWINDOW->m_group ? (!(PWINDOW->m_groupRules & Desktop::View::GROUP_DENY) ? ACTIVECOL : NOGROUPACTIVECOL) : (GROUPLOCKED ? GROUPACTIVELOCKEDCOL : GROUPACTIVECOL);
std::string borderColorString = PWINDOW->m_ruleApplicator->activeBorderColor().valueOr(*ACTIVECOLOR).toString();
if (FORMNORM)
@ -1617,8 +1616,8 @@ static std::string dispatchGetProp(eHyprCtlOutputFormat format, std::string requ
auto* const NOGROUPINACTIVECOL = (CGradientValueData*)(PNOGROUPINACTIVECOL.ptr())->getData();
auto* const GROUPINACTIVECOL = (CGradientValueData*)(PGROUPINACTIVECOL.ptr())->getData();
auto* const GROUPINACTIVELOCKEDCOL = (CGradientValueData*)(PGROUPINACTIVELOCKEDCOL.ptr())->getData();
const auto* const INACTIVECOLOR = !PWINDOW->m_groupData.pNextWindow.lock() ? (!PWINDOW->m_groupData.deny ? INACTIVECOL : NOGROUPINACTIVECOL) :
(GROUPLOCKED ? GROUPINACTIVELOCKEDCOL : GROUPINACTIVECOL);
const auto* const INACTIVECOLOR = !PWINDOW->m_group ? (!(PWINDOW->m_groupRules & Desktop::View::GROUP_DENY) ? INACTIVECOL : NOGROUPINACTIVECOL) :
(GROUPLOCKED ? GROUPINACTIVELOCKEDCOL : GROUPINACTIVECOL);
std::string borderColorString = PWINDOW->m_ruleApplicator->inactiveBorderColor().valueOr(*INACTIVECOLOR).toString();
if (FORMNORM)
@ -2050,7 +2049,12 @@ static std::string submapRequest(eHyprCtlOutputFormat format, std::string reques
}
static std::string reloadShaders(eHyprCtlOutputFormat format, std::string request) {
if (g_pHyprOpenGL->initShaders())
CVarList vars(request, 0, ' ');
if (vars.size() > 2)
return "too many args";
if (g_pHyprOpenGL && g_pHyprRenderer->reloadShaders(vars.size() == 2 ? vars[1] : ""))
return format == FORMAT_JSON ? "{\"ok\": true}" : "ok";
else
return format == FORMAT_JSON ? "{\"ok\": false}" : "error";
@ -2073,13 +2077,12 @@ CHyprCtl::CHyprCtl() {
registerCommand(SHyprCtlCommand{"systeminfo", true, systemInfoRequest});
registerCommand(SHyprCtlCommand{"animations", true, animationsRequest});
registerCommand(SHyprCtlCommand{"rollinglog", true, rollinglogRequest});
registerCommand(SHyprCtlCommand{"layouts", true, layoutsRequest});
registerCommand(SHyprCtlCommand{"configerrors", true, configErrorsRequest});
registerCommand(SHyprCtlCommand{"locked", true, getIsLocked});
registerCommand(SHyprCtlCommand{"descriptions", true, getDescriptions});
registerCommand(SHyprCtlCommand{"submap", true, submapRequest});
registerCommand(SHyprCtlCommand{.name = "reloadshaders", .exact = true, .fn = reloadShaders});
registerCommand(SHyprCtlCommand{.name = "reloadshaders", .exact = false, .fn = reloadShaders});
registerCommand(SHyprCtlCommand{"monitors", false, monitorsRequest});
registerCommand(SHyprCtlCommand{"reload", false, reloadRequest});
registerCommand(SHyprCtlCommand{"plugin", false, dispatchPlugin});
@ -2186,10 +2189,6 @@ std::string CHyprCtl::getReply(std::string request) {
g_pInputManager->setTouchDeviceConfigs(); // update touch device cfgs
g_pInputManager->setTabletConfigs(); // update tablets
static auto PLAYOUT = CConfigValue<std::string>("general:layout");
g_pLayoutManager->switchToLayout(*PLAYOUT); // update layout
g_pHyprOpenGL->m_reloadScreenShader = true;
for (auto& [m, rd] : g_pHyprOpenGL->m_monitorRenderResources) {
@ -2203,9 +2202,17 @@ std::string CHyprCtl::getReply(std::string request) {
Desktop::Rule::ruleEngine()->updateAllRules();
}
for (const auto& ws : g_pCompositor->getWorkspaces()) {
if (!ws)
continue;
ws->updateWindows();
ws->updateWindowData();
ws->updateWindowDecos();
}
for (auto const& m : g_pCompositor->m_monitors) {
g_pHyprRenderer->damageMonitor(m);
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(m->m_id);
}
}
@ -2217,16 +2224,38 @@ std::string CHyprCtl::makeDynamicCall(const std::string& input) {
}
static bool successWrite(int fd, const std::string& data, bool needLog = true) {
if (write(fd, data.c_str(), data.length()) > 0)
return true;
size_t totalWritten = 0;
size_t remaining = data.length();
size_t waitsDone = 0;
constexpr const size_t MAX_WAITS = 20; // 2000µs = 2ms
if (errno == EAGAIN)
return true;
while (totalWritten < data.length()) {
ssize_t written = write(fd, data.c_str() + totalWritten, remaining);
if (needLog)
Log::logger->log(Log::ERR, "Couldn't write to socket. Error: " + std::string(strerror(errno)));
if (waitsDone > MAX_WAITS) {
Log::logger->log(Log::ERR, "Couldn't write to socket. Buffer was full and the client couldn't read in time.");
return false;
}
return false;
if (written < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// socket buffer full, wait a bit and retry
std::this_thread::sleep_for(std::chrono::microseconds(100));
waitsDone++;
continue;
}
if (needLog)
Log::logger->log(Log::ERR, "Couldn't write to socket. Error: {}", strerror(errno));
return false;
}
waitsDone = 0;
totalWritten += written;
remaining -= written;
}
return true;
}
static void runWritingDebugLogThread(const int conn) {

View file

@ -8,7 +8,7 @@
#include "../desktop/state/FocusState.hpp"
CHyprDebugOverlay::CHyprDebugOverlay() {
m_texture = makeShared<CTexture>();
m_texture = g_pHyprRenderer->createTexture();
}
void CHyprMonitorDebugOverlay::renderData(PHLMONITOR pMonitor, float durationUs) {
@ -259,15 +259,7 @@ void CHyprDebugOverlay::draw() {
cairo_surface_flush(m_cairoSurface);
// copy the data to an OpenGL texture we have
const auto DATA = cairo_image_surface_get_data(m_cairoSurface);
m_texture->allocate();
m_texture->bind();
m_texture->setTexParameter(GL_TEXTURE_MAG_FILTER, GL_NEAREST);
m_texture->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_NEAREST);
m_texture->setTexParameter(GL_TEXTURE_SWIZZLE_R, GL_BLUE);
m_texture->setTexParameter(GL_TEXTURE_SWIZZLE_B, GL_RED);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, PMONITOR->m_pixelSize.x, PMONITOR->m_pixelSize.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, DATA);
m_texture = g_pHyprRenderer->createTexture(m_cairoSurface);
CTexPassElement::SRenderData data;
data.tex = m_texture;

View file

@ -42,7 +42,7 @@ class CHyprDebugOverlay {
cairo_surface_t* m_cairoSurface = nullptr;
cairo_t* m_cairo = nullptr;
SP<CTexture> m_texture;
SP<ITexture> m_texture;
friend class CHyprMonitorDebugOverlay;
friend class CHyprRenderer;

View file

@ -4,9 +4,9 @@
#include "../Compositor.hpp"
#include "../config/ConfigValue.hpp"
#include "../render/pass/TexPassElement.hpp"
#include "../event/EventBus.hpp"
#include "../managers/animation/AnimationManager.hpp"
#include "../managers/HookSystemManager.hpp"
#include "../render/Renderer.hpp"
static inline auto iconBackendFromLayout(PangoLayout* layout) {
@ -22,14 +22,12 @@ static inline auto iconBackendFromLayout(PangoLayout* layout) {
}
CHyprNotificationOverlay::CHyprNotificationOverlay() {
static auto P = g_pHookSystem->hookDynamic("focusedMon", [&](void* self, SCallbackInfo& info, std::any param) {
static auto P = Event::bus()->m_events.monitor.focused.listen([&](PHLMONITOR mon) {
if (m_notifications.empty())
return;
g_pHyprRenderer->damageBox(m_lastDamage);
});
m_texture = makeShared<CTexture>();
}
CHyprNotificationOverlay::~CHyprNotificationOverlay() {
@ -232,16 +230,7 @@ void CHyprNotificationOverlay::draw(PHLMONITOR pMonitor) {
m_lastDamage = damage;
// copy the data to an OpenGL texture we have
const auto DATA = cairo_image_surface_get_data(m_cairoSurface);
m_texture->allocate();
m_texture->bind();
m_texture->setTexParameter(GL_TEXTURE_MAG_FILTER, GL_NEAREST);
m_texture->setTexParameter(GL_TEXTURE_MIN_FILTER, GL_NEAREST);
m_texture->setTexParameter(GL_TEXTURE_SWIZZLE_R, GL_BLUE);
m_texture->setTexParameter(GL_TEXTURE_SWIZZLE_B, GL_RED);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, MONSIZE.x, MONSIZE.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, DATA);
m_texture = g_pHyprRenderer->createTexture(m_cairoSurface);
CTexPassElement::SRenderData data;
data.tex = m_texture;

View file

@ -18,7 +18,7 @@ enum eIconBackend : uint8_t {
static const std::array<std::array<std::string, ICON_NONE + 1>, 3 /* backends */> ICONS_ARRAY = {
std::array<std::string, ICON_NONE + 1>{"[!]", "[i]", "[Hint]", "[Err]", "[?]", "[ok]", ""},
std::array<std::string, ICON_NONE + 1>{"", "", "", "", "", "󰸞", ""}, std::array<std::string, ICON_NONE + 1>{"", "", "", "", "", ""}};
static const std::array<CHyprColor, ICON_NONE + 1> ICONS_COLORS = {CHyprColor{255.0 / 255.0, 204 / 255.0, 102 / 255.0, 1.0},
static const std::array<CHyprColor, ICON_NONE + 1> ICONS_COLORS = {CHyprColor{1.0, 204 / 255.0, 102 / 255.0, 1.0},
CHyprColor{128 / 255.0, 255 / 255.0, 255 / 255.0, 1.0},
CHyprColor{179 / 255.0, 255 / 255.0, 204 / 255.0, 1.0},
CHyprColor{255 / 255.0, 77 / 255.0, 77 / 255.0, 1.0},
@ -57,7 +57,7 @@ class CHyprNotificationOverlay {
PHLMONITORREF m_lastMonitor;
Vector2D m_lastSize = Vector2D(-1, -1);
SP<CTexture> m_texture;
SP<ITexture> m_texture;
};
inline UP<CHyprNotificationOverlay> g_pHyprNotificationOverlay;

View file

@ -1,9 +1,8 @@
#include "Logger.hpp"
#include "RollingLogFollow.hpp"
#include "../../defines.hpp"
#include "../../event/EventBus.hpp"
#include "../../managers/HookSystemManager.hpp"
#include "../../config/ConfigValue.hpp"
using namespace Log;
@ -39,7 +38,7 @@ void CLogger::initIS(const std::string_view& IS) {
}
void CLogger::initCallbacks() {
static auto P = g_pHookSystem->hookDynamic("configReloaded", [this](void* hk, SCallbackInfo& info, std::any param) { recheckCfg(); });
static auto P = Event::bus()->m_events.config.reloaded.listen([this]() { recheckCfg(); });
recheckCfg();
}

View file

@ -5,3 +5,7 @@
#include "helpers/Color.hpp"
#include "macros.hpp"
#include "desktop/DesktopTypes.hpp"
#if !defined(__GXX_RTTI)
#error "Hyprland requires C++ RTTI. Shit will hit the fan otherwise. Do not even try."
#endif

View file

@ -1,10 +1,13 @@
#include "Workspace.hpp"
#include "view/Group.hpp"
#include "../Compositor.hpp"
#include "../config/ConfigValue.hpp"
#include "config/ConfigManager.hpp"
#include "managers/animation/AnimationManager.hpp"
#include "../managers/EventManager.hpp"
#include "../managers/HookSystemManager.hpp"
#include "../layout/space/Space.hpp"
#include "../layout/supplementary/WorkspaceAlgoMatcher.hpp"
#include "../event/EventBus.hpp"
#include <hyprutils/animation/AnimatedVariable.hpp>
#include <hyprutils/string/String.hpp>
@ -34,13 +37,14 @@ void CWorkspace::init(PHLWORKSPACE self) {
if (RULEFORTHIS.defaultName.has_value())
m_name = RULEFORTHIS.defaultName.value();
m_focusedWindowHook = g_pHookSystem->hookDynamic("closeWindow", [this](void* self, SCallbackInfo& info, std::any param) {
const auto PWINDOW = std::any_cast<PHLWINDOW>(param);
if (PWINDOW == m_lastFocusedWindow.lock())
m_focusedWindowHook = Event::bus()->m_events.window.close.listen([this](PHLWINDOW pWindow) {
if (pWindow == m_lastFocusedWindow.lock())
m_lastFocusedWindow.reset();
});
m_space = Layout::CSpace::create(m_self.lock());
m_space->setAlgorithmProvider(Layout::Supplementary::algoMatcher()->createAlgorithmForWorkspace(m_self.lock()));
m_inert = false;
const auto WORKSPACERULE = g_pConfigManager->getWorkspaceRuleFor(self);
@ -52,22 +56,19 @@ void CWorkspace::init(PHLWORKSPACE self) {
g_pEventManager->postEvent({.event = "createworkspace", .data = m_name});
g_pEventManager->postEvent({.event = "createworkspacev2", .data = std::format("{},{}", m_id, m_name)});
EMIT_HOOK_EVENT("createWorkspace", this);
Event::bus()->m_events.workspace.created.emit(self);
}
CWorkspace::~CWorkspace() {
Log::logger->log(Log::DEBUG, "Destroying workspace ID {}", m_id);
// check if g_pHookSystem and g_pEventManager exist, they might be destroyed as in when the compositor is closing.
if (g_pHookSystem)
g_pHookSystem->unhook(m_focusedWindowHook);
if (g_pEventManager) {
g_pEventManager->postEvent({.event = "destroyworkspace", .data = m_name});
g_pEventManager->postEvent({.event = "destroyworkspacev2", .data = std::format("{},{}", m_id, m_name)});
EMIT_HOOK_EVENT("destroyWorkspace", this);
}
Event::bus()->m_events.workspace.removed.emit(m_self);
m_events.destroy.emit();
}
@ -406,14 +407,19 @@ bool CWorkspace::isVisibleNotCovered() {
int CWorkspace::getWindows(std::optional<bool> onlyTiled, std::optional<bool> onlyPinned, std::optional<bool> onlyVisible) {
int no = 0;
for (auto const& w : g_pCompositor->m_windows) {
if (w->workspaceID() != m_id || !w->m_isMapped)
if (!m_space)
return 0;
for (auto const& t : m_space->targets()) {
if (!t)
continue;
if (onlyTiled.has_value() && w->m_isFloating == onlyTiled.value())
if (onlyTiled.has_value() && t->floating() == onlyTiled.value())
continue;
if (onlyPinned.has_value() && w->m_pinned != onlyPinned.value())
if (onlyPinned.has_value() && (!t->window() || t->window()->m_pinned != onlyPinned.value()))
continue;
if (onlyVisible.has_value() && w->isHidden() == onlyVisible.value())
if (onlyVisible.has_value() && (!t->window() || t->window()->isHidden() == onlyVisible.value()))
continue;
no++;
}
@ -423,16 +429,16 @@ int CWorkspace::getWindows(std::optional<bool> onlyTiled, std::optional<bool> on
int CWorkspace::getGroups(std::optional<bool> onlyTiled, std::optional<bool> onlyPinned, std::optional<bool> onlyVisible) {
int no = 0;
for (auto const& w : g_pCompositor->m_windows) {
if (w->workspaceID() != m_id || !w->m_isMapped)
for (auto const& g : Desktop::View::groups()) {
const auto HEAD = g->head();
if (HEAD->workspaceID() != m_id || !HEAD->m_isMapped)
continue;
if (!w->m_groupData.head)
if (onlyTiled.has_value() && HEAD->m_isFloating == onlyTiled.value())
continue;
if (onlyTiled.has_value() && w->m_isFloating == onlyTiled.value())
if (onlyPinned.has_value() && HEAD->m_pinned != onlyPinned.value())
continue;
if (onlyPinned.has_value() && w->m_pinned != onlyPinned.value())
continue;
if (onlyVisible.has_value() && w->isHidden() == onlyVisible.value())
if (onlyVisible.has_value() && g->current()->isHidden() == onlyVisible.value())
continue;
no++;
}
@ -514,13 +520,11 @@ void CWorkspace::rename(const std::string& name) {
}
void CWorkspace::updateWindows() {
m_hasFullscreenWindow = std::ranges::any_of(g_pCompositor->m_windows, [this](const auto& w) { return w->m_isMapped && w->m_workspace == m_self && w->isFullscreen(); });
m_hasFullscreenWindow = std::ranges::any_of(m_space->targets(), [](const auto& t) { return t->fullscreenMode() != FSMODE_NONE; });
for (auto const& w : g_pCompositor->m_windows) {
if (!w->m_isMapped || w->m_workspace != m_self)
continue;
w->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_ON_WORKSPACE);
for (auto const& t : m_space->targets()) {
if (t->window())
t->window()->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_ON_WORKSPACE);
}
}

View file

@ -6,6 +6,10 @@
#include "../helpers/MiscFunctions.hpp"
#include "../helpers/signal/Signal.hpp"
namespace Layout {
class CSpace;
};
enum eFullscreenMode : int8_t {
FSMODE_NONE = 0,
FSMODE_MAXIMIZED = 1 << 0,
@ -20,7 +24,9 @@ class CWorkspace {
CWorkspace(WORKSPACEID id, PHLMONITOR monitor, std::string name, bool special = false, bool isEmpty = true);
~CWorkspace();
WP<CWorkspace> m_self;
WP<CWorkspace> m_self;
SP<Layout::CSpace> m_space;
// Workspaces ID-based have IDs > 0
// and workspaces name-based have IDs starting with -1337
@ -87,13 +93,13 @@ class CWorkspace {
} m_events;
private:
void init(PHLWORKSPACE self);
void init(PHLWORKSPACE self);
SP<HOOK_CALLBACK_FN> m_focusedWindowHook;
bool m_inert = true;
CHyprSignalListener m_focusedWindowHook;
bool m_inert = true;
SP<CWorkspace> m_selfPersistent; // for persistent workspaces.
bool m_persistent = false;
SP<CWorkspace> m_selfPersistent; // for persistent workspaces.
bool m_persistent = false;
};
inline bool valid(const PHLWORKSPACE& ref) {

View file

@ -1,7 +1,7 @@
#include "WindowHistoryTracker.hpp"
#include "../../managers/HookSystemManager.hpp"
#include "../view/Window.hpp"
#include "../../event/EventBus.hpp"
using namespace Desktop;
using namespace Desktop::History;
@ -12,18 +12,12 @@ SP<CWindowHistoryTracker> History::windowTracker() {
}
CWindowHistoryTracker::CWindowHistoryTracker() {
static auto P = g_pHookSystem->hookDynamic("openWindowEarly", [this](void* self, SCallbackInfo& info, std::any data) {
auto window = std::any_cast<PHLWINDOW>(data);
static auto P = Event::bus()->m_events.window.openEarly.listen([this](PHLWINDOW pWindow) {
// add a last track
m_history.insert(m_history.begin(), window);
m_history.insert(m_history.begin(), pWindow);
});
static auto P1 = g_pHookSystem->hookDynamic("activeWindow", [this](void* self, SCallbackInfo& info, std::any data) {
auto window = std::any_cast<PHLWINDOW>(data);
track(window);
});
static auto P1 = Event::bus()->m_events.window.active.listen([this](PHLWINDOW window, uint8_t reason) { track(window); });
}
void CWindowHistoryTracker::track(PHLWINDOW w) {

View file

@ -2,9 +2,13 @@
#include "../../helpers/Monitor.hpp"
#include "../Workspace.hpp"
#include "../../managers/HookSystemManager.hpp"
#include "../state/FocusState.hpp"
#include "../../managers/eventLoop/EventLoopManager.hpp"
#include "../../event/EventBus.hpp"
#include "../../config/ConfigValue.hpp"
#include <hyprutils/utils/ScopeGuard.hpp>
using namespace Desktop;
using namespace Desktop::History;
@ -14,27 +18,16 @@ SP<CWorkspaceHistoryTracker> History::workspaceTracker() {
}
CWorkspaceHistoryTracker::CWorkspaceHistoryTracker() {
static auto P = g_pHookSystem->hookDynamic("workspace", [this](void* self, SCallbackInfo& info, std::any data) {
auto workspace = std::any_cast<PHLWORKSPACE>(data);
track(workspace);
});
static auto P = Event::bus()->m_events.workspace.active.listen([this](PHLWORKSPACE workspace) { track(workspace); });
static auto P1 = g_pHookSystem->hookDynamic("monitorAdded", [this](void* self, SCallbackInfo& info, std::any data) {
auto mon = std::any_cast<PHLMONITOR>(data);
track(mon);
});
}
CWorkspaceHistoryTracker::SMonitorData& CWorkspaceHistoryTracker::dataFor(PHLMONITOR mon) {
for (auto& ref : m_monitorDatas) {
if (ref.monitor != mon)
continue;
return ref;
}
return m_monitorDatas.emplace_back(SMonitorData{
.monitor = mon,
static auto P1 = Event::bus()->m_events.monitor.focused.listen([this](PHLMONITOR mon) {
// This sucks ASS, but we have to do this because switching to a workspace on another mon will trigger a workspace event right afterwards and we don't
// want to remember the workspace that was not visible there
// TODO: do something about this
g_pEventLoopManager->doLater([this, mon = PHLMONITORREF{mon}] {
if (mon)
track(mon->m_activeWorkspace);
});
});
}
@ -52,44 +45,32 @@ CWorkspaceHistoryTracker::SWorkspacePreviousData& CWorkspaceHistoryTracker::data
}
void CWorkspaceHistoryTracker::track(PHLWORKSPACE w) {
if (!w->m_monitor)
if (!w || !w->m_monitor || w == m_lastWorkspaceData.workspace)
return;
static auto PALLOWWORKSPACECYCLES = CConfigValue<Hyprlang::INT>("binds:allow_workspace_cycles");
static auto PALLOWWORKSPACECYCLES = CConfigValue<Hyprlang::INT>("binds:allow_workspace_cycles");
auto& data = dataFor(w);
auto& monData = dataFor(w->m_monitor.lock());
auto& data = dataFor(w);
if (!monData.workspace) {
data.previous.reset();
data.previousID = WORKSPACE_INVALID;
data.previousName = "";
Hyprutils::Utils::CScopeGuard x([&] { setLastWorkspaceData(w); });
if (m_lastWorkspaceData.workspace == w && !*PALLOWWORKSPACECYCLES)
return;
data.previous = m_lastWorkspaceData.workspace;
if (m_lastWorkspaceData.workspace) {
data.previousName = m_lastWorkspaceData.workspace->m_name;
data.previousID = m_lastWorkspaceData.workspace->m_id;
data.previousMon = m_lastWorkspaceData.workspace->m_monitor;
} else {
data.previousName = m_lastWorkspaceData.workspaceName;
data.previousID = m_lastWorkspaceData.workspaceID;
data.previousMon = m_lastWorkspaceData.monitor;
}
if (monData.workspace == w && !*PALLOWWORKSPACECYCLES) {
track(w->m_monitor.lock());
return;
}
data.previous = monData.workspace;
data.previousName = monData.workspace->m_name;
data.previousID = monData.workspace->m_id;
data.previousMon = monData.workspace->m_monitor;
track(w->m_monitor.lock());
}
void CWorkspaceHistoryTracker::track(PHLMONITOR mon) {
auto& data = dataFor(mon);
data.workspace = mon->m_activeWorkspace;
data.workspaceName = mon->m_activeWorkspace ? mon->m_activeWorkspace->m_name : "";
data.workspaceID = mon->activeWorkspaceID();
}
void CWorkspaceHistoryTracker::gc() {
std::erase_if(m_datas, [](const auto& e) { return !e.workspace; });
std::erase_if(m_monitorDatas, [](const auto& e) { return !e.monitor; });
}
const CWorkspaceHistoryTracker::SWorkspacePreviousData* CWorkspaceHistoryTracker::previousWorkspace(PHLWORKSPACE ws) {
@ -156,3 +137,15 @@ SWorkspaceIDName CWorkspaceHistoryTracker::previousWorkspaceIDName(PHLWORKSPACE
return SWorkspaceIDName{.id = DATA->previousID, .name = DATA->previousName, .isAutoIDd = DATA->previousID <= 0};
}
void CWorkspaceHistoryTracker::setLastWorkspaceData(PHLWORKSPACE w) {
if (!w) {
m_lastWorkspaceData = {};
return;
}
m_lastWorkspaceData.workspace = w;
m_lastWorkspaceData.workspaceID = w->m_id;
m_lastWorkspaceData.workspaceName = w->m_name;
m_lastWorkspaceData.monitor = w->m_monitor;
}

View file

@ -32,21 +32,19 @@ namespace Desktop::History {
SWorkspaceIDName previousWorkspaceIDName(PHLWORKSPACE ws, PHLMONITOR restrict);
private:
struct SMonitorData {
struct SLastWorkspaceData {
PHLMONITORREF monitor;
PHLWORKSPACEREF workspace;
std::string workspaceName = "";
WORKSPACEID workspaceID = WORKSPACE_INVALID;
};
} m_lastWorkspaceData;
std::vector<SWorkspacePreviousData> m_datas;
std::vector<SMonitorData> m_monitorDatas;
void track(PHLWORKSPACE w);
void track(PHLMONITOR mon);
void gc();
void setLastWorkspaceData(PHLWORKSPACE w);
SMonitorData& dataFor(PHLMONITOR mon);
SWorkspacePreviousData& dataFor(PHLWORKSPACE ws);
};

View file

@ -6,16 +6,18 @@ using namespace Desktop;
// fuck me. Writing this at 11pm, and I have an in-class test tomorrow.
// I am failing that bitch
CReservedArea::CReservedArea(const Vector2D& tl, const Vector2D& br) : m_initialTopLeft(tl), m_initialBottomRight(br) {
CReservedArea::CReservedArea(const Vector2D& tl, const Vector2D& br) : m_initialTopLeft(tl.clamp({0, 0})), m_initialBottomRight(br.clamp({0, 0})) {
calculate();
}
CReservedArea::CReservedArea(double top, double right, double bottom, double left) : m_initialTopLeft(left, top), m_initialBottomRight(right, bottom) {
CReservedArea::CReservedArea(double top, double right, double bottom, double left) :
m_initialTopLeft(std::max(left, 0.0), std::max(top, 0.0)), m_initialBottomRight(std::max(right, 0.0), std::max(bottom, 0.0)) {
calculate();
}
CReservedArea::CReservedArea(const CBox& parent, const CBox& child) {
ASSERT(!parent.empty() && !child.empty());
if (parent.empty() || child.empty())
return; // empty reserved area
ASSERT(parent.containsPoint(child.pos() + Vector2D{0.0001, 0.0001}));
ASSERT(parent.containsPoint(child.pos() + child.size() - Vector2D{0.0001, 0.0001}));
@ -81,6 +83,8 @@ void CReservedArea::addType(eReservedDynamicType t, const Vector2D& topLeft, con
auto& ref = m_dynamicReserved[t];
ref.topLeft += topLeft;
ref.bottomRight += bottomRight;
ref.topLeft = ref.topLeft.clamp({0, 0});
ref.bottomRight = ref.bottomRight.clamp({0, 0});
calculate();
}

View file

@ -52,6 +52,7 @@ static const std::unordered_map<eRuleProperty, eRuleMatchEngine> RULE_ENGINES =
{RULE_PROP_XDG_TAG, RULE_MATCH_ENGINE_REGEX}, //
{RULE_PROP_NAMESPACE, RULE_MATCH_ENGINE_REGEX}, //
{RULE_PROP_EXEC_TOKEN, RULE_MATCH_ENGINE_REGEX}, //
{RULE_PROP_EXEC_PID, RULE_MATCH_ENGINE_INT}, //
};
const std::vector<std::string>& Rule::allMatchPropStrings() {
@ -125,10 +126,11 @@ const std::string& IRule::name() {
return m_name;
}
void IRule::markAsExecRule(const std::string& token, bool persistent) {
void IRule::markAsExecRule(const std::string& token, uint64_t pid, bool persistent) {
m_execData.isExecRule = true;
m_execData.isExecPersistent = persistent;
m_execData.token = token;
m_execData.pid = pid;
m_execData.expiresAt = Time::steadyNow() + std::chrono::minutes(1);
}

View file

@ -6,7 +6,6 @@
#include "../../helpers/time/Time.hpp"
#include <vector>
#include <unordered_map>
#include <cstdint>
#include <optional>
namespace Desktop::Rule {
@ -31,6 +30,7 @@ namespace Desktop::Rule {
RULE_PROP_XDG_TAG = (1 << 16),
RULE_PROP_NAMESPACE = (1 << 17),
RULE_PROP_EXEC_TOKEN = (1 << 18),
RULE_PROP_EXEC_PID = (1 << 19),
RULE_PROP_ALL = std::numeric_limits<std::underlying_type_t<eRuleProperty>>::max(),
};
@ -52,7 +52,7 @@ namespace Desktop::Rule {
virtual std::underlying_type_t<eRuleProperty> getPropertiesMask();
void registerMatch(eRuleProperty, const std::string&);
void markAsExecRule(const std::string& token, bool persistent = false);
void markAsExecRule(const std::string& token, uint64_t pid, bool persistent = false);
bool isExecRule();
bool isExecPersistent();
bool execExpired();
@ -78,7 +78,8 @@ namespace Desktop::Rule {
bool isExecRule = false;
bool isExecPersistent = false;
std::string token;
uint64_t pid = 0;
Time::steady_tp expiresAt;
} m_execData;
};
}
}

View file

@ -4,6 +4,7 @@
#include "../../view/LayerSurface.hpp"
#include "../../types/OverridableVar.hpp"
#include "../../../helpers/MiscFunctions.hpp"
#include "../../../event/EventBus.hpp"
using namespace Desktop;
using namespace Desktop::Rule;
@ -32,11 +33,38 @@ void CLayerRuleApplicator::resetProps(std::underlying_type_t<eRuleProperty> prop
UNSET(aboveLock)
UNSET(ignoreAlpha)
UNSET(animationStyle)
#undef UNSET
if (prio == Types::PRIORITY_WINDOW_RULE)
std::erase_if(m_otherProps.props, [props](const auto& el) { return !el.second || el.second->propMask & props; });
}
void CLayerRuleApplicator::applyDynamicRule(const SP<CLayerRule>& rule) {
for (const auto& [key, effect] : rule->effects()) {
switch (key) {
default: {
if (key <= LAYER_RULE_EFFECT_LAST_STATIC) {
Log::logger->log(Log::TRACE, "CLayerRuleApplicator::applyDynamicRule: Skipping effect {}, not dynamic", sc<std::underlying_type_t<eLayerRuleEffect>>(key));
break;
}
// custom type, add to our vec
if (!m_otherProps.props.contains(key)) {
m_otherProps.props.emplace(key,
makeUnique<SCustomPropContainer>(SCustomPropContainer{
.idx = key,
.propMask = rule->getPropertiesMask(),
.effect = effect,
}));
} else {
auto& e = m_otherProps.props[key];
e->propMask |= rule->getPropertiesMask();
e->effect = effect;
}
break;
}
case LAYER_RULE_EFFECT_NONE: {
Log::logger->log(Log::ERR, "CLayerRuleApplicator::applyDynamicRule: BUG THIS: LAYER_RULE_EFFECT_NONE??");
break;
@ -73,8 +101,8 @@ void CLayerRuleApplicator::applyDynamicRule(const SP<CLayerRule>& rule) {
}
case LAYER_RULE_EFFECT_ORDER: {
try {
m_noScreenShare.first.set(std::stoi(effect), Types::PRIORITY_WINDOW_RULE);
m_noScreenShare.second |= rule->getPropertiesMask();
m_order.first.set(std::stoi(effect), Types::PRIORITY_WINDOW_RULE);
m_order.second |= rule->getPropertiesMask();
} catch (...) { Log::logger->log(Log::ERR, "CLayerRuleApplicator::applyDynamicRule: invalid order {}", effect); }
break;
}
@ -125,4 +153,7 @@ void CLayerRuleApplicator::propertiesChanged(std::underlying_type_t<eRulePropert
applyDynamicRule(wr);
}
// for plugins
Event::bus()->m_events.layer.updateRules.emit(m_ls.lock());
}

View file

@ -1,5 +1,6 @@
#pragma once
#include "LayerRuleEffectContainer.hpp"
#include "../../DesktopTypes.hpp"
#include "../Rule.hpp"
#include "../../types/OverridableVar.hpp"
@ -21,6 +22,17 @@ namespace Desktop::Rule {
void propertiesChanged(std::underlying_type_t<eRuleProperty> props);
void resetProps(std::underlying_type_t<eRuleProperty> props, Types::eOverridePriority prio = Types::PRIORITY_WINDOW_RULE);
struct SCustomPropContainer {
CLayerRuleEffectContainer::storageType idx = LAYER_RULE_EFFECT_NONE;
std::underlying_type_t<eRuleProperty> propMask = RULE_PROP_NONE;
std::string effect;
};
// This struct holds props that were dynamically registered. Plugins may read this.
struct {
std::unordered_map<CLayerRuleEffectContainer::storageType, UP<SCustomPropContainer>> props;
} m_otherProps;
#define COMMA ,
#define DEFINE_PROP(type, name, def) \
private: \

View file

@ -8,5 +8,5 @@ CWorkspaceMatchEngine::CWorkspaceMatchEngine(const std::string& s) : m_value(s)
}
bool CWorkspaceMatchEngine::match(PHLWORKSPACE ws) {
return ws->matchesStaticSelector(m_value);
return ws && ws->matchesStaticSelector(m_value);
}

View file

@ -77,7 +77,7 @@ bool CWindowRule::matches(PHLWINDOW w, bool allowEnvLookup) {
return false;
break;
case RULE_PROP_GROUP:
if (!engine->match(w->m_groupData.pNextWindow))
if (!engine->match(!!w->m_group))
return false;
break;
case RULE_PROP_MODAL:
@ -97,27 +97,31 @@ bool CWindowRule::matches(PHLWINDOW w, bool allowEnvLookup) {
return false;
break;
case RULE_PROP_CONTENT:
if (!engine->match(NContentType::toString(w->getContentType())))
if (!engine->match(w->getContentType()) && !engine->match(NContentType::toString(w->getContentType())))
return false;
break;
case RULE_PROP_XDG_TAG:
if (!w->xdgTag().has_value() || !engine->match(*w->xdgTag()))
return false;
break;
case RULE_PROP_EXEC_TOKEN:
// this is only allowed on static rules, we don't need it on dynamic plus it's expensive
if (!allowEnvLookup)
break;
const auto ENV = w->getEnv();
if (ENV.contains(EXEC_RULE_ENV_NAME)) {
const auto TKN = ENV.at(EXEC_RULE_ENV_NAME);
if (!engine->match(TKN))
return false;
break;
}
const auto ENV = w->getEnv();
bool match = false;
return false;
if (ENV.contains(EXEC_RULE_ENV_NAME)) {
if (engine->match(ENV.at(EXEC_RULE_ENV_NAME)))
match = true;
} else if (m_matchEngines.contains(RULE_PROP_EXEC_PID)) {
if (m_matchEngines.at(RULE_PROP_EXEC_PID)->match(w->getPID()))
match = true;
}
if (!match)
return false;
break;
}
}
@ -153,11 +157,6 @@ SP<CWindowRule> CWindowRule::buildFromExecString(std::string&& s) {
wr->addEffect(*EFFECT, std::string{"1"});
}
const auto TOKEN = g_pTokenManager->registerNewToken(nullptr, std::chrono::seconds(1));
wr->markAsExecRule(TOKEN, false /* TODO: could be nice. */);
wr->registerMatch(RULE_PROP_EXEC_TOKEN, TOKEN);
return wr;
}

View file

@ -4,8 +4,7 @@
#include "../utils/SetUtils.hpp"
#include "../../view/Window.hpp"
#include "../../types/OverridableVar.hpp"
#include "../../../managers/LayoutManager.hpp"
#include "../../../managers/HookSystemManager.hpp"
#include "../../../event/EventBus.hpp"
#include <hyprutils/string/String.hpp>
@ -151,7 +150,7 @@ CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyDynamicRule(const
CGradientValueData activeBorderGradient = {};
CGradientValueData inactiveBorderGradient = {};
bool active = true;
CVarList colorsAndAngles = CVarList(trim(effect.substr(effect.find_first_of(' ') + 1)), 0, 's', true);
CVarList colorsAndAngles = CVarList(trim(effect), 0, 's', true);
// Basic form has only two colors, everything else can be parsed as a gradient
if (colorsAndAngles.size() == 2 && !colorsAndAngles[1].contains("deg")) {
@ -159,6 +158,8 @@ CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyDynamicRule(const
Types::COverridableVar(CGradientValueData(CHyprColor(configStringToInt(colorsAndAngles[0]).value_or(0))), Types::PRIORITY_WINDOW_RULE);
m_inactiveBorderColor.first =
Types::COverridableVar(CGradientValueData(CHyprColor(configStringToInt(colorsAndAngles[1]).value_or(0))), Types::PRIORITY_WINDOW_RULE);
m_activeBorderColor.second |= rule->getPropertiesMask();
m_inactiveBorderColor.second |= rule->getPropertiesMask();
break;
}
@ -265,13 +266,17 @@ CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyDynamicRule(const
if (!m_window)
break;
const auto VEC = configStringToVector2D(effect);
if (VEC.x < 1 || VEC.y < 1) {
const auto VEC = m_window->calculateExpression(effect);
if (!VEC) {
Log::logger->log(Log::ERR, "failed to parse {} as an expression", effect);
break;
}
if (VEC->x < 1 || VEC->y < 1) {
Log::logger->log(Log::ERR, "Invalid size for maxsize");
break;
}
m_maxSize.first = Types::COverridableVar(VEC, Types::PRIORITY_WINDOW_RULE);
m_maxSize.first = Types::COverridableVar(*VEC, Types::PRIORITY_WINDOW_RULE);
if (*PCLAMP_TILED || m_window->m_isFloating)
m_window->clampWindowSize(std::nullopt, m_maxSize.first.value());
@ -286,13 +291,18 @@ CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyDynamicRule(const
if (!m_window)
break;
const auto VEC = configStringToVector2D(effect);
if (VEC.x < 1 || VEC.y < 1) {
const auto VEC = m_window->calculateExpression(effect);
if (!VEC) {
Log::logger->log(Log::ERR, "failed to parse {} as an expression", effect);
break;
}
if (VEC->x < 1 || VEC->y < 1) {
Log::logger->log(Log::ERR, "Invalid size for maxsize");
break;
}
m_minSize.first = Types::COverridableVar(VEC, Types::PRIORITY_WINDOW_RULE);
m_minSize.first = Types::COverridableVar(*VEC, Types::PRIORITY_WINDOW_RULE);
if (*PCLAMP_TILED || m_window->m_isFloating)
m_window->clampWindowSize(m_minSize.first.value(), std::nullopt);
} catch (std::exception& e) { Log::logger->log(Log::ERR, "minsize rule \"{}\" failed with: {}", effect, e.what()); }
@ -537,7 +547,7 @@ CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyStaticRule(const
return SRuleResult{};
}
void CWindowRuleApplicator::readStaticRules() {
void CWindowRuleApplicator::readStaticRules(bool preRead) {
if (!m_window)
return;
@ -592,7 +602,8 @@ void CWindowRuleApplicator::readStaticRules() {
for (const auto& wr : execRules) {
applyStaticRule(wr);
applyDynamicRule(wr);
ruleEngine()->unregisterRule(wr);
if (!preRead)
ruleEngine()->unregisterRule(wr);
}
}
@ -619,11 +630,13 @@ void CWindowRuleApplicator::propertiesChanged(std::underlying_type_t<eRuleProper
needsRelayout = needsRelayout || RES.needsRelayout;
}
m_window->updateWindowData();
m_window->updateWindowDecos();
m_window->updateDecorationValues();
if (needsRelayout)
g_pDecorationPositioner->forceRecalcFor(m_window.lock());
// for plugins
EMIT_HOOK_EVENT("windowUpdateRules", m_window.lock());
Event::bus()->m_events.window.updateRules.emit(m_window.lock());
}

View file

@ -33,8 +33,7 @@ namespace Desktop::Rule {
void propertiesChanged(std::underlying_type_t<eRuleProperty> props);
std::unordered_set<CWindowRuleEffectContainer::storageType> resetProps(std::underlying_type_t<eRuleProperty> props,
Types::eOverridePriority prio = Types::PRIORITY_WINDOW_RULE);
void readStaticRules();
void applyStaticRules();
void readStaticRules(bool preRead = false);
// static props
struct {

View file

@ -3,14 +3,19 @@
#include "../../Compositor.hpp"
#include "../../protocols/XDGShell.hpp"
#include "../../render/Renderer.hpp"
#include "../../managers/LayoutManager.hpp"
#include "../../managers/EventManager.hpp"
#include "../../managers/HookSystemManager.hpp"
#include "../../managers/input/InputManager.hpp"
#include "../../managers/SeatManager.hpp"
#include "../../xwayland/XSurface.hpp"
#include "../../protocols/PointerConstraints.hpp"
#include "managers/animation/DesktopAnimationManager.hpp"
#include "../../layout/LayoutManager.hpp"
#include "../../event/EventBus.hpp"
using namespace Desktop;
#define COMMA ,
SP<CFocusState> Desktop::focusState() {
static SP<CFocusState> state = makeShared<CFocusState>();
return state;
@ -32,6 +37,7 @@ static SFullscreenWorkspaceFocusResult onFullscreenWorkspaceFocusWindow(PHLWINDO
if (pWindow->m_isFloating) {
// if the window is floating, just bring it to the top
pWindow->m_createdOverFullscreen = true;
g_pDesktopAnimationManager->setFullscreenFloatingFade(pWindow, 1.f);
g_pHyprRenderer->damageWindow(pWindow);
return {};
}
@ -61,7 +67,7 @@ static SFullscreenWorkspaceFocusResult onFullscreenWorkspaceFocusWindow(PHLWINDO
return {};
}
void CFocusState::fullWindowFocus(PHLWINDOW pWindow, SP<CWLSurfaceResource> surface, bool forceFSCycle) {
void CFocusState::fullWindowFocus(PHLWINDOW pWindow, eFocusReason reason, SP<CWLSurfaceResource> surface, bool forceFSCycle) {
if (pWindow) {
if (!pWindow->m_workspace)
return;
@ -81,10 +87,10 @@ void CFocusState::fullWindowFocus(PHLWINDOW pWindow, SP<CWLSurfaceResource> surf
return;
}
rawWindowFocus(pWindow, surface);
rawWindowFocus(pWindow, reason, surface);
}
void CFocusState::rawWindowFocus(PHLWINDOW pWindow, SP<CWLSurfaceResource> surface) {
void CFocusState::rawWindowFocus(PHLWINDOW pWindow, eFocusReason reason, SP<CWLSurfaceResource> surface) {
static auto PFOLLOWMOUSE = CConfigValue<Hyprlang::INT>("input:follow_mouse");
static auto PSPECIALFALLTHROUGH = CConfigValue<Hyprlang::INT>("input:special_fallthrough");
@ -103,7 +109,9 @@ void CFocusState::rawWindowFocus(PHLWINDOW pWindow, SP<CWLSurfaceResource> surfa
if (pWindow && pWindow->m_isX11 && pWindow->isX11OverrideRedirect() && !pWindow->m_xwaylandSurface->wantsFocus())
return;
g_pLayoutManager->getCurrentLayout()->bringWindowToTop(pWindow);
// m_target on purpose, this avoids the group
if (pWindow)
g_layoutManager->bringTargetToTop(pWindow->m_target);
if (!pWindow || !validMapped(pWindow)) {
@ -125,9 +133,7 @@ void CFocusState::rawWindowFocus(PHLWINDOW pWindow, SP<CWLSurfaceResource> surfa
g_pEventManager->postEvent(SHyprIPCEvent{"activewindow", ","});
g_pEventManager->postEvent(SHyprIPCEvent{"activewindowv2", ""});
EMIT_HOOK_EVENT("activeWindow", PHLWINDOW{nullptr});
g_pLayoutManager->getCurrentLayout()->onWindowFocusChange(nullptr);
Event::bus()->m_events.window.active.emit(nullptr, reason);
m_focusSurface.reset();
@ -194,16 +200,14 @@ void CFocusState::rawWindowFocus(PHLWINDOW pWindow, SP<CWLSurfaceResource> surfa
g_pEventManager->postEvent(SHyprIPCEvent{.event = "activewindow", .data = pWindow->m_class + "," + pWindow->m_title});
g_pEventManager->postEvent(SHyprIPCEvent{.event = "activewindowv2", .data = std::format("{:x}", rc<uintptr_t>(pWindow.get()))});
EMIT_HOOK_EVENT("activeWindow", pWindow);
g_pLayoutManager->getCurrentLayout()->onWindowFocusChange(pWindow);
Event::bus()->m_events.window.active.emit(pWindow, reason);
g_pInputManager->recheckIdleInhibitorStatus();
if (*PFOLLOWMOUSE == 0)
g_pInputManager->sendMotionEventsToFocused();
if (pWindow->m_groupData.pNextWindow)
if (pWindow->m_group)
pWindow->deactivateGroupMembers();
}
@ -229,7 +233,7 @@ void CFocusState::rawSurfaceFocus(SP<CWLSurfaceResource> pSurface, PHLWINDOW pWi
g_pSeatManager->setKeyboardFocus(nullptr);
g_pEventManager->postEvent(SHyprIPCEvent{.event = "activewindow", .data = ","});
g_pEventManager->postEvent(SHyprIPCEvent{.event = "activewindowv2", .data = ""});
EMIT_HOOK_EVENT("keyboardFocus", SP<CWLSurfaceResource>{nullptr});
Event::bus()->m_events.input.keyboard.focus.emit(nullptr);
m_focusSurface.reset();
return;
}
@ -245,7 +249,7 @@ void CFocusState::rawSurfaceFocus(SP<CWLSurfaceResource> pSurface, PHLWINDOW pWi
g_pXWaylandManager->activateSurface(pSurface, true);
m_focusSurface = pSurface;
EMIT_HOOK_EVENT("keyboardFocus", pSurface);
Event::bus()->m_events.input.keyboard.focus.emit(pSurface);
const auto SURF = Desktop::View::CWLSurface::fromResource(pSurface);
const auto OLDSURF = Desktop::View::CWLSurface::fromResource(PLASTSURF);
@ -274,7 +278,7 @@ void CFocusState::rawMonitorFocus(PHLMONITOR pMonitor) {
g_pEventManager->postEvent(SHyprIPCEvent{.event = "focusedmon", .data = pMonitor->m_name + "," + WORKSPACE_NAME});
g_pEventManager->postEvent(SHyprIPCEvent{.event = "focusedmonv2", .data = pMonitor->m_name + "," + WORKSPACE_ID});
EMIT_HOOK_EVENT("focusedMon", pMonitor);
Event::bus()->m_events.monitor.focused.emit(pMonitor);
m_focusMonitor = pMonitor;
}
@ -294,3 +298,7 @@ void CFocusState::resetWindowFocus() {
m_focusWindow.reset();
m_focusSurface.reset();
}
bool Desktop::isHardInputFocusReason(eFocusReason r) {
return r == FOCUS_REASON_NEW_WINDOW || r == FOCUS_REASON_KEYBIND || r == FOCUS_REASON_GHOSTS || r == FOCUS_REASON_CLICK || r == FOCUS_REASON_DESKTOP_STATE_CHANGE;
}

View file

@ -1,11 +1,24 @@
#pragma once
#include "../DesktopTypes.hpp"
#include "../../SharedDefs.hpp"
#include "../../helpers/signal/Signal.hpp"
class CWLSurfaceResource;
namespace Desktop {
enum eFocusReason : uint8_t {
FOCUS_REASON_UNKNOWN = 0,
FOCUS_REASON_FFM,
FOCUS_REASON_KEYBIND,
FOCUS_REASON_CLICK,
FOCUS_REASON_OTHER,
FOCUS_REASON_DESKTOP_STATE_CHANGE,
FOCUS_REASON_NEW_WINDOW,
FOCUS_REASON_GHOSTS,
};
bool isHardInputFocusReason(eFocusReason r);
class CFocusState {
public:
CFocusState();
@ -15,8 +28,8 @@ namespace Desktop {
CFocusState(CFocusState&) = delete;
CFocusState(const CFocusState&) = delete;
void fullWindowFocus(PHLWINDOW w, SP<CWLSurfaceResource> surface = nullptr, bool forceFSCycle = false);
void rawWindowFocus(PHLWINDOW w, SP<CWLSurfaceResource> surface = nullptr);
void fullWindowFocus(PHLWINDOW w, eFocusReason reason, SP<CWLSurfaceResource> surface = nullptr, bool forceFSCycle = false);
void rawWindowFocus(PHLWINDOW w, eFocusReason reason, SP<CWLSurfaceResource> surface = nullptr);
void rawSurfaceFocus(SP<CWLSurfaceResource> s, PHLWINDOW pWindowOwner = nullptr);
void rawMonitorFocus(PHLMONITOR m);
@ -31,7 +44,7 @@ namespace Desktop {
PHLWINDOWREF m_focusWindow;
PHLMONITORREF m_focusMonitor;
SP<HOOK_CALLBACK_FN> m_windowOpen, m_windowClose;
CHyprSignalListener m_windowOpen, m_windowClose;
};
SP<CFocusState> focusState();

351
src/desktop/view/Group.cpp Normal file
View file

@ -0,0 +1,351 @@
#include "Group.hpp"
#include "Window.hpp"
#include "../../render/decorations/CHyprGroupBarDecoration.hpp"
#include "../../layout/target/WindowGroupTarget.hpp"
#include "../../layout/target/WindowTarget.hpp"
#include "../../layout/target/Target.hpp"
#include "../../layout/space/Space.hpp"
#include "../../layout/LayoutManager.hpp"
#include "../../desktop/state/FocusState.hpp"
#include "../../Compositor.hpp"
#include <algorithm>
using namespace Desktop;
using namespace Desktop::View;
std::vector<WP<CGroup>>& View::groups() {
static std::vector<WP<CGroup>> g;
return g;
}
SP<CGroup> CGroup::create(std::vector<PHLWINDOWREF>&& windows) {
auto x = SP<CGroup>(new CGroup(std::move(windows)));
x->m_self = x;
x->m_target = Layout::CWindowGroupTarget::create(x);
groups().emplace_back(x);
x->init();
return x;
}
CGroup::CGroup(std::vector<PHLWINDOWREF>&& windows) : m_windows(std::move(windows)) {
;
}
void CGroup::init() {
// for proper group logic:
// - add all windows to us
// - replace the first window with our target
// - remove all window targets from layout
// - apply updates
// FIXME: what if some windows are grouped? For now we only do 1-window but YNK
for (const auto& w : m_windows) {
RASSERT(!w->m_group, "CGroup: windows cannot contain grouped in init, this will explode");
w->m_group = m_self.lock();
}
g_layoutManager->switchTargets(m_windows.at(0)->m_target, m_target);
for (const auto& w : m_windows) {
w->m_target->setSpaceGhost(m_target->space());
}
for (const auto& w : m_windows) {
applyWindowDecosAndUpdates(w.lock());
}
updateWindowVisibility();
}
void CGroup::destroy() {
while (true) {
if (m_windows.size() == 1) {
remove(m_windows.at(0).lock());
break;
}
remove(m_windows.at(0).lock());
}
}
CGroup::~CGroup() {
if (m_target->space())
m_target->assignToSpace(nullptr);
std::erase_if(groups(), [this](const auto& e) { return !e || e == m_self; });
}
bool CGroup::has(PHLWINDOW w) const {
return std::ranges::contains(m_windows, w);
}
void CGroup::add(PHLWINDOW w) {
static auto INSERT_AFTER_CURRENT = CConfigValue<Hyprlang::INT>("group:insert_after_current");
if (w->m_group) {
if (w->m_group == m_self)
return;
const auto WINDOWS = w->m_group->windows();
for (const auto& w : WINDOWS) {
w->m_group->remove(w.lock());
add(w.lock());
}
return;
}
if (w->layoutTarget()->space()) {
// remove the target from a space if it is in one
g_layoutManager->removeTarget(w->layoutTarget());
}
w->m_group = m_self.lock();
w->m_target->setSpaceGhost(m_target->space());
w->m_target->setFloating(m_target->floating());
if (*INSERT_AFTER_CURRENT) {
m_windows.insert(m_windows.begin() + m_current + 1, w);
m_current++;
} else {
m_windows.emplace_back(w);
m_current = m_windows.size() - 1;
}
applyWindowDecosAndUpdates(w);
updateWindowVisibility();
m_target->recalc();
}
void CGroup::remove(PHLWINDOW w, Math::eDirection dir) {
std::optional<size_t> idx;
for (size_t i = 0; i < m_windows.size(); ++i) {
if (m_windows.at(i) == w) {
idx = i;
break;
}
}
if (!idx)
return;
if ((m_current >= *idx && idx != 0) || (m_current >= m_windows.size() - 1 && m_current > 0))
m_current--;
auto g = m_self.lock(); // keep ref to avoid uaf after w->m_group.reset()
w->m_group.reset();
removeWindowDecos(w);
w->setHidden(false);
const bool REMOVING_GROUP = m_windows.size() <= 1;
if (REMOVING_GROUP) {
w->m_target->assignToSpace(nullptr);
g_layoutManager->switchTargets(m_target, w->m_target);
}
// we do it after the above because switchTargets expects this to be a valid group
m_windows.erase(m_windows.begin() + *idx);
if (!m_windows.empty())
updateWindowVisibility();
// do this here: otherwise the new current is hidden and workspace rules get wrong data
if (!REMOVING_GROUP) {
std::optional<Vector2D> focalPoint;
if (dir != Math::DIRECTION_DEFAULT) {
const auto box = m_target->position();
switch (dir) {
case Math::DIRECTION_RIGHT: focalPoint = Vector2D(box.x + box.w, box.y + box.h / 2.0); break;
case Math::DIRECTION_LEFT: focalPoint = Vector2D(box.x, box.y + box.h / 2.0); break;
case Math::DIRECTION_DOWN: focalPoint = Vector2D(box.x + box.w / 2.0, box.y + box.h); break;
case Math::DIRECTION_UP: focalPoint = Vector2D(box.x + box.w / 2.0, box.y); break;
default: break;
}
}
w->m_target->assignToSpace(m_target->space(), focalPoint);
}
}
void CGroup::moveCurrent(bool next) {
size_t idx = m_current;
if (next) {
idx++;
if (idx >= m_windows.size())
idx = 0;
} else {
if (idx == 0)
idx = m_windows.size() - 1;
else
idx--;
}
setCurrent(idx);
}
void CGroup::setCurrent(size_t idx) {
if (idx == m_current)
return;
const auto FS_STATE = m_target->fullscreenMode();
const auto WASFOCUS = Desktop::focusState()->window() == current();
auto oldWindow = m_windows.at(m_current).lock();
if (FS_STATE != FSMODE_NONE)
g_pCompositor->setWindowFullscreenInternal(oldWindow, FSMODE_NONE);
m_current = std::clamp(idx, sc<size_t>(0), m_windows.size() - 1);
updateWindowVisibility();
auto newWindow = m_windows.at(m_current).lock();
if (FS_STATE != FSMODE_NONE) {
g_pCompositor->setWindowFullscreenInternal(newWindow, FS_STATE);
newWindow->m_target->warpPositionSize();
oldWindow->m_target->setPositionGlobal(newWindow->m_target->position()); // TODO: this is a hack and sucks
}
if (WASFOCUS)
Desktop::focusState()->rawWindowFocus(current(), FOCUS_REASON_DESKTOP_STATE_CHANGE);
}
void CGroup::setCurrent(PHLWINDOW w) {
if (w == current())
return;
for (size_t i = 0; i < m_windows.size(); ++i) {
if (m_windows.at(i) == w) {
setCurrent(i);
return;
}
}
}
size_t CGroup::getCurrentIdx() const {
return m_current;
}
PHLWINDOW CGroup::head() const {
return m_windows.front().lock();
}
PHLWINDOW CGroup::tail() const {
return m_windows.back().lock();
}
PHLWINDOW CGroup::current() const {
return m_windows.at(m_current).lock();
}
PHLWINDOW CGroup::next() const {
return (m_current >= m_windows.size() - 1 ? m_windows.front() : m_windows.at(m_current + 1)).lock();
}
PHLWINDOW CGroup::fromIndex(size_t idx) const {
if (idx >= m_windows.size())
return nullptr;
return m_windows.at(idx).lock();
}
const std::vector<PHLWINDOWREF>& CGroup::windows() const {
return m_windows;
}
void CGroup::applyWindowDecosAndUpdates(PHLWINDOW x) {
x->addWindowDeco(makeUnique<CHyprGroupBarDecoration>(x));
x->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_GROUP | Desktop::Rule::RULE_PROP_ON_WORKSPACE);
x->updateWindowDecos();
x->updateDecorationValues();
}
void CGroup::removeWindowDecos(PHLWINDOW x) {
x->removeWindowDeco(x->getDecorationByType(DECORATION_GROUPBAR));
x->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_GROUP | Desktop::Rule::RULE_PROP_ON_WORKSPACE);
x->updateWindowDecos();
x->updateDecorationValues();
}
void CGroup::updateWindowVisibility() {
for (size_t i = 0; i < m_windows.size(); ++i) {
if (i == m_current) {
auto& x = m_windows.at(i);
x->setHidden(false);
x->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_GROUP | Desktop::Rule::RULE_PROP_ON_WORKSPACE);
x->updateWindowDecos();
x->updateDecorationValues();
} else
m_windows.at(i)->setHidden(true);
}
m_target->recalc();
m_target->damageEntire();
}
size_t CGroup::size() const {
return m_windows.size();
}
bool CGroup::locked() const {
return m_locked;
}
void CGroup::setLocked(bool x) {
m_locked = x;
}
bool CGroup::denied() const {
return m_deny;
}
void CGroup::setDenied(bool x) {
m_deny = x;
}
void CGroup::updateWorkspace(PHLWORKSPACE ws) {
if (!ws)
return;
for (const auto& w : windows()) {
w->m_monitor = ws->m_monitor;
w->moveToWorkspace(ws);
w->updateToplevel();
w->updateWindowDecos();
w->m_target->setSpaceGhost(ws->m_space);
}
}
void CGroup::swapWithNext() {
const bool HAD_FOCUS = Desktop::focusState()->window() == m_windows.at(m_current);
size_t idx = m_current + 1 >= m_windows.size() ? 0 : m_current + 1;
std::iter_swap(m_windows.begin() + m_current, m_windows.begin() + idx);
m_current = idx;
updateWindowVisibility();
if (HAD_FOCUS)
Desktop::focusState()->fullWindowFocus(m_windows.at(m_current).lock(), FOCUS_REASON_DESKTOP_STATE_CHANGE);
}
void CGroup::swapWithLast() {
const bool HAD_FOCUS = Desktop::focusState()->window() == m_windows.at(m_current);
size_t idx = m_current == 0 ? m_windows.size() - 1 : m_current - 1;
std::iter_swap(m_windows.begin() + m_current, m_windows.begin() + idx);
m_current = idx;
updateWindowVisibility();
if (HAD_FOCUS)
Desktop::focusState()->fullWindowFocus(m_windows.at(m_current).lock(), FOCUS_REASON_DESKTOP_STATE_CHANGE);
}

View file

@ -0,0 +1,69 @@
#pragma once
#include "../DesktopTypes.hpp"
#include "../../helpers/math/Direction.hpp"
#include <vector>
namespace Layout {
class CWindowGroupTarget;
};
namespace Desktop::View {
class CGroup {
public:
static SP<CGroup> create(std::vector<PHLWINDOWREF>&& windows);
~CGroup();
bool has(PHLWINDOW w) const;
void add(PHLWINDOW w);
void remove(PHLWINDOW w, Math::eDirection dir = Math::DIRECTION_DEFAULT);
void moveCurrent(bool next);
void setCurrent(size_t idx);
void setCurrent(PHLWINDOW w);
size_t getCurrentIdx() const;
size_t size() const;
void destroy();
void updateWorkspace(PHLWORKSPACE);
void swapWithNext();
void swapWithLast();
PHLWINDOW head() const;
PHLWINDOW tail() const;
PHLWINDOW current() const;
PHLWINDOW next() const;
PHLWINDOW fromIndex(size_t idx) const;
bool locked() const;
void setLocked(bool x);
bool denied() const;
void setDenied(bool x);
const std::vector<PHLWINDOWREF>& windows() const;
SP<Layout::CWindowGroupTarget> m_target;
private:
CGroup(std::vector<PHLWINDOWREF>&& windows);
void applyWindowDecosAndUpdates(PHLWINDOW x);
void removeWindowDecos(PHLWINDOW x);
void init();
void updateWindowVisibility();
WP<CGroup> m_self;
std::vector<PHLWINDOWREF> m_windows;
bool m_locked = false;
bool m_deny = false;
size_t m_current = 0;
};
std::vector<WP<CGroup>>& groups();
};

View file

@ -10,8 +10,8 @@
#include "../../config/ConfigManager.hpp"
#include "../../helpers/Monitor.hpp"
#include "../../managers/input/InputManager.hpp"
#include "../../managers/HookSystemManager.hpp"
#include "../../managers/EventManager.hpp"
#include "../../event/EventBus.hpp"
using namespace Desktop;
using namespace Desktop::View;
@ -23,24 +23,11 @@ PHLLS CLayerSurface::create(SP<CLayerShellResource> resource) {
pLS->m_wlSurface->assign(resource->m_surface.lock(), pLS);
if (!pMonitor) {
Log::logger->log(Log::ERR, "New LS has no monitor??");
return pLS;
}
if (pMonitor->m_mirrorOf)
pMonitor = g_pCompositor->m_monitors.front();
pLS->m_self = pLS;
pLS->m_namespace = resource->m_layerNamespace;
pLS->m_layer = resource->m_current.layer;
pLS->m_popupHead = CPopup::create(pLS);
pLS->m_monitor = pMonitor;
pMonitor->m_layerSurfaceLayers[resource->m_current.layer].emplace_back(pLS);
pLS->m_ruleApplicator = makeUnique<Desktop::Rule::CLayerRuleApplicator>(pLS);
pLS->m_self = pLS;
pLS->m_namespace = resource->m_layerNamespace;
pLS->m_layer = std::clamp(resource->m_current.layer, ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY);
pLS->m_popupHead = CPopup::create(pLS);
g_pAnimationManager->createAnimation(0.f, pLS->m_alpha, g_pConfigManager->getAnimationPropertyConfig("fadeLayersIn"), pLS, AVARDAMAGE_ENTIRE);
g_pAnimationManager->createAnimation(Vector2D(0, 0), pLS->m_realPosition, g_pConfigManager->getAnimationPropertyConfig("layersIn"), pLS, AVARDAMAGE_ENTIRE);
@ -50,6 +37,19 @@ PHLLS CLayerSurface::create(SP<CLayerShellResource> resource) {
pLS->m_alpha->setValueAndWarp(0.f);
if (!pMonitor) {
Log::logger->log(Log::DEBUG, "LayerSurface {:x} (namespace {} layer {}) created on NO MONITOR ?!", rc<uintptr_t>(resource.get()), resource->m_layerNamespace,
sc<int>(pLS->m_layer));
return pLS;
}
if (pMonitor->m_mirrorOf)
pMonitor = g_pCompositor->m_monitors.front();
pLS->m_monitor = pMonitor;
pMonitor->m_layerSurfaceLayers[resource->m_current.layer].emplace_back(pLS);
Log::logger->log(Log::DEBUG, "LayerSurface {:x} (namespace {} layer {}) created on monitor {}", rc<uintptr_t>(resource.get()), resource->m_layerNamespace,
sc<int>(pLS->m_layer), pMonitor->m_name);
@ -222,7 +222,7 @@ void CLayerSurface::onMap() {
m_fadingOut = false;
g_pEventManager->postEvent(SHyprIPCEvent{.event = "openlayer", .data = m_namespace});
EMIT_HOOK_EVENT("openLayer", m_self.lock());
Event::bus()->m_events.layer.opened.emit(m_self.lock());
g_pCompositor->setPreferredScaleForSurface(m_wlSurface->resource(), PMONITOR->m_scale);
g_pCompositor->setPreferredTransformForSurface(m_wlSurface->resource(), PMONITOR->m_transform);
@ -232,7 +232,7 @@ void CLayerSurface::onUnmap() {
Log::logger->log(Log::DEBUG, "LayerSurface {:x} unmapped", rc<uintptr_t>(m_layerSurface.get()));
g_pEventManager->postEvent(SHyprIPCEvent{.event = "closelayer", .data = m_layerSurface->m_layerNamespace});
EMIT_HOOK_EVENT("closeLayer", m_self.lock());
Event::bus()->m_events.layer.closed.emit(m_self.lock());
std::erase_if(g_pInputManager->m_exclusiveLSes, [this](const auto& other) { return !other || other == m_self; });
@ -323,16 +323,18 @@ void CLayerSurface::onCommit() {
if (m_layerSurface->m_current.committed != 0) {
if (m_layerSurface->m_current.committed & CLayerShellResource::eCommittedState::STATE_LAYER && m_layerSurface->m_current.layer != m_layer) {
const auto NEW_LAYER = std::clamp(m_layerSurface->m_current.layer, ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY);
for (auto it = PMONITOR->m_layerSurfaceLayers[m_layer].begin(); it != PMONITOR->m_layerSurfaceLayers[m_layer].end(); it++) {
if (*it == m_self) {
PMONITOR->m_layerSurfaceLayers[m_layerSurface->m_current.layer].emplace_back(*it);
PMONITOR->m_layerSurfaceLayers[NEW_LAYER].emplace_back(*it);
PMONITOR->m_layerSurfaceLayers[m_layer].erase(it);
break;
}
}
m_layer = m_layerSurface->m_current.layer;
m_aboveFullscreen = m_layerSurface->m_current.layer >= ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY;
m_layer = NEW_LAYER;
m_aboveFullscreen = NEW_LAYER >= ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY;
// if in fullscreen, only overlay can be above.
*m_alpha = PMONITOR->inFullscreenMode() ? (m_layer >= ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY ? 1.F : 0.F) : 1.F;

View file

@ -9,6 +9,7 @@
#include "../../managers/animation/AnimationManager.hpp"
#include "LayerSurface.hpp"
#include "../../managers/input/InputManager.hpp"
#include "../../managers/eventLoop/EventLoopManager.hpp"
#include "../../render/Renderer.hpp"
#include "../../render/OpenGL.hpp"
#include <ranges>
@ -108,8 +109,12 @@ void CPopup::initAllSignals() {
m_alpha->setCallbackOnEnd(
[this](auto) {
if (inert()) {
g_pHyprRenderer->damageBox(CBox{coordsGlobal(), size()});
fullyDestroy();
g_pEventLoopManager->doLater([p = m_self] {
if (!p)
return;
g_pHyprRenderer->damageBox(CBox{p->coordsGlobal(), p->size()});
p->fullyDestroy();
});
}
},
false);
@ -194,7 +199,7 @@ void CPopup::onMap() {
//unconstrain();
sendScale();
m_resource->m_surface->m_surface->enter(PMONITOR->m_self.lock());
m_wlSurface->resource()->breadthfirst([PMONITOR](SP<CWLSurfaceResource> s, const Vector2D& offset, void* d) { s->enter(PMONITOR->m_self.lock()); }, nullptr);
if (!m_layerOwner.expired() && m_layerOwner->m_layer < ZWLR_LAYER_SHELL_V1_LAYER_TOP)
g_pHyprOpenGL->markBlurDirtyForMonitor(g_pCompositor->getMonitorFromID(m_layerOwner->m_layer));
@ -334,8 +339,7 @@ void CPopup::reposition() {
if (!PMONITOR)
return;
CBox box = {PMONITOR->m_position.x, PMONITOR->m_position.y, PMONITOR->m_size.x, PMONITOR->m_size.y};
m_resource->applyPositioning(box, COORDS);
m_resource->applyPositioning(m_windowOwner ? PMONITOR->logicalBoxMinusReserved() : PMONITOR->logicalBox(), COORDS);
}
SP<Desktop::View::CWLSurface> CPopup::getT1Owner() const {
@ -399,10 +403,14 @@ void CPopup::recheckChildrenRecursive() {
std::vector<WP<CPopup>> cpy;
std::ranges::for_each(m_children, [&cpy](const auto& el) { cpy.emplace_back(el); });
for (auto const& c : cpy) {
if (!c->visible())
if (!c || !c->visible())
continue;
c->onCommit(true);
c->recheckChildrenRecursive();
// keep ref, onCommit can call onDestroy
auto x = c.lock();
x->onCommit(true);
x->recheckChildrenRecursive();
}
}

File diff suppressed because it is too large Load diff

View file

@ -26,8 +26,19 @@ struct SWorkspaceRule;
class IWindowTransformer;
namespace Layout {
class ITarget;
class CWindowTarget;
}
namespace Desktop {
enum eFocusReason : uint8_t;
}
namespace Desktop::View {
class CGroup;
enum eGroupRules : uint8_t {
// effective only during first map, except for _ALWAYS variant
GROUP_NONE = 0,
@ -38,6 +49,7 @@ namespace Desktop::View {
GROUP_LOCK_ALWAYS = 1 << 4,
GROUP_INVADE = 1 << 5, // Force enter a group, event if lock is engaged
GROUP_OVERRIDE = 1 << 6, // Override other rules
GROUP_DENY = 1 << 7, // deny
};
enum eGetWindowProperties : uint8_t {
@ -61,6 +73,11 @@ namespace Desktop::View {
SUPPRESS_FULLSCREEN_OUTPUT = 1 << 4,
};
struct SWindowActiveEvent {
PHLWINDOW window = nullptr;
eFocusReason reason = sc<eFocusReason>(0) /* unknown */;
};
struct SInitialWorkspaceToken {
PHLWINDOWREF primaryOwner;
std::string workspace;
@ -92,11 +109,17 @@ namespace Desktop::View {
struct {
CSignalT<> destroy;
CSignalT<> unmap;
CSignalT<> hide;
CSignalT<> resize;
CSignalT<> monitorChanged;
} m_events;
WP<CXDGSurfaceResource> m_xdgSurface;
WP<CXWaylandSurface> m_xwaylandSurface;
SP<Layout::ITarget> m_target;
// this is the position and size of the "bounding box"
Vector2D m_position = Vector2D(0, 0);
Vector2D m_size = Vector2D(0, 0);
@ -112,30 +135,21 @@ namespace Desktop::View {
std::optional<std::pair<uint32_t, Vector2D>> m_pendingSizeAck;
std::vector<std::pair<uint32_t, Vector2D>> m_pendingSizeAcks;
// for restoring floating statuses
Vector2D m_lastFloatingSize;
Vector2D m_lastFloatingPosition;
// for floating window offset in workspace animations
Vector2D m_floatingOffset = Vector2D(0, 0);
// this is used for pseudotiling
bool m_isPseudotiled = false;
Vector2D m_pseudoSize = Vector2D(1280, 720);
// for recovering relative cursor position
Vector2D m_relativeCursorCoordsOnLastWarp = Vector2D(-1, -1);
bool m_firstMap = false; // for layouts
bool m_isFloating = false;
bool m_draggingTiled = false; // for dragging around tiled windows
SFullscreenState m_fullscreenState = {.internal = FSMODE_NONE, .client = FSMODE_NONE};
std::string m_title = "";
std::string m_class = "";
std::string m_initialTitle = "";
std::string m_initialClass = "";
PHLWORKSPACE m_workspace;
PHLMONITORREF m_monitor;
PHLMONITORREF m_monitor, m_prevMonitor;
bool m_isMapped = false;
@ -229,15 +243,13 @@ namespace Desktop::View {
std::string m_initialWorkspaceToken = "";
// for groups
struct SGroupData {
PHLWINDOWREF pNextWindow; // nullptr means no grouping. Self means single group.
bool head = false;
bool locked = false; // per group lock
bool deny = false; // deny window from enter a group or made a group
} m_groupData;
uint16_t m_groupRules = Desktop::View::GROUP_NONE;
SP<CGroup> m_group;
uint16_t m_groupRules = Desktop::View::GROUP_NONE;
bool m_tearingHint = false;
bool m_tearingHint = false;
// Stable ID for ext_foreign_toplevel_list
const uint64_t m_stableID = 0x2137;
// ANR
PHLANIMVAR<float> m_notRespondingTint;
@ -300,21 +312,6 @@ namespace Desktop::View {
bool isInCurvedCorner(double x, double y);
bool hasPopupAt(const Vector2D& pos);
int popupsCount();
void applyGroupRules();
void createGroup();
void destroyGroup();
PHLWINDOW getGroupHead();
PHLWINDOW getGroupTail();
PHLWINDOW getGroupCurrent();
PHLWINDOW getGroupPrevious();
PHLWINDOW getGroupWindowByIndex(int);
bool hasInGroup(PHLWINDOW);
int getGroupSize();
bool canBeGroupedInto(PHLWINDOW pWindow);
void setGroupCurrent(PHLWINDOW pWindow);
void insertWindowToGroup(PHLWINDOW pWindow);
void updateGroupOutputs();
void switchWithWindowInGroup(PHLWINDOW pWindow);
void setAnimationsToMove();
void onWorkspaceAnimUpdate();
void onFocusAnimUpdate();
@ -328,8 +325,6 @@ namespace Desktop::View {
PHLWINDOW getSwallower();
bool isX11OverrideRedirect();
bool isModal();
Vector2D requestedMinSize();
Vector2D requestedMaxSize();
Vector2D realToReportSize();
Vector2D realToReportPosition();
Vector2D xwaylandSizeToReal(Vector2D size);
@ -349,6 +344,8 @@ namespace Desktop::View {
std::optional<Vector2D> calculateExpression(const std::string& s);
std::optional<Vector2D> minSize();
std::optional<Vector2D> maxSize();
SP<Layout::ITarget> layoutTarget();
bool canBeGroupedInto(SP<CGroup> group);
CBox getWindowMainSurfaceBox() const {
return {m_realPosition->value().x, m_realPosition->value().y, m_realSize->value().x, m_realSize->value().y};

8
src/event/EventBus.cpp Normal file
View file

@ -0,0 +1,8 @@
#include "EventBus.hpp"
using namespace Event;
UP<CEventBus>& Event::bus() {
static UP<CEventBus> p = makeUnique<CEventBus>();
return p;
}

144
src/event/EventBus.hpp Normal file
View file

@ -0,0 +1,144 @@
#pragma once
#include "../helpers/memory/Memory.hpp"
#include "../helpers/signal/Signal.hpp"
#include "../helpers/math/Math.hpp"
#include "../devices/IPointer.hpp"
#include "../devices/IKeyboard.hpp"
#include "../devices/Tablet.hpp"
#include "../devices/ITouch.hpp"
#include "../desktop/DesktopTypes.hpp"
#include "../SharedDefs.hpp"
namespace Desktop {
enum eFocusReason : uint8_t;
}
namespace Event {
struct SCallbackInfo {
bool cancelled = false; /* on cancellable events, will cancel the event. */
};
class CEventBus {
public:
CEventBus() = default;
~CEventBus() = default;
template <typename... Args>
using Event = CSignalT<Args...>;
template <typename... Args>
using Cancellable = CSignalT<Args..., SCallbackInfo&>;
struct {
Event<> ready;
Event<> tick;
struct {
Event<PHLWINDOW> open;
Event<PHLWINDOW> openEarly;
Event<PHLWINDOW> destroy;
Event<PHLWINDOW> close;
Event<PHLWINDOW> kill;
Event<PHLWINDOW, Desktop::eFocusReason> active;
Event<PHLWINDOW> urgent;
Event<PHLWINDOW> title;
Event<PHLWINDOW> class_;
Event<PHLWINDOW> pin;
Event<PHLWINDOW> fullscreen;
Event<PHLWINDOW> updateRules;
Event<PHLWINDOW, PHLWORKSPACE> moveToWorkspace;
} window;
struct {
Event<PHLLS> opened;
Event<PHLLS> closed;
Event<PHLLS> updateRules;
} layer;
struct {
struct {
Cancellable<Vector2D> move;
Cancellable<IPointer::SButtonEvent> button;
Cancellable<IPointer::SAxisEvent> axis;
} mouse;
struct {
Cancellable<IKeyboard::SKeyEvent> key;
Event<SP<IKeyboard>, const std::string&> layout;
Event<SP<CWLSurfaceResource>> focus;
} keyboard;
struct {
Cancellable<CTablet::SAxisEvent> axis;
Cancellable<CTablet::SButtonEvent> button;
Cancellable<CTablet::SProximityEvent> proximity;
Cancellable<CTablet::STipEvent> tip;
} tablet;
struct {
Cancellable<ITouch::SCancelEvent> cancel;
Cancellable<ITouch::SDownEvent> down;
Cancellable<ITouch::SUpEvent> up;
Cancellable<ITouch::SMotionEvent> motion;
} touch;
} input;
struct {
Event<PHLMONITOR> pre;
Event<eRenderStage> stage;
} render;
struct {
Event<bool /* state start/stop */, uint8_t /* eScreenshareType */, const std::string& /* name */> state;
} screenshare;
struct {
struct {
Cancellable<IPointer::SSwipeBeginEvent> begin;
Cancellable<IPointer::SSwipeEndEvent> end;
Cancellable<IPointer::SSwipeUpdateEvent> update;
} swipe;
struct {
Cancellable<IPointer::SPinchBeginEvent> begin;
Cancellable<IPointer::SPinchEndEvent> end;
Cancellable<IPointer::SPinchUpdateEvent> update;
} pinch;
} gesture;
struct {
Event<PHLMONITOR> newMon;
Event<PHLMONITOR> preAdded;
Event<PHLMONITOR> added;
Event<PHLMONITOR> preRemoved;
Event<PHLMONITOR> removed;
Event<PHLMONITOR> preCommit;
Event<PHLMONITOR> focused;
Event<> layoutChanged;
} monitor;
struct {
Event<PHLWORKSPACE, PHLMONITOR> moveToMonitor;
Event<PHLWORKSPACE> active;
Event<PHLWORKSPACEREF> created;
Event<PHLWORKSPACEREF> removed;
} workspace;
struct {
Event<> preReload;
Event<> reloaded;
} config;
struct {
Event<const std::string&> submap;
} keybinds;
} m_events;
};
UP<CEventBus>& bus();
};

View file

@ -4,6 +4,8 @@
#include <algorithm>
#include <unistd.h>
#include "../managers/eventLoop/EventLoopManager.hpp"
#include "../desktop/rule/windowRule/WindowRule.hpp"
#include "../desktop/rule/Engine.hpp"
using namespace Hyprutils::OS;
@ -119,6 +121,9 @@ SP<CPromise<std::string>> CAsyncDialogBox::open() {
m_selfReference = m_selfWeakReference.lock();
if (!m_execRuleToken.empty())
proc.addEnv(Desktop::Rule::EXEC_RULE_ENV_NAME, m_execRuleToken);
if (!proc.runAsync()) {
Log::logger->log(Log::ERR, "CAsyncDialogBox::open: failed to run async");
wl_event_source_remove(m_readEventSource);
@ -154,3 +159,9 @@ pid_t CAsyncDialogBox::getPID() const {
SP<CAsyncDialogBox> CAsyncDialogBox::lockSelf() {
return m_selfWeakReference.lock();
}
void CAsyncDialogBox::setExecRule(std::string&& s) {
auto rule = Desktop::Rule::CWindowRule::buildFromExecString(std::move(s));
m_execRuleToken = rule->execToken();
Desktop::Rule::ruleEngine()->registerRule(std::move(rule));
}

View file

@ -27,6 +27,7 @@ class CAsyncDialogBox {
void kill();
bool isRunning() const;
pid_t getPID() const;
void setExecRule(std::string&& s);
SP<CAsyncDialogBox> lockSelf();
@ -41,7 +42,8 @@ class CAsyncDialogBox {
pid_t m_dialogPid = 0;
wl_event_source* m_readEventSource = nullptr;
Hyprutils::OS::CFileDescriptor m_pipeReadFd;
std::string m_stdout = "";
std::string m_stdout = "";
std::string m_execRuleToken = "";
const std::string m_title;
const std::string m_description;

20
src/helpers/Drm.cpp Normal file
View file

@ -0,0 +1,20 @@
#include <xf86drm.h>
#include "Drm.hpp"
bool DRM::sameGpu(int fd1, int fd2) {
drmDevice* devA = nullptr;
drmDevice* devB = nullptr;
if (drmGetDevice2(fd1, 0, &devA) != 0)
return false;
if (drmGetDevice2(fd2, 0, &devB) != 0) {
drmFreeDevice(&devA);
return false;
}
bool same = drmDevicesEqual(devA, devB);
drmFreeDevice(&devA);
drmFreeDevice(&devB);
return same;
}

5
src/helpers/Drm.hpp Normal file
View file

@ -0,0 +1,5 @@
#pragma once
namespace DRM {
bool sameGpu(int fd1, int fd2);
}

View file

@ -6,158 +6,186 @@
#include <xf86drm.h>
#include <drm_fourcc.h>
/*
DRM formats are LE, while OGL is BE. The two primary formats
will be flipped, so we will set flipRB which will later use swizzle
to flip the red and blue channels.
This will not work on GLES2, but I want to drop support for it one day anyways.
*/
inline const std::vector<SPixelFormat> GLES3_FORMATS = {
{
.drmFormat = DRM_FORMAT_ARGB8888,
.flipRB = true,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_BYTE,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_XRGB8888,
.bytesPerBlock = 4,
.drmFormat = DRM_FORMAT_ARGB8888,
.glInternalFormat = GL_RGBA8,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_BYTE,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_XRGB8888,
.bytesPerBlock = 4,
.swizzle = {SWIZZLE_BGRA},
},
{
.drmFormat = DRM_FORMAT_XRGB8888,
.flipRB = true,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_BYTE,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_XRGB8888,
.bytesPerBlock = 4,
.drmFormat = DRM_FORMAT_XRGB8888,
.glInternalFormat = GL_RGBA8,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_BYTE,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_XRGB8888,
.bytesPerBlock = 4,
.swizzle = {SWIZZLE_BGR1},
},
{
.drmFormat = DRM_FORMAT_XBGR8888,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_BYTE,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_XBGR8888,
.bytesPerBlock = 4,
.drmFormat = DRM_FORMAT_XBGR8888,
.glInternalFormat = GL_RGBA8,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_BYTE,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_XBGR8888,
.bytesPerBlock = 4,
.swizzle = {SWIZZLE_RGB1},
},
{
.drmFormat = DRM_FORMAT_ABGR8888,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_BYTE,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_XBGR8888,
.bytesPerBlock = 4,
.drmFormat = DRM_FORMAT_ABGR8888,
.glInternalFormat = GL_RGBA8,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_BYTE,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_XBGR8888,
.bytesPerBlock = 4,
.swizzle = {SWIZZLE_RGBA},
},
{
.drmFormat = DRM_FORMAT_BGR888,
.glFormat = GL_RGB,
.glType = GL_UNSIGNED_BYTE,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_BGR888,
.bytesPerBlock = 3,
.drmFormat = DRM_FORMAT_BGR888,
.glInternalFormat = GL_RGB8,
.glFormat = GL_RGB,
.glType = GL_UNSIGNED_BYTE,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_BGR888,
.bytesPerBlock = 3,
.swizzle = {SWIZZLE_RGB1},
},
{
.drmFormat = DRM_FORMAT_RGBX4444,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_SHORT_4_4_4_4,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_RGBX4444,
.bytesPerBlock = 2,
.drmFormat = DRM_FORMAT_RGBX4444,
.glInternalFormat = GL_RGBA4,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_SHORT_4_4_4_4,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_RGBX4444,
.bytesPerBlock = 2,
.swizzle = {SWIZZLE_RGB1},
},
{
.drmFormat = DRM_FORMAT_RGBA4444,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_SHORT_4_4_4_4,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_RGBX4444,
.bytesPerBlock = 2,
.drmFormat = DRM_FORMAT_RGBA4444,
.glInternalFormat = GL_RGBA4,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_SHORT_4_4_4_4,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_RGBX4444,
.bytesPerBlock = 2,
.swizzle = {SWIZZLE_RGBA},
},
{
.drmFormat = DRM_FORMAT_RGBX5551,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_SHORT_5_5_5_1,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_RGBX5551,
.bytesPerBlock = 2,
.drmFormat = DRM_FORMAT_RGBX5551,
.glInternalFormat = GL_RGB5_A1,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_SHORT_5_5_5_1,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_RGBX5551,
.bytesPerBlock = 2,
.swizzle = {SWIZZLE_RGB1},
},
{
.drmFormat = DRM_FORMAT_RGBA5551,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_SHORT_5_5_5_1,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_RGBX5551,
.bytesPerBlock = 2,
.drmFormat = DRM_FORMAT_RGBA5551,
.glInternalFormat = GL_RGB5_A1,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_SHORT_5_5_5_1,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_RGBX5551,
.bytesPerBlock = 2,
.swizzle = {SWIZZLE_RGBA},
},
{
.drmFormat = DRM_FORMAT_RGB565,
.glFormat = GL_RGB,
.glType = GL_UNSIGNED_SHORT_5_6_5,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_RGB565,
.bytesPerBlock = 2,
.drmFormat = DRM_FORMAT_RGB565,
.glInternalFormat = GL_RGB565,
.glFormat = GL_RGB,
.glType = GL_UNSIGNED_SHORT_5_6_5,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_RGB565,
.bytesPerBlock = 2,
.swizzle = {SWIZZLE_RGB1},
},
{
.drmFormat = DRM_FORMAT_XBGR2101010,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_INT_2_10_10_10_REV,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_XBGR2101010,
.bytesPerBlock = 4,
.drmFormat = DRM_FORMAT_XBGR2101010,
.glInternalFormat = GL_RGB10_A2,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_INT_2_10_10_10_REV,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_XBGR2101010,
.bytesPerBlock = 4,
.swizzle = {SWIZZLE_RGB1},
},
{
.drmFormat = DRM_FORMAT_ABGR2101010,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_INT_2_10_10_10_REV,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_XBGR2101010,
.bytesPerBlock = 4,
.drmFormat = DRM_FORMAT_ABGR2101010,
.glInternalFormat = GL_RGB10_A2,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_INT_2_10_10_10_REV,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_XBGR2101010,
.bytesPerBlock = 4,
.swizzle = {SWIZZLE_RGBA},
},
{
.drmFormat = DRM_FORMAT_XRGB2101010,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_INT_2_10_10_10_REV,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_XRGB2101010,
.bytesPerBlock = 4,
.drmFormat = DRM_FORMAT_XRGB2101010,
.glInternalFormat = GL_RGB10_A2,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_INT_2_10_10_10_REV,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_XRGB2101010,
.bytesPerBlock = 4,
.swizzle = {SWIZZLE_BGR1},
},
{
.drmFormat = DRM_FORMAT_ARGB2101010,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_INT_2_10_10_10_REV,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_XRGB2101010,
.bytesPerBlock = 4,
.drmFormat = DRM_FORMAT_ARGB2101010,
.glInternalFormat = GL_RGB10_A2,
.glFormat = GL_RGBA,
.glType = GL_UNSIGNED_INT_2_10_10_10_REV,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_XRGB2101010,
.bytesPerBlock = 4,
.swizzle = {SWIZZLE_BGRA},
},
{
.drmFormat = DRM_FORMAT_XBGR16161616F,
.glFormat = GL_RGBA,
.glType = GL_HALF_FLOAT,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_XBGR16161616F,
.bytesPerBlock = 8,
.drmFormat = DRM_FORMAT_XBGR16161616F,
.glInternalFormat = GL_RGBA16F,
.glFormat = GL_RGBA,
.glType = GL_HALF_FLOAT,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_XBGR16161616F,
.bytesPerBlock = 8,
.swizzle = {SWIZZLE_RGB1},
},
{
.drmFormat = DRM_FORMAT_ABGR16161616F,
.glFormat = GL_RGBA,
.glType = GL_HALF_FLOAT,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_XBGR16161616F,
.bytesPerBlock = 8,
.drmFormat = DRM_FORMAT_ABGR16161616F,
.glInternalFormat = GL_RGBA16F,
.glFormat = GL_RGBA,
.glType = GL_HALF_FLOAT,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_XBGR16161616F,
.bytesPerBlock = 8,
.swizzle = {SWIZZLE_RGBA},
},
{
.drmFormat = DRM_FORMAT_XBGR16161616,
.glFormat = GL_RGBA16UI,
.glType = GL_UNSIGNED_SHORT,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_XBGR16161616,
.bytesPerBlock = 8,
.drmFormat = DRM_FORMAT_XBGR16161616,
.glInternalFormat = GL_RGBA16UI,
.glFormat = GL_RGBA_INTEGER,
.glType = GL_UNSIGNED_SHORT,
.withAlpha = false,
.alphaStripped = DRM_FORMAT_XBGR16161616,
.bytesPerBlock = 8,
.swizzle = {SWIZZLE_RGBA},
},
{
.drmFormat = DRM_FORMAT_ABGR16161616,
.glFormat = GL_RGBA16UI,
.glType = GL_UNSIGNED_SHORT,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_XBGR16161616,
.bytesPerBlock = 8,
.drmFormat = DRM_FORMAT_ABGR16161616,
.glInternalFormat = GL_RGBA16UI,
.glFormat = GL_RGBA_INTEGER,
.glType = GL_UNSIGNED_SHORT,
.withAlpha = true,
.alphaStripped = DRM_FORMAT_XBGR16161616,
.bytesPerBlock = 8,
.swizzle = {SWIZZLE_RGBA},
},
{
.drmFormat = DRM_FORMAT_YVYU,
@ -170,24 +198,28 @@ inline const std::vector<SPixelFormat> GLES3_FORMATS = {
.blockSize = {2, 1},
},
{
.drmFormat = DRM_FORMAT_R8,
.bytesPerBlock = 1,
.drmFormat = DRM_FORMAT_R8,
.glInternalFormat = GL_R8,
.glFormat = GL_RED,
.glType = GL_UNSIGNED_BYTE,
.bytesPerBlock = 1,
.swizzle = {SWIZZLE_R001},
},
{
.drmFormat = DRM_FORMAT_GR88,
.bytesPerBlock = 2,
.drmFormat = DRM_FORMAT_GR88,
.glInternalFormat = GL_RG8,
.glFormat = GL_RG,
.glType = GL_UNSIGNED_BYTE,
.bytesPerBlock = 2,
.swizzle = {SWIZZLE_RG01},
},
{
.drmFormat = DRM_FORMAT_RGB888,
.bytesPerBlock = 3,
},
{
.drmFormat = DRM_FORMAT_BGR888,
.bytesPerBlock = 3,
},
{
.drmFormat = DRM_FORMAT_RGBX4444,
.bytesPerBlock = 2,
.drmFormat = DRM_FORMAT_RGB888,
.glInternalFormat = GL_RGB8,
.glFormat = GL_RGB,
.glType = GL_UNSIGNED_BYTE,
.bytesPerBlock = 3,
.swizzle = {SWIZZLE_BGR1},
},
};
@ -229,6 +261,26 @@ const SPixelFormat* NFormatUtils::getPixelFormatFromGL(uint32_t glFormat, uint32
return nullptr;
}
bool NFormatUtils::isFormatYUV(uint32_t drmFormat) {
switch (drmFormat) {
case DRM_FORMAT_YUYV:
case DRM_FORMAT_YVYU:
case DRM_FORMAT_UYVY:
case DRM_FORMAT_VYUY:
case DRM_FORMAT_AYUV:
case DRM_FORMAT_NV12:
case DRM_FORMAT_NV21:
case DRM_FORMAT_NV16:
case DRM_FORMAT_NV61:
case DRM_FORMAT_YUV410:
case DRM_FORMAT_YUV411:
case DRM_FORMAT_YUV420:
case DRM_FORMAT_YUV422:
case DRM_FORMAT_YUV444: return true;
default: return false;
}
}
bool NFormatUtils::isFormatOpaque(DRMFormat drm) {
const auto FMT = NFormatUtils::getPixelFormatFromDRM(drm);
if (!FMT)
@ -245,32 +297,38 @@ int NFormatUtils::minStride(const SPixelFormat* const fmt, int32_t width) {
return std::ceil((width * fmt->bytesPerBlock) / pixelsPerBlock(fmt));
}
uint32_t NFormatUtils::drmFormatToGL(DRMFormat drm) {
switch (drm) {
case DRM_FORMAT_XRGB8888:
case DRM_FORMAT_XBGR8888: return GL_RGBA; // doesn't matter, opengl is gucci in this case.
case DRM_FORMAT_XRGB2101010:
case DRM_FORMAT_XBGR2101010: return GL_RGB10_A2;
default: return GL_RGBA;
}
UNREACHABLE();
return GL_RGBA;
}
uint32_t NFormatUtils::glFormatToType(uint32_t gl) {
return gl != GL_RGBA ? GL_UNSIGNED_INT_2_10_10_10_REV : GL_UNSIGNED_BYTE;
}
std::string NFormatUtils::drmFormatName(DRMFormat drm) {
auto n = drmGetFormatName(drm);
auto n = drmGetFormatName(drm);
if (!n)
return "unknown";
std::string name = n;
free(n); // NOLINT(cppcoreguidelines-no-malloc,-warnings-as-errors)
return name;
}
std::string NFormatUtils::drmModifierName(uint64_t mod) {
auto n = drmGetFormatModifierName(mod);
auto n = drmGetFormatModifierName(mod);
if (!n)
return "unknown";
std::string name = n;
free(n); // NOLINT(cppcoreguidelines-no-malloc,-warnings-as-errors)
return name;
}
DRMFormat NFormatUtils::alphaFormat(DRMFormat prevFormat) {
switch (prevFormat) {
case DRM_FORMAT_XRGB8888: return DRM_FORMAT_ARGB8888;
case DRM_FORMAT_XBGR8888: return DRM_FORMAT_ABGR8888;
case DRM_FORMAT_BGRX8888: return DRM_FORMAT_BGRA8888;
case DRM_FORMAT_RGBX8888: return DRM_FORMAT_RGBA8888;
case DRM_FORMAT_XRGB2101010: return DRM_FORMAT_ARGB2101010;
case DRM_FORMAT_XBGR2101010: return DRM_FORMAT_ABGR2101010;
case DRM_FORMAT_RGBX1010102: return DRM_FORMAT_RGBA1010102;
case DRM_FORMAT_BGRX1010102: return DRM_FORMAT_BGRA1010102;
default: return 0;
}
}

View file

@ -2,22 +2,43 @@
#include <cstdint>
#include <string>
#include <GLES3/gl32.h>
#include "math/Math.hpp"
#include <aquamarine/backend/Misc.hpp>
using DRMFormat = uint32_t;
using SHMFormat = uint32_t;
#define SWIZZLE_A1GB {GL_ALPHA, GL_ONE, GL_GREEN, GL_BLUE}
#define SWIZZLE_ABG1 {GL_ALPHA, GL_BLUE, GL_GREEN, GL_ONE}
#define SWIZZLE_ABGR {GL_ALPHA, GL_BLUE, GL_GREEN, GL_RED}
#define SWIZZLE_ARGB {GL_ALPHA, GL_RED, GL_GREEN, GL_BLUE}
#define SWIZZLE_B1RG {GL_BLUE, GL_ONE, GL_RED, GL_GREEN}
#define SWIZZLE_BARG {GL_BLUE, GL_ALPHA, GL_RED, GL_GREEN}
#define SWIZZLE_BGR1 {GL_BLUE, GL_GREEN, GL_RED, GL_ONE}
#define SWIZZLE_BGRA {GL_BLUE, GL_GREEN, GL_RED, GL_ALPHA}
#define SWIZZLE_G1AB {GL_GREEN, GL_ONE, GL_ALPHA, GL_BLUE}
#define SWIZZLE_GBA1 {GL_GREEN, GL_BLUE, GL_ALPHA, GL_ONE}
#define SWIZZLE_GBAR {GL_GREEN, GL_BLUE, GL_ALPHA, GL_RED}
#define SWIZZLE_GRAB {GL_GREEN, GL_RED, GL_ALPHA, GL_BLUE}
#define SWIZZLE_R001 {GL_RED, GL_ZERO, GL_ZERO, GL_ONE}
#define SWIZZLE_R1BG {GL_RED, GL_ONE, GL_BLUE, GL_GREEN}
#define SWIZZLE_RABG {GL_RED, GL_ALPHA, GL_BLUE, GL_GREEN}
#define SWIZZLE_RG01 {GL_RED, GL_GREEN, GL_ZERO, GL_ONE}
#define SWIZZLE_GR01 {GL_GREEN, GL_RED, GL_ZERO, GL_ONE}
#define SWIZZLE_RGB1 {GL_RED, GL_GREEN, GL_BLUE, GL_ONE}
#define SWIZZLE_RGBA {GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA}
struct SPixelFormat {
DRMFormat drmFormat = 0; /* DRM_FORMAT_INVALID */
bool flipRB = false;
int glInternalFormat = 0;
int glFormat = 0;
int glType = 0;
bool withAlpha = true;
DRMFormat alphaStripped = 0; /* DRM_FORMAT_INVALID */
uint32_t bytesPerBlock = 0;
Vector2D blockSize;
DRMFormat drmFormat = 0; /* DRM_FORMAT_INVALID */
int glInternalFormat = 0;
int glFormat = 0;
int glType = 0;
bool withAlpha = true;
DRMFormat alphaStripped = 0; /* DRM_FORMAT_INVALID */
uint32_t bytesPerBlock = 0;
Vector2D blockSize;
std::optional<std::array<GLint, 4>> swizzle = std::nullopt;
};
using SDRMFormat = Aquamarine::SDRMFormat;
@ -28,11 +49,11 @@ namespace NFormatUtils {
const SPixelFormat* getPixelFormatFromDRM(DRMFormat drm);
const SPixelFormat* getPixelFormatFromGL(uint32_t glFormat, uint32_t glType, bool alpha);
bool isFormatYUV(uint32_t drmFormat);
bool isFormatOpaque(DRMFormat drm);
int pixelsPerBlock(const SPixelFormat* const fmt);
int minStride(const SPixelFormat* const fmt, int32_t width);
uint32_t drmFormatToGL(DRMFormat drm);
uint32_t glFormatToType(uint32_t gl);
std::string drmFormatName(DRMFormat drm);
std::string drmModifierName(uint64_t mod);
DRMFormat alphaFormat(DRMFormat prevFormat);
};

View file

@ -11,10 +11,7 @@ static int onDataRead(int fd, uint32_t mask, void* data) {
CMainLoopExecutor::CMainLoopExecutor(std::function<void()>&& callback) : m_fn(std::move(callback)) {
int fds[2];
pipe(fds);
RASSERT(fds[0] != 0, "CMainLoopExecutor: failed to open a pipe");
RASSERT(fds[1] != 0, "CMainLoopExecutor: failed to open a pipe");
RASSERT(pipe(fds) == 0, "CMainLoopExecutor: failed to open a pipe");
m_event = wl_event_loop_add_fd(g_pEventLoopManager->m_wayland.loop, fds[0], WL_EVENT_READABLE, ::onDataRead, this);

View file

@ -2,6 +2,7 @@
#include "MiscFunctions.hpp"
#include "../macros.hpp"
#include "SharedDefs.hpp"
#include "../helpers/TransferFunction.hpp"
#include "math/Math.hpp"
#include "../protocols/ColorManagement.hpp"
#include "../Compositor.hpp"
@ -22,16 +23,20 @@
#include "../protocols/core/DataDevice.hpp"
#include "../render/Renderer.hpp"
#include "../managers/EventManager.hpp"
#include "../managers/LayoutManager.hpp"
#include "../managers/screenshare/ScreenshareManager.hpp"
#include "../managers/animation/AnimationManager.hpp"
#include "../managers/animation/DesktopAnimationManager.hpp"
#include "../managers/input/InputManager.hpp"
#include "../hyprerror/HyprError.hpp"
#include "../layout/LayoutManager.hpp"
#include "../i18n/Engine.hpp"
#include "../helpers/cm/ColorManagement.hpp"
#include "sync/SyncTimeline.hpp"
#include "time/Time.hpp"
#include "../desktop/view/LayerSurface.hpp"
#include "../desktop/state/FocusState.hpp"
#include "../event/EventBus.hpp"
#include "Drm.hpp"
#include <aquamarine/output/Output.hpp>
#include "debug/log/Logger.hpp"
#include "debug/HyprNotificationOverlay.hpp"
@ -50,7 +55,7 @@ using namespace Hyprutils::OS;
using enum NContentType::eContentType;
using namespace NColorManagement;
CMonitor::CMonitor(SP<Aquamarine::IOutput> output_) : m_state(this), m_output(output_) {
CMonitor::CMonitor(SP<Aquamarine::IOutput> output_) : m_state(this), m_output(output_), m_imageDescription(DEFAULT_IMAGE_DESCRIPTION) {
g_pAnimationManager->createAnimation(0.f, m_specialFade, g_pConfigManager->getAnimationPropertyConfig("specialWorkspaceIn"), AVARDAMAGE_NONE);
m_specialFade->setUpdateCallback([this](auto) { g_pHyprRenderer->damageMonitor(m_self.lock()); });
static auto PZOOMFACTOR = CConfigValue<Hyprlang::FLOAT>("cursor:zoom_factor");
@ -71,23 +76,27 @@ CMonitor::~CMonitor() {
}
void CMonitor::onConnect(bool noRule) {
EMIT_HOOK_EVENT("preMonitorAdded", m_self.lock());
Event::bus()->m_events.monitor.preAdded.emit(m_self.lock());
CScopeGuard x = {[]() { g_pCompositor->arrangeMonitors(); }};
m_zoomAnimProgress->setValueAndWarp(0.F);
m_zoomAnimFrameCounter = 0;
g_pEventLoopManager->doLater([] { g_pConfigManager->ensurePersistentWorkspacesPresent(); });
g_pEventLoopManager->doLater([] {
g_pConfigManager->ensurePersistentWorkspacesPresent();
g_pCompositor->ensureWorkspacesOnAssignedMonitors();
});
m_listeners.frame = m_output->events.frame.listen([this] {
if (m_frameScheduler)
m_frameScheduler->onFrame();
});
m_listeners.commit = m_output->events.commit.listen([this] {
if (true) { // FIXME: E->state->committed & WLR_OUTPUT_STATE_BUFFER
PROTO::screencopy->onOutputCommit(m_self.lock());
PROTO::toplevelExport->onOutputCommit(m_self.lock());
}
m_events.commit.emit();
// FIXME: E->state->committed & WLR_OUTPUT_STATE_BUFFER
if (true && Screenshare::mgr())
Screenshare::mgr()->onOutputCommit(m_self.lock());
});
m_listeners.needsFrame = m_output->events.needsFrame.listen([this] { g_pCompositor->scheduleFrameForMonitor(m_self.lock(), Aquamarine::IOutput::AQ_SCHEDULE_NEEDS_FRAME); });
@ -114,10 +123,12 @@ void CMonitor::onConnect(bool noRule) {
ts = nullptr;
}
if (!ts)
PROTO::presentation->onPresented(m_self.lock(), Time::steadyNow(), event.refresh, event.seq, event.flags);
else
PROTO::presentation->onPresented(m_self.lock(), Time::fromTimespec(event.when), event.refresh, event.seq, event.flags);
if (!ts) {
timespec mono{};
clock_gettime(CLOCK_MONOTONIC, &mono);
PROTO::presentation->onPresented(m_self.lock(), mono, event.refresh, event.seq, event.flags & ~Aquamarine::IOutput::AQ_OUTPUT_PRESENT_HW_CLOCK);
} else
PROTO::presentation->onPresented(m_self.lock(), *ts, event.refresh, event.seq, event.flags);
if (m_zoomAnimFrameCounter < 5) {
m_zoomAnimFrameCounter++;
@ -282,10 +293,16 @@ void CMonitor::onConnect(bool noRule) {
if (!valid(ws))
continue;
if (ws->m_lastMonitor == m_name || g_pCompositor->m_monitors.size() == 1 /* avoid lost workspaces on recover */) {
const auto CURRENTMON = ws->m_monitor.lock();
const bool ORPHANED = !CURRENTMON || std::ranges::none_of(g_pCompositor->m_monitors, [&](const auto& mon) { return mon == CURRENTMON; });
const bool RETURNING = ws->m_lastMonitor == m_name;
const bool RECOVERY = g_pCompositor->m_monitors.size() == 1 && ORPHANED; // temporarily recover orphaned workspaces
if (RETURNING || RECOVERY) {
g_pCompositor->moveWorkspaceToMonitor(ws, m_self.lock());
g_pDesktopAnimationManager->startAnimation(ws, CDesktopAnimationManager::ANIMATION_TYPE_IN, true, true);
ws->m_lastMonitor = "";
if (RETURNING)
ws->m_lastMonitor = "";
}
}
@ -303,7 +320,7 @@ void CMonitor::onConnect(bool noRule) {
Desktop::focusState()->rawMonitorFocus(m_self.lock());
g_pHyprRenderer->arrangeLayersForMonitor(m_id);
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(m_id);
g_layoutManager->recalculateMonitor(m_self.lock());
// ensure VRR (will enable if necessary)
g_pConfigManager->ensureVRR(m_self.lock());
@ -341,17 +358,17 @@ void CMonitor::onConnect(bool noRule) {
g_pEventManager->postEvent(SHyprIPCEvent{"monitoradded", m_name});
g_pEventManager->postEvent(SHyprIPCEvent{"monitoraddedv2", std::format("{},{},{}", m_id, m_name, m_shortDescription)});
EMIT_HOOK_EVENT("monitorAdded", m_self.lock());
Event::bus()->m_events.monitor.added.emit(m_self.lock());
}
void CMonitor::onDisconnect(bool destroy) {
EMIT_HOOK_EVENT("preMonitorRemoved", m_self.lock());
Event::bus()->m_events.monitor.preRemoved.emit(m_self.lock());
CScopeGuard x = {[this]() {
if (g_pCompositor->m_isShuttingDown)
return;
g_pEventManager->postEvent(SHyprIPCEvent{"monitorremoved", m_name});
g_pEventManager->postEvent(SHyprIPCEvent{"monitorremovedv2", std::format("{},{},{}", m_id, m_name, m_shortDescription)});
EMIT_HOOK_EVENT("monitorRemoved", m_self.lock());
Event::bus()->m_events.monitor.removed.emit(m_self.lock());
g_pCompositor->scheduleMonitorStateRecheck();
}};
@ -411,7 +428,6 @@ void CMonitor::onDisconnect(bool destroy) {
m_layerSurfaceLayers[i].clear();
}
std::erase_if(g_pCompositor->m_monitors, [&](PHLMONITOR& el) { return el.get() == this; });
Log::logger->log(Log::DEBUG, "Removed monitor {}!", m_name);
if (!BACKUPMON) {
@ -422,19 +438,24 @@ void CMonitor::onDisconnect(bool destroy) {
m_enabled = false;
m_renderingInitPassed = false;
std::vector<PHLWORKSPACE> wspToMove;
for (auto const& w : g_pCompositor->getWorkspaces()) {
if (w->m_monitor == m_self || !w->m_monitor)
wspToMove.emplace_back(w.lock());
}
// Preserve ownership across cascaded monitor disconnects.
// The first disconnected monitor "owns" where a workspace should return.
for (auto const& w : wspToMove) {
if (w && w->m_lastMonitor.empty())
w->m_lastMonitor = m_name;
}
if (BACKUPMON) {
// snap cursor
g_pCompositor->warpCursorTo(BACKUPMON->m_position + BACKUPMON->m_transformedSize / 2.F, true);
// move workspaces
std::vector<PHLWORKSPACE> wspToMove;
for (auto const& w : g_pCompositor->getWorkspaces()) {
if (w->m_monitor == m_self || !w->m_monitor)
wspToMove.emplace_back(w.lock());
}
for (auto const& w : wspToMove) {
w->m_lastMonitor = m_name;
g_pCompositor->moveWorkspaceToMonitor(w, BACKUPMON);
g_pDesktopAnimationManager->startAnimation(w, CDesktopAnimationManager::ANIMATION_TYPE_IN, true, true);
}
@ -463,7 +484,7 @@ void CMonitor::onDisconnect(bool destroy) {
PHLMONITOR pMonitorMostHz = nullptr;
for (auto const& m : g_pCompositor->m_monitors) {
if (m->m_refreshRate > mostHz) {
if (m->m_refreshRate > mostHz && m != m_self) {
pMonitorMostHz = m;
mostHz = m->m_refreshRate;
}
@ -471,78 +492,118 @@ void CMonitor::onDisconnect(bool destroy) {
g_pHyprRenderer->m_mostHzMonitor = pMonitorMostHz;
}
std::erase_if(g_pCompositor->m_monitors, [&](PHLMONITOR& el) { return el.get() == this; });
}
void CMonitor::applyCMType(NCMType::eCMType cmType, int cmSdrEotf) {
auto oldImageDescription = m_imageDescription;
static auto PSDREOTF = CConfigValue<Hyprlang::INT>("render:cm_sdr_eotf");
auto chosenSdrEotf = cmSdrEotf == 0 ? (*PSDREOTF != 3 ? NColorManagement::CM_TRANSFER_FUNCTION_GAMMA22 : NColorManagement::CM_TRANSFER_FUNCTION_SRGB) :
(cmSdrEotf == 1 ? NColorManagement::CM_TRANSFER_FUNCTION_SRGB : NColorManagement::CM_TRANSFER_FUNCTION_GAMMA22);
static NColorManagement::eTransferFunction chooseTF(NTransferFunction::eTF tf) {
const auto sdrEOTF = NTransferFunction::fromConfig();
switch (tf) {
case NTransferFunction::TF_DEFAULT:
case NTransferFunction::TF_GAMMA22:
case NTransferFunction::TF_FORCED_GAMMA22: return NColorManagement::CM_TRANSFER_FUNCTION_GAMMA22;
case NTransferFunction::TF_SRGB: return NColorManagement::CM_TRANSFER_FUNCTION_SRGB;
case NTransferFunction::TF_AUTO: // use global setting
switch (sdrEOTF) {
case NTransferFunction::TF_AUTO: return NColorManagement::CM_TRANSFER_FUNCTION_GAMMA22;
default: return chooseTF(sdrEOTF);
}
default: UNREACHABLE();
}
}
void CMonitor::applyCMType(NCMType::eCMType cmType, NTransferFunction::eTF cmSdrEotf) {
auto oldImageDescription = m_imageDescription;
const auto chosenSdrEotf = chooseTF(cmSdrEotf);
const auto masteringPrimaries = getMasteringPrimaries();
const NColorManagement::SImageDescription::SPCMasteringLuminances masteringLuminances = getMasteringLuminances();
const auto maxFALL = this->maxFALL();
const auto maxCLL = this->maxCLL();
switch (cmType) {
case NCMType::CM_SRGB: m_imageDescription = CImageDescription::from({.transferFunction = chosenSdrEotf}); break; // assumes SImageDescription defaults to sRGB
case NCMType::CM_WIDE:
case NCMType::CM_SRGB:
m_imageDescription = CImageDescription::from({.transferFunction = chosenSdrEotf,
.primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_BT2020,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_BT2020)});
.primariesNamed = NColorManagement::CM_PRIMARIES_SRGB,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_SRGB)});
break; // assumes SImageDescription defaults to sRGB
case NCMType::CM_WIDE:
m_imageDescription = CImageDescription::from({.transferFunction = chosenSdrEotf,
.primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_BT2020,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_BT2020),
.masteringPrimaries = masteringPrimaries,
.masteringLuminances = masteringLuminances,
.maxCLL = maxCLL,
.maxFALL = maxFALL});
break;
case NCMType::CM_DCIP3:
m_imageDescription = CImageDescription::from({.transferFunction = chosenSdrEotf,
.primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_DCI_P3,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_DCI_P3)});
m_imageDescription = CImageDescription::from({.transferFunction = chosenSdrEotf,
.primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_DCI_P3,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_DCI_P3),
.masteringPrimaries = masteringPrimaries,
.masteringLuminances = masteringLuminances,
.maxCLL = maxCLL,
.maxFALL = maxFALL});
break;
case NCMType::CM_DP3:
m_imageDescription = CImageDescription::from({.transferFunction = chosenSdrEotf,
.primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_DISPLAY_P3,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_DISPLAY_P3)});
m_imageDescription = CImageDescription::from({.transferFunction = chosenSdrEotf,
.primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_DISPLAY_P3,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_DISPLAY_P3),
.masteringPrimaries = masteringPrimaries,
.masteringLuminances = masteringLuminances,
.maxCLL = maxCLL,
.maxFALL = maxFALL});
break;
case NCMType::CM_ADOBE:
m_imageDescription = CImageDescription::from({.transferFunction = chosenSdrEotf,
.primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_ADOBE_RGB,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_ADOBE_RGB)});
m_imageDescription = CImageDescription::from({.transferFunction = chosenSdrEotf,
.primariesNameSet = true,
.primariesNamed = NColorManagement::CM_PRIMARIES_ADOBE_RGB,
.primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_ADOBE_RGB),
.masteringPrimaries = masteringPrimaries,
.masteringLuminances = masteringLuminances,
.maxCLL = maxCLL,
.maxFALL = maxFALL});
break;
case NCMType::CM_EDID:
m_imageDescription =
CImageDescription::from({.transferFunction = chosenSdrEotf,
.primariesNameSet = false,
.primariesNamed = NColorManagement::CM_PRIMARIES_BT2020,
.primaries = {
.red = {.x = m_output->parsedEDID.chromaticityCoords->red.x, .y = m_output->parsedEDID.chromaticityCoords->red.y},
.green = {.x = m_output->parsedEDID.chromaticityCoords->green.x, .y = m_output->parsedEDID.chromaticityCoords->green.y},
.blue = {.x = m_output->parsedEDID.chromaticityCoords->blue.x, .y = m_output->parsedEDID.chromaticityCoords->blue.y},
.white = {.x = m_output->parsedEDID.chromaticityCoords->white.x, .y = m_output->parsedEDID.chromaticityCoords->white.y},
}});
m_imageDescription = CImageDescription::from({.transferFunction = chosenSdrEotf,
.primariesNameSet = false,
.primariesNamed = NColorManagement::CM_PRIMARIES_BT2020,
.primaries = masteringPrimaries,
.masteringPrimaries = masteringPrimaries,
.masteringLuminances = masteringLuminances,
.maxCLL = maxCLL,
.maxFALL = maxFALL});
break;
case NCMType::CM_HDR: m_imageDescription = DEFAULT_HDR_IMAGE_DESCRIPTION; break;
case NCMType::CM_HDR_EDID:
m_imageDescription =
CImageDescription::from({.transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_ST2084_PQ,
.primariesNameSet = false,
.primariesNamed = NColorManagement::CM_PRIMARIES_BT2020,
.primaries = m_output->parsedEDID.chromaticityCoords.has_value() ?
NColorManagement::SPCPRimaries{
.red = {.x = m_output->parsedEDID.chromaticityCoords->red.x, .y = m_output->parsedEDID.chromaticityCoords->red.y},
.green = {.x = m_output->parsedEDID.chromaticityCoords->green.x, .y = m_output->parsedEDID.chromaticityCoords->green.y},
.blue = {.x = m_output->parsedEDID.chromaticityCoords->blue.x, .y = m_output->parsedEDID.chromaticityCoords->blue.y},
.white = {.x = m_output->parsedEDID.chromaticityCoords->white.x, .y = m_output->parsedEDID.chromaticityCoords->white.y},
} :
NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_BT2020),
.luminances = {.min = m_output->parsedEDID.hdrMetadata->desiredContentMinLuminance,
.max = m_output->parsedEDID.hdrMetadata->desiredContentMaxLuminance,
.reference = m_output->parsedEDID.hdrMetadata->desiredMaxFrameAverageLuminance}});
m_imageDescription = CImageDescription::from(
{.transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_ST2084_PQ,
.primariesNameSet = false,
.primariesNamed = NColorManagement::CM_PRIMARIES_BT2020,
.primaries = m_output->parsedEDID.chromaticityCoords.has_value() ? masteringPrimaries : NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_BT2020),
.masteringPrimaries = masteringPrimaries,
.luminances = {.min = DEFAULT_HDR_IMAGE_DESCRIPTION->value().getTFMinLuminance(),
.max = DEFAULT_HDR_IMAGE_DESCRIPTION->value().getTFMaxLuminance(),
.reference = DEFAULT_HDR_IMAGE_DESCRIPTION->value().getTFRefLuminance()},
.masteringLuminances = masteringLuminances,
.maxCLL = maxCLL,
.maxFALL = maxFALL});
break;
default: UNREACHABLE();
}
if (m_minLuminance >= 0 || m_maxLuminance >= 0 || m_maxAvgLuminance >= 0)
if ((m_minLuminance >= 0 || m_maxLuminance >= 0 || m_maxAvgLuminance >= 0) && (cmType == NCMType::CM_HDR || cmType == NCMType::CM_HDR_EDID))
m_imageDescription = m_imageDescription->with({
.min = m_minLuminance >= 0 ? m_minLuminance : m_imageDescription->value().luminances.min, //
.max = m_maxLuminance >= 0 ? m_maxLuminance : m_imageDescription->value().luminances.max, //
.reference = m_maxAvgLuminance >= 0 ? m_maxAvgLuminance : m_imageDescription->value().luminances.reference //
.min = m_minLuminance >= 0 ? m_minLuminance : m_imageDescription->value().luminances.min, //
.max = m_maxLuminance >= 0 ? m_maxLuminance : m_imageDescription->value().luminances.max, //
.reference = m_imageDescription->value().luminances.reference //
});
if (oldImageDescription != m_imageDescription) {
@ -872,29 +933,46 @@ bool CMonitor::applyMonitorRule(SMonitorRule* pMonitorRule, bool force) {
m_supportsWideColor = RULE->supportsHDR;
m_supportsHDR = RULE->supportsHDR;
m_cmType = RULE->cmType;
switch (m_cmType) {
case NCMType::CM_AUTO: m_cmType = m_enabled10bit && supportsWideColor() ? NCMType::CM_WIDE : NCMType::CM_SRGB; break;
case NCMType::CM_EDID: m_cmType = m_output->parsedEDID.chromaticityCoords.has_value() ? NCMType::CM_EDID : NCMType::CM_SRGB; break;
case NCMType::CM_HDR:
case NCMType::CM_HDR_EDID: m_cmType = supportsHDR() ? m_cmType : NCMType::CM_SRGB; break;
default: break;
if (RULE->iccFile.empty()) {
// only apply explicit cm settings if we have no icc file
m_cmType = RULE->cmType;
switch (m_cmType) {
case NCMType::CM_AUTO: m_cmType = m_enabled10bit && supportsWideColor() ? NCMType::CM_WIDE : NCMType::CM_SRGB; break;
case NCMType::CM_EDID: m_cmType = m_output->parsedEDID.chromaticityCoords.has_value() ? NCMType::CM_EDID : NCMType::CM_SRGB; break;
case NCMType::CM_HDR:
case NCMType::CM_HDR_EDID: m_cmType = supportsHDR() ? m_cmType : NCMType::CM_SRGB; break;
default: break;
}
m_sdrEotf = RULE->sdrEotf;
m_sdrMinLuminance = RULE->sdrMinLuminance;
m_sdrMaxLuminance = RULE->sdrMaxLuminance;
m_minLuminance = RULE->minLuminance;
m_maxLuminance = RULE->maxLuminance;
m_maxAvgLuminance = RULE->maxAvgLuminance;
applyCMType(m_cmType, m_sdrEotf);
m_sdrSaturation = RULE->sdrSaturation;
m_sdrBrightness = RULE->sdrBrightness;
} else {
auto image = NColorManagement::SImageDescription::fromICC(RULE->iccFile);
if (!image) {
Log::logger->log(Log::ERR, "icc for {} ({}) failed: {}", m_name, RULE->iccFile, image.error());
g_pConfigManager->addParseError(std::format("failed to apply icc {} to {}: {}", RULE->iccFile, m_name, image.error()));
} else {
m_imageDescription = CImageDescription::from(*image);
if (!m_imageDescription) {
Log::logger->log(Log::ERR, "icc for {} ({}) failed 2: {}", m_name, RULE->iccFile, image.error());
g_pConfigManager->addParseError(std::format("failed to apply icc {} to {}: {}", RULE->iccFile, m_name, image.error()));
m_imageDescription = CImageDescription::from(SImageDescription{});
}
}
}
m_sdrEotf = RULE->sdrEotf;
m_sdrMinLuminance = RULE->sdrMinLuminance;
m_sdrMaxLuminance = RULE->sdrMaxLuminance;
m_minLuminance = RULE->minLuminance;
m_maxLuminance = RULE->maxLuminance;
m_maxAvgLuminance = RULE->maxAvgLuminance;
applyCMType(m_cmType, m_sdrEotf);
m_sdrSaturation = RULE->sdrSaturation;
m_sdrBrightness = RULE->sdrBrightness;
Vector2D logicalSize = m_pixelSize / m_scale;
if (!*PDISABLESCALECHECKS && (logicalSize.x != std::round(logicalSize.x) || logicalSize.y != std::round(logicalSize.y))) {
// invalid scale, will produce fractional pixels.
@ -976,6 +1054,8 @@ bool CMonitor::applyMonitorRule(SMonitorRule* pMonitorRule, bool force) {
m_damage.setSize(m_transformedSize);
updateVCGTRamps();
// Set scale for all surfaces on this monitor, needed for some clients
// but not on unsafe state to avoid crashes
if (!g_pCompositor->m_unsafeState) {
@ -992,7 +1072,7 @@ bool CMonitor::applyMonitorRule(SMonitorRule* pMonitorRule, bool force) {
Log::logger->log(Log::DEBUG, "Monitor {} data dump: res {:X}@{:.2f}Hz, scale {:.2f}, transform {}, pos {:X}, 10b {}", m_name, m_pixelSize, m_refreshRate, m_scale,
sc<int>(m_transform), m_position, sc<int>(m_enabled10bit));
EMIT_HOOK_EVENT("monitorLayoutChanged", nullptr);
Event::bus()->m_events.monitor.layoutChanged.emit();
m_events.modeChanged.emit();
@ -1026,8 +1106,10 @@ bool CMonitor::shouldSkipScheduleFrameOnMouseEvent() {
static auto PMINRR = CConfigValue<Hyprlang::INT>("cursor:min_refresh_rate");
// skip scheduling extra frames for fullsreen apps with vrr
const bool shouldSkip = inFullscreenMode() && (*PNOBREAK == 1 || (*PNOBREAK == 2 && m_activeWorkspace->getFullscreenWindow()->getContentType() == CONTENT_TYPE_GAME)) &&
m_output->state->state().adaptiveSync;
const auto FS_WINDOW = getFullscreenWindow();
const bool shouldRenderCursor = g_pHyprRenderer->shouldRenderCursor();
const bool noBreak = FS_WINDOW && (*PNOBREAK == 1 || (*PNOBREAK == 2 && FS_WINDOW->getContentType() == CONTENT_TYPE_GAME));
const bool shouldSkip = (!shouldRenderCursor || noBreak) && m_output->state->state().adaptiveSync;
// keep requested minimum refresh rate
if (shouldSkip && *PMINRR && m_lastPresentationTimer.getMillis() > 1000.0f / *PMINRR) {
@ -1096,7 +1178,7 @@ void CMonitor::setupDefaultWS(const SMonitorRule& monitorRule) {
// workspace exists, move it to the newly connected monitor
g_pCompositor->moveWorkspaceToMonitor(PNEWWORKSPACE, m_self.lock());
m_activeWorkspace = PNEWWORKSPACE;
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(m_id);
g_layoutManager->recalculateMonitor(m_self.lock());
g_pDesktopAnimationManager->startAnimation(PNEWWORKSPACE, CDesktopAnimationManager::ANIMATION_TYPE_IN, true, true);
} else {
if (newDefaultWorkspaceName.empty())
@ -1280,7 +1362,7 @@ void CMonitor::changeWorkspace(const PHLWORKSPACE& pWorkspace, bool internal, bo
// move pinned windows
for (auto const& w : g_pCompositor->m_windows) {
if (w->m_workspace == POLDWORKSPACE && w->m_pinned)
w->moveToWorkspace(pWorkspace);
w->layoutTarget()->assignToSpace(pWorkspace->m_space);
}
if (!noFocus && !Desktop::focusState()->monitor()->m_activeSpecialWorkspace &&
@ -1300,17 +1382,17 @@ void CMonitor::changeWorkspace(const PHLWORKSPACE& pWorkspace, bool internal, bo
pWindow = pWorkspace->getFirstWindow();
}
Desktop::focusState()->fullWindowFocus(pWindow);
Desktop::focusState()->fullWindowFocus(pWindow, Desktop::FOCUS_REASON_KEYBIND);
}
if (!noMouseMove)
g_pInputManager->simulateMouseMovement();
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(m_id);
g_layoutManager->recalculateMonitor(m_self.lock());
g_pEventManager->postEvent(SHyprIPCEvent{"workspace", pWorkspace->m_name});
g_pEventManager->postEvent(SHyprIPCEvent{"workspacev2", std::format("{},{}", pWorkspace->m_id, pWorkspace->m_name)});
EMIT_HOOK_EVENT("workspace", pWorkspace);
Event::bus()->m_events.workspace.active.emit(pWorkspace);
}
// set all LSes as not above fullscreen on workspace changes
@ -1357,17 +1439,23 @@ void CMonitor::setSpecialWorkspace(const PHLWORKSPACE& pWorkspace) {
g_pDesktopAnimationManager->startAnimation(m_activeSpecialWorkspace, CDesktopAnimationManager::ANIMATION_TYPE_OUT, false);
g_pEventManager->postEvent(SHyprIPCEvent{"activespecial", "," + m_name});
g_pEventManager->postEvent(SHyprIPCEvent{"activespecialv2", ",," + m_name});
// Reset layer surface state when closing special workspace
for (auto const& ls : g_pCompositor->m_layers) {
if (ls->m_monitor == m_self)
ls->m_aboveFullscreen = false;
}
}
m_activeSpecialWorkspace.reset();
if (POLDSPECIAL)
POLDSPECIAL->m_events.activeChanged.emit();
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(m_id);
g_layoutManager->recalculateMonitor(m_self.lock());
if (!(Desktop::focusState()->window() && Desktop::focusState()->window()->m_pinned && Desktop::focusState()->window()->m_monitor == m_self)) {
if (const auto PLAST = m_activeWorkspace->getLastFocusedWindow(); PLAST)
Desktop::focusState()->fullWindowFocus(PLAST);
Desktop::focusState()->fullWindowFocus(PLAST, Desktop::FOCUS_REASON_KEYBIND);
else
g_pInputManager->refocus();
}
@ -1392,10 +1480,17 @@ void CMonitor::setSpecialWorkspace(const PHLWORKSPACE& pWorkspace) {
const auto PMONITORWORKSPACEOWNER = pWorkspace->m_monitor.lock();
if (const auto PMWSOWNER = pWorkspace->m_monitor.lock(); PMWSOWNER && PMWSOWNER->m_activeSpecialWorkspace == pWorkspace) {
PMWSOWNER->m_activeSpecialWorkspace.reset();
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(PMWSOWNER->m_id);
g_layoutManager->recalculateMonitor(PMWSOWNER);
g_pHyprRenderer->damageMonitor(PMWSOWNER);
g_pEventManager->postEvent(SHyprIPCEvent{"activespecial", "," + PMWSOWNER->m_name});
g_pEventManager->postEvent(SHyprIPCEvent{"activespecialv2", ",," + PMWSOWNER->m_name});
// Reset layer surfaces on the old monitor when special workspace is stolen
for (auto const& ls : g_pCompositor->m_layers) {
if (ls->m_monitor == PMWSOWNER)
ls->m_aboveFullscreen = false;
}
const auto PACTIVEWORKSPACE = PMWSOWNER->m_activeWorkspace;
g_pDesktopAnimationManager->setFullscreenFadeAnimation(PACTIVEWORKSPACE,
PACTIVEWORKSPACE && PACTIVEWORKSPACE->m_hasFullscreenWindow ? CDesktopAnimationManager::ANIMATION_TYPE_IN :
@ -1409,6 +1504,12 @@ void CMonitor::setSpecialWorkspace(const PHLWORKSPACE& pWorkspace) {
m_activeSpecialWorkspace = pWorkspace;
m_activeSpecialWorkspace->m_visible = true;
// Reset layer surface state when opening special workspace
for (auto const& ls : g_pCompositor->m_layers) {
if (ls->m_monitor == m_self)
ls->m_aboveFullscreen = false;
}
if (POLDSPECIAL)
POLDSPECIAL->m_events.activeChanged.emit();
@ -1439,17 +1540,16 @@ void CMonitor::setSpecialWorkspace(const PHLWORKSPACE& pWorkspace) {
} else
pos = pos - PMONFROMMIDDLE->m_position + m_position;
*w->m_realPosition = pos;
w->m_position = pos;
w->layoutTarget()->setPositionGlobal(CBox{pos, w->layoutTarget()->position().size()});
}
}
}
g_pLayoutManager->getCurrentLayout()->recalculateMonitor(m_id);
g_layoutManager->recalculateMonitor(m_self.lock());
if (!(Desktop::focusState()->window() && Desktop::focusState()->window()->m_pinned && Desktop::focusState()->window()->m_monitor == m_self)) {
if (const auto PLAST = pWorkspace->getLastFocusedWindow(); PLAST)
Desktop::focusState()->fullWindowFocus(PLAST);
Desktop::focusState()->fullWindowFocus(PLAST, Desktop::FOCUS_REASON_KEYBIND);
else
g_pInputManager->refocus();
}
@ -1479,10 +1579,20 @@ Vector2D CMonitor::middle() {
return m_position + m_size / 2.f;
}
const Mat3x3& CMonitor::getTransformMatrix() {
return m_projMatrix;
}
const Mat3x3& CMonitor::getScaleMatrix() {
return m_projOutputMatrix;
}
void CMonitor::updateMatrix() {
m_projMatrix = Mat3x3::identity();
if (m_transform != WL_OUTPUT_TRANSFORM_NORMAL)
m_projMatrix.translate(m_pixelSize / 2.0).transform(Math::wlTransformToHyprutils(m_transform)).translate(-m_transformedSize / 2.0);
m_projOutputMatrix = Mat3x3::outputProjection(m_pixelSize, HYPRUTILS_TRANSFORM_NORMAL);
}
WORKSPACEID CMonitor::activeWorkspaceID() {
@ -1686,6 +1796,10 @@ uint8_t CMonitor::isTearingBlocked(bool full) {
}
}
// TODO: remove this when kernel allows tearing + hw cursor updated
if (g_pPointerManager->hasVisibleHWCursor(m_self.lock()))
reasons |= TC_HW_CURSOR;
if (m_solitaryClient.expired()) {
reasons |= TC_CANDIDATE;
return reasons;
@ -1727,12 +1841,6 @@ uint16_t CMonitor::isDSBlocked(bool full) {
}
}
if (m_tearingState.activelyTearing) {
reasons |= DS_BLOCK_TEARING;
if (!full)
return reasons;
}
if (!m_mirrors.empty() || isMirror()) {
reasons |= DS_BLOCK_MIRROR;
if (!full)
@ -1771,21 +1879,27 @@ uint16_t CMonitor::isDSBlocked(bool full) {
// we can't scanout shm buffers.
const auto params = PSURFACE->m_current.buffer->dmabuf();
if (!params.success || !PSURFACE->m_current.texture->m_eglImage /* dmabuf */) {
if (!params.success || !PSURFACE->m_current.texture->isDMA() /* dmabuf */) {
reasons |= DS_BLOCK_DMA;
if (!full)
return reasons;
}
const bool surfaceIsHDR = PSURFACE->m_colorManagement.valid() && (PSURFACE->m_colorManagement->isHDR() || PSURFACE->m_colorManagement->isWindowsScRGB());
if (needsCM() && *PNONSHADER != CM_NS_IGNORE && !canNoShaderCM() && ((inHDR() && (*PPASS == 0 || !surfaceIsHDR)) || (!inHDR() && (*PPASS != 1 || surfaceIsHDR))))
const bool surfaceIsHDR = PSURFACE->m_colorManagement.valid() && PSURFACE->m_colorManagement->isHDR();
const bool surfaceIsScRGB = surfaceIsHDR && PSURFACE->m_colorManagement->isWindowsScRGB();
if (needsCM() && (*PNONSHADER != CM_NS_IGNORE || surfaceIsScRGB) && !canNoShaderCM() &&
((inHDR() && (*PPASS == 0 || !surfaceIsHDR || surfaceIsScRGB)) || (!inHDR() && (*PPASS != 1 || surfaceIsHDR))))
reasons |= DS_BLOCK_CM;
return reasons;
}
bool CMonitor::attemptDirectScanout() {
const auto blockedReason = isDSBlocked();
static const auto PSAME = CConfigValue<Hyprlang::INT>("debug:ds_handle_same_buffer");
static const auto PSAMEFIFO = CConfigValue<Hyprlang::INT>("debug:ds_handle_same_buffer_fifo");
const auto blockedReason = isDSBlocked();
if (blockedReason)
return false;
@ -1799,7 +1913,7 @@ bool CMonitor::attemptDirectScanout() {
auto PBUFFER = PSURFACE->m_current.buffer.m_buffer;
// #TODO this entire bit needs figuring out, vrr goes down the drain without it
if (PBUFFER == m_output->state->state().buffer) {
if (PBUFFER == m_output->state->state().buffer && *PSAME) {
PSURFACE->presentFeedback(Time::steadyNow(), m_self.lock());
if (m_scanoutNeedsCursorUpdate) {
@ -1818,7 +1932,7 @@ bool CMonitor::attemptDirectScanout() {
}
//#TODO this entire bit is bootleg deluxe, above bit is to not make vrr go down the drain, returning early here means fifo gets forever locked.
if (PSURFACE->m_fifo)
if (PSURFACE->m_fifo && !m_tearingState.activelyTearing && *PSAMEFIFO)
PSURFACE->m_stateQueue.unlockFirst(LOCK_REASON_FIFO);
return true;
@ -1849,7 +1963,18 @@ bool CMonitor::attemptDirectScanout() {
PSURFACE->presentFeedback(Time::steadyNow(), m_self.lock());
m_output->state->addDamage(PSURFACE->m_current.accumulateBufferDamage());
m_output->state->resetExplicitFences();
// multigpu needs a fence to trigger fence syncing blits and also committing with the recreated dgpu fence
if (!DRM::sameGpu(m_output->getBackend()->preferredAllocator()->drmFD(), g_pCompositor->m_drm.fd) && g_pHyprOpenGL->explicitSyncSupported()) {
auto sync = CEGLSync::create();
if (sync->fd().isValid()) {
m_inFence = sync->takeFd();
m_output->state->setExplicitInFence(m_inFence.get());
} else
m_output->state->resetExplicitFences(); // good luck.
} else
m_output->state->resetExplicitFences();
// no need to do explicit sync here as surface current can only ever be ready to read
@ -2004,6 +2129,14 @@ int CMonitor::maxAvgLuminance(int defaultValue) {
(m_output->parsedEDID.hdrMetadata.has_value() ? m_output->parsedEDID.hdrMetadata->desiredMaxFrameAverageLuminance : defaultValue);
}
float CMonitor::maxFALL() {
return m_maxAvgLuminance >= 0 ? m_maxAvgLuminance : (m_output->parsedEDID.hdrMetadata.has_value() ? m_output->parsedEDID.hdrMetadata->desiredMaxFrameAverageLuminance : 0);
}
float CMonitor::maxCLL() {
return m_maxLuminance >= 0 ? m_maxLuminance : (m_output->parsedEDID.hdrMetadata.has_value() ? m_output->parsedEDID.hdrMetadata->desiredContentMaxLuminance : 0);
}
bool CMonitor::wantsWideColor() {
return supportsWideColor() && (wantsHDR() || m_imageDescription->value().primariesNamed == CM_PRIMARIES_BT2020);
}
@ -2017,22 +2150,52 @@ bool CMonitor::inHDR() {
}
bool CMonitor::inFullscreenMode() {
// Check special workspace first since it renders on top of regular workspaces
if (m_activeSpecialWorkspace && m_activeSpecialWorkspace->m_hasFullscreenWindow && m_activeSpecialWorkspace->m_fullscreenMode == FSMODE_FULLSCREEN)
return true;
return m_activeWorkspace && m_activeWorkspace->m_hasFullscreenWindow && m_activeWorkspace->m_fullscreenMode == FSMODE_FULLSCREEN;
}
PHLWINDOW CMonitor::getFullscreenWindow() {
// Check special workspace first since it renders on top of regular workspaces
if (m_activeSpecialWorkspace && m_activeSpecialWorkspace->m_hasFullscreenWindow && m_activeSpecialWorkspace->m_fullscreenMode == FSMODE_FULLSCREEN)
return m_activeSpecialWorkspace->getFullscreenWindow();
if (m_activeWorkspace && m_activeWorkspace->m_hasFullscreenWindow && m_activeWorkspace->m_fullscreenMode == FSMODE_FULLSCREEN)
return m_activeWorkspace->getFullscreenWindow();
return nullptr;
}
std::optional<NColorManagement::PImageDescription> CMonitor::getFSImageDescription() {
if (!inFullscreenMode())
return {};
const auto FS_WINDOW = m_activeWorkspace->getFullscreenWindow();
const auto FS_WINDOW = getFullscreenWindow();
if (!FS_WINDOW)
return {}; // should be unreachable
return {};
const auto ROOT_SURF = FS_WINDOW->wlSurface()->resource();
const auto SURF = ROOT_SURF->findWithCM();
return SURF ? NColorManagement::CImageDescription::from(SURF->m_colorManagement->imageDescription()) : DEFAULT_IMAGE_DESCRIPTION;
}
NColorManagement::SPCPRimaries CMonitor::getMasteringPrimaries() {
return m_output->parsedEDID.chromaticityCoords.has_value() ?
NColorManagement::SPCPRimaries{
.red = {.x = m_output->parsedEDID.chromaticityCoords->red.x, .y = m_output->parsedEDID.chromaticityCoords->red.y},
.green = {.x = m_output->parsedEDID.chromaticityCoords->green.x, .y = m_output->parsedEDID.chromaticityCoords->green.y},
.blue = {.x = m_output->parsedEDID.chromaticityCoords->blue.x, .y = m_output->parsedEDID.chromaticityCoords->blue.y},
.white = {.x = m_output->parsedEDID.chromaticityCoords->white.x, .y = m_output->parsedEDID.chromaticityCoords->white.y},
} :
NColorManagement::SPCPRimaries{};
}
NColorManagement::SImageDescription::SPCMasteringLuminances CMonitor::getMasteringLuminances() {
return {
.min = m_minLuminance >= 0 ? m_minLuminance : (m_output->parsedEDID.hdrMetadata.has_value() ? m_output->parsedEDID.hdrMetadata->desiredContentMinLuminance : 0),
.max = m_maxLuminance >= 0 ? m_maxLuminance : (m_output->parsedEDID.hdrMetadata.has_value() ? m_output->parsedEDID.hdrMetadata->desiredContentMaxLuminance : 0),
};
}
bool CMonitor::needsCM() {
const auto SRC_DESC = getFSImageDescription();
return SRC_DESC.has_value() && SRC_DESC.value() != m_imageDescription;
@ -2053,24 +2216,91 @@ bool CMonitor::canNoShaderCM() {
const auto SRC_DESC_VALUE = SRC_DESC.value()->value();
if (SRC_DESC_VALUE.icc.fd >= 0 || m_imageDescription->value().icc.fd >= 0)
return false; // no ICC support
if (m_imageDescription->value().icc.present)
return false;
static auto PSDREOTF = CConfigValue<Hyprlang::INT>("render:cm_sdr_eotf");
const auto sdrEOTF = NTransferFunction::fromConfig();
// only primaries differ
return ((SRC_DESC_VALUE.transferFunction == m_imageDescription->value().transferFunction ||
(*PSDREOTF == 2 && SRC_DESC_VALUE.transferFunction == NColorManagement::CM_TRANSFER_FUNCTION_SRGB &&
m_imageDescription->value().transferFunction == NColorManagement::CM_TRANSFER_FUNCTION_GAMMA22)) &&
SRC_DESC_VALUE.transferFunctionPower == m_imageDescription->value().transferFunctionPower &&
(!inHDR() || SRC_DESC_VALUE.luminances == m_imageDescription->value().luminances) &&
SRC_DESC_VALUE.masteringLuminances == m_imageDescription->value().masteringLuminances && SRC_DESC_VALUE.maxCLL == m_imageDescription->value().maxCLL &&
SRC_DESC_VALUE.maxFALL == m_imageDescription->value().maxFALL);
return (
(SRC_DESC_VALUE.transferFunction == m_imageDescription->value().transferFunction ||
(sdrEOTF == NTransferFunction::TF_FORCED_GAMMA22 && SRC_DESC_VALUE.transferFunction == NColorManagement::CM_TRANSFER_FUNCTION_SRGB &&
m_imageDescription->value().transferFunction == NColorManagement::CM_TRANSFER_FUNCTION_GAMMA22)) &&
SRC_DESC_VALUE.transferFunctionPower == m_imageDescription->value().transferFunctionPower &&
(!inHDR() || SRC_DESC_VALUE.luminances == m_imageDescription->value().luminances)
// not used by shaders atm
// && SRC_DESC_VALUE.masteringLuminances == m_imageDescription->value().masteringLuminances && SRC_DESC_VALUE.maxCLL == m_imageDescription->value().maxCLL && SRC_DESC_VALUE.maxFALL == m_imageDescription->value().maxFALL
);
}
bool CMonitor::doesNoShaderCM() {
return m_noShaderCTM;
}
static std::vector<uint16_t> resampleInterleavedToKms(const SVCGTTable16& t, size_t gammaSize) {
std::vector<uint16_t> out;
out.resize(gammaSize * 3);
//
auto sample = [&](int c, float x) -> uint16_t {
const float maxX = t.entries - 1;
x = std::clamp(x, 0.F, maxX);
const size_t i0 = (size_t)std::floor(x);
const size_t i1 = std::min(i0 + 1, (size_t)t.entries - 1);
const float f = x - sc<float>(i0);
const float v0 = sc<float>(t.ch[c][i0]);
const float v1 = sc<float>(t.ch[c][i1]);
const float v = v0 + ((v1 - v0) * f);
int64_t vi = std::round(v);
vi = std::clamp(vi, sc<int64_t>(0), sc<int64_t>(65535));
return sc<uint16_t>(vi);
};
for (size_t i = 0; i < gammaSize; ++i) {
float x = sc<float>(i) * sc<float>(t.entries - 1) / sc<float>(gammaSize - 1);
const uint16_t r = sample(0, x);
const uint16_t g = sample(1, x);
const uint16_t b = sample(2, x);
out[i * 3 + 0] = r;
out[i * 3 + 1] = g;
out[i * 3 + 2] = b;
}
return out;
}
void CMonitor::updateVCGTRamps() {
auto gammaSize = m_output->getGammaSize();
if (gammaSize <= 10) {
Log::logger->log(Log::DEBUG, "CMonitor::updateVCGTRamps: skipping, no gamma ramp for output");
return;
}
if (!m_imageDescription->value().icc.vcgt) {
if (m_vcgtRampsSet)
m_output->state->setGammaLut({});
m_vcgtRampsSet = false;
return;
}
// build table
auto table = resampleInterleavedToKms(*m_imageDescription->value().icc.vcgt, gammaSize);
m_output->state->setGammaLut(table);
m_vcgtRampsSet = true;
}
bool CMonitor::gammaRampsInUse() {
return m_vcgtRampsSet;
}
CMonitorState::CMonitorState(CMonitor* owner) : m_owner(owner) {
;
}
@ -2098,7 +2328,7 @@ bool CMonitorState::commit() {
if (!updateSwapchain())
return false;
EMIT_HOOK_EVENT("preMonitorCommit", m_owner->m_self.lock());
Event::bus()->m_events.monitor.preCommit.emit(m_owner->m_self.lock());
ensureBufferPresent();

View file

@ -16,13 +16,15 @@
#include "math/Math.hpp"
#include "../desktop/reserved/ReservedArea.hpp"
#include <optional>
#include "../protocols/types/ColorManagement.hpp"
#include "cm/ColorManagement.hpp"
#include "signal/Signal.hpp"
#include "DamageRing.hpp"
#include <aquamarine/output/Output.hpp>
#include <aquamarine/allocator/Swapchain.hpp>
#include <hyprutils/os/FileDescriptor.hpp>
#include "../helpers/TransferFunction.hpp"
class CMonitorFrameScheduler;
// Enum for the different types of auto directions, e.g. auto-left, auto-up.
@ -50,10 +52,11 @@ struct SMonitorRule {
std::string mirrorOf = "";
bool enable10bit = false;
NCMType::eCMType cmType = NCMType::CM_SRGB;
int sdrEotf = 0;
NTransferFunction::eTF sdrEotf = NTransferFunction::TF_DEFAULT;
float sdrSaturation = 1.0f; // SDR -> HDR
float sdrBrightness = 1.0f; // SDR -> HDR
Desktop::CReservedArea reservedArea;
std::string iccFile;
int supportsWideColor = 0; // 0 - auto, 1 - force enable, -1 - force disable
int supportsHDR = 0; // 0 - auto, 1 - force enable, -1 - force disable
@ -124,7 +127,7 @@ class CMonitor {
bool m_scheduledRecalc = false;
wl_output_transform m_transform = WL_OUTPUT_TRANSFORM_NORMAL;
float m_xwaylandScale = 1.f;
Mat3x3 m_projMatrix;
std::optional<Vector2D> m_forceSize;
SP<Aquamarine::SOutputMode> m_currentMode;
SP<Aquamarine::CSwapchain> m_cursorSwapchain;
@ -137,7 +140,7 @@ class CMonitor {
bool m_vrrActive = false; // this can be TRUE even if VRR is not active in the case that this display does not support it.
bool m_enabled10bit = false; // as above, this can be TRUE even if 10 bit failed.
NCMType::eCMType m_cmType = NCMType::CM_SRGB;
int m_sdrEotf = 0;
NTransferFunction::eTF m_sdrEotf = NTransferFunction::TF_DEFAULT;
float m_sdrSaturation = 1.0f;
float m_sdrBrightness = 1.0f;
float m_sdrMinLuminance = 0.2f;
@ -177,6 +180,7 @@ class CMonitor {
// for direct scanout
PHLWINDOWREF m_lastScanout;
bool m_directScanoutIsActive = false; // for cleanup logic. m_lastScanout.expired() can become true before the DS cleanup if client crashes/exits while DS is active.
bool m_scanoutNeedsCursorUpdate = false;
// for special fade/blur
@ -207,6 +211,7 @@ class CMonitor {
} m_tearingState;
struct {
CSignalT<> commit;
CSignalT<> destroy;
CSignalT<> connect;
CSignalT<> disconnect;
@ -232,9 +237,8 @@ class CMonitor {
DS_BLOCK_SURFACE = (1 << 8),
DS_BLOCK_TRANSFORM = (1 << 9),
DS_BLOCK_DMA = (1 << 10),
DS_BLOCK_TEARING = (1 << 11),
DS_BLOCK_FAILED = (1 << 12),
DS_BLOCK_CM = (1 << 13),
DS_BLOCK_FAILED = (1 << 11),
DS_BLOCK_CM = (1 << 12),
DS_CHECKS_COUNT = 14,
};
@ -275,14 +279,15 @@ class CMonitor {
TC_SUPPORT = (1 << 4),
TC_CANDIDATE = (1 << 5),
TC_WINDOW = (1 << 6),
TC_HW_CURSOR = (1 << 7),
TC_CHECKS_COUNT = 7,
TC_CHECKS_COUNT = 8,
};
// methods
void onConnect(bool noRule);
void onDisconnect(bool destroy = false);
void applyCMType(NCMType::eCMType cmType, int cmSdrEotf);
void applyCMType(NCMType::eCMType cmType, NTransferFunction::eTF cmSdrEotf);
bool applyMonitorRule(SMonitorRule* pMonitorRule, bool force = false);
void addDamage(const pixman_region32_t* rg);
void addDamage(const CRegion& rg);
@ -298,7 +303,6 @@ class CMonitor {
void setSpecialWorkspace(const WORKSPACEID& id);
void moveTo(const Vector2D& pos);
Vector2D middle();
void updateMatrix();
WORKSPACEID activeWorkspaceID();
WORKSPACEID activeSpecialWorkspaceID();
CBox logicalBox();
@ -321,17 +325,29 @@ class CMonitor {
float minLuminance(float defaultValue = 0);
int maxLuminance(int defaultValue = 80);
int maxAvgLuminance(int defaultValue = 80);
float maxFALL();
float maxCLL();
bool wantsWideColor();
bool wantsHDR();
bool inHDR();
bool gammaRampsInUse();
/// Has an active workspace with a real fullscreen window
bool inFullscreenMode();
std::optional<NColorManagement::PImageDescription> getFSImageDescription();
//
const Mat3x3& getTransformMatrix();
const Mat3x3& getScaleMatrix();
bool needsCM();
/// Has an active workspace with a real fullscreen window (includes special workspace)
bool inFullscreenMode();
/// Get fullscreen window from active or special workspace
PHLWINDOW getFullscreenWindow();
std::optional<NColorManagement::PImageDescription> getFSImageDescription();
NColorManagement::SPCPRimaries getMasteringPrimaries();
NColorManagement::SImageDescription::SPCMasteringLuminances getMasteringLuminances();
bool needsCM();
/// Can do CM without shader
bool canNoShaderCM();
bool doesNoShaderCM();
@ -342,8 +358,8 @@ class CMonitor {
PHLWINDOWREF m_previousFSWindow;
bool m_needsHDRupdate = false;
NColorManagement::PImageDescription m_imageDescription;
bool m_noShaderCTM = false; // sets drm CTM, restore needed
NColorManagement::PImageDescription m_imageDescription = NColorManagement::CImageDescription::from(NColorManagement::SImageDescription{});
bool m_noShaderCTM = false; // sets drm CTM, restore needed
// For the list lookup
@ -351,12 +367,19 @@ class CMonitor {
return m_position == rhs.m_position && m_size == rhs.m_size && m_name == rhs.m_name;
}
Mat3x3 m_projMatrix;
private:
void updateMatrix();
Mat3x3 m_projOutputMatrix;
void setupDefaultWS(const SMonitorRule&);
WORKSPACEID findAvailableDefaultWS();
void commitDPMSState(bool state);
void updateVCGTRamps();
bool m_doneScheduled = false;
bool m_vcgtRampsSet = false;
std::stack<WORKSPACEID> m_prevWorkSpaces;
struct {

View file

@ -11,7 +11,7 @@ CMonitorFrameScheduler::CMonitorFrameScheduler(PHLMONITOR m) : m_monitor(m) {
bool CMonitorFrameScheduler::newSchedulingEnabled() {
static auto PENABLENEW = CConfigValue<Hyprlang::INT>("render:new_render_scheduling");
return *PENABLENEW && g_pHyprOpenGL->explicitSyncSupported();
return *PENABLENEW && g_pHyprOpenGL->explicitSyncSupported() && m_monitor && !m_monitor->m_directScanoutIsActive;
}
void CMonitorFrameScheduler::onSyncFired() {

View file

@ -38,10 +38,10 @@ int NSystemd::sdNotify(int unsetEnvironment, const char* state) {
if (!addr)
return 0;
// address length must be at most this; see man 7 unix
size_t addrLen = strnlen(addr, 107);
struct sockaddr_un unixAddr = {0};
size_t addrLen = strnlen(addr, sizeof(unixAddr.sun_path) - 1);
struct sockaddr_un unixAddr;
unixAddr.sun_family = AF_UNIX;
strncpy(unixAddr.sun_path, addr, addrLen);
if (unixAddr.sun_path[0] == '@')

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