merge: adopt the v1 development line onto main

Brings main current with the 51 commits of post-rewrite feature work
(laptop power, idle cohesion, menu plugins, fwupd, docs restructure)
that had been accumulating on v1. New model: main is the development
default; v1 is the release pointer downstreams pin (?ref=v1), advanced
to main once a batch is tested. Rolling, no tags.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 20:38:36 +01:00
71 changed files with 3648 additions and 113 deletions

284
.claude/settings.local.json Normal file
View File

@@ -0,0 +1,284 @@
{
"permissions": {
"allow": [
"Bash(grep -i \"\\\\.nix$\")",
"Bash(xargs ls *)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --raw .#homeConfigurations.nomarchy.activationPackage.outPath)",
"Bash(git *)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --raw .#nixosConfigurations.default.config.system.activationScripts.home-manager-activate.text)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --raw .#nixosConfigurations.installerIsoGraphical.config.system.activationScripts.home-manager-activate.text)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#nixosConfigurations.default.config.system.build.toplevel.drvPath)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#nixosConfigurations.installerIsoGraphical.config.system.build.toplevel.drvPath)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#homeConfigurations.nomarchy.activationPackage.drvPath)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --raw .#nixosConfigurations.default.config.systemd.services.\"home-manager-nomarchy\".serviceConfig.ExecStart)",
"Bash(nix --extra-experimental-features 'nix-command flakes' build .#nixosConfigurations.default.config.system.build.vm --no-link --print-out-paths)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#homeConfigurations.nomarchy.config.xdg.configFile.\"nomarchy/current/background\".source)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --raw .#homeConfigurations.nomarchy.config.home-files.outPath)",
"Bash(nix --extra-experimental-features 'nix-command flakes' build .#homeConfigurations.nomarchy.config.home-files --no-link --print-out-paths)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#homeConfigurations.nomarchy.config.systemd.user.startServices)",
"Bash(xxd)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --raw .#packages.x86_64-linux.allThemeVariants.drvPath)",
"Bash(chmod +x /home/bernardo/Projects/Coding/Nomarchy/themes/engine/scripts/nomarchy-themes-prebuild)",
"Bash(nix --extra-experimental-features 'nix-command flakes' build .#homeConfigurations.nomarchy.activationPackage --no-link --print-out-paths)",
"Bash(nix --extra-experimental-features 'nix-command flakes' build .#homeConfigurations.nomarchy.config.home.packages.outPath)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --raw .#homeConfigurations.nomarchy.config.home-path.outPath)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --impure --expr '\\(builtins.readDir ./themes/engine/scripts\\)' --json)",
"Bash(nix-build -I nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos /home/bernardo/Projects/Coding/Nomarchy/flake.nix --argstr attr nixosConfigurations.installerIsoGraphical)",
"Bash(awk '{print $NF}')",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#nixosConfigurations.installerIsoGraphical.config.hardware.pulseaudio.enable .#nixosConfigurations.installerIsoGraphical.config.services.pipewire.enable .#nixosConfigurations.installerIsoGraphical.config.services.pipewire.pulse.enable)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#nixosConfigurations.installerIsoGraphical.config.services.displayManager.sddm.enable .#nixosConfigurations.installerIsoGraphical.config.services.displayManager.defaultSession .#nixosConfigurations.installerIsoGraphical.config.programs.hyprland.enable)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --json .#nixosConfigurations.installerIsoGraphical.config.users.users.nixos.extraGroups)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#nixosConfigurations.installerIsoGraphical.config.hardware.pulseaudio.enable)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#nixosConfigurations.installerIsoGraphical.config.services.pipewire.enable)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#nixosConfigurations.installerIsoGraphical.config.services.pipewire.pulse.enable)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#nixosConfigurations.installerIsoGraphical.config.programs.hyprland.enable)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval '.#nixosConfigurations.installerIsoGraphical.config.__TRACKED_VAR__')",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#nixosConfigurations.installerIsoGraphical.config.home-manager.users.nixos.home.packages --apply 'builtins.length')",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#nixosConfigurations.default.config.home-manager.users.nomarchy.home.packages --apply 'builtins.length')",
"Bash(chmod +x /home/bernardo/Projects/Coding/Nomarchy/features/scripts/utils/nomarchy-test-live-iso)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --json .#nixosConfigurations.installerIsoGraphical.config.boot.initrd.kernelModules)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#homeConfigurations.nomarchy.config.programs.waybar.systemd.enable)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --raw .#homeConfigurations.nomarchy.config.systemd.user.services --apply 'svcs: builtins.concatStringsSep \"\\\\n\" \\(builtins.attrNames svcs\\)')",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --raw .#homeConfigurations.nomarchy.config.gtk.iconTheme.package.pname)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --raw nixpkgs#everforest-gtk-variant.pname)",
"Bash(nix --extra-experimental-features 'nix-command flakes' search nixpkgs everforest)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#nixosConfigurations.default.pkgs.everforest-gtk-variant.pname)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#nixosConfigurations.default.pkgs.everforest-gtk-theme.pname)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#nixosConfigurations.default.pkgs.everforest-gtk-variant)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --impure --expr 'let pkgs = import \\(builtins.getFlake \"/home/bernardo/Projects/Coding/Nomarchy\"\\).inputs.nixpkgs { system = \"x86_64-linux\"; config.allowUnfree = true; }; in builtins.attrNames \\(pkgs // {} \\)')",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --impure --expr 'let pkgs = import \\(builtins.getFlake \"/home/bernardo/Projects/Coding/Nomarchy\"\\).inputs.nixpkgs { system = \"x86_64-linux\"; config.allowUnfree = true; }; in builtins.concatStringsSep \"\\\\n\" \\(builtins.filter \\(n: builtins.match \".*[Ee]verforest.*\" n != null\\) \\(builtins.attrNames pkgs\\)\\)' --raw)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --raw .#homeConfigurations.nomarchy.config.gtk.iconTheme.name)",
"Bash(bash -n /home/bernardo/Projects/Coding/Nomarchy/installer/install.sh)",
"Bash(bash -n /home/bernardo/Projects/Coding/Nomarchy/installer/hardware-db.sh)",
"Read(//tmp/**)",
"Bash(bash -c ' *)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --raw .#overlays.default --apply 'o: toString \\(builtins.typeOf o\\)')",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#overlays.default --apply 'o: \"overlay present: \" + builtins.typeOf o')",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --raw .#nixosConfigurations.default.config.system.nixos.distroName)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --raw .#nixosConfigurations.default.config.system.nixos.distroId)",
"Bash(nix-instantiate --parse /home/bernardo/Projects/Coding/Nomarchy/installer/disko-golden.nix)",
"Bash(nix --extra-experimental-features 'nix-command flakes' build .#nixosConfigurations.installerIsoGraphical.config.system.build.toplevel --no-link --print-out-paths)",
"Bash(bash /home/bernardo/Projects/Coding/Nomarchy/installer/install.sh --help)",
"Bash(bash -c 'echo \"HOSTNAME=$HOSTNAME\"')",
"Bash(bash -c 'echo \"HOSTNAME default=$HOSTNAME\"')",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --raw .#nixosConfigurations.installerIsoGraphical.config.services.getty.helpLine)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#nixosConfigurations.default.pkgs.voxtype.pname)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval .#nixosConfigurations.default.pkgs.opencode.pname)",
"Bash(nix --extra-experimental-features 'nix-command flakes' build .#nixosConfigurations.default.config.system.build.vm --no-link)",
"Bash(nix log *)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --json --impure --expr ' *)",
"Bash(cat)",
"Bash(chmod +x /tmp/theme_comparison.sh)",
"Bash(/tmp/theme_comparison.sh)",
"Bash(xargs -I {} dirname {})",
"Bash(xargs -I {} basename {})",
"Bash(chmod +x /tmp/color_compare.sh)",
"Bash(bash /tmp/color_compare.sh)",
"Bash(nix --extra-experimental-features \"nix-command flakes\" eval --raw 'github:nix-community/disko/latest#packages.x86_64-linux.disko.outPath')",
"Bash(find /nix/store/*disko*/lib/disko -name \"luks*\")",
"Bash(nix flake *)",
"Bash(nix-instantiate --parse /home/bernardo/Projects/Coding/Nomarchy/flake.nix)",
"Bash(nix eval *)",
"Bash(bash installer/install.sh --dry-run)",
"Bash(sudo bash *)",
"Bash(chmod +x /home/bernardo/Projects/Coding/Nomarchy/core/system/scripts/nomarchy-env-update)",
"Bash(bash -n /home/bernardo/Projects/Coding/Nomarchy/core/system/scripts/nomarchy-env-update)",
"Bash(sed -n '1450,1500p' /home/bernardo/Projects/Coding/Nomarchy/installer/install.sh)",
"Bash(grep -A 5 'nixosConfigurations.$HOSTNAME' /home/bernardo/Projects/Coding/Nomarchy/installer/install.sh)",
"Bash(mkdir -p /tmp/globtest)",
"Bash(touch nvme0n1 *)",
"Bash(shopt -s nullglob)",
"Bash(bash -n installer/install.sh)",
"Bash(nix --extra-experimental-features \"nix-command flakes\" eval .#nixosConfigurations.nomarchy-live.config.system.build.toplevel.outPath)",
"Bash(nix --extra-experimental-features \"nix-command flakes\" eval --impure --expr 'let f = import ./core/system/impermanence.nix; in \\(f { config = { nomarchy.system.impermanence = { enable = true; mainLuksName = \"crypted_main\"; }; }; lib = \\(import <nixpkgs> {}\\).lib; pkgs = null; inputs = { impermanence.nixosModules.impermanence = {}; }; }\\).config.boot.initrd.postDeviceCommands')",
"Bash(nix --extra-experimental-features \"nix-command flakes\" eval .#nixosConfigurations.nomarchy-installer.config.system.build.toplevel.outPath)",
"Bash(nix --extra-experimental-features \"nix-command flakes\" eval .#nixosConfigurations.nomarchy-live.config.nomarchy.system.theme)",
"Bash(chmod +x *)",
"Bash(./bin/utils/nomarchy-docs-scripts --out docs/SCRIPTS.md)",
"Bash(awk '/^## Missing references/,/^## Menu items/' docs/SCRIPTS.md)",
"Bash(bash -n bin/utils/nomarchy-docs-scripts)",
"Bash(bash -n features/scripts/utils/nomarchy-docs-scripts)",
"Bash(bash -n themes/engine/scripts/nomarchy-theme-next)",
"Bash(grep '`unused?`' docs/SCRIPTS.md)",
"Bash(awk -F'|' '{print $2}')",
"Bash(awk *)",
"Bash(grep -c '`unused?`' docs/SCRIPTS.md)",
"Bash(grep -n '`unused?`' docs/SCRIPTS.md)",
"Bash(grep '^| `__TRACKED_VAR__` ' docs/SCRIPTS.md)",
"Bash(nix --extra-experimental-features 'nix-command flakes' flake check --no-build)",
"Bash(grep -c '`kept`' docs/SCRIPTS.md)",
"Bash(nix shell *)",
"Bash(nix shell 'nixpkgs#shellcheck' --command bash -c ' *)",
"Bash(./bin/utils/nomarchy-docs-scripts --out /tmp/SCRIPTS.regen.md)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --impure --expr ' *)",
"Bash(sed -n '100,108p' core/home/options.nix)",
"Bash(sed -n '185,195p' core/system/options.nix)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --impure --json --expr ' *)",
"Bash(bash -n *)",
"Bash(nix --extra-experimental-features 'nix-command flakes' eval --impure --raw --expr ' *)",
"Bash(grep '^| `nomarchy-manual`' docs/SCRIPTS.md)",
"Bash(nix *)",
"Bash(sudo -n cat /etc/environment)",
"Bash(systemctl --user show-environment)",
"Bash(./bin/utils/nomarchy-docs-keybindings --out /tmp/keybindings-fresh.md)",
"Bash(bash bin/utils/nomarchy-docs-keybindings --out /tmp/keybindings-fresh.md)",
"Bash(nix-shell -p shellcheck --run \"shellcheck --severity=error features/scripts/utils/nomarchy-menu features/scripts/utils/nomarchy-launch-screensaver\")",
"Bash(./bin/utils/nomarchy-docs-keybindings --out docs/KEYBINDINGS.md)",
"Bash(grep -rEho 'jq[^\"]*--arg [a-zA-Z]+|\\\\.[a-zA-Z][a-zA-Z_0-9]+ = \\\\$|jq -r .\\\\.[a-zA-Z]+' features/scripts core/system/scripts themes/engine/scripts)",
"Read(//home/bernardo/.config/mako/**)",
"Read(//home/bernardo/.config/nomarchy/default/hypr/**)",
"Read(//home/bernardo/.config/nomarchy/current/theme/**)",
"Read(//etc/nixos/**)",
"Bash(sudo git -C /etc/nixos log --oneline -3)",
"Bash(PATH=/home/bernardo/Projects/Coding/Nomarchy/features/scripts/utils:/home/bernardo/Projects/Coding/Nomarchy/themes/engine/scripts:__TRACKED_VAR__ bash /home/bernardo/Projects/Coding/Nomarchy/features/scripts/utils/nomarchy-installed-summary)",
"Bash(grep *)",
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); r=d['results'][0]['extensions'][0]; print\\('version:', r['versions'][0]['version']\\); print\\('displayName:', r['displayName']\\); print\\('publisher:', r['publisher']['publisherName']\\); print\\('extName:', r['extensionName']\\)\")",
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); [print\\(x['file'],'=',x['value']\\) for x in d]\")",
"Bash([ -f \"core/home/config/nomarchy/default/hypr/apps/$f.conf\" ])",
"Bash([ -f \"core/home/config/nomarchy/default/hypr/bindings/$f.conf\" ])",
"Bash([ -f \"core/home/config/nomarchy/default/hypr/$f.conf\" ])",
"Bash([ -f \"features/desktop/hyprland/config/$f.conf\" ])",
"Bash(timeout 580 ./bin/utils/nomarchy-eval-matrix)",
"Bash(break)",
"Bash(sed -n 1,45p __TRACKED_VAR__/nix/package.nix)",
"Bash(sed -n 1,50p __TRACKED_VAR__/nix/modules/home-manager.nix)",
"Bash(python3 -c 'import json,sys; d=json.load\\(sys.stdin\\); print\\('\\\\''top path:'\\\\'', d.get\\('\\\\''path'\\\\'','\\\\''?'\\\\''\\)\\); inp=d.get\\('\\\\''inputs'\\\\'',{}\\); print\\('\\\\''inputs:'\\\\'', list\\(inp.keys\\(\\)\\)\\); print\\('\\\\''walker present:'\\\\'', '\\\\''walker'\\\\'' in inp\\); *)",
"Bash(echo \"exit: $?\")",
"Bash(echo 'output: [__TRACKED_VAR__]')",
"Bash(time ./bin/utils/nomarchy-eval-matrix)",
"Bash(python3 -c \"import json,sys; [print\\(x\\) for x in sorted\\(json.load\\(sys.stdin\\)\\)]\")",
"Read(//dev/**)",
"Bash(command -v qemu-system-x86_64 qemu-kvm)",
"Bash(echo \"BUILD_EXIT=$?\")",
"Bash(rm -f /tmp/nomarchy-mon.sock)",
"Bash(mkdir -p /tmp/nomarchy-vmrun)",
"Bash(QEMU_OPTS='-display none -monitor unix:/tmp/nomarchy-mon.sock,server,nowait -vga none' /tmp/nomarchy-vm/bin/run-nomarchy-vm)",
"Bash(echo \"VM_EXIT=$?\")",
"Bash(magick /tmp/shot.png -crop 5120x130+0+0 +repage /tmp/shot-top.png)",
"Bash(magick /tmp/shot.png -crop 1400x420+0+440 +repage /tmp/shot-term.png)",
"Bash(/nix/store/wv8bpzriikv65xnd1vciqpq7rnr8h2q2-bash-5.3p3/bin/bash -c 'compgen -G \"/etc/*\" >/dev/null 2>&1 && echo \"compgen WORKS\" || echo \"compgen MISSING \\(progcomp disabled\\)\"')",
"Bash(compgen -G \"/etc/*\")",
"Bash(/nix/store/wv8bpzriikv65xnd1vciqpq7rnr8h2q2-bash-5.3p3/bin/bash -c 'shopt -s nullglob; a=\\(/etc/hos*\\); echo \"nullglob ok: ${#a[@]} match\\(es\\)\"')",
"Bash(mkdir -p /tmp/nomarchy-vmrun2)",
"Bash(pgrep -f qemu-system)",
"Bash(rm -rf /tmp/nomarchy-vmrun3)",
"Bash(mkdir -p /tmp/nomarchy-vmrun3)",
"Bash(echo \"exit=$?\")",
"Bash(socat - unix-connect:/tmp/nomarchy-mon.sock)",
"Bash(magick /tmp/shot3.ppm /tmp/shot3.png)",
"Bash(magick identify *)",
"Bash(echo \"captured -> $\\(magick identify -format '%wx%h' /tmp/shot3.png\\)\")",
"Bash(magick /tmp/shot3.png -crop 5120x150+0+0 +repage /tmp/shot3-top.png)",
"Bash(pkill -x qemu-system-x86)",
"Bash(pkill -f \"qemu-system-x86_64 -machine\")",
"Bash(rm -rf /tmp/nomarchy-vmrun4)",
"Bash(mkdir -p /tmp/nomarchy-vmrun4)",
"Bash(echo \"boot-exit=$?\")",
"Bash(magick /tmp/shot4.ppm /tmp/shot4.png)",
"Bash(echo \"captured -> $\\(magick identify -format '%wx%h' /tmp/shot4.png\\)\")",
"Bash(magick /tmp/shot4.png -crop 5120x160+0+0 +repage /tmp/shot4-top.png)",
"Bash(rm -rf /tmp/vmrun_final)",
"Bash(mkdir -p /tmp/vmrun_final)",
"Bash(magick /tmp/shot5.ppm /tmp/shot5.png)",
"Bash(echo \"captured -> $\\(magick identify -format '%wx%h' /tmp/shot5.png\\)\")",
"Bash(magick /tmp/shot5.png -crop 5120x170+0+0 +repage /tmp/shot5-top.png)",
"Bash(rm -rf /tmp/vmssh)",
"Bash(mkdir -p /tmp/vmssh)",
"Bash(QEMU_OPTS='-display none -monitor unix:/tmp/nomarchy-mon.sock,server,nowait -vga none' QEMU_NET_OPTS=hostfwd=tcp::2222-:22 /tmp/nomarchy-vm-ssh/bin/run-nixos-vm)",
"Bash(command -v expect)",
"Read(//home/bernardo/.ssh/**)",
"Bash(QEMU_OPTS='-display none -monitor unix:/tmp/nomarchy-mon.sock,server,nowait -vga none' QEMU_NET_OPTS=hostfwd=tcp::2222-:22 /tmp/nomarchy-vm-ssh/bin/run-nomarchy-vm)",
"Bash(echo \"vm-exited rc=$?\")",
"Bash(pkill -f \"run-nomarchy-vm\")",
"Bash(rm -rf /tmp/vmtheme)",
"Bash(mkdir -p /tmp/vmtheme)",
"Bash(echo \"vm exited rc=$?\")",
"Bash(magick /tmp/latte.ppm -resize 1700 /tmp/latte.png)",
"Bash(echo \"saved $\\(magick identify -format '%wx%h' /tmp/latte.png\\)\")",
"Bash(./bin/utils/nomarchy-eval-matrix)",
"Bash(rm -f /tmp/nomarchy.qcow2 /tmp/vm-boot.log)",
"Bash(NIX_DISK_IMAGE=/tmp/nomarchy.qcow2 QEMU_OPTS='-display none -m 4096' /tmp/vm-verify-result/bin/run-nomarchy-vm)",
"Bash(echo \"VM PID $!\")",
"Bash(pkill -f 'qemu-system-x86_64 -machine accel=kvm.*nomarchy')",
"Bash(rm -f /tmp/vm-verify.nix /tmp/nomarchy.qcow2 /tmp/vm-boot.log /tmp/vm-verify-result)",
"Bash(pkill -9 -f 'qemu-system')",
"Bash(rm -f /tmp/nomarchy.qcow2)",
"Bash(python3 -)",
"Bash(sed -n '206,228p' features/scripts/utils/nomarchy-menu)",
"Bash(pkill -9 -f 'run-nixos-vm\\\\|qemu-system-x86_64.*nomarchy')",
"Bash(rm -f /tmp/nomarchy.qcow2 /tmp/vm-verify.nix /tmp/vm-boot.log /tmp/menu-*.sh /tmp/layout-check.sh /tmp/validate-fixes.sh /tmp/menu-cmds.txt /tmp/shipped.txt)",
"Bash(rm -f /tmp/vm-verify-result)",
"Bash(kill -9 1285354)",
"Bash(sed -n '1,30p' features/scripts/battery-monitor.nix)",
"Bash(sed -n '40,75p' features/desktop/waybar/default.nix)",
"Bash(sed -n '140,160p' features/desktop/waybar/themes/summer-night/config.jsonc)",
"Bash(sed -n '10,20p' features/desktop/waybar/config/config.jsonc)",
"Bash(python3 -m json.tool)",
"Bash(/tmp/nomarchy-multidisk-test/run-vm.sh)",
"Bash(socat - UNIX-CONNECT:mon.sock)",
"Bash(/tmp/nomarchy-multidisk-test/boot-from-disk.sh)",
"Bash(socat - UNIX-CONNECT:bootmon.sock)",
"Bash(convert /tmp/nomarchy-multidisk-test/screen.ppm /tmp/nomarchy-multidisk-test/screen.png)",
"Bash(magick /tmp/nomarchy-multidisk-test/screen.ppm /tmp/nomarchy-multidisk-test/screen.png)",
"Bash(shellcheck --severity=error installer/install.sh)",
"Bash(curl -s \"https://api.github.com/repos/nix-community/home-manager/contents/modules/programs/vscode?ref=0d02ec1d0a05f88ef9e74b516842900c41f0f2fe\")",
"Bash(python3 -c \"import json,sys;[print\\(x['name']\\) for x in json.load\\(sys.stdin\\)]\")",
"Bash(curl -s \"https://api.github.com/repos/nix-community/home-manager/contents/modules/programs/vscode?ref=release-25.11\")",
"Bash(curl -s \"https://api.github.com/repos/nix-community/home-manager/contents/modules/programs/vscode?ref=49ca96b2714c5931e17401eff87f3edd42d2b0f2\")",
"Bash(python3 -c \"import json,sys;d=json.load\\(sys.stdin\\);[print\\(x['name']\\) for x in d]\")",
"Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" \"https://raw.githubusercontent.com/nix-community/home-manager/49ca96b2714c5931e17401eff87f3edd42d2b0f2/modules/programs/vscode/each-config.nix\")",
"Read(//nix/store/**)",
"Bash(mkdir -p __TRACKED_VAR__/nomarchy-install-test)",
"Bash(qemu-img create *)",
"Read(//home/bernardo/.config/nomarchy/**)",
"Read(//home/bernardo/.local/share/nomarchy/**)",
"Read(//home/bernardo/.config/elephant/**)",
"Read(//home/bernardo/.local/share/elephant/**)",
"Read(//home/bernardo/.config/walker/**)",
"WebFetch(domain:raw.githubusercontent.com)",
"Bash(python3 -c ' *)",
"Bash(nix-store -r /nix/store/hydnry53i2qbmqq0b0872wlbzdkpq1bd-source)",
"Bash(echo \"EXIT=$?\")",
"Bash(sed -n '44,50p' features/default.nix)",
"Bash(sed -n '14,20p' features/scripts/default.nix)",
"Bash(sed -n '11,17p' themes/engine/scripts.nix)",
"WebSearch",
"WebFetch(domain:github.com)",
"Bash(curl -sL \"https://api.github.com/repos/hyprwm/Hyprland/commits?sha=v0.55.3&per_page=100\")",
"Bash(curl -sL -o /tmp/hypr-fence-fix.patch \"https://github.com/hyprwm/Hyprland/commit/bdf0aaa5409fe2e4715e2796d669e16776de0233.patch\")",
"Bash(rm -f /tmp/nomarchy-2605.qcow2 /tmp/nomarchy-mon.sock /tmp/nomarchy-serial.sock)",
"Bash(NIX_DISK_IMAGE=/tmp/nomarchy-2605.qcow2 QEMU_OPTS=\"-display none -monitor unix:/tmp/nomarchy-mon.sock,server,nowait -serial unix:/tmp/nomarchy-serial.sock,server,nowait\" /tmp/nomarchy-vm-2605/bin/run-nomarchy-vm)",
"Bash(python3 *)",
"Bash(curl -sL https://raw.githubusercontent.com/hyprwm/Hyprland/v0.55.3/CMakeLists.txt)",
"Bash(curl -sL https://raw.githubusercontent.com/hyprwm/Hyprland/v0.55.2/CMakeLists.txt)",
"Bash(NIX_DISK_IMAGE=/tmp/nomarchy-2605.qcow2 QEMU_OPTS=\"-display none -monitor unix:/tmp/nomarchy-mon.sock,server,nowait -serial unix:/tmp/nomarchy-serial.sock,server,nowait\" /tmp/nomarchy-vm-2605-fixed/bin/run-nomarchy-vm)",
"Bash(xargs du -sh)",
"Bash(xargs du -csh)",
"Bash(curl -s https://raw.githubusercontent.com/nix-community/stylix/release-26.05/flake.nix)",
"Bash(nix build *)",
"Bash(echo \"exit $?\")",
"Bash(pkill -f \"test-install.sh\")",
"Bash(pkill -f \"qemu-system-x86_64.*NOMARCHY\\\\|qemu-system-x86_64.*nomarchy\")",
"Bash(NOMARCHY_PATH=__TRACKED_VAR__ python3 pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py --quiet apply summer-night --no-switch)",
"Bash(NOMARCHY_PATH=__TRACKED_VAR__ python3 pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py --quiet apply summer-day --no-switch)",
"Bash(xargs cat)",
"Bash(nix-instantiate --parse modules/home/rofi.nix)",
"Bash(nix-instantiate --parse modules/home/shell.nix)",
"Bash(command -v nix)",
"Bash(nix-instantiate --parse modules/home/keys.nix)",
"Bash(cp /home/bernardo/Projects/Nomarchy/modules/nixos/plymouth/logo.png /tmp/logo-orig.png)",
"Bash(nix run *)",
"Bash(cp /tmp/logo-text.png /home/bernardo/Projects/Nomarchy/.tmp-logo-preview.png)",
"Bash(cp /tmp/logo-text.png /home/bernardo/Projects/Nomarchy/preview-logo.png)",
"Bash(nix-instantiate --parse modules/nixos/plymouth.nix)",
"Bash(echo \"parse OK\")",
"Bash(echo \"=== build the plymouth theme derivation directly ===\")",
"Bash(nix-instantiate --parse modules/nixos/default.nix)",
"Bash(cp /nix/store/f18ij42lmpg07fn1m29aplayakx7kqpz-splash-test.png/* preview-splash-built.png)",
"Bash(cp /nix/store/f18ij42lmpg07fn1m29aplayakx7kqpz-splash-test.png preview-splash-built.png)",
"Bash(nix-instantiate --parse hosts/live.nix)",
"Bash(nix-instantiate --parse modules/home/stylix.nix)",
"Bash(nix-shell -p fontconfig --run \"fc-scan --format '%{family}\\\\n' modules/nixos/branding/Nomarchy.ttf\")"
]
}
}

View File

@@ -45,7 +45,10 @@ Flat on purpose. Two module trees, one options file each, no hidden layers.
├── modules/
│ ├── nixos/ # the distro, system side
│ │ ├── default.nix # Hyprland session, Pipewire, greetd, fonts
│ │ ── options.nix # nomarchy.system.* toggles
│ │ ── options.nix # nomarchy.system.* toggles
│ │ ├── plymouth.nix # boot splash, tinted from the JSON
│ │ ├── file-manager.nix # Thunar GUI + gvfs/tumbler/udisks2
│ │ └── power.nix # power-profiles-daemon/TLP, thermald, charge limit
│ └── home/ # the distro, user side
│ ├── default.nix # entry point
│ ├── options.nix # nomarchy.* option surface
@@ -54,7 +57,16 @@ Flat on purpose. Two module trees, one options file each, no hidden layers.
│ ├── hyprland.nix # all JSON-driven
│ ├── waybar.nix
│ ├── ghostty.nix
── btop.nix
── btop.nix
│ ├── rofi.nix # launcher + nomarchy-menu (calc, emoji, clip…)
│ ├── keybinds.nix # single source: Hyprland binds + SUPER+? sheet
│ ├── swaync.nix # notifications, same JSON
│ ├── idle.nix # hyprlock + hypridle, same JSON
│ ├── yazi.nix # flagship TUI file manager + plugins
│ ├── osd.nix # swayosd volume/brightness OSD
│ ├── shell.nix # zsh + starship + bat/eza/zoxide
│ ├── keys.nix # gpg-agent fronting SSH, pinentry-qt
│ └── fastfetch.nix # themed Nomarchy logo + system info
├── hosts/
│ ├── default/ # reference machine (thin: boot, user, hostname)
│ └── live.nix # bootable live ISO (try the distro, no install)
@@ -63,6 +75,8 @@ Flat on purpose. Two module trees, one options file each, no hidden layers.
│ └── nomarchy-install/ # live-ISO installer (gum + disko + mkFlake)
├── templates/downstream/ # `nix flake init -t` starter for users
├── docs/TESTING.md # how to verify changes (incl. AI-agent rules)
├── docs/OVERRIDES.md # how downstream users override defaults
├── docs/ROADMAP.md # forward-looking plans + shipped-fixes log
└── tools/ # maintainer-only
├── import-palettes.py # converts old-distro themes → JSON + assets
├── test-live-iso.sh # build the ISO + boot it in QEMU
@@ -144,22 +158,69 @@ sudo nixos-rebuild switch --flake .#default # system: rare
home-manager switch --flake .#me # desktop: every theme change, no sudo
```
Override anything with plain NixOS/HM options (the distro uses `mkDefault`
throughout) or the `nomarchy.*` surface:
Day-to-day you'll use the shipped shortcuts instead:
```sh
sys-update # nix flake update + system rebuild (BTRFS snapshot first when available)
home-update # home-manager switch (no flake update, no sudo)
```
**Order matters when pulling distro updates.** `home-update` does *not*
touch the lock — it rebuilds the desktop against the **current**
`flake.lock`. A new Nomarchy revision (new keybinds, theming, modules)
arrives only when the lock is updated, which `sys-update` does
(`nix flake update`). So to pull an update that affects the desktop layer:
run `sys-update` **first** (updates the lock + rebuilds the system), **then**
`home-update` (re-applies the desktop against the new lock). Doing them in
the other order rebuilds the desktop against the *old* inputs and silently
skips the new home-side changes. After a home-side keybind/config change,
also `hyprctl reload` (or relogin) so the running session re-reads it.
Override anything via the `nomarchy.*` surface or plain NixOS/HM options:
appearance (gaps/colors/fonts) changes through `nomarchy-theme-sync`,
behaviour (input/misc/monitor/chrome) is `mkDefault` so a plain `home.nix`
assignment wins, and bind/exec-once lists concatenate. Full guide with
examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
| Option | Default | Purpose |
|---|---|---|
| `nomarchy.stateFile` | — (required) | Path to your theme-state.json |
| `nomarchy.terminal` | `"ghostty"` | Terminal for keybinds and `$TERMINAL` |
| `nomarchy.keyboard.layout` | `"us"` | XKB layout for the Hyprland session (installer writes it; pairs with xkb + `console.useXkbConfig` in system.nix) |
| `nomarchy.keyboard.variant` | `""` | XKB variant for the session |
| `nomarchy.hyprland.enable` | `true` | Nomarchy's Hyprland config |
| `nomarchy.waybar.enable` | `true` | Nomarchy's Waybar |
| `nomarchy.rofi.enable` | `true` | Themed rofi launcher + `nomarchy-menu` dispatcher |
| `nomarchy.swaync.enable` | `true` | swaync notifications, themed |
| `nomarchy.idle.enable` | `true` | hyprlock + hypridle (idle lock 5 min, display off 10, suspend 30) |
| `nomarchy.yazi.enable` | `true` | yazi TUI file manager, themed + curated plugins |
| `nomarchy.osd.enable` | `true` | swayosd on-screen display for volume/brightness/mute |
| `nomarchy.shell.enable` | `true` | zsh + starship prompt + bat/eza/zoxide (zsh is the default login shell) |
| `nomarchy.ghostty.enable` | `true` | Nomarchy's Ghostty |
| `nomarchy.btop.enable` | `true` | btop with per-theme colors |
| `nomarchy.stylix.enable` | `true` | GTK/Qt/cursor theming |
| `nomarchy.fastfetch.enable` | `true` | fastfetch fronted by the themed Nomarchy logo |
| `nomarchy.keys.enable` | `true` | gpg-agent fronting SSH + pinentry-qt |
| `nomarchy.themesDir` | Nomarchy's `themes/` | Where per-theme app overrides are probed |
| `nomarchy.system.plymouth.enable` | `true` | Branded boot splash, background from the theme JSON (recolors on system rebuilds) |
| `nomarchy.system.fileManager.enable` | `true` | Thunar GUI + gvfs/tumbler/udisks2 (the "open folder" handler) |
| `nomarchy.system.greeter.enable` | `true` | greetd/tuigreet |
| `nomarchy.system.greeter.autoLogin` | `null` | Auto-login this user into Hyprland (installer sets it on LUKS machines) |
| `nomarchy.system.audio.enable` | `true` | Pipewire stack |
| `nomarchy.system.bluetooth.enable` | `true` | Bluetooth + blueman |
| `nomarchy.system.snapper.enable` | `false` | Hourly/daily BTRFS timeline snapshots + `nixos-rebuild-snap` (installer enables it; no-op unless root is BTRFS) |
| `nomarchy.system.power.enable` | `true` | Active power management (see below) |
| `nomarchy.system.power.backend` | `"ppd"` | `"ppd"` (power-profiles-daemon + menu/Waybar switcher) or `"tlp"` (deeper battery tuning, no switcher) — mutually exclusive |
| `nomarchy.system.power.laptop` | `false` | Marks a laptop, gating battery-only features; the installer sets it when a battery is present |
| `nomarchy.system.power.thermal.enable` | `false` | thermald (Intel-only); the installer enables it on a GenuineIntel CPU |
| `nomarchy.system.power.batteryChargeLimit` | `null` | Stop charging at this % (e.g. `80`) where the hardware supports it; needs `power.laptop` |
Beyond the `nomarchy.*` surface, the system layer turns on the usual
desktop services with `lib.mkDefault` (override natively). One worth
calling out: **`services.fwupd.enable`** is on by default for firmware
updates via LVFS — it only refreshes metadata, never flashes on its own,
so run `fwupdmgr update` to apply. Disable with `services.fwupd.enable =
false;` on machines without real firmware (VMs/headless).
## 4. How theming works
@@ -199,6 +260,7 @@ per theme — a single place to look, unlike the old distro's split:
| `btop.theme` | baked into the generation (generated from the palette when absent) |
| `waybar.css` | **whole-swap**: replaces the generated bar style entirely (probed at eval time, self-contained) |
| `waybar.jsonc` | whole-swap for the bar *layout* (must be plain JSON) |
| `rofi.rasi` | **whole-swap**: replaces the generated launcher/menu theme entirely |
Six ported themes ship a `waybar.css` identity (catppuccin, lumon, nord,
retro-82, summer-day, summer-night). Custom user themes can live in
@@ -214,11 +276,15 @@ nomarchy-theme-sync set ui.gapsOut 16 # tweak one knob (also a switch)
nomarchy-theme-sync bg next # cycle wallpapers — instant, no rebuild
nomarchy-theme-sync bg auto # back to the theme's default wallpaper
nomarchy-theme-sync get colors.accent
sys-update # update inputs + rebuild the system (snapshots first)
home-update # rebuild just the desktop layer
```
Keybinds: `SUPER+Return` terminal · `SUPER+D` launcher · `SUPER+T` theme
picker · `SUPER+SHIFT+T` next wallpaper · `SUPER+Q` close · `SUPER+1..9`
workspaces · `Print` region screenshot.
picker · `SUPER+SHIFT+T` next wallpaper · `SUPER+X` power menu ·
`SUPER+E` file manager (yazi) · `SUPER+N` notifications · `SUPER+CTRL+V`
clipboard history · `SUPER+Q`
close · `SUPER+1..9` workspaces · `Print` region screenshot.
## 6. Extending
@@ -229,12 +295,8 @@ workspaces · `Print` region screenshot.
- **Importing more old-distro palettes:**
`tools/import-palettes.py <palettes-dir> themes/`.
## Roadmap
## Roadmap & known issues
- **Faster switches:** move `backgrounds/` out of the flake source (the 86 MB
re-copy on every state write is the main eval tax), then pre-built theme
variants if still needed
- Plymouth + SDDM/greeter theming from the same JSON
- Installer round 2: multi-disk BTRFS RAID, impermanence, BIOS/legacy
boot (v1 `nomarchy-install` is single-disk UEFI — see `pkgs/nomarchy-install`)
- `hyprlock`/`hypridle`, swayosd, launch-or-focus UX scripts
See **[docs/ROADMAP.md](docs/ROADMAP.md)** — forward-looking plans plus the
log of shipped fixes. Kept out of the README so this stays a focused entry
point.

147
docs/OVERRIDES.md Normal file
View File

@@ -0,0 +1,147 @@
# Overriding Nomarchy's defaults
You consume Nomarchy as a flake input and own two files: `system.nix` and
`home.nix`. Your config is **merged** with Nomarchy's modules, so changing a
default is just a matter of knowing which of three knobs to reach for. The
rule of thumb:
> **Appearance → the CLI. Behaviour → `home.nix`. Whole component → toggle it off.**
## 1. Appearance (gaps, colors, rounding, fonts, opacity) — use the CLI
Everything that defines the *look* flows from `theme-state.json`, the single
source of truth. Change it with `nomarchy-theme-sync`, which writes the JSON
and rebuilds — one generation, applied to Hyprland, Waybar, Ghostty, btop and
Stylix at once:
```sh
nomarchy-theme-sync set ui.gapsOut 16 # gaps, borders, rounding, opacity
nomarchy-theme-sync set ui.rounding 0
nomarchy-theme-sync set fonts.mono "FiraCode Nerd Font"
nomarchy-theme-sync apply gruvbox # whole palette
```
These values are deliberately kept at normal priority in the modules, so they
stay owned by the theme system. If you *insist* on pinning one in `home.nix`
regardless of the active theme, use `lib.mkForce` (see §4) — but then the CLI
can no longer change that knob.
## 2. Behaviour (input, misc, monitor, animations, terminal chrome) — `home.nix`
Non-appearance defaults are set with `lib.mkDefault`, so a **plain assignment**
in your `home.nix` wins — no `mkForce` needed:
```nix
# home.nix
{ ... }:
{
wayland.windowManager.hyprland.settings = {
input.follow_mouse = 0; # was 1
input.touchpad.natural_scroll = false;
misc.disable_splash_rendering = false;
monitor = [ "DP-1,2560x1440@144,0x0,1" ]; # replaces the default rule
animations.enabled = false;
};
programs.ghostty.settings = {
window-padding-x = 4; # was 12
window-decoration = true;
};
}
```
### Adding vs. overriding lists
`bind`, `bindel`, `bindl`, `bindm` and `exec-once` are lists kept at normal
priority, so anything you add **concatenates** with Nomarchy's — your binds and
autostarts run *alongside* the defaults:
```nix
{
wayland.windowManager.hyprland.settings = {
bind = [
"$mod, B, exec, firefox"
"$mod SHIFT, S, exec, grim -g \"$(slurp)\" - | swappy -f -"
];
exec-once = [ "nm-applet --indicator" ];
};
}
```
To **remove or replace** a default bind, override the whole `bind` list with
`lib.mkForce [ … ]` (you then own the full list), or rebind the key to
something else (last definition for a key wins in Hyprland).
You can also drop raw config that doesn't fit the Nix schema:
```nix
{ wayland.windowManager.hyprland.extraConfig = ''
bindl = , switch:Lid Switch, exec, hyprlock
'';
}
```
### Waybar
For a themed bar identity, ship whole-swap assets
(`themes/<slug>/waybar.jsonc` and `waybar.css`) — see the main README. To
replace the bar wholesale from `home.nix`, assign `programs.waybar.settings.
mainBar` / `programs.waybar.style` (both `mkDefault`, so a plain assignment
wins). For a one-off tweak of a single generated key, `lib.mkForce` it.
## 3. Whole component — toggle it off and bring your own
Each desktop piece has an enable flag (all default `true`). Turn one off and
Nomarchy contributes nothing for it, leaving the field clear for your own:
```nix
# home.nix
{
nomarchy.hyprland.enable = false; # then write your own wayland.windowManager.hyprland
nomarchy.waybar.enable = false;
nomarchy.swaync.enable = false;
nomarchy.idle.enable = false; # no hyprlock/hypridle
}
```
```nix
# system.nix
{
nomarchy.system.plymouth.enable = false;
nomarchy.system.greeter.enable = false; # bring your own login manager
}
```
See the option tables in the README for the full list.
## 4. Last resort: `lib.mkForce`
To override a value Nomarchy sets at normal priority (an appearance value, or
a whole list), raise your definition's priority:
```nix
{ lib, ... }:
{
# hardcode rounding against the theme (the CLI can no longer change it)
wayland.windowManager.hyprland.settings.decoration.rounding = lib.mkForce 0;
# own the entire keybind list
wayland.windowManager.hyprland.settings.bind = lib.mkForce [ "$mod, Return, exec, kitty" ];
}
```
If a plain assignment errors with *"has conflicting definition values"*, that
value is theme-owned at normal priority — either change it via the CLI (§1) or
`mkForce` it here.
## Quick reference
| You want to… | Do this |
|---|---|
| Change gaps / colors / rounding / fonts | `nomarchy-theme-sync set …` or `apply` |
| Change input / misc / monitor / animations / terminal chrome | plain assignment in `home.nix` |
| Add keybinds / autostarts | add to the `bind` / `exec-once` list (concatenates) |
| Replace all keybinds | `bind = lib.mkForce [ … ]` |
| Hardcode an appearance value against the theme | `lib.mkForce` in `home.nix` |
| Drop a whole component | `nomarchy.<component>.enable = false` |
| Raw Hyprland lines | `wayland.windowManager.hyprland.extraConfig` |

291
docs/ROADMAP.md Normal file
View File

@@ -0,0 +1,291 @@
# Roadmap & changelog
Forward-looking plans, plus a running log of shipped fixes (the
"Known issues & follow-ups" section). Split out of the README so that
stays a focused entry point — what Nomarchy is, how to install it, and
how to override it. Items marked ✓ are shipped.
## Roadmap
- **Menu system** (apps launcher + theme switching + system actions), built
on rofi 2.0 (native Wayland on 26.05) — its `.rasi` theme is baked from
theme-state.json like every other app, with rich per-element styling:
- ✓ shipped: `modules/home/rofi.nix` (per-element theme generated from
the palette, `themes/<slug>/rofi.rasi` whole-swap) and the
`nomarchy-menu` dispatcher: root picker (no args) · `power`
(lock/logout/suspend/hibernate/reboot/shutdown, SUPER+X) ·
`theme` (SUPER+T) · `clipboard` (cliphist, SUPER+CTRL+V) · `calc`
(rofi-calc, live, SUPER+CTRL+C) · `files` (fd → xdg-open,
SUPER+CTRL+F) · `emoji` (rofi-emoji, SUPER+CTRL+E) · `web` (Google).
SUPER+D is `rofi -show drun`.
- ✓ shipped modules: `network` (nmtui in `$TERMINAL`) · `bluetooth`
(blueman-manager) · `capture` (grim/slurp submenu: region/full →
clipboard/file, saved to `~/Pictures/Screenshots`) · `keybinds` (the
cheatsheet, see below) · `ask` (free-text → claude CLI in a terminal;
auths via OAuth, no API key; pulled fresh via `npx
@anthropic-ai/claude-code@latest` rather than the nixpkgs package, which
lags model releases — `nodejs` is bundled for npx; REPL stays open)
- ✓ keybindings cheatsheet: `modules/home/keybinds.nix` is the **single
source** for both the Hyprland binds and the SUPER+? rofi list, so they
can't drift; `nomarchy-menu keybinds` renders the padded two-column
sheet (generated/mouse binds carried in its `extra` rows)
- ✓ shipped binds: `SUPER+Space``rofi -show drun` (quick launch) ·
`SUPER+M``nomarchy-menu` (main menu) · `SUPER+?` → the cheatsheet;
`SUPER+D` stays `-show drun`
- launcher icons: ✓ `show-icons` on, drawing from the theme's icon set
(Papirus, via the icon-themes work below)
- decision record: resolves the old Walker/Lua question — no GTK4
launcher, no second theming pipeline; the dispatcher owns the menu
structure, so the renderer stays swappable (we moved fuzzel → rofi 2.0
once mainline gained native Wayland, for its richer theming)
-**Menu modules from rofi plugins:** the old `calc` flow committed the
expression blind (result only in the *next* menu's `-mesg`) and `qalc -t`
misparsed common phrasings (`15% of 200``rem(15, 1 B)`, the natural-
language "of" tripping the CLI). Resolved by adopting purpose-built rofi
modi (`programs.rofi.plugins`), all themed through the same `.rasi`:
- **calc** → **rofi-calc**: live results as you type via libqalculate
directly (dodging the qalc-CLI "of" bug), Enter copies, menu persists
to chain calculations.
- **emoji** → **rofi-emoji** (new module, SUPER+CTRL+E): glyph picker,
copies via the plugin's Wayland clipboard adapter.
- **files** stays the hand-rolled `fd``rofi -dmenu` → xdg-open fuzzy
search: rofi-file-browser-extended was tried and dropped — its
navigate-a-tree model felt worse than flat fuzzy-find for a quick
launcher, and yazi (SUPER+E) already covers real browsing.
- **More menu modules from rofi tools:** the script-based counterparts
(run via `rofi -dmenu`, like the hand-rolled modules), each a deliberate
replacement of an existing flow: **rofi-network-manager** (a keyboard
wifi/VPN picker vs today's `nmtui`-in-terminal `network`), **rofi-rbw /
rofi-pass** (a secrets module — Bitwarden via rbw, or `pass` — pairs with
`keys.nix`), and **rofi-pulse-select** (an audio sink/source switcher).
Decide per-module whether it earns replacing the current path.
- **Theme parity with legacy:** summer-day/night now carry their legacy
bar layouts as `waybar.jsonc` whole-swaps (adapted: dead legacy script
modules dropped, Nerd-Fonts-v2 codepoints remapped to FontAwesome/v3,
logo button opens nomarchy-menu); the other four identity themes are
palette recolors and already match. Remaining: a visual pass over all
six on the live ISO
- **Per-theme rofi identity:** the `themes/<slug>/rofi.rasi` whole-swap
ships, and summer-day/night carry their legacy designs (inverted window,
green inputbar, yellow bottom-border). Remaining: author `.rasi`
identities for the other four ported themes if/when they want one (the
generated palette theme is the default and looks fine)
- **Faster switches:** move `backgrounds/` out of the flake source (the 86 MB
re-copy on every state write is the main eval tax), then pre-built theme
variants if still needed
- Greeter (tuigreet/SDDM) theming from the same JSON (Plymouth ships since
v1: `nomarchy.system.plymouth.*`, background tinted from the state file)
- Installer round 2: multi-disk BTRFS RAID, impermanence, BIOS/legacy
boot (v1 `nomarchy-install` is single-disk UEFI — see `pkgs/nomarchy-install`)
- launch-or-focus UX scripts (swayosd volume/brightness OSD ships since v1:
`nomarchy.osd.*`, media keys drive `swayosd-client`, themed from the JSON)
- **Distro branding, round 2:** `distroName = "Nomarchy"` ships
(os-release `PRETTY_NAME`, systemd-boot entries, ISO menu label).
✓ tuigreet greeting (`Welcome to <distroName>`) and a branded `users.motd`
(doubling as a helper cheat sheet), both keyed off `distroName`.
`isoImage.splashImage` — the vendored vector logo
(`modules/nixos/branding/logo.svg`, from legacy) recolored to the palette
accent on the theme base, built at ISO-build time (`hosts/live.nix`).
Remaining: `isoImage.grubTheme` so UEFI boot matches the isolinux splash
(needs a full grub theme dir), and the `distroId` question (it changes
`DEFAULT_HOSTNAME` and upstream `isNixos` checks — needs a test pass;
nixos-* CLI names stay regardless)
-**fastfetch branding:** `modules/home/fastfetch.nix`
(`nomarchy.fastfetch.enable`) — the vendored vector logo, recolored to
the palette accent and rendered to compact block-art via chafa at build
time (tracks the theme), fronting a curated module list. Replaces the
oversized legacy ASCII with a themed, sized logo.
-**Nomarchy logo font in Waybar:** vendored `Nomarchy.ttf`
(`modules/nixos/branding/`), installed via `fonts.packages`, and the
summer-day/night menu buttons now use its `U+F000` glyph with
`font-family: Nomarchy` pinned in their CSS (Nerd Fonts also occupy
U+F000, so the pin is required). The other themes have no logo button.
- **Quality-of-life command aliases:** assemble a curated collection of
shell aliases/abbreviations for common operations (git, nix, navigation,
the nomarchy helpers, …), themed into the zsh shell experience
(`modules/home/shell.nix`). Decide scope and which to ship on by default.
- **Theme-switch feedback:** ✓ the "rebuilding…" notification is now
persistent (timeout 0) and replaced in place by "applied ✓" / failure
via a synchronous tag, so a multi-minute switch never reads as a failed
selection. (An honest indeterminate indicator — HM gives no % — a
literal progress widget would be cosmetic; revisit only if wanted.)
- **Icon themes:** ✓ ships `papirus-icon-theme`; the resolved name lives on
`nomarchy.theme.iconTheme` (the JSON's optional `icons` field, else
Papirus-Dark/Light by `mode`) and feeds both Stylix's `gtk.iconTheme`
(Thunar/GTK apps) and rofi's `show-icons`. Remaining (optional): per-theme
`icons` overrides for the presets, or shipping more icon packs.
- **Nicer shell out of the box:** ✓ zsh is the default login shell, with a
starship prompt themed from the JSON, autosuggestions + syntax
highlighting, and modern-CLI ergonomics — `cat`→bat (theme "ansi", so it
tracks the palette), `ls`→eza, `cd`→zoxide, plus `lt`/`tree`.
`nomarchy.shell.enable`. (Deliberately did NOT alias grep→rg / find→fd:
their flags differ enough to surprise; `rg`/`fd` ship as themselves.)
- **Default application suite:** install a complete-workstation set
(vscode, libreoffice, gimp, inkscape, texlive-full, …) behind a
`nomarchy.apps.*` option surface so each is individually opt-out for
users who want a leaner machine. Watch closure size (texlive-full is
multi-GB — likely default-off or a lighter scheme).
-**Plymouth logo contrast:** the shipped art was a fixed navy that
vanished on dark bases. `modules/nixos/plymouth.nix` now recolors every
element from the palette at build time (flat fill, alpha kept):
logo/lock/bullet → text, entry/progress-box → surface, progress-bar →
accent. Reads on light and dark; follows the theme as of the last system
rebuild (like the background tint).
-**Hibernate double-unlock:** on resume from hibernate the LUKS
passphrase already gates the machine, but hypridle's `before_sleep_cmd`
also locked hyprlock, so the user typed a password twice. Fixed with a
`nomarchy-hibernate-unlock` systemd unit (`modules/nixos/default.nix`)
that dismisses hyprlock *after* a hibernate resume (`WantedBy
hibernate.target`, `After systemd-hibernate.service` → runs post-resume),
gated on the disk being LUKS-encrypted. Suspend (and the RAM-resume phase
of suspend-then-hibernate) keep locking — they have no passphrase gate.
-**Key agents & pinentry:** ships `modules/home/keys.nix`
(`nomarchy.keys.enable`): one agent — `services.gpg-agent` with
`enableSshSupport` fronts SSH, so a single `pinentry-qt` (native-Wayland,
Stylix-themed Qt) handles GPG and SSH passphrases alike. `SSH_AUTH_SOCK`
comes from the agent's zsh integration; cache TTLs (30 min / 2 h) govern
re-prompting. gnome-keyring stays the Secret Service (modern versions run
no SSH agent, so no socket contention); screen lock doesn't flush the
cache. Remaining (optional): a session-level `SSH_AUTH_SOCK` export so GUI
clients launched outside a shell also see the agent.
- **Sanitize & organize the repo:** a housekeeping pass for consistency
and clarity. Candidate targets (scope still to be defined): prune
now-redundant config (e.g. the installer still writes
`console.useXkbConfig` / `boot.initrd.systemd.enable` into `system.nix`,
which are distro-wide defaults as of the LUKS-keymap fix); reconcile the
README option tables with the actual `nomarchy.*` surface; audit
comments/docs for drift against the code; and re-check that every file
still earns its place under the `modules`/`hosts`/`themes`/`pkgs`/`tools`
rule of thumb.
- **Full docs review & restructure:** a dedicated pass over all the prose,
not just the code-adjacent cleanup the repo-sanitize item covers. The
README has grown to ~540 lines with a ~200-line roadmap inline — the
biggest single move is likely **splitting the roadmap out** (e.g.
`ROADMAP.md` / `docs/`) so the README stays a focused "what it is / how
to install / how to override" entry point. Also: reconcile every option
table against the live `nomarchy.*` surface (snapper and others are
missing); a pass over `docs/OVERRIDES.md` + `docs/TESTING.md` and the
`templates/downstream/README.md` for drift; check the install/first-run
story reads cleanly end to end; and decide whether anything wants a real
docs site vs. staying Markdown-in-repo. Pairs with the first-boot welcome
and control-center items (shared "how do I…" surface).
- **Laptop power / battery management:** a real power story behind a
`nomarchy.system.power.*` surface (`modules/nixos/power.nix`), replacing the
old "only `services.upower` for Waybar reporting" baseline:
-**profiles:** `power-profiles-daemon` ships by default (the upstream-
aligned choice — power-saver/balanced/performance via `powerprofilesctl`,
switched through polkit so no root prompt). A `power-profile` menu module
(in the SUPER+M picker) and the first Waybar `custom/*` indicator (click to
cycle) both **self-gate** on a battery being present + PPD running, so they
hide on desktops and under TLP — no system→home wiring. **TLP** is the
opt-in (`backend = "tlp"`) for deeper battery tuning; the two are mutually
exclusive (asserted).
-**thermal:** `thermald` behind `power.thermal.enable`; the installer
turns it on for a `GenuineIntel` CPU.
-**longevity:** `power.batteryChargeLimit` (e.g. 80) — a backend-
independent sysfs oneshot writes `charge_control_end_threshold`. Off by
default; the installer scaffolds it commented-out on laptops.
-**installer:** writes `power.laptop = true` (battery probe) and
`power.thermal.enable` (Intel) into the generated `system.nix`.
-**idle cohesion:** `modules/home/idle.nix` now suspends only on
battery (and sooner — 15 min vs the old fixed 30), gating the hypridle
suspend listener on a `nomarchy-on-ac` probe so a plugged-in machine
stays up mid-idle; lock/screen-off are unchanged on both sources.
logind's lid handling is left at its defaults (suspend on lid close,
ignore when docked) — an explicit "I'm done" that coheres with the
idle behaviour.
- Remaining: a boot-only→event-driven charge-limit re-apply (udev) if a
firmware resets the threshold on unplug.
- **Opt-in services & integrations:** the counterpart to the opt-*out*
application suite above — heavier or more personal integrations shipped
**off by default**, each a `nomarchy.services.<name>.enable` toggle a
downstream flips on in one line. Keeps the base lean while making common
additions trivial. Candidates by area:
- **cloud/sync:** Nextcloud client; Syncthing (`services.syncthing`)
- **local AI:** LM Studio (`lmstudio`); Ollama (`services.ollama`, optional
GPU accel) — pairs with the menu's Ask-Claude philosophy
- **containers/VMs:** Docker/Podman; libvirt + virt-manager
- **networking:** Tailscale (`services.tailscale`); WireGuard
- **gaming/media:** Steam (`programs.steam`); OBS Studio
- **devices:** printing (CUPS + Avahi); KDE Connect / phone integration;
OpenRGB
- **backup:** restic/borg (`services.restic`)
- **escape hatch:** Flatpak (`services.flatpak`) for apps outside nixpkgs
Decisions: the curated set; whether system services and GUI apps share one
surface or split (`nomarchy.services.*` system-side vs home-side packages);
and how it relates to `nomarchy.apps.*` (opt-out suite). Unfree entries are
already covered (`allowUnfree = true`).
- **Display / monitor management:** today Hyprland just uses
`monitor = ,preferred,auto,1` (mkDefault). Add a real external-monitor/dock
story: declarative per-output config, hotplug profiles (kanshi-style, or
Hyprland's own monitor rules), and optionally a GUI arranger (nwg-displays
writes Hyprland monitor config). Decide declarative-only vs GUI, and the
option shape (`nomarchy.monitors` vs plain Hyprland `monitor` lists).
- **Night light / blue-light filter:** schedulable colour-temperature shift
via `hyprsunset` (Hyprland-native; wlsunset/gammastep are alternatives) —
sunset/sunrise or a fixed schedule, with a menu + Waybar toggle.
- **Runtime keyboard-layout switching:** the session layout is a single
`nomarchy.keyboard.layout` today. Support a list of layouts with a toggle
bind (`hyprctl switchxkblayout` / xkb `grp:` options) and a Waybar
indicator for the active layout. Natural follow-on to the LUKS-keymap work
— keep the system (console/initrd) and session layouts in sync.
- **Do-Not-Disturb:** a swaync DND toggle (`swaync-client -dn`) wired into
the menu and a Waybar indicator, to silence notifications for
presentations/focus.
- **Snapshot browse/restore UX:** snapper already takes BTRFS timeline
snapshots (`nomarchy.system.snapper`); surface them from the desktop — a
rofi menu (or btrfs-assistant) to browse/diff/restore and boot-from-
snapshot — so rollback isn't CLI-only.
- **Update awareness:** updates are manual today (`sys-update`/`home-update`);
add a Waybar indicator / notification when flake inputs are stale or a new
nixpkgs rev is available (optionally with a pending-change count). Augments,
never replaces, the explicit rebuild flow.
- **"nomarchy" control center:** a single TUI/GUI front-end over the common
toggles — theme, power profile, opt-in services, display, DND — built on
the same `nomarchy-theme-sync` / `nomarchy.*` surface the menu already
uses. Plus a first-boot welcome that lands new installs in a guided "pick
your theme / essentials" flow (ties into the branding work).
## Known issues & follow-ups
-**Yazi TOML parse error on startup:** yazi 26.x made fetchers'
`group` field required, so `[[plugin.prepend_fetchers]]` failed to parse
and yazi fell back to presets. Fixed by adding `group = "git"` to both
git fetcher entries in `modules/home/yazi.nix` (the git plugin's
`setup()` only renders the linemode; the fetcher still needs registering).
-**Starship prompt styling:** dropped the powerline look (the `[]`
separator glyph + `bg:` fills) for a flat prompt — bold-accent directory,
warn git branch, subtext status/duration, `` character, all plain
colored text (`modules/home/shell.nix`). Also wired in `cmd_duration`,
which was configured but never referenced in `format`.
-**xfce deprecation warnings:** moved `xfce.exo``pkgs.xfce4-exo` and
the three Thunar plugins to their new top-level names in
`modules/nixos/file-manager.nix`; the eval warnings are gone.
-**Rofi function keybindings:** direct `SUPER+CTRL+<mnemonic>` binds jump
straight to each `nomarchy-menu` module — V clipboard · C calc · W web ·
F files · N network · B bluetooth · S capture · A ask — added to
`modules/home/keybinds.nix`, so they also show in the SUPER+? cheatsheet.
-**Enable nix-ld by default:** `programs.nix-ld.enable` is on distro-wide
(`modules/nixos/default.nix`), so prebuilt/foreign dynamically-linked
binaries run out of the box.
-**Hyprland border colors off for some themes:** the cause was v1
forcing an `accent→accentAlt` gradient on every theme; legacy used a
*solid* border (accent for nord/retro-82/lumon, the text tone for
kanagawa/summer-day/summer-night). Added a palette-resolved `border`
field (`{active, inactive}`) to the theme schema (`theme.nix`), consumed
solid in `hyprland.nix`, and declared it in every preset so a switch
always replaces it. All six identity themes now match legacy exactly.
-**Waybar shows non-existent workspaces:** the v1 summer port "fixed"
legacy's deprecated `persistent_workspaces` (underscore — silently
ignored by current Waybar, so legacy only ever showed existing
workspaces) into the modern `persistent-workspaces` (hyphen), which
Waybar honours → all 10 rendered as phantoms. Dropped the block from
`themes/summer-{day,night}/waybar.jsonc`; the other themes use the
generated `waybar.nix`, which never had it, so they already match.
-**GTK/Qt ignore the theme's light/dark mode:** Stylix set `polarity`
but not the `org.freedesktop.appearance color-scheme` that GTK4/libadwaita
and Qt6 read via the portal, so a light theme could still render apps
dark. `stylix.nix` now sets `dconf` `org/gnome/desktop/interface
color-scheme` = `prefer-light`/`prefer-dark` from `t.mode`
(xdg-desktop-portal-gtk already ships, and `programs.dconf` is on
system-side), so libadwaita/Qt follow the palette. Needs a live session
to confirm the portal picks it up for all apps.

17
flake.lock generated
View File

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

View File

@@ -10,9 +10,12 @@
};
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.home-manager.follows = "home-manager";
};
# Pinned by Nomarchy so distro + hardware quirks are tested together
@@ -32,6 +35,9 @@
pkgs = import nixpkgs {
inherit system;
overlays = [ self.overlays.default ];
# Matches nixpkgs.config.allowUnfree in modules/nixos — the
# standalone HM generation must come from like-configured pkgs.
config.allowUnfree = true;
};
# The guided installer — live-ISO only (not in the overlay): it bakes
@@ -142,6 +148,7 @@
specialArgs = { inherit username; };
modules = [
self.nixosModules.nomarchy
{ nomarchy.system.stateFile = ./theme-state.json; }
./hosts/default/configuration.nix
];
};
@@ -245,6 +252,20 @@
({ lib, ... }: {
nomarchy.system.snapper.enable = true;
nomarchy.system.greeter.autoLogin = username;
# The installer writes these (keyboard-everywhere
# incl. the LUKS prompt) — mirror them or the
# initrd/vconsole pins don't match an offline
# install's rebuild set.
console.useXkbConfig = true;
boot.initrd.systemd.enable = true;
# nixos-generate-config keys microcode off
# enableRedistributableFirmware (now on): the
# real machine enables exactly one vendor —
# pin both, or the offline install builds
# amd-ucode/intel-ucode from source (found the
# hard way: the 2700s regression hang).
hardware.cpu.amd.updateMicrocode = true;
hardware.cpu.intel.updateMicrocode = true;
swapDevices = [{ device = "/swap/swapfile"; }];
boot.resumeDevice = "/dev/disk/by-uuid/00000000-0000-0000-0000-000000000000";
boot.kernelParams = [ "resume_offset=1" ];
@@ -270,6 +291,20 @@
representativeInstall.config.system.build.toplevel
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
# user's hostname/username/UUIDs (etc, initrd, units,
# toplevel, HM activation). inputDerivation pins each
@@ -278,6 +313,20 @@
representativeInstall.config.system.build.etc.inputDerivation
representativeInstall.config.system.build.initialRamdisk.inputDerivation
template.homeConfigurations.${username}.activationPackage.inputDerivation
# home-files COPIES user-dbus-services instead of
# referencing it (same story as the self pin above) —
# a custom-username install rebuilds home-files and
# needs its direct inputs present.
template.homeConfigurations.${username}.config.home-files.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
# (their parents are themselves rebuilt, so parent
@@ -299,6 +348,18 @@
# tries to BUILD remarshal, whose test closure pulls
# matplotlib/ffmpeg/x265 (the ~1900-drv cascade).
pkgs.remarshal
# system-path/initrd-bin-env are buildEnvs rebuilt per
# machine; their shared builder.pl is build-time-only
# (never in any runtime closure) — an empty env's
# inputDerivation retains it.
(pkgs.buildEnv { name = "nomarchy-pin-buildenv"; paths = [ ]; }).inputDerivation
# /etc/dbus-1 reassembly wants this setup hook.
pkgs.findXMLCatalogs
# nixos-generate-config keys microcode off
# enableRedistributableFirmware (now on): exactly one
# vendor per real machine, so pin both ucode cpios.
pkgs.microcode-amd
pkgs.microcode-intel
# …and one representative disko script: its tool closure
# (file, which, wrapper hooks, …) isn't otherwise on the

View File

@@ -3,15 +3,51 @@
# exists to test the distro end-to-end on real hardware.
{ lib, pkgs, username, nomarchySrc, ... }:
let
# ISO boot splash: the Nomarchy monogram recolored to the palette accent,
# centred on the theme base. Built from the vendored vector logo and the
# live theme-state.json (tokyo-night by default). Shows on the isolinux
# (BIOS) boot menu; UEFI/GRUB still uses the stock theme (see roadmap).
state = builtins.fromJSON (builtins.readFile ../theme-state.json);
isoColor = key: fallback: lib.removePrefix "#" ((state.colors or { }).${key} or fallback);
isoSplash = pkgs.runCommand "nomarchy-iso-splash.png"
{ nativeBuildInputs = [ pkgs.imagemagick pkgs.librsvg ]; } ''
rsvg-convert -h 320 ${../modules/nixos/branding/logo.svg} > logo.png
magick logo.png -fill "#${isoColor "accent" "7aa2f7"}" -colorize 100 logo-c.png
magick -size 1920x1080 xc:"#${isoColor "base" "1a1b26"}" \
logo-c.png -gravity center -composite $out
'';
in
{
networking.hostName = "nomarchy-live";
isoImage.volumeID = lib.mkForce "NOMARCHY_LIVE";
isoImage.edition = lib.mkForce "live";
isoImage.splashImage = isoSplash;
# The minimal-CD profile enables wpa_supplicant; Nomarchy ships
# NetworkManager and the two must not run together.
networking.wireless.enable = lib.mkForce false;
# The minimal-CD profile slims the image for a CONSOLE installer; this
# ISO is the desktop, so re-enable what it strips. Above all
# fontconfig (upstream forces it off at mkOverride 500): without it
# no configured family resolves — Waybar icons render as tofu and
# Ghostty silently falls back to the wrong font (seen on the
# Latitude 5410). Normal priority (100) beats the override.
fonts.fontconfig.enable = true;
# No boot splash on the install medium: the installer ISO boots its
# own initrd path (squashfs), and visibility of boot messages is
# worth more than polish here. Installed systems get the splash.
nomarchy.system.plymouth.enable = false;
xdg.icons.enable = true;
xdg.mime.enable = true;
xdg.autostart.enable = true;
documentation.enable = true;
documentation.man.enable = true;
# Do NOT touch networking.wireless here: since NixOS 26.05 the
# NetworkManager module drives its wifi backend through it
# (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 ──────────────
users.users.${username} = {
@@ -24,7 +60,7 @@
# Boot straight into Hyprland once; logging out lands on tuigreet.
services.greetd.settings.initial_session = {
command = "Hyprland";
command = "start-hyprland";
user = username;
};
@@ -70,6 +106,12 @@
# ── Live-session desktop tweaks ─────────────────────────────────────
home-manager.users.${username} = {
# No idle lock/suspend on the install medium: an offline install
# runs 20-30 min unattended, and hypridle would blank the display
# then SUSPEND the machine mid-install (it did — the install-hung
# regression). Installed systems keep idle management.
nomarchy.idle.enable = false;
wayland.windowManager.hyprland.settings = {
# QEMU (and some panels) report a tiny "preferred" mode; ask for
# the highest resolution instead.

View File

@@ -47,6 +47,9 @@
pkgs = import nixpkgs {
inherit system;
overlays = [ nomarchy.overlays.default ];
# Matches nixpkgs.config.allowUnfree in modules/nixos: the system
# and the standalone HM desktop see the same package set.
config.allowUnfree = true;
};
in
{
@@ -62,6 +65,9 @@
# (`nomarchy-theme-sync apply` → `home-manager switch`) works
# out of the box — same pinned input as the desktop modules.
{ environment.systemPackages = [ home-manager.packages.${system}.home-manager ]; }
# System-side theme consumers (Plymouth splash background)
# read the same JSON the desktop does.
{ nomarchy.system.stateFile = src + "/theme-state.json"; }
]
++ hardwareModules
++ [

View File

@@ -12,13 +12,29 @@
./waybar.nix
./ghostty.nix
./btop.nix
./rofi.nix # launcher theming + the nomarchy-menu dispatcher
./swaync.nix # notification daemon, themed from the same JSON
./idle.nix # hyprlock + hypridle, themed from the same JSON
./yazi.nix # flagship TUI file manager, themed + plugins
./osd.nix # swayosd volume/brightness OSD, themed
./shell.nix # zsh + starship + bat/eza/zoxide, themed
./keys.nix # gpg-agent (fronts SSH) + pinentry-qt
./fastfetch.nix # system info with the themed Nomarchy logo
];
# Clipboard history (wl-paste watcher); browsed via the SUPER+CTRL+V
# menu module.
services.cliphist.enable = true;
# Wifi from the bar: nm-applet lives in waybar's tray (SNI flag via
# preferStatusNotifierItems — without it there is no tray icon).
services.network-manager-applet.enable = true;
xsession.preferStatusNotifierItems = true;
home.stateVersion = "26.05";
home.packages = with pkgs; [
awww # wallpaper daemon with animated transitions (the swww fork)
fuzzel # launcher / dmenu (used by the theme picker bind)
libnotify
];

View File

@@ -0,0 +1,50 @@
# fastfetch — system summary fronted by the Nomarchy monogram. The logo is
# rendered from the vendored vector mark (modules/nixos/branding/logo.svg)
# via chafa at build time, recolored to the palette accent — so it's a
# compact, themed block-art logo that tracks the theme on each
# home-manager switch (rather than the oversized legacy ASCII).
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy;
c = cfg.theme.colors;
logo = pkgs.runCommand "nomarchy-fastfetch-logo"
{ nativeBuildInputs = [ pkgs.imagemagick pkgs.librsvg pkgs.chafa ]; } ''
rsvg-convert -w 220 ${../nixos/branding/logo.svg} > logo.png
magick logo.png -fill "${c.accent}" -colorize 100 logo-c.png
chafa --format symbols --symbols block --size 20x10 --colors full --polite on logo-c.png > $out
'';
in
{
config = lib.mkIf cfg.fastfetch.enable {
programs.fastfetch = {
enable = true;
settings = {
logo = {
type = "file-raw";
source = "${logo}";
padding = { top = 1; left = 2; right = 3; };
};
display.separator = " ";
modules = [
"break"
{ type = "os"; key = "distro"; keyColor = "blue"; }
{ type = "kernel"; key = "kernel"; keyColor = "blue"; }
{ type = "uptime"; key = "uptime"; keyColor = "blue"; }
{ type = "packages"; key = "pkgs"; keyColor = "blue"; }
{ type = "wm"; key = "wm"; keyColor = "blue"; }
{ type = "shell"; key = "shell"; keyColor = "blue"; }
{ type = "terminal"; key = "term"; keyColor = "blue"; }
"break"
{ type = "cpu"; key = "cpu"; keyColor = "magenta"; }
{ type = "gpu"; key = "gpu"; keyColor = "magenta"; }
{ type = "memory"; key = "memory"; keyColor = "magenta"; }
{ type = "disk"; key = "disk"; keyColor = "magenta"; }
"break"
{ type = "colors"; symbol = "circle"; }
];
};
};
};
}

View File

@@ -30,12 +30,14 @@ in
palette = lib.imap0 (i: color: "${toString i}=${color}") t.ansi;
# ── Chrome ────────────────────────────────────────────────────
# background-opacity is theme-driven (normal priority — use the
# CLI); the rest are mkDefault so a plain home.nix value wins.
background-opacity = t.ui.terminalOpacity;
window-padding-x = 12;
window-padding-y = 12;
window-decoration = false;
gtk-single-instance = true;
confirm-close-surface = false;
window-padding-x = lib.mkDefault 12;
window-padding-y = lib.mkDefault 12;
window-decoration = lib.mkDefault false;
gtk-single-instance = lib.mkDefault true;
confirm-close-surface = lib.mkDefault false;
};
};
}

View File

@@ -1,13 +1,32 @@
# Hyprland — fully driven by config.nomarchy.theme (theme-state.json).
# Gaps, borders and colors are baked from the JSON at eval time; theme
# changes arrive via `home-manager switch`.
{ config, lib, ... }:
{ config, lib, pkgs, ... }:
let
t = config.nomarchy.theme;
c = t.colors;
inherit (config.nomarchy.lib) rgb rgba;
# swayosd's `--input-volume mute-toggle` draws the OSD but never flips the
# source mute (verified on hardware: wpctl toggles the state, swayosd's
# input-mute path is a no-op — unlike its working output-mute path). Do the
# real toggle with wpctl, then re-render the same icon + volume-bar OSD the
# native input path would, reading the resulting state back from wpctl:
# --custom-progress is the mic level (0.0-1.0), --custom-icon the
# muted/active glyph. Pure bash builtins for parsing so the script needs no
# PATH beyond the two pinned tools.
micMute = pkgs.writeShellScript "nomarchy-mic-mute" ''
${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_SOURCE@ toggle
out=$(${pkgs.wireplumber}/bin/wpctl get-volume @DEFAULT_SOURCE@)
set -- $out # "Volume: 0.85 [MUTED]" -> $2 is the level
case "$out" in
*MUTED*) icon=microphone-sensitivity-muted-symbolic ;;
*) icon=microphone-sensitivity-high-symbolic ;;
esac
${pkgs.swayosd}/bin/swayosd-client --custom-icon "$icon" --custom-progress "$2"
'';
# SUPER+1..9 / SUPER+SHIFT+1..9 workspace binds, generated.
workspaceBinds = builtins.concatLists (builtins.genList
(i:
@@ -17,6 +36,11 @@ let
"$mod SHIFT, ${ws}, movetoworkspace, ${ws}"
])
9);
# The keyboard binds — single source shared with the SUPER+? cheatsheet
# (rofi.nix renders the same list). Edit them in ./keybinds.nix.
keybinds = import ./keybinds.nix;
mkBind = b: "${b.mods}, ${b.key}, ${b.action}";
in
{
wayland.windowManager.hyprland = lib.mkIf config.nomarchy.hyprland.enable {
@@ -35,8 +59,13 @@ in
"$mod" = "SUPER";
"$terminal" = config.nomarchy.terminal;
monitor = [ ",preferred,auto,1" ];
# mkDefault so a downstream `monitor = [...]` replaces it with a
# plain assignment (the live ISO uses mkForce for the same reason).
monitor = lib.mkDefault [ ",preferred,auto,1" ];
# exec-once is a list at normal priority, so downstream additions
# CONCATENATE (your autostart runs alongside ours) rather than
# replace. Same for the bind* lists below.
exec-once = [
# nixpkgs' swww is awww (renamed fork); the binary is awww-daemon.
"awww-daemon"
@@ -46,14 +75,21 @@ in
];
# ── Theme-driven look ──────────────────────────────────────────
# These flow from theme-state.json and stay at NORMAL priority:
# change them with `nomarchy-theme-sync set ui.<key>` (the intended
# path), or lib.mkForce in home.nix to hardcode against the theme.
# The non-theme knobs around them are lib.mkDefault — override
# those with a plain home.nix assignment. See docs/OVERRIDES.md.
general = {
gaps_in = t.ui.gapsIn;
gaps_out = t.ui.gapsOut;
border_size = t.ui.borderSize;
"col.active_border" = "${rgb c.accent} ${rgb c.accentAlt} 45deg";
"col.inactive_border" = rgb c.overlay;
layout = "dwindle";
resize_on_border = true;
# Solid borders from theme.border (per-theme, palette-resolved) —
# matches the legacy identity themes, which never used a gradient.
"col.active_border" = rgb t.border.active;
"col.inactive_border" = rgb t.border.inactive;
layout = lib.mkDefault "dwindle";
resize_on_border = lib.mkDefault true;
};
decoration = {
@@ -62,25 +98,26 @@ in
inactive_opacity = t.ui.inactiveOpacity;
blur = {
enabled = t.ui.blur;
size = 8;
passes = 2;
popups = true;
size = lib.mkDefault 8;
passes = lib.mkDefault 2;
popups = lib.mkDefault true;
};
shadow = {
enabled = t.ui.shadow;
range = 20;
render_power = 3;
range = lib.mkDefault 20;
render_power = lib.mkDefault 3;
color = rgba c.mantle "aa";
};
};
# ── Behaviour (mkDefault → plain home.nix assignment overrides) ──
animations = {
enabled = true;
bezier = [
enabled = lib.mkDefault true;
bezier = lib.mkDefault [
"smooth, 0.25, 0.1, 0.25, 1.0"
"snappy, 0.6, 0.0, 0.1, 1.0"
];
animation = [
animation = lib.mkDefault [
"windows, 1, 4, snappy, popin 85%"
"border, 1, 8, smooth"
"fade, 1, 5, smooth"
@@ -88,53 +125,56 @@ in
];
};
# ── Behaviour ──────────────────────────────────────────────────
input = {
kb_layout = "us";
follow_mouse = 1;
touchpad.natural_scroll = true;
# kb_layout/variant come from nomarchy.keyboard.* — set that
# option (the installer writes it; it also drives tty/LUKS).
kb_layout = config.nomarchy.keyboard.layout;
kb_variant = config.nomarchy.keyboard.variant;
follow_mouse = lib.mkDefault 1;
touchpad.natural_scroll = lib.mkDefault true;
};
# dwindle:pseudotile was removed in Hyprland 0.55 (the `pseudo`
# dispatcher still exists); setting it aborts config parsing.
dwindle.preserve_split = true;
dwindle.preserve_split = lib.mkDefault true;
# Hyprland 0.51+ replaced gestures:workspace_swipe with the
# gesture keyword — without this, touchpads have NO gestures.
gesture = lib.mkDefault [
"3, horizontal, workspace"
"4, pinch, fullscreen"
];
misc = {
disable_hyprland_logo = true;
disable_splash_rendering = true;
force_default_wallpaper = 0;
disable_hyprland_logo = lib.mkDefault true;
disable_splash_rendering = lib.mkDefault true;
force_default_wallpaper = lib.mkDefault 0;
};
bind = [
"$mod, Return, exec, $terminal"
"$mod, D, exec, fuzzel"
"$mod, Q, killactive"
"$mod, F, fullscreen"
"$mod, V, togglefloating"
"$mod SHIFT, E, exit"
# Rendered from ./keybinds.nix (the cheatsheet reads the same list),
# plus the generated per-workspace binds. Like exec-once above this
# stays a normal-priority list, so a downstream `bind = [...]`
# concatenates rather than replaces.
bind = map mkBind keybinds.binds ++ workspaceBinds;
# Theme picker: fuzzel menu over the presets; apply writes the
# state and runs home-manager switch (progress via notify-send).
"$mod, T, exec, nomarchy-theme-sync apply \"$(nomarchy-theme-sync list | fuzzel --dmenu --prompt 'theme> ')\""
# Cycle the current theme's wallpapers (instant, no rebuild).
"$mod SHIFT, T, exec, nomarchy-theme-sync bg next"
# Media keys via swayosd-client: it performs the action AND shows
# the on-screen display (the nomarchy.osd module). e = repeat,
# l = also on a locked screen — the standard flags for hardware keys.
bindel = [
", XF86AudioRaiseVolume, exec, swayosd-client --output-volume raise"
", XF86AudioLowerVolume, exec, swayosd-client --output-volume lower"
", XF86MonBrightnessUp, exec, swayosd-client --brightness raise"
", XF86MonBrightnessDown, exec, swayosd-client --brightness lower"
];
# Focus
"$mod, H, movefocus, l"
"$mod, L, movefocus, r"
"$mod, K, movefocus, u"
"$mod, J, movefocus, d"
# Screenshot region to clipboard
", Print, exec, grim -g \"$(slurp)\" - | wl-copy"
] ++ workspaceBinds;
binde = [
", XF86AudioRaiseVolume, exec, pamixer -i 5"
", XF86AudioLowerVolume, exec, pamixer -d 5"
", XF86AudioMute, exec, pamixer -t"
", XF86MonBrightnessUp, exec, brightnessctl set +5%"
", XF86MonBrightnessDown, exec, brightnessctl set 5%-"
bindl = [
", XF86AudioMute, exec, swayosd-client --output-volume mute-toggle"
# swayosd's input mute-toggle is broken on 0.2.1 (see micMute above).
", XF86AudioMicMute, exec, ${micMute}"
", XF86AudioPlay, exec, playerctl play-pause"
", XF86AudioPause, exec, playerctl play-pause"
", XF86AudioNext, exec, playerctl next"
", XF86AudioPrev, exec, playerctl previous"
];
bindm = [

93
modules/home/idle.nix Normal file
View File

@@ -0,0 +1,93 @@
# hyprlock + hypridle — screen locking and idle management, themed from
# theme-state.json. One concern, one file: hypridle drives WHEN (idle
# lock, display off, suspend, lock-before-sleep), hyprlock is the
# themed lock screen itself (also behind the power menu's Lock entry).
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy;
t = cfg.theme;
c = t.colors;
inherit (config.nomarchy.lib) rgb;
# Exits 0 when running on AC: a mains adapter reports `online` 1 (battery
# supplies have no `online` node, so they never match). Used as
# `${onAc} || <action>` so the action is skipped while plugged in.
onAc = pkgs.writeShellScript "nomarchy-on-ac" ''
for f in /sys/class/power_supply/*/online; do
[ -r "$f" ] && [ "$(cat "$f")" = "1" ] && exit 0
done
exit 1
'';
in
{
config = lib.mkIf cfg.idle.enable {
programs.hyprlock = {
enable = true;
settings = {
general.hide_cursor = true;
background = [{
monitor = "";
color = rgb c.base;
}];
input-field = [{
monitor = "";
size = "300, 50";
outline_thickness = t.ui.borderSize;
dots_size = 0.25;
outer_color = rgb c.accent;
inner_color = rgb c.surface;
font_color = rgb c.text;
check_color = rgb c.warn;
fail_color = rgb c.bad;
rounding = t.ui.rounding;
placeholder_text = "<i>password</i>";
}];
label = [{
monitor = "";
text = "$TIME";
color = rgb c.text;
font_size = 64;
font_family = t.fonts.ui;
position = "0, 120";
halign = "center";
valign = "center";
}];
};
};
services.hypridle = {
enable = true;
settings = {
general = {
lock_cmd = "pidof hyprlock || hyprlock";
# Locks before every sleep. For suspend that's exactly right; on an
# encrypted hibernate the LUKS resume already gates the machine, so
# the system side dismisses this lock post-resume to avoid a double
# unlock — see nomarchy-hibernate-unlock in modules/nixos/default.nix.
before_sleep_cmd = "loginctl lock-session";
after_sleep_cmd = "hyprctl dispatch dpms on";
};
listener = [
# Lock and screen-off are the same on either power source —
# they're about privacy and the panel, not battery.
{ timeout = 300; on-timeout = "loginctl lock-session"; }
{
timeout = 600;
on-timeout = "hyprctl dispatch dpms off";
on-resume = "hyprctl dispatch dpms on";
}
# Suspend only on battery, and sooner than the old fixed 30 min
# (it now only fires unplugged). Plugged in, the machine stays
# up — long builds, media, presentations aren't killed mid-idle.
# Closing the lid still suspends on AC (logind's default): that's
# an explicit "I'm done", distinct from sitting idle.
{ timeout = 900; on-timeout = "${onAc} || systemctl suspend"; }
];
};
};
};
}

67
modules/home/keybinds.nix Normal file
View File

@@ -0,0 +1,67 @@
# Canonical keybind list — the single source of truth for BOTH the
# Hyprland `bind` strings (hyprland.nix) and the SUPER+? cheatsheet
# rendered by nomarchy-menu (rofi.nix). One place, no second renderer to
# keep in sync — the same rule the theme pipeline follows.
#
# mods — modifier string, Hyprland syntax ("$mod", "$mod SHIFT", "")
# key — the key name
# action — the Hyprland dispatcher (+args) that the bind runs
# desc — human label, shown in the cheatsheet
#
# `extra` holds cheatsheet-only rows for binds generated elsewhere
# (per-workspace numbers, mouse drags) so they still show up under SUPER+?.
{
binds = [
{ mods = "$mod"; key = "Return"; action = "exec, $terminal"; desc = "Open terminal"; }
{ mods = "$mod"; key = "Space"; action = "exec, rofi -show drun"; desc = "Quick launch (apps)"; }
{ mods = "$mod"; key = "D"; action = "exec, rofi -show drun"; desc = "App launcher"; }
{ mods = "$mod"; key = "M"; action = "exec, nomarchy-menu"; desc = "Main menu"; }
{ mods = "$mod"; key = "E"; action = "exec, $terminal -e yazi"; desc = "File manager (yazi)"; }
{ mods = "$mod"; key = "Q"; action = "killactive"; desc = "Close window"; }
{ mods = "$mod"; key = "F"; action = "fullscreen"; desc = "Fullscreen"; }
{ mods = "$mod"; key = "V"; action = "togglefloating"; desc = "Toggle floating"; }
{ mods = "$mod SHIFT"; key = "E"; action = "exit"; desc = "Exit Hyprland"; }
# Theme picker (menu dispatcher): apply writes the state and runs
# home-manager switch (progress via notify-send).
{ mods = "$mod"; key = "T"; action = "exec, nomarchy-menu theme"; desc = "Theme picker"; }
# Cycle the current theme's wallpapers (instant, no rebuild).
{ mods = "$mod SHIFT"; key = "T"; action = "exec, nomarchy-theme-sync bg next"; desc = "Next wallpaper"; }
# Power menu via the dispatcher. Not Escape: Super+Escape gets
# swallowed before reaching the dispatcher on some setups.
{ mods = "$mod"; key = "X"; action = "exec, nomarchy-menu power"; desc = "Power menu"; }
{ mods = "$mod"; key = "N"; action = "exec, swaync-client -t"; desc = "Notification centre"; }
# SUPER+? (the "question" keysym already implies Shift on most layouts).
{ mods = "$mod"; key = "question"; action = "exec, nomarchy-menu keybinds"; desc = "Keybindings cheatsheet"; }
# Menu functions — SUPER+CTRL+<mnemonic> jumps straight to a
# nomarchy-menu module (all also reachable from the SUPER+M picker).
{ mods = "$mod CTRL"; key = "V"; action = "exec, nomarchy-menu clipboard"; desc = "Clipboard history"; }
{ mods = "$mod CTRL"; key = "C"; action = "exec, nomarchy-menu calc"; desc = "Calculator"; }
{ mods = "$mod CTRL"; key = "W"; action = "exec, nomarchy-menu web"; desc = "Web search"; }
{ mods = "$mod CTRL"; key = "F"; action = "exec, nomarchy-menu files"; desc = "File search"; }
{ mods = "$mod CTRL"; key = "E"; action = "exec, nomarchy-menu emoji"; desc = "Emoji picker"; }
{ mods = "$mod CTRL"; key = "N"; action = "exec, nomarchy-menu network"; desc = "Network (nmtui)"; }
{ mods = "$mod CTRL"; key = "B"; action = "exec, nomarchy-menu bluetooth"; desc = "Bluetooth"; }
{ mods = "$mod CTRL"; key = "S"; action = "exec, nomarchy-menu capture"; desc = "Screenshot / capture"; }
{ mods = "$mod CTRL"; key = "A"; action = "exec, nomarchy-menu ask"; desc = "Ask Claude"; }
# Focus
{ mods = "$mod"; key = "H"; action = "movefocus, l"; desc = "Focus left"; }
{ mods = "$mod"; key = "L"; action = "movefocus, r"; desc = "Focus right"; }
{ mods = "$mod"; key = "K"; action = "movefocus, u"; desc = "Focus up"; }
{ mods = "$mod"; key = "J"; action = "movefocus, d"; desc = "Focus down"; }
# Screenshot region to clipboard (the menu's Capture module has more).
{ mods = ""; key = "Print"; action = "exec, grim -g \"$(slurp)\" - | wl-copy"; desc = "Screenshot region clipboard"; }
];
extra = [
{ keys = "SUPER + 1-9"; desc = "Switch to workspace 1-9"; }
{ keys = "SUPER + SHIFT + 1-9"; desc = "Move window to workspace 1-9"; }
{ keys = "SUPER + drag"; desc = "Move (LMB) / resize (RMB) window"; }
{ keys = "Volume / Brightness"; desc = "Hardware keys, shown via the OSD"; }
];
}

42
modules/home/keys.nix Normal file
View File

@@ -0,0 +1,42 @@
# SSH + GPG agent — one agent for everything.
#
# gpg-agent fronts SSH (enableSshSupport), so a single pinentry handles
# every passphrase: GPG signing/decryption and SSH key unlocks alike. SSH
# keys are added with `ssh-add` (gpg-agent stores them by keygrip).
#
# Pinentry is pinentry-qt — reliable native-Wayland under Hyprland (no
# GNOME session to lean on) and themed by Stylix's Qt config, so the
# passphrase dialog tracks the palette.
#
# SSH_AUTH_SOCK is exported by the agent's shell integration (zsh, on via
# home.shell.enableZshIntegration in shell.nix). Terminal git/ssh is the
# supported path; a GUI client launched outside a shell won't inherit the
# socket — revisit with a session-level export if that's ever wanted.
#
# gnome-keyring (system side) stays the Secret Service for application
# secrets; modern gnome-keyring no longer runs an SSH agent, so there is no
# SSH_AUTH_SOCK contention with gpg-agent. Screen lock (hyprlock/hypridle)
# doesn't flush the agent cache — unlocked keys persist across a lock, as
# expected; the cache TTLs below govern re-prompting instead.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy;
in
{
config = lib.mkIf cfg.keys.enable {
programs.gpg.enable = true;
services.gpg-agent = {
enable = true;
enableSshSupport = true;
pinentry.package = pkgs.pinentry-qt;
# Cache unlocked keys for a working session, then re-prompt.
defaultCacheTtl = 1800; # 30 min since last use
maxCacheTtl = 7200; # 2 h hard cap
defaultCacheTtlSsh = 1800;
maxCacheTtlSsh = 7200;
};
};
}

View File

@@ -23,6 +23,25 @@
description = "Terminal emulator command, used by keybinds and $TERMINAL.";
};
keyboard.layout = lib.mkOption {
type = lib.types.str;
default = "us";
example = "de";
description = ''
XKB layout for the Hyprland session. The console (and the LUKS
passphrase prompt) take theirs from the system side the
installer writes services.xserver.xkb + console.useXkbConfig
into system.nix with the same value.
'';
};
keyboard.variant = lib.mkOption {
type = lib.types.str;
default = "";
example = "nodeadkeys";
description = "XKB variant for the Hyprland session.";
};
package = lib.mkOption {
type = lib.types.package;
default = pkgs.nomarchy-theme-sync;
@@ -44,6 +63,14 @@
# ── Component toggles ──────────────────────────────────────────
hyprland.enable = lib.mkEnableOption "Nomarchy's Hyprland configuration" // { default = true; };
waybar.enable = lib.mkEnableOption "Nomarchy's Waybar configuration" // { default = true; };
rofi.enable = lib.mkEnableOption "Nomarchy's themed rofi launcher + the nomarchy-menu dispatcher" // { default = true; };
swaync.enable = lib.mkEnableOption "swaync notifications, themed from the state file" // { default = true; };
idle.enable = lib.mkEnableOption "hyprlock + hypridle (idle lock, display off, suspend)" // { default = true; };
yazi.enable = lib.mkEnableOption "the yazi TUI file manager, themed with a curated plugin set" // { default = true; };
osd.enable = lib.mkEnableOption "swayosd on-screen display for volume/brightness/mute" // { default = true; };
shell.enable = lib.mkEnableOption "the zsh shell experience (starship prompt, bat/eza/zoxide)" // { default = true; };
keys.enable = lib.mkEnableOption "the SSH + GPG agent (gpg-agent fronting SSH, pinentry-qt)" // { default = true; };
fastfetch.enable = lib.mkEnableOption "fastfetch system info fronted by the themed Nomarchy logo" // { default = true; };
ghostty.enable = lib.mkEnableOption "Nomarchy's Ghostty configuration" // { default = true; };
btop.enable = lib.mkEnableOption "btop with the per-theme nomarchy theme" // { default = true; };
stylix.enable = lib.mkEnableOption "Stylix theming for the long tail of apps (GTK, Qt, cursors)" // { default = true; };

55
modules/home/osd.nix Normal file
View File

@@ -0,0 +1,55 @@
# swayosd — on-screen display for volume / brightness / mute (and caps
# lock). The media keys drive it via swayosd-client (it performs the
# action AND shows the OSD), so changing volume or brightness gives the
# visual feedback that bare wpctl/brightnessctl didn't. Themed from
# theme-state.json. Brightness needs the backlight udev rule shipped
# system-side (modules/nixos/default.nix → services.udev.packages).
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy;
t = cfg.theme;
c = t.colors;
style = pkgs.writeText "swayosd-style.css" ''
window#osd {
border-radius: ${toString t.ui.rounding}px;
border: ${toString t.ui.borderSize}px solid ${c.accent};
background: alpha(${c.base}, 0.92);
}
window#osd #container { margin: 16px; }
window#osd image,
window#osd label { color: ${c.text}; }
window#osd progressbar:disabled,
window#osd image:disabled { opacity: 0.5; }
window#osd progressbar,
window#osd segmentedprogress {
min-height: 8px;
border-radius: 999px;
background: transparent;
border: none;
}
window#osd trough,
window#osd segment {
min-height: inherit;
border-radius: inherit;
border: none;
background: ${c.surface};
}
window#osd progress,
window#osd segment.active {
min-height: inherit;
border-radius: inherit;
border: none;
background: ${c.accent};
}
'';
in
{
config = lib.mkIf cfg.osd.enable {
services.swayosd = {
enable = true;
stylePath = style;
};
};
}

325
modules/home/rofi.nix Normal file
View File

@@ -0,0 +1,325 @@
# rofi (2.0, native Wayland on 26.05) — launcher + dmenu renderer for
# the menu system, themed from theme-state.json. rofi's .rasi is far more
# expressive than a flat scheme, so the generated theme styles each
# element (accent border, highlighted selection, rounded inputbar); a
# themes/<slug>/rofi.rasi whole-swap replaces it entirely.
#
# Home of nomarchy-menu, the dispatcher all menu modules hang off.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy;
t = cfg.theme;
c = t.colors;
inherit (config.lib.formats.rasi) mkLiteral;
px = n: mkLiteral "${toString n}px";
# Per-theme override probe (same convention as waybar.css).
rasiOverride = cfg.themesDir + "/${t.slug}/rofi.rasi";
hasRasiOverride = builtins.pathExists rasiOverride;
# Keybindings cheatsheet (SUPER+? → the `keybinds` menu module). Built
# from the SAME ./keybinds.nix that hyprland.nix binds, so it can never
# drift from the live shortcuts. "$mod" reads as SUPER; rows are padded
# into two columns. extra[] carries the generated/mouse binds.
keybinds = import ./keybinds.nix;
padRight = w: s:
let n = w - builtins.stringLength s;
in s + (if n > 0 then lib.concatStrings (builtins.genList (_: " ") n) else " ");
prettyKeys = b:
let m = lib.replaceStrings [ "$mod" ] [ "SUPER" ] b.mods;
prefix = if m == "" then "" else lib.concatStringsSep " + " (lib.splitString " " m) + " + ";
key = lib.replaceStrings [ "question" "slash" ] [ "?" "/" ] b.key;
in prefix + key;
cheatRows =
map (b: padRight 22 (prettyKeys b) + b.desc) keybinds.binds
++ map (e: padRight 22 e.keys + e.desc) keybinds.extra;
cheatsheetFile = pkgs.writeText "nomarchy-keybinds.txt"
(lib.concatStringsSep "\n" cheatRows + "\n");
nomarchy-menu = pkgs.writeShellScriptBin "nomarchy-menu" ''
# Nomarchy menu dispatcher thin presentation layer over
# `rofi -dmenu`; actions delegate to nomarchy-theme-sync, systemctl,
# hyprctl and friends. Icons are nf-md glyphs (Pango falls back to a
# Nerd Font for them). `nomarchy-menu` with no argument shows the
# module picker.
urlencode() {
local s="$1" out="" ch i
for ((i = 0; i < ''${#s}; i++)); do
ch=''${s:i:1}
case "$ch" in
[a-zA-Z0-9.~_-]) out+="$ch" ;;
' ') out+="+" ;;
*) printf -v ch '%%%02X' "'$ch"; out+="$ch" ;;
esac
done
printf '%s' "$out"
}
case "''${1:-}" in
power)
choice=$(printf '%s\n' \
"󰌾 Lock" \
"󰍃 Logout" \
"󰤄 Suspend" \
"󰒲 Hibernate" \
"󰜉 Reboot" \
"󰐥 Shutdown" \
| rofi -dmenu -p power) || exit 0
case "$choice" in
*Lock) loginctl lock-session ;;
*Logout) hyprctl dispatch exit ;;
*Suspend) systemctl suspend ;;
*Hibernate) systemctl hibernate ;;
*Reboot) systemctl reboot ;;
*Shutdown) systemctl poweroff ;;
esac ;;
power-profile)
# power-profiles-daemon profile switcher (the system side ships it
# by default on laptops nomarchy.system.power). Switching goes
# through polkit, so no root prompt.
if ! command -v powerprofilesctl >/dev/null 2>&1; then
notify-send "Power profiles" "power-profiles-daemon isn't running on this machine."
exit 0
fi
cur=$(powerprofilesctl get 2>/dev/null)
choice=$(powerprofilesctl list 2>/dev/null \
| sed -nE 's/^[* ] ([a-z-]+):$/\1/p' \
| rofi -dmenu -p "profile (now: $cur)") || exit 0
[ -n "$choice" ] && powerprofilesctl set "$choice" ;;
theme)
choice=$(nomarchy-theme-sync list | rofi -dmenu -p theme) || exit 0
[ -n "$choice" ] && exec nomarchy-theme-sync apply "$choice" ;;
clipboard)
sel=$(cliphist list | rofi -dmenu -p clip) || exit 0
printf '%s' "$sel" | cliphist decode | wl-copy ;;
calc)
# Live calculator (rofi-calc): re-evaluates every keystroke via
# libqalculate which dodges the qalc CLI's "15% of 200" misparse
# with the answer as the top entry. Enter copies the result; the
# menu stays open to chain calculations (Esc closes).
exec rofi -show calc -modi calc -no-show-match -no-sort \
-calc-command "echo -n '{result}' | wl-copy" ;;
files)
sel=$(fd . "$HOME" --type f --hidden --exclude .git --exclude .cache 2>/dev/null \
| head -n 50000 \
| sed "s|^$HOME/||" \
| rofi -dmenu -p file) || exit 0
[ -n "$sel" ] && exec xdg-open "$HOME/$sel" ;;
emoji)
# Emoji / symbol picker (rofi-emoji). The default action copies the
# glyph to the clipboard through the plugin's Wayland adapter
# (wl-copy); the alternate action types it into the focused window.
exec rofi -show emoji -modi emoji ;;
web)
q=$(rofi -dmenu -p search < /dev/null) || exit 0
[ -n "$q" ] || exit 0
exec xdg-open "https://www.google.com/search?q=$(urlencode "$q")" ;;
network)
# nmtui in a terminal (NetworkManager is the system network stack).
exec ${cfg.terminal} -e nmtui ;;
bluetooth)
# blueman-manager GUI (services.blueman.enable, system-side).
exec blueman-manager ;;
capture)
choice=$(printf '%s\n' \
"󰆟 Region clipboard" \
"󰆟 Region file" \
"󰍹 Full screen clipboard" \
"󰍹 Full screen file" \
| rofi -dmenu -p capture) || exit 0
dir="$HOME/Pictures/Screenshots"
file="$dir/$(date +%Y%m%d-%H%M%S).png"
case "$choice" in
*"Region clipboard"*) grim -g "$(slurp)" - | wl-copy ;;
*"Region file"*) mkdir -p "$dir"; grim -g "$(slurp)" "$file" \
&& notify-send "Screenshot saved" "$file" ;;
*"Full screen clipboard"*) grim - | wl-copy ;;
*"Full screen file"*) mkdir -p "$dir"; grim "$file" \
&& notify-send "Screenshot saved" "$file" ;;
esac ;;
keybinds)
# Read-only cheatsheet, generated from keybinds.nix at build time.
rofi -dmenu -i -p keys < ${cheatsheetFile} >/dev/null || exit 0 ;;
ask)
# Free-text question claude CLI in a terminal (OAuth, no API key).
# Pulled fresh from npm via npx rather than nixpkgs' claude-code,
# which lags behind model releases; npx caches after first run. The
# REPL stays open for follow-ups.
q=$(rofi -dmenu -p claude < /dev/null) || exit 0
[ -n "$q" ] || exit 0
exec ${cfg.terminal} -e npx --yes @anthropic-ai/claude-code@latest "$q" ;;
"")
entries=(
"󰀻 Apps"
"󰏘 Theme"
"󰅌 Clipboard"
"󰃬 Calculator"
"󰈞 Files"
"󰒨 Emoji"
"󰖟 Web search"
"󰖩 Network"
"󰂯 Bluetooth"
"󰄄 Capture"
"󰋖 Keybindings"
"󰭹 Ask Claude"
)
# Power-profile picker only when this is a laptop (battery present)
# running power-profiles-daemon matches the Waybar indicator's
# self-gating; desktops and the TLP backend don't show it.
bats=(/sys/class/power_supply/BAT*)
if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then
entries+=("󰓅 Power profile")
fi
entries+=("󰐥 Power")
choice=$(printf '%s\n' "''${entries[@]}" | rofi -dmenu -p menu) || exit 0
case "$choice" in
*Apps*) exec rofi -show drun ;;
*Theme*) exec "$0" theme ;;
*Clipboard*) exec "$0" clipboard ;;
*Calc*) exec "$0" calc ;;
*Files*) exec "$0" files ;;
*Emoji*) exec "$0" emoji ;;
*Web*) exec "$0" web ;;
*Network*) exec "$0" network ;;
*Bluetooth*) exec "$0" bluetooth ;;
*Capture*) exec "$0" capture ;;
*Keybindings*) exec "$0" keybinds ;;
*Ask*) exec "$0" ask ;;
*"Power profile"*) exec "$0" power-profile ;;
*Power*) exec "$0" power ;;
esac ;;
*)
echo "usage: nomarchy-menu [power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|capture|keybinds|ask]" >&2
exit 64 ;;
esac
'';
in
{
config = lib.mkIf cfg.rofi.enable {
home.packages = [
nomarchy-menu
pkgs.fd # files module (fuzzy search over $HOME)
pkgs.xdg-utils # xdg-open for the files + web modules
pkgs.nodejs # npx, for the Ask Claude module (claude-code from npm)
];
programs.rofi = {
enable = true;
terminal = cfg.terminal;
font = "${t.fonts.ui} 12";
# Native rofi modi backing two menu modules: calc (live, via
# libqalculate — no qalc CLI needed) and emoji (glyph picker, copies
# via its bundled Wayland adapter). Both render through the
# generated/whole-swap theme like every other modi.
plugins = [ pkgs.rofi-calc pkgs.rofi-emoji ];
extraConfig = {
modi = "drun,run";
show-icons = true; # app icons via the theme's icon set
icon-theme = t.iconTheme; # resolved in theme.nix (Papirus-*/per-theme)
drun-display-format = "{name}";
display-drun = "apps";
display-run = "run";
};
# Whole-swap themes bring their own rofi.rasi; otherwise the theme
# is generated from the palette.
theme =
if hasRasiOverride then rasiOverride
else {
"*" = {
bg = mkLiteral c.base;
fg = mkLiteral c.text;
dim = mkLiteral c.subtext;
surface = mkLiteral c.surface;
accent = mkLiteral c.accent;
background-color = mkLiteral "transparent";
text-color = mkLiteral "@fg";
margin = 0;
padding = 0;
spacing = 0;
};
"window" = {
background-color = mkLiteral "@bg";
border = px t.ui.borderSize;
border-color = mkLiteral "@accent";
border-radius = px t.ui.rounding;
width = mkLiteral "40%";
padding = px 8;
};
"mainbox" = {
children = map mkLiteral [ "inputbar" "message" "listview" ];
};
"inputbar" = {
background-color = mkLiteral "@surface";
border-radius = px t.ui.rounding;
padding = mkLiteral "8px 12px";
spacing = px 8;
children = map mkLiteral [ "prompt" "entry" ];
};
"prompt" = { text-color = mkLiteral "@accent"; };
"entry" = {
placeholder = "Search";
placeholder-color = mkLiteral "@dim";
};
"listview" = {
lines = 8;
columns = 1;
dynamic = true;
scrollbar = false;
spacing = px 2;
padding = mkLiteral "8px 0px 0px 0px";
};
"element" = {
padding = mkLiteral "8px 12px";
border-radius = px t.ui.rounding;
spacing = px 8;
};
"element selected" = {
background-color = mkLiteral "@accent";
text-color = mkLiteral "@bg";
};
"element-icon" = {
background-color = mkLiteral "transparent";
size = mkLiteral "1em";
};
"element-text" = {
background-color = mkLiteral "transparent";
text-color = mkLiteral "inherit";
};
"message" = { padding = mkLiteral "8px 0px 0px 0px"; };
"textbox" = {
background-color = mkLiteral "@surface";
text-color = mkLiteral "@fg";
border-radius = px t.ui.rounding;
padding = mkLiteral "8px 12px";
};
};
};
};
}

120
modules/home/shell.nix Normal file
View File

@@ -0,0 +1,120 @@
# Shell experience — zsh (the default login shell, set system-side) with
# autosuggestions + syntax highlighting, a themed starship prompt, and
# modern CLI tools wired in: bat (cat), eza (ls), zoxide (cd). bat uses
# the "ansi" theme so it follows the terminal palette (Ghostty is themed
# from the same JSON); starship is themed from the palette directly.
#
# home.shell.enableZshIntegration = true is the single lever that makes
# every integrating program (starship, eza, zoxide, ghostty, yazi) emit
# its zsh hooks — no per-module flags needed.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy;
t = cfg.theme;
c = t.colors;
in
{
config = lib.mkIf cfg.shell.enable {
home.shell.enableZshIntegration = true;
# rg/fd as themselves (NOT aliased over grep/find — their flags
# differ enough to surprise). Available regardless of yazi/rofi.
home.packages = with pkgs; [ ripgrep fd ];
programs.zsh = {
enable = true;
enableCompletion = true;
autosuggestion.enable = true;
syntaxHighlighting.enable = true;
autocd = true;
history = {
size = 50000;
save = 50000;
ignoreDups = true;
ignoreSpace = true;
expireDuplicatesFirst = true;
};
shellAliases = {
cat = "bat --paging=never";
less = "bat";
grep = "grep --color=auto";
# eza enableZshIntegration aliases ls/ll/la; add a couple extras.
lt = "eza --tree --level=2 --icons=auto --group-directories-first";
tree = "eza --tree --icons=auto --group-directories-first";
".." = "cd ..";
"..." = "cd ../..";
};
};
programs.starship = {
enable = true;
settings = {
add_newline = false;
palette = "nomarchy";
# starship wants full #rrggbb (or names) — keep the leading #.
palettes.nomarchy = {
inherit (c) base text accent good warn bad subtext;
};
# Flat prompt — colored text only, no powerline separators or
# background fills. Each module emits its own trailing space so
# there are no stray gaps when git/duration are absent.
format = lib.concatStrings [
"$directory"
"$git_branch"
"$git_status"
"$cmd_duration"
"$character"
];
directory = {
style = "bold accent";
format = "[$path]($style) ";
truncation_length = 3;
truncate_to_repo = true;
};
git_branch = {
style = "warn";
format = "[$symbol$branch]($style) ";
symbol = " ";
};
git_status = {
style = "subtext";
# Parenthesised → the segment (and its trailing space) only
# render when there's actually a status to show.
format = "([$all_status$ahead_behind]($style) )";
};
character = {
success_symbol = "[](good)";
error_symbol = "[](bad)";
};
cmd_duration = {
min_time = 2000;
style = "subtext";
format = "[ $duration]($style) ";
};
};
};
# bat: "ansi" follows the terminal's 16-color palette → tracks the
# active Nomarchy theme (Ghostty bakes it) with no per-theme config.
programs.bat = {
enable = true;
config = {
theme = "ansi";
style = "numbers,changes,header";
};
};
programs.eza = {
enable = true;
icons = "auto";
git = true;
extraOptions = [ "--group-directories-first" ];
};
programs.zoxide = {
enable = true;
options = [ "--cmd cd" ]; # `cd` becomes the smart jumper (z under the hood)
};
};
}

View File

@@ -55,6 +55,17 @@ in
size = lib.mkDefault 24;
};
# GTK/file-manager/rofi icon theme, resolved from the JSON in
# theme.nix (per-theme `icons`, else Papirus-Dark/Light by mode).
# Stylix sets gtk.iconTheme from this; both dark/light point at the
# already-mode-resolved name.
icons = {
enable = true;
package = lib.mkDefault pkgs.papirus-icon-theme;
dark = t.iconTheme;
light = t.iconTheme;
};
fonts = {
monospace = {
name = t.fonts.mono;
@@ -75,5 +86,14 @@ in
sizes.terminal = t.fonts.size;
};
};
# GTK4/libadwaita and Qt6 apps decide dark vs light from the XDG
# portal's color-scheme (org.freedesktop.appearance), which
# xdg-desktop-portal-gtk sources from this gsettings key. Stylix sets
# the GTK theme and `polarity` but not this, so a light theme still
# rendered libadwaita/Qt apps dark (and vice-versa). Drive it from the
# palette mode. (programs.dconf.enable is already on system-side.)
dconf.settings."org/gnome/desktop/interface".color-scheme =
if t.mode == "light" then "prefer-light" else "prefer-dark";
};
}

92
modules/home/swaync.nix Normal file
View File

@@ -0,0 +1,92 @@
# swaync — notification daemon + control centre, themed from
# theme-state.json. Until this shipped, nothing rendered notify-send at
# all: the theme-switch progress toasts, the CLI's font warnings and the
# live ISO's welcome message were all invisible.
{ config, lib, ... }:
let
cfg = config.nomarchy;
t = cfg.theme;
c = t.colors;
r = toString t.ui.rounding;
in
{
config = lib.mkIf cfg.swaync.enable {
services.swaync = {
enable = true;
settings = {
positionX = "right";
positionY = "top";
control-center-margin-top = t.ui.gapsOut;
control-center-margin-right = t.ui.gapsOut;
control-center-width = 420;
notification-window-width = 400;
notification-icon-size = 48;
timeout = 6;
timeout-low = 3;
timeout-critical = 0; # critical stays until dismissed
};
style = ''
/* Palette baked from theme-state.json */
@define-color base ${c.base};
@define-color surface ${c.surface};
@define-color text ${c.text};
@define-color subtext ${c.subtext};
@define-color accent ${c.accent};
@define-color bad ${c.bad};
.notification {
background: alpha(@base, 0.95);
border: ${toString t.ui.borderSize}px solid alpha(@accent, 0.4);
border-radius: ${r}px;
color: @text;
}
.notification-content .summary {
color: @text;
font-weight: bold;
}
.notification-content .body {
color: @subtext;
}
.notification.critical {
border-color: @bad;
}
.control-center {
background: alpha(@base, 0.95);
border: ${toString t.ui.borderSize}px solid alpha(@accent, 0.4);
border-radius: ${r}px;
color: @text;
}
.control-center .notification-row:focus,
.control-center .notification-row:hover {
background: alpha(@surface, 0.6);
border-radius: ${r}px;
}
.widget-title {
color: @text;
font-weight: bold;
}
.widget-title > button {
background: @surface;
color: @text;
border: none;
border-radius: ${r}px;
}
.widget-title > button:hover {
background: @accent;
color: @base;
}
'';
};
};
}

View File

@@ -44,11 +44,38 @@ let
blur = true;
shadow = true;
};
# Icon theme name; "" means "pick by mode" (resolved below). A preset
# or the state file can name any theme in the shipped icon package.
icons = "";
# Window border colors. Values are palette keys (resolved against
# `colors` below) or literal #rrggbb. Solid, not a gradient — every
# legacy identity theme used a solid border (accent for most, the text
# tone for kanagawa/summer-*). Each preset declares its own so a theme
# switch always replaces it (deep_merge would otherwise leave it stuck).
border = { active = "accent"; inactive = "overlay"; };
};
parsed = lib.recursiveUpdate defaults themeState;
# A border value is a palette key (look it up in colors) unless it's
# already a literal hex; unknown keys fall through to the raw string.
resolveColor = v: if lib.hasPrefix "#" v then v else parsed.colors.${v} or v;
border = {
active = resolveColor parsed.border.active;
inactive = resolveColor parsed.border.inactive;
};
# Resolve the icon theme once and expose it on nomarchy.theme so both
# the GTK side (stylix.nix) and rofi (rofi.nix) agree. The shipped
# package (papirus-icon-theme) carries the Dark/Light pair.
iconTheme =
if parsed.icons != "" then parsed.icons
else if parsed.mode == "light" then "Papirus-Light"
else "Papirus-Dark";
in
{
config = {
nomarchy.theme = lib.recursiveUpdate defaults themeState;
nomarchy.theme = parsed // { inherit iconTheme border; };
nomarchy.lib = {
# "#7aa2f7" -> "rgb(7aa2f7)" (Hyprland color syntax)

View File

@@ -7,11 +7,39 @@
# <themesDir>/<slug>/waybar.css (and optionally waybar.jsonc) which
# replace the generated style/layout entirely. Probed at eval time —
# pure, it's flake source.
{ config, lib, ... }:
{ config, lib, pkgs, ... }:
let
t = config.nomarchy.theme;
# Power-profile indicator (power-profiles-daemon). Both scripts self-
# gate: they exit silently unless this is a laptop (a battery is
# present) running PPD, so the module hides itself on desktops and
# under the TLP backend — no system→home coupling. Speedometer glyphs
# (slow/medium/fast) map to power-saver/balanced/performance.
powerProfileStatus = pkgs.writeShellScript "nomarchy-powerprofile-status" ''
case "$(ls /sys/class/power_supply/ 2>/dev/null)" in *BAT*) ;; *) exit 0 ;; esac
command -v powerprofilesctl >/dev/null 2>&1 || exit 0
prof=$(powerprofilesctl get 2>/dev/null) || exit 0
case "$prof" in
power-saver) icon="󰾆" ;;
performance) icon="󰓅" ;;
*) icon="󰾅" ;;
esac
printf '{"text":"%s","tooltip":"Power profile: %s","class":"%s"}\n' "$icon" "$prof" "$prof"
'';
# Cycle to the next profile this machine actually offers (wraps around).
powerProfileCycle = pkgs.writeShellScript "nomarchy-powerprofile-cycle" ''
command -v powerprofilesctl >/dev/null 2>&1 || exit 0
cur=$(powerprofilesctl get 2>/dev/null) || exit 0
avail=$(powerprofilesctl list 2>/dev/null | sed -nE 's/^[* ] ([a-z-]+):$/\1/p')
next=$(printf '%s\n' "$avail" | awk -v cur="$cur" '
{ a[NR] = $0 }
END { for (i = 1; i <= NR; i++) if (a[i] == cur) { print a[i % NR + 1]; f = 1 }
if (!f && NR) print a[1] }')
[ -n "$next" ] && powerprofilesctl set "$next"
'';
# Per-theme override probe.
assetDir = config.nomarchy.themesDir + "/${t.slug}";
styleOverride = assetDir + "/waybar.css";
@@ -38,7 +66,7 @@ let
modules-left = [ "hyprland/workspaces" "hyprland/window" ];
modules-center = [ "clock" ];
modules-right = [ "tray" "pulseaudio" "network" "cpu" "memory" "battery" ];
modules-right = [ "tray" "pulseaudio" "network" "cpu" "memory" "custom/powerprofile" "battery" ];
"hyprland/workspaces" = {
format = "{icon}";
@@ -60,7 +88,7 @@ let
format = "{icon} {volume}%";
format-muted = "󰝟";
format-icons.default = [ "󰕿" "󰖀" "󰕾" ];
on-click = "pamixer -t";
on-click = "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
};
network = {
@@ -68,6 +96,9 @@ let
format-ethernet = "󰈀";
format-disconnected = "󰤭";
tooltip-format = "{ipaddr} via {gwaddr}";
# nm-applet sits in the tray for the GUI path; this is the
# keyboard-friendly one.
on-click = "${config.nomarchy.terminal} -e nmtui";
};
cpu.format = "󰍛 {usage}%";
@@ -80,6 +111,13 @@ let
format-icons = [ "󰁺" "󰁼" "󰁾" "󰂀" "󰁹" ];
};
"custom/powerprofile" = {
exec = "${powerProfileStatus}";
return-type = "json";
interval = 5;
on-click = "${powerProfileCycle}";
};
tray.spacing = 8;
};
@@ -126,7 +164,7 @@ let
font-weight: bold;
}
#tray, #pulseaudio, #network, #cpu, #memory, #battery {
#tray, #pulseaudio, #network, #cpu, #memory, #custom-powerprofile, #battery {
color: @subtext;
padding: 0 10px;
}
@@ -142,14 +180,21 @@ in
enable = true;
systemd.enable = true; # started/stopped with graphical-session.target
settings.mainBar =
# mkDefault so downstream can replace the whole bar config/style with
# a plain home.nix assignment. For per-theme identity, prefer the
# whole-swap assets (themes/<slug>/waybar.{jsonc,css}); for one-off
# tweaks of the generated bar, lib.mkForce a sub-key. See
# docs/OVERRIDES.md.
settings.mainBar = lib.mkDefault (
if hasConfigOverride
then builtins.fromJSON (builtins.readFile configOverride)
else generatedSettings;
else generatedSettings
);
style =
style = lib.mkDefault (
if hasStyleOverride
then builtins.readFile styleOverride
else generatedStyle;
else generatedStyle
);
};
}

133
modules/home/yazi.nix Normal file
View File

@@ -0,0 +1,133 @@
# yazi — the flagship file manager: a fast, keyboard-driven TUI that
# fits the distro's identity (Ghostty's Kitty-graphics previews, rofi
# menus, everything from one JSON). Themed from theme-state.json and
# shipped with a curated plugin set. The GUI half (Thunar, "open folder"
# handler) is nomarchy.system.fileManager on the system side.
#
# `y` (the shell wrapper) cd's the shell to yazi's last directory on
# exit; SUPER+E opens it in a fresh terminal.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy;
t = cfg.theme;
c = t.colors;
in
{
config = lib.mkIf cfg.yazi.enable {
# Preview + tooling deps yazi shells out to (thumbnails, search,
# archives, the plugins below). All small CLIs.
home.packages = with pkgs; [
ffmpegthumbnailer # video thumbnails
poppler-utils # PDF previews (pdftoppm)
p7zip # archive preview + the compress plugin
jq # JSON preview
fd ripgrep fzf # navigation / search integrations
chafa # image fallback when the terminal lacks graphics
mediainfo # mediainfo plugin (audio/subtitle metadata)
glow # glow plugin (markdown rendering)
];
programs.yazi = {
enable = true;
shellWrapperName = "y"; # `y` cd's on exit; `yazi` stays a no-cd launcher
enableBashIntegration = true;
settings = {
mgr = {
show_hidden = false;
sort_by = "natural";
sort_dir_first = true;
linemode = "size";
};
preview = {
max_width = 1000;
max_height = 1000;
};
plugin = {
# git status in the linemode (fetcher registered before setup).
# yazi 26.x made `group` required on fetchers (without it the
# config fails to parse and yazi falls back to presets); "*"
# matches files, "*/" dirs, so they share the "git" group.
prepend_fetchers = [
{ id = "git"; url = "*"; run = "git"; group = "git"; }
{ id = "git"; url = "*/"; run = "git"; group = "git"; }
];
# Route only audio + subtitles to mediainfo's text metadata;
# images/videos keep yazi's native visual thumbnails. Markdown
# renders through glow.
# Match field is `url` (a path glob) or `mime` — yazi 26.x renamed
# the old `name` to `url`, same as the fetchers above. Using `name`
# fails the parse ("at least one of 'url' or 'mime'") and yazi falls
# back to its presets.
prepend_previewers = [
{ url = "*.md"; run = "glow"; }
{ mime = "audio/*"; run = "mediainfo"; }
{ mime = "application/subrip"; run = "mediainfo"; }
];
};
};
keymap.mgr.prepend_keymap = [
{ on = "l"; run = "plugin smart-enter"; desc = "Enter dir or open file"; }
{ on = "<Enter>"; run = "plugin smart-enter"; desc = "Enter dir or open file"; }
{ on = [ "c" "m" ]; run = "plugin chmod"; desc = "Chmod on selected files"; }
{ on = "M"; run = "plugin mount"; desc = "Mount/unmount manager"; }
{ on = [ "c" "a" "a" ]; run = "plugin compress"; desc = "Archive selected files"; }
];
# Curated plugins (nixpkgs yaziPlugins). setup = true emits the
# require(...):setup() call in init.lua; the rest are invoked via
# the keymap/previewer/fetcher wiring above.
plugins = {
full-border = { package = pkgs.yaziPlugins.full-border; setup = true; };
git = { package = pkgs.yaziPlugins.git; setup = true; };
smart-enter = { package = pkgs.yaziPlugins.smart-enter; };
chmod = { package = pkgs.yaziPlugins.chmod; };
mount = { package = pkgs.yaziPlugins.mount; };
compress = { package = pkgs.yaziPlugins.compress; };
mediainfo = { package = pkgs.yaziPlugins.mediainfo; };
glow = { package = pkgs.yaziPlugins.glow; };
};
# Theme from the palette. yazi merges this over its built-in theme,
# so only the accent-bearing UI is specified; everything else keeps
# yazi's defaults (which already follow Ghostty's ANSI colors).
theme = {
mgr = {
cwd = { fg = c.accent; };
hovered = { fg = c.base; bg = c.accent; };
preview_hovered = { underline = true; };
find_keyword = { fg = c.warn; italic = true; };
find_position = { fg = c.accentAlt; bg = "reset"; italic = true; };
marker_copied = { fg = c.good; bg = c.good; };
marker_cut = { fg = c.bad; bg = c.bad; };
marker_marked = { fg = c.accent; bg = c.accent; };
marker_selected = { fg = c.warn; bg = c.warn; };
count_copied = { fg = c.base; bg = c.good; };
count_cut = { fg = c.base; bg = c.bad; };
count_selected = { fg = c.base; bg = c.warn; };
border_symbol = "";
border_style = { fg = c.muted; };
};
mode = {
normal_main = { fg = c.base; bg = c.accent; bold = true; };
normal_alt = { fg = c.accent; bg = c.surface; };
select_main = { fg = c.base; bg = c.good; bold = true; };
select_alt = { fg = c.good; bg = c.surface; };
unset_main = { fg = c.base; bg = c.accentAlt; bold = true; };
unset_alt = { fg = c.accentAlt; bg = c.surface; };
};
status = {
perm_type = { fg = c.accent; };
perm_read = { fg = c.good; };
perm_write = { fg = c.bad; };
perm_exec = { fg = c.warn; };
progress_label = { fg = c.text; bold = true; };
progress_normal = { fg = c.accent; bg = c.surface; };
progress_error = { fg = c.bad; bg = c.surface; };
};
};
};
};
}

Binary file not shown.

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="157.40488mm"
height="162.48518mm"
viewBox="0 0 157.40488 162.48518"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
sodipodi:docname="icon.svg"
inkscape:export-filename="logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.82803284"
inkscape:cx="216.175"
inkscape:cy="452.27675"
inkscape:window-width="1914"
inkscape:window-height="1012"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1"><inkscape:page
x="0"
y="-1.1741086e-21"
width="157.40488"
height="162.48518"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-28.332214,-25.599269)"><g
id="g4"
transform="translate(3.1953242,-22.686801)"
style="fill:#000088;fill-opacity:1"><path
style="fill:#000088;fill-opacity:1;stroke-width:0.264583"
d="M 25.136891,85.823024 25.557592,210.77125 88.452409,174.38061 67.417351,160.49747 57.110174,166.38729 V 105.80633 Z"
id="path1" /><path
style="fill:#000088;fill-opacity:1;stroke-width:0.264583"
d="M 67.728991,112.41131 182.54178,185.60757 153.16137,202.85259 67.830432,148.17947 Z"
id="path2" /><path
style="fill:#000088;fill-opacity:1;stroke-width:0.264583"
d="M 139.74857,145.88014 140.00405,110.4959 54.800856,56.333749 25.675926,73.32329 Z"
id="path3" /><path
style="fill:#000088;fill-opacity:1;stroke-width:0.264583"
d="M 182.2863,172.70573 V 48.286069 l -62.59305,36.406166 20.82177,13.668277 10.21927,-5.74834 0.12774,60.165978 z"
id="path4" /></g></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -9,11 +9,66 @@
let
cfg = config.nomarchy.system;
# Reused by the greeter banner and MOTD so the brand name lives in one
# place (system.nixos.distroName, set below).
distroName = config.system.nixos.distroName;
# The Nomarchy glyph font: a single-glyph face (the logo at U+F000) used
# by the Waybar menu button in the summer themes. Pinned via
# `font-family: Nomarchy` in their CSS, since Nerd Fonts also occupy
# U+F000 (a glass icon) and would otherwise win.
nomarchyLogoFont = pkgs.runCommandLocal "nomarchy-logo-font" { } ''
install -Dm444 ${./branding/Nomarchy.ttf} $out/share/fonts/truetype/Nomarchy.ttf
'';
in
{
imports = [ ./options.nix ];
imports = [ ./options.nix ./plymouth.nix ./file-manager.nix ./power.nix ];
config = {
# The safe half of distro branding: distroName flows into
# /etc/os-release PRETTY_NAME, systemd-boot entry titles and the
# ISO boot-menu label. distroId stays "nixos" on purpose — it feeds
# DEFAULT_HOSTNAME and upstream isNixos checks (see roadmap).
system.nixos.distroName = lib.mkDefault "Nomarchy";
# MOTD on TTY/SSH login (the desktop auto-logs into Hyprland, so this
# is mostly seen over SSH or on a bare console). Branded, and doubles
# as a cheat sheet for the distro's own helpers.
users.motd = lib.mkDefault ''
${distroName} a NixOS desktop, themed from one JSON.
sys-update update inputs + rebuild the system
home-update apply home/theme changes (no sudo)
nomarchy-theme-sync apply <theme> switch the whole palette
SUPER+? keybindings cheatsheet
'';
# Unfree allowed distro-wide: pragmatic-desktop territory (vendor
# GPU/wifi drivers, firmware, fonts, …). The custom nixpkgs-config
# type can't carry a nested mkDefault; disagree with
# `nixpkgs.config = lib.mkForce { allowUnfree = false; }`.
nixpkgs.config.allowUnfree = true;
# ── One keyboard layout everywhere, incl. the LUKS prompt ────────
# services.xserver.xkb.layout is the single source of truth for the
# layout (the installer writes it; downstream sets it in system.nix).
# These bridge that one knob through to the encrypted-disk prompt —
# without them, setting only xkb.layout moves the graphical session
# but leaves the console keymap unset, so the initrd LUKS passphrase
# prompt falls back to US:
# useXkbConfig the virtual-console keymap follows xkb (TTYs)
# earlySetup bakes that keymap into the initrd
# systemd initrd loads it before cryptsetup asks for the passphrase
# (the legacy scripted initrd cannot; also what
# Plymouth needs — defaulted here so it no longer
# hinges on the splash being enabled).
# All mkDefault, so an explicit console.keyMap or a scripted initrd
# still wins.
console.useXkbConfig = lib.mkDefault true;
console.earlySetup = lib.mkDefault true;
boot.initrd.systemd.enable = lib.mkDefault true;
# ── Wayland session: Hyprland ────────────────────────────────────
# Installs the binary, registers the session, wires up
# xdg-desktop-portal-hyprland. Configuration is Home Manager's job.
@@ -28,12 +83,14 @@ in
enable = lib.mkDefault true;
settings = {
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 --greeting 'Welcome to ${distroName}' --cmd start-hyprland";
user = "greeter";
};
# Boot straight into the session once; logout → normal greeter.
initial_session = lib.mkIf (cfg.greeter.autoLogin != null) {
command = "Hyprland";
command = "start-hyprland";
user = cfg.greeter.autoLogin;
};
};
@@ -55,11 +112,65 @@ in
services.gnome.gnome-keyring.enable = lib.mkDefault true;
services.dbus.enable = lib.mkDefault true;
services.upower.enable = lib.mkDefault true;
# Firmware updates via LVFS. Ships the daemon + its metadata-refresh
# timer only — it never flashes anything on its own; applying an update
# is an explicit `fwupdmgr update`. On by default as desktop security
# hygiene (BIOS/UEFI/Thunderbolt/SSD fixes land here); a clean no-op
# where LVFS sees no devices (VMs/headless). Disable natively with
# `services.fwupd.enable = false`.
services.fwupd.enable = lib.mkDefault true;
networking.networkmanager.enable = lib.mkDefault true;
# No double-unlock on hibernate. hypridle locks hyprlock before every
# sleep (right for suspend), but a hibernate resume is already gated by
# the LUKS passphrase entered at boot — so the user would type a password
# twice. Once we're back from an *encrypted* hibernate, drop the now-
# redundant hyprlock. Ordered After the hibernate service, so it runs
# post-resume; pulled in only by hibernate.target (suspend, and the
# RAM-resume phase of suspend-then-hibernate, stay locked — they have no
# passphrase gate). Gated on LUKS: an unencrypted hibernate keeps its lock.
systemd.services.nomarchy-hibernate-unlock =
lib.mkIf (builtins.attrNames config.boot.initrd.luks.devices != [ ]) {
description = "Dismiss hyprlock after resuming from an encrypted hibernate";
after = [ "systemd-hibernate.service" ];
wantedBy = [ "hibernate.target" ];
serviceConfig = {
Type = "oneshot";
# `-`: a missing hyprlock (idle disabled, or not locked) isn't a failure.
ExecStart = "-${pkgs.procps}/bin/pkill -x hyprlock";
};
};
# zsh as the default login shell (the desktop's shell experience —
# starship/bat/eza/zoxide — is configured home-side in shell.nix).
# programs.zsh.enable wires /etc/zshrc, completion and /etc/shells;
# defaultUserShell applies to normal users that don't set their own.
programs.zsh.enable = lib.mkDefault true;
# mkOverride 500: beats nixpkgs' bash.nix `mkDefault bashInteractive`
# (equal mkDefault priority would conflict) while staying below a
# plain downstream assignment. A per-user `users.users.<n>.shell`
# overrides this regardless.
users.defaultUserShell = lib.mkOverride 500 pkgs.zsh;
hardware.bluetooth.enable = lib.mkDefault cfg.bluetooth.enable;
services.blueman.enable = lib.mkDefault cfg.bluetooth.enable;
# ── Foreign binaries: nix-ld ─────────────────────────────────────
# An ld.so shim so dynamically-linked binaries not built for NixOS
# (downloaded tools, language servers, pip/npm-installed ELFs, the
# npx-fetched claude-code) run without manual patchelf. On by default —
# a pragmatic-desktop expectation.
programs.nix-ld.enable = lib.mkDefault true;
# ── 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) ─
# Guarded on the actual filesystem so enabling it on an ext4 machine
# is a clean no-op rather than a failing timer.
@@ -79,12 +190,31 @@ in
};
# ── 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 = {
packages = with pkgs; [
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
noto-fonts
noto-fonts-color-emoji
# The legacy-ported waybar identities (summer-day/night) use
# FontAwesome codepoints in their layouts and list it in css.
font-awesome
# The Nomarchy logo glyph (U+F000) for the summer menu button.
nomarchyLogoFont
];
fontconfig.defaultFonts = {
monospace = lib.mkDefault [ "JetBrainsMono Nerd Font" ];
@@ -96,6 +226,31 @@ in
# ── Essential packages ───────────────────────────────────────────
environment.systemPackages = with pkgs; [
nomarchy-theme-sync # provided by overlays.default
# Friendly wrappers for the two rebuild paths (README §3). Run as
# your user: `nix flake update` must NOT run as root (libgit2
# refuses the user-owned flake repo) — sudo happens inside, only
# for the system switch.
(pkgs.writeShellScriptBin "sys-update" ''
set -e
if [ "$(id -u)" -eq 0 ]; then
echo "sys-update: run as your normal user (it sudos the rebuild itself)" >&2
exit 1
fi
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
echo "sys-update: updating flake inputs in $flake"
nix flake update --flake "$flake"
if command -v nixos-rebuild-snap >/dev/null 2>&1; then
sudo nixos-rebuild-snap "$@" # BTRFS snapshot first
else
sudo nixos-rebuild switch --flake "$flake#default" "$@"
fi
'')
(pkgs.writeShellScriptBin "home-update" ''
set -e
exec home-manager switch --flake "''${NOMARCHY_PATH:-$HOME/.nomarchy}" "$@"
'')
git
vim
wget
@@ -123,6 +278,14 @@ in
nixos-rebuild switch --flake /etc/nixos#default "$@"
'');
# Don't let boot entries fill the ESP over the years.
boot.loader.systemd-boot.configurationLimit = lib.mkDefault 10;
# Backlight udev rule (chgrp video + g+w on /sys/class/backlight) so
# the desktop's swayosd brightness control works for video-group
# users without root. Harmless baseline even if the OSD is disabled.
services.udev.packages = [ pkgs.swayosd ];
# ── Nix itself ───────────────────────────────────────────────────
nix = {
settings = {

View File

@@ -0,0 +1,35 @@
# GUI file manager — Thunar (lightweight GTK, Stylix-themed) plus the
# backend services a standalone compositor needs for it to work fully:
# gvfs (trash/mount/network), tumbler (thumbnails), udisks2 (removable
# media — also what yazi's mount plugin drives). The keyboard-driven TUI
# flagship is yazi (modules/home/yazi.nix); this is the point-and-click
# half and the XDG handler for "open folder" / "show in files".
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy.system;
in
{
config = lib.mkIf cfg.fileManager.enable {
programs.thunar = {
enable = lib.mkDefault true;
plugins = with pkgs; [
thunar-archive-plugin # create/extract archives from the context menu
thunar-volman # auto-manage removable drives/media
thunar-media-tags-plugin # rename/inspect audio by tags
];
};
services.gvfs.enable = lib.mkDefault true; # trash, mounts, network shares
services.tumbler.enable = lib.mkDefault true; # thumbnail generation
services.udisks2.enable = lib.mkDefault true; # mount/unmount removable media
environment.systemPackages = with pkgs; [
ffmpegthumbnailer # video thumbnails (Thunar + yazi)
libgsf # ODF thumbnails
poppler-utils # PDF thumbnails / pdftoppm (yazi PDF preview too)
xfce4-exo # "Open Terminal Here" and default-app associations
shared-mime-info # MIME database
];
};
}

View File

@@ -21,6 +21,28 @@
'';
};
plymouth.enable = lib.mkEnableOption ''
the Nomarchy Plymouth boot splash (logo + progress + LUKS prompt),
background-tinted from theme-state.json via nomarchy.system.stateFile.
Recolors on system rebuilds theme switches don't touch the initrd'' // { default = true; };
stateFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = lib.literalExpression "./theme-state.json";
description = ''
theme-state.json for the system-side consumers (currently the
Plymouth splash background). lib.mkFlake wires it automatically
from your flake; null falls back to the Tokyo Night base color.
'';
};
fileManager.enable = lib.mkEnableOption ''
the Thunar GUI file manager + backend services (gvfs/tumbler/udisks2)
for point-and-click file management and the "open folder" handler.
The keyboard-driven TUI flagship (yazi) is the nomarchy.yazi.* home
option'' // { default = true; };
audio.enable = lib.mkEnableOption "the Pipewire audio stack" // { default = true; };
bluetooth.enable = lib.mkEnableOption "Bluetooth support with blueman" // { default = true; };
@@ -29,5 +51,58 @@
`nixos-rebuild-snap` pre-rebuild-snapshot helper. No-op unless the
root filesystem is BTRFS with a /.snapshots subvolume (the installer
creates one)'';
power = {
enable = lib.mkEnableOption ''
active power management. By default ships power-profiles-daemon
the upstream-aligned power-saver/balanced/performance model,
switchable from the menu (nomarchy-menu power-profile) and shown
in Waybar on laptops plus thermald and a battery charge limit
where applicable. Harmless on desktops (the profile daemon just
offers balanced/performance)'' // { default = true; };
backend = lib.mkOption {
type = lib.types.enum [ "ppd" "tlp" ];
default = "ppd";
description = ''
Which daemon governs CPU/platform power. "ppd"
(power-profiles-daemon) is the default: a clean three-profile
model with the menu switcher and Waybar indicator. "tlp" trades
that for TLP's deeper, more aggressive battery tuning, at the
cost of the profile switcher (TLP has no profile concept). The
two manage the same knobs and are mutually exclusive.
'';
};
laptop = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Marks this machine as a laptop, gating battery-only features
(the charge limit below). The installer sets it to true when it
detects a battery at install time.
'';
};
thermal.enable = lib.mkEnableOption ''
thermald, Intel's thermal-management daemon, to avoid aggressive
throttling under sustained load. Intel-only the installer
enables it when it detects a GenuineIntel CPU. Harmless alongside
either backend'';
batteryChargeLimit = lib.mkOption {
type = lib.types.nullOr (lib.types.ints.between 50 100);
default = null;
example = 80;
description = ''
Stop charging at this percentage to extend battery lifespan,
where the hardware exposes a charge threshold
(/sys/class/power_supply/BAT*/charge_control_end_threshold).
null leaves charging at the firmware default. Backend-independent
(a small systemd unit writes the sysfs knob at boot), so it
works under PPD too; needs nomarchy.system.power.laptop.
'';
};
};
};
}

110
modules/nixos/plymouth.nix Normal file
View File

@@ -0,0 +1,110 @@
# Plymouth boot splash — Nomarchy-branded (ported from the legacy
# iteration), background tinted from the same theme-state.json that
# drives the desktop (nomarchy.system.stateFile, wired automatically by
# lib.mkFlake). One caveat by design: theme switches are Home
# Manager-only and never touch the initrd, so the splash follows the
# theme as of the last SYSTEM rebuild (`sys-update`), not the last
# `nomarchy-theme-sync apply`.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy.system;
state =
if cfg.stateFile != null
then builtins.fromJSON (builtins.readFile cfg.stateFile)
else { };
colorOf = key: fallback: lib.removePrefix "#" ((state.colors or { }).${key} or fallback);
base = colorOf "base" "#1a1b26";
# Splash elements are recolored from the palette so they read on any
# base, light or dark: foreground glyphs → text, field/track boxes →
# surface (raised from base in both polarities), the progress fill →
# accent. The shipped art is a fixed navy that vanished on dark themes.
text = colorOf "text" "#a9b1d6";
surface = colorOf "surface" "#32344a";
accent = colorOf "accent" "#7aa2f7";
magick = lib.getExe' pkgs.imagemagick "magick";
# Plymouth's Window.SetBackgroundTopColor takes three floats in
# 0.01.0; the .plymouth metadata's ConsoleLogBackgroundColor takes a
# 0xRRGGBB hex. Nix has no float math: multiply, integer-divide, pad.
byteToFloat = n:
let
thousandths = (n * 1000) / 255;
s = toString thousandths;
padded =
if lib.stringLength s == 1 then "00${s}"
else if lib.stringLength s == 2 then "0${s}"
else s;
in "0.${padded}";
channel = off: byteToFloat (lib.fromHexString (lib.substring off 2 base));
nomarchy-plymouth = pkgs.stdenv.mkDerivation {
pname = "nomarchy-plymouth";
version = "1.0";
src = ./plymouth;
nativeBuildInputs = [ pkgs.imagemagick ];
installPhase = ''
themedir=$out/share/plymouth/themes/nomarchy
mkdir -p "$themedir"
cp * "$themedir/"
# Recolor the splash art from the palette (flat fill, alpha kept) so
# it reads on any base instead of the shipped fixed navy.
recolor() { ${magick} "$themedir/$1" -fill "#$2" -colorize 100 "$themedir/$1"; }
recolor logo.png ${text} # centre monogram
recolor lock.png ${text} # password-prompt padlock
recolor bullet.png ${text} # password dots
recolor entry.png ${surface} # password field box
recolor progress_box.png ${surface} # progress track
recolor progress_bar.png ${accent} # progress fill
# Point the .plymouth metadata into the store
sed -i "s|/usr/share/plymouth/themes/nomarchy|$themedir|g" \
"$themedir/nomarchy.plymouth"
# Bake the theme's base color: RGB floats for the script's
# Window.SetBackground* calls, 0xRRGGBB for ConsoleLogBackground.
sed -i \
-e 's|@BG_R@|${channel 0}|g' \
-e 's|@BG_G@|${channel 2}|g' \
-e 's|@BG_B@|${channel 4}|g' \
"$themedir/nomarchy.script"
sed -i 's|@BG_HEX@|${base}|g' \
"$themedir/nomarchy.plymouth"
'';
};
in
{
config = lib.mkIf cfg.plymouth.enable {
# The systemd initrd (boot.initrd.systemd.enable) and console.earlySetup
# that Plymouth needs are now distro-wide defaults in ./default.nix
# (they also carry the keyboard layout to the LUKS prompt), so the
# splash no longer has to turn them on. Just keep the console quiet
# around the splash.
boot.initrd.verbose = lib.mkDefault false;
boot.consoleLogLevel = lib.mkDefault 0;
boot.plymouth = {
enable = lib.mkDefault true;
themePackages = lib.mkDefault [ nomarchy-plymouth ];
theme = lib.mkDefault "nomarchy";
};
# Not mkDefault: kernelParams is a list other modules add to at
# normal priority — a mkDefault def would be dropped entirely,
# losing the quiet/splash boot. These merge with the rest.
boot.kernelParams = [
"quiet"
"splash"
"loglevel=3"
"rd.systemd.show_status=false"
"rd.udev.log_level=3"
"udev.log_priority=3"
"boot.shell_on_fail"
];
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,11 @@
[Plymouth Theme]
Name=Nomarchy
Description=Nomarchy splash screen.
ModuleName=script
[script]
ImageDir=/usr/share/plymouth/themes/nomarchy
ScriptFile=/usr/share/plymouth/themes/nomarchy/nomarchy.script
ConsoleLogBackgroundColor=0x@BG_HEX@
MonospaceFont=Cantarell 11
Font=Cantarell 11

View File

@@ -0,0 +1,271 @@
# Nomarchy Plymouth Theme Script
Window.SetBackgroundTopColor(@BG_R@, @BG_G@, @BG_B@);
Window.SetBackgroundBottomColor(@BG_R@, @BG_G@, @BG_B@);
logo.image = Image("logo.png");
# Calculate scale factor to make logo ~15% of screen height
logo_scale_factor = (Window.GetHeight() * 0.15) / logo.image.GetHeight();
logo_width = logo.image.GetWidth() * logo_scale_factor;
logo_height = logo.image.GetHeight() * logo_scale_factor;
logo.image = logo.image.Scale(logo_width, logo_height);
logo.sprite = Sprite(logo.image);
logo.sprite.SetX (Window.GetWidth() / 2 - logo.image.GetWidth() / 2);
logo.sprite.SetY (Window.GetHeight() / 2 - logo.image.GetHeight() / 2);
logo.sprite.SetOpacity (1);
# Use these to adjust the progress bar timing
global.fake_progress_limit = 0.7; # Target percentage for fake progress (0.0 to 1.0)
global.fake_progress_duration = 15.0; # Duration in seconds to reach limit
# Progress bar animation variables
global.fake_progress = 0.0;
global.real_progress = 0.0;
global.fake_progress_active = 0; # 0 / 1 boolean
global.animation_frame = 0;
global.fake_progress_start_time = 0; # Track when fake progress started
global.password_shown = 0; # Track if password dialog has been shown
global.max_progress = 0.0; # Track the maximum progress reached to prevent backwards movement
fun refresh_callback ()
{
global.animation_frame++;
# Animate fake progress to limit over time with easing
if (global.fake_progress_active == 1)
{
# Calculate elapsed time since start
elapsed_time = global.animation_frame / 50.0; # Convert frames to seconds (50 FPS)
# Calculate linear progress ratio (0 to 1) based on time
time_ratio = elapsed_time / global.fake_progress_duration;
if (time_ratio > 1.0)
time_ratio = 1.0;
# Apply easing curve: ease-out quadratic
# Formula: 1 - (1 - x)^2
eased_ratio = 1 - ((1 - time_ratio) * (1 - time_ratio));
# Calculate fake progress based on eased ratio
global.fake_progress = eased_ratio * global.fake_progress_limit;
# Update progress bar with fake progress
update_progress_bar(global.fake_progress);
}
}
Plymouth.SetRefreshFunction (refresh_callback);
#----------------------------------------- Helper Functions --------------------------------
fun update_progress_bar(progress)
{
# Only update if progress is moving forward
if (progress > global.max_progress)
{
global.max_progress = progress;
width = Math.Int(progress_bar.original_image.GetWidth() * progress);
if (width < 1) width = 1; # Ensure minimum width of 1 pixel
progress_bar.image = progress_bar.original_image.Scale(width, progress_bar.original_image.GetHeight());
progress_bar.sprite.SetImage(progress_bar.image);
}
}
fun show_progress_bar()
{
progress_box.sprite.SetOpacity(1);
progress_bar.sprite.SetOpacity(1);
}
fun hide_progress_bar()
{
progress_box.sprite.SetOpacity(0);
progress_bar.sprite.SetOpacity(0);
}
fun show_password_dialog()
{
lock.sprite.SetOpacity(1);
entry.sprite.SetOpacity(1);
}
fun hide_password_dialog()
{
lock.sprite.SetOpacity(0);
entry.sprite.SetOpacity(0);
for (index = 0; bullet.sprites[index]; index++)
bullet.sprites[index].SetOpacity(0);
}
fun start_fake_progress()
{
# Don't reset if we already have progress
if (global.max_progress == 0.0)
{
global.fake_progress = 0.0;
global.real_progress = 0.0;
update_progress_bar(0.0);
}
global.fake_progress_active = 1;
global.animation_frame = 0;
}
fun stop_fake_progress()
{
global.fake_progress_active = 0;
}
#----------------------------------------- Dialogue --------------------------------
lock.image = Image("lock.png");
entry.image = Image("entry.png");
bullet.image = Image("bullet.png");
entry.sprite = Sprite(entry.image);
entry.x = Window.GetWidth()/2 - entry.image.GetWidth() / 2;
entry.y = logo.sprite.GetY() + logo.image.GetHeight() + 40;
entry.sprite.SetPosition(entry.x, entry.y, 10001);
entry.sprite.SetOpacity(0);
# Scale lock to be slightly shorter than entry field height
# Original lock is 84x96, entry height determines scale
lock_height = entry.image.GetHeight() * 0.8;
lock_scale = lock_height / 96;
lock_width = 84 * lock_scale;
scaled_lock = lock.image.Scale(lock_width, lock_height);
lock.sprite = Sprite(scaled_lock);
lock.x = entry.x - lock_width - 15;
lock.y = entry.y + entry.image.GetHeight()/2 - lock_height/2;
lock.sprite.SetPosition(lock.x, lock.y, 10001);
lock.sprite.SetOpacity(0);
# Bullet array
bullet.sprites = [];
fun display_normal_callback ()
{
hide_password_dialog();
# Get current mode
mode = Plymouth.GetMode();
# Only show progress bar for boot and resume modes
if (mode == "boot" || mode == "resume")
{
show_progress_bar();
start_fake_progress();
}
}
fun display_password_callback (prompt, bullets)
{
global.password_shown = 1; # Mark that password dialog has been shown
# Reset progress when password dialog appears
stop_fake_progress();
hide_progress_bar();
global.max_progress = 0.0;
global.fake_progress = 0.0;
global.real_progress = 0.0;
show_password_dialog();
# Clear all bullets first
for (index = 0; bullet.sprites[index]; index++)
bullet.sprites[index].SetOpacity(0);
# Create and show bullets for current password (max 21)
max_bullets = 21;
bullets_to_show = bullets;
if (bullets_to_show > max_bullets)
bullets_to_show = max_bullets;
for (index = 0; index < bullets_to_show; index++)
{
if (!bullet.sprites[index])
{
# Scale bullet image to 7x7 pixels
scaled_bullet = bullet.image.Scale(7, 7);
bullet.sprites[index] = Sprite(scaled_bullet);
bullet.x = entry.x + 20 + index * (7 + 5);
bullet.y = entry.y + entry.image.GetHeight() / 2 - 3.5;
bullet.sprites[index].SetPosition(bullet.x, bullet.y, 10002);
}
bullet.sprites[index].SetOpacity(1);
}
}
Plymouth.SetDisplayNormalFunction(display_normal_callback);
Plymouth.SetDisplayPasswordFunction(display_password_callback);
#----------------------------------------- Progress Bar --------------------------------
progress_box.image = Image("progress_box.png");
progress_box.sprite = Sprite(progress_box.image);
progress_box.x = Window.GetWidth() / 2 - progress_box.image.GetWidth() / 2;
progress_box.y = entry.y + entry.image.GetHeight() / 2 - progress_box.image.GetHeight() / 2;
progress_box.sprite.SetPosition(progress_box.x, progress_box.y, 0);
progress_box.sprite.SetOpacity(0);
progress_bar.original_image = Image("progress_bar.png");
progress_bar.sprite = Sprite();
progress_bar.image = progress_bar.original_image.Scale(1, progress_bar.original_image.GetHeight());
progress_bar.x = Window.GetWidth() / 2 - progress_bar.original_image.GetWidth() / 2;
progress_bar.y = progress_box.y + (progress_box.image.GetHeight() - progress_bar.original_image.GetHeight()) / 2;
progress_bar.sprite.SetPosition(progress_bar.x, progress_bar.y, 1);
progress_bar.sprite.SetOpacity(0);
fun progress_callback (duration, progress)
{
global.real_progress = progress;
# If real progress is above limit, stop fake progress and use real progress
if (progress > global.fake_progress_limit)
{
stop_fake_progress();
update_progress_bar(progress);
}
}
Plymouth.SetBootProgressFunction(progress_callback);
#----------------------------------------- Quit --------------------------------
fun quit_callback ()
{
logo.sprite.SetOpacity (1);
}
Plymouth.SetQuitFunction(quit_callback);
#----------------------------------------- Message --------------------------------
message_sprite = Sprite();
message_sprite.SetPosition(10, 10, 10000);
fun display_message_callback (text)
{
my_image = Image.Text(text, 1, 1, 1);
message_sprite.SetImage(my_image);
}
fun hide_message_callback (text)
{
message_sprite.SetOpacity(0);
}
Plymouth.SetDisplayMessageFunction (display_message_callback);
Plymouth.SetHideMessageFunction (hide_message_callback);
# Initialize progress bar immediately for normal boots
if (Plymouth.GetMode() == "boot" || Plymouth.GetMode() == "resume")
{
show_progress_bar();
start_fake_progress();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

60
modules/nixos/power.nix Normal file
View File

@@ -0,0 +1,60 @@
# Active power management. One concern, one file: this is the system
# side — the power daemon (power-profiles-daemon by default, or TLP),
# thermald, and the battery charge limit. The profile *switcher* and the
# Waybar *indicator* live home-side (rofi.nix / waybar.nix); they self-
# gate on powerprofilesctl being present, so there's no system→home wiring
# to keep in sync (the same way the Waybar battery widget auto-hides on
# desktops). See the roadmap in README.md.
{ config, lib, ... }:
let
cfg = config.nomarchy.system.power;
ppd = cfg.backend == "ppd";
tlp = cfg.backend == "tlp";
chargeLimit = cfg.laptop && cfg.batteryChargeLimit != null;
in
{
config = lib.mkIf cfg.enable {
# PPD and TLP both drive CPU/platform power — running both fights.
# `backend` selects exactly one; this assertion only fires if a
# downstream force-enables the other service directly.
assertions = [{
assertion = !(config.services.power-profiles-daemon.enable
&& config.services.tlp.enable);
message = ''
nomarchy: power-profiles-daemon and TLP are mutually exclusive.
Pick one with nomarchy.system.power.backend ("ppd" or "tlp").
'';
}];
# Unprivileged profile switching (the menu/Waybar) goes through
# polkit, already enabled distro-wide in default.nix.
services.power-profiles-daemon.enable = lib.mkDefault ppd;
services.tlp.enable = lib.mkDefault tlp;
# thermald is Intel-only, so off unless asked (the installer enables
# it on a GenuineIntel CPU). Sits happily next to either backend.
services.thermald.enable = lib.mkDefault cfg.thermal.enable;
# Battery charge limit via sysfs. PPD can't cap charge at all, and
# TLP's own knob only applies under TLP — so a tiny oneshot writes
# the threshold directly, independent of the backend. Boot-time only:
# some firmwares reset the threshold on unplug; revisit with a udev
# hook if that bites. `-` paths that don't exist are skipped, so this
# is a clean no-op on hardware without the control.
systemd.services.nomarchy-battery-charge-limit = lib.mkIf chargeLimit {
description = "Cap battery charging at ${toString cfg.batteryChargeLimit}%";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
for thresh in /sys/class/power_supply/BAT*/charge_control_end_threshold; do
[ -w "$thresh" ] && echo ${toString cfg.batteryChargeLimit} > "$thresh"
done
exit 0
'';
};
};
}

View File

@@ -16,6 +16,7 @@
# NOMARCHY_UNATTENDED=1 NOMARCHY_DISK=/dev/vda NOMARCHY_USERNAME=me \
# NOMARCHY_PASSWORD=secret [NOMARCHY_HOSTNAME=nomarchy] \
# [NOMARCHY_TIMEZONE=UTC] [NOMARCHY_LUKS_PASSPHRASE=...] \
# [NOMARCHY_LOCALE=en_US.UTF-8] [NOMARCHY_KB_LAYOUT=us] [NOMARCHY_KB_VARIANT=] \
# [NOMARCHY_SWAP_GB=N (default: RAM size; 0 = none)] \
# [NOMARCHY_HW="auto"|"none"|"mod1 mod2"] [NOMARCHY_FINISH=none|reboot|poweroff]
# nomarchy-install
@@ -150,6 +151,9 @@ if [[ "$UNATTENDED" == "1" ]]; then
PASSWORD="${NOMARCHY_PASSWORD:?}"
HOSTNAME_="${NOMARCHY_HOSTNAME:-nomarchy}"
TIMEZONE="${NOMARCHY_TIMEZONE:-UTC}"
LOCALE="${NOMARCHY_LOCALE:-en_US.UTF-8}"
KB_LAYOUT="${NOMARCHY_KB_LAYOUT:-us}"
KB_VARIANT="${NOMARCHY_KB_VARIANT:-}"
else
while true; do
USERNAME=$(gum input --placeholder "username (lowercase, e.g. ada)")
@@ -169,6 +173,26 @@ else
warn "Invalid hostname."
done
TIMEZONE=$(timedatectl list-timezones | gum filter --placeholder "timezone (type to search)" || echo UTC)
# Curated UTF-8 locales: the live system only generates en_US, so
# `locale -a` can't enumerate what the TARGET could use.
LOCALE=$(printf '%s\n' \
en_US en_GB de_DE fr_FR es_ES es_MX pt_PT pt_BR it_IT nl_NL \
pl_PL ru_RU uk_UA cs_CZ sk_SK sv_SE nb_NO da_DK fi_FI tr_TR \
el_GR hu_HU ro_RO bg_BG hr_HR sl_SI lt_LT lv_LV et_EE ja_JP \
ko_KR zh_CN zh_TW ar_EG he_IL hi_IN th_TH vi_VN id_ID \
| sed 's/$/.UTF-8/' \
| gum filter --placeholder "language / locale (type to search)" \
|| echo en_US.UTF-8)
KB_LAYOUT=$(localectl list-x11-keymap-layouts 2>/dev/null \
| gum filter --placeholder "keyboard layout (type to search)" \
|| echo us)
KB_VARIANT=""
if [[ "$KB_LAYOUT" != "us" ]] || gum confirm --default=No "Pick a keyboard variant (intl, nodeadkeys, …)?"; then
KB_VARIANT=$( { echo "(none)"; localectl list-x11-keymap-variants "$KB_LAYOUT" 2>/dev/null; } \
| gum filter --placeholder "variant for $KB_LAYOUT (pick '(none)' for the default)" \
|| echo "(none)")
[[ "$KB_VARIANT" == "(none)" ]] && KB_VARIANT=""
fi
fi
[[ "$USERNAME" =~ ^[a-z_][a-z0-9_-]*$ ]] || fail "Invalid username '$USERNAME'."
[[ -f "/usr/share/zoneinfo/$TIMEZONE" || -e "/etc/zoneinfo/$TIMEZONE" ]] \
@@ -177,6 +201,7 @@ fi
HASHED_PASSWORD=$(printf '%s' "$PASSWORD" | mkpasswd -m sha-512 -s)
unset PASSWORD
info "User: $USERNAME @ $HOSTNAME_ ($TIMEZONE)"
info "Locale: $LOCALE · keyboard: $KB_LAYOUT${KB_VARIANT:+ ($KB_VARIANT)}"
# ─── Hardware profile ───────────────────────────────────────────────────
section "Hardware detection"
@@ -320,9 +345,35 @@ nixos-generate-config --root /mnt
mv /mnt/etc/nixos/hardware-configuration.nix "$FLAKE_DIR/"
rm -rf /mnt/etc/nixos
cp "$SHARE/template/home.nix" "$FLAKE_DIR/"
cp "$SHARE/template/theme-state.json" "$FLAKE_DIR/"
# home.nix is generated (not copied from the template) so the chosen
# keyboard layout reaches the Hyprland session — standalone HM cannot
# read system.nix.
cat > "$FLAKE_DIR/home.nix" <<EOF
# Your user environment. The Nomarchy desktop (Hyprland, Waybar,
# Ghostty, theming engine, Stylix) comes from homeModules.nomarchy;
# tune it via the nomarchy.* options, add your own packages and
# programs below.
{ pkgs, ... }:
{
# Keyboard for the desktop session; console + LUKS prompt get the
# same layout from system.nix (xkb + console.useXkbConfig).
nomarchy.keyboard.layout = "$KB_LAYOUT";
nomarchy.keyboard.variant = "$KB_VARIANT";
# Examples:
# nomarchy.terminal = "kitty"; # swap the default terminal
# nomarchy.waybar.enable = false; # bring your own bar
# nomarchy.stylix.enable = false; # opt out of GTK/Qt theming
home.packages = with pkgs; [
# firefox
];
}
EOF
hw_nix=""
for p in "${HW_PROFILES[@]:-}"; do
[[ -n "$p" ]] && hw_nix+=" \"$p\""
@@ -357,6 +408,26 @@ NIX
)
fi
# Laptop power: power-profiles-daemon ships by default; mark this a laptop
# so battery-only features apply, and enable thermald on Intel. Keyed off
# the same battery probe that chose the common-pc-laptop hardware profile.
POWER_CONFIG=""
if [[ " ${HW_PROFILES[*]:-} " == *" common-pc-laptop "* ]]; then
power_lines=" # Laptop power management (power-profiles-daemon + menu/Waybar
# switcher). Uncomment to stop charging at 80% to extend battery life.
nomarchy.system.power.laptop = true;
# nomarchy.system.power.batteryChargeLimit = 80;"
if grep -q GenuineIntel /proc/cpuinfo 2>/dev/null; then
power_lines+="
nomarchy.system.power.thermal.enable = true; # thermald (Intel)"
fi
POWER_CONFIG=$(cat <<NIX
$power_lines
NIX
)
fi
# initialHashedPassword is safe to template: mkpasswd's alphabet is
# [a-zA-Z0-9./$] — no Nix string metacharacters.
cat > "$FLAKE_DIR/system.nix" <<EOF
@@ -371,7 +442,17 @@ cat > "$FLAKE_DIR/system.nix" <<EOF
networking.hostName = "$HOSTNAME_";
time.timeZone = "$TIMEZONE";
i18n.defaultLocale = "en_US.UTF-8";
i18n.defaultLocale = "$LOCALE";
# One keyboard layout everywhere: xkb is the source of truth, the
# virtual console derives from it (greeter/ttys), and the systemd
# initrd applies it to the LUKS passphrase prompt too (the classic
# script initrd cannot). The Hyprland session reads the same layout
# from nomarchy.keyboard.* in home.nix.
services.xserver.xkb.layout = "$KB_LAYOUT";
services.xserver.xkb.variant = "$KB_VARIANT";
console.useXkbConfig = true;
boot.initrd.systemd.enable = true;
# Your login user — \`username\` flows in from flake.nix automatically.
users.users.\${username} = {
@@ -379,7 +460,7 @@ cat > "$FLAKE_DIR/system.nix" <<EOF
extraGroups = [ "wheel" "networkmanager" "video" "input" ];
initialHashedPassword = "$HASHED_PASSWORD";
};
$AUTOLOGIN_CONFIG$RESUME_CONFIG
$AUTOLOGIN_CONFIG$POWER_CONFIG$RESUME_CONFIG
# Hourly/daily BTRFS timeline snapshots + nixos-rebuild-snap.
nomarchy.system.snapper.enable = true;
@@ -410,6 +491,9 @@ fi
# outright. The first normal NixOS user is always 1000:users(100); the
# account doesn't exist in the target yet, so numeric ids it is.
chown -R 1000:100 "$FLAKE_DIR"
# The templates come out of the nix store mode 0444 and cp preserves
# that — without this the user owns home.nix but can't edit it.
chmod -R u+w "$FLAKE_DIR"
# /etc/nixos on the installed system points at the user-owned flake.
mkdir -p /mnt/etc
@@ -464,6 +548,25 @@ success "System installed (bootloader in place)"
# fully themed desktop, not bare Hyprland. Best-effort: a failure here
# only costs the user one `home-manager switch` after logging in.
section "Baking the desktop"
# Build the generation HERE in the live environment, not in the chroot:
# the live store always carries the offline pin set, while the target
# store only gets the full ISO store on OFFLINE installs — an online
# install skips that copy, and the old in-chroot build (substituters
# deliberately empty) then tried to compile the world from source.
# Building against the live store is the exact path the live session's
# own theme switching exercises, online or off.
info "Building the desktop generation..."
hm_out=""
if hm_out=$(nix --extra-experimental-features "nix-command flakes" \
build --no-link --print-out-paths \
--option substituters "" \
"path:$FLAKE_DIR#homeConfigurations.$USERNAME.activationPackage"); then
nix --extra-experimental-features "nix-command flakes" \
copy --no-check-sigs --to "local?root=/mnt" "$hm_out" \
|| hm_out=""
fi
[[ -n "$hm_out" ]] || warn "live-side desktop build failed — retrying inside the chroot"
# NOT /mnt/tmp: nixos-enter mounts a fresh tmpfs over /tmp inside the
# chroot, which silently vaporizes any script staged there (cost us a
# full verification round to find). /root persists into the chroot.
@@ -471,10 +574,15 @@ cat > /mnt/root/nomarchy-hm-activate.sh <<EOF
set -ex
exec > /var/log/nomarchy-hm-preactivate.log 2>&1
export PATH=/run/current-system/sw/bin:\$PATH
out=\$(nix --extra-experimental-features "nix-command flakes" \
build --no-link --print-out-paths \
--option substituters "" \
"path:/home/$USERNAME/.nomarchy#homeConfigurations.$USERNAME.activationPackage")
# Normally pre-built in the live env and copied over; the in-chroot
# build (default substituters — the live-side build already proved the
# no-network case) is a last-resort fallback.
out="$hm_out"
if [ -z "\$out" ]; then
out=\$(nix --extra-experimental-features "nix-command flakes" \
build --no-link --print-out-paths \
"path:/home/$USERNAME/.nomarchy#homeConfigurations.$USERNAME.activationPackage")
fi
install -d -o "$USERNAME" -g users /nix/var/nix/profiles/per-user/$USERNAME
install -d -o "$USERNAME" -g users /nix/var/nix/gcroots/per-user/$USERNAME
# activate's profile ops need store access; as the user that means a

View File

@@ -59,10 +59,27 @@ def die(msg: str) -> "None":
sys.exit(1)
def notify(body: str) -> None:
if shutil.which("notify-send"):
subprocess.run(["notify-send", "-a", "Nomarchy", "Nomarchy", body],
capture_output=True)
# All Nomarchy theme notifications share this synchronous tag, so the
# notification daemon (swaync) REPLACES the previous one in place instead
# of stacking — the "rebuilding…" toast becomes the "applied ✓" toast.
NOTIFY_SYNC_TAG = "nomarchy-theme-sync"
def notify(body: str, summary: str = "Nomarchy", *,
persistent: bool = False, urgency: str = "normal") -> None:
"""Desktop notification. persistent (timeout 0) keeps it up for the
whole rebuild so a slow switch never reads as a failed selection;
the success/failure call replaces it via the synchronous tag."""
if shutil.which("notify-send") is None:
return
subprocess.run(
["notify-send", "-a", "Nomarchy",
"-u", urgency,
"-t", "0" if persistent else "5000",
"-h", f"string:x-canonical-private-synchronous:{NOTIFY_SYNC_TAG}",
summary, body],
capture_output=True,
)
# ─── State management ─────────────────────────────────────────────────────
@@ -109,6 +126,29 @@ def deep_merge(base: dict, override: dict) -> dict:
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 ─────────────────────────────────────────────────────
def run_switch() -> None:
@@ -122,10 +162,13 @@ def run_switch() -> None:
return
log(f"rebuilding: {' '.join(argv)}")
notify("Applying theme — rebuilding the desktop…")
# Persistent (timeout 0): stays up for the whole rebuild — replaced
# in place by the success/failure notification below — so a multi-
# minute switch never looks like it silently failed.
notify("Applying theme — rebuilding the desktop…", persistent=True)
result = subprocess.run(argv) # stream output to the caller's terminal
if result.returncode != 0:
notify("Theme rebuild FAILED — see terminal / journal")
notify("Theme rebuild FAILED — see terminal / journal", urgency="critical")
die("rebuild failed (state file already updated; fix and re-run)")
notify("Theme applied ✓")
@@ -219,6 +262,7 @@ def cmd_apply(args) -> None:
state = deep_merge(load_state(), preset)
write_state(state)
log(f"theme: {state.get('name', args.theme)}")
check_fonts(state)
if not args.no_switch:
run_switch()
apply_wallpaper(state)
@@ -241,6 +285,8 @@ def cmd_set(args) -> None:
write_state(state)
log(f"set {args.path} = {value!r}")
if args.path.startswith("fonts."):
check_fonts(state)
if not args.no_switch:
run_switch()
apply_wallpaper(state)

View File

@@ -9,6 +9,9 @@
# nomarchy.terminal = "kitty"; # swap the default terminal
# nomarchy.waybar.enable = false; # bring your own bar
# nomarchy.stylix.enable = false; # opt out of GTK/Qt theming
# nomarchy.keyboard.layout = "de"; # session keyboard (pair with
# # xkb + console.useXkbConfig in
# # system.nix for tty/LUKS)
home.packages = with pkgs; [
# firefox

View File

@@ -18,6 +18,10 @@
"warn": "#e0af68",
"bad": "#f7768e"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#32344a",
"#f7768e",

View File

@@ -18,6 +18,10 @@
"warn": "#df8e1d",
"bad": "#d20f39"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#bcc0cc",
"#d20f39",

View File

@@ -18,6 +18,10 @@
"warn": "#f9e2af",
"bad": "#f38ba8"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#45475a",
"#f38ba8",

View File

@@ -18,6 +18,10 @@
"warn": "#E9BB4F",
"bad": "#ED5B5A"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#060B1E",
"#ED5B5A",

View File

@@ -18,6 +18,10 @@
"warn": "#dbbc7f",
"bad": "#e67e80"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#475258",
"#e67e80",

View File

@@ -18,6 +18,10 @@
"warn": "#D0A215",
"bad": "#D14D41"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#100F0F",
"#D14D41",

View File

@@ -18,6 +18,10 @@
"warn": "#d8a657",
"bad": "#ea6962"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#3c3836",
"#ea6962",

View File

@@ -18,6 +18,10 @@
"warn": "#50f7d4",
"bad": "#50f872"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#0B0C16",
"#50f872",

View File

@@ -18,6 +18,10 @@
"warn": "#c0a36e",
"bad": "#c34043"
},
"border": {
"active": "text",
"inactive": "overlay"
},
"ansi": [
"#090618",
"#c34043",

View File

@@ -18,6 +18,10 @@
"warn": "#6fa4c9",
"bad": "#4d86b0"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#1b2d40",
"#4d86b0",

View File

@@ -18,6 +18,10 @@
"warn": "#b91c1c",
"bad": "#D35F5F"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#333333",
"#D35F5F",

View File

@@ -18,6 +18,10 @@
"warn": "#b36d43",
"bad": "#685742"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#000000",
"#685742",

View File

@@ -18,6 +18,10 @@
"warn": "#ebcb8b",
"bad": "#bf616a"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#3b4252",
"#bf616a",

View File

@@ -18,6 +18,10 @@
"warn": "#459451",
"bad": "#FF5345"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#23372B",
"#FF5345",

View File

@@ -18,6 +18,10 @@
"warn": "#e97b3c",
"bad": "#f85525"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#00172e",
"#f85525",

View File

@@ -18,6 +18,10 @@
"warn": "#f9cc6c",
"bad": "#fd6883"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#72696a",
"#fd6883",

View File

@@ -18,6 +18,10 @@
"warn": "#ea9d34",
"bad": "#b4637a"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#f2e9e1",
"#b4637a",

View File

@@ -18,6 +18,10 @@
"warn": "#dfa000",
"bad": "#f85552"
},
"border": {
"active": "text",
"inactive": "overlay"
},
"ansi": [
"#5c6a72",
"#f85552",

131
themes/summer-day/rofi.rasi Normal file
View File

@@ -0,0 +1,131 @@
/**
* Nomarchy Summer Day — rofi whole-swap (themes/<slug>/rofi.rasi).
* Ported from the legacy branch. The `configuration {}` block is omitted
* on purpose: modi/terminal/show-icons come from modules/home/rofi.nix.
* Iosevka 17 → a shipped Nerd Font.
*/
* {
bg0: #fdf6e3;
fg: #5c6a72;
red: #f85552;
yellow: #dfa000;
green: #8da101;
aqua: #35a77c;
blue: #3a94c5;
purple: #df69ba;
grey0: #a6b0a0;
font: "JetBrainsMono Nerd Font 14";
background-color: transparent;
text-color: @fg;
}
window {
transparency: "real";
background-color: @fg;
text-color: @bg0;
border-bottom: 5px;
border-color: @yellow;
border-radius: 15px;
width: 40%;
height: 50%;
location: center;
anchor: center;
x-offset: 0;
y-offset: 0;
}
mainbox {
background-color: transparent;
children: [ inputbar, listview ];
spacing: 0px;
padding: 0px;
}
inputbar {
children: [ prompt, entry ];
background-color: @green;
text-color: @bg0;
expand: false;
border-bottom: 5px;
border-color: @grey0;
border-radius: 10px;
margin: 15px;
padding: 10px;
spacing: 15px;
}
prompt {
enabled: true;
background-color: inherit;
text-color: inherit;
}
entry {
background-color: inherit;
text-color: inherit;
cursor: text;
placeholder: "Type to search...";
placeholder-color: @bg0;
}
listview {
background-color: transparent;
columns: 1;
lines: 8;
spacing: 0px;
cycle: true;
dynamic: true;
layout: vertical;
padding: 20px;
}
element {
background-color: transparent;
text-color: @bg0;
orientation: horizontal;
border-radius: 10px;
padding: 10px;
spacing: 15px;
}
element-icon {
background-color: transparent;
text-color: inherit;
size: 40px;
cursor: inherit;
}
element-text {
background-color: transparent;
text-color: inherit;
highlight: bold;
cursor: inherit;
vertical-align: 0.5;
horizontal-align: 0.0;
}
element selected {
background-color: @bg0;
text-color: @fg;
border-bottom: 5px;
border-color: #bdc3af;
border-radius: 10px;
}
element-text selected {
text-color: @fg;
}
element-icon selected {
text-color: @fg;
}
scrollbar {
width: 0px;
background-color: transparent;
handle-width: 0px;
handle-color: transparent;
border: 0px;
}

View File

@@ -49,6 +49,7 @@ window#waybar {
#pulseaudio,
#network,
#battery,
#tray,
#custom-powermenu {
background-color: @bg0;
color: @fg;
@@ -89,6 +90,10 @@ window#waybar {
margin-left: 15px;
padding-left: 20px;
padding-right: 21px;
/* The Nomarchy mark lives at U+F000 in the dedicated "Nomarchy" font;
pin the family or the Nerd Font's glass icon at U+F000 wins. */
font-family: Nomarchy;
font-size: 18px;
}
#custom-powermenu {

View File

@@ -0,0 +1,88 @@
{
"margin-top": 0,
"margin-left": 120,
"margin-bottom": 0,
"margin-right": 120,
"height": 60,
"layer": "top",
"position": "top",
"spacing": 15,
"reload_style_on_change": true,
"modules-left": ["custom/launcher", "clock", "clock#date"],
"modules-center": ["hyprland/workspaces"],
"modules-right": ["pulseaudio", "network", "battery", "tray", "custom/powermenu"],
"hyprland/workspaces": {
"disable-scroll": true,
"all-outputs": true,
"on-click": "activate",
"on-scroll-up": "hyprctl dispatch workspace e+1",
"on-scroll-down": "hyprctl dispatch workspace e-1"
},
"custom/launcher": {
"interval": "once",
"format": "\uf000",
"on-click": "nomarchy-menu",
"tooltip-format": "Nomarchy menu"
},
"pulseaudio": {
"format": "{icon} {volume}%",
"format-bluetooth": "{icon}  {volume}%",
"format-bluetooth-muted": "  muted",
"format-muted": " muted",
"format-icons": {
"headphone": "",
"hands-free": "",
"headset": "",
"phone": "",
"portable": "",
"car": "",
"default": ["", "", ""]
},
"on-click": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle",
"tooltip-format": "Playing at {volume}%"
},
"network": {
"format-wifi": " {signalStrength}%",
"format-ethernet": "",
"format-disconnected": "󰤭",
"on-click": "sh -c '$TERMINAL -e nmtui'",
"tooltip-format": "{ipaddr} via {gwaddr}"
},
"battery": {
"interval": 60,
"states": { "warning": 30, "critical": 15 },
"max-length": 10,
"format": "{icon} {capacity}%",
"format-charging": " {capacity}%",
"format-plugged": " {capacity}%",
"format-full": " 100%",
"format-icons": ["", "", "", "", ""],
"on-click": "nomarchy-menu power",
"tooltip-format": "Battery status"
},
"clock": {
"format": " {:%H:%M}",
"tooltip": true,
"tooltip-format": "<tt><small>{calendar}</small></tt>"
},
"clock#date": {
"format": " {:%A, %B %d, %Y}",
"tooltip": false
},
"tray": { "spacing": 8 },
"custom/powermenu": {
"format": "",
"on-click": "nomarchy-menu power",
"tooltip": false
}
}

View File

@@ -18,6 +18,10 @@
"warn": "#d9bb80",
"bad": "#e68183"
},
"border": {
"active": "text",
"inactive": "overlay"
},
"ansi": [
"#3c474d",
"#e68183",

View File

@@ -0,0 +1,131 @@
/**
* Nomarchy Summer Night — rofi whole-swap (themes/<slug>/rofi.rasi).
* Ported from the legacy branch. The `configuration {}` block is omitted
* on purpose: modi/terminal/show-icons come from modules/home/rofi.nix.
* Iosevka 17 → a shipped Nerd Font.
*/
* {
bg0: #2d353b;
fg: #d3c6aa;
red: #e68183;
yellow: #d9bb80;
green: #a7c080;
aqua: #83b6af;
blue: #83b6af;
purple: #d39bb6;
grey0: #868d80;
font: "JetBrainsMono Nerd Font 14";
background-color: transparent;
text-color: @fg;
}
window {
transparency: "real";
background-color: @fg;
text-color: @bg0;
border-bottom: 5px;
border-color: @yellow;
border-radius: 15px;
width: 40%;
height: 50%;
location: center;
anchor: center;
x-offset: 0;
y-offset: 0;
}
mainbox {
background-color: transparent;
children: [ inputbar, listview ];
spacing: 0px;
padding: 0px;
}
inputbar {
children: [ prompt, entry ];
background-color: @green;
text-color: @bg0;
expand: false;
border-bottom: 5px;
border-color: @grey0;
border-radius: 10px;
margin: 15px;
padding: 10px;
spacing: 15px;
}
prompt {
enabled: true;
background-color: inherit;
text-color: inherit;
}
entry {
background-color: inherit;
text-color: inherit;
cursor: text;
placeholder: "Type to search...";
placeholder-color: @bg0;
}
listview {
background-color: transparent;
columns: 1;
lines: 8;
spacing: 0px;
cycle: true;
dynamic: true;
layout: vertical;
padding: 20px;
}
element {
background-color: transparent;
text-color: @bg0;
orientation: horizontal;
border-radius: 10px;
padding: 10px;
spacing: 15px;
}
element-icon {
background-color: transparent;
text-color: inherit;
size: 40px;
cursor: inherit;
}
element-text {
background-color: transparent;
text-color: inherit;
highlight: bold;
cursor: inherit;
vertical-align: 0.5;
horizontal-align: 0.0;
}
element selected {
background-color: @bg0;
text-color: @fg;
border-bottom: 5px;
border-color: #161a1d;
border-radius: 10px;
}
element-text selected {
text-color: @fg;
}
element-icon selected {
text-color: @fg;
}
scrollbar {
width: 0px;
background-color: transparent;
handle-width: 0px;
handle-color: transparent;
border: 0px;
}

View File

@@ -53,8 +53,9 @@ window#waybar {
#clock.date,
#workspaces,
#pulseaudio,
#network,
#idle_inhibitor,
#custom-battery,
#battery,
#custom-powermenu {
background-color: @bg0;
color: @fg;
@@ -98,6 +99,8 @@ window#waybar {
margin-left: 15px;
padding-left: 20px;
padding-right: 21px;
/* The Nomarchy mark lives at U+F000 in the dedicated "Nomarchy" font;
pin the family or the Nerd Font's glass icon at U+F000 wins. */
font-family: Nomarchy;
font-size: 20px;
}
@@ -112,7 +115,7 @@ window#waybar {
padding-right: 23px;
}
#custom-battery.preservation {
#battery.charging {
color: @green;
}

View File

@@ -0,0 +1,95 @@
{
"margin-top": 0,
"margin-left": 120,
"margin-bottom": 0,
"margin-right": 120,
"height": 61,
"layer": "top",
"position": "top",
"spacing": 15,
"reload_style_on_change": true,
"modules-left": ["custom/nomarchy", "clock", "clock#date"],
"modules-center": ["hyprland/workspaces"],
"modules-right": ["idle_inhibitor", "pulseaudio", "network", "battery", "tray", "custom/powermenu"],
"hyprland/workspaces": {
"disable-scroll": true,
"all-outputs": true,
"on-click": "activate",
"on-scroll-up": "hyprctl dispatch workspace r+1",
"on-scroll-down": "hyprctl dispatch workspace r-1"
},
"custom/nomarchy": {
"interval": "once",
"format": "\uf000",
"on-click": "nomarchy-menu",
"tooltip-format": "Nomarchy menu"
},
"idle_inhibitor": {
"format": "{icon}",
"format-icons": {
"activated": " 󰅶 ",
"deactivated": " 󰾪 "
}
},
"pulseaudio": {
"scroll-step": 5,
"format": "{icon} {volume}%",
"format-bluetooth": "{icon} {volume}%",
"format-bluetooth-muted": " {icon}",
"format-muted": " muted",
"format-icons": {
"headphone": "",
"hands-free": "",
"headset": "",
"phone": "",
"portable": "",
"car": "",
"default": ["", "", ""]
},
"on-click": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle",
"tooltip-format": "{desc} | {volume}%"
},
"network": {
"format-wifi": " {essid}",
"format-ethernet": "",
"format-disconnected": "󰤭",
"on-click": "sh -c '$TERMINAL -e nmtui'",
"tooltip-format": "{ipaddr} via {gwaddr}"
},
"battery": {
"interval": 30,
"states": { "warning": 25, "critical": 10 },
"format": "{icon} {capacity}%",
"format-charging": " {capacity}%",
"format-plugged": " {capacity}%",
"format-icons": ["", "", "", "", ""],
"on-click": "nomarchy-menu power",
"tooltip-format": "Battery status"
},
"clock": {
"format": " {:%H:%M}",
"tooltip": true,
"tooltip-format": "<tt><small>{calendar}</small></tt>"
},
"clock#date": {
"format": " {:%A, %B %d, %Y}",
"tooltip": false
},
"tray": { "spacing": 8 },
"custom/powermenu": {
"format": "",
"on-click": "nomarchy-menu power",
"tooltip": false
}
}

View File

@@ -18,6 +18,10 @@
"warn": "#e0af68",
"bad": "#f7768e"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#32344a",
"#f7768e",

View File

@@ -18,6 +18,10 @@
"warn": "#cecece",
"bad": "#a4a4a4"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#0d0d0d",
"#a4a4a4",

View File

@@ -18,6 +18,10 @@
"warn": "#4a4a4a",
"bad": "#2a2a2a"
},
"border": {
"active": "accent",
"inactive": "overlay"
},
"ansi": [
"#ffffff",
"#2a2a2a",

View File

@@ -47,8 +47,8 @@ if df --output=fstype "$TARGET_DIR" | tail -1 | grep -q tmpfs; then
exit 1
fi
OVMF=""
for c in /run/current-system/sw/share/qemu/edk2-x86_64-code.fd \
OVMF="${NOMARCHY_OVMF:-}"
[ -n "$OVMF" ] || for c in /run/current-system/sw/share/qemu/edk2-x86_64-code.fd \
/run/current-system/sw/share/OVMF/OVMF_CODE.fd \
/usr/share/OVMF/OVMF_CODE.fd; do
[ -f "$c" ] && { OVMF="$c"; break; }