# ABOUTME: Deploys the XDG configs from the moonarch repo and the package set. # ABOUTME: Niri, Quickshell, terminal, theming, portals. { pkgs, lib, moonarchSrc, moonarchPkgs, moonarchThemes, ... }: let xdg = "${moonarchSrc}/defaults/xdg"; etc = "${moonarchSrc}/defaults/etc"; # waypaper calls `swww`/`swww-daemon`, but the maintained successor ships as # `awww`. Same compatibility symlinks the Arch package creates. swww-compat = pkgs.runCommand "swww-compat" { } '' mkdir -p "$out/bin" ln -s ${pkgs.awww}/bin/awww "$out/bin/swww" ln -s ${pkgs.awww}/bin/awww-daemon "$out/bin/swww-daemon" ''; wallpaper = "${moonarchSrc}/defaults/backgrounds/wallpaper.jpg"; wallpaperBlur = "${moonarchSrc}/defaults/backgrounds/wallpaper-blur.jpg"; # waypaper's config points at /usr/share/moonarch/wallpaper.jpg, which does # not exist here. Generated with the store path instead of copied verbatim. waypaperConfig = pkgs.runCommand "waypaper-config.ini" { } '' substitute ${xdg}/waypaper/config.ini "$out" \ --replace-fail /usr/share/moonarch/wallpaper.jpg ${wallpaper} ''; # Same treatment for swaylock's wallpaper path. swaylock searches # ~/.swaylock/config, $XDG_CONFIG_HOME/swaylock/config and # SYSCONFDIR/swaylock/config — and its sysconfdir is inside the store here, # so the home copy seeded below is the only one it ever reads. On Arch the # same file ships to /etc/swaylock/config, which is why it lives in # defaults/etc/ rather than defaults/xdg/. swaylockConfig = pkgs.runCommand "swaylock-config" { } '' substitute ${etc}/swaylock/config "$out" \ --replace-fail /usr/share/moonarch/wallpaper-blur.jpg ${wallpaperBlur} ''; # PipeWire searches $PIPEWIRE_CONFIG_DIR, then $XDG_CONFIG_HOME/pipewire, then # its compiled-in /etc/pipewire — never /etc/xdg (verified in PipeWire's # src/pipewire/conf.c). The shared file therefore cannot be deployed the way # the other XDG configs are; it goes through the module's configPackages, # which is what its own option documentation shows for a conf.d file. # # The file names the plugin as `librnnoise_ladspa`, resolved through # LADSPA_PATH — /usr/lib/ladspa on Arch, nothing here. Substituted to the # store path, like the wallpaper paths above. LADSPA has no counterpart to # services.pipewire.extraLv2Packages, so the path belongs in the file. denoisingConfig = pkgs.runCommand "moonarch-pipewire-denoising" { } '' mkdir -p "$out/share/pipewire/pipewire.conf.d" substitute ${xdg}/pipewire/pipewire.conf.d/99-input-denoising.conf \ "$out/share/pipewire/pipewire.conf.d/99-input-denoising.conf" \ --replace-fail "plugin = librnnoise_ladspa" \ "plugin = ${pkgs.rnnoise-plugin}/lib/ladspa/librnnoise_ladspa.so" ''; # zsh/newuser runs its setup wizard on every interactive login for as long as # the user has none of .zshenv, .zprofile, .zshrc or .zlogin — the whole # configuration living in /etc/zshrc does not count. Seeded below to stop # that, and deliberately without configuration: on Arch this file carries # `source /etc/zsh/zshrc.moonarch`, which here would source the shared zshrc # a second time, on top of the one /etc/zshrc already did. userZshrc = pkgs.writeText "zshrc" '' # The moonarch zsh configuration is system-wide, in /etc/zshrc. # # This file keeps zsh from running zsh-newuser-install and is the place for # your own additions: zsh reads it after /etc/zshrc, so it wins over it. ''; in { # --- XDG configs -> /etc/xdg/ --- # # Deployed verbatim from the moonarch repo; defaults/ stays the single # source of truth for both distros. # # defaults/xdg/walker/ is not deployed: the Quickshell launcher covers it. environment.etc = { # niri reads /etc/niri, not /etc/xdg/niri. Both are provided: the former # is what niri actually loads, the latter keeps the layout consistent with # the Arch package and with any user config that includes it. "niri/config.kdl".source = "${xdg}/niri/config.kdl"; "xdg/niri/config.kdl".source = "${xdg}/niri/config.kdl"; "xdg/quickshell/moonarch".source = "${xdg}/quickshell/moonarch"; "xdg/foot/foot.ini".source = "${xdg}/foot/foot.ini"; "xdg/fastfetch/config.jsonc".source = "${xdg}/fastfetch/config.jsonc"; # defaults/xdg/kanshi/config is an empty placeholder — on Arch too. Not # deployed: monitor profiles are machine specific and belong in # ~/.config/kanshi/config. An empty file parses fine; a missing one is what # kanshi reports as "failed to parse config file", which is why the unit in # modules/services.nix waits for the home config to exist. "xdg/Kvantum/kvantum.kvconfig".source = "${xdg}/Kvantum/kvantum.kvconfig"; "xdg/qt6ct/qt6ct.conf".source = "${xdg}/qt6ct/qt6ct.conf"; "xdg/gtk-3.0/settings.ini".source = "${xdg}/gtk-3.0/settings.ini"; "xdg/gtk-4.0/settings.ini".source = "${xdg}/gtk-4.0/settings.ini"; # No xdg/pipewire/ entry: PipeWire does not read /etc/xdg, so the file was # deployed and inert. It goes through services.pipewire.configPackages # below instead. # Kept for layout parity with the Arch package; waypaper itself only reads # the copy in the home directory, seeded below. "xdg/waypaper/config.ini".source = waypaperConfig; # stasis reads /etc/stasis/ as its system-wide fallback, never /etc/xdg/. "stasis/stasis.rune".source = "${xdg}/stasis/stasis.rune"; # Configs outside /etc/xdg/ in the Arch package as well. "mpv/mpv.conf".source = "${etc}/mpv/mpv.conf"; "xdg-desktop-portal/niri-portals.conf".source = "${etc}/xdg-desktop-portal/niri-portals.conf"; }; # Generic-family defaults. Same content as # defaults/etc/fonts/conf.d/65-moonarch-fonts.conf, but routed through the # fontconfig module: /etc/fonts/conf.d/ is owned by NixOS and rejects # environment.etc entries. # localConf is written to /etc/fonts/local.conf verbatim, so it has to be a # complete XML document — not just the inner elements. fonts.fontconfig.localConf = '' Moonarch generic-family defaults sans-serif UbuntuSans Nerd Font monospace UbuntuSansMono Nerd Font ''; # --- Compositor --- programs.niri.enable = true; # niri searches $XDG_CONFIG_HOME/niri/config.kdl, then /etc/niri/config.kdl — # not /etc/xdg. On Arch a one-line ~/.config/niri/config.kdl bridges there # via `include`; here the system path is used directly, so nothing has to be # placed in the home directory. # config.kdl spawns the polkit agent by its FHS path # (/usr/lib/polkit-gnome/...). Provide that path rather than diverging the # config, which is shared with Arch. systemd.tmpfiles.rules = [ "d /usr/lib 0755 root root -" "L+ /usr/lib/polkit-gnome - - - - ${pkgs.polkit_gnome}/libexec" ]; # --- Shell --- programs.zsh.enable = true; # Same file the Arch package installs as /etc/zsh/zshrc.moonarch. zsh reads # /etc/zshrc before ~/.zshrc, so a user file still wins over it. programs.zsh.interactiveShellInit = '' source ${moonarchSrc}/defaults/shell/zshrc ''; # /etc/zshrc places promptInit after interactiveShellInit, and its default # ends in `prompt suse`, which assigns PS1 — the same parameter as PROMPT. # Left at the default it overwrites the prompt the sourced zshrc just set. # The zshrc owns the prompt, so nothing else has to run here. programs.zsh.promptInit = ""; # The zshrc sources both plugins from /usr/share/zsh/plugins, the path the # Arch packages use; the lookups are guarded and silently find nothing here. # These modules source the same plugins from the store instead. The strategy # is the one the zshrc sets on Arch. programs.zsh.syntaxHighlighting.enable = true; programs.zsh.autosuggestions = { enable = true; strategy = [ "history" "completion" ]; }; # environment.shellAliases carries ls, ll and l as defaults, and /etc/zshrc # renders them after interactiveShellInit, so they shadow the eza aliases of # the sourced zshrc. null drops an alias. Scoped to zsh: bash keeps the # coreutils ls, which is the only thing it has. programs.zsh.shellAliases = { ls = null; ll = null; l = null; }; # --- File manager --- # # Counterpart to thunar, thunar-volman, thunar-archive-plugin, tumbler and # ffmpegthumbnailer in packages/official.txt. programs.thunar = { enable = true; plugins = with pkgs; [ thunar-volman thunar-archive-plugin ]; }; services.tumbler.enable = true; # --- Theme, systemwide instead of the gsettings calls in post-install.sh --- programs.dconf = { enable = true; profiles.user.databases = [{ settings."org/gnome/desktop/interface" = { gtk-theme = "Colloid-Grey-Dark-Catppuccin"; icon-theme = "Colloid-Grey-Catppuccin-Dark"; color-scheme = "prefer-dark"; font-name = "UbuntuSans Nerd Font 11"; }; }]; }; # --- Portals --- xdg.portal = { enable = true; extraPortals = with pkgs; [ xdg-desktop-portal-gnome xdg-desktop-portal-gtk ]; }; fonts.packages = with pkgs; [ nerd-fonts.ubuntu-sans openmoji-color ]; # --- Audio --- # # RNNoise input denoising, from the shared config. The filter chain appears as # a separate "Noise Canceling source" input; it does not replace the raw # microphone, so applications have to select it. services.pipewire.configPackages = [ denoisingConfig ]; # --- Printing --- # # Driverless only, and no printer is declared here. Asked over IPP, the # Samsung M267x 287x answers with image/urf and application/PCLm among its # document formats, so CUPS builds the queue from the capabilities the device # reports about itself — no PPD, no driver package, nothing about anyone's # network in this repository. Any AirPrint-capable printer works the same way, # including ones this machine has never seen. # # Avahi is the discovery half: without it the printer is not found and its # .local name does not resolve. openFirewall defaults to true and opens # UDP 5353 for mDNS. services.printing.enable = true; services.avahi = { enable = true; nssmdns4 = true; openFirewall = true; }; # --- VPN --- # # networking.networkmanager.plugins defaults to an empty list, and without the # OpenVPN plugin NetworkManager cannot create such a connection at all — # neither nmcli, nmtui nor nm-applet offer the type. Counterpart to # networkmanager-openvpn in packages/official.txt. nm-applet, installed below, # is what hands the keyring password to the connection. networking.networkmanager.plugins = [ pkgs.networkmanager-openvpn ]; environment.systemPackages = with pkgs; [ # XDG plumbing, before anything that calls into it. xdg-terminal-exec # resolves a caller to whichever TerminalEmulator entry the XDG data # hierarchy offers — footclient here, which reuses the server niri starts # — so nothing downstream has to name an emulator. xdg-terminal-exec # Own programs moonarchPkgs.sweet-cursors moonarchPkgs.moonarch-scripts # Browser. Arch runs waterfox-bin, which nixpkgs does not carry; see # DECISIONS.md for why this is not packaged here. librewolf # Session lock. The power menu is a Quickshell popout, not a package. swaylock # Compositor extras xwayland-satellite kanshi # Bar, notifications, clipboard. # Neither waybar nor swaync are installed: the Quickshell bar owns both # roles. The "kept as a reserve" notes in the Arch config date from the # migration and do not apply to a fresh setup. quickshell libnotify cliphist wl-clipboard # Display and wallpaper wlsunset nwg-look brightnessctl awww swww-compat waypaper # Terminal foot # Audio. No pulseaudio for pactl: Steam probes wpctl and pactl both and # takes whichever is on PATH (nixpkgs#525793), and wireplumber puts wpctl # there. Its "pactl: command not found" is noise from Steam, not a gap. pavucontrol alsa-utils # Network and bluetooth. openvpn is the binary the NetworkManager plugin # below drives; it also stands on its own for a config file. networkmanagerapplet blueman openvpn # Media vlc viewnior # File manager stack. thunar and tumbler come from their modules above; # ffmpegthumbnailer is what tumbler uses for video thumbnails. gvfs udisks2 ntfs3g ffmpegthumbnailer # Backend for thunar-archive-plugin above; without it its menu entries do # nothing. xarchiver zip unzip # Theming. Same derivations the greeter uses, see flake.nix. moonarchThemes.gtk moonarchThemes.icons libsForQt5.qtstyleplugin-kvantum qt6Packages.qtstyleplugin-kvantum qt6Packages.qt6ct qt6.qt5compat # CLI tools referenced by zshrc and the helper scripts bat btop eza fastfetch fd fzf jq lazygit ripgrep neovim git # System fwupd upower polkit_gnome wdisplays wl-color-picker stasis claude-code ]; security.pam.services.swaylock = {}; # Polkit agent for privilege prompts inside the session. security.polkit.enable = true; # nixpkgs.config.allowUnfree covers the system build only. `nix profile` and # friends evaluate a flake purely, so they see neither that setting nor # ~/.config/nixpkgs/config.nix, and refuse every unfree package. The hosts # already declare unfree as acceptable; this makes the imperative commands # agree instead of demanding NIXPKGS_ALLOW_UNFREE=1 in front of each one. # Set by PAM at session start, so it takes a re-login, not just a rebuild. environment.sessionVariables.NIXPKGS_ALLOW_UNFREE = "1"; # Seed configs for programs that read only from the home directory and have # no system-wide fallback. 'C' copies once and never overwrites, so user # edits survive. This is what post-install.sh does imperatively on Arch. systemd.user.tmpfiles.rules = [ "C %h/.config/waypaper/config.ini 0644 - - - ${waypaperConfig}" # stasis prefers the user config over /etc and generates its own if none # exists, so the moonarch config has to be in place before it starts. "C %h/.config/stasis/stasis.rune 0644 - - - ${xdg}/stasis/stasis.rune" "C %h/.config/swaylock/config 0644 - - - ${swaylockConfig}" "C %h/.zshrc 0644 - - - ${userZshrc}" ]; }