{ config, lib, pkgs, ... }: let c = import ./colors.nix; renderColors = text: builtins.replaceStrings (map (name: "@${name}@") ( builtins.attrNames c )) (builtins.attrValues c) text; rgb = color: "rgb(${lib.removePrefix "#" color})"; wallpaper = import ./wallpaper.nix { inherit pkgs; }; help = pkgs.writeShellApplication { name = "desktop-help"; runtimeInputs = [ pkgs.python3 pkgs.hyprland pkgs.fuzzel ]; text = ''exec python ${./desktop-help.py} "$@"''; }; wallpaperInit = pkgs.writeShellApplication { name = "initialize-wallpaper"; runtimeInputs = [ pkgs.awww pkgs.coreutils ]; text = '' # Socket readiness precedes Wayland output discovery on a cold login. for ((attempt=0; attempt<30; attempt++)); do if awww img ${wallpaper} --transition-type fade --transition-duration 0.5; then exit 0 fi sleep 0.5 done echo 'No wallpaper output became ready.' >&2 exit 1 ''; }; actions = pkgs.writeShellApplication { name = "desktop"; runtimeInputs = with pkgs; [ python3 hyprland uwsm systemd fuzzel libnotify cliphist wl-clipboard grim slurp satty wf-recorder wireplumber pulseaudio brightnessctl power-profiles-daemon xdg-user-dirs xdg-utils swaynotificationcenter hyprlock networkmanager util-linux ]; text = '' export DESKTOP_EMOJI_FILE=${pkgs.unicode-emoji.emoji-test}/share/unicode/emoji/emoji-test.txt exec python ${./desktop-actions.py} "$@" ''; }; screenshot = pkgs.writeShellApplication { name = "desktop-screenshot"; runtimeInputs = [ actions ]; text = ''exec desktop screenshot area --annotate "$@"''; }; launcherExec = pkgs.writeShellScript "anyrun-uwsm" '' kind=$1 shift if [ "$kind" = term ]; then printf '%s\n' "$*" else printf 'uwsm app -- %s\n' "$*" fi ''; networkManager = config.networking.networkmanager.enable; bluetooth = config.hardware.bluetooth.enable; power = config.services.upower.enable; in { programs.hyprland = { enable = true; withUWSM = true; }; programs.hyprlock.enable = true; services.hypridle.enable = true; hardware.graphics.enable = true; security.polkit.enable = true; security.rtkit.enable = true; programs.dconf.enable = true; services.pipewire = { enable = true; alsa.enable = true; alsa.support32Bit = true; pulse.enable = true; wireplumber.enable = true; }; # NixOS's Hyprland module already installs the Hyprland and GTK portals. xdg.portal.config.hyprland = { default = [ "hyprland" "gtk" ]; "org.freedesktop.impl.portal.FileChooser" = [ "gtk" ]; }; environment.sessionVariables.NIXOS_OZONE_WL = "1"; environment.systemPackages = with pkgs; [ wl-clipboard grim slurp satty screenshot actions help hyprpicker wf-recorder pavucontrol playerctl brightnessctl xdg-utils libnotify ]; services.udev.packages = [ pkgs.brightnessctl ]; fonts.packages = with pkgs; [ inter noto-fonts noto-fonts-cjk-sans noto-fonts-color-emoji ]; fonts.fontconfig.defaultFonts = { sansSerif = [ "Inter" "Noto Sans" ]; serif = [ "Noto Serif" ]; emoji = [ "Noto Color Emoji" ]; }; home-manager.users.dev = { xdg.enable = true; xdg.userDirs = { enable = true; createDirectories = true; }; xdg.autostart.enable = true; home.pointerCursor = { package = pkgs.bibata-cursors; name = "Bibata-Modern-Ice"; size = 24; gtk.enable = true; x11.enable = true; }; gtk = { enable = true; font = { name = "Inter"; size = 11; }; theme = { name = "Adwaita-dark"; package = pkgs.gnome-themes-extra; }; iconTheme = { name = "Papirus-Dark"; package = pkgs.papirus-icon-theme; }; gtk3.extraConfig.gtk-application-prefer-dark-theme = true; gtk4.extraConfig.gtk-application-prefer-dark-theme = true; }; qt = { enable = true; platformTheme.name = "gtk3"; # Let Home Manager provide BOTH Qt 5 and Qt 6 style plugins. style.name = "adwaita-dark"; }; dconf.settings."org/gnome/desktop/interface" = { color-scheme = "prefer-dark"; font-name = "Inter 11"; monospace-font-name = "JetBrainsMono Nerd Font 12"; accent-color = "yellow"; }; wayland.windowManager.hyprland = { enable = true; package = null; portalPackage = null; systemd.enable = false; # UWSM owns the session and environment. configType = "lua"; extraConfig = renderColors (builtins.readFile ./hyprland.lua); }; services.hyprpolkitagent.enable = true; services.awww.enable = true; systemd.user.services.awww.Service = { Type = "notify"; CacheDirectory = "awww"; ExecStartPost = "${wallpaperInit}/bin/initialize-wallpaper"; }; programs.ashell = { enable = true; systemd.enable = true; settings = { log_level = "warn"; position = "Top"; layer = "Top"; modules = { left = [ [ "appLauncher" "Workspaces" "desktopHelp" ] "WindowTitle" ]; center = [ "Tempo" ]; right = [ "recording" "MediaPlayer" "SystemInfo" "notifications" "Tray" "Privacy" "Settings" ]; }; CustomModule = [ { name = "appLauncher"; icon = "󰍜"; command = "uwsm app -- anyrun"; } { name = "desktopHelp"; icon = "󰋖"; command = "uwsm app -- desktop-help"; } { name = "notifications"; icon = "󰂚"; command = "uwsm app -- desktop notifications"; listen_cmd = "swaync-client -swb"; icons."dnd.*" = "󰂛"; alert = "notification"; } { name = "recording"; command = "uwsm app -- desktop record"; listen_cmd = "desktop status"; alert = "recording"; } ]; enable_esc_key = true; media_player.max_title_length = 18; window_title = { mode = "Title"; truncate_title_after_length = 26; }; system_info = { indicators = [ "Cpu" "Memory" "Temperature" ]; interval = 5; }; tempo.clock_format = "%a %d %b %H:%M"; workspaces = { visibility_mode = "MonitorSpecific"; enable_workspace_filling = false; workspace_names = [ "01" "02" "03" "04" "05" "06" "07" "08" "09" "10" ]; }; settings = { lock_cmd = "loginctl lock-session"; logout_cmd = "uwsm app -- desktop power"; shutdown_cmd = "uwsm app -- desktop power"; reboot_cmd = "uwsm app -- desktop power"; CustomButton = [ { name = "Actions"; icon = "󰒓"; command = "uwsm app -- desktop menu"; } { name = "Health"; icon = "󰄦"; command = "uwsm app -- kitty --hold -e desktop health"; } ]; audio_sinks_more_cmd = "uwsm app -- pavucontrol -t 3"; audio_sources_more_cmd = "uwsm app -- pavucontrol -t 4"; wifi_more_cmd = "uwsm app -- nm-connection-editor"; vpn_more_cmd = "uwsm app -- nm-connection-editor"; bluetooth_more_cmd = "uwsm app -- blueman-manager"; indicators = [ "IdleInhibitor" "Audio" ] ++ lib.optionals networkManager [ "Network" "Vpn" ] ++ lib.optionals bluetooth [ "Bluetooth" ] ++ lib.optionals power [ "Battery" ]; audio_indicator_format = "IconAndPercentage"; }; appearance = { font_name = "Inter"; scale_factor = 1.15; style = "Solid"; opacity = 1.0; primary_color = { base = c.accent; text = c.background; }; text_color = c.text; success_color = c.green; danger_color = c.red; background_color = { base = c.background; weak = c.surface; strong = c.raised; }; secondary_color.base = c.surface; workspace_colors = [ c.accent ]; }; }; }; systemd.user.services.ashell.Unit.PartOf = [ "graphical-session.target" ]; programs.anyrun = { enable = true; config = { plugins = [ "${pkgs.anyrun}/lib/libapplications.so" "${pkgs.anyrun}/lib/librink.so" ]; width.fraction = 0.5; y.fraction = 0.12; hidePluginInfo = true; closeOnClick = true; showResultsImmediately = true; maxEntries = 6; }; extraCss = renderColors (builtins.readFile ./anyrun.css); extraConfigFiles."applications.ron".text = '' ( desktop_actions: false, hide_description: true, max_entries: 6, terminal: Some((command: "uwsm", args: "app -- kitty -e {}")), preprocess_exec_script: Some("${launcherExec}"), ) ''; }; # This pinned launcher needs a daemon for calculator copy-to-clipboard. systemd.user.services.anyrun = { Unit = { Description = "Application launcher and calculator"; After = [ "graphical-session.target" ]; PartOf = [ "graphical-session.target" ]; ConditionEnvironment = "WAYLAND_DISPLAY"; }; Service = { ExecStart = "${pkgs.anyrun}/bin/anyrun daemon"; Restart = "on-failure"; }; Install.WantedBy = [ "graphical-session.target" ]; }; # Fuzzel is the shared dmenu-style picker for actions/help; Anyrun remains # the app launcher/calculator, not a second competing autostarted shell. programs.fuzzel = { enable = true; settings = { main = { font = "Inter:size=11"; terminal = "kitty"; "launch-prefix" = "uwsm app --"; "line-height" = 22; width = 46; lines = 10; "horizontal-pad" = 18; "vertical-pad" = 14; }; colors = { background = "${lib.removePrefix "#" c.background}ff"; text = "${lib.removePrefix "#" c.text}ff"; match = "${lib.removePrefix "#" c.accent}ff"; selection = "${lib.removePrefix "#" c.selection}ff"; "selection-text" = "${lib.removePrefix "#" c.text}ff"; "selection-match" = "${lib.removePrefix "#" c.accent}ff"; border = "${lib.removePrefix "#" c.border}ff"; prompt = "${lib.removePrefix "#" c.muted}ff"; }; border = { width = 2; radius = 6; }; }; }; services.swaync = { enable = true; style = renderColors (builtins.readFile ./swaync.css); settings = { positionX = "right"; positionY = "top"; control-center-width = 340; control-center-height = 500; control-center-margin-top = 8; control-center-margin-right = 8; notification-window-width = 360; fit-to-screen = false; timeout = 6; timeout-critical = 0; keyboard-shortcuts = true; hide-on-action = true; widgets = [ "title" "dnd" "mpris" "notifications" ]; widget-config = { title = { text = "Notifications"; clear-all-button = true; button-text = "Clear"; }; dnd.text = "Do not disturb"; mpris = { image-size = 64; image-radius = 4; }; }; }; }; services.hyprsunset = { enable = true; settings.profile = [ { time = "07:00"; identity = true; } { time = "21:00"; temperature = 4200; } ]; }; systemd.user.services.desktop-clipboard = { Unit = { Description = "Session-local clipboard history"; After = [ "graphical-session.target" ]; PartOf = [ "graphical-session.target" ]; ConditionEnvironment = "WAYLAND_DISPLAY"; }; Service = { ExecStart = "${pkgs.wl-clipboard}/bin/wl-paste --watch ${actions}/bin/desktop clipboard-store"; ExecStopPost = "${actions}/bin/desktop clipboard-clear"; Restart = "on-failure"; UMask = "0077"; }; Install.WantedBy = [ "graphical-session.target" ]; }; systemd.user.services.desktop-battery = lib.mkIf power { Unit = { Description = "Low/critical battery notifications"; After = [ "graphical-session.target" ]; PartOf = [ "graphical-session.target" ]; ConditionPathExistsGlob = "/sys/class/power_supply/BAT*"; }; Service = { ExecStart = "${actions}/bin/desktop battery"; Restart = "on-failure"; }; Install.WantedBy = [ "graphical-session.target" ]; }; programs.hyprlock = { enable = true; package = null; settings = { general = { hide_cursor = true; ignore_empty_input = true; }; background = [ { monitor = ""; path = "${wallpaper}"; blur_passes = 2; } ]; label = [ { monitor = ""; text = "cmd[update:1000] date +'%H:%M'"; font_size = 64; font_family = "Inter"; color = rgb c.text; position = "0,100"; halign = "center"; valign = "center"; } { monitor = ""; text = "$USER"; font_size = 18; font_family = "Inter"; color = rgb c.muted; position = "0,0"; halign = "center"; valign = "center"; } ]; input-field = [ { monitor = ""; size = "320,60"; position = "0,-100"; halign = "center"; valign = "center"; outline_thickness = 2; dots_center = true; fade_on_empty = false; font_family = "Inter"; rounding = 6; inner_color = rgb c.surface; outer_color = rgb c.accent; font_color = rgb c.text; check_color = rgb c.green; fail_color = rgb c.red; capslock_color = rgb c.yellow; placeholder_text = "Password"; fail_text = "Try again ($ATTEMPTS)"; } ]; }; }; services.hypridle = { enable = true; package = null; # NixOS owns the service and PAM integration. settings = { general = { lock_cmd = "${actions}/bin/desktop lock-start"; before_sleep_cmd = "loginctl lock-session"; after_sleep_cmd = "hyprctl dispatch 'hl.dsp.dpms({ action = \"enable\" })'"; inhibit_sleep = 3; }; listener = [ { timeout = 600; on-timeout = "loginctl lock-session"; } { timeout = 900; on-timeout = "hyprctl dispatch 'hl.dsp.dpms({ action = \"disable\" })'"; on-resume = "hyprctl dispatch 'hl.dsp.dpms({ action = \"enable\" })'"; } ]; }; }; }; }