{ config, lib, pkgs, ... }: let cfg = config.sydnix.users.crumb.niri; in { options.sydnix.users.crumb.niri = { enable = lib.mkEnableOption "Niri, à la crumb"; wallpaper = lib.mkOption { type = lib.types.nullOr lib.types.path; default = config.stylix.image; }; # xwayland-satellite implements rootless Xwayland in a separate application, # without the host compositor's involvement. It makes X11 windows appear as # normal windows, just like a native Xwayland integration. While it is still # somewhat experimental, it handles a lot of applications correctly, like # Steam, games and Discord. xwayland-satellite.enable = lib.mkEnableOption "xwayland-satellite starting with Niri" // { default = true; }; }; config = lib.mkIf cfg.enable { stylix.targets.swaylock.enable = true; services.polkit-gnome.enable = true; # File-picker. home.packages = [ pkgs.nautilus ]; # Enable the XDG portal, an interface for programs to request # permissions/resources. This is necessary to screen-cast on # Wayland/Pipewire. # # See https://github.com/YaLTeR/niri/wiki/Important-Software#portals. # Niri's NixOS module automatically brings in xdg-desktop-portal-gnome and # gnome-keyring. xdg.portal = { enable = true; xdgOpenUsePortal = true; extraPortals = [ pkgs.xdg-desktop-portal-gtk pkgs.xdg-desktop-portal-gnome ]; # Use xdg-desktop-portal-gtk by default, but fallback to # xdg-desktop-portal-gnome for screenshots and screen-casts, two important # features unsupported by the GTK implementation. See implementations and # their supported interfaces at # https://wiki.archlinux.org/title/XDG_Desktop_Portal. config.niri = { default = [ "gtk" ]; "org.freedesktop.impl.portal.Screenshot" = [ "gnome" ]; "org.freedesktop.impl.portal.ScreenCast" = [ "gnome" ]; }; }; systemd.user.services.xwayland-satellite = lib.mkIf cfg.xwayland-satellite.enable { Service.ExecStart = ["${pkgs.xwayland-satellite}/bin/xwayland-satellite"]; Unit.After = ["niri.service"]; Unit.PartOf = ["niri.service"]; Install.WantedBy = ["niri.service"]; }; programs.niri = { settings = { # Activates some Nixpkgs patches to help run Electron applications under # Wayland. environment."NIXOS_OZONE_WL" = "1"; environment."DISPLAY" = ":0"; spawn-at-startup = [{ command = [ "${pkgs.swaynotificationcenter}/bin/swaync" ]; } { command = [ "${pkgs.gammastep}/bin/gammastep" "-o" "-O" "4000k" "-b" "0.9:0.9" "-l" "0:0"]; }] ++ lib.optional (! builtins.isNull cfg.wallpaper) { command = [ "${pkgs.wbg}/bin/wbg" "${cfg.wallpaper}" ]; }; input = { keyboard.xkb = { layout = "us"; options = "ctrl:swapcaps,compose:menu"; }; mouse = { accel-speed = -0.5; accel-profile = "flat"; }; focus-follows-mouse.enable = true; }; outputs."DP-1" = { mode.width = 2560; mode.height = 1440; mode.refresh = 143.912; scale = 1; }; layout = { background-color = "transparent"; gaps = 8; # center-focused-column = "always"; center-focused-column = "never"; preset-column-widths = [ { proportion = 1.0 / 3.0; } { proportion = 1.0 / 2.0; } { proportion = 2.0 / 3.0; } ]; default-column-width.proportion = 0.5; # default-column-width.proportion = {}; focus-ring = { enable = false; width = 2; }; border = { enable = true; width = 2; # Override Niri's Stylix integration. Default uses Crystal Blue; we # override it with Old White. active.color = config.lib.stylix.colors.withHashtag.base06; }; shadow = { enable = true; softness = 30; spread = 5; offset.x = 5; offset.y = 5; }; tab-indicator = { place-within-column = true; position = "top"; # active.color = config.lib.stylix.colors.withHashtag.blue; inactive.color = config.lib.stylix.colors.withHashtag.base01; width = 8; corner-radius = 2.0; gaps-between-tabs = 10; }; }; prefer-no-csd = true; screenshot-path = "~/Pictures/Screenshots/%Y-%m-%d %H-%M-%S.png"; cursor = { theme = "default"; size = 24; }; window-rules = [ { # Set red borders to indicate that a window is being captured. matches = [ { is-window-cast-target = true; } ]; focus-ring = { enable = true; width = 2; active.color = "#f38ba8"; inactive.color = "#7d0d2d"; }; border = { width = 2; inactive.color = "#7d0d2d"; active.color = "#7d0d2d"; }; shadow = { color = "#7d0d2d70"; softness = 30; spread = 10; offset.x = 0; offset.y = 0; }; tab-indicator = { active.color = "#f38ba8"; inactive.color = "#7d0d2d"; }; } { # Block private DMs from screencast... matches = [ { title = "Element.*\\|"; } { title = "Discord.*@"; } ]; excludes = [ # ... except when explicitly targeting the window. { is-window-cast-target = true; } ]; block-out-from = "screen-capture"; } { # This regular expression is intentionally made as specific as # possible, since this is the default config, and we want no false # positives. You can get away with just app-id="wezterm" if you # want. matches = [ { app-id = "^org\\.wezfurlong\\.wezterm$"; } ]; default-column-width = {}; } { # Floating windows. matches = [ # Open the Firefox picture-in-picture player as floating by default. { app-id = "firefox$"; title = "^Picture-in-Picture$"; } # Gimp pop-ups. { app-id = "^Gimp$"; title = "^(New Layer|Change Foreground Color|Save Image)$"; } ]; open-floating = true; } { # Start with low width. matches = [ { app-id = "com\\.mitchellh\\.ghostty"; } { app-id = "^emacs$"; } ]; default-column-width.proportion = 1.0 / 3.0; } { # Enable rounded corners for all windows. geometry-corner-radius = let r = 8.0; in { bottom-left = r; top-left = r; bottom-right = r; top-right = r; }; clip-to-geometry = true; } ]; binds = { "Mod+Q".action.close-window = []; "Mod+Shift+Q".action.quit = []; "Mod+Ctrl+L".action.spawn = ["${pkgs.swaylock}/bin/swaylock"]; "Mod+D".action.spawn = ["${pkgs.fuzzel}/bin/fuzzel"]; "Mod+Shift+Slash".action.show-hotkey-overlay = []; # Powers off the monitors. They will be powered back on upon any # keyboard/mouse input. "Mod+Shift+P".action.power-off-monitors = []; "Mod+I".action.set-dynamic-cast-window = []; "Mod+Shift+I".action.set-dynamic-cast-monitor = []; "Mod+Ctrl+I".action.clear-dynamic-cast-target = []; "Mod+BracketLeft".action.consume-or-expel-window-left = []; "Mod+BracketRight".action.consume-or-expel-window-right = []; "Mod+Comma".action.consume-window-into-column = []; "Mod+Period".action.expel-window-from-column = []; "Mod+Minus".action.set-column-width = ["-10%"]; "Mod+Equal".action.set-column-width = ["+10%"]; "Mod+Shift+Minus".action.set-window-height = ["-10%"]; "Mod+Shift+Equal".action.set-window-height = ["+10%"]; "Mod+C".action.center-column = []; "Mod+M".action.maximize-column = []; "Mod+Shift+M".action.fullscreen-window = []; "Mod+Ctrl+M".action.toggle-windowed-fullscreen = []; "Mod+Shift+S".action.screenshot = []; "Print".action.screenshot-screen = []; "Mod+Print".action.screenshot-window = []; "Mod+X".action.spawn = [ "emacsclient-or-emacs" "-ce" "(eshell t)" ]; "Mod+Shift+F".action.toggle-window-floating = []; "Mod+F".action.switch-focus-between-floating-and-tiling = []; "Mod+Shift+T".action.toggle-column-tabbed-display = []; "Alt+Tab".action.focus-window-previous = []; "Mod+H".action.focus-column-left = []; "Mod+J".action.focus-window-down = []; "Mod+K".action.focus-window-up = []; "Mod+L".action.focus-column-right = []; "Mod+Shift+H".action.move-column-left = []; "Mod+Shift+J".action.move-window-down = []; "Mod+Shift+K".action.move-window-up = []; "Mod+Shift+L".action.move-column-right = []; "XF86MonBrightnessDown".action.spawn = [(lib.getExe pkgs.brightnessctl) "s" "5%-"]; "XF86MonBrightnessUp".action.spawn = [(lib.getExe pkgs.brightnessctl) "s" "+5%"]; "Mod+1".action.focus-workspace = [1]; "Mod+2".action.focus-workspace = [2]; "Mod+3".action.focus-workspace = [3]; "Mod+4".action.focus-workspace = [4]; "Mod+5".action.focus-workspace = [5]; "Mod+6".action.focus-workspace = [6]; "Mod+7".action.focus-workspace = [7]; "Mod+8".action.focus-workspace = [8]; "Mod+9".action.focus-workspace = [9]; "Mod+Shift+1".action.move-column-to-workspace = [1]; "Mod+Shift+2".action.move-column-to-workspace = [2]; "Mod+Shift+3".action.move-column-to-workspace = [3]; "Mod+Shift+4".action.move-column-to-workspace = [4]; "Mod+Shift+5".action.move-column-to-workspace = [5]; "Mod+Shift+6".action.move-column-to-workspace = [6]; "Mod+Shift+7".action.move-column-to-workspace = [7]; "Mod+Shift+8".action.move-column-to-workspace = [8]; "Mod+Shift+9".action.move-column-to-workspace = [9]; "Mod+Shift+R".action.switch-preset-window-height = []; "Mod+Ctrl+R".action.reset-window-height = []; "XF86AudioRaiseVolume".action.spawn = ["wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1+"]; "XF86AudioLowerVolume".action.spawn = ["wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.1-"]; }; layer-rules = [ # Don't move wallpaper with workspace. { matches = [{ namespace = "^wallpaper$"; }]; place-within-backdrop = true; } ]; debug = { wait-for-frame-completion-in-pipewire = {}; }; }; }; }; }