{ self, inputs, ... }: let # Keep defaults in this let — a bare niri-settings.nix next to this file would get picked up by import-tree. niriBaseSettings = pkgs: { input.keyboard = { xkb.layout = "ca"; xkb.variant = ""; }; input."focus-follows-mouse" = _: { props."max-scroll-amount" = "45%"; content = { }; }; input."warp-mouse-to-focus" = _: { }; layout.gaps = 5; window-rules = [ { matches = [ { app-id = "^$"; title = "^$"; } ]; open-floating = true; open-focused = false; } ]; #TODO[epic=Binds] Go over binds again binds = { "Mod+T"."spawn-sh" = "${pkgs.lib.getExe pkgs.kitty} -e fish"; #TODO[epic=Binds] This should only be set if having kitty "Mod+Control+T"."spawn-sh" = "konsole"; #TODO[epic=Binds] This should only be set if having konsole "Mod+D"."spawn-sh" = "rofi -show drun"; #TODO[epic=Binds] This should only be set if having rofi "Mod+Space"."spawn-sh" = "dms ipc call spotlight toggle"; #TODO[epic=Binds] This should only be set if having dms "Mod+E"."spawn-sh" = "dolphin"; #TODO[epic=Binds] This should only be set if having dolphin "Mod+Control+O"."spawn-sh" = "rofi -show window"; #TODO[epic=Binds] This should only be set if having rofi "Mod+V"."spawn-sh" = "dms ipc call clipboard toggle"; #TODO[epic=Binds] This should only be set if having dms "Mod+N"."spawn-sh" = "dms ipc call notepad toggle"; #TODO[epic=Binds] This should only be set if having dms "Mod+Shift+N"."spawn-sh" = "dms ipc call notifications toggle"; #TODO[epic=Binds] This should only be set if having dms "Mod+M"."spawn-sh" = "dms ipc call processlist toggle"; #TODO[epic=Binds] This should only be set if having dms "Mod+L"."spawn-sh" = "dms ipc call lock lock"; #TODO[epic=Binds] This should only be set if having dms "Mod+Q"."close-window" = _: { }; "Mod+F"."maximize-column" = _: { }; "Mod+Shift+F"."fullscreen-window" = _: { }; "Mod+O"."toggle-overview" = _: { }; "Mod+Shift+NumberSign"."show-hotkey-overlay" = _: { }; "Mod+Shift+E".quit = _: { }; "Mod+Left"."focus-column-or-monitor-left" = _: { }; "Mod+Down"."focus-window-or-monitor-down" = _: { }; "Mod+Up"."focus-window-or-monitor-up" = _: { }; "Mod+Right"."focus-column-or-monitor-right" = _: { }; "Mod+Shift+WheelScrollDown"."focus-workspace-down" = _: { }; "Mod+Shift+WheelScrollUp"."focus-workspace-up" = _: { }; "Mod+WheelScrollDown"."focus-column-or-monitor-right" = _: { }; "Mod+WheelScrollUp"."focus-column-or-monitor-left" = _: { }; "Mod+Shift+Left"."move-column-left-or-to-monitor-left" = _: { }; "Mod+Shift+Down"."move-window-down" = _: { }; "Mod+Shift+Up"."move-window-up" = _: { }; "Mod+Shift+Right"."move-column-right-or-to-monitor-right" = _: { }; "Mod+Page_Up"."focus-workspace-up" = _: { }; "Mod+Page_Down"."focus-workspace-down" = _: { }; "Mod+Shift+Page_Up"."move-column-to-workspace-up" = _: { }; "Mod+Shift+Page_Down"."move-column-to-workspace-down" = _: { }; "Mod+R"."switch-preset-column-width" = _: { }; "Mod+BracketLeft"."consume-or-expel-window-left" = _: { }; "Mod+BracketRight"."consume-or-expel-window-right" = _: { }; "Mod+Comma"."consume-window-into-column" = _: { }; "Mod+Period"."expel-window-from-column" = _: { }; "Mod+Alt+Space"."toggle-window-floating" = _: { }; #"Mod+Shift+V"."switch-focus-between-floating-and-tiling" = _: { }; #TODO[epic=Binds] Find free bind that is not in the way and not overcomplicated to remember "Mod+1"."focus-workspace" = 1; "Mod+2"."focus-workspace" = 2; "Mod+3"."focus-workspace" = 3; "Mod+4"."focus-workspace" = 4; "Mod+5"."focus-workspace" = 5; "Mod+6"."focus-workspace" = 6; "Mod+7"."focus-workspace" = 7; "Mod+8"."focus-workspace" = 8; "Mod+9"."focus-workspace" = 9; "Mod+Ctrl+1"."move-column-to-workspace" = 1; "Mod+Ctrl+2"."move-column-to-workspace" = 2; "Mod+Ctrl+3"."move-column-to-workspace" = 3; "Mod+Ctrl+4"."move-column-to-workspace" = 4; "Mod+Ctrl+5"."move-column-to-workspace" = 5; "Mod+Ctrl+6"."move-column-to-workspace" = 6; "Mod+Ctrl+7"."move-column-to-workspace" = 7; "Mod+Ctrl+8"."move-column-to-workspace" = 8; "Mod+Ctrl+9"."move-column-to-workspace" = 9; "XF86AudioRaiseVolume".spawn = [ "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.05+" ]; "XF86AudioLowerVolume".spawn = [ "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.05-" ]; "XF86AudioMute".spawn = [ "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle" ]; # Backlight: relies on `pkgs.brightnessctl` being on PATH (provided by `desktopGui` when # any GUI session is enabled) and the user being in the `video` group (catalog default). "XF86MonBrightnessUp".spawn = [ "brightnessctl" "set" "+5%" ]; "XF86MonBrightnessDown".spawn = [ "brightnessctl" "set" "5%-" ]; Print.screenshot = _: { }; "Ctrl+Print"."screenshot-screen" = _: { }; "Alt+Print"."screenshot-window" = _: { }; }; }; mergeNiriSettings = pkgs: niriCfg: let lib = pkgs.lib; pi5 = self.lib.pi5NiriKdl; rpi5Extra = lib.optionalString (niriCfg.raspberryPi5DrmWorkaround or false) pi5.drmExtraConfig; userExtra = niriCfg.extraSettings or { }; extraConfigMerged = rpi5Extra + (userExtra.extraConfig or ""); in lib.recursiveUpdate (niriBaseSettings pkgs) ( userExtra // lib.optionalAttrs (rpi5Extra != "" || (userExtra.extraConfig or "") != "") { extraConfig = extraConfigMerged; } ); in { flake.homeManagerModules.desktopNiri = { lib, pkgs, osConfig ? { }, ... }: let niriOs = osConfig.chiasson.desktop.niri or { }; niriEnabled = osConfig.chiasson.desktop.niri.enable or false; mergedSettings = mergeNiriSettings pkgs niriOs; niriConfigPkg = inputs.wrapper-modules.wrappers.niri.wrap { inherit pkgs; settings = mergedSettings; }; in { config = lib.mkIf niriEnabled { xdg.configFile."niri/config.kdl".source = "${niriConfigPkg}/niri-config.kdl"; }; }; flake.nixosModules.desktopNiri = { config, options, lib, pkgs, self, ... }: let cfg = config.chiasson.desktop; hmAvailable = lib.hasAttrByPath [ "home-manager" "sharedModules" ] options; in { options.chiasson.desktop.niri = { enable = lib.mkOption { type = lib.types.bool; default = false; description = "Niri compositor session + NixOS packages."; }; raspberryPi5DrmWorkaround = lib.mkEnableOption '' Pi 5 + RP1 DSI: Niri DRM debug rules (renderD128, ignore card1/2) + matching DankGreeter niri config. Opt-in only — HDMI-only / other Pi setups do not need this. ''; extraSettings = lib.mkOption { type = lib.types.attrs; default = { }; description = '' Merged into shared defaults → `config.kdl` via wrapper-modules. Put raw KDL in `extraConfig`. ''; }; }; config = lib.mkMerge [ { assertions = [ { assertion = !cfg.niri.raspberryPi5DrmWorkaround || cfg.niri.enable; message = "chiasson.desktop.niri.raspberryPi5DrmWorkaround requires chiasson.desktop.niri.enable."; } ]; } (lib.mkIf cfg.niri.enable { programs.niri.enable = true; programs.niri.package = pkgs.niri; programs.xwayland.enable = true; xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gnome ]; # Niri resolves `xwayland-satellite` from PATH to provide XWayland + `$DISPLAY` for X11 # clients (Steam, etc.). See https://github.com/YaLTeR/niri/issues/452 environment.systemPackages = [ pkgs.xwayland-satellite ]; }) (lib.mkIf (cfg.niri.enable && hmAvailable) { "home-manager".sharedModules = [ self.homeManagerModules.desktopNiri ]; }) ]; }; }