{ inputs, lib, config, system, pkgs, ... }: with lib; let cfg = config.modules.desktop.niri; in { options.modules.desktop.niri = { enable = mkEnableOption "Enable niri, a scrolling wayland compositor"; package = mkOption { type = types.package; default = pkgs.niri-unstable; example = "pkgs.niri"; }; }; config = mkIf cfg.enable { services.displayManager.sessionPackages = [ cfg.package ]; programs.niri = { enable = true; package = cfg.package; }; systemd.user.services.niri-flake-polkit.enable = false; # niri-flake has its own polkit agent, disable it hm.programs.niri = { package = cfg.package; settings = let allCorners = r: { bottom-left = r; bottom-right = r; top-left = r; top-right = r; }; in { input = { power-key-handling.enable = false; focus-follows-mouse.enable = true; focus-follows-mouse.max-scroll-amount = "0%"; # TODO: uhm?? why do i have to do this here. should be automatic from libinput # whatever. ill just hardcode for now touchpad = { click-method = "button-areas"; tap = true; natural-scroll = true; }; mouse = { accel-profile = "flat"; }; }; clipboard = { disable-primary = true; }; cursor = { theme = config.modules.desktop.themes.cursorTheme.name; size = config.modules.desktop.themes.cursorTheme.size; }; environment = { DISPLAY = ":0"; }; prefer-no-csd = true; layout = { gaps = 6; center-focused-column = "on-overflow"; focus-ring = { enable = false; width = 1; active.color = config.modules.desktop.themes.niri.accent; inactive.color = config.modules.desktop.themes.niri.inactive; }; border = { enable = true; width = 1; active.color = config.modules.desktop.themes.niri.accent; inactive.color = config.modules.desktop.themes.niri.inactive; }; shadow = { enable = true; # TODO: remove this? # this is a way to make the shadow appear on rounded corners # see: https://github.com/YaLTeR/niri/blob/e251ca7340bc71870c3a81a7ffc3d9bde58e685a/resources/default-config.kdl#L201 draw-behind-window = true; offset.x = 0; offset.y = 0; softness = 30; spread = 2; color = config.modules.desktop.themes.niri.shadow; }; }; hotkey-overlay.skip-at-startup = true; screenshot-path = null; animations = { shaders.window-resize = '' vec4 resize_color(vec3 coords_curr_geo, vec3 size_curr_geo) { vec3 coords_next_geo = niri_curr_geo_to_next_geo * coords_curr_geo; vec3 coords_stretch = niri_geo_to_tex_next * coords_curr_geo; vec3 coords_crop = niri_geo_to_tex_next * coords_next_geo; // We can crop if the current window size is smaller than the next window // size. One way to tell is by comparing to 1.0 the X and Y scaling // coefficients in the current-to-next transformation matrix. bool can_crop_by_x = niri_curr_geo_to_next_geo[0][0] <= 1.0; bool can_crop_by_y = niri_curr_geo_to_next_geo[1][1] <= 1.0; vec3 coords = coords_stretch; if (can_crop_by_x) coords.x = coords_crop.x; if (can_crop_by_y) coords.y = coords_crop.y; vec4 color = texture2D(niri_tex_next, coords.st); // However, when we crop, we also want to crop out anything outside the // current geometry. This is because the area of the shader is unspecified // and usually bigger than the current geometry, so if we don't fill pixels // outside with transparency, the texture will leak out. // // When stretching, this is not an issue because the area outside will // correspond to client-side decoration shadows, which are already supposed // to be outside. if (can_crop_by_x && (coords_curr_geo.x < 0.0 || 1.0 < coords_curr_geo.x)) color = vec4(0.0); if (can_crop_by_y && (coords_curr_geo.y < 0.0 || 1.0 < coords_curr_geo.y)) color = vec4(0.0); return color; } ''; window-close = { easing = { curve = "linear"; duration-ms = 600; }; }; shaders.window-close = '' vec4 fall_and_rotate(vec3 coords_geo, vec3 size_geo) { float progress = niri_clamped_progress * niri_clamped_progress; vec2 coords = (coords_geo.xy - vec2(0.5, 1.0)) * size_geo.xy; coords.y -= progress * 1440.0; float random = (niri_random_seed - 0.5) / 2.0; random = sign(random) - random; float max_angle = 0.5 * random; float angle = progress * max_angle; mat2 rotate = mat2(cos(angle), -sin(angle), sin(angle), cos(angle)); coords = rotate * coords; coords_geo = vec3(coords / size_geo.xy + vec2(0.5, 1.0), 1.0); vec3 coords_tex = niri_geo_to_tex * coords_geo; vec4 color = texture2D(niri_tex, coords_tex.st); return color; } vec4 close_color(vec3 coords_geo, vec3 size_geo) { return fall_and_rotate(coords_geo, size_geo); } ''; }; window-rules = [ { geometry-corner-radius = allCorners 10.0; clip-to-geometry = true; } { matches = [ { app-id = "^org\.wezfurlong\.wezterm$"; } ]; # see earlier shadow config; this is here because it's a transparent window, special case shadow.draw-behind-window = false; } { matches = [ { app-id = "^gcr-prompter$"; } { app-id = "^.*pinentry-.*$"; } { app-id = "^polkit-.*$"; } { app-id = "^org.gnome.seahorse.Application$"; } ]; block-out-from = "screen-capture"; } { matches = [ { app-id = "^gcr-prompter$"; } { app-id = "^.*pinentry-.*$"; } { app-id = "^polkit-.*$"; } { app-id = "^org\.gnome\.Loupe$"; } { title = "^Open Folder$"; } { title = "^Open Files$"; } { title = "^Open File$"; } { title = "^Open$"; } { title = "^Save$"; } { title = "^Save As$"; } ]; open-floating = true; focus-ring = { enable = true; width = 4000; active.color = "${config.modules.desktop.themes.niri.shadow}65"; inactive.color = "${config.modules.desktop.themes.niri.shadow}65"; }; } { matches = [ { app-id = "firefox$"; title = "^Picture-in-Picture$"; } { title = "^Picture in picture$"; } { title = "^Discord Popout$"; } ]; open-floating = true; default-floating-position = { x = 15; y = 15; relative-to = "top-right"; }; } { matches = [ { app-id = "^gcr-prompter$"; } { app-id = "^.*pinentry-.*$"; } { app-id = "^polkit-.*$"; } { app-id = "^file-roller$"; } { app-id = "^org\.gnome\.FileRoller$"; } { app-id = "^org\.gnome\.Loupe$"; } { title = "^Open Folder$"; } { title = "^Open Files$"; } { title = "^Open File$"; } { title = "^Open$"; } { title = "^Save$"; } { title = "^Save As$"; } ]; open-floating = true; default-column-width.proportion = 0.6; default-window-height.proportion = 0.8; } ]; layer-rules = [ { matches = [ { namespace = "^notifications$"; } { namespace = "^rofi$"; } # a bit silly, but we use rofi for clipboard history ]; block-out-from = "screencast"; } { matches = [ { namespace = "^launcher$"; } { namespace = "^notifications$"; } { namespace = "^rofi$"; } { namespace = "^waybar$"; } { namespace = "^wob$"; } ]; shadow = { enable = true; }; } { matches = [ { namespace = "^launcher$"; } { namespace = "^wob$"; } ]; # see earlier shadow config; this is here because it's a transparent window, special case shadow.draw-behind-window = false; } ]; binds = with config.hm.lib.niri.actions; let sh = spawn "sh" "-c"; in { "Mod+Shift+Slash".action = show-hotkey-overlay; "Mod+D".action = spawn "fuzzel"; "Mod+Q".action = close-window; "Mod+H".action = toggle-window-floating; "Mod+Shift+H".action = switch-focus-between-floating-and-tiling; "Mod+Left".action = focus-column-left; "Mod+Down".action = focus-window-down; "Mod+Up".action = focus-window-up; "Mod+Right".action = focus-column-right; "Mod+Shift+Left".action = move-column-left; "Mod+Shift+Down".action = move-window-down; "Mod+Shift+Up".action = move-window-up; "Mod+Shift+Right".action = move-column-right; "Mod+Ctrl+Left".action = focus-monitor-left; "Mod+Ctrl+Down".action = focus-monitor-down; "Mod+Ctrl+Up".action = focus-monitor-up; "Mod+Ctrl+Right".action = focus-monitor-right; "Mod+Home".action = focus-column-first; "Mod+End".action = focus-column-last; "Mod+Shift+Home".action = move-column-to-first; "Mod+Shift+End".action = move-column-to-last; "Mod+Page_Down".action = focus-workspace-down; "Mod+Page_Up".action = focus-workspace-up; "Mod+Shift+Page_Down".action = move-column-to-workspace-down; "Mod+Shift+Page_Up".action = move-column-to-workspace-up; "Mod+Ctrl+Page_Down".action = move-workspace-down; "Mod+Ctrl+Page_Up".action = move-workspace-up; "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+0".action = focus-workspace 10; "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+0".action = move-column-to-workspace 10; "Mod+Comma".action = consume-window-into-column; "Mod+Period".action = expel-window-from-column; "Mod+BracketLeft".action = consume-or-expel-window-left; "Mod+BracketRight".action = consume-or-expel-window-right; "Mod+R".action = switch-preset-column-width; "Mod+Shift+R".action = switch-preset-window-height; "Mod+Ctrl+R".action = reset-window-height; "Mod+F".action = maximize-column; "Mod+Shift+F".action = fullscreen-window; "Mod+C".action = center-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%"; "Print".action = screenshot; "Mod+Shift+E".action = quit; "XF86AudioMicMute".action = spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; "XF86AudioMicMute".allow-when-locked = true; "XF86LaunchA".action = screenshot; "XF86LaunchB".action = sh "${lib.getExe pkgs.rofi-rbw-wayland} -a copy -t password --clear-after 20"; "XF86ScreenSaver".action = sh "${pkgs.systemd}/bin/loginctl lock-session"; # substitutions for when not on laptop "Mod+Shift+S".action = screenshot; "Mod+Shift+P".action = sh "${lib.getExe pkgs.rofi-rbw-wayland} -a copy -t password --clear-after 20"; "Mod+L".action = sh "${pkgs.systemd}/bin/loginctl lock-session"; "Mod+T".action = spawn "wezterm"; "Mod+E".action = spawn "nautilus"; "XF86AudioPrev".action = sh "${lib.getExe pkgs.playerctl} previous"; "XF86AudioPlay".action = sh "${lib.getExe pkgs.playerctl} play-pause"; "XF86AudioNext".action = sh "${lib.getExe pkgs.playerctl} next"; "Mod+V".action = sh "${config.modules.desktop.cliphist.summonScript}"; "Mod+Shift+Control+T".action = toggle-debug-tint; "Mod+Shift+Control+O".action = debug-toggle-opaque-regions; "Mod+Shift+Control+D".action = debug-toggle-damage; } // (if config.modules.desktop.wob.enable then let wobSock = config.modules.desktop.wob.sockPath; in { "XF86AudioRaiseVolume".action = sh "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+ && wpctl get-volume @DEFAULT_AUDIO_SINK@ | sed 's/[^0-9]//g' > ${wobSock}"; "XF86AudioRaiseVolume".allow-when-locked = true; "XF86AudioLowerVolume".action = sh "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%- && wpctl get-volume @DEFAULT_AUDIO_SINK@ | sed 's/[^0-9]//g' > ${wobSock}"; "XF86AudioLowerVolume".allow-when-locked = true; "XF86AudioMute".action = sh "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle && (wpctl get-volume @DEFAULT_AUDIO_SINK@ | grep -q MUTED && echo 0 > ${wobSock}) || wpctl get-volume @DEFAULT_AUDIO_SINK@ | sed 's/[^0-9]//g' > ${wobSock}"; "XF86AudioMute".allow-when-locked = true; "XF86MonBrightnessUp".action = sh "${lib.getExe pkgs.brightnessctl} -c backlight s +5% | sed -n 's/.*(\\([0-9]*\\)%).*/\\1/p' > ${wobSock}"; "XF86MonBrightnessUp".allow-when-locked = true; "XF86MonBrightnessDown".action = sh "${lib.getExe pkgs.brightnessctl} -c backlight s 5%- | sed -n 's/.*(\\([0-9]*\\)%).*/\\1/p' > ${wobSock}"; "XF86MonBrightnessDown".allow-when-locked = true; "XF86KbdBrightnessUp".action = sh "${lib.getExe pkgs.brightnessctl} -d '*:kbd_backlight' s +5% | sed -n 's/.*(\\([0-9]*\\)%).*/\\1/p' > ${wobSock}"; "XF86KbdBrightnessUp".allow-when-locked = true; "XF86KbdBrightnessDown".action = sh "${lib.getExe pkgs.brightnessctl} -d '*:kbd_backlight' s 5%- | sed -n 's/.*(\\([0-9]*\\)%).*/\\1/p' > ${wobSock}"; "XF86KbdBrightnessDown".allow-when-locked = true; } else { "XF86AudioRaiseVolume".action = sh "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+"; "XF86AudioRaiseVolume".allow-when-locked = true; "XF86AudioLowerVolume".action = sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"; "XF86AudioLowerVolume".allow-when-locked = true; "XF86AudioMute".action = sh "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; "XF86AudioMute".allow-when-locked = true; "XF86MonBrightnessUp".action = sh "${lib.getExe pkgs.brightnessctl} -c backlight s +5%"; "XF86MonBrightnessUp".allow-when-locked = true; "XF86MonBrightnessDown".action = sh "${lib.getExe pkgs.brightnessctl} -c backlight s 5%-"; "XF86MonBrightnessDown".allow-when-locked = true; "XF86KbdBrightnessUp".action = sh "${lib.getExe pkgs.brightnessctl} -d '*:kbd_backlight' s +5%"; "XF86KbdBrightnessUp".allow-when-locked = true; "XF86KbdBrightnessDown".action = sh "${lib.getExe pkgs.brightnessctl} -d '*:kbd_backlight' s 5%-"; "XF86KbdBrightnessDown".allow-when-locked = true; }); }; }; }; }