diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..354cf9b --- /dev/null +++ b/.claude/settings.local.json @@ -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 {}\\).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\")" + ] + } +} diff --git a/README.md b/README.md index c1bff82..2848563 100644 --- a/README.md +++ b/README.md @@ -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 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. diff --git a/docs/OVERRIDES.md b/docs/OVERRIDES.md new file mode 100644 index 0000000..cc917d3 --- /dev/null +++ b/docs/OVERRIDES.md @@ -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//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..enable = false` | +| Raw Hyprland lines | `wayland.windowManager.hyprland.extraConfig` | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..ad78800 --- /dev/null +++ b/docs/ROADMAP.md @@ -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//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//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 `) 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..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+` 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. diff --git a/flake.lock b/flake.lock index 075584c..7546885 100644 --- a/flake.lock +++ b/flake.lock @@ -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" } diff --git a/flake.nix b/flake.nix index 11862e5..825a676 100644 --- a/flake.nix +++ b/flake.nix @@ -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 ` 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 diff --git a/hosts/live.nix b/hosts/live.nix index 786a0bc..a8e9b2f 100644 --- a/hosts/live.nix +++ b/hosts/live.nix @@ -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. diff --git a/lib.nix b/lib.nix index 674ad95..ff9746c 100644 --- a/lib.nix +++ b/lib.nix @@ -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 ++ [ diff --git a/modules/home/default.nix b/modules/home/default.nix index 699ce68..5b22305 100644 --- a/modules/home/default.nix +++ b/modules/home/default.nix @@ -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 ]; diff --git a/modules/home/fastfetch.nix b/modules/home/fastfetch.nix new file mode 100644 index 0000000..6d2422e --- /dev/null +++ b/modules/home/fastfetch.nix @@ -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"; } + ]; + }; + }; + }; +} diff --git a/modules/home/ghostty.nix b/modules/home/ghostty.nix index 01c45f5..1c54715 100644 --- a/modules/home/ghostty.nix +++ b/modules/home/ghostty.nix @@ -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; }; }; } diff --git a/modules/home/hyprland.nix b/modules/home/hyprland.nix index 6a44b8a..13260da 100644 --- a/modules/home/hyprland.nix +++ b/modules/home/hyprland.nix @@ -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.` (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 = [ diff --git a/modules/home/idle.nix b/modules/home/idle.nix new file mode 100644 index 0000000..62d7849 --- /dev/null +++ b/modules/home/idle.nix @@ -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} || ` 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 = "password…"; + }]; + + 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"; } + ]; + }; + }; + }; +} diff --git a/modules/home/keybinds.nix b/modules/home/keybinds.nix new file mode 100644 index 0000000..7b60b1c --- /dev/null +++ b/modules/home/keybinds.nix @@ -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+ 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"; } + ]; +} diff --git a/modules/home/keys.nix b/modules/home/keys.nix new file mode 100644 index 0000000..aea9840 --- /dev/null +++ b/modules/home/keys.nix @@ -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; + }; + }; +} diff --git a/modules/home/options.nix b/modules/home/options.nix index 69eafb8..0a328b7 100644 --- a/modules/home/options.nix +++ b/modules/home/options.nix @@ -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; }; diff --git a/modules/home/osd.nix b/modules/home/osd.nix new file mode 100644 index 0000000..df82ed1 --- /dev/null +++ b/modules/home/osd.nix @@ -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; + }; + }; +} diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix new file mode 100644 index 0000000..39b5f0d --- /dev/null +++ b/modules/home/rofi.nix @@ -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//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"; + }; + }; + }; + }; +} diff --git a/modules/home/shell.nix b/modules/home/shell.nix new file mode 100644 index 0000000..654294b --- /dev/null +++ b/modules/home/shell.nix @@ -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) + }; + }; +} diff --git a/modules/home/stylix.nix b/modules/home/stylix.nix index 0edb147..ec04d83 100644 --- a/modules/home/stylix.nix +++ b/modules/home/stylix.nix @@ -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"; }; } diff --git a/modules/home/swaync.nix b/modules/home/swaync.nix new file mode 100644 index 0000000..ed2cba9 --- /dev/null +++ b/modules/home/swaync.nix @@ -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; + } + ''; + }; + }; +} diff --git a/modules/home/theme.nix b/modules/home/theme.nix index 0710fe8..8221f19 100644 --- a/modules/home/theme.nix +++ b/modules/home/theme.nix @@ -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) diff --git a/modules/home/waybar.nix b/modules/home/waybar.nix index e1bc3e5..0cf1f59 100644 --- a/modules/home/waybar.nix +++ b/modules/home/waybar.nix @@ -7,11 +7,39 @@ # //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//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 + ); }; } diff --git a/modules/home/yazi.nix b/modules/home/yazi.nix new file mode 100644 index 0000000..6ca8d7d --- /dev/null +++ b/modules/home/yazi.nix @@ -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 = ""; 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; }; + }; + }; + }; + }; +} diff --git a/modules/nixos/branding/Nomarchy.ttf b/modules/nixos/branding/Nomarchy.ttf new file mode 100644 index 0000000..b09cd9f Binary files /dev/null and b/modules/nixos/branding/Nomarchy.ttf differ diff --git a/modules/nixos/branding/logo.svg b/modules/nixos/branding/logo.svg new file mode 100644 index 0000000..b060ba0 --- /dev/null +++ b/modules/nixos/branding/logo.svg @@ -0,0 +1,64 @@ + + + + diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix index f4e03f7..ac4ab14 100644 --- a/modules/nixos/default.nix +++ b/modules/nixos/default.nix @@ -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 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..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 = { diff --git a/modules/nixos/file-manager.nix b/modules/nixos/file-manager.nix new file mode 100644 index 0000000..adc8c92 --- /dev/null +++ b/modules/nixos/file-manager.nix @@ -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 + ]; + }; +} diff --git a/modules/nixos/options.nix b/modules/nixos/options.nix index 2b93139..9815bbf 100644 --- a/modules/nixos/options.nix +++ b/modules/nixos/options.nix @@ -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. + ''; + }; + }; }; } diff --git a/modules/nixos/plymouth.nix b/modules/nixos/plymouth.nix new file mode 100644 index 0000000..91fb4b1 --- /dev/null +++ b/modules/nixos/plymouth.nix @@ -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.0–1.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" + ]; + }; +} diff --git a/modules/nixos/plymouth/bullet.png b/modules/nixos/plymouth/bullet.png new file mode 100644 index 0000000..62249b3 Binary files /dev/null and b/modules/nixos/plymouth/bullet.png differ diff --git a/modules/nixos/plymouth/entry.png b/modules/nixos/plymouth/entry.png new file mode 100644 index 0000000..5c78917 Binary files /dev/null and b/modules/nixos/plymouth/entry.png differ diff --git a/modules/nixos/plymouth/lock.png b/modules/nixos/plymouth/lock.png new file mode 100644 index 0000000..3046de1 Binary files /dev/null and b/modules/nixos/plymouth/lock.png differ diff --git a/modules/nixos/plymouth/logo.png b/modules/nixos/plymouth/logo.png new file mode 100644 index 0000000..dbcde56 Binary files /dev/null and b/modules/nixos/plymouth/logo.png differ diff --git a/modules/nixos/plymouth/nomarchy.plymouth b/modules/nixos/plymouth/nomarchy.plymouth new file mode 100644 index 0000000..4ade91c --- /dev/null +++ b/modules/nixos/plymouth/nomarchy.plymouth @@ -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 diff --git a/modules/nixos/plymouth/nomarchy.script b/modules/nixos/plymouth/nomarchy.script new file mode 100644 index 0000000..216dac9 --- /dev/null +++ b/modules/nixos/plymouth/nomarchy.script @@ -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(); + } diff --git a/modules/nixos/plymouth/progress_bar.png b/modules/nixos/plymouth/progress_bar.png new file mode 100644 index 0000000..dbb9fd7 Binary files /dev/null and b/modules/nixos/plymouth/progress_bar.png differ diff --git a/modules/nixos/plymouth/progress_box.png b/modules/nixos/plymouth/progress_box.png new file mode 100644 index 0000000..6a263f2 Binary files /dev/null and b/modules/nixos/plymouth/progress_box.png differ diff --git a/modules/nixos/power.nix b/modules/nixos/power.nix new file mode 100644 index 0000000..8e8ce22 --- /dev/null +++ b/modules/nixos/power.nix @@ -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 + ''; + }; + }; +} diff --git a/pkgs/nomarchy-install/nomarchy-install.sh b/pkgs/nomarchy-install/nomarchy-install.sh index 8ea40b6..2d108ea 100644 --- a/pkgs/nomarchy-install/nomarchy-install.sh +++ b/pkgs/nomarchy-install/nomarchy-install.sh @@ -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" </dev/null; then + power_lines+=" + nomarchy.system.power.thermal.enable = true; # thermald (Intel)" + fi + POWER_CONFIG=$(cat < "$FLAKE_DIR/system.nix" < "$FLAKE_DIR/system.nix" < "$FLAKE_DIR/system.nix" < /mnt/root/nomarchy-hm-activate.sh < /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 diff --git a/pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py b/pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py index ba136a7..16c1e54 100644 --- a/pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py +++ b/pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py @@ -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) diff --git a/templates/downstream/home.nix b/templates/downstream/home.nix index 29b4ba1..9420a2c 100644 --- a/templates/downstream/home.nix +++ b/templates/downstream/home.nix @@ -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 diff --git a/theme-state.json b/theme-state.json index 635e11d..04b0e7b 100644 --- a/theme-state.json +++ b/theme-state.json @@ -18,6 +18,10 @@ "warn": "#e0af68", "bad": "#f7768e" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#32344a", "#f7768e", diff --git a/themes/catppuccin-latte.json b/themes/catppuccin-latte.json index a6565d7..321a28e 100644 --- a/themes/catppuccin-latte.json +++ b/themes/catppuccin-latte.json @@ -18,6 +18,10 @@ "warn": "#df8e1d", "bad": "#d20f39" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#bcc0cc", "#d20f39", diff --git a/themes/catppuccin.json b/themes/catppuccin.json index da34057..c7cb9f9 100644 --- a/themes/catppuccin.json +++ b/themes/catppuccin.json @@ -18,6 +18,10 @@ "warn": "#f9e2af", "bad": "#f38ba8" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#45475a", "#f38ba8", diff --git a/themes/ethereal.json b/themes/ethereal.json index 2b8fdc0..2ac204b 100644 --- a/themes/ethereal.json +++ b/themes/ethereal.json @@ -18,6 +18,10 @@ "warn": "#E9BB4F", "bad": "#ED5B5A" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#060B1E", "#ED5B5A", diff --git a/themes/everforest.json b/themes/everforest.json index 054406e..e604bbd 100644 --- a/themes/everforest.json +++ b/themes/everforest.json @@ -18,6 +18,10 @@ "warn": "#dbbc7f", "bad": "#e67e80" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#475258", "#e67e80", diff --git a/themes/flexoki-light.json b/themes/flexoki-light.json index e972540..4be4409 100644 --- a/themes/flexoki-light.json +++ b/themes/flexoki-light.json @@ -18,6 +18,10 @@ "warn": "#D0A215", "bad": "#D14D41" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#100F0F", "#D14D41", diff --git a/themes/gruvbox.json b/themes/gruvbox.json index a95691d..fd8c94a 100644 --- a/themes/gruvbox.json +++ b/themes/gruvbox.json @@ -18,6 +18,10 @@ "warn": "#d8a657", "bad": "#ea6962" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#3c3836", "#ea6962", diff --git a/themes/hackerman.json b/themes/hackerman.json index 39bd534..04d6518 100644 --- a/themes/hackerman.json +++ b/themes/hackerman.json @@ -18,6 +18,10 @@ "warn": "#50f7d4", "bad": "#50f872" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#0B0C16", "#50f872", diff --git a/themes/kanagawa.json b/themes/kanagawa.json index 96076d0..6bc93bb 100644 --- a/themes/kanagawa.json +++ b/themes/kanagawa.json @@ -18,6 +18,10 @@ "warn": "#c0a36e", "bad": "#c34043" }, + "border": { + "active": "text", + "inactive": "overlay" + }, "ansi": [ "#090618", "#c34043", diff --git a/themes/lumon.json b/themes/lumon.json index e8558df..554c34b 100644 --- a/themes/lumon.json +++ b/themes/lumon.json @@ -18,6 +18,10 @@ "warn": "#6fa4c9", "bad": "#4d86b0" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#1b2d40", "#4d86b0", diff --git a/themes/matte-black.json b/themes/matte-black.json index 714b5f2..a5432d2 100644 --- a/themes/matte-black.json +++ b/themes/matte-black.json @@ -18,6 +18,10 @@ "warn": "#b91c1c", "bad": "#D35F5F" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#333333", "#D35F5F", diff --git a/themes/miasma.json b/themes/miasma.json index bcc23cf..910f279 100644 --- a/themes/miasma.json +++ b/themes/miasma.json @@ -18,6 +18,10 @@ "warn": "#b36d43", "bad": "#685742" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#000000", "#685742", diff --git a/themes/nord.json b/themes/nord.json index 6c12550..b3f7910 100644 --- a/themes/nord.json +++ b/themes/nord.json @@ -18,6 +18,10 @@ "warn": "#ebcb8b", "bad": "#bf616a" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#3b4252", "#bf616a", diff --git a/themes/osaka-jade.json b/themes/osaka-jade.json index e794f56..78fc5ee 100644 --- a/themes/osaka-jade.json +++ b/themes/osaka-jade.json @@ -18,6 +18,10 @@ "warn": "#459451", "bad": "#FF5345" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#23372B", "#FF5345", diff --git a/themes/retro-82.json b/themes/retro-82.json index a5c9177..ccd05b6 100644 --- a/themes/retro-82.json +++ b/themes/retro-82.json @@ -18,6 +18,10 @@ "warn": "#e97b3c", "bad": "#f85525" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#00172e", "#f85525", diff --git a/themes/ristretto.json b/themes/ristretto.json index 528d5aa..eab2487 100644 --- a/themes/ristretto.json +++ b/themes/ristretto.json @@ -18,6 +18,10 @@ "warn": "#f9cc6c", "bad": "#fd6883" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#72696a", "#fd6883", diff --git a/themes/rose-pine.json b/themes/rose-pine.json index b11da41..9ada386 100644 --- a/themes/rose-pine.json +++ b/themes/rose-pine.json @@ -18,6 +18,10 @@ "warn": "#ea9d34", "bad": "#b4637a" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#f2e9e1", "#b4637a", diff --git a/themes/summer-day.json b/themes/summer-day.json index 29132ad..da9124a 100644 --- a/themes/summer-day.json +++ b/themes/summer-day.json @@ -18,6 +18,10 @@ "warn": "#dfa000", "bad": "#f85552" }, + "border": { + "active": "text", + "inactive": "overlay" + }, "ansi": [ "#5c6a72", "#f85552", diff --git a/themes/summer-day/rofi.rasi b/themes/summer-day/rofi.rasi new file mode 100644 index 0000000..5ae6c11 --- /dev/null +++ b/themes/summer-day/rofi.rasi @@ -0,0 +1,131 @@ +/** + * Nomarchy Summer Day — rofi whole-swap (themes//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; +} diff --git a/themes/summer-day/waybar.css b/themes/summer-day/waybar.css index 18ac8f3..85617ca 100755 --- a/themes/summer-day/waybar.css +++ b/themes/summer-day/waybar.css @@ -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 { diff --git a/themes/summer-day/waybar.jsonc b/themes/summer-day/waybar.jsonc new file mode 100644 index 0000000..7909d2c --- /dev/null +++ b/themes/summer-day/waybar.jsonc @@ -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": "{calendar}" + }, + + "clock#date": { + "format": " {:%A, %B %d, %Y}", + "tooltip": false + }, + + "tray": { "spacing": 8 }, + + "custom/powermenu": { + "format": "", + "on-click": "nomarchy-menu power", + "tooltip": false + } +} diff --git a/themes/summer-night.json b/themes/summer-night.json index 488d3b1..99b07cf 100644 --- a/themes/summer-night.json +++ b/themes/summer-night.json @@ -18,6 +18,10 @@ "warn": "#d9bb80", "bad": "#e68183" }, + "border": { + "active": "text", + "inactive": "overlay" + }, "ansi": [ "#3c474d", "#e68183", diff --git a/themes/summer-night/rofi.rasi b/themes/summer-night/rofi.rasi new file mode 100644 index 0000000..a268ad7 --- /dev/null +++ b/themes/summer-night/rofi.rasi @@ -0,0 +1,131 @@ +/** + * Nomarchy Summer Night — rofi whole-swap (themes//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; +} diff --git a/themes/summer-night/waybar.css b/themes/summer-night/waybar.css index 83466ad..da7538c 100644 --- a/themes/summer-night/waybar.css +++ b/themes/summer-night/waybar.css @@ -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; } diff --git a/themes/summer-night/waybar.jsonc b/themes/summer-night/waybar.jsonc new file mode 100644 index 0000000..875a96d --- /dev/null +++ b/themes/summer-night/waybar.jsonc @@ -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": "{calendar}" + }, + + "clock#date": { + "format": " {:%A, %B %d, %Y}", + "tooltip": false + }, + + "tray": { "spacing": 8 }, + + "custom/powermenu": { + "format": "", + "on-click": "nomarchy-menu power", + "tooltip": false + } +} diff --git a/themes/tokyo-night.json b/themes/tokyo-night.json index aa7b209..e60a0d4 100644 --- a/themes/tokyo-night.json +++ b/themes/tokyo-night.json @@ -18,6 +18,10 @@ "warn": "#e0af68", "bad": "#f7768e" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#32344a", "#f7768e", diff --git a/themes/vantablack.json b/themes/vantablack.json index fada473..ef3e4ca 100644 --- a/themes/vantablack.json +++ b/themes/vantablack.json @@ -18,6 +18,10 @@ "warn": "#cecece", "bad": "#a4a4a4" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#0d0d0d", "#a4a4a4", diff --git a/themes/white.json b/themes/white.json index b65f62f..97dff38 100644 --- a/themes/white.json +++ b/themes/white.json @@ -18,6 +18,10 @@ "warn": "#4a4a4a", "bad": "#2a2a2a" }, + "border": { + "active": "accent", + "inactive": "overlay" + }, "ansi": [ "#ffffff", "#2a2a2a", diff --git a/tools/test-install.sh b/tools/test-install.sh index 48cd1a2..37db568 100755 --- a/tools/test-install.sh +++ b/tools/test-install.sh @@ -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; }