77 remoteHyprlandStartVnc = pkgs . writeShellScript "remote-hyprland-start-vnc" ''
88 set -euo pipefail
99
10- export WAYLAND_DISPLAY=${ cfg . socket }
1110 export XDG_CURRENT_DESKTOP=Hyprland
1211 export XDG_SESSION_DESKTOP=Hyprland
1312 export XDG_SESSION_TYPE=wayland
13+ export LIBSEAT_BACKEND=seatd
1414
1515 for _ in $(${ pkgs . coreutils } /bin/seq 1 50); do
16- if ${ hyprlandPackage } /bin/hyprctl -j monitors >/dev/null 2>&1; then
16+ instance="$(
17+ ${ hyprlandPackage } /bin/hyprctl instances \
18+ | ${ pkgs . gawk } /bin/awk '
19+ /^instance / {
20+ sig = $2
21+ sub(/:$/, "", sig)
22+ }
23+ /^[[:space:]]*time:/ { time = $2 }
24+ /^[[:space:]]*wl socket:/ {
25+ if (sig != "" && time != "") {
26+ print time " " sig " " $3
27+ }
28+ }
29+ ' \
30+ | ${ pkgs . coreutils } /bin/sort -n \
31+ | ${ pkgs . coreutils } /bin/tail -n 1
32+ )"
33+
34+ if [ -n "$instance" ]; then
35+ read -r _ HYPRLAND_INSTANCE_SIGNATURE WAYLAND_DISPLAY <<EOF
36+ $instance
37+ EOF
38+ export HYPRLAND_INSTANCE_SIGNATURE WAYLAND_DISPLAY
39+ break
40+ fi
41+ ${ pkgs . coreutils } /bin/sleep 0.1
42+ done
43+
44+ if [ -z "'' ${HYPRLAND_INSTANCE_SIGNATURE:-}" ] || [ -z "'' ${WAYLAND_DISPLAY:-}" ]; then
45+ echo "Timed out waiting for a Hyprland instance" >&2
46+ exit 1
47+ fi
48+
49+ for _ in $(${ pkgs . coreutils } /bin/seq 1 50); do
50+ if ${ hyprlandPackage } /bin/hyprctl -i "$HYPRLAND_INSTANCE_SIGNATURE" -j monitors >/dev/null 2>&1; then
1751 break
1852 fi
1953 ${ pkgs . coreutils } /bin/sleep 0.1
2054 done
2155
2256 # Give wayvnc a stable output name instead of relying on Hyprland's
2357 # fallback HEADLESS-* naming.
24- ${ hyprlandPackage } /bin/hyprctl output create headless ${ cfg . output } >/dev/null 2>&1 || true
25- ${ hyprlandPackage } /bin/hyprctl keyword monitor '${ monitorRule } ' >/dev/null 2>&1 || true
58+ ${ hyprlandPackage } /bin/hyprctl -i "$HYPRLAND_INSTANCE_SIGNATURE" output create headless ${ cfg . output } >/dev/null 2>&1 || true
59+ ${ hyprlandPackage } /bin/hyprctl -i "$HYPRLAND_INSTANCE_SIGNATURE" keyword monitor '${ monitorRule } ' >/dev/null 2>&1 || true
2660
2761 exec ${ pkgs . wayvnc } /bin/wayvnc \
2862 --log-level=info \
@@ -100,35 +134,73 @@ let
100134 bind = $mainMod SHIFT, 8, movetoworkspace, 8
101135 bind = $mainMod SHIFT, 9, movetoworkspace, 9
102136
103- exec-once = ${ remoteHyprlandStartVnc }
104137 exec-once = ${ cfg . terminalCommand }
105138 '' ;
139+ servicePath = lib . makeBinPath [
140+ pkgs . coreutils
141+ pkgs . gnugrep
142+ pkgs . gnused
143+ pkgs . systemd
144+ ] ;
145+ autostartInstall = lib . optionalAttrs cfg . autoStart {
146+ Install = {
147+ WantedBy = [ "default.target" ] ;
148+ } ;
149+ } ;
106150 enabledModule = makeEnable config "myModules.remote-hyprland" false {
107151 myModules . hyprland . enable = true ;
108152
153+ services . seatd = {
154+ enable = true ;
155+ group = "video" ;
156+ } ;
157+
109158 users . manageLingering = true ;
110159 users . users . ${ cfg . user } . linger = true ;
111160
112161 environment . systemPackages = [ pkgs . wayvnc ] ;
113162
114- home-manager . users . ${ cfg . user } . systemd . user . services . remote-hyprland = {
115- Unit = {
116- Description = "Headless Hyprland session for remote VNC access" ;
117- After = [ "default.target" ] ;
118- } ;
119- Service = {
120- ExecStart = "${ hyprlandPackage } /bin/start-hyprland --path ${ hyprlandPackage } /bin/Hyprland -- --socket ${ cfg . socket } --config ${ remoteHyprlandConfig } " ;
121- Restart = "on-failure" ;
122- RestartSec = 5 ;
123- Environment = [
124- "XDG_CURRENT_DESKTOP=Hyprland"
125- "XDG_SESSION_DESKTOP=Hyprland"
126- "XDG_SESSION_TYPE=wayland"
127- "WAYLAND_DISPLAY=${ cfg . socket } "
128- ] ;
129- } ;
130- Install = {
131- WantedBy = [ "default.target" ] ;
163+ home-manager . users . ${ cfg . user } = {
164+ systemd . user . services = {
165+ remote-hyprland = {
166+ Unit = {
167+ Description = "Headless Hyprland session for remote VNC access" ;
168+ After = [ "default.target" ] ;
169+ } ;
170+ Service = {
171+ ExecStart = "${ hyprlandPackage } /bin/start-hyprland --path ${ hyprlandPackage } /bin/Hyprland -- --config ${ remoteHyprlandConfig } " ;
172+ Restart = "on-failure" ;
173+ RestartSec = 5 ;
174+ Environment = [
175+ "XDG_CURRENT_DESKTOP=Hyprland"
176+ "XDG_SESSION_DESKTOP=Hyprland"
177+ "XDG_SESSION_TYPE=wayland"
178+ "LIBSEAT_BACKEND=seatd"
179+ "PATH=${ servicePath } "
180+ ] ;
181+ } ;
182+ } // autostartInstall ;
183+
184+ remote-hyprland-wayvnc = {
185+ Unit = {
186+ Description = "VNC server for the headless Hyprland session" ;
187+ After = [ "remote-hyprland.service" ] ;
188+ Requires = [ "remote-hyprland.service" ] ;
189+ PartOf = [ "remote-hyprland.service" ] ;
190+ } ;
191+ Service = {
192+ ExecStart = "${ remoteHyprlandStartVnc } " ;
193+ Restart = "on-failure" ;
194+ RestartSec = 5 ;
195+ Environment = [
196+ "XDG_CURRENT_DESKTOP=Hyprland"
197+ "XDG_SESSION_DESKTOP=Hyprland"
198+ "XDG_SESSION_TYPE=wayland"
199+ "LIBSEAT_BACKEND=seatd"
200+ "PATH=${ servicePath } "
201+ ] ;
202+ } ;
203+ } // autostartInstall ;
132204 } ;
133205 } ;
134206 } ;
@@ -154,12 +226,6 @@ enabledModule // {
154226 description = "TCP port for wayvnc." ;
155227 } ;
156228
157- socket = lib . mkOption {
158- type = lib . types . str ;
159- default = "wayland-remote-hyprland" ;
160- description = "Wayland socket name used by the remote Hyprland instance." ;
161- } ;
162-
163229 output = lib . mkOption {
164230 type = lib . types . str ;
165231 default = "remote" ;
@@ -195,6 +261,16 @@ enabledModule // {
195261 default = "${ pkgs . ghostty } /bin/ghostty --gtk-single-instance=false" ;
196262 description = "Command launched for the default terminal binding and initial window." ;
197263 } ;
264+
265+ autoStart = lib . mkOption {
266+ type = lib . types . bool ;
267+ default = false ;
268+ description = ''
269+ Whether to start the remote Hyprland session automatically with the
270+ user's systemd manager. Keep this disabled on single-GPU hosts with
271+ an active display manager, because Hyprland needs DRM master.
272+ '' ;
273+ } ;
198274 } ;
199275 } ;
200276}
0 commit comments