Add reusable remote Hyprland module

This commit is contained in:
2026-04-29 11:17:02 -07:00
parent a37e83fb23
commit bb909849bd
3 changed files with 202 additions and 0 deletions

View File

@@ -35,6 +35,7 @@
./postgres.nix
./rabbitmq.nix
./quickshell.nix
./remote-hyprland.nix
./secrets.nix
./ssh.nix
./sni.nix

View File

@@ -63,6 +63,7 @@
# Disable the old multi-node railbird k3s setup
myModules.railbird-k3s.enable = false;
myModules."keepbook-sync".enable = true;
myModules.remote-hyprland.enable = true;
# Mirror the old biskcomp "Syncthing hosting" pattern: serve the synced railbird tree over HTTPS with autoindex.
services.nginx.virtualHosts."syncthing.railbird.ai" = {

200
nixos/remote-hyprland.nix Normal file
View File

@@ -0,0 +1,200 @@
{ 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 WAYLAND_DISPLAY=${cfg.socket}
export XDG_CURRENT_DESKTOP=Hyprland
export XDG_SESSION_DESKTOP=Hyprland
export XDG_SESSION_TYPE=wayland
for _ in $(${pkgs.coreutils}/bin/seq 1 50); do
if ${hyprlandPackage}/bin/hyprctl -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 output create headless ${cfg.output} >/dev/null 2>&1 || true
${hyprlandPackage}/bin/hyprctl 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 = ${remoteHyprlandStartVnc}
exec-once = ${cfg.terminalCommand}
'';
enabledModule = makeEnable config "myModules.remote-hyprland" false {
myModules.hyprland.enable = true;
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/Hyprland --socket ${cfg.socket} --config ${remoteHyprlandConfig}";
Restart = "on-failure";
RestartSec = 5;
Environment = [
"XDG_CURRENT_DESKTOP=Hyprland"
"XDG_SESSION_DESKTOP=Hyprland"
"XDG_SESSION_TYPE=wayland"
"WAYLAND_DISPLAY=${cfg.socket}"
];
};
Install = {
WantedBy = [ "default.target" ];
};
};
};
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.";
};
socket = lib.mkOption {
type = lib.types.str;
default = "wayland-remote-hyprland";
description = "Wayland socket name used by the remote Hyprland instance.";
};
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.";
};
};
};
}