NixDaemon

NixOS pentest workstation as one flake — IceBreaker's successor
git clone https://git.daemon-sec.xyz/NixDaemon.git
Log | Files | Refs | README

niri.nix (11477B)


      1 # modules/features/desktop/niri.nix — Niri (scrolling-column Wayland
      2 # compositor) with Noctalia Shell, as a wrapped, portable package plus the
      3 # NixOS module that installs it as a login session.
      4 #
      5 #   nix run ~/NixDaemon#niri      try it nested inside the running desktop (Alt is the modifier)
      6 #   daemon.desktop.niri.enable    the switch (modules/features/desktop/options.nix)
      7 #
      8 # The settings below become niri's config.kdl at build time (the wrapper runs
      9 # `niri validate` on it, so a bad bind fails the build, not the login) and the
     10 # package carries every program its binds spawn. Keys mirror the Hyprland set
     11 # (modules/home/hypr/defaults.lua) where a Niri or Noctalia counterpart exists.
     12 # Rosé Pine Main: the focus ring is rose #ebbcba on overlay #26233a.
     13 { self, inputs, ... }:
     14 {
     15   perSystem =
     16     { pkgs, lib, self', ... }:
     17     let
     18       kitty = lib.getExe pkgs.kitty;
     19       zen = lib.getExe' inputs.zen-browser.packages.${pkgs.stdenv.hostPlatform.system}.default "zen-beta"; # default browser (modules/home/zen.nix)
     20       nautilus = lib.getExe pkgs.nautilus;
     21       grim = lib.getExe pkgs.grim;
     22       slurp = lib.getExe pkgs.slurp;
     23       gradia = lib.getExe self'.packages.gradia;
     24       wl-copy = "${pkgs.wl-clipboard}/bin/wl-copy";
     25       wpctl = "${pkgs.wireplumber}/bin/wpctl";
     26       brightnessctl = lib.getExe pkgs.brightnessctl;
     27       playerctl = lib.getExe pkgs.playerctl;
     28 
     29       # a bind that also works on the lock screen (volume, brightness, media)
     30       locked = cmd: _: {
     31         props.allow-when-locked = true;
     32         content.spawn-sh = cmd;
     33       };
     34       # a bind listed in the Mod+Shift+/ hotkey overlay under its own title
     35       titled = title: action: _: {
     36         props.hotkey-overlay-title = title;
     37         content = action;
     38       };
     39       workspaceBinds = lib.foldl' (acc: n: acc // {
     40         "Mod+${toString n}".focus-workspace = n;
     41         "Mod+Shift+${toString n}".move-column-to-workspace = n;
     42       }) { } (lib.range 1 9);
     43 
     44       # One niri, parameterised by the Noctalia it starts and the keyboard.
     45       mkNiri =
     46         {
     47           noctaliaPkg,
     48           xkb ? { layout = "us"; },
     49         }:
     50         let
     51           noctalia = lib.getExe noctaliaPkg;
     52           ipc = target: "${noctalia} ipc call ${target}";
     53         in
     54         inputs.wrapper-modules.wrappers.niri.wrap {
     55           inherit pkgs;
     56 
     57         settings = {
     58           input = {
     59             keyboard = {
     60               inherit xkb;
     61             };
     62             touchpad = {
     63               tap = _: { };
     64               natural-scroll = _: { };
     65             };
     66             focus-follows-mouse = _: { };
     67           };
     68 
     69           layout = {
     70             gaps = 8;
     71             focus-ring = {
     72               width = 2;
     73               active-color = "#ebbcba";
     74               inactive-color = "#26233a";
     75             };
     76             border.off = _: { };
     77             preset-column-widths = [
     78               { proportion = 0.33333; }
     79               { proportion = 0.5; }
     80               { proportion = 0.66667; }
     81             ];
     82           };
     83 
     84           prefer-no-csd = _: { };
     85           hotkey-overlay.skip-at-startup = _: { };
     86           xwayland-satellite.path = lib.getExe pkgs.xwayland-satellite;
     87 
     88           # The same session variables the Hyprland side sets (hosts/laptop configuration.nix).
     89           environment = {
     90             ELECTRON_OZONE_PLATFORM_HINT = "auto";
     91             QT_QPA_PLATFORM = "wayland;xcb";
     92           };
     93 
     94           spawn-at-startup = [ noctalia ];
     95 
     96           binds = {
     97             # apps
     98             "Mod+Return".spawn = [ kitty ];
     99             "Mod+Shift+Return".spawn = [ zen ];
    100             "Mod+Shift+B".spawn = [ zen ];
    101             "Mod+Shift+Alt+B".spawn = [ zen "--private-window" ];
    102             "Mod+Shift+F".spawn = [ nautilus "--new-window" ];
    103             "Mod+Shift+O".spawn-sh = "obsidian"; # unfree, from the system profile (modules/features/workstation.nix)
    104             "Mod+Shift+N".spawn-sh = "${kitty} -e \${EDITOR:-nano}";
    105 
    106             # Noctalia
    107             "Mod+Space".spawn-sh = ipc "launcher toggle";
    108             "Mod+Alt+Space".spawn-sh = ipc "launcher toggle";
    109             "Mod+Escape".spawn-sh = ipc "sessionMenu toggle";
    110             "Mod+Ctrl+P".spawn-sh = ipc "sessionMenu toggle";
    111             "Mod+A".spawn-sh = ipc "controlCenter toggle";
    112             "Mod+Comma".spawn-sh = ipc "notifications clear";
    113             "Mod+Ctrl+V".spawn-sh = ipc "launcher clipboard";
    114             "Mod+Ctrl+Space".spawn-sh = ipc "wallpaper toggle";
    115             "Mod+Shift+Escape".spawn-sh = ipc "lockScreen lock";
    116 
    117             # windows and columns
    118             "Mod+Q".close-window = _: { };
    119             "Mod+F".maximize-column = _: { };
    120             "Mod+G".fullscreen-window = _: { };
    121             "Mod+Shift+V".toggle-window-floating = _: { };
    122             "Mod+H".focus-column-left = _: { };
    123             "Mod+J".focus-window-down = _: { };
    124             "Mod+K".focus-window-up = _: { };
    125             "Mod+L".focus-column-right = _: { };
    126             "Mod+Left".focus-column-left = _: { };
    127             "Mod+Down".focus-window-down = _: { };
    128             "Mod+Up".focus-window-up = _: { };
    129             "Mod+Right".focus-column-right = _: { };
    130             "Mod+Shift+H".move-column-left = _: { };
    131             "Mod+Shift+J".move-window-down = _: { };
    132             "Mod+Shift+K".move-window-up = _: { };
    133             "Mod+Shift+L".move-column-right = _: { };
    134             "Mod+Shift+Left".move-column-left = _: { };
    135             "Mod+Shift+Down".move-window-down = _: { };
    136             "Mod+Shift+Up".move-window-up = _: { };
    137             "Mod+Shift+Right".move-column-right = _: { };
    138             "Mod+Ctrl+H".set-column-width = "-5%";
    139             "Mod+Ctrl+L".set-column-width = "+5%";
    140             "Mod+Ctrl+J".set-window-height = "-5%";
    141             "Mod+Ctrl+K".set-window-height = "+5%";
    142             # stacking: pull the focused window into the column beside it
    143             # (under the window there), or push it back out into its own column
    144             "Mod+BracketLeft" = titled "Stack window under the column on the left" { consume-or-expel-window-left = _: { }; };
    145             "Mod+BracketRight" = titled "Stack window under the column on the right" { consume-or-expel-window-right = _: { }; };
    146             "Mod+Period" = titled "Pop bottom window out of the column" { expel-window-from-column = _: { }; };
    147             "Mod+W" = titled "Toggle column as tabs" { toggle-column-tabbed-display = _: { }; };
    148             # evening out: preset widths/heights, back to an equal split
    149             "Mod+R" = titled "Cycle column width ⅓ ½ ⅔" { switch-preset-column-width = _: { }; };
    150             "Mod+Shift+R" = titled "Cycle window height ⅓ ½ ⅔" { switch-preset-window-height = _: { }; };
    151             "Mod+Ctrl+R" = titled "Reset window height (even split)" { reset-window-height = _: { }; };
    152             "Mod+Equal" = titled "Column width ½ (even columns)" { set-column-width = "50%"; };
    153             "Mod+Ctrl+F" = titled "Expand column into free space" { expand-column-to-available-width = _: { }; };
    154             "Mod+C" = titled "Centre column" { center-column = _: { }; };
    155             "Mod+WheelScrollDown" = _: {
    156               props.cooldown-ms = 150;
    157               content.focus-workspace-down = _: { };
    158             };
    159             "Mod+WheelScrollUp" = _: {
    160               props.cooldown-ms = 150;
    161               content.focus-workspace-up = _: { };
    162             };
    163             "Mod+O".toggle-overview = _: { };
    164             "Mod+Shift+Slash".show-hotkey-overlay = _: { };
    165 
    166             # screenshots: Print and Shift+Print open the shot in Gradia to
    167             # annotate (arrows, text, blur, background) then copy or save;
    168             # Ctrl+Print is the quick region-to-clipboard grab
    169             "Print" = titled "Screenshot region → editor" {
    170               spawn-sh = "region=$(${slurp}) && ${grim} -g \"$region\" - | ${gradia}";
    171             };
    172             "Shift+Print" = titled "Screenshot screen → editor" { spawn-sh = "${grim} - | ${gradia}"; };
    173             "Ctrl+Print" = titled "Screenshot region → clipboard" {
    174               spawn-sh = "region=$(${slurp}) && ${grim} -g \"$region\" - | ${wl-copy}";
    175             };
    176 
    177             # media and hardware keys, also on the lock screen
    178             "XF86AudioRaiseVolume" = locked "${wpctl} set-volume -l 1.4 @DEFAULT_AUDIO_SINK@ 5%+";
    179             "XF86AudioLowerVolume" = locked "${wpctl} set-volume -l 1.4 @DEFAULT_AUDIO_SINK@ 5%-";
    180             "XF86AudioMute" = locked "${wpctl} set-mute @DEFAULT_AUDIO_SINK@ toggle";
    181             "XF86AudioMicMute" = locked "${wpctl} set-mute @DEFAULT_AUDIO_SOURCE@ toggle";
    182             "XF86MonBrightnessUp" = locked "${brightnessctl} set 5%+";
    183             "XF86MonBrightnessDown" = locked "${brightnessctl} set 5%-";
    184             "XF86AudioPlay" = locked "${playerctl} play-pause";
    185             "XF86AudioPause" = locked "${playerctl} play-pause";
    186             "XF86AudioNext" = locked "${playerctl} next";
    187             "XF86AudioPrev" = locked "${playerctl} previous";
    188 
    189             "Mod+Shift+E".quit = _: { }; # niri asks for confirmation
    190           } // workspaceBinds;
    191         };
    192               };
    193     in
    194     {
    195       # Gradia, the screenshot editor the Print binds open, with its Censor tool
    196       # replaced by a secure blur (_gradia-secure-blur.patch). Upstream
    197       # pixelates in fixed 8 px blocks with the size slider greyed out, which
    198       # leaves large text readable. Patched, the slider sets the strength: the
    199       # area is averaged into cells 4× the slider value (12–100 px, 56 by
    200       # default), each cell gets random brightness noise so depixelation tools
    201       # can't match the true averages, and it is scaled back up smoothly.
    202       # Still not zero-leak: for secrets, a filled rectangle is the safe choice.
    203       # Shared with home-manager (modules/home/session.nix) so the launcher
    204       # entry runs the same build.
    205       packages.gradia = pkgs.gradia.overrideAttrs (old: {
    206         patches = (old.patches or [ ]) ++ [ ./_gradia-secure-blur.patch ];
    207       });
    208 
    209       # packages.niri is what anyone running the flake gets (US layout, the
    210       # neutral Noctalia); packages.niri-daemon is the author's (his keyboard
    211       # options, packages.noctalia-daemon). The generic host builds its own
    212       # with its keyboard layout through legacyPackages.mkNiri.
    213       packages.niri = mkNiri { noctaliaPkg = self'.packages.noctalia; };
    214       packages.niri-daemon = mkNiri {
    215         noctaliaPkg = self'.packages.noctalia-daemon;
    216         xkb = {
    217           layout = "us";
    218           options = "compose:caps,shift:both_capslock_cancel"; # as services.xserver.xkb
    219         };
    220       };
    221       legacyPackages.mkNiri = mkNiri;
    222     };
    223 
    224   flake.nixosModules.desktop-niri =
    225     { config, lib, pkgs, ... }:
    226     {
    227       config = lib.mkIf config.daemon.desktop.niri.enable {
    228         # nixpkgs' module registers the session for tuigreet and adds the
    229         # GNOME portal Niri documents; the package is the wrapped one above.
    230         programs.niri = {
    231           enable = true;
    232           package = config.daemon.desktop.niri.package;
    233         };
    234         # Noctalia's battery widget reads UPower (nixpkgs' module leaves it off).
    235         services.upower.enable = true;
    236         # nixpkgs' portal config for niri names the gtk implementations for
    237         # Access, FileChooser and Notification; keep that portal installed even
    238         # when the Hyprland side (which also adds it) is switched off.
    239         xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gtk ];
    240       };
    241     };
    242 }