diff --git a/modules/misc/news.nix b/modules/misc/news.nix index 9248ee4fa..7def533ab 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -1144,6 +1144,14 @@ in A new module is available: 'services.swayosd' ''; } + + { + time = "2023-07-20T21:56:49+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'wayland.windowManager.hyprland' + ''; + } ]; }; } diff --git a/modules/modules.nix b/modules/modules.nix index a77b75e60..074efcb13 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -330,6 +330,7 @@ let ./services/window-managers/bspwm/default.nix ./services/window-managers/fluxbox.nix ./services/window-managers/herbstluftwm.nix + ./services/window-managers/hyprland.nix ./services/window-managers/i3-sway/i3.nix ./services/window-managers/i3-sway/sway.nix ./services/window-managers/i3-sway/swaynag.nix diff --git a/modules/services/window-managers/hyprland.nix b/modules/services/window-managers/hyprland.nix new file mode 100644 index 000000000..71395115e --- /dev/null +++ b/modules/services/window-managers/hyprland.nix @@ -0,0 +1,216 @@ +{ config, lib, pkgs, ... }: +let + + cfg = config.wayland.windowManager.hyprland; + +in { + meta.maintainers = [ lib.maintainers.fufexan ]; + + # A few option removals and renames to aid those migrating from the upstream + # module. + imports = [ + (lib.mkRemovedOptionModule # \ + [ "wayland" "windowManager" "hyprland" "disableAutoreload" ] + "Autoreloading now always happen") + + (lib.mkRemovedOptionModule # \ + [ "wayland" "windowManager" "hyprland" "recommendedEnvironment" ] + "Recommended environment variables are now always set") + + (lib.mkRenamedOptionModule # \ + [ "wayland" "windowManager" "hyprland" "nvidiaPatches" ] # \ + [ "wayland" "windowManager" "hyprland" "enableNvidiaPatches" ]) + ]; + + options.wayland.windowManager.hyprland = { + enable = lib.mkEnableOption "Hyprland wayland compositor"; + + package = lib.mkPackageOption pkgs "hyprland" { }; + + finalPackage = lib.mkOption { + type = lib.types.package; + readOnly = true; + default = cfg.package.override { + enableXWayland = cfg.xwayland.enable; + hidpiXWayland = cfg.xwayland.hidpi; + nvidiaPatches = cfg.enableNvidiaPatches; + }; + defaultText = lib.literalMD + "`wayland.windowManager.hyprland.package` with applied configuration"; + description = '' + The Hyprland package after applying configuration. + ''; + }; + + plugins = lib.mkOption { + type = with lib.types; listOf (either package path); + default = [ ]; + description = '' + List of Hyprland plugins to use. Can either be packages or + absolute plugin paths. + ''; + }; + + systemdIntegration = lib.mkOption { + type = lib.types.bool; + default = pkgs.stdenv.isLinux; + description = '' + Whether to enable {file}`hyprland-session.target` on + hyprland startup. This links to `graphical-session.target`. + Some important environment variables will be imported to systemd + and dbus user environment before reaching the target, including + - `DISPLAY` + - `HYPRLAND_INSTANCE_SIGNATURE` + - `WAYLAND_DISPLAY` + - `XDG_CURRENT_DESKTOP` + ''; + }; + + xwayland = { + enable = lib.mkEnableOption "XWayland" // { default = true; }; + hidpi = lib.mkEnableOption null // { + description = '' + Enable HiDPI XWayland, based on [XWayland MR 733](https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/733). + See for more info. + ''; + }; + }; + + enableNvidiaPatches = + lib.mkEnableOption "patching wlroots for better Nvidia support"; + + 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 valueType; + default = { }; + description = '' + Hyprland configuration written in Nix. Entries with the same key + should be written as lists. Variables' and colors' names should be + quoted. See for more examples. + + ::: {.note} + Use the [](#opt-wayland.windowManager.hyprland.plugins) option to + declare plugins. + ::: + + ''; + example = lib.literalExpression '' + { + decoration = { + shadow_offset = "0 5"; + "col.shadow" = "rgba(00000099)"; + }; + + "$mod" = "SUPER"; + + bindm = [ + # mouse movements + "$mod, mouse:272, movewindow" + "$mod, mouse:273, resizewindow" + "$mod ALT, mouse:272, resizewindow" + ]; + } + ''; + }; + + extraConfig = lib.mkOption { + type = lib.types.lines; + default = ""; + example = '' + # window resize + bind = $mod, S, submap, resize + + submap = resize + binde = , right, resizeactive, 10 0 + binde = , left, resizeactive, -10 0 + binde = , up, resizeactive, 0 -10 + binde = , down, resizeactive, 0 10 + bind = , escape, submap, reset + submap = reset + ''; + description = '' + Extra configuration lines to add to `~/.config/hypr/hyprland.conf`. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + (lib.hm.assertions.assertPlatform "wayland.windowManager.hyprland" pkgs + lib.platforms.linux) + ]; + + warnings = let + inconsistent = (cfg.systemdIntegration || cfg.plugins != [ ]) + && cfg.extraConfig == "" && cfg.settings == { }; + warning = + "You have enabled hyprland.systemdIntegration or listed plugins in hyprland.plugins but do not have any configuration in hyprland.settings or hyprland.extraConfig. This is almost certainly a mistake."; + in lib.optional inconsistent warning; + + home.packages = lib.optional (cfg.package != null) cfg.finalPackage; + + xdg.configFile."hypr/hyprland.conf" = let + combinedSettings = cfg.settings // { + plugin = let + mkEntry = entry: + if lib.types.package.check entry then + "${entry}/lib/lib${entry.pname}.so" + else + entry; + in map mkEntry cfg.plugins; + }; + + shouldGenerate = cfg.systemdIntegration || cfg.extraConfig != "" + || combinedSettings != { }; + + toHyprconf = attrs: + let + mkSection = n: attrs: '' + ${n} { + ${toHyprconf attrs}} + ''; + mkFields = lib.generators.toKeyValue { listsAsDuplicateKeys = true; }; + sections = lib.filterAttrs (n: v: lib.isAttrs v) attrs; + fields = lib.filterAttrs (n: v: !(lib.isAttrs v)) attrs; + in lib.concatStringsSep "\n" (lib.mapAttrsToList mkSection sections) + + mkFields fields; + in lib.mkIf shouldGenerate { + text = lib.optionalString cfg.systemdIntegration '' + exec-once = ${pkgs.dbus}/bin/dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP && systemctl --user start hyprland-session.target + '' + lib.optionalString (combinedSettings != { }) + (toHyprconf combinedSettings) + + lib.optionalString (cfg.extraConfig != "") cfg.extraConfig; + onChange = lib.mkIf (cfg.package != null) '' + ( # execute in subshell so that `shopt` won't affect other scripts + shopt -s nullglob # so that nothing is done if /tmp/hypr/ does not exist or is empty + for instance in /tmp/hypr/*; do + HYPRLAND_INSTANCE_SIGNATURE=''${instance##*/} ${cfg.finalPackage}/bin/hyprctl reload config-only \ + || true # ignore dead instance(s) + done + ) + ''; + }; + + systemd.user.targets.hyprland-session = lib.mkIf cfg.systemdIntegration { + Unit = { + Description = "Hyprland compositor session"; + Documentation = [ "man:systemd.special(7)" ]; + BindsTo = [ "graphical-session.target" ]; + Wants = [ "graphical-session-pre.target" ]; + After = [ "graphical-session-pre.target" ]; + }; + }; + }; +} diff --git a/tests/default.nix b/tests/default.nix index 5b6cbcb2f..97f4b4efb 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -232,6 +232,7 @@ import nmt { ./modules/services/udiskie ./modules/services/window-managers/bspwm ./modules/services/window-managers/herbstluftwm + ./modules/services/window-managers/hyprland ./modules/services/window-managers/i3 ./modules/services/window-managers/spectrwm ./modules/services/window-managers/sway diff --git a/tests/modules/services/window-managers/hyprland/default.nix b/tests/modules/services/window-managers/hyprland/default.nix new file mode 100644 index 000000000..96cae5e45 --- /dev/null +++ b/tests/modules/services/window-managers/hyprland/default.nix @@ -0,0 +1,4 @@ +{ + hyprland-simple-config = ./simple-config.nix; + hyprland-inconsistent-config = ./inconsistent-config.nix; +} diff --git a/tests/modules/services/window-managers/hyprland/inconsistent-config.nix b/tests/modules/services/window-managers/hyprland/inconsistent-config.nix new file mode 100644 index 000000000..3daad8260 --- /dev/null +++ b/tests/modules/services/window-managers/hyprland/inconsistent-config.nix @@ -0,0 +1,21 @@ +{ config, lib, ... }: + +{ + wayland.windowManager.hyprland = { + enable = true; + package = lib.makeOverridable + (attrs: config.lib.test.mkStubPackage { name = "hyprland"; }) { }; + plugins = + [ "/path/to/plugin1" (config.lib.test.mkStubPackage { name = "foo"; }) ]; + }; + + test.asserts.warnings.expected = [ + "You have enabled hyprland.systemdIntegration or listed plugins in hyprland.plugins but do not have any configuration in hyprland.settings or hyprland.extraConfig. This is almost certainly a mistake." + ]; + test.asserts.warnings.enable = true; + + nmt.script = '' + config=home-files/.config/hypr/hyprland.conf + assertFileExists "$config" + ''; +} diff --git a/tests/modules/services/window-managers/hyprland/simple-config.conf b/tests/modules/services/window-managers/hyprland/simple-config.conf new file mode 100644 index 000000000..9d310ea8e --- /dev/null +++ b/tests/modules/services/window-managers/hyprland/simple-config.conf @@ -0,0 +1,30 @@ +exec-once = /nix/store/00000000000000000000000000000000-dbus/bin/dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP && systemctl --user start hyprland-session.target +decoration { + col.shadow=rgba(00000099) +shadow_offset=0 5 +} + +input { + touchpad { + scroll_factor=0.300000 +} +accel_profile=flat +follow_mouse=1 +kb_layout=ro +} +$mod=SUPER +bindm=$mod, mouse:272, movewindow +bindm=$mod, mouse:273, resizewindow +bindm=$mod ALT, mouse:272, resizewindow +plugin=/path/to/plugin1 +plugin=/nix/store/00000000000000000000000000000000-foo/lib/libfoo.so +# window resize +bind = $mod, S, submap, resize + +submap = resize +binde = , right, resizeactive, 10 0 +binde = , left, resizeactive, -10 0 +binde = , up, resizeactive, 0 -10 +binde = , down, resizeactive, 0 10 +bind = , escape, submap, reset +submap = reset diff --git a/tests/modules/services/window-managers/hyprland/simple-config.nix b/tests/modules/services/window-managers/hyprland/simple-config.nix new file mode 100644 index 000000000..2748e5952 --- /dev/null +++ b/tests/modules/services/window-managers/hyprland/simple-config.nix @@ -0,0 +1,53 @@ +{ config, lib, ... }: + +{ + wayland.windowManager.hyprland = { + enable = true; + package = lib.makeOverridable + (attrs: config.lib.test.mkStubPackage { name = "hyprland"; }) { }; + plugins = + [ "/path/to/plugin1" (config.lib.test.mkStubPackage { name = "foo"; }) ]; + settings = { + decoration = { + shadow_offset = "0 5"; + "col.shadow" = "rgba(00000099)"; + }; + + "$mod" = "SUPER"; + + input = { + kb_layout = "ro"; + follow_mouse = 1; + accel_profile = "flat"; + touchpad = { scroll_factor = 0.3; }; + }; + + bindm = [ + # mouse movements + "$mod, mouse:272, movewindow" + "$mod, mouse:273, resizewindow" + "$mod ALT, mouse:272, resizewindow" + ]; + }; + extraConfig = '' + # window resize + bind = $mod, S, submap, resize + + submap = resize + binde = , right, resizeactive, 10 0 + binde = , left, resizeactive, -10 0 + binde = , up, resizeactive, 0 -10 + binde = , down, resizeactive, 0 10 + bind = , escape, submap, reset + submap = reset + ''; + }; + + nmt.script = '' + config=home-files/.config/hypr/hyprland.conf + assertFileExists "$config" + + normalizedConfig=$(normalizeStorePaths "$config") + assertFileContent "$normalizedConfig" ${./simple-config.conf} + ''; +}