From dba33d246312135a776f6cb362b4878829b46246 Mon Sep 17 00:00:00 2001 From: nevaforget Date: Thu, 24 Sep 2026 15:18:33 +0200 Subject: [PATCH] refactor(modules): one topic per module, and Firefox preconfigured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes that cannot be separated in the result: the Firefox preconfiguration is what made desktop.nix untenable, and the split is what it now lives in. Firefox replaces LibreWolf, with Betterfox as its preference base and the settings that Betterfox does not cover expressed as policies: uBlock Origin, the Catppuccin theme, no Pocket, no Sync, autoscroll, DuckDuckGo, no translation offer, no password prompts, no bookmarks toolbar, about:blank as home and new tab. Both add-ons are normal_installed and the preferences keep status "default", so none of it is locked away — undoing settings by hand was the reason to leave LibreWolf. desktop.nix carried thirteen topics over 637 lines, and the existing split followed the NixOS option namespace rather than the topic: services.pipewire here, its configPackages there; services.gvfs and services.udisks2 apart from their packages; Bluetooth likewise. Nine modules now each hold one topic with its options, packages, configs and services together. desktop.nix keeps the compositor, the /etc deployment, portals and what belongs to no topic. The refactor changes nothing about the built system, and that was checked after each of the eight steps rather than assumed. The plain store path does not serve as the criterion: moving a package to another module reorders environment.systemPackages, which changes the system-path hash and propagates into every file naming it. What was compared instead is the package set through diff-closures and every differing file with store hashes normalised away. Two steps reordered the user tmpfiles list, which is neutral only because no two rules name the same path — checked, not presumed. --- DECISIONS.md | 140 +++++++++++++++++ README.md | 13 +- flake.lock | 25 ++- flake.nix | 21 +++ modules/audio.nix | 49 ++++++ modules/browsers.nix | 111 ++++++++++++++ modules/desktop.nix | 357 ++----------------------------------------- modules/files.nix | 33 ++++ modules/lock.nix | 65 ++++++++ modules/network.nix | 27 ++++ modules/printing.nix | 24 +++ modules/services.nix | 16 -- modules/shell.nix | 80 ++++++++++ modules/theme.nix | 152 ++++++++++++++++++ pkgs/betterfox.nix | 32 ++++ 15 files changed, 775 insertions(+), 370 deletions(-) create mode 100644 modules/audio.nix create mode 100644 modules/browsers.nix create mode 100644 modules/files.nix create mode 100644 modules/lock.nix create mode 100644 modules/network.nix create mode 100644 modules/printing.nix create mode 100644 modules/shell.nix create mode 100644 modules/theme.nix create mode 100644 pkgs/betterfox.nix diff --git a/DECISIONS.md b/DECISIONS.md index 2c56df7..cd90d78 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1,5 +1,145 @@ # Decisions +## 2026-09-24 – One topic per module, split out of desktop.nix + +- **Who**: Dominik, ClaudeCode +- **Why**: `desktop.nix` had grown to 637 lines carrying thirteen topics, and + the Firefox preconfiguration took its browser block from 5 lines to 70. The + deeper problem only showed on reading it: the existing split followed the + NixOS option namespace rather than the topic. `services.pipewire` lived in + `services.nix` while its RNNoise `configPackages` sat in `desktop.nix`; + `services.gvfs` and `services.udisks2` there, their packages here; Bluetooth + likewise. Touching audio meant opening two files and knowing that it did. +- **Tradeoffs**: (1) **No enable option per module.** The stricter form would + give each module an `options` block so a host could switch it off. `disk.nix` + and `user.nix` have that for a reason — every host sets a different device and + login name — but all four hosts run the identical desktop, so the option would + never take a second value. This matches what `nix-starter-configs` reserves + `modules/` for: abstractions with options, justified by reuse across hosts. + (2) **Flat, not a directory per topic.** At fourteen files a directory level + would add a path component and nothing else. (3) **Comments moved with their + code**, and the three that pointed "above" or "below" across the new file + boundary were rewritten to name the module instead — they had become wrong, + which is the only reason to touch a comment. +- **How**: Eight steps, one module each, ordered from independent to entangled: + browsers, printing, lock, shell, theme, audio, files, network. The last three + are the merges that pull a service declaration out of `services.nix` to join + its packages. `desktop.nix` ends at 194 lines with the compositor, the XDG + config deployment, portals and the packages that belong to no topic; + `services.nix` at 362 with the flake-age check, the hardening profiles and the + user units. + + The refactor had to change nothing, and that was made provable rather than + assumed. The first step already showed the naive check does not work: moving a + package to another module reorders `environment.systemPackages`, which changes + the `system-path` hash, which propagates into every file naming it — the store + path of the system differs although nothing about it does. The criterion is + therefore: `nix store diff-closures` empty, and every differing file identical + once store hashes are normalised away. Two steps also reordered the user + tmpfiles list; that counts as neutral only because no two rules name the same + path, which is checked rather than presumed. Both hosts were built and + compared after each of the eight steps. + +## 2026-09-24 – Firefox preconfiguration through policies + +- **Who**: Dominik, ClaudeCode +- **Why**: Firefox arrived with Betterfox as its preference base, but Betterfox + covers telemetry, the new tab page and sponsored content — nothing of what was + actually wanted on top: uBlock Origin, the Catppuccin theme, no Pocket, no + Sync, autoscroll, DuckDuckGo, no translation offer, no password prompts, a + hidden bookmarks toolbar and about:blank as the home page. A grep over its + user.js finds not one of those. +- **Tradeoffs**: (1) **normal_installed over force_installed** for both add-ons. + Force-installing is the usual choice for an ad blocker, but it also takes away + the ability to remove it, and leaving LibreWolf was precisely about hardening + that could only be undone by hand. (2) **preferencesStatus "default" over the + option's own "locked" default** — locked preferences cannot be changed in + about:config, which would make these three stricter than the 115 Betterfox + values around them; Betterfox rewrites user_pref to defaultPref for that same + reason. (3) **Two policies for Pocket, not one.** DisablePocket is marked + deprecated and, by Mozilla's own note, does not touch the new tab page. Pocket + the service is discontinued; since Firefox 141 the block there is Stories, so + FirefoxHome has to cover the other half. Checked before relying on it: + SearchEngines was ESR-only until Firefox 139 and is now available everywhere, + and all three preferences fall under prefixes the Preferences policy permits — + general.autoScroll is listed by name. +- **How**: `policies` and `preferences` on the existing `programs.firefox` block + in `modules/desktop.nix`. The theme needs two entries rather than one: + ExtensionSettings installs it, `extensions.activeThemeID` selects it, and + without the second Firefox stays on the default look. The add-on ID sits in a + `let` binding because both read it. Verified at eval that the module's own + ExtensionSettings entry for the German language pack survives alongside the two + added ones — the option is a JSON-format type, so attribute sets merge per key. + Two of the settings are starting points rather than fixed states, which the + policies cannot change: DisplayBookmarksToolbar sets the toolbar's initial + state and the View menu still brings it back, and Homepage without `Locked` + leaves about:blank editable. Locking either one was not the point — the reason + for leaving LibreWolf was settings that could not be undone. + +## 2026-09-24 – ungoogled-chromium as the second browser, Firefox as the handler + +- **Who**: Dominik, ClaudeCode +- **Why**: Testing against a Blink engine needs a Chromium next to Firefox. + That makes the default browser a question for the first time: until now + `firefox.desktop` was the only entry in the system path claiming + `x-scheme-handler/http`, so `xdg-open` had exactly one candidate and no + registration was needed. With a second claimant the choice is whatever order + the entries are found in. +- **Tradeoffs**: (1) **Which Chromium** — the ungoogled variant, because stock + `chromium` carries the Google endpoints, and the point here is a rendering + engine, not a second account-bound browser. The usual objection is the build: + ungoogled-chromium is a patch set on Chromium and building it locally takes + hours. Checked before committing to it — all four paths come from the binary + cache (209 MiB download), nothing is built here. Had that not held, stock + `chromium` would have been the answer. (2) **Which types get the default** — + the four `firefox.desktop` declares, no more: `text/html`, + `application/xhtml+xml` and the two scheme handlers. A wildcard would claim + types neither browser should own. +- **How**: `ungoogled-chromium` in `environment.systemPackages` and + `xdg.mime.defaultApplications` in `modules/desktop.nix`, which writes + `/etc/xdg/mimeapps.list`. Verified on the built system: both + `chromium-browser.desktop` and `firefox.desktop` claim http, and the + generated `mimeapps.list` points all four types at Firefox. + +## 2026-09-24 – The browser is Firefox from nixpkgs, hardened by Betterfox + +- **Who**: Dominik, ClaudeCode +- **Why**: LibreWolf ships no language packs, so the interface stays English, + and its hardening is a property of the build: `resistFingerprinting`, + clearing on shutdown and restricted WebGL have to be undone by hand wherever + they get in the way. The 2026-08-20 entry named that price when it chose + LibreWolf; it turned out to be the reason to leave. Firefox from nixpkgs has + `programs.firefox.languagePacks`, and moving the hardening into this repo + makes it a file that can be swapped instead of a property of the browser. +- **Tradeoffs**: (1) **Which preference set** — `arkenfox-userjs` is in + nixpkgs, which would need no input at all, but it sits at `144.0` while the + Firefox from this flake is `156.0`. Betterfox tracks releases more closely + (`154.0`) and is not in nixpkgs, so it comes in as an input with + `flake = false`, the same shape as `moonarch` and `sweet-cursors`. That way + `nix flake update` moves it and neither version nor hash is kept by hand — + the argument that sank the self-packaged Waterfox on 2026-08-20. (2) **Which + Betterfox file** — the curated `user.js` from the repository root, not the + modules it is assembled from (`Securefox.js` alone is ten times its size). + Leaving LibreWolf's strictness behind is the point of the move, so the + moderate set is the one that fits; switching to the modules later is one line + in the `installPhase`. (3) **How the preferences arrive** — without + home-manager there is no declarative way to place a `user.js` in a profile. + `programs.firefox.autoConfigFiles` is the way in: the wrapper concatenates + each file into `mozilla.cfg`, which Firefox reads before a profile exists. + AutoConfig has no `user_pref`, so the package installs the list twice and + rewrites the copy to `defaultPref` — the shape nixpkgs' `arkenfox-userjs` + uses, and it leaves every value changeable in `about:config`. +- **How**: `betterfox` as a flake input, `pkgs/betterfox.nix` building the two + files, and `programs.firefox` in `modules/desktop.nix` with + `languagePacks = [ "de" ]` and the `user.cfg` in `autoConfigFiles`. + `librewolf` is gone from the package list; the module installs the wrapped + Firefox itself, which is why no browser is named there any more. Verified by + building both hosts: `mozilla.cfg` carries 115 `defaultPref` lines and no + `user_pref`, the language pack policy names `langpack-de@firefox.mozilla.org`, + and nothing called LibreWolf is left in the system path. The language pack is + not in the store — the policy has Firefox fetch it from releases.mozilla.org + on first start, so without a network the interface stays English until then. + ## 2026-09-23 – The ThinkPad keeps amdgpu out of the initrd so LUKS stays readable - **Who**: Dominik, ClaudeCode diff --git a/README.md b/README.md index 3989725..8476781 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,19 @@ flake.nix Inputs and outputs pkgs/ Build recipes sweet-cursors.nix cursor theme moonarch-scripts.nix battery and camera helpers + betterfox.nix Firefox preference set modules/ user.nix moonarch.user option, set per host disk.nix partitioning: GPT, optional LUKS2, btrfs subvolumes - desktop.nix /etc deployment, package set, theme + desktop.nix compositor, /etc deployment, portals, the rest of the packages + theme.nix GTK, Qt, Kvantum, cursors, fonts + browsers.nix Firefox with its preconfiguration, Chromium, handler + shell.nix zsh and the command line tools + lock.nix gtklock and its PAM stack + audio.nix PipeWire and the RNNoise denoising + files.nix thunar, thumbnails, mounting + network.nix NetworkManager VPN plugin, Bluetooth, firewall + printing.nix driverless CUPS and Avahi greetd.nix greetd + regreet, autologin per host services.nix systemd system and user services zed.nix Zed editor, nix-ld, seeded user config @@ -35,7 +44,7 @@ hosts/ ``` `nixosModules.moonarch` is the entry point: it imports disko's NixOS module -alongside the six above and hands them the packages and the config source. +alongside the fourteen above and hands them the packages and the config source. ## Usage diff --git a/flake.lock b/flake.lock index 639eab4..17f582b 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,21 @@ { "nodes": { + "betterfox": { + "flake": false, + "locked": { + "lastModified": 1788713351, + "narHash": "sha256-mIP/WcXUcGrJsWCJzR4zqPOmt0BbbpTZVaN/MbIwBbw=", + "owner": "yokoffing", + "repo": "Betterfox", + "rev": "067172a4b0dc90e78e5b8b94d9abfe6430c6a7be", + "type": "github" + }, + "original": { + "owner": "yokoffing", + "repo": "Betterfox", + "type": "github" + } + }, "disko": { "inputs": { "nixpkgs": [ @@ -24,11 +40,11 @@ "moonarch": { "flake": false, "locked": { - "lastModified": 1789404100, - "narHash": "sha256-Hirv+gCdFDqY79CZZ0gd2QQsGGXM1E/rfTTebdUTrw4=", + "lastModified": 1790177314, + "narHash": "sha256-2YTKIvK8gtoDf+zE0QyLyoWT8KQW82O0WQ4pWkHgX6g=", "ref": "refs/heads/main", - "rev": "a172f90c1c00d9c8f71e6403a2cdf39e667ca85a", - "revCount": 187, + "rev": "2774d0947de08376651c05fb1b68907c7e3ffd61", + "revCount": 188, "type": "git", "url": "https://gitea.moonarch.de/nevaforget/moonarch.git" }, @@ -86,6 +102,7 @@ }, "root": { "inputs": { + "betterfox": "betterfox", "disko": "disko", "moonarch": "moonarch", "nixos-hardware": "nixos-hardware", diff --git a/flake.nix b/flake.nix index 3a75569..0c66182 100644 --- a/flake.nix +++ b/flake.nix @@ -14,6 +14,14 @@ inputs.nixpkgs.follows = "nixpkgs"; }; + # Firefox preference set, consumed by pkgs/betterfox.nix. Not in nixpkgs; + # carried as an input so `nix flake update` moves it and neither version + # nor hash have to be kept by hand. + betterfox = { + url = "github:yokoffing/Betterfox"; + flake = false; + }; + # Project sources. These repos are not flakes themselves, they are # consumed as plain source trees following their default branch. moonarch = { @@ -32,6 +40,11 @@ pkgs = nixpkgs.legacyPackages.${system}; moonarchPackages = { + betterfox = pkgs.callPackage ./pkgs/betterfox.nix { + src = inputs.betterfox; + version = "154.0-unstable"; + }; + sweet-cursors = pkgs.callPackage ./pkgs/sweet-cursors.nix { src = inputs.sweet-cursors; version = "0-unstable"; @@ -90,6 +103,14 @@ ./modules/user.nix ./modules/disk.nix ./modules/desktop.nix + ./modules/browsers.nix + ./modules/printing.nix + ./modules/lock.nix + ./modules/shell.nix + ./modules/theme.nix + ./modules/audio.nix + ./modules/files.nix + ./modules/network.nix ./modules/greetd.nix ./modules/services.nix ./modules/zed.nix diff --git a/modules/audio.nix b/modules/audio.nix new file mode 100644 index 0000000..ecf8e08 --- /dev/null +++ b/modules/audio.nix @@ -0,0 +1,49 @@ +# ABOUTME: PipeWire and the RNNoise input denoising from the shared config. +# ABOUTME: Both halves in one place; they used to sit in two separate modules. + +{ pkgs, moonarchSrc, ... }: + +let + xdg = "${moonarchSrc}/defaults/xdg"; + + # 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, which points at nothing here. Substituted to the + # store path, the way modules/desktop.nix does it for the wallpaper. 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" + ''; +in +{ + services.pipewire = { + enable = true; + alsa.enable = true; + alsa.support32Bit = true; + pulse.enable = true; + jack.enable = true; + + # 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. + configPackages = [ denoisingConfig ]; + }; + + # 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. + environment.systemPackages = with pkgs; [ + pavucontrol + alsa-utils + ]; +} diff --git a/modules/browsers.nix b/modules/browsers.nix new file mode 100644 index 0000000..dd19c87 --- /dev/null +++ b/modules/browsers.nix @@ -0,0 +1,111 @@ +# ABOUTME: Firefox with its preconfiguration, and Chromium as the second browser. +# ABOUTME: Carries the default-handler registration the two of them make necessary. + +{ pkgs, moonarchPkgs, ... }: + +let + # The Firefox theme's add-on ID, read twice below: once to install the theme, + # once to select it. This is upstream Catppuccin's own build; the similarly + # named catppuccin-mocha on the same store belongs to a third party. + catppuccinThemeId = "{8446b178-c865-4f5c-8ccc-1d7887811ae3}"; +in +{ + # Browser. moonarch runs waterfox-bin, which nixpkgs does not carry; see + # DECISIONS.md for why this is not packaged here. + # + # The module puts the wrapped Firefox into environment.systemPackages itself, + # which is why the package list below names only the second browser. + # autoConfigFiles is what carries Betterfox: the wrapper concatenates every + # file named here into mozilla.cfg, which Firefox reads before a profile + # exists — the only way in without home-manager. + programs.firefox = { + enable = true; + languagePacks = [ "de" ]; + autoConfigFiles = [ "${moonarchPkgs.betterfox}/user.cfg" ]; + + # Betterfox rewrites user_pref to defaultPref so that each of its 115 values + # stays changeable in about:config. This option defaults to "locked", which + # would nail these three down harder than the set they sit in. + preferencesStatus = "default"; + preferences = { + "general.autoScroll" = true; + + # Only the offer to translate. Translating by hand from the context menu + # keeps working. + "browser.translations.automaticallyPopup" = false; + + # ExtensionSettings installs the theme but does not select it; without + # this, Firefox starts on the default look with Catppuccin sitting unused + # in the add-on manager. + "extensions.activeThemeID" = catppuccinThemeId; + }; + + # normal_installed rather than force_installed: both add-ons stay removable. + # Leaving LibreWolf was about hardening that could only be undone by hand. + # + # The latest.xpi URLs redirect to whatever the current version is, so + # neither add-on needs a version bump here. + policies = { + ExtensionSettings = { + "uBlock0@raymondhill.net" = { + installation_mode = "normal_installed"; + install_url = "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi"; + }; + ${catppuccinThemeId} = { + installation_mode = "normal_installed"; + install_url = "https://addons.mozilla.org/firefox/downloads/latest/catppuccin-mocha-lavender-git/latest.xpi"; + }; + }; + + SearchEngines.Default = "DuckDuckGo"; + OfferToSaveLogins = false; + + # Startup and the home button both land on a blank page. StartPage + # "homepage" is what makes the URL apply to startup as well; "none" would + # reach the same screen but leave the home button pointing elsewhere. + Homepage = { + URL = "about:blank"; + StartPage = "homepage"; + }; + + # A new tab opens blank as well, rather than on the Firefox start page + # with its search field and shortcuts. + NewTabPage = false; + + # The initial state only — the toolbar stays something you can bring back + # from the View menu. There is no policy that locks it away. + DisplayBookmarksToolbar = "never"; + + # This is the Sync policy. There is no DisableSync: the CCK2 name of this + # one is disableSync, and it sets identity.fxaccounts.enabled. + DisableFirefoxAccounts = true; + + # DisablePocket is deprecated and, by Mozilla's own note, leaves the new + # tab page alone. FirefoxHome covers that half: Stories replaced Pocket + # there in Firefox 141, after the service was discontinued. + DisablePocket = true; + FirefoxHome = { + Stories = false; + SponsoredStories = false; + }; + }; + }; + + # Both browsers claim x-scheme-handler/http in their desktop entries, and + # with two candidates xdg-open picks by whatever order it finds them in. + # These four types are exactly what firefox.desktop declares. + xdg.mime.defaultApplications = { + "text/html" = "firefox.desktop"; + "application/xhtml+xml" = "firefox.desktop"; + "x-scheme-handler/http" = "firefox.desktop"; + "x-scheme-handler/https" = "firefox.desktop"; + }; + + environment.systemPackages = [ + # Second browser, for testing against a Blink engine. The ungoogled + # variant because the stock chromium carries the Google endpoints; it is + # fully substitutable, so the patch set does not have to be built here. + # Firefox comes from programs.firefox above and is the default handler. + pkgs.ungoogled-chromium + ]; +} diff --git a/modules/desktop.nix b/modules/desktop.nix index 3fc3225..43b739b 100644 --- a/modules/desktop.nix +++ b/modules/desktop.nix @@ -1,7 +1,7 @@ # ABOUTME: Deploys the XDG configs from the moonarch repo and the package set. -# ABOUTME: Niri, Quickshell, terminal, theming, portals. +# ABOUTME: Niri, Quickshell, terminal, portals and the packages without a theme. -{ pkgs, lib, moonarchSrc, moonarchPkgs, moonarchThemes, ... }: +{ pkgs, moonarchSrc, moonarchPkgs, moonarchThemes, ... }: let xdg = "${moonarchSrc}/defaults/xdg"; @@ -16,7 +16,6 @@ let ''; wallpaper = "${moonarchSrc}/defaults/backgrounds/wallpaper.jpg"; - wallpaperBlur = "${moonarchSrc}/defaults/backgrounds/wallpaper-blur.jpg"; # The FHS path the wallpaper is published under, provided by the tmpfiles # link further down. waypaper's config names /usr/share/moonarch/wallpaper.jpg @@ -32,56 +31,6 @@ let --replace-fail /usr/share/moonarch/wallpaper.jpg ${wallpaperPath} ''; - # gtklock reads $XDG_CONFIG_HOME/gtklock/config.ini and gtklock/config.ini - # under every $XDG_CONFIG_DIRS entry, so /etc/xdg is enough and nothing has - # to be seeded into the home directory — the one thing swaylock could not do, - # its sysconfdir being inside the store. - # - # The background stays the pre-blurred image. gtklock cannot blur either: - # GTK3 has no `filter` CSS property at all (checked against - # libgtk-3.so.0.2420.32 — it knows `background-image` and `box-shadow`, and - # neither `filter` nor `backdrop-filter`), and it takes no screenshot of the - # session, so there is nothing to blur at lock time in the first place. - # - # No gtk-theme key: gtklock is GTK3 and reads the session theme out of - # /etc/xdg/gtk-3.0/settings.ini, so the theme name keeps living in one place. - # - # Modules are named by absolute store path. `-m` resolves a bare name against - # the directory compiled into gtklock, which under Nix is gtklock's own store - # path and contains no module at all. powerbar needs no configuration: its - # built-in commands are `systemctl reboot|-i poweroff|suspend`, and systemd - # is on the PATH the stasis unit sets in modules/services.nix. - gtklockConfig = pkgs.writeText "gtklock-config.ini" '' - [main] - background=${wallpaperBlur} - modules=${moduleList} - ''; - - moduleList = lib.concatStringsSep ";" [ - "${pkgs.gtklock-userinfo-module}/lib/gtklock/userinfo-module.so" - "${pkgs.gtklock-powerbar-module}/lib/gtklock/powerbar-module.so" - ]; - - # The GTK theme name, in the one place both consumers below read it from. - gtkTheme = "catppuccin-mocha-lavender-standard"; - - # Both distributions ship the same Catppuccin GTK theme, 1.0.3, and still - # disagree on its name — permanently, not as a migration. Arch installs the - # upstream release zips through catppuccin-gtk-theme-mocha, whose directories - # carry the full build name with the `+default` tweak marker; nixpkgs builds - # the same source with no tweaks and so names it without that suffix. Neither - # side can adopt the other's name without going out of its way. - # - # The rewrite therefore has to stay. It matters because GTK3 reads - # settings.ini directly — the dconf entry further down only reaches - # applications that ask GSettings, which on Wayland the GTK3 ones do not — so - # a name that does not resolve puts every GTK3 application on Adwaita, with - # no error anywhere. - gtkSettings = version: pkgs.runCommand "gtk${version}-settings.ini" { } '' - substitute ${xdg}/gtk-${version}/settings.ini "$out" \ - --replace-fail catppuccin-mocha-lavender-standard+default ${gtkTheme} - ''; - # niri prefers ~/.config/niri/config.kdl over /etc/niri/config.kdl and does not # merge the two, so a home config would otherwise replace the whole deployed # one. This seed pulls the system config back in through `include`, which niri @@ -95,52 +44,6 @@ let niriUserConfig = pkgs.writeText "niri-config.kdl" '' include "/etc/niri/config.kdl" ''; - - # 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, which points at 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" - ''; - - # The theme deployed Kvantum/kvantum.kvconfig selects. It goes into - # systemPackages: nixpkgs patches Kvantum to search XDG_DATA_DIRS for themes - # (tsujan/Kvantum 01989083, in the package's patches list), and - # /run/current-system/sw/share is on that path, so a theme package is found - # system-wide with nothing seeded into the home directory. - # - # Verified under strace: a Qt6 client opens the theme's .kvconfig straight - # out of the package's store path. The patch only takes in the Qt6 build - # (NixOS/nixpkgs#355277) — a Qt5 client, here only VLC, falls back to the - # default style. - kvantumTheme = pkgs.catppuccin-kvantum.override { - accent = "lavender"; - variant = "mocha"; - }; - - # 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: moonarch's own copy of this - # file sources the shared zshrc, which here would run it 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/ --- @@ -157,20 +60,9 @@ in "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"; - "xdg/Kvantum/kvantum.kvconfig".source = "${xdg}/Kvantum/kvantum.kvconfig"; - "xdg/qt6ct/qt6ct.conf".source = "${xdg}/qt6ct/qt6ct.conf"; - # moonarch ships defaults/xdg/gtklock/config.ini, but it is not deployed - # here: it names /usr/share/moonarch for the background and resolves the - # modules by bare name against /usr/lib/gtklock. Both are store paths under - # Nix, so the file is generated above rather than copied — the same reason - # PipeWire's config takes its own route. - "xdg/gtklock/config.ini".source = gtklockConfig; - "xdg/gtk-3.0/settings.ini".source = gtkSettings "3.0"; - "xdg/gtk-4.0/settings.ini".source = gtkSettings "4.0"; # 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. + # in modules/audio.nix instead. # Kept for layout parity with moonarch's tree; waypaper itself only reads # the copy in the home directory, seeded below. "xdg/waypaper/config.ini".source = waypaperConfig; @@ -183,34 +75,6 @@ in "${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; @@ -232,118 +96,11 @@ in "L+ ${wallpaperPath} - - - - ${wallpaper}" ]; - # --- Shell --- - programs.zsh.enable = true; - - # moonarch's shared zshrc, which it 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 lookups are - # guarded and silently find nothing here. These modules source the same - # plugins from the store instead, with the strategy the zshrc sets. - 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 = gtkTheme; - icon-theme = "Colloid-Grey-Catppuccin-Dark"; - color-scheme = "prefer-dark"; - font-name = "UbuntuSans Nerd Font 11"; - }; - }]; - }; - - # Qt applications are wrapped, so each one looks for plugins in its own - # closure and never finds a style installed alongside it — which is why VLC - # kept coming up in the stock Qt style. Enabling this puts the profile's Qt5 - # and Qt6 plugin directories on QT_PLUGIN_PATH, which the wrappers append to. - # platformTheme and style stay null: it would add packages that are already - # listed below, and niri's environment block sets both variables already. - qt.enable = true; - # --- 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 @@ -352,25 +109,8 @@ in xdg-terminal-exec # Own programs - moonarchPkgs.sweet-cursors moonarchPkgs.moonarch-scripts - # Browser. moonarch 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. - # gtklock speaks the same ext-session-lock-v1 as swaylock, through - # gtk-session-lock, so a crash still leaves the screen locked. The modules - # are the reason for it: avatar and clock, and the power buttons. They are - # installed for the sake of `nix-store --query --roots`; gtklock itself - # loads them from the store paths named in the config above. playerctl is - # deliberately absent — it needs playerctld, which nothing here runs. - gtklock - gtklock-userinfo-module - gtklock-powerbar-module - - # Compositor extras xwayland-satellite # Bar, notifications, clipboard. @@ -387,15 +127,14 @@ in # parent's cgroup, so everything started from the launcher lands inside # quickshell.service — which systemd kills whole on every restart of the # unit (KillMode=control-group is the default). A crash of the bar took the - # open browser with it, and Librewolf counted each one as a crash until it - # stopped restoring the session at all. app2unit wraps systemd-run to put + # open browser with it, which counted each one as a crash until it stopped + # restoring the session at all. app2unit wraps systemd-run to put # each application in its own transient app-*.scope, the same shape niri # gives the windows it spawns itself. app2unit # Display and wallpaper wlsunset - nwg-look brightnessctl awww swww-compat @@ -403,57 +142,10 @@ in # 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 - kvantumTheme - 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 upower polkit_gnome @@ -465,14 +157,12 @@ in galculator ]; - # Without this gtklock takes the session lock and then rejects every - # password: there would be no /etc/pam.d/gtklock, and /etc/pam.d/other denies - # auth outright. The generated stack is `auth sufficient pam_unix.so`, the - # same one swaylock had. - security.pam.services.gtklock = {}; - # Polkit agent for privilege prompts inside the session. security.polkit.enable = true; + # The setuid pkexec wrapper is opt-in and off by default. Without it pkexec + # refuses to run at all, which takes the camera and battery toggles with it — + # both reach their privileged half through pkexec. + security.polkit.enablePkexecWrapper = true; # nixpkgs.config.allowUnfree covers the system build only. `nix profile` and # friends evaluate a flake purely, so they see neither that setting nor @@ -482,26 +172,6 @@ in # Set by PAM at session start, so it takes a re-login, not just a rebuild. environment.sessionVariables.NIXPKGS_ALLOW_UNFREE = "1"; - # niri sets both in its own environment block, but that only reaches what niri - # itself spawns. Anything under the systemd user manager — quickshell, and so - # everything started from its launcher — never sees them, and Qt applications - # come up unstyled. Set here they travel through /etc/profile into - # niri-session's import-environment, and from there into the user manager. - environment.sessionVariables.QT_QPA_PLATFORMTHEME = "qt6ct"; - environment.sessionVariables.QT_STYLE_OVERRIDE = "kvantum"; - - # Same gap, cursors: niri's cursor block reaches xwayland-satellite, so X11 - # clients have a themed pointer available, but everything under the user - # manager arrives with XCURSOR_PATH alone and has no theme name to look up. - # The name matches modules/greetd.nix, so greeter and session agree. - environment.sessionVariables.XCURSOR_THEME = "Sweet-cursors"; - environment.sessionVariables.XCURSOR_SIZE = "24"; - - # systemPackages alone does not put the theme on XDG_DATA_DIRS: the system - # path links only the /share subdirectories named here, and Kvantum's is not - # among the defaults. - environment.pathsToLink = [ "/share/Kvantum" ]; - # 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 moonarch's post-install.sh does imperatively. @@ -520,14 +190,5 @@ in # nwg-displays needs this file to exist before it will start, and niri would # otherwise ignore the monitor.kdl it writes next to it. "C %h/.config/niri/config.kdl 0644 - - - ${niriUserConfig}" - "C %h/.zshrc 0644 - - - ${userZshrc}" - # Steam gets XCURSOR_THEME from the block above — verified in its own - # environment — and still drew the core pointer, so parts of it resolve the - # theme literally named 'default' rather than the variable. Xcursor looks - # that name up along XCURSOR_PATH like any other, and ~/.icons is first, - # so pointing it at the theme covers those lookups too. 'L+' replaces the - # link on every activation, unlike the 'C' rules above, which keeps it - # following the theme instead of freezing at today's one. - "L+ %h/.icons/default - - - - /run/current-system/sw/share/icons/Sweet-cursors" ]; } diff --git a/modules/files.nix b/modules/files.nix new file mode 100644 index 0000000..3f4d37e --- /dev/null +++ b/modules/files.nix @@ -0,0 +1,33 @@ +# ABOUTME: The file manager stack: thunar, its plugins, thumbnails and mounting. +# ABOUTME: The services half used to live in modules/services.nix. + +{ pkgs, ... }: + +{ + # 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; + + # Mounting and device discovery for thunar's side pane. + services.udisks2.enable = true; + services.gvfs.enable = true; + + environment.systemPackages = with pkgs; [ + # 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. The plugin looks up a .tap helper by the handler's desktop id + # inside its own store path, and it ships one for file-roller. xarchiver + # carries its .tap in its own package, where the plugin never looks. + file-roller + zip + unzip + ]; +} diff --git a/modules/lock.nix b/modules/lock.nix new file mode 100644 index 0000000..6bb8bad --- /dev/null +++ b/modules/lock.nix @@ -0,0 +1,65 @@ +# ABOUTME: The session lock: gtklock, its two modules and the PAM stack it needs. +# ABOUTME: Its config is generated because moonarch's copy names FHS paths. + +{ pkgs, lib, moonarchSrc, ... }: + +let + wallpaperBlur = "${moonarchSrc}/defaults/backgrounds/wallpaper-blur.jpg"; + + # gtklock reads $XDG_CONFIG_HOME/gtklock/config.ini and gtklock/config.ini + # under every $XDG_CONFIG_DIRS entry, so /etc/xdg is enough and nothing has + # to be seeded into the home directory — the one thing swaylock could not do, + # its sysconfdir being inside the store. + # + # The background stays the pre-blurred image. gtklock cannot blur either: + # GTK3 has no `filter` CSS property at all (checked against + # libgtk-3.so.0.2420.32 — it knows `background-image` and `box-shadow`, and + # neither `filter` nor `backdrop-filter`), and it takes no screenshot of the + # session, so there is nothing to blur at lock time in the first place. + # + # No gtk-theme key: gtklock is GTK3 and reads the session theme out of + # /etc/xdg/gtk-3.0/settings.ini, so the theme name keeps living in one place. + # + # Modules are named by absolute store path. `-m` resolves a bare name against + # the directory compiled into gtklock, which under Nix is gtklock's own store + # path and contains no module at all. powerbar needs no configuration: its + # built-in commands are `systemctl reboot|-i poweroff|suspend`, and systemd + # is on the PATH the stasis unit sets in modules/services.nix. + gtklockConfig = pkgs.writeText "gtklock-config.ini" '' + [main] + background=${wallpaperBlur} + modules=${moduleList} + ''; + + moduleList = lib.concatStringsSep ";" [ + "${pkgs.gtklock-userinfo-module}/lib/gtklock/userinfo-module.so" + "${pkgs.gtklock-powerbar-module}/lib/gtklock/powerbar-module.so" + ]; +in +{ + # moonarch ships defaults/xdg/gtklock/config.ini, but it is not deployed + # here: it names /usr/share/moonarch for the background and resolves the + # modules by bare name against /usr/lib/gtklock. Both are store paths under + # Nix, so the file is generated above rather than copied — the same reason + # PipeWire's config takes its own route. + environment.etc."xdg/gtklock/config.ini".source = gtklockConfig; + + # Session lock. The power menu is a Quickshell popout, not a package. + # gtklock speaks the same ext-session-lock-v1 as swaylock, through + # gtk-session-lock, so a crash still leaves the screen locked. The modules + # are the reason for it: avatar and clock, and the power buttons. They are + # installed for the sake of `nix-store --query --roots`; gtklock itself + # loads them from the store paths named in the config above. playerctl is + # deliberately absent — it needs playerctld, which nothing here runs. + environment.systemPackages = with pkgs; [ + gtklock + gtklock-userinfo-module + gtklock-powerbar-module + ]; + + # Without this gtklock takes the session lock and then rejects every + # password: there would be no /etc/pam.d/gtklock, and /etc/pam.d/other denies + # auth outright. The generated stack is `auth sufficient pam_unix.so`, the + # same one swaylock had. + security.pam.services.gtklock = { }; +} diff --git a/modules/network.nix b/modules/network.nix new file mode 100644 index 0000000..f8c0e5e --- /dev/null +++ b/modules/network.nix @@ -0,0 +1,27 @@ +# ABOUTME: NetworkManager's OpenVPN half, Bluetooth, and the firewall. +# ABOUTME: The service declarations used to sit in modules/services.nix. + +{ pkgs, ... }: + +{ + # 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 ]; + + hardware.bluetooth.enable = true; + services.blueman.enable = true; + + # Replaces the ufw calls in post-install.sh. + networking.firewall.enable = true; + + environment.systemPackages = with pkgs; [ + # openvpn is the binary the NetworkManager plugin above drives; it also + # stands on its own for a config file. + networkmanagerapplet + blueman + openvpn + ]; +} diff --git a/modules/printing.nix b/modules/printing.nix new file mode 100644 index 0000000..c0241ed --- /dev/null +++ b/modules/printing.nix @@ -0,0 +1,24 @@ +# ABOUTME: Driverless printing over IPP, and the Avahi half that discovers it. +# ABOUTME: No printer and no driver package is declared here. + +{ ... }: + +{ + # 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; + }; +} diff --git a/modules/services.nix b/modules/services.nix index 34c854d..7e67834 100644 --- a/modules/services.nix +++ b/modules/services.nix @@ -157,21 +157,8 @@ let }; in { - # --- Audio --- - services.pipewire = { - enable = true; - alsa.enable = true; - alsa.support32Bit = true; - pulse.enable = true; - jack.enable = true; - }; - # --- Hardware --- - hardware.bluetooth.enable = true; - services.blueman.enable = true; - services.udisks2.enable = true; services.upower.enable = true; - services.gvfs.enable = true; services.fwupd.enable = true; # power-profiles-daemon over auto-cpufreq: on amd-pstate in active mode the # platform decides the frequency from the min/max and EPP registers alone, so @@ -179,9 +166,6 @@ in # where neither exists it falls back to a driver that touches no hardware. services.power-profiles-daemon.enable = true; - # --- Firewall, replaces the ufw calls in post-install.sh --- - networking.firewall.enable = true; - # --- System services from defaults/etc/systemd/system/ --- # # Rebuilt rather than copied, same reason as the user units: the originals diff --git a/modules/shell.nix b/modules/shell.nix new file mode 100644 index 0000000..5861741 --- /dev/null +++ b/modules/shell.nix @@ -0,0 +1,80 @@ +# ABOUTME: zsh as the interactive shell, sourcing moonarch's shared zshrc. +# ABOUTME: Carries the command line tools that zshrc and the helper scripts call. + +{ pkgs, moonarchSrc, ... }: + +let + xdg = "${moonarchSrc}/defaults/xdg"; + + # 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: moonarch's own copy of this + # file sources the shared zshrc, which here would run it 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 +{ + environment.etc."xdg/fastfetch/config.jsonc".source = "${xdg}/fastfetch/config.jsonc"; + + programs.zsh.enable = true; + + # moonarch's shared zshrc, which it 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 lookups are + # guarded and silently find nothing here. These modules source the same + # plugins from the store instead, with the strategy the zshrc sets. + 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; + }; + + # CLI tools referenced by zshrc and the helper scripts + environment.systemPackages = with pkgs; [ + bat + btop + eza + fastfetch + fd + fzf + jq + lazygit + ripgrep + neovim + git + ]; + + # zsh/newuser runs its setup wizard on every interactive login for as long as + # the user has none of .zshenv, .zprofile, .zshrc or .zlogin, so the seed + # above has to reach the home directory. 'C' copies once and never + # overwrites, so user edits survive. + systemd.user.tmpfiles.rules = [ + "C %h/.zshrc 0644 - - - ${userZshrc}" + ]; +} diff --git a/modules/theme.nix b/modules/theme.nix new file mode 100644 index 0000000..d9d9d27 --- /dev/null +++ b/modules/theme.nix @@ -0,0 +1,152 @@ +# ABOUTME: GTK, Qt, Kvantum, cursors and fonts — the look, in one place. +# ABOUTME: Same theme derivations the greeter uses, so the two cannot drift apart. + +{ pkgs, moonarchSrc, moonarchPkgs, moonarchThemes, ... }: + +let + xdg = "${moonarchSrc}/defaults/xdg"; + + # The GTK theme name, in the one place both consumers below read it from. + gtkTheme = "catppuccin-mocha-lavender-standard"; + + # Both distributions ship the same Catppuccin GTK theme, 1.0.3, and still + # disagree on its name — permanently, not as a migration. Arch installs the + # upstream release zips through catppuccin-gtk-theme-mocha, whose directories + # carry the full build name with the `+default` tweak marker; nixpkgs builds + # the same source with no tweaks and so names it without that suffix. Neither + # side can adopt the other's name without going out of its way. + # + # The rewrite therefore has to stay. It matters because GTK3 reads + # settings.ini directly — the dconf entry further down only reaches + # applications that ask GSettings, which on Wayland the GTK3 ones do not — so + # a name that does not resolve puts every GTK3 application on Adwaita, with + # no error anywhere. + gtkSettings = version: pkgs.runCommand "gtk${version}-settings.ini" { } '' + substitute ${xdg}/gtk-${version}/settings.ini "$out" \ + --replace-fail catppuccin-mocha-lavender-standard+default ${gtkTheme} + ''; + + # The theme deployed Kvantum/kvantum.kvconfig selects. It goes into + # systemPackages: nixpkgs patches Kvantum to search XDG_DATA_DIRS for themes + # (tsujan/Kvantum 01989083, in the package's patches list), and + # /run/current-system/sw/share is on that path, so a theme package is found + # system-wide with nothing seeded into the home directory. + # + # Verified under strace: a Qt6 client opens the theme's .kvconfig straight + # out of the package's store path. The patch only takes in the Qt6 build + # (NixOS/nixpkgs#355277) — a Qt5 client, here only VLC, falls back to the + # default style. + kvantumTheme = pkgs.catppuccin-kvantum.override { + accent = "lavender"; + variant = "mocha"; + }; +in +{ + environment.etc = { + "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 = gtkSettings "3.0"; + "xdg/gtk-4.0/settings.ini".source = gtkSettings "4.0"; + }; + + # 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 + + + + ''; + + # Set systemwide instead of through the gsettings calls in post-install.sh. + programs.dconf = { + enable = true; + profiles.user.databases = [{ + settings."org/gnome/desktop/interface" = { + gtk-theme = gtkTheme; + icon-theme = "Colloid-Grey-Catppuccin-Dark"; + color-scheme = "prefer-dark"; + font-name = "UbuntuSans Nerd Font 11"; + }; + }]; + }; + + # Qt applications are wrapped, so each one looks for plugins in its own + # closure and never finds a style installed alongside it — which is why VLC + # kept coming up in the stock Qt style. Enabling this puts the profile's Qt5 + # and Qt6 plugin directories on QT_PLUGIN_PATH, which the wrappers append to. + # platformTheme and style stay null: it would add packages that are already + # listed below, and niri's environment block sets both variables already. + qt.enable = true; + + fonts.packages = with pkgs; [ + nerd-fonts.ubuntu-sans + openmoji-color + ]; + + # niri sets both in its own environment block, but that only reaches what niri + # itself spawns. Anything under the systemd user manager — quickshell, and so + # everything started from its launcher — never sees them, and Qt applications + # come up unstyled. Set here they travel through /etc/profile into + # niri-session's import-environment, and from there into the user manager. + environment.sessionVariables.QT_QPA_PLATFORMTHEME = "qt6ct"; + environment.sessionVariables.QT_STYLE_OVERRIDE = "kvantum"; + + # Same gap, cursors: niri's cursor block reaches xwayland-satellite, so X11 + # clients have a themed pointer available, but everything under the user + # manager arrives with XCURSOR_PATH alone and has no theme name to look up. + # The name matches modules/greetd.nix, so greeter and session agree. + environment.sessionVariables.XCURSOR_THEME = "Sweet-cursors"; + environment.sessionVariables.XCURSOR_SIZE = "24"; + + # systemPackages alone does not put the theme on XDG_DATA_DIRS: the system + # path links only the /share subdirectories named here, and Kvantum's is not + # among the defaults. + environment.pathsToLink = [ "/share/Kvantum" ]; + + systemd.user.tmpfiles.rules = [ + # Steam gets XCURSOR_THEME from the block above — verified in its own + # environment — and still drew the core pointer, so parts of it resolve the + # theme literally named 'default' rather than the variable. Xcursor looks + # that name up along XCURSOR_PATH like any other, and ~/.icons is first, + # so pointing it at the theme covers those lookups too. 'L+' replaces the + # link on every activation, unlike the 'C' rules elsewhere, which keeps it + # following the theme instead of freezing at today's one. + "L+ %h/.icons/default - - - - /run/current-system/sw/share/icons/Sweet-cursors" + ]; + + environment.systemPackages = with pkgs; [ + moonarchPkgs.sweet-cursors + + # Theming. Same derivations the greeter uses, see flake.nix. + moonarchThemes.gtk + moonarchThemes.icons + libsForQt5.qtstyleplugin-kvantum + qt6Packages.qtstyleplugin-kvantum + kvantumTheme + qt6Packages.qt6ct + qt6.qt5compat + + # GTK settings editor; it writes the same settings.ini this module deploys. + nwg-look + ]; +} diff --git a/pkgs/betterfox.nix b/pkgs/betterfox.nix new file mode 100644 index 0000000..d625406 --- /dev/null +++ b/pkgs/betterfox.nix @@ -0,0 +1,32 @@ +# ABOUTME: Nix build recipe for the Betterfox preference set. +# ABOUTME: Ships the curated user.js, plus an autoconfig copy for the Firefox wrapper. + +{ lib, stdenvNoCC, src, version }: + +stdenvNoCC.mkDerivation { + pname = "betterfox"; + inherit version src; + + dontBuild = true; + dontConfigure = true; + + # The same list twice. user.js is the upstream file, for dropping into a + # profile by hand. user.cfg is what programs.firefox.autoConfigFiles takes: + # the wrapper concatenates it into mozilla.cfg, where user_pref does not + # exist — defaultPref is its counterpart there and leaves every value + # changeable in about:config. Same shape as nixpkgs' arkenfox-userjs. + installPhase = '' + runHook preInstall + install -Dm644 user.js "$out/user.js" + install -Dm644 user.js "$out/user.cfg" + substituteInPlace "$out/user.cfg" --replace-fail user_pref defaultPref + runHook postInstall + ''; + + meta = { + description = "Curated Firefox preference set"; + homepage = "https://github.com/yokoffing/Betterfox"; + license = lib.licenses.mit; + platforms = lib.platforms.all; + }; +}