{ config, lib, pkgs, ... }: # The Hyprland compositor. let cfg = config.modules.desktop.session.hyprland; # NOLINT(let_order) cursor = config.stylix.cursor; user = config.user.name; # Flip the tiling strategy between the two built-in layouts, since neither a # dispatcher nor a keyword toggles it on its own. toggleLayout = pkgs.writeShellScript "hypr-toggle-layout" '' if [ "$(hyprctl getoption -j general:layout | ${pkgs.jq}/bin/jq -r .str)" = dwindle ]; then hyprctl keyword general:layout master else hyprctl keyword general:layout dwindle fi ''; in { options.modules.desktop.session.hyprland = { enable = lib.mkEnableOption "the Hyprland compositor"; blur = lib.mkEnableOption '' window blur. Off by default as the single biggest battery cost on a laptop, left on for a host with the headroom to spend it''; }; config = lib.mkIf cfg.enable { # This program integration owns the session, portals, and polkit, launched # through the universal Wayland session manager. programs.hyprland = { enable = true; withUWSM = true; }; home-manager.users.${user}.wayland.windowManager.hyprland = { enable = true; # One package drives the whole session, so there is never a version split. # The program integration above installs it and the portal, leaving home- # manager to write only the config. package = null; portalPackage = null; # uwsm owns the systemd graphical-session targets. systemd.enable = false; # Write the native Lua Hyprland config. Home Manager renders the Nix # settings below as hl.* calls. configType = "lua"; importantPrefixes = [ "config" "curve" "animation" ]; settings = { # Session variables handed to the compositor directly. # UWSM launches the session without the shell profile that would carry # them, so a variable the compositor or its children must see is set # here rather than through home-manager's sessionVariables. env = [ # Chromium and Electron apps read this to select native Wayland; # nixpkgs wrappers (Obsidian's included) gate their Wayland flags on # it, so without it they run under XWayland and blur at this DPI. { _args = [ "NIXOS_OZONE_WL" "1" ]; } ] # Bibata ships XCursor only. # The hyprcursor variables name the same theme, which Hyprland # resolves through its XCursor fallback. ++ lib.optionals (cursor != null) [ { _args = [ "XCURSOR_THEME" cursor.name ]; } { _args = [ "XCURSOR_SIZE" (toString cursor.size) ]; } { _args = [ "HYPRCURSOR_THEME" cursor.name ]; } { _args = [ "HYPRCURSOR_SIZE" (toString cursor.size) ]; } ]; config = { input = { kb_layout = "us"; # Caps is a second Escape. # Shift+Caps still toggles a real CapsLock. kb_options = "caps:escape_shifted_capslock"; # Snappy key repeat. repeat_delay = 250; repeat_rate = 45; accel_profile = "flat"; touchpad = { natural_scroll = true; tap_to_click = true; disable_while_typing = true; }; }; general = { gaps_in = 4; gaps_out = 8; border_size = 2; layout = "dwindle"; }; decoration = { rounding = 6; blur.enabled = cfg.blur; }; # XWayland clients render at the panel's native resolution instead of # being raster-scaled by the compositor at the fractional monitor scale. xwayland.force_zero_scaling = true; animations.enabled = true; dwindle = { preserve_split = true; }; }; curve = { _args = [ "ease" { type = "bezier"; points = [ [ 0.25 0.1 ] [ 0.25 1.0 ] ]; } ]; }; # Durations are in centiseconds. # Short values keep the motion subtle. animation = [ { leaf = "windows"; enabled = true; speed = 3; bezier = "ease"; } { leaf = "fade"; enabled = true; speed = 3; bezier = "ease"; } # Layer surfaces like the launcher fade in a touch quicker than windows. { leaf = "layersIn"; enabled = true; speed = 2; bezier = "ease"; } { leaf = "fadeLayersIn"; enabled = true; speed = 2; bezier = "ease"; } { leaf = "workspaces"; enabled = true; speed = 3; bezier = "ease"; } { leaf = "border"; enabled = true; speed = 3; bezier = "ease"; } ]; bind = [ { _args = [ "SUPER + Return" (lib.generators.mkLuaInline "hl.dsp.exec_cmd(${(lib.generators.toLua { }) "ghostty"})") ]; } # Move focus. { _args = [ "SUPER + H" (lib.generators.mkLuaInline "hl.dsp.focus(${(lib.generators.toLua { }) { direction = "left"; }})") ]; } { _args = [ "SUPER + J" (lib.generators.mkLuaInline "hl.dsp.focus(${(lib.generators.toLua { }) { direction = "down"; }})") ]; } { _args = [ "SUPER + K" (lib.generators.mkLuaInline "hl.dsp.focus(${(lib.generators.toLua { }) { direction = "up"; }})") ]; } { _args = [ "SUPER + L" (lib.generators.mkLuaInline "hl.dsp.focus(${(lib.generators.toLua { }) { direction = "right"; }})") ]; } # Move the window within the layout. { _args = [ "SUPER + SHIFT + H" (lib.generators.mkLuaInline "hl.dsp.window.move(${ (lib.generators.toLua { }) { direction = "left"; } })") ]; } { _args = [ "SUPER + SHIFT + J" (lib.generators.mkLuaInline "hl.dsp.window.move(${ (lib.generators.toLua { }) { direction = "down"; } })") ]; } { _args = [ "SUPER + SHIFT + K" (lib.generators.mkLuaInline "hl.dsp.window.move(${ (lib.generators.toLua { }) { direction = "up"; } })") ]; } { _args = [ "SUPER + SHIFT + L" (lib.generators.mkLuaInline "hl.dsp.window.move(${ (lib.generators.toLua { }) { direction = "right"; } })") ]; } # Resize the active window. { _args = [ "SUPER + ALT + H" (lib.generators.mkLuaInline "hl.dsp.window.resize(${ (lib.generators.toLua { }) { x = -40; y = 0; relative = true; } })") ]; } { _args = [ "SUPER + ALT + J" (lib.generators.mkLuaInline "hl.dsp.window.resize(${ (lib.generators.toLua { }) { x = 0; y = 40; relative = true; } })") ]; } { _args = [ "SUPER + ALT + K" (lib.generators.mkLuaInline "hl.dsp.window.resize(${ (lib.generators.toLua { }) { x = 0; y = -40; relative = true; } })") ]; } { _args = [ "SUPER + ALT + L" (lib.generators.mkLuaInline "hl.dsp.window.resize(${ (lib.generators.toLua { }) { x = 40; y = 0; relative = true; } })") ]; } { _args = [ "SUPER + Space" (lib.generators.mkLuaInline "hl.dsp.window.float(${ (lib.generators.toLua { }) { action = "toggle"; } })") ]; } { _args = [ "SUPER + F" (lib.generators.mkLuaInline "hl.dsp.window.fullscreen()") ]; } # togglesplit is a dwindle layout message, reached through layout. { _args = [ "SUPER + T" (lib.generators.mkLuaInline "hl.dsp.layout(${(lib.generators.toLua { }) "togglesplit"})") ]; } { _args = [ "SUPER + SHIFT + T" (lib.generators.mkLuaInline "hl.dsp.exec_cmd(${(lib.generators.toLua { }) "${toggleLayout}"})") ]; } { _args = [ "SUPER + SHIFT + Q" (lib.generators.mkLuaInline "hl.dsp.window.close()") ]; } { _args = [ "SUPER + CTRL + Q" (lib.generators.mkLuaInline "hl.dsp.window.kill()") ]; } # Volume and brightness keys repeat while held, each raising a popup # through the OSD client. # Volume is capped at 100 percent. # The client floors brightness so a full hold cannot black the screen out. { _args = [ "XF86AudioRaiseVolume" (lib.generators.mkLuaInline "hl.dsp.exec_cmd(${ (lib.generators.toLua { }) "${pkgs.swayosd}/bin/swayosd-client --output-volume raise --max-volume 100" })") { repeating = true; } ]; } { _args = [ "XF86AudioLowerVolume" (lib.generators.mkLuaInline "hl.dsp.exec_cmd(${ (lib.generators.toLua { }) "${pkgs.swayosd}/bin/swayosd-client --output-volume lower" })") { repeating = true; } ]; } { _args = [ "XF86MonBrightnessUp" (lib.generators.mkLuaInline "hl.dsp.exec_cmd(${ (lib.generators.toLua { }) "${pkgs.swayosd}/bin/swayosd-client --brightness raise" })") { repeating = true; } ]; } { _args = [ "XF86MonBrightnessDown" (lib.generators.mkLuaInline "hl.dsp.exec_cmd(${ (lib.generators.toLua { }) "${pkgs.swayosd}/bin/swayosd-client --brightness lower" })") { repeating = true; } ]; } # Mute and media transport still fire while the session is locked. { _args = [ "XF86AudioMute" (lib.generators.mkLuaInline "hl.dsp.exec_cmd(${ (lib.generators.toLua { }) "${pkgs.swayosd}/bin/swayosd-client --output-volume mute-toggle" })") { locked = true; } ]; } { _args = [ "XF86AudioPlay" (lib.generators.mkLuaInline "hl.dsp.exec_cmd(${ (lib.generators.toLua { }) "${pkgs.playerctl}/bin/playerctl play-pause" })") { locked = true; } ]; } { _args = [ "XF86AudioNext" (lib.generators.mkLuaInline "hl.dsp.exec_cmd(${ (lib.generators.toLua { }) "${pkgs.playerctl}/bin/playerctl next" })") { locked = true; } ]; } { _args = [ "XF86AudioPrev" (lib.generators.mkLuaInline "hl.dsp.exec_cmd(${ (lib.generators.toLua { }) "${pkgs.playerctl}/bin/playerctl previous" })") { locked = true; } ]; } ] ++ lib.concatMap (n: [ { _args = [ "SUPER + ${toString n}" (lib.generators.mkLuaInline "hl.dsp.focus(${(lib.generators.toLua { }) { workspace = n; }})") ]; } { _args = [ "SUPER + SHIFT + ${toString n}" (lib.generators.mkLuaInline "hl.dsp.window.move(${(lib.generators.toLua { }) { workspace = n; }})") ]; } ]) (lib.range 1 9); }; }; }; }