feat(compositors)!: Add niri

According to its github page, niri is "A scrollable-tiling Wayland
compositor". Myself and coded have been looking for alternatives to
hyprland for stability and community reasons, and niri looks reasonable
for us

It's possible we'll remove hyprland altogether. If we do, we plan to
still maintain a more traditional compositor (e.g. swayfx)

BREAKING-CHANGE: This patch renames some hyprland options
Change-Id: I170dde769b55ebb32a3212a54012443fb4d0faf8
Reviewed-on: https://git.clicks.codes/c/Chimera/NixFiles/+/713
Reviewed-by: Samuel Shuert <coded@clicks.codes>
Tested-by: Samuel Shuert <coded@clicks.codes>
diff --git a/modules/home/wayland/default.nix b/modules/home/wayland/default.nix
new file mode 100644
index 0000000..3ea8d65
--- /dev/null
+++ b/modules/home/wayland/default.nix
@@ -0,0 +1,43 @@
+{
+  lib,
+  config,
+  ...
+}: {
+  options.chimera = {
+    wayland.enable = lib.mkOption {
+      type = lib.types.bool;
+      description = "Enable generic options which are useful for all wayland compositors";
+      default = false;
+      internal = true;
+    };
+    input.mouse.scrolling.natural = lib.mkEnableOption "Enable natural scrolling";
+    input.touchpad.scrolling.natural = lib.mkOption {
+      type = lib.types.bool;
+      description = "Enable natural scrolling";
+      default = config.chimera.input.mouse.scrolling.natural;
+    };
+    input.keyboard = {
+      layout = lib.mkOption {
+        type = lib.types.str;
+        description = "Keyboard layouts, comma seperated";
+        example = "us,de";
+        default = "us";
+      };
+      variant = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        description = "Keyboard layout variants, comma seperated";
+        example = "dvorak";
+        default = null;
+      };
+    };
+
+    input.keybinds.alternativeSearch.enable = lib.mkEnableOption "Use alt + space or SUPER + D to open search";
+
+    nvidia.enable = lib.mkEnableOption "Enable NVIDIA support";
+  };
+
+  config = lib.mkIf config.chimera.wayland.enable {
+    chimera.waybar.enable = lib.mkDefault true;
+    chimera.notifications.mako.enable = true;
+  };
+}
diff --git a/modules/home/wayland/hyprland/default.nix b/modules/home/wayland/hyprland/default.nix
new file mode 100644
index 0000000..fab449e
--- /dev/null
+++ b/modules/home/wayland/hyprland/default.nix
@@ -0,0 +1,232 @@
+{
+  pkgs,
+  config,
+  inputs,
+  system,
+  lib,
+  ...
+}:
+{
+  options.chimera = {
+    hyprland = {
+      enable = lib.mkEnableOption "Use hyprland as your window manager";
+      monitors = lib.mkOption {
+        type = lib.types.listOf lib.types.str;
+        description = "List of default monitors to set";
+        default = [ ];
+      };
+      window = {
+        rounding = lib.mkOption {
+          type = lib.types.int;
+          description = "How round the windows should be";
+          default = 7;
+        };
+        blur = lib.mkOption {
+          type = lib.types.int;
+          description = "How blurred the wallpaper under innactive windows should be";
+          default = 8;
+        };
+      };
+      startupApplications = lib.mkOption {
+        type = lib.types.listOf lib.types.str;
+        description = "List of commands to run on hyprland start";
+        default = [ ];
+      };
+      keybinds = {
+        volumeStep = lib.mkOption {
+          type = lib.types.int;
+          description = "Amount to increase volume by when media keys are pressed in %";
+          example = "5";
+          default = 5;
+        };
+        extraBinds = let
+          binds = lib.types.submodule {
+            options = {
+              meta = lib.mkOption {
+                type = lib.types.nullOr lib.types.str;
+                description = "Additional modifier keys space seperated";
+                default = null;
+              };
+              key = lib.mkOption {
+                type = lib.types.str;
+                description = "Final key";
+              };
+              function = lib.mkOption {
+                type = lib.types.str;
+                description = "Hyprland bind function";
+              };
+            };
+          };
+        in lib.mkOption {
+          type = lib.types.listOf binds;
+          description = "Extra keybinds to add";
+          default = [ ];
+        };
+      };
+    };
+  };
+  config = lib.mkIf config.chimera.hyprland.enable (let
+    lock = "${pkgs.waylock}/bin/waylock";
+  in {
+    chimera.wayland.enable = true;
+
+    programs.bash.profileExtra = lib.mkIf config.chimera.shell.bash.enable (lib.mkBefore ''
+      if [ -z $WAYLAND_DISPLAY ] && [ "$(tty)" = "/dev/tty1" ]; then
+        exec ${pkgs.systemd}/bin/systemd-cat -t hyprland ${pkgs.dbus}/bin/dbus-run-session ${config.wayland.windowManager.hyprland.package}/bin/Hyprland
+      fi
+    '');
+
+    programs.zsh.profileExtra = lib.mkIf config.chimera.shell.zsh.enable (lib.mkBefore ''
+      if [ -z $WAYLAND_DISPLAY ] && [ "$(tty)" = "/dev/tty1" ]; then
+        exec ${pkgs.systemd}/bin/systemd-cat -t hyprland ${pkgs.dbus}/bin/dbus-run-session ${config.wayland.windowManager.hyprland.package}/bin/Hyprland
+      fi
+    '');
+
+    home.packages = [ pkgs.hyprpicker ];
+
+    services.fusuma.settings.swipe = lib.mkIf config.chimera.touchpad.enable (
+      let
+        hyprctl = "${config.wayland.windowManager.hyprland.package}/bin/hyprctl";
+        jq = "${pkgs.jq}/bin/jq";
+        awk = "${pkgs.gawk}/bin/awk";
+      in
+      {
+        "3".up.command = "${hyprctl} dispatch fullscreen 0";
+        "3".down.command = "${hyprctl} dispatch fullscreen 0";
+        "4".down.command = lock;
+        "3".left.command = "${hyprctl} dispatch workspace $(${hyprctl} activeworkspace -j | ${jq} .id | ${awk} '{print $1+1}')";
+        "3".right.command = "${hyprctl} dispatch workspace $(${hyprctl} activeworkspace -j | ${jq} .id | ${awk} '{print $1-1}')";
+      }
+    );
+
+    wayland.windowManager.hyprland = {
+      enable = true;
+
+      xwayland.enable = true;
+      systemd.enable = true;
+
+      settings =
+        let
+          mod = "SUPER";
+          terminal = "${pkgs.kitty}/bin/kitty";
+          menu = (if config.chimera.runner.anyrun.enable then "${inputs.anyrun.packages.${system}.anyrun}/bin/anyrun" else "");
+          screenshot = "${pkgs.grim}/bin/grim -g \"$(${pkgs.slurp}/bin/slurp -d)\" - | ${pkgs.wl-clipboard}/bin/wl-copy";
+        in
+        {
+          misc = {
+            disable_hyprland_logo = true;
+            disable_splash_rendering = true;
+          };
+
+          exec-once = [
+            "${pkgs.hyprpaper}/bin/hyprpaper"
+            "hyprctl setcursor ${config.chimera.theme.cursor.name} ${builtins.toString config.chimera.theme.cursor.size}"
+            "${pkgs.waybar}/bin/waybar"
+          ] ++ config.chimera.hyprland.startupApplications;
+
+          monitor = config.chimera.hyprland.monitors ++ [ ",preferred,auto,1" ];
+
+          general = {
+            border_size = 1;
+            "col.active_border" = "rgba(${config.chimera.theme.colors.Surface0.hex}FF)";
+            "col.inactive_border" = "rgba(${config.chimera.theme.colors.Surface0.hex}FF)";
+          };
+
+          decoration = {
+            rounding = config.chimera.hyprland.window.rounding;
+            drop_shadow = false;
+            blur.size = config.chimera.hyprland.window.blur;
+          };
+
+          input = {
+            kb_layout = config.chimera.input.keyboard.layout;
+            kb_variant =
+              lib.mkIf (config.chimera.input.keyboard.variant != null)
+                config.chimera.input.keyboard.variant;
+            natural_scroll = config.chimera.input.mouse.scrolling.natural;
+
+            numlock_by_default = true;
+
+            touchpad = {
+              natural_scroll = config.chimera.input.touchpad.scrolling.natural;
+            };
+          };
+
+          xwayland = {
+            force_zero_scaling = true;
+          };
+
+          dwindle = {
+            pseudotile = true;
+            smart_split = true;
+          };
+
+          master = {
+            allow_small_split = true;
+            new_is_master = true;
+          };
+
+          windowrulev2 = [ "opacity 1.0 0.85,title:(.*)" ];
+
+          bind =
+            [
+              "${mod}, Q, killactive"
+              "${mod}, SPACE, togglefloating"
+              "${mod}, RETURN, exec, ${terminal}"
+              "${mod}, down, movefocus, d"
+              "${mod}, up, movefocus, u"
+              "${mod}, right, movefocus, r"
+              "${mod}, left, movefocus, l"
+              "${mod}, L, exec, ${lock}"
+              "${mod}, R, exec, ${screenshot}"
+              ", Print, exec, ${screenshot}"
+            ]
+            ++ (if config.chimera.runner.enable then [ "${mod}, D, exec, ${menu}" ] else [])
+            ++ (if lib.and config.chimera.input.keybinds.alternativeSearch.enable config.chimera.runner.enable then [ "ALT, SPACE, exec, ${menu}" ] else [])
+            ++ (builtins.concatLists (
+              builtins.genList
+                (
+                  x:
+                  let
+                    ws =
+                      let
+                        c = (x + 1) / 10;
+                      in
+                      builtins.toString (x + 1 - (c * 10));
+                  in
+                  [
+                    "${mod}, ${ws}, workspace, ${toString (x + 1)}"
+                    "${mod} SHIFT, ${ws}, movetoworkspace, ${toString (x + 1)}"
+                  ]
+                )
+                10
+            ))
+            ++ (builtins.map (item: "SUPER_${item.meta}, ${item.key}, ${item.function}") config.chimera.hyprland.keybinds.extraBinds)
+            ++ [
+              # Volume controls
+              ", XF86AudioRaiseVolume, exec, ${pkgs.pamixer}/bin/pamixer -i ${toString config.chimera.hyprland.keybinds.volumeStep}"
+              ", XF86AudioLowerVolume, exec, ${pkgs.pamixer}/bin/pamixer -d ${toString config.chimera.hyprland.keybinds.volumeStep}"
+              ", XF86AudioMute, exec, ${pkgs.pamixer}/bin/pamixer -t"
+              # Pause and play
+              ", XF86AudioPlay, exec, ${pkgs.playerctl}/bin/playerctl play-pause"
+              # Next and previous
+              ", XF86AudioNext, exec, ${pkgs.playerctl}/bin/playerctl next"
+              ", XF86AudioPrev, exec, ${pkgs.playerctl}/bin/playerctl previous"
+            ];
+
+          bindm = [
+            "${mod}, mouse:272, movewindow"
+            "${mod}, mouse:273, resizewindow"
+          ];
+
+          env = lib.mkIf config.chimera.nvidia.enable [
+            "LIBVA_DRIVER_NAME,nvidia"
+            "XDG_SESSION_TYPE,wayland"
+            "GBM_BACKEND,nvidia-drm"
+            "__GLX_VENDOR_LIBRARY_NAME,nvidia"
+            "WLR_NO_HARDWARE_CURSORS,1"
+          ];
+        };
+    };
+  });
+}
diff --git a/modules/home/wayland/niri/default.nix b/modules/home/wayland/niri/default.nix
new file mode 100644
index 0000000..0695273
--- /dev/null
+++ b/modules/home/wayland/niri/default.nix
@@ -0,0 +1,277 @@
+{
+  pkgs,
+  config,
+  inputs,
+  system,
+  lib,
+  ...
+}: {
+
+  options.chimera.niri = {
+    enable = lib.mkEnableOption "Use Niri as your window manager";
+    monitors = lib.mkOption {
+      type = lib.types.attrsOf (lib.types.submodule {
+        options = {
+          enable = lib.mkOption {
+            type = lib.types.bool;
+            default = true;
+            description = "Enable this monitor";
+          };
+          mode = lib.mkOption {
+            type = lib.types.nullOr (lib.types.submodule {
+              options = {
+                width = lib.mkOption {
+                  type = lib.types.int;
+                };
+                height = lib.mkOption {
+                  type = lib.types.int;
+                };
+                refresh = lib.mkOption {
+                  type = lib.types.nullOr lib.types.float;
+                  default = null;
+                };
+              };
+            });
+            default = null;
+          };
+          position = lib.mkOption {
+            type = lib.types.nullOr (lib.types.submodule {
+              options = {
+                x = lib.mkOption {
+                  type = lib.types.int;
+                };
+                y = lib.mkOption {
+                  type = lib.types.int;
+                };
+              };
+            });
+            default = null;
+          };
+          scale = lib.mkOption {
+            type = lib.types.float;
+            default = 1.;
+          };
+          transform = {
+            flipped = lib.mkOption {
+              type = lib.types.bool;
+              default = false;
+            };
+            rotation = lib.mkOption {
+              type = lib.types.enum [ 0 90 180 270 ];
+              default = 0;
+            };
+          };
+          variable-refresh-rate = lib.mkEnableOption "Enable Variable Refresh Rate (AMD FreeSync / Nvidia G-Sync)";
+        };
+      });
+      description = "Atribute set of monitors";
+      default = { };
+    };
+  };
+
+  config = lib.mkIf config.chimera.niri.enable {
+    chimera.wayland.enable = true;
+
+    programs.bash.profileExtra = lib.mkIf config.chimera.shell.bash.enable (lib.mkBefore ''
+      if [ -z $WAYLAND_DISPLAY ] && [ "$(tty)" = "/dev/tty1" ]; then
+        exec ${pkgs.systemd}/bin/systemd-cat -t niri ${pkgs.dbus}/bin/dbus-run-session ${config.programs.niri.package}/bin/niri --session
+      fi
+    '');
+
+    programs.zsh.profileExtra = lib.mkIf config.chimera.shell.zsh.enable (lib.mkBefore ''
+      if [ -z $WAYLAND_DISPLAY ] && [ "$(tty)" = "/dev/tty1" ]; then
+        exec ${pkgs.systemd}/bin/systemd-cat -t niri ${pkgs.dbus}/bin/dbus-run-session ${config.programs.niri.package}/bin/niri --session
+      fi
+    '');
+
+    home.sessionVariables.NIXOS_OZONE_WL = "1";
+
+    programs.niri = let
+      mod = "Super";
+      mod1 = "Alt";
+      terminal = "${pkgs.kitty}/bin/kitty";
+      menu = (if config.chimera.runner.anyrun.enable then "${inputs.anyrun.packages.${system}.anyrun}/bin/anyrun" else "");
+
+      lock = ''${pkgs.swaylock}/bin/swaylock -i ${config.chimera.theme.wallpaper} -s fill'';
+    in {
+      enable = true;
+      package = pkgs.niri-stable;
+      settings = {
+        input.keyboard = {
+          track-layout = "window";
+          repeat-delay = 200;
+          repeat-rate = 25;
+          xkb = {
+            layout = config.chimera.input.keyboard.layout;
+            variant = config.chimera.input.keyboard.variant;
+          };
+        };
+
+        input.mouse.natural-scroll = config.chimera.input.mouse.scrolling.natural;
+        input.touchpad.natural-scroll = config.chimera.input.touchpad.scrolling.natural;
+
+        input.warp-mouse-to-focus = true;
+        input.focus-follows-mouse = true;
+
+        input.power-key-handling.enable = false;
+
+        binds = let
+          inherit (config.lib.niri) actions;
+
+          generateWorkspaceBindings = workspaceNumber: {
+            "${mod}+${builtins.toString (lib.mod workspaceNumber 10)}".action = actions.focus-workspace workspaceNumber;
+            "${mod}+Shift+${builtins.toString (lib.mod workspaceNumber 10)}".action = actions.move-column-to-workspace workspaceNumber;
+          };
+          joinAttrsetList = listOfAttrsets: lib.fold (a: b: a // b) {} listOfAttrsets;
+        in { # General Keybinds
+          "${mod}+Q".action = actions.close-window;
+          "${mod}+Return".action = actions.spawn "${terminal}";
+          "${mod}+L".action = actions.spawn ["sh" "-c" "${config.programs.niri.package}/bin/niri msg action do-screen-transition && ${lock}"];
+          "${mod}+P".action = actions.power-off-monitors;
+
+          "${mod}+R".action = actions.screenshot;
+          "${mod}+Ctrl+R".action = actions.screenshot-screen;
+          "${mod}+Shift+R".action = actions.screenshot-window;
+          "Print".action = actions.screenshot;
+          "Ctrl+Print".action = actions.screenshot-screen;
+          "Shift+Print".action = actions.screenshot-window;
+
+          "${mod}+Space".action = actions."switch-layout" "next";
+          "${mod}+Shift+Space".action = actions."switch-layout" "prev";
+
+          "${mod}+D" = lib.mkIf config.chimera.runner.enable { action = actions.spawn "${menu}"; };
+          "Alt+Space" = lib.mkIf (
+              config.chimera.runner.enable
+              && config.chimera.input.keybinds.alternativeSearch.enable
+            ) { action = actions.spawn "${menu}"; };
+          "${mod}+Shift+Slash".action = actions.show-hotkey-overlay;
+        } // ( # Workspace Keybinds
+          lib.pipe (lib.range 1 10) [
+            (map generateWorkspaceBindings)
+            joinAttrsetList
+          ]
+        ) // ( # Window Manipulation Bindings
+        {
+          "${mod}+BracketLeft".action = actions."consume-or-expel-window-left";
+          "${mod}+BracketRight".action = actions."consume-or-expel-window-right";
+          "${mod}+Shift+BracketLeft".action = actions."consume-window-into-column";
+          "${mod}+Shift+BracketRight".action = actions."expel-window-from-column";
+          "${mod}+Slash".action = actions."switch-preset-column-width";
+          "${mod}+${mod1}+F".action = actions."fullscreen-window";
+
+          # Focus
+          "${mod}+Up".action = actions."focus-window-or-workspace-up";
+          "${mod}+Down".action = actions."focus-window-or-workspace-down";
+
+          # Non Jump Movement
+          "${mod}+Shift+Up".action = actions."move-window-up-or-to-workspace-up";
+          "${mod}+Shift+Down".action = actions."move-window-down-or-to-workspace-down";
+          "${mod}+Shift+Left".action = actions."consume-or-expel-window-left";
+          "${mod}+Shift+Right".action = actions."consume-or-expel-window-right";
+
+          # To Monitor
+          "${mod}+Shift+Ctrl+Up".action = actions."move-window-to-monitor-up";
+          "${mod}+Shift+Ctrl+Down".action = actions."move-window-to-monitor-down";
+          "${mod}+Shift+Ctrl+Left".action = actions."move-window-to-monitor-left";
+          "${mod}+Shift+Ctrl+Right".action = actions."move-window-to-monitor-right";
+
+          # To Workspace
+          "${mod}+Ctrl+Up".action = actions."move-window-to-workspace-up";
+          "${mod}+Ctrl+Down".action = actions."move-window-to-workspace-down";
+
+          # Sizing
+          "${mod}+Equal".action = actions."set-window-height" "+5%";
+          "${mod}+Minus".action = actions."set-window-height" "-5%";
+        }) // ( # Column Manipulation Bindings
+        {
+          # Focus
+          "${mod}+Left".action = actions."focus-column-left";
+          "${mod}+Right".action = actions."focus-column-right";
+          "${mod}+${mod1}+C".action = actions."center-column";
+          "${mod}+F".action = actions."maximize-column";
+
+          # Non Monitor Movement
+          "${mod}+${mod1}+Shift+Up".action = actions."move-column-to-workspace-up";
+          "${mod}+${mod1}+Shift+Down".action = actions."move-column-to-workspace-down";
+          "${mod}+${mod1}+Shift+Left".action = actions."move-column-left";
+          "${mod}+${mod1}+Shift+Right".action = actions."move-column-right";
+
+          # To Monitor
+          "${mod}+${mod1}+Shift+Ctrl+Up".action = actions."move-column-to-monitor-up";
+          "${mod}+${mod1}+Shift+Ctrl+Down".action = actions."move-column-to-monitor-down";
+          "${mod}+${mod1}+Shift+Ctrl+Left".action = actions."move-column-to-monitor-left";
+          "${mod}+${mod1}+Shift+Ctrl+Right".action = actions."move-column-to-monitor-right";
+
+          # Sizing
+          "${mod}+${mod1}+Equal".action = actions."set-column-width" "+5%";
+          "${mod}+${mod1}+Minus".action = actions."set-column-width" "-5%";
+        }) // ( # Workspace Manipulation Bindings
+        {
+          # Focus
+          "${mod}+Page_Up".action = actions."focus-workspace-up";
+          "${mod}+Page_Down".action = actions."focus-workspace-down";
+
+          # Within Itself
+          "${mod}+Shift+Page_Up".action = actions."move-workspace-up";
+          "${mod}+Shift+Page_Down".action = actions."move-workspace-down";
+
+          # To Monitor
+          "${mod}+Shift+Ctrl+Page_Up".action = actions."move-workspace-to-monitor-up";
+          "${mod}+Shift+Ctrl+Page_Down".action = actions."move-workspace-to-monitor-down";
+          "${mod}+Shift+Ctrl+Home".action = actions."move-workspace-to-monitor-left";
+          "${mod}+Shift+Ctrl+End".action = actions."move-workspace-to-monitor-right";
+        }) // { # Audio
+          "XF86AudioRaiseVolume" = {
+            allow-when-locked = true;
+            action = actions.spawn "${pkgs.wireplumber}/bin/wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.05+";
+          };
+          "XF86AudioLowerVolume" = {
+            allow-when-locked = true;
+            action = actions.spawn "${pkgs.wireplumber}/bin/wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.05-";
+          };
+          "XF86AudioMute" = {
+            allow-when-locked = true;
+            action = actions.spawn "${pkgs.wireplumber}/bin/wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle";
+          };
+          "XF86AudioMicMute" = {
+            allow-when-locked = true;
+            action = actions.spawn "${pkgs.wireplumber}/bin/wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle";
+          };
+        };
+
+        outputs = config.chimera.niri.monitors;
+
+        cursor = {
+          size = config.chimera.theme.cursor.size;
+          theme = config.chimera.theme.cursor.name;
+        };
+
+        layout = {
+          gaps = 16;
+
+          center-focused-column = "on-overflow";
+
+          preset-column-widths = [
+            { proportion = 1. / 4.; }
+            { proportion = 1. / 3.; }
+            { proportion = 1. / 2.; }
+            { proportion = 2. / 3.; }
+          ]; # TODO: clicks to PR a docs update for niri-flake
+        };
+
+        prefer-no-csd = true; # No "client-side-decorations" (i.e. client-side window open/close buttons)
+        hotkey-overlay.skip-at-startup = true;
+        screenshot-path = null;
+
+        spawn-at-startup = [
+          {
+            command = [ "${pkgs.waybar}/bin/waybar" ];
+          }
+          {
+            command = [ "${pkgs.swaybg}/bin/swaybg" "-i" "${config.chimera.theme.wallpaper}" "-m" "fill" ];
+          }
+        ];
+      };
+    };
+  };
+}