287 lines
8.7 KiB
Nix
287 lines
8.7 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
makeEnable,
|
|
...
|
|
}: let
|
|
cfg = config.myModules.remote-hyprland;
|
|
hyprlandPackage = config.programs.hyprland.package;
|
|
geometry = "${toString cfg.width}x${toString cfg.height}@${toString cfg.refreshRate}";
|
|
monitorRule = "${cfg.output},${geometry},0x0,${toString cfg.scale}";
|
|
remoteHyprlandStartVnc = pkgs.writeShellScript "remote-hyprland-start-vnc" ''
|
|
set -euo pipefail
|
|
|
|
export XDG_CURRENT_DESKTOP=Hyprland
|
|
export XDG_SESSION_DESKTOP=Hyprland
|
|
export XDG_SESSION_TYPE=wayland
|
|
export LIBSEAT_BACKEND=seatd
|
|
|
|
for _ in $(${pkgs.coreutils}/bin/seq 1 50); do
|
|
instance="$(
|
|
${hyprlandPackage}/bin/hyprctl instances \
|
|
| ${pkgs.gawk}/bin/awk '
|
|
/^instance / {
|
|
sig = $2
|
|
sub(/:$/, "", sig)
|
|
}
|
|
/^[[:space:]]*time:/ { time = $2 }
|
|
/^[[:space:]]*wl socket:/ {
|
|
if (sig != "" && time != "") {
|
|
print time " " sig " " $3
|
|
}
|
|
}
|
|
' \
|
|
| ${pkgs.coreutils}/bin/sort -n \
|
|
| ${pkgs.coreutils}/bin/tail -n 1
|
|
)"
|
|
|
|
if [ -n "$instance" ]; then
|
|
read -r _ HYPRLAND_INSTANCE_SIGNATURE WAYLAND_DISPLAY <<EOF
|
|
$instance
|
|
EOF
|
|
export HYPRLAND_INSTANCE_SIGNATURE WAYLAND_DISPLAY
|
|
break
|
|
fi
|
|
${pkgs.coreutils}/bin/sleep 0.1
|
|
done
|
|
|
|
if [ -z "''${HYPRLAND_INSTANCE_SIGNATURE:-}" ] || [ -z "''${WAYLAND_DISPLAY:-}" ]; then
|
|
echo "Timed out waiting for a Hyprland instance" >&2
|
|
exit 1
|
|
fi
|
|
|
|
for _ in $(${pkgs.coreutils}/bin/seq 1 50); do
|
|
if ${hyprlandPackage}/bin/hyprctl -i "$HYPRLAND_INSTANCE_SIGNATURE" -j monitors >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
${pkgs.coreutils}/bin/sleep 0.1
|
|
done
|
|
|
|
# Give wayvnc a stable output name instead of relying on Hyprland's
|
|
# fallback HEADLESS-* naming.
|
|
${hyprlandPackage}/bin/hyprctl -i "$HYPRLAND_INSTANCE_SIGNATURE" output create headless ${cfg.output} >/dev/null 2>&1 || true
|
|
${hyprlandPackage}/bin/hyprctl -i "$HYPRLAND_INSTANCE_SIGNATURE" keyword monitor '${monitorRule}' >/dev/null 2>&1 || true
|
|
|
|
exec ${pkgs.wayvnc}/bin/wayvnc \
|
|
--log-level=info \
|
|
--output ${cfg.output} \
|
|
${cfg.bindAddress} ${toString cfg.port}
|
|
'';
|
|
remoteHyprlandConfig = pkgs.writeText "remote-hyprland.conf" ''
|
|
monitor=${monitorRule}
|
|
monitor=,${geometry},0x0,${toString cfg.scale}
|
|
|
|
env = XDG_CURRENT_DESKTOP,Hyprland
|
|
env = XDG_SESSION_DESKTOP,Hyprland
|
|
env = XDG_SESSION_TYPE,wayland
|
|
|
|
input {
|
|
kb_layout = us
|
|
follow_mouse = 1
|
|
}
|
|
|
|
general {
|
|
gaps_in = 4
|
|
gaps_out = 8
|
|
border_size = 2
|
|
layout = dwindle
|
|
}
|
|
|
|
decoration {
|
|
rounding = 4
|
|
}
|
|
|
|
dwindle {
|
|
pseudotile = true
|
|
preserve_split = true
|
|
}
|
|
|
|
misc {
|
|
disable_hyprland_logo = true
|
|
disable_splash_rendering = true
|
|
force_default_wallpaper = 0
|
|
}
|
|
|
|
$mainMod = SUPER
|
|
bind = $mainMod, Return, exec, ${cfg.terminalCommand}
|
|
bind = $mainMod, D, exec, ${pkgs.rofi}/bin/rofi -show drun
|
|
bind = $mainMod, Q, killactive
|
|
bind = $mainMod SHIFT, M, exit
|
|
|
|
bind = $mainMod, H, movefocus, l
|
|
bind = $mainMod, J, movefocus, d
|
|
bind = $mainMod, K, movefocus, u
|
|
bind = $mainMod, L, movefocus, r
|
|
|
|
bind = $mainMod SHIFT, H, movewindow, l
|
|
bind = $mainMod SHIFT, J, movewindow, d
|
|
bind = $mainMod SHIFT, K, movewindow, u
|
|
bind = $mainMod SHIFT, L, movewindow, r
|
|
|
|
bind = $mainMod, 1, workspace, 1
|
|
bind = $mainMod, 2, workspace, 2
|
|
bind = $mainMod, 3, workspace, 3
|
|
bind = $mainMod, 4, workspace, 4
|
|
bind = $mainMod, 5, workspace, 5
|
|
bind = $mainMod, 6, workspace, 6
|
|
bind = $mainMod, 7, workspace, 7
|
|
bind = $mainMod, 8, workspace, 8
|
|
bind = $mainMod, 9, workspace, 9
|
|
|
|
bind = $mainMod SHIFT, 1, movetoworkspace, 1
|
|
bind = $mainMod SHIFT, 2, movetoworkspace, 2
|
|
bind = $mainMod SHIFT, 3, movetoworkspace, 3
|
|
bind = $mainMod SHIFT, 4, movetoworkspace, 4
|
|
bind = $mainMod SHIFT, 5, movetoworkspace, 5
|
|
bind = $mainMod SHIFT, 6, movetoworkspace, 6
|
|
bind = $mainMod SHIFT, 7, movetoworkspace, 7
|
|
bind = $mainMod SHIFT, 8, movetoworkspace, 8
|
|
bind = $mainMod SHIFT, 9, movetoworkspace, 9
|
|
|
|
exec-once = ${cfg.terminalCommand}
|
|
'';
|
|
servicePath = lib.makeBinPath [
|
|
pkgs.coreutils
|
|
pkgs.gnugrep
|
|
pkgs.gnused
|
|
pkgs.systemd
|
|
];
|
|
autostartInstall = lib.optionalAttrs cfg.autoStart {
|
|
Install = {
|
|
WantedBy = ["default.target"];
|
|
};
|
|
};
|
|
enabledModule = makeEnable config "myModules.remote-hyprland" false {
|
|
myModules.hyprland.enable = true;
|
|
|
|
services.seatd = {
|
|
enable = true;
|
|
group = "video";
|
|
};
|
|
|
|
users.manageLingering = true;
|
|
users.users.${cfg.user}.linger = true;
|
|
|
|
environment.systemPackages = [pkgs.wayvnc];
|
|
|
|
home-manager.users.${cfg.user} = {
|
|
systemd.user.services = {
|
|
remote-hyprland =
|
|
{
|
|
Unit = {
|
|
Description = "Headless Hyprland session for remote VNC access";
|
|
After = ["default.target"];
|
|
};
|
|
Service = {
|
|
ExecStart = "${hyprlandPackage}/bin/start-hyprland --path ${hyprlandPackage}/bin/Hyprland -- --config ${remoteHyprlandConfig}";
|
|
Restart = "on-failure";
|
|
RestartSec = 5;
|
|
Environment = [
|
|
"XDG_CURRENT_DESKTOP=Hyprland"
|
|
"XDG_SESSION_DESKTOP=Hyprland"
|
|
"XDG_SESSION_TYPE=wayland"
|
|
"LIBSEAT_BACKEND=seatd"
|
|
"PATH=${servicePath}"
|
|
];
|
|
};
|
|
}
|
|
// autostartInstall;
|
|
|
|
remote-hyprland-wayvnc =
|
|
{
|
|
Unit = {
|
|
Description = "VNC server for the headless Hyprland session";
|
|
After = ["remote-hyprland.service"];
|
|
Requires = ["remote-hyprland.service"];
|
|
PartOf = ["remote-hyprland.service"];
|
|
};
|
|
Service = {
|
|
ExecStart = "${remoteHyprlandStartVnc}";
|
|
Restart = "on-failure";
|
|
RestartSec = 5;
|
|
Environment = [
|
|
"XDG_CURRENT_DESKTOP=Hyprland"
|
|
"XDG_SESSION_DESKTOP=Hyprland"
|
|
"XDG_SESSION_TYPE=wayland"
|
|
"LIBSEAT_BACKEND=seatd"
|
|
"PATH=${servicePath}"
|
|
];
|
|
};
|
|
}
|
|
// autostartInstall;
|
|
};
|
|
};
|
|
};
|
|
in
|
|
enabledModule
|
|
// {
|
|
options = lib.recursiveUpdate enabledModule.options {
|
|
myModules.remote-hyprland = {
|
|
user = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "imalison";
|
|
description = "User account that owns the remote Hyprland session.";
|
|
};
|
|
|
|
bindAddress = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "127.0.0.1";
|
|
description = "Address for wayvnc to bind. Keep localhost when using SSH or Tailscale forwarding.";
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 5900;
|
|
description = "TCP port for wayvnc.";
|
|
};
|
|
|
|
output = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "remote";
|
|
description = "Stable Hyprland headless output name captured by wayvnc.";
|
|
};
|
|
|
|
width = lib.mkOption {
|
|
type = lib.types.ints.positive;
|
|
default = 1920;
|
|
description = "Remote output width.";
|
|
};
|
|
|
|
height = lib.mkOption {
|
|
type = lib.types.ints.positive;
|
|
default = 1080;
|
|
description = "Remote output height.";
|
|
};
|
|
|
|
refreshRate = lib.mkOption {
|
|
type = lib.types.ints.positive;
|
|
default = 60;
|
|
description = "Remote output refresh rate.";
|
|
};
|
|
|
|
scale = lib.mkOption {
|
|
type = lib.types.number;
|
|
default = 1;
|
|
description = "Remote output scale.";
|
|
};
|
|
|
|
terminalCommand = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "${pkgs.ghostty}/bin/ghostty --gtk-single-instance=false";
|
|
description = "Command launched for the default terminal binding and initial window.";
|
|
};
|
|
|
|
autoStart = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = ''
|
|
Whether to start the remote Hyprland session automatically with the
|
|
user's systemd manager. Keep this disabled on single-GPU hosts with
|
|
an active display manager, because Hyprland needs DRM master.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
}
|