Compare commits

...

32 Commits

Author SHA1 Message Date
Bernardo Magri
82659566ac chore: untrack config.rasi (scratch reference committed by mistake)
config.rasi was the rofi theme dropped in to show the desired style; it's
not part of the distro. Remove it from tracking (the file stays on disk,
untracked). Its content informed 994025f and remains in that history.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 22:12:02 +01:00
Bernardo Magri
994025f0c8 feat(rofi): real icons, zebra rows, content-fit menus across all themes
Reworked the menu look, palette-driven so it lands on every theme:

  - Real Papirus icons in place of Nerd Font glyphs: a row() helper emits
    rofi's per-row icon protocol, and the five static menus (root, Tools,
    System, Power, Capture) map each entry to a freedesktop icon name
    (verified present in Papirus). Entry text is now clean ("Apps"), the
    case-matching is unchanged.
  - Larger icons via a new t.ui.iconSize knob (36px, generated theme); the
    summer whole-swaps keep their 40px identity.
  - Rounded, roomy rows with subtle zebra striping: alternate rows lift to
    @surface (a faint bg0 wash on the summer cream/slate windows), the
    selected row to @accent — using rofi's normal/alternate/selected row
    parities.
  - Content-fit windows: fixed-num-lines=false so a 6-entry menu stops
    reserving 8 rows of empty space (the launcher still fills + scrolls).
    Dropped the fixed height:50% from the summer .rasi so they size to
    content too — which also unhid their clipped Power/Keybindings rows.
  - Title-cased the menu prompts.

Verified by building the HM config and rendering the generated theme plus
both summer whole-swaps headlessly (sway + grim): icons resolve, zebra
shows, every menu hugs its rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 22:11:25 +01:00
Bernardo Magri
b0b2a4d732 docs(services): note the restic-nomarchy list/restore wrapper
VM-verified the full batch of opt-in service toggles (libvirt, docker, obs
+ v4l2loopback, ollama, printing, openrgb, wireshark, adb, gamemode,
kdeconnect, restic) in a headless nixosTest — all green, no module bugs.

The one thing worth surfacing: services.restic ships a pre-configured
`restic-nomarchy` wrapper (repository + passwordFile baked in) for listing
and restoring snapshots. Mention it in the option description and README so
the restore path is discoverable — a backup you can't restore is half a
feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:18:49 +01:00
Bernardo Magri
964a5c98b7 feat(services): openrgb + restic toggles; close out the opt-in services item
Finishing the four remaining "opt-in services" candidates — but two of
them aren't service-shaped, so the package-vs-toggle principle applies:

  - Nextcloud client  → a bare tray app; already in the app-suite menu,
                        not a service.
  - WireGuard         → NetworkManager (on distro-wide) imports .conf
                        tunnels natively, so there's no daemon to "just
                        enable". Only the wg/wg-quick CLI is missing →
                        wireguard-tools added to the app-suite menu.

The two that ARE config-backed become nomarchy.services.* toggles:

  - openrgb → services.hardware.openrgb (daemon + device udev rules).
  - restic  → a small scaffolded surface (repository / passwordFile /
              paths) over services.restic.backups: daily timer, 7/4/6
              retention, --exclude-caches, repo auto-init. Asserts that
              repository + passwordFile are set when enabled. passwordFile
              is typed `str`, not `path`, so a flake-relative secret can't
              be copied into the world-readable store.

README option table and ROADMAP (services now "Seventeen shipped";
cloud-sync / networking / backup / devices areas updated) + the downstream
system.nix examples. Both branches eval-verified; the restic assertions
confirmed firing when repo/password are unset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:02:20 +01:00
Bernardo Magri
c45fb80b79 feat(services): full optional app menu + 7 more opt-in service toggles
Make the whole workstation menu available, all off/commented by default.

home.nix: the bare GUI apps become a categorized commented menu (~50
suggestions across browsers, comms, office, notes, code, graphics, A/V,
media, gaming, files/sync, security, system, local AI) — uncomment to add.

services.nix: the members that need system config (not just a package)
become opt-in nomarchy.services.* toggles, each with a commented example
in the downstream system.nix:
  - docker     → rootful Docker + user in docker group; asserts against the
                 podman docker-compat (both provide the `docker` command)
  - kdeconnect → programs.kdeconnect (opens phone-pairing ports)
  - gamemode   → Feral GameMode daemon
  - adb        → android-tools (programs.adb was removed; systemd ≥258 does
                 the device uaccess udev rules, so no group needed)
  - wireshark  → Qt GUI + user in wireshark group (capture without root)
  - ollama     → local LLM runtime on 127.0.0.1:11434
  - printing   → CUPS + Avahi/mDNS network printer discovery

README option table and ROADMAP (services item now "Fifteen shipped";
local-AI / containers / gaming / devices areas marked) updated. All seven
branches eval-verified with the toggles forced on; the docker/podman
assertion confirmed firing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:52:38 +01:00
Bernardo Magri
0c668c2eb1 feat(apps): starter workstation app suite + steam/libvirt/obs services
The "default application suite" roadmap item, but deliberately NOT a
nomarchy.apps.* option surface: a bare package install has no config to
put behind a toggle, so a thin `mkIf x { systemPackages = [p]; }` only
reinvents what a package list already gives. Instead the suite is a
curated home.packages list in templates/downstream/home.nix (extending
the existing `# firefox` convention) — active staples (libreoffice,
vscode, gimp, inkscape, mpv, amberol) plus commented suggestions. The
opt-out is deleting a line; it also keeps the apps out of the live ISO
automatically (the ISO never scaffolds from the template).

The members that DO need system config become nomarchy.services.* opt-in
toggles instead (config behind the switch, like the existing services):
  - steam   → programs.steam (32-bit stack, controller udev, RP ports)
  - libvirt → libvirtd + virt-manager, login user added to libvirtd group
  - obs     → obs-studio + a v4l2loopback virtual camera (exclusive_caps=1)
              selectable as a webcam in Zoom/Teams

README option table, ROADMAP (services item now "Eight shipped"; suite
item marked done with the no-surface rationale) and the downstream
system.nix examples updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 20:45:10 +01:00
Bernardo Magri
1f4bd3d5fa docs(readme): document the curated shell aliases in §5 Day-to-day
List the on-by-default zsh aliases (navigation, git, nix, misc) in the
Day-to-day section so they're discoverable from the README, not just by
running `alias`. Note the doc location in the roadmap item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:48:22 +01:00
Bernardo Magri
017bb1aa41 feat(shell): curated git/nix/navigation aliases on by default
Extend the zsh shellAliases in shell.nix with a curated, on-by-default
set: navigation (.., ..., ....), a git block (g, gst, ga/gaa, gc/gcm,
gco/gsw, gb, gd/gds, gl/glg, gp/gpl, gf), a nix block (ns, nr, nfu, nfc,
nsearch, ngc), and path/reload. Same restraint as rg/fd: short where it
helps but never shadowing a real binary — the git set keeps off gs
(ghostscript), and the system/home rebuilds keep their sys-update /
home-update names rather than cryptic aliases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:46:17 +01:00
Bernardo Magri
b856bb93e8 docs(roadmap): close the sanitize file-audit; menu grouping is ongoing
The repo-organization re-check found nothing to move: no orphaned modules
(keybinds.nix is a data import, not a HM module), no dead-code markers or
editor/backup cruft, every themes/<slug>.json pairs with its optional
overrides dir, and hosts/{default,live} are the reference + ISO hosts.
Recorded the one open decision: no formatter is declared and the .nix
hand-formatting is deliberate, so adopting nixfmt is a maintainer call,
left untouched. Also marked the menu category grouping a standing
convention — new menu-facing features get placed in the right submenu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:40:50 +01:00
Bernardo Magri
e0ee5b9e7e feat(menu): group the rofi picker into Tools / System submenus
The root picker had grown to 16 flat entries. Collapse it to six —
Apps · Theme · Tools › · System › · Power · Keybindings — with Tools
(Calculator, Clipboard, Emoji, Files, Web, Capture, Ask) and System
(Network, Bluetooth, DND, plus the self-gated Snapshots / Power-profile)
as submenus the dispatcher routes via `nomarchy-menu tools|system`. Each
submenu ends in a ← Back entry that re-opens the root. The direct
SUPER+CTRL+<mnemonic> binds still reach the leaves straight (bypassing
the menu), and the gated entries still hide when unavailable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:35:03 +01:00
Bernardo Magri
f68b245d92 feat(services): add podman, flatpak (+ Flathub) and pika backup
Extend nomarchy.services.* with three more opt-in toggles:
- podman: rootless containers, `docker` aliased to it, container DNS, and a
  subuid/subgid range for the login user.
- flatpak: services.flatpak + a oneshot that adds the Flathub remote
  system-wide (idempotent, retries when online).
- pika: ships pika-backup, the GUI for scheduled Borg backups.
Commented examples in the downstream system.nix template, per convention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:15:45 +01:00
Bernardo Magri
fb748c217c feat(services): opt-in Tailscale + Syncthing (nomarchy.services.*)
Start the opt-in services surface: nomarchy.services.{tailscale,syncthing},
off by default, in modules/nixos/services.nix. Tailscale ships the daemon
(authenticate with `sudo tailscale up`); Syncthing runs as the login user
with its GUI on 127.0.0.1:8384 (overrideDevices/Folders off so GUI-managed
folders survive rebuilds). username is read lazily, only when Syncthing is
on. Commented examples in the downstream system.nix template per the opt-in
convention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:08:41 +01:00
Bernardo Magri
8d3911de1a docs(template): ship opt-in features as commented examples downstream
Convention: an off-by-default feature that needs nomarchy.* settings in the
downstream files should appear commented-out in templates/downstream/{home,
system}.nix, so a new install enables it by uncommenting + tweaking rather
than hunting the docs for the option name. Apply it now: night light,
nomarchy.monitors, per-device keyboard layouts (home.nix) and power
management (system.nix). Also fix the stale keyboard.layout note
(console/LUKS follow xkb automatically now). Documented in README S6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:53:13 +01:00
Bernardo Magri
6ad80c045e feat(nightlight): scheduled blue-light filter via hyprsunset
Add nomarchy.nightlight (opt-in): a scheduled colour-temperature shift via
the HM services.hyprsunset module -- warm (.temperature, default 4000K) at
night, identity (no shift) by day, switching at .sunset / .sunrise. Two
time-based hyprsunset profiles, so hyprsunset handles the schedule and the
on-login state. New modules/home/nightlight.nix, imported in the home module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:37:03 +01:00
Bernardo Magri
54c6e100fb docs(roadmap): organize the menu picker into category submenus
The root picker has grown into a long flat list; group it into category
submenus (System / Tools / Look & Feel / Power) with the dispatcher routing
nomarchy-menu <category> to its leaf modules, keeping the SUPER+CTRL+<mnemonic>
binds as direct fast paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:29:30 +01:00
Bernardo Magri
eb76bd61b4 docs(roadmap): note btrfs-assistant segfault; snapper backend VM-verified
A VM regression test caught that btrfs-assistant 2.2 segfaults on launch
inside libbtrfsutil.so.1.4.0 -- a library ABI mismatch in nixpkgs 26.05, not
a GL/VM artifact (crashes with software GL and every Qt platform; the fault
is in a btrfs library). The snapshot menu wiring is correct and the app is
installed, but the GUI doesn't open. The snapshot backend is unaffected and
is itself VM-verified (full BTRFS+LUKS install: root+home snapper configs, a
/home timeline snapshot, /home/.snapshots a real subvolume, the oneshot
Result=success). Keep the entry, document the bug pending a nixpkgs fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:43:46 +01:00
Bernardo Magri
71f37bfc30 feat(keyboard): interactive new-keyboard layout prompt + persist
The runtime-remember complement to nomarchy.keyboard.devices. Set
nomarchy.keyboard.layouts (candidate layouts) and a nomarchy-keyboard-watch
daemon runs (exec-once): it polls hyprctl devices, and when a keyboard
connects after login that isn't declared and hasn't been chosen before, it
pops a rofi layout picker, applies the choice via hyprctl switchxkblayout (an
index into the candidate set carried by input.kb_layout), and remembers it
per-device in ~/.local/state -- re-applied silently on later reconnects. The
boot-time set (incl. the built-in keyboard) is never prompted.

Pure bash + jq/hyprctl/rofi (all in the session PATH); state lives outside
the flake (a stateful runtime piece by design). Eval + bash-syntax verified;
the hotplug behaviour needs an on-hardware test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:43:59 +01:00
Bernardo Magri
69f959365e docs(roadmap): interactive new-keyboard layout prompt + persist
A runtime-remember complement to the declarative nomarchy.keyboard.devices:
a daemon watches Hyprland's socket, pops a rofi layout picker when an unknown
keyboard connects, applies it, and persists per-device so it re-applies on
reconnect — optionally writing the choice back into nomarchy.keyboard.devices.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:32:05 +01:00
Bernardo Magri
aef45b3c8d feat(keyboard): per-device layout for external keyboards
Add nomarchy.keyboard.devices ({ "<hyprctl-device-name>" = { layout; variant; }; })
generating Hyprland `device` blocks that override the session
nomarchy.keyboard.layout for a named keyboard -- e.g. an external board
that's physically a different layout than the laptop's built-in one.
Hyprland applies it whenever that device connects, so the external keyboard
"remembers" its layout the declarative way. A Waybar hyprland/language
indicator shows the active layout, placed only when more than one layout is
in play (a comma in the session layout, or any per-device override) so
single-layout bars stay clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:27:28 +01:00
Bernardo Magri
0897391cfb feat(snapshots): desktop browse/restore + snapshot /home by default
Surface snapper snapshots from the desktop: `nomarchy-menu snapshot` (in the
SUPER+M picker, self-gated) launches btrfs-assistant, the polkit-aware
browse/diff/restore/rollback UI -- safer than a fat-fingerable rofi rollback
for a destructive root-only op. Shipped system-side gated on
nomarchy.system.snapper.

Also snapshot /home by default (hourly 5 / daily 7 / weekly 4) when it's its
own BTRFS subvolume -- the installer only set up snapper for /. A boot
oneshot creates the required /home/.snapshots subvolume if missing, so
already-installed machines get it too (no disko change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:14:05 +01:00
Bernardo Magri
5c83b52727 docs: mark the GTK/Qt color-scheme portal confirmed in a live session
Verified this session in headless QEMU: the org.freedesktop.appearance
color-scheme portal reads 1 on a dark theme and 2 after switching to a light
one, so the dconf value reaches GTK4/Qt apps. Removes the stale "needs a
live session to confirm" note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:47:59 +01:00
Bernardo Magri
0751102915 fix(waybar): show the power-profile indicator in the summer themes
custom/powerprofile only appeared in the generated bar -- its exec was a
writeShellScript store path, which can't go in the summer themes' static
whole-swap waybar.jsonc, so summer-day/night were missing it (the DND bell
already had parity). Promote powerProfileStatus/Cycle to named
writeShellScriptBins on PATH (waybar.nix home.packages) and reference them
by bare name, then add custom/powerprofile to both summer waybar.jsonc +
their waybar.css. Same pattern as the swaync bell.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:46:45 +01:00
Bernardo Magri
5c59da73e7 docs(roadmap): full hardware enablement beyond nixos-hardware
nixos-hardware covers basic per-model enablement, but advanced features
(keyed on CPU/GPU generation) are left to the user. Capture a big item:
extend the installer's autodetection + a nomarchy.hardware.* surface to turn
on safe features automatically (opt-out) and gate the heavy/experimental
ones (opt-in), without per-feature fiddling. Worked example on the ThinkPad
T14s (Ryzen 7 PRO 7840U + Radeon 780M): ROCm (gfx1103 override), Ryzen AI
NPU/XDNA, AMD P-State EPP, VA-API, fprintd biometrics (fwupd already ships),
SSD fstrim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:35:07 +01:00
Bernardo Magri
42801548b7 feat(displays): declarative nomarchy.monitors + nwg-displays arranger
Add a per-output monitor schema (name / resolution / position / scale /
transform / mirror / bitdepth / vrr / disable) that generates Hyprland
`monitor` rules, keeping the ,preferred,auto,1 wildcard as the fallback.
Hyprland applies the rules on hotplug, so a declared external/dock output
arranges itself on connect (no kanshi -- it fights Hyprland's own output
management). Default [] -> wildcard only, so no change for existing setups;
set wayland.windowManager.hyprland.settings.monitor directly to override.

nwg-displays ships behind nomarchy.displays.enable (default true) as an
interactive arranger -- a helper to discover values; the declarative config
stays the source of truth (its output file isn't sourced).

Remaining (roadmap): docked/undocked profile switching, workspace-to-monitor
binding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:22:18 +01:00
Bernardo Magri
1b0eeeaf6c fix(idle): don't lock before an encrypted hibernate (was: unlock after)
The previous nomarchy-hibernate-unlock ran `pkill hyprlock` after a
hibernate resume, but killing a Wayland session-lock client without
releasing the lock trips hyprlock's "go to a tty" crash failsafe -- the
compositor keeps the screen locked for safety. So resume showed a hyprlock
error screen instead of unlocking.

Fix it the right way: never engage the lock before an encrypted hibernate
(the LUKS passphrase at resume is the gate). Replace the post-resume unlock
with a nomarchy-lock-before-sleep unit that takes over hypridle's
before_sleep_cmd: it locks on the RAM-resume sleeps (suspend / hybrid-sleep
/ suspend-then-hibernate) always, and on hibernate.target only when the disk
is unencrypted. idle.nix drops before_sleep_cmd accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:15:31 +01:00
a606c0d498 docs: log power-profile Waybar parity as a follow-up
The custom/powerprofile indicator is missing from the whole-swap summer
themes (its exec uses Nix-built script paths that can't go in static
JSON). Note the fix (promote the scripts to named bins on PATH) and the
general "new module → also add to whole-swap themes" rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 21:12:38 +01:00
cd495d1720 fix(dnd): add the notification bell to the whole-swap summer themes
The DND bell only landed in the generated Waybar config; summer-day and
summer-night ship waybar.jsonc whole-swaps that replace it entirely, so
they'd have shown no bell. Add custom/notification (swaync-client -swb,
left-click panel / right-click DND) to both legacy layouts with their
FontAwesome bell / bell-slash glyphs, and style it alongside the other
right-side modules in each waybar.css.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 21:08:01 +01:00
dbcb8efd1e feat(dnd): swaync Do-Not-Disturb toggle + Waybar bell indicator
Adds a Do-Not-Disturb story on top of swaync:

- Waybar `custom/notification` module via `swaync-client -swb` — streams
  the notification count + DND state (no polling). Left-click toggles the
  panel, right-click toggles DND; bell-off glyph + muted color when DND is
  on, accent when notifications are waiting.
- `nomarchy-menu dnd` toggle in the root picker and bound to SUPER+CTRL+D
  (so it shows in the SUPER+? cheatsheet). Turning DND off confirms with a
  toast; turning it on is silent — notifications are suppressed and the
  bell-off icon is the cue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 21:01:11 +01:00
07e467027b feat(nix): silence the "Git tree is dirty" warning on rebuilds
The downstream flake at ~/.nomarchy is a live working tree by design —
nomarchy-theme-sync rewrites theme-state.json on every switch — so Nix's
dirty-tree warning fired on every sys-update/home-update and was just
noise. Default nix.settings.warn-dirty to false (mkDefault, overridable).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:53:18 +01:00
9a5473e179 docs: record the branch/release model + sanitize progress
Document the main=dev / v1=release-pointer model in flake.nix (where the
v1 ref is defined) and mark the repo-sanitize roadmap items shipped
(installer prune, option-table reconcile, settings untrack, branch
realignment).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:48:06 +01:00
db6a0ba455 chore: untrack .claude/settings.local.json (machine-local)
It holds this machine's Claude Code permission allowlist — local by
convention (the `.local` suffix; settings.json is the shared one), so it
shouldn't be versioned. Remove from tracking (keeping the working file)
and gitignore it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:45:54 +01:00
8c47716f9a chore(install): stop writing distro-default config into system.nix
console.useXkbConfig and boot.initrd.systemd.enable are set distro-wide
(lib.mkDefault true in modules/nixos/default.nix, since the LUKS-keymap
fix), so the installer rewriting them in the generated system.nix was
redundant. Drop them there and from the offline-pin test fixture in
flake.nix — the nomarchy module still defaults them on, so the closure
(and the pin set) is unchanged; verified the values still resolve true
and `nix flake check` passes. Only the machine-specific xkb layout is
written now. Adjusts the README keyboard-option wording to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:42:36 +01:00
26 changed files with 1398 additions and 460 deletions

View File

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

3
.gitignore vendored
View File

@@ -8,3 +8,6 @@ result-*
__pycache__/
.DS_Store
# Claude Code machine-local settings (permissions etc.)
.claude/settings.local.json

View File

@@ -186,8 +186,11 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|---|---|---|
| `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.layout` | `"us"` | XKB layout for the Hyprland session (installer writes the matching `services.xserver.xkb` in system.nix; the console + LUKS prompt follow via the distro default) |
| `nomarchy.keyboard.variant` | `""` | XKB variant for the session |
| `nomarchy.keyboard.devices` | `{}` | Per-device layout overrides (Hyprland `device` blocks keyed by `hyprctl devices` name) — e.g. an external keyboard with its own layout/variant |
| `nomarchy.keyboard.layouts` | `[]` | Extra candidate layouts; when set, a watcher prompts (rofi) for a layout on a newly-connected keyboard and remembers it per-device |
| `nomarchy.nightlight.enable` | `false` | Scheduled blue-light filter (hyprsunset) — warm at night (`.temperature`, default 4000K) between `.sunset`/`.sunrise`, no shift by day |
| `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 |
@@ -201,6 +204,8 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
| `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.displays.enable` | `true` | nwg-displays interactive monitor arranger (helper for `nomarchy.monitors`) |
| `nomarchy.monitors` | `[]` | Declarative per-output layout → Hyprland `monitor` rules (applied on hotplug); `,preferred,auto,1` wildcard kept as fallback |
| `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) |
@@ -214,6 +219,23 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
| `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` |
| `nomarchy.services.tailscale.enable` | `false` | Opt-in: Tailscale mesh VPN (then `sudo tailscale up`) |
| `nomarchy.services.syncthing.enable` | `false` | Opt-in: Syncthing file sync as the login user (GUI at `127.0.0.1:8384`) |
| `nomarchy.services.podman.enable` | `false` | Opt-in: rootless Podman (`docker` aliased to it) |
| `nomarchy.services.flatpak.enable` | `false` | Opt-in: Flatpak + the Flathub remote |
| `nomarchy.services.pika.enable` | `false` | Opt-in: Pika Backup (GUI Borg backups) |
| `nomarchy.services.steam.enable` | `false` | Opt-in: Steam via `programs.steam` (32-bit libs, controller udev, Remote-Play ports) |
| `nomarchy.services.libvirt.enable` | `false` | Opt-in: libvirt/KVM + virt-manager GUI (login user added to `libvirtd`) |
| `nomarchy.services.obs.enable` | `false` | Opt-in: OBS Studio + a v4l2loopback virtual camera (selectable as a webcam in Zoom/Teams) |
| `nomarchy.services.docker.enable` | `false` | Opt-in: rootful Docker (user in `docker` group); don't enable alongside podman |
| `nomarchy.services.kdeconnect.enable` | `false` | Opt-in: KDE Connect phone integration (opens its firewall ports) |
| `nomarchy.services.gamemode.enable` | `false` | Opt-in: Feral GameMode performance daemon (`gamemoderun`) |
| `nomarchy.services.adb.enable` | `false` | Opt-in: Android adb/fastboot tools (systemd handles device udev rules) |
| `nomarchy.services.wireshark.enable` | `false` | Opt-in: Wireshark Qt GUI (user in `wireshark` group, capture without root) |
| `nomarchy.services.ollama.enable` | `false` | Opt-in: Ollama local LLM runtime on `127.0.0.1:11434` (CPU; GPU via `services.ollama.acceleration`) |
| `nomarchy.services.printing.enable` | `false` | Opt-in: CUPS + Avahi/mDNS network printer discovery |
| `nomarchy.services.openrgb.enable` | `false` | Opt-in: OpenRGB daemon for RGB peripheral/motherboard lighting |
| `nomarchy.services.restic.enable` | `false` | Opt-in: scheduled daily restic backup (set `.repository` + `.passwordFile`; 7/4/6 retention; list/restore via the `restic-nomarchy` wrapper) |
Beyond the `nomarchy.*` surface, the system layer turns on the usual
desktop services with `lib.mkDefault` (override natively). One worth
@@ -286,6 +308,32 @@ picker · `SUPER+SHIFT+T` next wallpaper · `SUPER+X` power menu ·
clipboard history · `SUPER+Q`
close · `SUPER+1..9` workspaces · `Print` region screenshot.
Shell aliases (zsh, gated on `nomarchy.shell.enable`) — `alias` lists them
all; the curated set:
```sh
# navigation
.. ... .... # cd up 1 / 2 / 3 levels
# git
g gst # git · git status -sb
ga gaa # git add · git add -A
gc gcm # git commit · git commit -m
gco gsw gb # checkout · switch · branch
gd gds # diff · diff --staged
gl glg # log graph (last 20 · all branches)
gp gpl gf # push · pull · fetch --all --prune
# nix (system/home rebuilds keep their full sys-update / home-update names)
ns nr # nix shell · nix run (e.g. ns nixpkgs#ripgrep)
nfu nfc # nix flake update · check
nsearch ngc # nix search nixpkgs · nix-collect-garbage -d
# misc
path # print $PATH, one entry per line
reload # exec zsh (reload the shell)
```
## 6. Extending
- **New theme:** drop a JSON into `themes/` (schema = any existing preset),
@@ -294,6 +342,18 @@ close · `SUPER+1..9` workspaces · `Print` region screenshot.
the Nix modules. One place — there is no second renderer to keep in sync.
- **Importing more old-distro palettes:**
`tools/import-palettes.py <palettes-dir> themes/`.
- **New opt-in feature (convention):** when a feature is off by default and
needs the user to set `nomarchy.*` options (e.g. night light, per-device
keyboard layouts, monitor layout, power management), ship a **commented**
example of it in `templates/downstream/home.nix` or `system.nix`. New
installs then enable it by uncommenting + tweaking, rather than hunting the
docs for the option name.
- **Applications:** the starter complete-workstation set (LibreOffice, VS
Code, GIMP, Inkscape) lives in the `home.packages` list in your
`home.nix` — it's a plain package list, so curate it directly: delete a
line to slim the machine, uncomment an extra (a browser, email, full TeX
Live), or add your own. No `nomarchy.apps.*` toggles — a package list is
already its own opt-out, so the distro doesn't impose these or wrap them.
## Roadmap & known issues

View File

@@ -37,6 +37,20 @@ how to override it. Items marked ✓ are shipped.
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)
-**Organize the picker into category submenus:** the root picker had grown
into a long flat list of 16 entries. It's now six: **Apps · Theme · Tools
· System · Power · Keybindings**. `Tools` (Calculator, Clipboard, Emoji,
Files, Web, Capture, Ask) and `System` (Network, Bluetooth, DND, and the
self-gated Snapshots / Power-profile) are submenus the dispatcher routes via
`nomarchy-menu tools|system`, each ending in a `← Back` entry that re-opens
the root. The direct `SUPER+CTRL+<mnemonic>` binds still hit the leaves
straight (bypassing the menu), and the self-gated entries still hide when
unavailable. Remaining (optional): a **Look & Feel** category once there are
more appearance toggles to group with Theme (night-light, wallpaper).
**General pattern (ongoing):** the grouping is a standing convention, not a
one-off — any new feature that earns a menu entry must be placed in the
right submenu (don't let the root creep back to a flat list), with its
direct `SUPER+CTRL+<mnemonic>` bind and self-gating as applicable.
-**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-
@@ -99,10 +113,15 @@ how to override it. Items marked ✓ are shipped.
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.
- **Quality-of-life command aliases:** a curated set ships on by default in
`modules/home/shell.nix` — navigation (`..`/`...`/`....`), a git block
(`g`, `gst`, `ga`/`gaa`, `gc`/`gcm`, `gco`/`gsw`, `gb`, `gd`/`gds`,
`gl`/`glg`, `gp`/`gpl`, `gf`), and a nix block (`ns`, `nr`, `nfu`, `nfc`,
`nsearch`, `ngc`), plus `path` and `reload`. Scope decision: short where it
helps but **never shadow a real binary** (same rule as rg/fd — the git block
deliberately avoids `gs`/ghostscript); system/home rebuilds keep their full
`sys-update`/`home-update` names rather than getting cryptic aliases. The set
is documented in README §5 (Day-to-day), and `alias` lists them in-shell.
- **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
@@ -119,11 +138,21 @@ how to override it. Items marked ✓ are shipped.
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).
- **Default application suite:** a starter complete-workstation set
`libreoffice-fresh`, `vscode`, `gimp`, `inkscape` — ships *active* in the
downstream `templates/downstream/home.nix` `home.packages`, with the heavier
opt-ins (`texliveFull` — multi-GB; `texliveMedium` is lighter — plus
browser/email) commented just below. **Decision: no `nomarchy.apps.*` option
surface.** Unlike `nomarchy.services.*` (real config behind each toggle —
systemd units, subuid ranges, the Flathub oneshot), a bare package install
has nothing behind the toggle: `mkIf x { environment.systemPackages = [p]; }`
just reinholds what a package list already gives, and the honest opt-out for
a package is *deleting the line*. So the suite is the user's own curated list
(extending the existing `# firefox` convention), not a distro-module feature —
which also keeps the apps out of the live ISO automatically (it never starts
from the template) and respects that editor/office/graphics are personal-taste
choices, not defaults to impose. Unfree `vscode` evaluates fine — `mkFlake`
and the system both set `allowUnfree = true`.
-**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):
@@ -131,13 +160,15 @@ how to override it. Items marked ✓ are shipped.
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.
passphrase already gates the machine, but locking hyprlock before sleep
meant a second password. Fixed by *not locking* before an encrypted
hibernate: a `nomarchy-lock-before-sleep` systemd unit
(`modules/nixos/default.nix`) takes over from hypridle's `before_sleep_cmd`
and locks on the RAM-resume sleeps (suspend / hybrid-sleep / suspend-then-
hibernate) always, but skips `hibernate.target` when the disk is LUKS-
encrypted. (A first attempt that dismissed hyprlock *after* resume was
wrong — killing a Wayland session-lock client trips its "go to a tty"
crash failsafe instead of unlocking, which is the error screen it caused.)
-**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,
@@ -148,14 +179,31 @@ how to override it. Items marked ✓ are shipped.
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.
and clarity.
- ✓ pruned now-redundant config: the installer no longer writes
`console.useXkbConfig` / `boot.initrd.systemd.enable` into `system.nix`
(distro-wide defaults since the LUKS-keymap fix), nor the offline-pin
test fixture in `flake.nix`.
- ✓ reconciled the README option tables with the live `nomarchy.*`
surface (added keys/fastfetch/snapper/greeter.autoLogin).
- ✓ untracked `.claude/settings.local.json` (machine-local) + gitignored.
-**branch/release model:** `main` is the development default; `v1` is
the release pointer downstreams pin (`?ref=v1`), advanced to main once
a batch is tested — rolling, no tags. Brought `main` current (it was a
stale legacy-merge), and deleted the dead `refactor` / `wave/nixos-26.05`
branches (kept `legacy` for history). See the comment in `flake.nix`.
- ✓ re-checked that every file earns its place under the
`modules`/`hosts`/`themes`/`pkgs`/`tools` rule of thumb: no orphaned
modules (`keybinds.nix` is a *data* module `import`-ed by hyprland/rofi,
so correctly absent from `default.nix`), no dead-code markers, no
editor/backup cruft, each `themes/<slug>.json` palette pairs with its
optional `<slug>/` overrides dir, and `hosts/{default,live}` are the
documented reference + ISO hosts. Structure is clean as-is.
- Open decision (deferred): the repo declares **no formatter** (no flake
`formatter`, no `.editorconfig`); `.nix` files use deliberate aligned
hand-formatting, so `nixfmt-rfc-style --check` flags ~33 files. Adopting a
formatter would be a one-time repo-wide reformat that flattens that
alignment — a maintainer call, not a silent cleanup, so left untouched.
- **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
@@ -195,47 +243,178 @@ how to override it. Items marked ✓ are shipped.
idle behaviour.
- Remaining: a boot-only→event-driven charge-limit re-apply (udev) if a
firmware resets the threshold on unplug.
-**Waybar parity:** the `custom/powerprofile` indicator now shows in the
summer-day/night whole-swap themes too. `powerProfileStatus`/`Cycle` are
named `writeShellScriptBin`s on PATH (`waybar.nix` home.packages), so the
generated config and the static `waybar.jsonc` both exec them by bare
name (the same parity the DND bell has). General pattern: any new
generated-config module needs adding to the whole-swap themes too.
- **Full hardware enablement (beyond nixos-hardware):** nixos-hardware
profiles cover *basic* per-model functionality (quirks, drivers), but the
*advanced* features of a machine — keyed more on the CPU/GPU generation
than the model — are left for the user to wire up by hand. Close that gap
without making it finnicky: extend the installer's existing autodetection
(DMI → nixos-hardware profile today; add CPU vendor/arch, GPU, NPU,
fingerprint reader, SSD) to switch on the *safe, broadly-beneficial*
features automatically, and expose a small `nomarchy.hardware.*` surface
(per-vendor where it helps, e.g. `nomarchy.hardware.amd.*`) for the heavy
or experimental bits. Goal: full utilisation out of the box — opt-*out* for
the safe defaults, opt-*in* for the multi-GB / experimental extras.
Complements nixos-hardware (model quirks) rather than replacing it; keep
the detection in the installer's `hardware-db` so an installed machine
bakes the right defaults, all overridable through `nomarchy.hardware.*`.
Worked example — ThinkPad T14s (Ryzen 7 PRO 7840U + Radeon 780M):
- **GPU compute — ROCm:** the 780M is an RDNA3 iGPU (gfx1103) ROCm doesn't
officially list, so it needs `HSA_OVERRIDE_GFX_VERSION=11.0.0`. Multi-GB
closure → opt-in (`nomarchy.hardware.amd.rocm.enable`); unlocks GPU
PyTorch / Ollama.
- **Ryzen AI NPU (XDNA):** the `amdxdna` kernel driver (mainline 6.14+) +
the XRT/xdna userspace runtime + firmware. Niche/experimental → opt-in.
- **AMD P-State EPP (Zen 4 power):** `amd_pstate=active` for the EPP
governor, which power-profiles-daemon (already shipped, `power.nix`)
drives per profile. Broadly beneficial on Zen 2+ → default on when the
installer sees an AMD CPU.
- **Hardware video acceleration (VA-API):** mesa's radeonsi exposes VA-API
on AMD (`LIBVA_DRIVER_NAME=radeonsi`, libva + drivers in
`hardware.graphics.extraPackages`); Intel uses `intel-media-driver`.
Broadly beneficial → default on, vendor-detected.
- **LVFS firmware & biometrics:** fwupd already ships (✓, `services.fwupd`).
Add fingerprint: `services.fprintd` when a reader is detected, with
opt-in PAM integration for login/sudo (password-only stays the default
for the cautious).
- **SSD periodic TRIM:** `services.fstrim.enable` (weekly) — safe and
beneficial, so default on (trivial enough it may warrant being
distro-wide regardless).
Sub-items here can graduate into their own roadmap entries as they're
scoped; the unifying work is the detection + `nomarchy.hardware.*` surface.
- **Opt-in services & integrations:** the counterpart to the opt-*out*
application suite above — heavier or more personal integrations shipped
**off by default**, each a `nomarchy.services.<name>.enable` toggle a
downstream flips on in one line. Keeps the base lean while making common
additions trivial. Candidates by area:
- **cloud/sync:** Nextcloud client; Syncthing (`services.syncthing`)
- **local AI:** LM Studio (`lmstudio`); Ollama (`services.ollama`, optional
GPU accel) — pairs with the menu's Ask-Claude philosophy
- **containers/VMs:** Docker/Podman; libvirt + virt-manager
- **networking:** Tailscale (`services.tailscale`); WireGuard
- **gaming/media:** Steam (`programs.steam`); OBS Studio
- **devices:** printing (CUPS + Avahi); KDE Connect / phone integration;
OpenRGB
- **backup:** restic/borg (`services.restic`)
- **escape hatch:** Flatpak (`services.flatpak`) for apps outside nixpkgs
additions trivial. **✓ Seventeen shipped** in `modules/nixos/services.nix`
(system-side), each with a commented example in the downstream `system.nix`
template: `tailscale`, `syncthing` (runs as the login user, GUI on
127.0.0.1:8384), `podman` (rootless, `docker` aliased, user subuid/subgid),
`flatpak` (+ Flathub remote added by a oneshot), `pika` (Pika Backup GUI),
`steam` (`programs.steam` — the 32-bit stack, controller udev, Remote-Play
ports a bare package can't), `libvirt` (libvirtd + virt-manager, user added
to the `libvirtd` group), `obs` (OBS Studio + a v4l2loopback virtual camera
usable as a webcam in Zoom/Teams), `docker` (rootful; asserts against the
podman docker-compat), `kdeconnect`, `gamemode`, `adb` (android-tools;
systemd handles the udev rules), `wireshark` (Qt GUI + the `wireshark`
group), `ollama` (local LLM API on 127.0.0.1:11434), `printing` (CUPS +
Avahi mDNS), `openrgb` (RGB lighting daemon), and `restic` (scheduled
daily backup — a small option surface: `repository`/`passwordFile`/`paths`,
7/4/6 retention). More candidates by area:
-**cloud/sync:** Syncthing (`services.syncthing`); Nextcloud client is a
bare tray app (in the app-suite menu, not a service)
-**local AI:** Ollama (`nomarchy.services.ollama`, optional GPU accel)
ships; LM Studio is a (commented) app-suite package (a bare GUI, no
service) — pairs with the menu's Ask-Claude philosophy
-**containers/VMs:** Podman (`virtualisation.podman`); libvirt +
virt-manager (`nomarchy.services.libvirt`); Docker (`nomarchy.services.docker`)
-**networking:** Tailscale (`services.tailscale`); WireGuard needs no
toggle — NetworkManager (on by default) imports `.conf` tunnels natively;
the `wireguard-tools` CLI is in the app-suite menu
-**gaming/media:** Steam (`nomarchy.services.steam`); OBS Studio
(`nomarchy.services.obs`, with the v4l2loopback virtual camera); GameMode
(`nomarchy.services.gamemode`)
-**devices:** printing (CUPS + Avahi, `nomarchy.services.printing`); KDE
Connect (`nomarchy.services.kdeconnect`); OpenRGB
(`nomarchy.services.openrgb`). Plus dev tooling — Android `adb` and
`wireshark` toggles
-**backup:** Pika Backup (`pika-backup`, GUI over Borg); scheduled
headless restic (`nomarchy.services.restic`)
-**escape hatch:** Flatpak (`services.flatpak`) + Flathub, 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
Surface decided: `nomarchy.services.*`, system-side for system services
(home-side ones can extend the same prefix later). Remaining: curate the
rest (Ollama, containers, Steam/OBS, printing, backups, Flatpak …). The
opt-out application *suite* is deliberately **not** a parallel
`nomarchy.apps.*` surface — it's a curated `home.packages` list in the
downstream template (see "Default application suite" above), since a bare
package install has no config to put behind a toggle. 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.
- **Display / monitor management:** declarative `nomarchy.monitors` (a
per-output schema — name/resolution/position/scale/transform/mirror/
bitdepth/vrr/disable) generates Hyprland `monitor` rules, keeping the
`,preferred,auto,1` wildcard as the fallback; Hyprland applies them on
hotplug, so a declared external/dock output arranges itself on connect
(no kanshi — it fights Hyprland's own output management). `nwg-displays`
ships behind `nomarchy.displays.enable` as an interactive arranger (a
helper to find values; the declarative config stays the source of truth
its output file isn't sourced). Remaining: true docked/undocked **profile
switching** of the *same* outputs (Hyprland's per-output rules cover the
common "external connects → arrange" case, not multi-layout toggles), and
optionally workspace-to-monitor binding.
-**Night light / blue-light filter:** `nomarchy.nightlight` (opt-in) —
a scheduled colour-temperature shift via `hyprsunset` (Hyprland-native),
warm at night, identity (no shift) by day. `modules/home/nightlight.nix`
drives the HM `services.hyprsunset` with two time-based profiles
(`sunrise` → identity, `sunset``temperature`), so hyprsunset handles the
schedule and the on-login state. Needs an on-hardware check that hyprsunset
applies the active profile at session start. Remaining (optional): a menu +
Waybar toggle to force it on/off, and geo (lat/long) auto sunset/sunrise
(would mean wlsunset, which schedules by location).
- **Keyboard layouts (per-device + switching):**
-**Per-device declarative layout:** `nomarchy.keyboard.devices`
(`{ "<hyprctl-device-name>" = { layout; variant; }; }`) generates Hyprland
`device` blocks that override the session `nomarchy.keyboard.layout` for a
named keyboard — e.g. an external board that's physically a different
layout than the laptop's. Hyprland applies it whenever that device
connects (the "external keyboard remembers its layout" case, done the
declarative way). A Waybar `hyprland/language` indicator shows the active
layout, gated on more than one layout being in play (comma in the session
layout, or any per-device override) so single-layout bars stay clean.
-**Interactive new-keyboard prompt (runtime + persist):** set
`nomarchy.keyboard.layouts` (candidate layouts) and a `nomarchy-keyboard-
watch` daemon runs (exec-once): it polls `hyprctl devices`, and when a
keyboard connects *after* login that isn't in `keyboard.devices` and
hasn't been chosen before, it pops a rofi layout picker, applies the
choice with `hyprctl switchxkblayout` (an index into the candidate set,
which `input.kb_layout` carries), and remembers it per-device in
`~/.local/state/nomarchy/keyboard-layouts` — re-applied silently on later
reconnects. The boot-time set (incl. the built-in keyboard) is never
prompted. The runtime-remember complement to the declarative
`keyboard.devices`; a stateful runtime piece by design. **Pending an
on-hardware test** (hotplug isn't verifiable in CI). Remaining (bonus):
offer to write a choice into `keyboard.devices` so it graduates to the
reproducible config.
- Remaining: a multi-layout cycle bind (`hyprctl switchxkblayout` / xkb
`grp:` options) for switching layouts on one keyboard; add the
`hyprland/language` module to the summer whole-swap themes for parity
(the gating doesn't translate to their static JSON). Keep the system
(console/initrd) and session layouts in sync (the LUKS-keymap work).
-**Do-Not-Disturb:** swaync DND toggle wired into the menu
(`nomarchy-menu dnd`, in the picker, SUPER+CTRL+D) and a Waybar bell
indicator (`custom/notification` via `swaync-client -swb`: shows the
notification count + DND state, left-click toggles the panel,
right-click toggles DND). Turning DND off confirms with a toast; turning
it on is silent (notifications are suppressed — the bell-off glyph is the
cue).
-**Snapshot browse/restore UX:** `nomarchy-menu snapshot` (in the SUPER+M
picker, self-gated on snapper) launches **btrfs-assistant** — the
desktop browse/diff/restore/rollback UI, elevating via polkit (the safe
choice for a destructive, root-only op vs a fat-fingerable rofi rollback).
Shipped system-side gated on `nomarchy.system.snapper`. Also extended
snapper to snapshot **/home** by default (hourly 5 / daily 7 / weekly 4)
when it's its own BTRFS subvolume; a boot oneshot creates the required
`/home/.snapshots` subvolume if missing (covers existing installs, no disko
change). **The backend is VM-verified** (a full BTRFS+LUKS install: both
`root`+`home` configs, a `/home` timeline snapshot taken, `/home/.snapshots`
a real subvolume, the oneshot `Result=success`).
-**Known bug — btrfs-assistant 2.2 segfaults on launch** in nixpkgs
26.05: it crashes inside `libbtrfsutil.so.1.4.0` (a library ABI mismatch,
confirmed in the VM regardless of GL/Qt platform — so it's not a VM/GL
artifact and will crash on hardware too). The menu wiring is correct and
the app is installed, but the GUI doesn't open. The snapshot *backend*
(the actual snapshots) is unaffected. Fix path: a nixpkgs override aligning
its libbtrfsutil, or wait for an upstream bump; meanwhile the menu entry
is a no-op when the binary crashes. Fallback if it lingers: a rofi-based
snapshot menu (no btrfs-assistant dependency).
- Remaining (optional): a keyboard-driven rofi browse for quick glances;
boot-from-snapshot needs a systemd-boot equivalent of grub-btrfs.
- **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,
@@ -287,5 +466,8 @@ how to override it. Items marked ✓ are shipped.
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.
system-side), so libadwaita/Qt follow the palette. Confirmed in a live
session (headless QEMU): the portal's `org.freedesktop.appearance`
`color-scheme` reads `1` on a dark theme and flips to `2` after switching
to a light one (catppuccin-latte) — so xdg-desktop-portal-gtk does relay
the dconf value GTK4/Qt apps read.

View File

@@ -51,8 +51,11 @@
(nixpkgs.lib.concatStringsSep "\n"
(builtins.attrNames nixos-hardware.nixosModules));
nixpkgsPath = nixpkgs.outPath;
# v1 is the release branch: installed machines track it on
# `nix flake update` instead of the forge's default branch.
# Branch model: `main` is the development default; `v1` is the
# release pointer — the stable line for the NixOS 26.05 rewrite,
# advanced to main once a batch is tested (rolling, no tags). A
# future major bump would become `v2`. Installed machines track
# `?ref=v1` on `nix flake update`, not the forge's default branch.
flakeUrl = "git+${gitUrl}?ref=v1";
# Future updates re-resolve from the forge (original); the install
# itself resolves from the ISO store (locked = path) — fully
@@ -252,12 +255,6 @@
({ 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 —

View File

@@ -17,6 +17,7 @@
./idle.nix # hyprlock + hypridle, themed from the same JSON
./yazi.nix # flagship TUI file manager, themed + plugins
./osd.nix # swayosd volume/brightness OSD, themed
./nightlight.nix # scheduled blue-light filter (hyprsunset), opt-in
./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

View File

@@ -41,6 +41,77 @@ let
# (rofi.nix renders the same list). Edit them in ./keybinds.nix.
keybinds = import ./keybinds.nix;
mkBind = b: "${b.mods}, ${b.key}, ${b.action}";
# A nomarchy.monitors entry -> a Hyprland `monitor` rule. Unset optional
# fields are omitted; `resolution = "disable"` collapses to the short form.
monitorRule = m:
if m.resolution == "disable" then "${m.name}, disable"
else lib.concatStringsSep ", " (
[ m.name m.resolution (toString m.position) (toString m.scale) ]
++ lib.optionals (m.transform != null) [ "transform" (toString m.transform) ]
++ lib.optionals (m.mirror != null) [ "mirror" m.mirror ]
++ lib.optionals (m.bitdepth != null) [ "bitdepth" (toString m.bitdepth) ]
++ lib.optionals (m.vrr != null) [ "vrr" (toString m.vrr) ]
++ lib.optional (m.extra != "") m.extra
);
# Keyboard layout candidates: the primary nomarchy.keyboard.layout (split on
# commas) plus any nomarchy.keyboard.layouts. Drives input.kb_layout so all
# of them are switchable, and feeds the watcher's picker.
kbLayouts = lib.unique (
(lib.splitString "," config.nomarchy.keyboard.layout)
++ config.nomarchy.keyboard.layouts
);
kbAutoSwitch = config.nomarchy.keyboard.layouts != [ ];
# On a keyboard that connects after login (not declared in
# keyboard.devices, not already remembered), ask for a layout and persist it
# per-device, re-applying silently on later reconnects. Stateful runtime
# piece — the complement to the declarative keyboard.devices. Reliable
# primitives only: poll hyprctl devices, apply via switchxkblayout (an index
# into kbLayouts). NOTE: needs an on-hardware test (hotplug isn't verifiable
# in CI).
keyboardWatch = pkgs.writeShellScriptBin "nomarchy-keyboard-watch" ''
set -u
layouts="${lib.concatStringsSep " " kbLayouts}"
declared="${lib.concatStringsSep " " (builtins.attrNames config.nomarchy.keyboard.devices)}"
state="''${XDG_STATE_HOME:-$HOME/.local/state}/nomarchy/keyboard-layouts"
mkdir -p "$(dirname "$state")"; [ -f "$state" ] || : > "$state"
layout_index() { i=0; for l in $layouts; do [ "$l" = "$1" ] && { printf %s "$i"; return; }; i=$((i + 1)); done; printf %s -1; }
apply() { idx=$(layout_index "$2"); [ "$idx" -ge 0 ] && hyprctl switchxkblayout "$1" "$idx" >/dev/null 2>&1; }
keyboards() { hyprctl devices -j | jq -r '.keyboards[].name'; }
is_declared() { case " $declared " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
saved_for() { while IFS='=' read -r k v; do [ "$k" = "$1" ] && { printf '%s' "$v"; return; }; done < "$state"; }
# Startup: re-apply remembered layouts, never prompt so the built-in
# keyboard just stays on the session default.
prev=" "
for kb in $(keyboards); do
prev="$prev$kb "
is_declared "$kb" && continue
s=$(saved_for "$kb"); [ -n "$s" ] && apply "$kb" "$s"
done
# Watch for keyboards that connect later.
while :; do
sleep 3
cur=" "
for kb in $(keyboards); do
cur="$cur$kb "
case "$prev" in *" $kb "*) continue ;; esac
is_declared "$kb" && continue
s=$(saved_for "$kb")
if [ -n "$s" ]; then
apply "$kb" "$s"
else
choice=$(printf '%s\n' $layouts | rofi -dmenu -p "Layout · $kb")
[ -n "$choice" ] && { printf '%s=%s\n' "$kb" "$choice" >> "$state"; apply "$kb" "$choice"; }
fi
done
prev="$cur"
done
'';
in
{
wayland.windowManager.hyprland = lib.mkIf config.nomarchy.hyprland.enable {
@@ -59,9 +130,12 @@ in
"$mod" = "SUPER";
"$terminal" = config.nomarchy.terminal;
# 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" ];
# `,preferred,auto,1` is the wildcard fallback for any output not in
# nomarchy.monitors; the generated rules are appended and win by name,
# and Hyprland applies them on hotplug. mkDefault so a downstream
# `monitor = [...]` replaces the lot (the live ISO uses mkForce too).
monitor = lib.mkDefault
([ ",preferred,auto,1" ] ++ map monitorRule config.nomarchy.monitors);
# exec-once is a list at normal priority, so downstream additions
# CONCATENATE (your autostart runs alongside ours) rather than
@@ -72,7 +146,7 @@ in
# Paint the wallpaper as soon as the session is up (waits for
# the daemon internally).
"nomarchy-theme-sync wallpaper"
];
] ++ lib.optional kbAutoSwitch "${keyboardWatch}/bin/nomarchy-keyboard-watch";
# ── Theme-driven look ──────────────────────────────────────────
# These flow from theme-state.json and stay at NORMAL priority:
@@ -128,12 +202,20 @@ in
input = {
# 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_layout = lib.concatStringsSep "," kbLayouts;
kb_variant = config.nomarchy.keyboard.variant;
follow_mouse = lib.mkDefault 1;
touchpad.natural_scroll = lib.mkDefault true;
};
# Per-device keyboard layouts (nomarchy.keyboard.devices): each
# overrides the session kb_layout for one named keyboard — e.g. an
# external board that's physically a different layout than the laptop's
# built-in one. Hyprland applies them whenever the device connects.
device = lib.mapAttrsToList
(name: d: { inherit name; kb_layout = d.layout; kb_variant = d.variant; })
config.nomarchy.keyboard.devices;
# dwindle:pseudotile was removed in Hyprland 0.55 (the `pseudo`
# dispatcher still exists); setting it aborts config parsing.
dwindle.preserve_split = lib.mkDefault true;
@@ -183,4 +265,12 @@ in
];
};
};
# nwg-displays: interactive monitor arranger (applies live via hyprctl and
# writes a config file). A helper to find values for nomarchy.monitors —
# the declarative config stays the source of truth (we don't source its
# output). Gated on its toggle; pointless without the Hyprland session.
home.packages = lib.optionals
(config.nomarchy.hyprland.enable && config.nomarchy.displays.enable)
[ pkgs.nwg-displays ];
}

View File

@@ -64,11 +64,13 @@ in
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";
# No before_sleep_cmd here: locking before sleep is driven
# system-side so it can fire for suspend but skip an encrypted
# hibernate — whose LUKS resume already gates the machine, so a
# hyprlock on top is a second password (and a Wayland session-lock
# can't be safely dropped after the fact — killing the locker trips
# its "go to a tty" crash failsafe). See nomarchy-lock-before-sleep
# in modules/nixos/default.nix.
after_sleep_cmd = "hyprctl dispatch dpms on";
};
listener = [

View File

@@ -47,6 +47,7 @@
{ 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"; }
{ mods = "$mod CTRL"; key = "D"; action = "exec, nomarchy-menu dnd"; desc = "Do Not Disturb toggle"; }
# Focus
{ mods = "$mod"; key = "H"; action = "movefocus, l"; desc = "Focus left"; }

View File

@@ -0,0 +1,25 @@
# Night light — a scheduled blue-light filter via hyprsunset (Hyprland's own
# gamma/temperature tool). Warm at night, identity (no shift) by day;
# hyprsunset's time-based `profile` entries handle the schedule and pick the
# right state on session start. Opt-in via nomarchy.nightlight.enable.
#
# The hyprsunset HM service module is provided by home-manager; this only
# configures it. Override anything with plain services.hyprsunset.* options.
{ config, lib, ... }:
let
cfg = config.nomarchy.nightlight;
in
{
config = lib.mkIf cfg.enable {
services.hyprsunset = {
enable = true;
settings.profile = [
# Daytime: identity = no colour change.
{ time = cfg.sunrise; identity = true; }
# Night: shift to the warm temperature.
{ time = cfg.sunset; temperature = cfg.temperature; }
];
};
};
}

View File

@@ -2,6 +2,60 @@
# configure in their home.nix. Kept small on purpose.
{ lib, pkgs, ... }:
let
# One output's layout — turned into a Hyprland `monitor` rule in
# hyprland.nix. Friendlier than raw positional CSV; unset optional fields
# are omitted from the rule.
monitorType = lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
example = "HDMI-A-1";
description = "Output name (see `hyprctl monitors`), or a `desc:<>` match.";
};
resolution = lib.mkOption {
type = lib.types.str;
default = "preferred";
description = "`preferred` | `highres` | `highrr` | `<w>x<h>@<hz>` | `disable`.";
};
position = lib.mkOption {
type = lib.types.str;
default = "auto";
description = "`auto` | `auto-right`/`auto-left`/ | `<x>x<y>`.";
};
scale = lib.mkOption {
type = lib.types.oneOf [ lib.types.str lib.types.int lib.types.float ];
default = 1;
description = "Scale factor (1, 1.5, ) or `auto`.";
};
transform = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
description = "Rotation 07 (90°/180°/270° = 1/2/3).";
};
mirror = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Mirror another output by name.";
};
bitdepth = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
description = "Colour bit depth (8 or 10).";
};
vrr = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
description = "Variable refresh: 0 off, 1 on, 2 fullscreen-only.";
};
extra = lib.mkOption {
type = lib.types.str;
default = "";
description = "Extra comma-separated Hyprland monitor args, appended verbatim.";
};
};
};
in
{
options.nomarchy = {
# ── Required ───────────────────────────────────────────────────
@@ -42,6 +96,49 @@
description = "XKB variant for the Hyprland session.";
};
keyboard.layouts = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "de" "fr" ];
description = ''
Extra candidate layouts offered by the interactive new-keyboard
picker. When non-empty, a small watcher runs in the session: when a
keyboard connects that isn't covered by nomarchy.keyboard.devices and
hasn't been chosen before, it pops a rofi picker (these layouts plus
the primary nomarchy.keyboard.layout), applies the choice, and
remembers it per-device (~/.local/state) re-applying automatically
on later reconnects. The runtime-remember complement to the
declarative keyboard.devices; a stateful runtime piece by design.
'';
};
keyboard.devices = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule {
options = {
layout = lib.mkOption {
type = lib.types.str;
description = "XKB layout for this specific keyboard.";
};
variant = lib.mkOption {
type = lib.types.str;
default = "";
description = "XKB variant for this keyboard.";
};
};
});
default = { };
example = lib.literalExpression ''{ "keychron-keychron-k2" = { layout = "de"; }; }'';
description = ''
Per-device keyboard layout. The key is the device name from
`hyprctl devices` (lower-case, hyphenated). Each entry generates a
Hyprland `device` block that overrides the session-wide
nomarchy.keyboard.layout for that keyboard only e.g. an external
keyboard that's physically a different layout than the laptop's
built-in one. Hyprland applies it automatically whenever that
keyboard is connected.
'';
};
package = lib.mkOption {
type = lib.types.package;
default = pkgs.nomarchy-theme-sync;
@@ -60,6 +157,54 @@
'';
};
nightlight = {
enable = lib.mkEnableOption ''
a scheduled blue-light filter (hyprsunset): warm at night, no shift
by day. Opt-in; tune the temperature + sunrise/sunset below'';
temperature = lib.mkOption {
type = lib.types.int;
default = 4000;
example = 3500;
description = "Warm colour temperature (K) applied at night lower is warmer.";
};
sunrise = lib.mkOption {
type = lib.types.str;
default = "07:00";
example = "06:30";
description = "Time (HH:MM) the filter turns OFF daytime, no colour shift.";
};
sunset = lib.mkOption {
type = lib.types.str;
default = "20:00";
example = "21:00";
description = "Time (HH:MM) the filter turns ON warm.";
};
};
monitors = lib.mkOption {
type = lib.types.listOf monitorType;
default = [ ];
example = lib.literalExpression ''
[
{ name = "eDP-1"; position = "0x0"; }
{ name = "HDMI-A-1"; position = "auto-right"; scale = 1; }
]
'';
description = ''
Declarative per-output monitor layout. Each entry becomes a Hyprland
`monitor` rule; Hyprland applies them on hotplug, so a declared
external/dock output arranges itself when connected. The built-in
`,preferred,auto,1` wildcard stays as the fallback for any output you
don't list. For raw control set
`wayland.windowManager.hyprland.settings.monitor` directly instead
(it replaces this). Run `nwg-displays` (nomarchy.displays.enable) to
find the right values interactively.
'';
};
# ── Component toggles ──────────────────────────────────────────
hyprland.enable = lib.mkEnableOption "Nomarchy's Hyprland configuration" // { default = true; };
waybar.enable = lib.mkEnableOption "Nomarchy's Waybar configuration" // { default = true; };
@@ -74,6 +219,7 @@
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; };
displays.enable = lib.mkEnableOption "the nwg-displays interactive monitor arranger (a helper to find nomarchy.monitors values; the declarative config stays the source of truth)" // { default = true; };
# ── Computed (read-only) ───────────────────────────────────────
theme = lib.mkOption {

View File

@@ -41,9 +41,9 @@ let
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.
# hyprctl and friends. Menu entries carry real icons from the theme's
# icon set (Papirus) via rofi's per-row icon protocol see row() below.
# `nomarchy-menu` with no argument shows the module picker.
urlencode() {
local s="$1" out="" ch i
@@ -58,16 +58,22 @@ let
printf '%s' "$out"
}
# Emit one dmenu row with a themed icon: `row "Label" icon-name`. The NUL
# + 0x1f field separator is rofi's per-row icon protocol; printed straight
# to the pipe because bash can't store NUL in a variable. Rendered when
# rofi has show-icons (on globally; passed explicitly on the icon menus).
row() { printf '%s\0icon\x1f%s\n' "$1" "$2"; }
case "''${1:-}" in
power)
choice=$(printf '%s\n' \
"󰌾 Lock" \
"󰍃 Logout" \
"󰤄 Suspend" \
"󰒲 Hibernate" \
"󰜉 Reboot" \
"󰐥 Shutdown" \
| rofi -dmenu -p power) || exit 0
choice=$( {
row "Lock" system-lock-screen
row "Logout" system-log-out
row "Suspend" system-suspend
row "Hibernate" system-hibernate
row "Reboot" system-reboot
row "Shutdown" system-shutdown
} | rofi -dmenu -show-icons -p Power) || exit 0
case "$choice" in
*Lock) loginctl lock-session ;;
*Logout) hyprctl dispatch exit ;;
@@ -134,12 +140,12 @@ let
exec blueman-manager ;;
capture)
choice=$(printf '%s\n' \
"󰆟 Region clipboard" \
"󰆟 Region file" \
"󰍹 Full screen clipboard" \
"󰍹 Full screen file" \
| rofi -dmenu -p capture) || exit 0
choice=$( {
row "Region clipboard" applets-screenshooter
row "Region file" applets-screenshooter
row "Full screen clipboard" camera-photo
row "Full screen file" camera-photo
} | rofi -dmenu -show-icons -p Capture) || exit 0
dir="$HOME/Pictures/Screenshots"
file="$dir/$(date +%Y%m%d-%H%M%S).png"
case "$choice" in
@@ -164,49 +170,93 @@ let
[ -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
dnd)
# Toggle swaync Do-Not-Disturb (-d prints the new state). With DND
# on, notifications are suppressed the Waybar bell-off icon is the
# cue so only confirm with a toast when turning it back off.
[ "$(swaync-client -d)" = "false" ] \
&& notify-send "Do Not Disturb off" "Notifications resumed."
exit 0 ;;
snapshot)
# btrfs-assistant: snapshot browse / diff / restore / rollback over
# snapper, elevating via polkit. Shipped system-side with
# nomarchy.system.snapper; self-gate so the entry no-ops elsewhere.
command -v btrfs-assistant >/dev/null 2>&1 \
|| { notify-send "Snapshots" "btrfs-assistant isn't installed (BTRFS snapshots off?)."; exit 0; }
exec btrfs-assistant ;;
tools)
# Tools submenu utilities you invoke. Each leaf is also reachable
# directly (SUPER+CTRL+<mnemonic>); this just keeps the root short.
choice=$( {
row "Calculator" accessories-calculator
row "Clipboard" edit-paste
row "Emoji" face-smile-big
row "Files" system-file-manager
row "Web search" system-search
row "Capture" applets-screenshooter
row "Ask Claude" internet-chat
row "Back" go-previous
} | rofi -dmenu -show-icons -p Tools) || 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 ;;
*Clipboard*) exec "$0" clipboard ;;
*Emoji*) exec "$0" emoji ;;
*Files*) exec "$0" files ;;
*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 ;;
*Back*) exec "$0" ;;
esac ;;
system)
# System submenu connectivity and machine state. Snapshots and the
# power-profile picker self-gate (BTRFS+snapper / laptop on ppd), so
# the submenu shrinks to match the hardware, like the Waybar modules.
bats=(/sys/class/power_supply/BAT*)
choice=$( {
row "Network" network-wireless
row "Bluetooth" bluetooth
row "Do Not Disturb" notification-disabled
command -v btrfs-assistant >/dev/null 2>&1 && row "Snapshots" timeshift
if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then
row "Power profile" preferences-system-power
fi
row "Back" go-previous
} | rofi -dmenu -show-icons -p System) || exit 0
case "$choice" in
*Network*) exec "$0" network ;;
*Bluetooth*) exec "$0" bluetooth ;;
*"Do Not Disturb"*) exec "$0" dnd ;;
*Snapshots*) exec "$0" snapshot ;;
*"Power profile"*) exec "$0" power-profile ;;
*Back*) exec "$0" ;;
esac ;;
"")
# Root picker grouped into category submenus so the top level stays
# short and scannable. Apps/Theme/Power/Keybindings are frequent enough
# to keep at the top; everything else lives under Tools or System.
choice=$( {
row "Apps" applications-all
row "Theme" preferences-desktop-theme
row "Tools" applications-utilities
row "System" preferences-system
row "Power" system-shutdown
row "Keybindings" preferences-desktop-keyboard
} | rofi -dmenu -show-icons -p Menu) || exit 0
case "$choice" in
*Apps*) exec rofi -show drun ;;
*Theme*) exec "$0" theme ;;
*Tools*) exec "$0" tools ;;
*System*) exec "$0" system ;;
*Power*) exec "$0" power ;;
*Keybindings*) exec "$0" keybinds ;;
esac ;;
*)
echo "usage: nomarchy-menu [power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|capture|keybinds|ask]" >&2
echo "usage: nomarchy-menu [tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|capture|keybinds|ask|dnd|snapshot]" >&2
exit 64 ;;
esac
'';
@@ -238,6 +288,10 @@ in
drun-display-format = "{name}";
display-drun = "apps";
display-run = "run";
# Size the window to the actual number of rows instead of always
# reserving `lines` (8) — so a 6-entry menu doesn't leave empty space,
# while the app launcher still fills + scrolls past `lines`.
fixed-num-lines = false;
};
# Whole-swap themes bring their own rofi.rasi; otherwise the theme
@@ -290,22 +344,34 @@ in
columns = 1;
dynamic = true;
scrollbar = false;
spacing = px 2;
spacing = px 4; # a little air so the rounded zebra rows separate
padding = mkLiteral "8px 0px 0px 0px";
};
# Rounded, roomy rows with subtle zebra striping: alternate rows
# lift to @surface, the selected row to @accent. normal/alternate/
# selected are rofi's row parities (the `.normal` suffix is the
# match-state, untouched by our menus).
"element" = {
padding = mkLiteral "8px 12px";
padding = mkLiteral "10px 14px";
border-radius = px t.ui.rounding;
spacing = px 8;
spacing = px 12; # gap between the (larger) icon and the label
};
"element selected" = {
"element normal.normal" = {
background-color = mkLiteral "@bg";
text-color = mkLiteral "@fg";
};
"element alternate.normal" = {
background-color = mkLiteral "@surface";
text-color = mkLiteral "@fg";
};
"element selected.normal" = {
background-color = mkLiteral "@accent";
text-color = mkLiteral "@bg";
};
"element-icon" = {
background-color = mkLiteral "transparent";
size = mkLiteral "1em";
size = px t.ui.iconSize; # real icons (Papirus), not 1em font glyphs
};
"element-text" = {
background-color = mkLiteral "transparent";

View File

@@ -35,6 +35,9 @@ in
ignoreSpace = true;
expireDuplicatesFirst = true;
};
# Curated, on by default. The rule (same as rg/fd above): short where
# it helps, but never shadow a real binary — so the git block keeps off
# `gs` (ghostscript), and nothing here masks a coreutil.
shellAliases = {
cat = "bat --paging=never";
less = "bat";
@@ -42,8 +45,42 @@ in
# 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";
# Navigation — climb without counting dots.
".." = "cd ..";
"..." = "cd ../..";
"...." = "cd ../../..";
# Git — the verbs you retype all day.
g = "git";
gst = "git status -sb";
ga = "git add";
gaa = "git add -A";
gc = "git commit";
gcm = "git commit -m";
gco = "git checkout";
gsw = "git switch";
gb = "git branch";
gd = "git diff";
gds = "git diff --staged";
gl = "git log --oneline --graph --decorate -20";
glg = "git log --oneline --graph --decorate --all";
gp = "git push";
gpl = "git pull";
gf = "git fetch --all --prune";
# Nix — the flake/store verbs (system/home rebuilds already have the
# sys-update / home-update commands, so they're not re-aliased here).
ns = "nix shell"; # ns nixpkgs#ripgrep
nr = "nix run"; # nr nixpkgs#cowsay
nfu = "nix flake update";
nfc = "nix flake check";
nsearch = "nix search nixpkgs";
ngc = "nix-collect-garbage -d"; # user generations; sudo for system roots
# Misc.
path = "echo $PATH | tr ':' '\\n'";
reload = "exec zsh";
};
};

View File

@@ -38,6 +38,7 @@ let
gapsOut = 12;
borderSize = 2;
rounding = 10;
iconSize = 36; # rofi launcher + menu icon size (px) for the generated theme
activeOpacity = 1.0;
inactiveOpacity = 0.95;
terminalOpacity = 0.96;

View File

@@ -12,12 +12,23 @@
let
t = config.nomarchy.theme;
# Show the active-keyboard-layout indicator only when more than one layout
# is in play — multiple session layouts (comma-separated) or per-device
# overrides (nomarchy.keyboard.devices) — so single-layout bars stay clean.
showLanguage = lib.hasInfix "," config.nomarchy.keyboard.layout
|| config.nomarchy.keyboard.layouts != [ ]
|| config.nomarchy.keyboard.devices != { };
# 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" ''
#
# Named writeShellScriptBins (put on PATH via home.packages below) rather
# than bare writeShellScript store paths, so the whole-swap themes' static
# waybar.jsonc can exec them by name too — same as the swaync bell.
powerProfileStatus = pkgs.writeShellScriptBin "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
@@ -29,7 +40,7 @@ let
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" ''
powerProfileCycle = pkgs.writeShellScriptBin "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')
@@ -66,7 +77,9 @@ let
modules-left = [ "hyprland/workspaces" "hyprland/window" ];
modules-center = [ "clock" ];
modules-right = [ "tray" "pulseaudio" "network" "cpu" "memory" "custom/powerprofile" "battery" ];
modules-right = [ "tray" "pulseaudio" "network" "cpu" "memory" "custom/powerprofile" ]
++ lib.optional showLanguage "hyprland/language"
++ [ "battery" "custom/notification" ];
"hyprland/workspaces" = {
format = "{icon}";
@@ -84,6 +97,13 @@ let
tooltip-format = "<tt><small>{calendar}</small></tt>";
};
# Active keyboard layout (per focused device) — only placed in
# modules-right when showLanguage (see above).
"hyprland/language" = {
format = "󰌌 {short}";
tooltip = false;
};
pulseaudio = {
format = "{icon} {volume}%";
format-muted = "󰝟";
@@ -112,10 +132,35 @@ let
};
"custom/powerprofile" = {
exec = "${powerProfileStatus}";
exec = "nomarchy-powerprofile-status";
return-type = "json";
interval = 5;
on-click = "${powerProfileCycle}";
on-click = "nomarchy-powerprofile-cycle";
};
# swaync notification bell + Do-Not-Disturb state. `-swb` streams JSON
# (text/tooltip/class) on every change, so it tracks count and DND with
# no polling. Left-click toggles the panel; right-click toggles DND.
# The `dnd-*` classes (bell-off glyph) are styled muted below.
"custom/notification" = {
exec = "swaync-client -swb";
return-type = "json";
exec-if = "which swaync-client";
format = "{icon}";
format-icons = {
none = "󰂜";
notification = "󰂚";
dnd-none = "󰂛";
dnd-notification = "󰂛";
inhibited-none = "󰂜";
inhibited-notification = "󰂚";
dnd-inhibited-none = "󰂛";
dnd-inhibited-notification = "󰂛";
};
on-click = "swaync-client -t -sw";
on-click-right = "swaync-client -d -sw";
tooltip = true;
escape = true;
};
tray.spacing = 8;
@@ -164,11 +209,16 @@ let
font-weight: bold;
}
#tray, #pulseaudio, #network, #cpu, #memory, #custom-powerprofile, #battery {
#tray, #pulseaudio, #network, #cpu, #memory, #custom-powerprofile, #language, #battery, #custom-notification {
color: @subtext;
padding: 0 10px;
}
/* notifications waiting accent; Do-Not-Disturb muted bell-off */
#custom-notification.notification { color: @accent; }
#custom-notification.dnd-none,
#custom-notification.dnd-notification { color: @muted; }
#pulseaudio.muted { color: @muted; }
#battery.warning { color: @warn; }
#battery.critical { color: @bad; }
@@ -197,4 +247,9 @@ in
else generatedStyle
);
};
# The power-profile helpers on PATH, so both the generated bar and the
# whole-swap themes' static waybar.jsonc can exec them by name.
home.packages = lib.optionals config.nomarchy.waybar.enable
[ powerProfileStatus powerProfileCycle ];
}

View File

@@ -22,7 +22,7 @@ let
'';
in
{
imports = [ ./options.nix ./plymouth.nix ./file-manager.nix ./power.nix ];
imports = [ ./options.nix ./plymouth.nix ./file-manager.nix ./power.nix ./services.nix ];
config = {
# The safe half of distro branding: distroName flows into
@@ -123,23 +123,32 @@ in
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" ];
# No double-unlock on hibernate. Locking the session before sleep is
# right for suspend (resumes from RAM, no other gate), but an encrypted
# hibernate already resumes through the LUKS passphrase — a hyprlock on
# top is a second password. And we can't just drop the lock after the
# resume: a Wayland session-lock whose client dies without releasing
# trips hyprlock's "go to a tty" crash failsafe (the compositor keeps the
# screen locked for safety). So don't lock before an encrypted hibernate
# in the first place. hypridle can't tell suspend from hibernate, hence a
# system unit hooked to the sleep targets: it locks on the RAM-resume
# sleeps always, and on hibernate only when the disk is unencrypted (no
# LUKS gate to rely on). Replaces hypridle's old before_sleep_cmd.
systemd.services.nomarchy-lock-before-sleep =
let
encrypted = builtins.attrNames config.boot.initrd.luks.devices != [ ];
# Sleeps whose normal resume is from RAM — always lock these.
ramTargets = [ "suspend.target" "hybrid-sleep.target" "suspend-then-hibernate.target" ];
ramServices = [ "systemd-suspend.service" "systemd-hybrid-sleep.service" "systemd-suspend-then-hibernate.service" ];
in
{
description = "Lock the session before sleep (skipped for an encrypted hibernate)";
before = ramServices ++ lib.optional (!encrypted) "systemd-hibernate.service";
wantedBy = ramTargets ++ lib.optional (!encrypted) "hibernate.target";
serviceConfig = {
Type = "oneshot";
# `-`: a missing hyprlock (idle disabled, or not locked) isn't a failure.
ExecStart = "-${pkgs.procps}/bin/pkill -x hyprlock";
# Plural: a system unit has no session of its own to lock.
ExecStart = "${config.systemd.package}/bin/loginctl lock-sessions";
};
};
@@ -176,7 +185,7 @@ in
# is a clean no-op rather than a failing timer.
services.snapper.configs = lib.mkIf
(cfg.snapper.enable && (config.fileSystems."/".fsType or "") == "btrfs")
{
({
root = {
SUBVOLUME = "/";
TIMELINE_CREATE = true;
@@ -187,6 +196,39 @@ in
TIMELINE_LIMIT_MONTHLY = "0";
TIMELINE_LIMIT_YEARLY = "0";
};
}
# /home is user data — snapshot it too when it's its own BTRFS subvolume
# (the installer's @home). Keep the recent hourlies for oops-recovery
# plus dailies, and a month of weeklies; "at least daily" by default.
// lib.optionalAttrs ((config.fileSystems."/home".fsType or "") == "btrfs") {
home = {
SUBVOLUME = "/home";
TIMELINE_CREATE = true;
TIMELINE_CLEANUP = true;
TIMELINE_LIMIT_HOURLY = "5";
TIMELINE_LIMIT_DAILY = "7";
TIMELINE_LIMIT_WEEKLY = "4";
TIMELINE_LIMIT_MONTHLY = "0";
TIMELINE_LIMIT_YEARLY = "0";
};
});
# snapper needs a `.snapshots` subvolume inside each tracked subvolume.
# The installer made one for / (@snapshots → /.snapshots) but not /home,
# so create a nested one there if missing (idempotent via the condition;
# excluded from /home snapshots since BTRFS snapshots are non-recursive).
# Runs at boot so it also fixes already-installed machines — no disko
# change needed.
systemd.services.nomarchy-home-snapshots-subvol = lib.mkIf
(cfg.snapper.enable && (config.fileSystems."/home".fsType or "") == "btrfs") {
description = "Create /home/.snapshots for snapper if missing";
wantedBy = [ "multi-user.target" ];
before = [ "snapper-timeline.service" "snapper-cleanup.service" ];
unitConfig.ConditionPathExists = "!/home/.snapshots";
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.btrfs-progs}/bin/btrfs subvolume create /home/.snapshots";
};
};
# ── Fonts ────────────────────────────────────────────────────────
@@ -276,7 +318,10 @@ in
--cleanup-algorithm number
echo "Rebuilding..."
nixos-rebuild switch --flake /etc/nixos#default "$@"
'');
'')
# The desktop snapshot manager (browse / diff / restore / rollback over
# snapper, elevating via polkit) — what `nomarchy-menu snapshot` launches.
++ lib.optional cfg.snapper.enable pkgs.btrfs-assistant;
# Don't let boot entries fill the ESP over the years.
boot.loader.systemd-boot.configurationLimit = lib.mkDefault 10;
@@ -291,6 +336,11 @@ in
settings = {
experimental-features = [ "nix-command" "flakes" ];
auto-optimise-store = lib.mkDefault true;
# The downstream flake (~/.nomarchy) is meant to be a live working
# tree: nomarchy-theme-sync rewrites theme-state.json on every
# switch (and you needn't commit each tweak), so the "Git tree is
# dirty" warning fires on every rebuild and is pure noise here.
warn-dirty = lib.mkDefault false;
};
gc = {
automatic = lib.mkDefault true;

280
modules/nixos/services.nix Normal file
View File

@@ -0,0 +1,280 @@
# Opt-in services & integrations — off by default, one toggle each, enabled
# in system.nix (see the commented examples in the downstream template).
# These are system-side: their own `nomarchy.services.*` namespace, distinct
# from the distro's core `nomarchy.system.*` toggles. `username` (from
# specialArgs) is read lazily, only where a service needs it.
{ config, lib, pkgs, ... }@args:
let
cfg = config.nomarchy.services;
in
{
options.nomarchy.services = {
tailscale.enable = lib.mkEnableOption ''
Tailscale, the mesh VPN ships the daemon; authenticate once with
`sudo tailscale up`'';
syncthing.enable = lib.mkEnableOption ''
Syncthing continuous file sync, running as the login user add
folders and devices in the web UI at http://127.0.0.1:8384'';
podman.enable = lib.mkEnableOption ''
Podman for rootless containers, with `docker` aliased to it and DNS on
the default network; the login user gets a subuid/subgid range'';
flatpak.enable = lib.mkEnableOption ''
Flatpak, with the Flathub remote added system-wide install apps with
`flatpak install flathub <app>`'';
pika.enable = lib.mkEnableOption ''
Pika Backup, a friendly GUI for scheduled Borg backups (the app;
configure repositories and schedules inside it)'';
steam.enable = lib.mkEnableOption ''
Steam (Valve's game store/launcher) via programs.steam which wires up
the 32-bit graphics libraries, controller udev rules and Remote-Play
firewall ports a bare package can't. Unfree'';
libvirt.enable = lib.mkEnableOption ''
libvirt/KVM virtualisation with the virt-manager GUI runs the
libvirtd daemon and adds the login user to the `libvirtd` group, so VMs
can be managed without root'';
obs.enable = lib.mkEnableOption ''
OBS Studio for screen recording and streaming, plus a v4l2loopback
virtual camera so an OBS scene can be selected as a webcam in Zoom,
Teams or a browser (screen capture itself already works over the
desktop's PipeWire portal)'';
docker.enable = lib.mkEnableOption ''
Docker (rootful) with the login user in the `docker` group. Mutually
exclusive with the podman toggle's docker-compat (both provide the
`docker` command) enable only one'';
kdeconnect.enable = lib.mkEnableOption ''
KDE Connect for phone integration shared clipboard, notifications and
file transfer with a paired phone; opens the firewall ports it needs'';
gamemode.enable = lib.mkEnableOption ''
Feral GameMode a daemon games ask to apply a performance CPU governor
and IO/nice priorities while running (via `gamemoderun`)'';
adb.enable = lib.mkEnableOption ''
the Android platform tools (adb/fastboot) for flashing/debugging phones.
systemd 258 applies the device uaccess udev rules automatically, so the
tools alone are enough no group dance'';
wireshark.enable = lib.mkEnableOption ''
Wireshark (the Qt GUI) for packet capture, with the login user in the
`wireshark` group so capture works without root'';
ollama.enable = lib.mkEnableOption ''
Ollama, a local LLM runtime serving an API on 127.0.0.1:11434 (pair it
with the lmstudio/alpaca GUIs). CPU by default set
`services.ollama.acceleration` natively for GPU offload'';
printing.enable = lib.mkEnableOption ''
CUPS printing with Avahi/mDNS, so network printers are auto-discovered
(add vendor drivers via `services.printing.drivers`)'';
openrgb.enable = lib.mkEnableOption ''
the OpenRGB daemon and GUI for controlling RGB lighting on peripherals
and (where supported) motherboards installs the udev rules for device
access. Some motherboard controllers also need `hardware.i2c.enable`'';
restic = {
enable = lib.mkEnableOption ''
a scheduled (daily) restic backup headless, complementing the Pika
GUI. Backs up `paths` with sane retention (7 daily / 4 weekly /
6 monthly); needs a `repository` and a `passwordFile` (below). List
and restore with the generated `restic-nomarchy` wrapper (repo +
password baked in), e.g. `restic-nomarchy snapshots`'';
repository = lib.mkOption {
type = lib.types.str;
default = "";
example = "/mnt/backup/restic";
description = ''
The restic repository as a path or URL e.g. `/mnt/backup/restic`,
`sftp:user@host:/srv/restic`, `b2:bucket:/path`. Required when
`restic.enable` is set.
'';
};
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/etc/nomarchy/restic-password";
description = ''
Path to a file holding the repository password. Give an absolute
path that lives ON THE MACHINE not a `./file` in the flake, which
would copy the secret into the world-readable Nix store. Required
when `restic.enable` is set.
'';
};
paths = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "/home" ];
description = "Paths to include in the backup.";
};
};
};
config = lib.mkMerge [
(lib.mkIf cfg.tailscale.enable {
services.tailscale.enable = true;
})
(lib.mkIf cfg.syncthing.enable {
services.syncthing = {
enable = true;
user = args.username;
# Manage folders/devices in the GUI; don't let the (empty) declarative
# config wipe them on a rebuild.
overrideDevices = false;
overrideFolders = false;
guiAddress = "127.0.0.1:8384";
};
})
(lib.mkIf cfg.podman.enable {
virtualisation.podman = {
enable = true;
dockerCompat = lib.mkDefault true; # `docker` → podman (off if you run real docker)
defaultNetwork.settings.dns_enabled = true; # DNS for rootless containers
};
# Rootless podman needs a subuid/subgid range for the user.
users.users.${args.username}.autoSubUidGidRange = lib.mkDefault true;
})
(lib.mkIf cfg.flatpak.enable {
services.flatpak.enable = true;
# Flatpak ships no remotes; add Flathub system-wide. Idempotent
# (--if-not-exists), and it reads the .flatpakrepo over the network, so
# it waits for connectivity and simply retries next boot if offline.
systemd.services.nomarchy-flathub = {
description = "Add the Flathub Flatpak remote";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
path = [ pkgs.flatpak ];
serviceConfig.Type = "oneshot";
script = ''
flatpak remote-add --system --if-not-exists flathub \
https://dl.flathub.org/repo/flathub.flatpakrepo
'';
};
})
(lib.mkIf cfg.pika.enable {
environment.systemPackages = [ pkgs.pika-backup ];
})
(lib.mkIf cfg.steam.enable {
# The Steam package alone misses the system bits a game needs: the
# 32-bit graphics stack, controller udev rules, Remote-Play ports.
programs.steam.enable = true;
})
(lib.mkIf cfg.libvirt.enable {
virtualisation.libvirtd.enable = true;
programs.virt-manager.enable = true;
# Group membership lets the user reach the system libvirtd socket
# (manage VMs) without sudo. Merges with the user's other extraGroups.
users.users.${args.username}.extraGroups = [ "libvirtd" ];
})
(lib.mkIf cfg.obs.enable {
environment.systemPackages = [ pkgs.obs-studio ];
# Virtual camera: OBS streams a scene into a v4l2 loopback device that
# Zoom/Teams/browsers then pick up as a webcam. exclusive_caps=1 is the
# bit that makes those apps actually list the device.
boot.extraModulePackages = [ config.boot.kernelPackages.v4l2loopback ];
boot.kernelModules = [ "v4l2loopback" ];
boot.extraModprobeConfig = ''
options v4l2loopback devices=1 video_nr=10 card_label="OBS Virtual Camera" exclusive_caps=1
'';
})
(lib.mkIf cfg.docker.enable {
virtualisation.docker.enable = true;
users.users.${args.username}.extraGroups = [ "docker" ];
# Podman's docker-compat and real Docker both claim the `docker`
# command — guard against enabling both (unless you turn that off).
assertions = [{
assertion = !(cfg.podman.enable && config.virtualisation.podman.dockerCompat);
message = ''
nomarchy.services: docker and podman both provide the `docker`
command. Enable only one, or set
`virtualisation.podman.dockerCompat = false`.
'';
}];
})
(lib.mkIf cfg.kdeconnect.enable {
programs.kdeconnect.enable = true; # installs the app and opens its ports
})
(lib.mkIf cfg.gamemode.enable {
programs.gamemode.enable = true;
})
(lib.mkIf cfg.adb.enable {
# `programs.adb` was removed — systemd ≥258 handles the device uaccess
# udev rules, so the tools package is all that's needed now.
environment.systemPackages = [ pkgs.android-tools ];
})
(lib.mkIf cfg.wireshark.enable {
programs.wireshark = {
enable = true;
package = pkgs.wireshark; # the Qt GUI (the module defaults to the CLI)
};
users.users.${args.username}.extraGroups = [ "wireshark" ];
})
(lib.mkIf cfg.ollama.enable {
services.ollama.enable = true;
})
(lib.mkIf cfg.printing.enable {
services.printing.enable = true;
# mDNS so network printers show up without manual setup.
services.avahi = {
enable = true;
nssmdns4 = true;
openFirewall = true;
};
})
(lib.mkIf cfg.openrgb.enable {
services.hardware.openrgb.enable = true;
})
(lib.mkIf cfg.restic.enable {
assertions = [
{
assertion = cfg.restic.repository != "";
message = "nomarchy.services.restic.repository must be set when restic is enabled.";
}
{
assertion = cfg.restic.passwordFile != null;
message = "nomarchy.services.restic.passwordFile must be set when restic is enabled.";
}
];
services.restic.backups.nomarchy = {
repository = cfg.restic.repository;
passwordFile = cfg.restic.passwordFile;
paths = cfg.restic.paths;
initialize = true; # create the repo on first run if it doesn't exist
extraBackupArgs = [ "--exclude-caches" ];
pruneOpts = [ "--keep-daily 7" "--keep-weekly 4" "--keep-monthly 6" ];
timerConfig = {
OnCalendar = "daily";
Persistent = true; # catch up a missed run after downtime
};
};
})
];
}

View File

@@ -444,15 +444,14 @@ cat > "$FLAKE_DIR/system.nix" <<EOF
time.timeZone = "$TIMEZONE";
i18n.defaultLocale = "$LOCALE";
# One keyboard layout everywhere: xkb is the source of truth, the
# virtual console derives from it (greeter/ttys), and the systemd
# initrd applies it to the LUKS passphrase prompt too (the classic
# script initrd cannot). The Hyprland session reads the same layout
# from nomarchy.keyboard.* in home.nix.
# One keyboard layout everywhere: xkb is the source of truth, and the
# distro defaults (console.useXkbConfig + the systemd initrd, set in
# the nomarchy module) derive the virtual console and the LUKS
# passphrase prompt from it — so only the chosen layout is written here.
# The Hyprland session reads the same layout from nomarchy.keyboard.* in
# home.nix.
services.xserver.xkb.layout = "$KB_LAYOUT";
services.xserver.xkb.variant = "$KB_VARIANT";
console.useXkbConfig = true;
boot.initrd.systemd.enable = true;
# Your login user — \`username\` flows in from flake.nix automatically.
users.users.\${username} = {

View File

@@ -5,15 +5,136 @@
{ pkgs, ... }:
{
# Examples:
# ── Overrides (the desktop is on by default; tweak or opt out) ──────
# 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)
# nomarchy.keyboard.layout = "de"; # session keyboard (set the matching
# # services.xserver.xkb.layout in
# # system.nix too; the console + LUKS
# # prompt follow it automatically)
# ── Opt-in features — uncomment and tweak to enable ─────────────────
# nomarchy.nightlight = { # scheduled blue-light filter (hyprsunset)
# enable = true;
# temperature = 4000; # night warmth in K — lower is warmer
# sunset = "20:00";
# sunrise = "07:00";
# };
#
# nomarchy.monitors = [ # declarative per-output layout, hotplug-applied
# { name = "eDP-1"; position = "0x0"; }
# { name = "HDMI-A-1"; position = "auto-right"; }
# ]; # names from `hyprctl devices`
#
# nomarchy.keyboard.devices = { # a specific keyboard's own layout
# "keychron-keychron-k2" = { layout = "de"; };
# };
#
# nomarchy.keyboard.layouts = [ "de" "fr" ]; # rofi-prompt for a layout when a
# # new keyboard connects + remember it
# ── Application suite ───────────────────────────────────────────────
# A starter complete-workstation set, installed for your user — yours to
# curate. Delete a line to slim the machine; uncomment a suggestion (or
# add your own) to extend it. The desktop itself — terminal, editor
# keybinds, theming — comes from the modules; these are the heavier
# standalone apps the distro doesn't impose by default.
#
# Apps that need system setup beyond a package (Steam's 32-bit stack,
# a daemon, group membership, a kernel module) are NOT here — they're
# opt-in service toggles in system.nix instead (nomarchy.services.* —
# steam, libvirt, obs, docker, gamemode, kdeconnect, adb, wireshark,
# ollama, printing).
home.packages = with pkgs; [
libreoffice-fresh # office suite (documents, sheets, slides)
vscode # code editor (unfree; allowUnfree is on)
gimp # raster image editor
inkscape # vector graphics
mpv # media player
amberol # music player (local library; streaming → spotify below)
# ── More apps — uncomment to add ─────────────────────────────────
# Web browsers
# firefox
# chromium
# ungoogled-chromium
# brave # (unfree)
# google-chrome # (unfree)
# Communication
# signal-desktop
# telegram-desktop
# element-desktop # Matrix
# vesktop # Discord (themeable client)
# slack # (unfree)
# zoom-us # (unfree)
# teams-for-linux # community client (no official Linux app)
# thunderbird # email
# Office & documents
# onlyoffice-desktopeditors # stronger MS-Office fidelity
# kdePackages.okular # PDF viewer
# xournalpp # PDF annotation
# pdfarranger
# calibre # ebook library
# texliveFull # full LaTeX toolchain — multi-GB (texliveMedium is lighter)
# Notes / research
# logseq
# obsidian # (unfree)
# joplin-desktop
# anki-bin # flashcards
# zotero # reference manager
# Code & data
# zed-editor
# lazygit
# dbeaver-bin # database GUI
# bruno # API client
# distrobox # containerized dev shells
# Graphics & design
# krita # digital painting
# blender # 3D
# darktable # RAW photo
# Audio / video
# audacity
# shotcut # video editor
# kdePackages.kdenlive # video editor (pulls in KDE libs)
# handbrake # transcoder
# Media
# vlc
# spotify # (unfree)
# newsflash # RSS reader
# Gaming (Steam + GameMode are service toggles in system.nix)
# lutris
# heroic # Epic/GOG launcher
# bottles # Wine prefixes
# prismlauncher # Minecraft
# protonup-qt # Proton-GE manager
# mangohud # in-game overlay
# Files, sync & torrents
# nextcloud-client
# localsend # AirDrop-like transfer
# qbittorrent
# Security
# keepassxc
# bitwarden-desktop
# System
# gnome-disk-utility
# gparted
# baobab # disk usage
# wireguard-tools # WireGuard CLI (NetworkManager imports .conf tunnels natively)
# Local AI (Ollama is a service toggle in system.nix)
# lmstudio # (unfree; large closure)
# alpaca # GTK Ollama UI
];
}

View File

@@ -18,9 +18,41 @@
extraGroups = [ "wheel" "networkmanager" "video" "input" ];
};
# Examples:
# ── Overrides (uncomment to change) ─────────────────────────────────
# nomarchy.system.greeter.enable = false; # bring your own login manager
# environment.systemPackages = [ pkgs.htop ];
# ── Opt-in features — uncomment and tweak to enable ─────────────────
# nomarchy.system.power = { # active power management (laptops)
# enable = true;
# backend = "ppd"; # "ppd" (default) or "tlp"
# laptop = true; # required by batteryChargeLimit
# batteryChargeLimit = 80; # cap charging for longevity
# thermal.enable = true; # thermald (Intel CPUs)
# };
#
# nomarchy.services.tailscale.enable = true; # mesh VPN — then `sudo tailscale up`
# nomarchy.services.syncthing.enable = true; # file sync — GUI at http://127.0.0.1:8384
# nomarchy.services.podman.enable = true; # rootless containers (docker → podman)
# nomarchy.services.flatpak.enable = true; # Flatpak + the Flathub remote
# nomarchy.services.pika.enable = true; # Pika Backup (GUI Borg backups)
# nomarchy.services.steam.enable = true; # Steam (32-bit libs, controllers, ports)
# nomarchy.services.libvirt.enable = true; # libvirt/KVM + virt-manager GUI
# nomarchy.services.obs.enable = true; # OBS Studio + v4l2loopback virtual camera
# nomarchy.services.docker.enable = true; # Docker rootful (not alongside podman)
# nomarchy.services.kdeconnect.enable = true;# KDE Connect phone integration (opens ports)
# nomarchy.services.gamemode.enable = true; # Feral GameMode performance daemon
# nomarchy.services.adb.enable = true; # Android adb/fastboot tools
# nomarchy.services.wireshark.enable = true; # Wireshark GUI (wireshark group, no root)
# nomarchy.services.ollama.enable = true; # local LLM runtime (127.0.0.1:11434)
# nomarchy.services.printing.enable = true; # CUPS + Avahi network printer discovery
# nomarchy.services.openrgb.enable = true; # RGB peripheral/motherboard lighting daemon
# nomarchy.services.restic = { # scheduled (daily) restic backup
# enable = true;
# repository = "/mnt/backup/restic"; # path or URL (sftp:/b2:/…)
# passwordFile = "/etc/nomarchy/restic-password"; # absolute path, NOT in the flake
# paths = [ "/home" ];
# };
system.stateVersion = "26.05";
}

View File

@@ -29,7 +29,8 @@ window {
border-color: @yellow;
border-radius: 15px;
width: 40%;
height: 50%;
/* No fixed height — size to content (fixed-num-lines:false) so every
* entry shows without scrolling and there's no empty space below. */
location: center;
anchor: center;
x-offset: 0;
@@ -90,6 +91,12 @@ element {
spacing: 15px;
}
/* Zebra striping: every other row lifts with a faint light wash over the
* dark window (bg0 at ~12% alpha). The selected row below still wins. */
element alternate.normal {
background-color: #fdf6e31f;
}
element-icon {
background-color: transparent;
text-color: inherit;

View File

@@ -48,8 +48,10 @@ window#waybar {
#workspaces,
#pulseaudio,
#network,
#custom-powerprofile,
#battery,
#tray,
#custom-notification,
#custom-powermenu {
background-color: @bg0;
color: @fg;

View File

@@ -11,7 +11,7 @@
"modules-left": ["custom/launcher", "clock", "clock#date"],
"modules-center": ["hyprland/workspaces"],
"modules-right": ["pulseaudio", "network", "battery", "tray", "custom/powermenu"],
"modules-right": ["pulseaudio", "network", "custom/powerprofile", "battery", "tray", "custom/notification", "custom/powermenu"],
"hyprland/workspaces": {
"disable-scroll": true,
@@ -80,6 +80,34 @@
"tray": { "spacing": 8 },
"custom/powerprofile": {
"return-type": "json",
"interval": 5,
"exec": "nomarchy-powerprofile-status",
"on-click": "nomarchy-powerprofile-cycle"
},
"custom/notification": {
"format": "{icon}",
"return-type": "json",
"exec": "swaync-client -swb",
"exec-if": "which swaync-client",
"escape": true,
"tooltip": true,
"on-click": "swaync-client -t -sw",
"on-click-right": "swaync-client -d -sw",
"format-icons": {
"none": "",
"notification": "",
"dnd-none": "",
"dnd-notification": "",
"inhibited-none": "",
"inhibited-notification": "",
"dnd-inhibited-none": "",
"dnd-inhibited-notification": ""
}
},
"custom/powermenu": {
"format": "",
"on-click": "nomarchy-menu power",

View File

@@ -29,7 +29,8 @@ window {
border-color: @yellow;
border-radius: 15px;
width: 40%;
height: 50%;
/* No fixed height — size to content (fixed-num-lines:false) so every
* entry shows without scrolling and there's no empty space below. */
location: center;
anchor: center;
x-offset: 0;
@@ -90,6 +91,12 @@ element {
spacing: 15px;
}
/* Zebra striping: every other row lifts with a faint dark wash over the
* cream window (bg0 at ~12% alpha). The selected row below still wins. */
element alternate.normal {
background-color: #2d353b1f;
}
element-icon {
background-color: transparent;
text-color: inherit;

View File

@@ -54,8 +54,10 @@ window#waybar {
#workspaces,
#pulseaudio,
#network,
#custom-powerprofile,
#idle_inhibitor,
#battery,
#custom-notification,
#custom-powermenu {
background-color: @bg0;
color: @fg;

View File

@@ -11,7 +11,7 @@
"modules-left": ["custom/nomarchy", "clock", "clock#date"],
"modules-center": ["hyprland/workspaces"],
"modules-right": ["idle_inhibitor", "pulseaudio", "network", "battery", "tray", "custom/powermenu"],
"modules-right": ["idle_inhibitor", "pulseaudio", "network", "custom/powerprofile", "battery", "tray", "custom/notification", "custom/powermenu"],
"hyprland/workspaces": {
"disable-scroll": true,
@@ -87,6 +87,34 @@
"tray": { "spacing": 8 },
"custom/powerprofile": {
"return-type": "json",
"interval": 5,
"exec": "nomarchy-powerprofile-status",
"on-click": "nomarchy-powerprofile-cycle"
},
"custom/notification": {
"format": "{icon}",
"return-type": "json",
"exec": "swaync-client -swb",
"exec-if": "which swaync-client",
"escape": true,
"tooltip": true,
"on-click": "swaync-client -t -sw",
"on-click-right": "swaync-client -d -sw",
"format-icons": {
"none": "",
"notification": "",
"dnd-none": "",
"dnd-notification": "",
"inhibited-none": "",
"inhibited-notification": "",
"dnd-inhibited-none": "",
"dnd-inhibited-notification": ""
}
},
"custom/powermenu": {
"format": "",
"on-click": "nomarchy-menu power",