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:
@@ -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
17
flake.lock
generated
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
30
flake.nix
30
flake.nix
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user