{self, ...}: { # Hyprland desktop. The compositor config itself (hyprland.lua + ahmza/*.lua, # hyprlock, hypridle, hyprsunset) is managed via stow (dotfiles: hypr package). # Here we enable Hyprland and install the runtime tools that config + the # helper scripts in the `bin` package expect on PATH. flake.nixosModules.hyprland = {pkgs, ...}: let modules = with self.nixosModules; [ audio systemTheme fuzzel ghostty waybar quickshell screenshot ]; in { imports = modules; programs.hyprland = { enable = true; withUWSM = true; }; # hypr = compositor config, dunst = notifications, wal = pywal templates, # bin = helper scripts (swp/launch-waybar/passmenu) referenced by keybinds. ahmza.stow.packages = ["hypr" "dunst" "wal" "bin"]; services.displayManager.defaultSession = "hyprland-uwsm"; services.xserver.enable = true; security.polkit.enable = true; # Removable-media automounting. # # udiskie is only a tray frontend to the udisks2 D-Bus daemon, so the daemon # has to be enabled or udiskie runs but can never mount anything. Note this # covers removable media *only*: udisks2 reports the internal SATA disks as # HintSystem=true, so udiskie leaves them alone. Permanently attached drives # are declared in the host's disks.nix instead. services.udisks2.enable = true; # Session daemons run as systemd user units bound to graphical-session.target # (which UWSM manages properly) rather than from the hypr autostart, so they # start and stop with the session instead of leaking across logins. # # hypridle and hyprsunset both ship their own unit, but in # $out/share/systemd/user — NOT $out/lib/systemd/user. That distinction is # why they need redeclaring rather than just enabling: # # * `systemd.packages = [pkgs.hypridle]` would collect nothing. nixpkgs' # generateUnits globs only $pkg/etc/systemd/user and $pkg/lib/systemd/user. # * The units are nonetheless *visible* to the user manager, via # environment.pathsToLink -> /run/current-system/sw/share/systemd/user, # which is on the search path because XDG_DATA_DIRS ends with # /run/current-system/sw/share. But visible is not enabled: nothing # creates the WantedBy symlink, so they sit unstarted. # * `systemd.user.services..wantedBy` alone would not help either — with # no other keys NixOS synthesises a *replacement* unit with an empty # [Service] section, shadowing upstream's via /etc/systemd/user (which # takes precedence over $XDG_DATA_DIRS). # # So the full definition below is deliberate. It is close to upstream's but # not identical: hyprsunset's `Requires=graphical-session.target` is dropped # on purpose (PartOf already propagates the stop, while Requires would also # make starting hyprsunset by hand pull up the whole session target), and # hypridle gains an explicit `path` because its config shells out. systemd.user.services = { udiskie = { description = "udiskie removable media automounter"; partOf = ["graphical-session.target"]; after = ["graphical-session.target"]; wantedBy = ["graphical-session.target"]; serviceConfig = { Type = "simple"; # -s = smart tray: the icon appears only while removable media is present. ExecStart = "${pkgs.udiskie}/bin/udiskie -s"; Restart = "on-failure"; }; }; hypridle = { description = "Hyprland idle daemon"; documentation = ["https://wiki.hyprland.org/Hypr-Ecosystem/hypridle"]; partOf = ["graphical-session.target"]; after = ["graphical-session.target"]; wantedBy = ["graphical-session.target"]; # ~/.config/hypr/hypridle.conf calls out to these: hyprlock (lock_cmd), # pidof (the `pidof hyprlock ||` guard), brightnessctl (backlight # listener), hyprctl (dpms), loginctl + systemctl (sleep/lock), # notify-send (unlock_cmd). # # MAINTENANCE HAZARD: that config lives in a *different* repo (the stow'd # `hypr` package in the dotfiles repo), so this list can silently drift # out of sync with it. A binary used there but missing here fails as a # bare "command not found" inside hypridle, and Restart=on-failure will # NOT catch it because hypridle itself stays up — the idle action just # quietly stops working. Add any new binary to both places. path = with pkgs; [ hyprlock hyprland brightnessctl libnotify procps systemd ]; unitConfig.ConditionEnvironment = "WAYLAND_DISPLAY"; serviceConfig = { Type = "simple"; ExecStart = "${pkgs.hypridle}/bin/hypridle"; Restart = "on-failure"; }; }; hyprsunset = { description = "Hyprland blue-light filter"; documentation = ["https://wiki.hyprland.org/Hypr-Ecosystem/hyprsunset"]; partOf = ["graphical-session.target"]; after = ["graphical-session.target"]; wantedBy = ["graphical-session.target"]; unitConfig.ConditionEnvironment = "WAYLAND_DISPLAY"; serviceConfig = { Type = "simple"; ExecStart = "${pkgs.hyprsunset}/bin/hyprsunset"; Restart = "on-failure"; # Intentionally only on hyprsunset, not hypridle/udiskie: upstream's # hyprsunset.service sets Slice=session.slice and upstream's # hypridle.service does not. Not an oversight — don't "fix" it. Slice = "session.slice"; }; }; }; environment.systemPackages = with pkgs; [ # hypr* ecosystem (keybinds / hypridle / autostart) hyprlock hypridle hyprsunset hyprpicker # launcher / file manager / browser (vars.lua: Picker/FileManager/Browser) kdePackages.dolphin firefox # wallpaper daemon used by the `swp` script + autostart (awww-daemon) awww # scripting deps for bin/ helpers (swp uses jq + hyprctl) jq # display / applets referenced by autostart xrandr networkmanagerapplet blueman udiskie brightnessctl # clipboard cliphist # theming gsettings-desktop-schemas dunst pywal16 ]; }; }