2026-08-05 18:57:23 +01:00
|
|
|
{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
|
2026-08-06 14:02:26 +01:00
|
|
|
quickshell
|
2026-08-16 17:52:18 +01:00
|
|
|
screenshot
|
2026-08-05 18:57:23 +01:00
|
|
|
];
|
|
|
|
|
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;
|
|
|
|
|
|
2026-08-05 22:05:49 +01:00
|
|
|
# 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;
|
|
|
|
|
|
2026-08-06 17:09:30 +01:00
|
|
|
# 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.<n>.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";
|
|
|
|
|
};
|
2026-08-05 22:05:49 +01:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-05 18:57:23 +01:00
|
|
|
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
|
2026-08-05 21:51:30 +01:00
|
|
|
xrandr
|
2026-08-05 18:57:23 +01:00
|
|
|
networkmanagerapplet
|
|
|
|
|
blueman
|
|
|
|
|
udiskie
|
|
|
|
|
brightnessctl
|
|
|
|
|
# clipboard
|
|
|
|
|
cliphist
|
|
|
|
|
# theming
|
|
|
|
|
gsettings-desktop-schemas
|
|
|
|
|
dunst
|
|
|
|
|
pywal16
|
|
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
}
|