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
diff --git a/modules/home/gestures/default.nix b/modules/home/gestures/default.nix
index f80d690..c471fcb 100644
--- a/modules/home/gestures/default.nix
+++ b/modules/home/gestures/default.nix
@@ -2,7 +2,8 @@
 {
   options.chimera.touchpad.enable = lib.mkEnableOption "Enable touchpad gestures";
 
-  config = lib.mkIf config.chimera.touchpad.enable {
+
+  config = lib.mkIf (config.chimera.touchpad.enable && config.chimera.hyprland.enable) {
     services.fusuma.enable = true;
 
     systemd.user.startServices = "sd-switch";
diff --git a/modules/home/waybar/default.nix b/modules/home/waybar/default.nix
index 9f3194c..52da59a 100644
--- a/modules/home/waybar/default.nix
+++ b/modules/home/waybar/default.nix
@@ -84,7 +84,7 @@
           layer = "top";
           position = "top";
           margin = "9 13 -10 18";
-          modules-left = [ "hyprland/workspaces" "keyboard-state" ];
+          modules-left = (if config.chimera.hyprland.enable then ["hyprland/workspaces"] else []);
           modules-center = ["clock"];
           modules-right = ["pulseaudio" "custom/mem" "cpu"]
             ++ (if config.chimera.waybar.modules.temperature.enable then [ "temperature" ] else [])
@@ -92,7 +92,7 @@
             ++ (if config.chimera.waybar.modules.battery.enable then [ "battery" ] else [])
             ++ [ "tray" ];
 
-          "hyprland/workspaces" = {
+          "hyprland/workspaces" = lib.mkIf config.chimera.hyprland.enable {
             "disable-scroll" = true;
           };
           clock = {
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/hyprland/default.nix b/modules/home/wayland/hyprland/default.nix
similarity index 73%
rename from modules/home/hyprland/default.nix
rename to modules/home/wayland/hyprland/default.nix
index 6f70088..fab449e 100644
--- a/modules/home/hyprland/default.nix
+++ b/modules/home/wayland/hyprland/default.nix
@@ -6,72 +6,8 @@
   lib,
   ...
 }:
-let
-  lock = "${pkgs.waylock}/bin/waylock";
-in
 {
   options.chimera = {
-    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";
-      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 = [ ];
-      };
-    };
-    startupApplications = lib.mkOption {
-      type = lib.types.listOf lib.types.str;
-      description = "List of commands to run on hyprland start";
-      default = [ ];
-    };
-
-    nvidia.enable = lib.mkEnableOption "Enable NVIDIA support";
-
     hyprland = {
       enable = lib.mkEnableOption "Use hyprland as your window manager";
       monitors = lib.mkOption {
@@ -91,11 +27,48 @@
           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 {
-    chimera.waybar.enable = lib.mkDefault true;
+  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
@@ -149,7 +122,7 @@
             "${pkgs.hyprpaper}/bin/hyprpaper"
             "hyprctl setcursor ${config.chimera.theme.cursor.name} ${builtins.toString config.chimera.theme.cursor.size}"
             "${pkgs.waybar}/bin/waybar"
-          ] ++ config.chimera.startupApplications;
+          ] ++ config.chimera.hyprland.startupApplications;
 
           monitor = config.chimera.hyprland.monitors ++ [ ",preferred,auto,1" ];
 
@@ -228,11 +201,11 @@
                 )
                 10
             ))
-            ++ (builtins.map (item: "SUPER${if isNull item.meta then "" else "_${item.meta}"}, ${item.key}, ${item.function}") config.chimera.input.keybinds.extraBinds)
+            ++ (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.input.keybinds.volumeStep}"
-              ", XF86AudioLowerVolume, exec, ${pkgs.pamixer}/bin/pamixer -d ${toString config.chimera.input.keybinds.volumeStep}"
+              ", 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"
@@ -255,5 +228,5 @@
           ];
         };
     };
-  };
+  });
 }
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" ];
+          }
+        ];
+      };
+    };
+  };
+}
diff --git a/modules/nixos/compositors/default.nix b/modules/nixos/compositors/default.nix
new file mode 100644
index 0000000..3d4f109
--- /dev/null
+++ b/modules/nixos/compositors/default.nix
@@ -0,0 +1,28 @@
+{ pkgs, lib, config, ... }:
+{
+  options.chimera.compositors = {
+    hyprland.enable = lib.mkEnableOption "Enable if at least 1 user on the system uses hyprland";
+    niri.enable = lib.mkEnableOption "Enable if at least 1 user on the system uses niri";
+  };
+
+  config = {
+    fonts.enableDefaultPackages = lib.mkDefault true;
+    hardware.opengl.enable = lib.mkDefault true;
+
+    programs.hyprland.enable = config.chimera.compositors.hyprland.enable;
+
+    xdg.portal.enable = lib.mkIf config.chimera.compositors.niri.enable true;
+    xdg.portal.config.common.default = "*"; # HACK: fixme @minion3665, try removing this and check the warning
+
+    xdg.portal.extraPortals = lib.mkIf (
+      config.chimera.compositors.hyprland.enable
+      || config.chimera.compositors.niri.enable
+    ) [ pkgs.xdg-desktop-portal-gtk ];
+
+    programs.dconf.enable = true; # FIXME: should be set to true if gtk is being used
+
+    security.polkit.enable = true;
+
+    chimera.xdg-open.enable = lib.mkDefault true;
+  };
+}
diff --git a/modules/nixos/hyprland/default.nix b/modules/nixos/hyprland/default.nix
deleted file mode 100644
index 80a7733..0000000
--- a/modules/nixos/hyprland/default.nix
+++ /dev/null
@@ -1,6 +0,0 @@
-{ pkgs, lib, ... }:
-{
-  programs.hyprland.enable = true;
-  xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gtk ];
-  chimera.xdg-open.enable = lib.mkDefault true;
-}
diff --git a/modules/nixos/users/default.nix b/modules/nixos/users/default.nix
index 4f8b70d..6556aeb 100644
--- a/modules/nixos/users/default.nix
+++ b/modules/nixos/users/default.nix
@@ -37,5 +37,8 @@
     ];
   };
 
+   # TODO: can we determine these from what home-manager says our users are using?
+  security.pam.services.swaylock = { };
   security.pam.services.waylock = { };
+  security.pam.services.gtklock = { };
 }