{ inputs, getFlakePkg', lib, pkgs, config, ... }: let inherit (config) rice; inherit (inputs.niri.lib.kdl) leaf node flag serialize; niri = getFlakePkg' inputs.niri "niri-unstable"; xwayland-satellite = getFlakePkg' inputs.niri "xwayland-satellite-unstable"; ui = rice.roles rice.palette.shortHex; # serialize binds attrset to KDL serializeBind = key: bind: if bind ? action then leaf key { action = bind.action; } else if bind ? spawn then leaf key bind.spawn else flag key; bindsKdl = serialize.nodes [ (node "binds" [] ( lib.mapAttrsToList serializeBind config.niri.binds )) ]; niriConfig = let template = builtins.readFile ./niri.kdl; baseConfig = with rice; lib.replaceStrings [ "%CURSOR%" "%ROUNDING%" "%GAPS%" "%BORDER%" "%ACTIVE%" "%INACTIVE%" "%SATELLITE%" ] [ cursor.name (toString borders.rounding) (toString borders.gaps) (toString borders.thickness) ui.highlight ui.muted (lib.getExe xwayland-satellite) ] template; in baseConfig + "\n" + bindsKdl + "\n" + config.niri.extraConfig; niriConfigFile = pkgs.writeText "niri-config.kdl" niriConfig; niri-session-direct = pkgs.writeShellScript "niri-session-direct" '' export NIRI_CONFIG="${niriConfigFile}" systemctl --user import-environment dbus-update-activation-environment --all systemctl --user start niri-session-bridge.service & exec ${lib.getExe niri} --session ''; in { imports = [ inputs.niri-tag.nixosModules.niri-tag inputs.niri-s76.nixosModules.default ]; options.niri = { binds = lib.mkOption { type = lib.types.attrsOf lib.types.anything; default = { }; description = "Niri keybindings"; }; extraConfig = lib.mkOption { type = lib.types.lines; default = ""; description = "Extra KDL config appended to niri config"; }; }; config.environment.systemPackages = [ niri xwayland-satellite ]; config.environment.etc."greetd/wayland-sessions/niri.desktop".text = '' [Desktop Entry] Name=Niri Comment=A scrollable-tiling Wayland compositor Exec=${niri-session-direct} Type=Application DesktopNames=niri ''; config.programs.niri = { enable = true; package = niri; }; config.services.niri-tag = { enable = true; prepopulate = 10; strict = true; scratchpads = { "chrome-listen.lobotomise.me__-Default" = 99; "Bitwarden" = 101; }; }; config.services.niri-s76-bridge.enable = true; # niri runs directly from greetd (not as a user service), # so that it stays inside the logind session scope for proper polkit/dbus access. config.systemd.user.services.niri.wantedBy = lib.mkForce [ ]; config.systemd.user.services.niri.enable = lib.mkForce false; # bridge service to activate graphical-session.target for the direct-launched niri. # waits for niri IPC readiness before pulling in the target. config.systemd.user.services.niri-session-bridge = { unitConfig = { Description = "Activate graphical-session.target for direct-launched niri"; BindsTo = [ "graphical-session.target" ]; Before = [ "graphical-session.target" ]; Wants = [ "graphical-session-pre.target" ]; After = [ "graphical-session-pre.target" ]; }; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStart = "${lib.getExe niri} msg version"; }; }; }