fix: real-hardware QA sweep — wifi, fonts, firmware, Hyprland 0.55, offline switching

Roadmap items 1+2 plus the issues found testing on a Latitude 5410:

- Ship the 10 most popular Nerd Fonts by default (iosevka swapped for
  mononoki: 1.1 GB vs 27 MB) and warn from nomarchy-theme-sync when a
  configured fonts.mono/fonts.ui family isn't installed (fc-match) —
  fontconfig substitutes silently otherwise.
- hardware.enableRedistributableFirmware: installed systems shipped NO
  firmware blobs (wifi/SOF audio/BT) — nixos-generate-config only emits
  microcode lines referencing this flag, it never sets it.
- Live ISO wifi: networking.wireless.enable = mkForce false killed
  NetworkManager's supplicant — since 26.05 the NM module drives its
  wifi backend THROUGH networking.wireless (dbusControlled). Devices
  vanished from nmtui with iwlwifi loaded and no rfkill block.
- Hyprland 0.55: launch sessions via start-hyprland (watchdog; bare
  binary warns), add the new gesture keyword (workspace_swipe is gone —
  touchpads had no gestures at all), media keys via wpctl/bindel/bindl
  plus the missing mic/play/next/prev binds.
- Offline theme switching: pin mustache-go + stdenv + the repo's own
  standalone HM generation (incl. home-files inputDerivation) into the
  ISO — stylix re-renders base16 templates per switch and an offline
  `apply` cascaded into building stdenv from source (1107 drvs).
  Verified with tools/vm/gap-analysis.py: 17 config drvs, zero fetches.
- Stylix: track release-26.05 (kills the HM version-skew warning and
  the stale home-manager follows), lock updated.
- Roadmap: swaync (no notification daemon ships today).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bernardo Magri
2026-06-12 17:50:55 +01:00
parent bc5faec6b8
commit dccceb4c48
7 changed files with 117 additions and 22 deletions

View File

@@ -230,8 +230,6 @@ workspaces · `Print` region screenshot.
`tools/import-palettes.py <palettes-dir> themes/`. `tools/import-palettes.py <palettes-dir> themes/`.
## Roadmap ## Roadmap
- Fix UI: Install Nerd fonts by default (10 most popular ones) and check if the theme's font is installed and correctly set across the sytem.
- Check if distro is shipping drivers (a test on real hardware was missing wifi driver)
- We need to start creating a menu system (for apps launcher and for theme switching) - We need to start creating a menu system (for apps launcher and for theme switching)
- We need to add networkManager applet to waybar so user can easily connect to internet - We need to add networkManager applet to waybar so user can easily connect to internet
- **Faster switches:** move `backgrounds/` out of the flake source (the 86 MB - **Faster switches:** move `backgrounds/` out of the flake source (the 86 MB
@@ -241,3 +239,4 @@ workspaces · `Print` region screenshot.
- Installer round 2: multi-disk BTRFS RAID, impermanence, BIOS/legacy - Installer round 2: multi-disk BTRFS RAID, impermanence, BIOS/legacy
boot (v1 `nomarchy-install` is single-disk UEFI — see `pkgs/nomarchy-install`) boot (v1 `nomarchy-install` is single-disk UEFI — see `pkgs/nomarchy-install`)
- `hyprlock`/`hypridle`, swayosd, launch-or-focus UX scripts - `hyprlock`/`hypridle`, swayosd, launch-or-focus UX scripts
- swaync notification center (notify-send currently renders bare)

17
flake.lock generated
View File

@@ -208,11 +208,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1780281641, "lastModified": 1779766384,
"narHash": "sha256-M/+hUKoKbHXpV0xGVfELbN1Ds1aoe3pL5p5/t46YhVo=", "narHash": "sha256-P7Ohnlq8b8b2fU+Sgkrej7LBTM60LBTkHleLuYzmLmU=",
"owner": "nix-community", "owner": "nix-community",
"repo": "NUR", "repo": "NUR",
"rev": "30f9ae2f04174de63ba8bcf3580ca90843b28a01", "rev": "57800b7ab648725ccd33551d01484ee14952ad3f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -249,15 +249,16 @@
"tinted-zed": "tinted-zed" "tinted-zed": "tinted-zed"
}, },
"locked": { "locked": {
"lastModified": 1781018772, "lastModified": 1780702455,
"narHash": "sha256-C+cGIUaC6dqfwTbI+BwCd572PbESGA3WYxR1sLTqxkY=", "narHash": "sha256-+srjPGNy67nKytYwdlepycL51IG6S34sS4MKRZXK8G0=",
"owner": "danth", "owner": "nix-community",
"repo": "stylix", "repo": "stylix",
"rev": "a378e4c09031fb15a4d65da88aa628f71fc52f6b", "rev": "54fa19702f4f2c7f6a981a92850678933588af9a",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "danth", "owner": "nix-community",
"ref": "release-26.05",
"repo": "stylix", "repo": "stylix",
"type": "github" "type": "github"
} }

View File

@@ -10,9 +10,12 @@
}; };
stylix = { stylix = {
url = "github:danth/stylix"; # Track the release branch matching nixpkgs/HM — master is built
# against unstable and warns about the version skew on every
# activation. (No home-manager input to follow anymore: stylix
# vendors its HM integration.)
url = "github:nix-community/stylix/release-26.05";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
inputs.home-manager.follows = "home-manager";
}; };
# Pinned by Nomarchy so distro + hardware quirks are tested together # Pinned by Nomarchy so distro + hardware quirks are tested together
@@ -270,6 +273,20 @@
representativeInstall.config.system.build.toplevel representativeInstall.config.system.build.toplevel
template.homeConfigurations.${username}.activationPackage template.homeConfigurations.${username}.activationPackage
# The live session's `home-manager switch` builds the
# REPO's standalone generation (the seeded ~/.nomarchy
# flake) — not the template's, and not the NixOS-module
# one baked into the ISO. It has drvs the other two
# variants don't (user-dbus-services et al.), so pin it
# and its direct build inputs too.
self.homeConfigurations.${username}.activationPackage
self.homeConfigurations.${username}.activationPackage.inputDerivation
# home-files is rebuilt per theme and COPIES some of its
# inputs (user-dbus-services) instead of referencing
# them — the activation closure doesn't carry those, so
# pin home-files' direct build inputs separately.
self.homeConfigurations.${username}.config.home-files.inputDerivation
# A real install rebuilds the drvs that embed the # A real install rebuilds the drvs that embed the
# user's hostname/username/UUIDs (etc, initrd, units, # user's hostname/username/UUIDs (etc, initrd, units,
# toplevel, HM activation). inputDerivation pins each # toplevel, HM activation). inputDerivation pins each
@@ -278,6 +295,15 @@
representativeInstall.config.system.build.etc.inputDerivation representativeInstall.config.system.build.etc.inputDerivation
representativeInstall.config.system.build.initialRamdisk.inputDerivation representativeInstall.config.system.build.initialRamdisk.inputDerivation
template.homeConfigurations.${username}.activationPackage.inputDerivation template.homeConfigurations.${username}.activationPackage.inputDerivation
] ++ [
# A theme switch re-renders stylix's base16 templates
# (gtk.css, kvantum, …) on the machine: the renderer and
# the stdenv its trivial drvs build with must be on
# board, or an offline `apply <theme>` cascades into
# building mustache-go (and eventually stdenv) from
# source. Found with tools/vm/gap-analysis.py.
pkgs.mustache-go
pkgs.stdenv
] ++ [ ] ++ [
# …and the second-level build tools those rebuilds need # …and the second-level build tools those rebuilds need
# (their parents are themselves rebuilt, so parent # (their parents are themselves rebuilt, so parent

View File

@@ -9,9 +9,11 @@
isoImage.volumeID = lib.mkForce "NOMARCHY_LIVE"; isoImage.volumeID = lib.mkForce "NOMARCHY_LIVE";
isoImage.edition = lib.mkForce "live"; isoImage.edition = lib.mkForce "live";
# The minimal-CD profile enables wpa_supplicant; Nomarchy ships # Do NOT touch networking.wireless here: since NixOS 26.05 the
# NetworkManager and the two must not run together. # NetworkManager module drives its wifi backend through it
networking.wireless.enable = lib.mkForce false; # (wireless.enable = true + dbusControlled). Force-disabling it
# kills NM's supplicant — wifi devices vanish from nmtui even
# though the driver is loaded (seen on a Latitude 5410 / AX201).
# ── Live user: no password, straight into the desktop ────────────── # ── Live user: no password, straight into the desktop ──────────────
users.users.${username} = { users.users.${username} = {
@@ -24,7 +26,7 @@
# Boot straight into Hyprland once; logging out lands on tuigreet. # Boot straight into Hyprland once; logging out lands on tuigreet.
services.greetd.settings.initial_session = { services.greetd.settings.initial_session = {
command = "Hyprland"; command = "start-hyprland";
user = username; user = username;
}; };

View File

@@ -99,6 +99,13 @@ in
# dispatcher still exists); setting it aborts config parsing. # dispatcher still exists); setting it aborts config parsing.
dwindle.preserve_split = true; dwindle.preserve_split = true;
# Hyprland 0.51+ replaced gestures:workspace_swipe with the
# gesture keyword — without this, touchpads have NO gestures.
gesture = [
"3, horizontal, workspace"
"4, pinch, fullscreen"
];
misc = { misc = {
disable_hyprland_logo = true; disable_hyprland_logo = true;
disable_splash_rendering = true; disable_splash_rendering = true;
@@ -129,14 +136,25 @@ in
", Print, exec, grim -g \"$(slurp)\" - | wl-copy" ", Print, exec, grim -g \"$(slurp)\" - | wl-copy"
] ++ workspaceBinds; ] ++ workspaceBinds;
binde = [ # Media keys: wpctl talks to wireplumber directly (no
", XF86AudioRaiseVolume, exec, pamixer -i 5" # pipewire-pulse shim like pamixer); e = repeat, l = also on a
", XF86AudioLowerVolume, exec, pamixer -d 5" # locked screen — the standard flags for hardware keys.
", XF86AudioMute, exec, pamixer -t" bindel = [
", XF86AudioRaiseVolume, exec, wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 5%+"
", XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"
", XF86MonBrightnessUp, exec, brightnessctl set +5%" ", XF86MonBrightnessUp, exec, brightnessctl set +5%"
", XF86MonBrightnessDown, exec, brightnessctl set 5%-" ", XF86MonBrightnessDown, exec, brightnessctl set 5%-"
]; ];
bindl = [
", XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"
", XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"
", XF86AudioPlay, exec, playerctl play-pause"
", XF86AudioPause, exec, playerctl play-pause"
", XF86AudioNext, exec, playerctl next"
", XF86AudioPrev, exec, playerctl previous"
];
bindm = [ bindm = [
"$mod, mouse:272, movewindow" "$mod, mouse:272, movewindow"
"$mod, mouse:273, resizewindow" "$mod, mouse:273, resizewindow"

View File

@@ -28,12 +28,14 @@ in
enable = lib.mkDefault true; enable = lib.mkDefault true;
settings = { settings = {
default_session = { default_session = {
command = lib.mkDefault "${pkgs.tuigreet}/bin/tuigreet --time --remember --cmd Hyprland"; # start-hyprland is Hyprland 0.55's watchdog launcher; running
# the bare binary makes every session print a warning.
command = lib.mkDefault "${pkgs.tuigreet}/bin/tuigreet --time --remember --cmd start-hyprland";
user = "greeter"; user = "greeter";
}; };
# Boot straight into the session once; logout → normal greeter. # Boot straight into the session once; logout → normal greeter.
initial_session = lib.mkIf (cfg.greeter.autoLogin != null) { initial_session = lib.mkIf (cfg.greeter.autoLogin != null) {
command = "Hyprland"; command = "start-hyprland";
user = cfg.greeter.autoLogin; user = cfg.greeter.autoLogin;
}; };
}; };
@@ -60,6 +62,13 @@ in
hardware.bluetooth.enable = lib.mkDefault cfg.bluetooth.enable; hardware.bluetooth.enable = lib.mkDefault cfg.bluetooth.enable;
services.blueman.enable = lib.mkDefault cfg.bluetooth.enable; services.blueman.enable = lib.mkDefault cfg.bluetooth.enable;
# ── Firmware ─────────────────────────────────────────────────────
# Blobs for in-kernel drivers: wifi (iwlwifi/ath/rtw/brcm), SOF
# audio, Bluetooth. Drivers ship with the kernel, but without these
# a real machine can boot with no wifi — QEMU never catches it.
# nixos-generate-config also keys CPU microcode off this flag.
hardware.enableRedistributableFirmware = lib.mkDefault true;
# ── BTRFS timeline snapshots (ported from the previous iteration) ─ # ── BTRFS timeline snapshots (ported from the previous iteration) ─
# Guarded on the actual filesystem so enabling it on an ext4 machine # Guarded on the actual filesystem so enabling it on an ext4 machine
# is a clean no-op rather than a failing timer. # is a clean no-op rather than a failing timer.
@@ -79,9 +88,23 @@ in
}; };
# ── Fonts ──────────────────────────────────────────────────────── # ── Fonts ────────────────────────────────────────────────────────
# The ten most popular Nerd Fonts ship by default, so any of them
# can be named in the theme state's fonts.mono and actually resolve
# (nomarchy-theme-sync warns when a configured font is missing).
fonts = { fonts = {
packages = with pkgs; [ packages = with pkgs; [
nerd-fonts.jetbrains-mono nerd-fonts.jetbrains-mono
nerd-fonts.fira-code
nerd-fonts.caskaydia-cove
nerd-fonts.hack
nerd-fonts.sauce-code-pro
nerd-fonts.meslo-lg
nerd-fonts.roboto-mono
nerd-fonts.ubuntu-mono
# iosevka would be next by popularity, but its package is 1.1 GB
# (every weight × variant) — too heavy for the ISO and closures.
nerd-fonts.mononoki
nerd-fonts.inconsolata
inter inter
noto-fonts noto-fonts
noto-fonts-color-emoji noto-fonts-color-emoji

View File

@@ -109,6 +109,29 @@ def deep_merge(base: dict, override: dict) -> dict:
return out return out
# ─── Font verification ────────────────────────────────────────────────────
def check_fonts(state: dict) -> None:
"""Warn when a configured font family isn't installed. fontconfig
substitutes silently, so a typo'd or missing fonts.mono would just
render the whole desktop in some fallback with no error anywhere."""
if shutil.which("fc-match") is None:
return
for key in ("mono", "ui"):
name = (state.get("fonts") or {}).get(key)
if not name:
continue
result = subprocess.run(["fc-match", "-f", "%{family}", name],
capture_output=True, text=True)
families = [f.strip().lower() for f in result.stdout.split(",")]
if result.returncode != 0 or name.strip().lower() not in families:
fallback = result.stdout.strip() or "unknown"
print(f"nomarchy-theme-sync: warning: fonts.{key} '{name}' is not "
f"installed — fontconfig will substitute '{fallback}'",
file=sys.stderr)
notify(f"Font '{name}' is not installed — using '{fallback}'")
# ─── Rebuild dispatch ───────────────────────────────────────────────────── # ─── Rebuild dispatch ─────────────────────────────────────────────────────
def run_switch() -> None: def run_switch() -> None:
@@ -219,6 +242,7 @@ def cmd_apply(args) -> None:
state = deep_merge(load_state(), preset) state = deep_merge(load_state(), preset)
write_state(state) write_state(state)
log(f"theme: {state.get('name', args.theme)}") log(f"theme: {state.get('name', args.theme)}")
check_fonts(state)
if not args.no_switch: if not args.no_switch:
run_switch() run_switch()
apply_wallpaper(state) apply_wallpaper(state)
@@ -241,6 +265,8 @@ def cmd_set(args) -> None:
write_state(state) write_state(state)
log(f"set {args.path} = {value!r}") log(f"set {args.path} = {value!r}")
if args.path.startswith("fonts."):
check_fonts(state)
if not args.no_switch: if not args.no_switch:
run_switch() run_switch()
apply_wallpaper(state) apply_wallpaper(state)