# settings/hyprland.nix { config, pkgs, lib, inputs, ... }: let isMacbook = config.networking.hostName == "FredOS-Macbook"; isGaming = !isMacbook; in { config = lib.mkIf (lib.elem config.networking.hostName [ "FredOS-Gaming" "FredOS-Macbook" ]) { programs.hyprland = { enable = true; xwayland.enable = true; }; xdg.portal = { enable = true; # xdg-desktop-portal-hyprland is registered automatically by # programs.hyprland.portalPackage; listing it here too produced a # duplicate user-unit symlink during nixos-rebuild. extraPortals = with pkgs; [ xdg-desktop-portal-gtk ]; config.hyprland.default = [ "hyprland" "gtk" ]; }; security.polkit.enable = true; # Polkit GUI agent for GUI sudo prompts under Hyprland systemd.user.services.polkit-gnome-authentication-agent-1 = { description = "polkit-gnome-authentication-agent-1"; wantedBy = [ "graphical-session.target" ]; partOf = [ "graphical-session.target" ]; after = [ "graphical-session.target" ]; serviceConfig = { Type = "simple"; ExecStart = "${pkgs.polkit_gnome}/libexec/polkit-gnome-authentication-agent-1"; Restart = "on-failure"; }; }; environment.systemPackages = with pkgs; [ ghostty libnotify grim slurp wl-clipboard cliphist brightnessctl swayosd playerctl hyprpaper hyprlock hypridle hyprshot networkmanagerapplet pavucontrol polkit_gnome zenity libcanberra-gtk3 ]; home-manager.users.fred = { config, lib, pkgs, inputs, ... }: let c = config.lib.stylix.colors; rgb = hex: "rgb(${hex})"; rgba = hex: a: "rgba(${hex}${a})"; in { # Stylix's Hyprland target injects settings.{general,decoration,group,misc} # as top-level keys, which render as hl.general()/hl.decoration()/… in Lua # mode — functions that don't exist. Disable it and absorb the colours # into settings.config below. stylix.targets.hyprland.enable = false; # The disabled Hyprland target would normally enable this; do it # manually. Stylix's hyprpaper target (auto-enabled) still handles # preload/wallpaper settings. services.hyprpaper.enable = true; wayland.windowManager.hyprland = { enable = true; configType = "lua"; systemd.variables = [ "--all" ]; settings = { # hl.config({...}) — all static named-section configuration. # monitor is set per-host in hosts/FredOS-{Gaming,Macbook}.nix. config = { general = { gaps_in = 6; # 6px of the outer gap is occupied by the quickshell screen # frame (Theme.frameWidth) — 18 on framed sides keeps the # visible frame↔window gap equal to the inter-window gap (12). # Top stays 12: the bar reserves its own space. gaps_out = { top = 12; right = 18; bottom = 18; left = 18; }; border_size = 2; layout = "dwindle"; resize_on_border = true; "col.active_border" = rgb c.base0D; "col.inactive_border" = rgb c.base03; }; decoration = { rounding = 8; blur = { enabled = true; size = 1; passes = 3; }; shadow.color = rgba c.base00 "99"; }; group = { "col.border_active" = rgb c.base0D; "col.border_inactive" = rgb c.base03; "col.border_locked_active" = rgb c.base0C; groupbar = { text_color = rgb c.base05; "col.active" = rgb c.base0D; "col.inactive" = rgb c.base03; }; }; render = { direct_scanout = false; }; animations = { enabled = true; }; input = { kb_layout = "gb,no"; kb_options = "grp:alt_shift_toggle"; follow_mouse = 1; accel_profile = "flat"; sensitivity = 0; } // lib.optionalAttrs isMacbook { touchpad = { tap_to_click = true; tap_button_map = "lrm"; natural_scroll = true; }; }; cursor = { no_warps = true; }; dwindle = { preserve_split = true; }; misc = { disable_hyprland_logo = true; disable_splash_rendering = true; # Apps demanding attention don't get to yank focus — they'll # show as urgent in the bar instead. focus_on_activate = false; vrr = 2; background_color = rgb c.base00; }; # vfr moved from misc: to debug: in 0.55.0 debug = { vfr = false; # keep compositor ticking, don't idle between frames disable_logs = false; }; }; }; extraConfig = let kbdBrightUp = pkgs.writeShellScript "kbd-bright-up" '' ${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight set +10% brightness=$(${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight get) max=$(${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight max) echo $(( brightness * 100 / max )) > "$XDG_RUNTIME_DIR/wob.fifo" ''; kbdBrightDown = pkgs.writeShellScript "kbd-bright-down" '' ${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight set 10%- brightness=$(${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight get) max=$(${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight max) echo $(( brightness * 100 / max )) > "$XDG_RUNTIME_DIR/wob.fifo" ''; in '' -- Environment hl.env("XCURSOR_THEME", "Bibata-Modern-Ice") hl.env("XCURSOR_SIZE", "24") hl.env("HYPRCURSOR_THEME", "Bibata-Modern-Ice") hl.env("HYPRCURSOR_SIZE", "24") hl.env("ELECTRON_OZONE_PLATFORM_HINT", "wayland") hl.env("MOZ_ENABLE_WAYLAND", "1") hl.env("QT_QPA_PLATFORM", "wayland;xcb") hl.env("SDL_VIDEODRIVER", "wayland") hl.env("_JAVA_AWT_WM_NONREPARENTING", "1") ${lib.optionalString isGaming '' -- GPU pinning — Navi 22 is card1 on the dual-GPU gaming box. hl.env("AQ_DRM_DEVICES", "/dev/dri/card1") hl.env("DRI_PRIME", "pci-0000_03_00_0") ''} -- Layer rules: blur behind bar and toasts hl.layer_rule({ match = { namespace = "quickshell-bar" }, blur = true, ignore_alpha = 0.3 }) -- Startup hl.on("hyprland.start", function() -- Ensure hyprland-session.target starts even if HM's -- dbus-update-activation-environment chain fails upstream. hl.exec_cmd("systemctl --user start hyprland-session.target") -- mako removed; notifications handled by quickshell hl.exec_cmd("wl-paste --type text --watch cliphist store") hl.exec_cmd("wl-paste --type image --watch cliphist store") hl.exec_cmd("hyprctl setcursor Bibata-Modern-Ice 24") hl.exec_cmd("swayosd-server") ${lib.optionalString isMacbook ''hl.exec_cmd("hypridle")''} end) -- Animation curve and definitions hl.curve("snap", { type = "bezier", points = { {0.05, 0.9}, {0.1, 1.0} } }) hl.animation({ leaf = "windows", enabled = true, speed = 1, bezier = "snap" }) hl.animation({ leaf = "windowsOut", enabled = true, speed = 1, bezier = "snap", style = "popin 80%" }) hl.animation({ leaf = "layers", enabled = true, speed = 1, bezier = "snap" }) hl.animation({ leaf = "border", enabled = true, speed = 2, bezier = "default" }) hl.animation({ leaf = "fade", enabled = true, speed = 1, bezier = "default" }) hl.animation({ leaf = "workspaces", enabled = true, speed = 1, bezier = "snap" }) -- Window rules -- Battle.net tray icon leaks as a tiny floating XWayland window. hl.window_rule({ match = { class = "steam_app_0", title = "^$", float = true }, workspace = "special silent", }) -- Tiny Terraces opens floating by default; force it to tile. hl.window_rule({ match = { class = "steam_app_3136330" }, tile = true, }) -- Binds local mod = "SUPER" -- Apps hl.bind(mod .. " + T", hl.dsp.exec_cmd("ghostty")) hl.bind(mod .. " + E", hl.dsp.exec_cmd("nemo")) hl.bind(mod .. " + R", hl.dsp.exec_cmd("${pkgs.quickshell}/bin/qs ipc call launcher toggle")) hl.bind(mod .. " + Q", hl.dsp.window.close()) hl.bind(mod .. " + SHIFT + E", hl.dsp.exit()) -- Floating / layout hl.bind(mod .. " + V", hl.dsp.window.float({ action = "toggle" })) hl.bind(mod .. " + F", hl.dsp.window.fullscreen()) hl.bind(mod .. " + P", hl.dsp.window.pseudo()) hl.bind(mod .. " + S", hl.dsp.layout("togglesplit")) -- Focus hl.bind(mod .. " + left", hl.dsp.focus({ direction = "left" })) hl.bind(mod .. " + right", hl.dsp.focus({ direction = "right" })) hl.bind(mod .. " + up", hl.dsp.focus({ direction = "up" })) hl.bind(mod .. " + down", hl.dsp.focus({ direction = "down" })) hl.bind(mod .. " + H", hl.dsp.focus({ direction = "left" })) hl.bind(mod .. " + K", hl.dsp.focus({ direction = "up" })) hl.bind(mod .. " + J", hl.dsp.focus({ direction = "down" })) -- Power menu (quickshell launcher in power mode) hl.bind(mod .. " + L", hl.dsp.exec_cmd("${pkgs.quickshell}/bin/qs ipc call launcher powermenu")) -- Move windows hl.bind(mod .. " + SHIFT + left", hl.dsp.window.move({ direction = "left" })) hl.bind(mod .. " + SHIFT + right", hl.dsp.window.move({ direction = "right" })) hl.bind(mod .. " + SHIFT + up", hl.dsp.window.move({ direction = "up" })) hl.bind(mod .. " + SHIFT + down", hl.dsp.window.move({ direction = "down" })) -- Workspaces for i = 0, 9 do local workspace_id = tostring((i == 0) and 10 or i) hl.bind(mod .. " + " .. i, hl.dsp.focus({ workspace = workspace_id })) hl.bind(mod .. " + SHIFT + " .. i, hl.dsp.window.move({ workspace = workspace_id, follow = false })) end -- Screenshots — Shift+Super+S matches GNOME binding -- Pin/unpin quickshell's focus grab around the region select so an -- open menu survives slurp's input grab (no-ops if qs isn't up). hl.bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd("sh -c 'qs ipc call screenshot pin; hyprshot -m region; qs ipc call screenshot unpin'")) hl.bind("Print", hl.dsp.exec_cmd("hyprshot -m output")) -- Settings shortcut — Super+I matches GNOME binding hl.bind(mod .. " + I", hl.dsp.exec_cmd("pavucontrol")) -- Custom shortcuts hl.bind(mod .. " + Z", hl.dsp.exec_cmd("zen-beta")) -- Mouse window manipulation hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) hl.bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true }) -- Volume / brightness (repeating) hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("swayosd-client --output-volume raise"), { repeating = true }) hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("swayosd-client --output-volume lower"), { repeating = true }) hl.bind("XF86AudioMute", hl.dsp.exec_cmd("swayosd-client --output-volume mute-toggle"), { repeating = true }) hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("swayosd-client --brightness raise"), { repeating = true }) hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("swayosd-client --brightness lower"), { repeating = true }) ${lib.optionalString isMacbook '' hl.bind("XF86KbdBrightnessUp", hl.dsp.exec_cmd("${kbdBrightUp}"), { repeating = true }) hl.bind("XF86KbdBrightnessDown", hl.dsp.exec_cmd("${kbdBrightDown}"), { repeating = true }) ''} -- Media keys (locked — work through lockscreen) hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true }) hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true }) hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true }) ''; }; programs.hyprlock = { enable = true; settings = { general = { grace = 5; hide_cursor = true; }; background = { blur_passes = 3; blur_size = 8; brightness = 0.6; }; input-field = { size = "280, 44"; outline_thickness = 2; dots_size = 0.25; dots_spacing = 0.2; dots_center = true; fade_on_empty = true; fade_timeout = 3000; placeholder_text = ""; fail_text = ""; position = "0, -40"; halign = "center"; valign = "center"; rounding = 8; }; label = [ { text = "$TIME"; color = "rgb(${c.base05})"; font_size = 72; font_family = "Inter"; position = "0, 120"; halign = "center"; valign = "center"; } { text = "cmd[update:60000] date +'%A, %d %B'"; color = "rgb(${c.base04})"; font_size = 16; font_family = "Inter"; position = "0, 60"; halign = "center"; valign = "center"; } ]; }; }; services.hypridle = lib.mkIf isMacbook { enable = true; settings = { general = { lock_cmd = "pidof hyprlock || hyprlock"; before_sleep_cmd = "loginctl lock-session"; after_sleep_cmd = "hyprctl dispatch dpms on"; }; listener = [ { timeout = 300; # 5 min — lock on-timeout = "loginctl lock-session"; } { timeout = 420; # 7 min — display off on-timeout = "hyprctl dispatch dpms off"; on-resume = "hyprctl dispatch dpms on"; } { timeout = 600; # 10 min — suspend on-timeout = "systemctl suspend"; } ]; }; }; # Scope all HM Wayland services (hyprpaper, etc.) to the # Hyprland session so they don't crash-loop in a GNOME session. wayland.systemd.target = "hyprland-session.target"; }; }; }