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; + }; +}