{ 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; prefer-no-csd = true; window-rules = [ { matches = [ { app-id = "^$"; title = "^$"; } ]; open-floating = true; open-focused = false; } { matches = [ { title = "^(Picture-in-Picture|Picture in picture)$"; } ]; open-floating = true; open-focused = false; default-column-width = { fixed = 640; }; default-window-height = { fixed = 360; }; default-floating-position = _: { props = { x = 32; y = 32; relative-to = "bottom-right"; }; }; } { matches = [ { app-id = "^Minecraft\\*"; } { app-id = "^Minecraft$"; } ]; tiled-state = true; } ]; #TODO[epic=Binds] Go over binds again binds = { "Mod+T"."spawn-sh" = pkgs.lib.getExe pkgs.kitty; # shell from kitty HM config "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" = "nemo"; #TODO[epic=Binds] This should only be set if having nemo "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+B"."spawn-sh" = "dms ipc call bar toggle name \"Main Bar\"; dms ipc call bar toggle name \"Bar 2\""; #TODO[epic=Binds] This should only be set if having dms "Mod+Shift+B"."spawn-sh" = "dms ipc call bar toggle name \"Bar 2\""; #TODO[epic=Binds] This should only be set if having dms "Mod+A"."spawn-sh" = "dms ipc call ephemera toggle"; #TODO[epic=Binds] This should only be set if having dms "Mod+numbersign"."spawn-sh" = "dms ipc call wallpaperCarousel toggle"; # Mod+# — 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" = _: { }; }; }; keyringNiriStartupKdl = '' spawn-at-startup "dbus-update-activation-environment" "--systemd" "--all" ''; mergeNiriSettings = pkgs: niriCfg: keyringEnable: let lib = pkgs.lib; pi5 = self.lib.pi5NiriKdl; rpi5Extra = lib.optionalString (niriCfg.raspberryPi5DrmWorkaround or false) pi5.drmExtraConfig; base = niriBaseSettings pkgs; userExtra = niriCfg.extraSettings or { }; keyringExtra = lib.optionalString keyringEnable keyringNiriStartupKdl; extraConfigMerged = keyringExtra + rpi5Extra + (userExtra.extraConfig or ""); windowRules = (base.window-rules or [ ]) ++ (userExtra.window-rules or [ ]); userExtraNoSpecial = lib.removeAttrs userExtra [ "window-rules" "extraConfig" "binds" ]; mergedBinds = (base.binds or { }) // (userExtra.binds or { }) // (niriCfg.extraBinds or { }); in lib.recursiveUpdate base ( userExtraNoSpecial // { binds = mergedBinds; } // lib.optionalAttrs (windowRules != [ ]) { window-rules = windowRules; } // lib.optionalAttrs (extraConfigMerged != "") { extraConfig = extraConfigMerged; } ); in { flake.homeManagerModules.desktopNiri = { config, lib, pkgs, osConfig ? { }, ... }: let home = config.home.homeDirectory; niriOs = osConfig.chiasson.desktop.niri or { }; niriEnabled = osConfig.chiasson.desktop.niri.enable or false; keyringEnabled = osConfig.chiasson.desktop.keyring.enable or false; dmsEnabled = (osConfig.chiasson.desktop.shell or null) == "dms"; dmsWindowRulesPath = "${home}/.config/niri/dms/windowrules.kdl"; # wrapper-modules emits quoted KDL (`"include" "…"`) which DMS's parser ignores. # config.kdl is a store symlink, so a relative include would not resolve to the real # ~/.config/niri/dms/windowrules.kdl that DMS writes at runtime. dmsWindowRulesInclude = lib.optionalString dmsEnabled '' include "${dmsWindowRulesPath}" ''; baseMergedSettings = mergeNiriSettings pkgs niriOs keyringEnabled; mergedSettings = lib.recursiveUpdate baseMergedSettings { extraConfig = dmsWindowRulesInclude + (baseMergedSettings.extraConfig or ""); }; niriConfigPkg = inputs.wrapper-modules.wrappers.niri.wrap { inherit pkgs; settings = mergedSettings; # User-owned rules path is not available during the Nix build. disableConfigValidation = dmsEnabled; }; in { config = lib.mkIf niriEnabled (lib.mkMerge [ { xdg.configFile."niri/config.kdl".source = "${niriConfigPkg}/niri-config.kdl"; } (lib.mkIf dmsEnabled { # Seed empty rules file so niri can load the include before DMS first deploy. home.activation.dmsNiriWindowRulesSeed = lib.hm.dag.entryAfter [ "writeBoundary" ] '' rulesDir="$HOME/.config/niri/dms" rulesFile="$rulesDir/windowrules.kdl" mkdir -p "$rulesDir" if [ ! -e "$rulesFile" ]; then ${pkgs.coreutils}/bin/touch "$rulesFile" fi ''; }) ]); }; 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`. ''; }; extraBinds = lib.mkOption { type = lib.types.attrsOf lib.types.anything; default = { }; description = '' Keybind overrides merged with shared defaults and `extraSettings.binds`. Use this when multiple modules need to add binds without overwriting each other. ''; }; }; 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 ]; }) ]; }; }