Compare commits

..

21 Commits

Author SHA1 Message Date
Bernardo Magri
c2f90c7d0a feat(menu): printer setup — system-config-printer in the System submenu
A `nomarchy-menu printers` entry opens system-config-printer (the CUPS
admin GUI), mirroring the bluetooth/blueman pattern: the printing service
ships the package via environment.systemPackages and the menu execs it,
self-gated on the system-config-printer binary so the entry appears only
when nomarchy.services.printing is on. No direct keybind (rare one-off).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:09:35 +01:00
Bernardo Magri
5747dc9839 feat(nightlight): persistent off via state marker
A manual `off` now drops ~/.local/state/nomarchy/nightlight-off and the
hyprsunset unit refuses to (re)start while it exists
(Unit.ConditionPathExists=!%h/...), so the off survives logout/reboot
until toggled back on. `on`/toggle-on remove the marker before starting
(a failed condition would otherwise no-op the start). Without the marker
behaviour is unchanged: the service stays WantedBy=graphical-session.target
and resumes its schedule at login, so persistence is opt-in via the toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:09:22 +01:00
Bernardo Magri
d1344712b8 docs(roadmap): note automated upstream lock-bump CI
Capture the maintainer-side plan to automate advancing Nomarchy's own
flake.lock (the single-input delivery channel): a bot drives `main`
behind `nix flake check` + the runNixOSTest suite, and a human still
promotes `main -> v1` behind the on-hardware QA gate that can't be CI'd.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 19:45:56 +01:00
Bernardo Magri
37204f5f45 feat(themes): per-theme rofi identities for nord/retro-82/lumon/kanagawa
Author a designed rofi.rasi whole-swap for the four remaining identity
themes, so all six now carry a distinct menu look (summer-day/night
already shipped their legacy ports). Each is built from the theme's
character rather than a legacy layout (their waybar overrides are
palette-only, so there was nothing to port):

- nord:     soft rounded "frost panel" — frost border, brighter-frost
            selection, aurora-purple prompt
- retro-82: sharp CRT terminal — square corners, amber-on-navy, a teal
            scanline underline, mono
- lumon:    clinical cyan "bezel" — thick cyan frame, a framed-not-filled
            readout inputbar, mono
- kanagawa: ink & paper — warm washi-paper frame, deep ink-well inputbar,
            crystal-blue wave selection

Each file is self-contained (it replaces the generated theme) and keeps
the element structure the theme-grid picker's per-invocation -theme-str
layers onto. rofi.nix needs no change — its existing themes/<slug>/
rofi.rasi probe whole-swaps when the file is present. All four parse
clean under `rofi -dump-theme`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 19:45:37 +01:00
97b5944dc1 fix(menu): force-own networkmanager-dmenu config.ini
networkmanager_dmenu writes a default config on first run, so an
unmanaged file can already sit at ~/.config/networkmanager-dmenu/
config.ini and abort `home-manager switch` with a clobber error. We
fully generate this file, so set force = true to overwrite it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:27:35 +01:00
0c483f9512 feat(menu): network + audio modules; case-insensitive fuzzy search
- network: replace the nmtui-in-a-terminal flow with networkmanager_dmenu
  (native rofi wifi/VPN picker), configured via xdg config.ini to drive
  rofi with rofi_highlight; editing a connection drops to nmtui.
- audio: new System -> Audio sink/source switcher via rofi-pulse-select,
  self-gated on the PipeWire/pulse socket.
- search: case-insensitive fuzzy matching with fzf sorting across every
  menu module and the launcher (so "system" finds "System").

Secrets module (rbw/pass) deferred — no Bitwarden/pass setup here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:23:21 +01:00
dfb57c2e34 feat(home): create XDG user directories by default
Enable xdg.userDirs with createDirectories so a fresh install lands with
Downloads/Documents/Pictures/Music/Videos/Desktop/Public/Templates (plus
home-manager's Projects) present and written to user-dirs.dirs, rather
than appearing only on first app use. mkDefault so a downstream home.nix
can flip it off or remap individual paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:23:21 +01:00
995810927d fix(install): pin HOME=/root so root nix never strays into /home/nomarchy
`exec sudo --preserve-env` (needed to carry the NOMARCHY_* vars) also
drags the live session user's HOME=/home/nomarchy into the root install
process. Root-run `nix build` then writes an eval cache + .nix-defexpr
there, and the in-chroot one lands on the TARGET as a stray, orphaned
/home/nomarchy (no such user on the installed system). Pin root's own
HOME after the sudo re-exec, and again inside the nixos-enter chroot
script. The user activation already sets HOME=/home/$USERNAME and is
unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:23:21 +01:00
e1cf190dd2 feat(menu): visual theme picker — rofi preview grid
Replace the plain-text `nomarchy-menu theme` list with a grid of real
desktop previews. The grid, the Name→slug map and the active `✓` mark
are generated at eval time from the preset JSONs (`readDir` + `fromJSON`
in rofi.nix); "active" is just `t.slug`, since every switch rebuilds the
menu. Grouped dark-first then light in one scrollable grid (the previews
make the mode obvious — no light/dark submenu split), `flow: horizontal`
so Down scrolls row-by-row.

Previews are committed as themes/<slug>/preview.png at 480×270 (~2.4 MB
for all 21). rofi's element-icon `size` is single-value → a *square*
cell that *contains* the icon, so a 16:9 preview would letterbox; a
build-time imagemagick step centre-crops each to a square so it fills
the cell edge-to-edge (source stays 16:9, crop is reversible). The
window width is derived from `themeGridIconW` so a column is exactly the
icon side — no slack margins. Themes without a preview degrade to a
plain-name row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:26:39 +01:00
Bernardo Magri
70334e68bb docs(roadmap): record the faster-switches diagnosis + decided approach
Flesh out the "Faster switches" item with the confirmed diagnosis (themes/
is 86 MB and backgrounds/ is all of it; the config is ~208 KB; wallpapers
are never read at eval; theme-state.json is git-tracked so every apply
re-copies the whole 86 MB source) and the decided-but-deferred approach:
Option 1, a separate pinned wallpapers artifact (repo/tarball via flake
input/fetchurl, read through the existing NOMARCHY_DEFAULT_THEMES hook),
which keeps eval pure. Notes the rejected impure-state alternative and the
pre-built-theme-variants follow-on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:13:08 +01:00
Bernardo Magri
6a4af69b0f feat(updates): passive update-awareness indicator + notification
Add opt-in nomarchy.updates (modules/home/updates.nix): a nomarchy-updates
checker on a systemd user timer (interval, default daily) that surfaces a
Waybar indicator + a notification when updates are available, without ever
changing anything (you still run sys-update / flatpak update).

It counts:
- flake inputs behind upstream — only the flake's DIRECT inputs (root.inputs:
  nixpkgs, the Nomarchy input, home-manager, stylix…), not the transitive
  closure, via `git ls-remote` vs the locked rev. Offline → skipped, never a
  false alarm.
- Flatpak updates, when the flatpak CLI is present (.flatpak, gated on
  services.flatpak being on) — Bernardo's suggestion.

Waybar custom/updates self-gates (hidden until the timer finds something;
accent "󰚰 N", signal 9 for instant refresh), in the generated bar and both
summer whole-swaps. The notification fires only when the count grows, so a
daily timer doesn't nag. Click → the upgrade flow in a terminal (sys-update /
flatpak update, each confirmed). nomarchy-updates ships always on PATH and
self-gates so the static whole-swap bars can exec it even when the feature
is off.

Verified: flake check clean; home generation builds; the jq direct-input
filter + status self-gate exercised against the real flake.lock; script
passes bash -n. Pending an on-machine check (ls-remote/notify/timer need a
live session).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:59:09 +01:00
Bernardo Magri
d9466d6555 docs(roadmap): plan the visual theme picker (preview thumbnails)
Add the agreed approach for prettifying nomarchy-menu theme into a rofi icon
grid of real themed-desktop previews: Bernardo captures previews on real
hardware and commits themes/<slug>/preview.png; a build-time derivation
downscales each to a thumbnail that the picker loads as the rofi element icon
(rofi already renders image files — same as the Papirus menu icons), with a
wallpaper/swatch/plain-name fallback while previews are added incrementally.

Notes the headless VM-render route was prototyped and works (runNixOSTest +
software-GL Hyprland + machine.screenshot QMP dump) but real-hardware capture
was chosen for fidelity; the working VM recipe is parked in memory. The
temporary flake checks.theme-shot spike is removed (too heavy for a CI gate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:36:01 +01:00
Bernardo Magri
0e42763aea feat(menu): Back on every submenu + working snapshot fallback
Two menu improvements.

Back everywhere: every list menu now ends with a "↩ Back" entry that
returns one level up — power, theme, power-profile, clipboard, files,
capture, not just Tools/System — so you no longer have to Esc out and
reopen to back up. A `back` helper + a shared `BACK` label keep it uniform;
matched exactly so it can't collide with clipboard/filename content. Esc
still quits instantly.

Snapshot fallback: btrfs-assistant 2.2 segfaults on 26.05 (libbtrfsutil
ABI), so `nomarchy-menu snapshot` now launches nomarchy-snapshots instead —
a keyboard-driven snapper browser/restore. snapper is root-only here and
rofi can't run as root under Wayland, so it runs in a terminal via sudo
(one password prompt) and uses fzf to pick: browse/diff (read-only),
restore files (undochange), or roll back (root config only), each behind a
typed-`yes` confirmation — safer than a one-click rofi rollback.
btrfs-assistant stays installed for when nixpkgs fixes it.

Verified: flake check clean; home generation builds and the menu passes
bash -n with all Back + snapshot wiring; nomarchy-snapshots passes
shellcheck (writeShellApplication) in a snapper-enabled system build.
Pending an on-machine check of the restore/rollback paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 19:54:42 +01:00
Bernardo Magri
a6d6860054 docs(roadmap): note night-light toggle is session-scoped; persistent-off follow-up
The menu/Waybar toggle stops hyprsunset for the session only — it's
WantedBy=graphical-session.target, so the next login auto-starts it and the
off-state doesn't persist (deliberate "true colours for now" behaviour).
Record the persistent-off option (a state marker honoured via the unit's
ConditionPathExists) as an optional follow-up so it isn't forgotten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 22:29:54 +01:00
Bernardo Magri
5c43a93285 feat(nightlight): menu + Waybar on/off toggle
Add a runtime toggle for the scheduled blue-light filter, the remaining
optional item on the night-light roadmap entry.

nomarchy-nightlight (nightlight.nix, always on PATH) starts/stops the
hyprsunset *service* rather than poking its runtime gamma: `systemctl
--user is-active` is the single source of truth, the screen restores to
true colours when stopped, and it lets you force the filter off for
colour-sensitive work (even at night) and back on without touching config.
It self-gates — `status` prints nothing when the unit is absent — so the
indicator hides itself when night-light isn't enabled.

- Menu: `nomarchy-menu nightlight` toggles it; a self-gated row in the
  System submenu (shown only when the hyprsunset unit exists).
- Waybar: a self-gating `custom/nightlight` indicator (moon = on, sun =
  off), in the generated bar and both summer whole-swap themes for parity.

Verified: home generation builds, generated waybar config carries
custom/nightlight, the menu has the dispatcher case + System row, both
scripts pass bash -n, and status self-gates to empty with no unit. Pending
an on-machine check that stopping hyprsunset cleanly restores the gamma.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 22:23:12 +01:00
Bernardo Magri
09c308b93c chore(boot): default boot.zfs.forceImportRoot = false (silence eval warning)
Nomarchy roots are BTRFS, never ZFS, but the zfs module's `true` default for
forceImportRoot emits a warning on every eval/build ("recommended to set it
to false, the new default from 26.11"). Adopt that default early via
mkDefault — silences the noise, future-proofs for 26.11, and a real ZFS-root
downstream can still force it back on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 22:10:28 +01:00
Bernardo Magri
bdf20f2d8e feat(hardware): xe-aware GuC, class-based NPU detect, latestKernel + VM test
Harden the hardware-enablement broad seed for newer hardware:

- xe vs i915: intel.guc emits the i915 param (i915.enable_guc=3), which the
  newer `xe` driver (Lunar Lake / Battlemage / Panther Lake, recent Xe GPUs)
  ignores — it enables GuC by default. hardware-db.sh now reads the in-use
  GPU driver (lspci -k) and writes `intel.guc = false` on xe hardware, so the
  toggle isn't a misleading no-op there.

- NPU detection by PCI accelerator class [1200] (+ keywords), attributing the
  vendor, instead of a fixed device-ID list — so new NPUs (e.g. Panther Lake)
  are caught without a code change. Known IDs kept as a fallback/reference.

- Kernel awareness: a nomarchy.hardware.latestKernel escape hatch
  (linuxPackages_latest) for very-new hardware whose drivers only just landed,
  and a build-time warning when npu.enable predates the shipped kernel
  (amdxdna needs 6.14+, intel_vpu wants recent). Downstream default is 6.18,
  which already carries amd-pstate + amdxdna; the reference host pins latest.

- VM test (checks.hardware-toggles, pkgs.testers.runNixOSTest): boots a
  minimal VM with the toggles on and asserts the config landed — amd_pstate
  + i915.enable_guc in /proc/cmdline, fprintd.service present, pam_fprintd in
  /etc/pam.d/sudo. Passing. (Hardware behaviour still needs bare metal.)

Docs: README + template note latestKernel; ROADMAP records the hardening.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 20:13:31 +01:00
Bernardo Magri
c57d26864e feat(hardware): nomarchy.hardware.* enablement beyond nixos-hardware
The nixos-hardware "common-*" profiles the installer selects cover the
basics (microcode, the Intel/AMD VA-API media stack, weekly fstrim), and
power.nix adds thermald + power-profiles-daemon. This adds the gap above
them, generically and detected at install time.

New modules/nixos/hardware.nix exposes a vendor-keyed nomarchy.hardware.*
surface — broadly-beneficial bits default ON when the vendor is detected
(opt-out), heavy/experimental bits behind opt-in toggles:
  - intel.enable -> GuC/HuC (i915.enable_guc=3); intel.computeRuntime
    (opt-in: intel-compute-runtime + vpl-gpu-rt)
  - amd.enable -> amd_pstate=active + radeonsi VA-API env; amd.rocm.enable
    + amd.rocm.gfxOverride (opt-in: ROCm HIP/OpenCL)
  - fingerprint.enable -> fprintd; fingerprint.pam (opt-in: login + sudo)
  - npu.enable (opt-in/experimental) -> the in-kernel driver
    (amdxdna/intel_vpu) keyed by vendor; userspace runtime is BYO

hardware-db.sh now detects Intel/AMD, a fingerprint reader (libfprint USB
vendor IDs) and an NPU (Intel VPU / AMD XDNA PCI IDs), emitting NOMARCHY
lines the installer bakes into system.nix — safe defaults active, opt-ins
commented. Audited against the commons to avoid double-setting.

Verified: flake check green; a toggles-on build has amd_pstate=active +
i915.enable_guc=3 in kernel-params and ships fprintd.service; detection
runs correctly on the (AMD Ryzen-AI) dev machine. On-hardware verification
of the AMD/NPU/Intel-compute runtime bits is still pending.

Docs: template commented examples, README option table, ROADMAP status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 19:49:39 +01:00
Bernardo Magri
019fdfc8bb feat(waybar): drop redundant network module, enlarge power-profile glyph
The network/wifi indicator duplicated nm-applet, which already sits in the
tray (services.network-manager-applet.enable), so remove it from the bar.
And the power-profile speedometer glyph rendered noticeably smaller than
the neighbouring indicators, so bump its font-size (+3pt generated; 20px
summer-day, 17px summer-night).

Applied to the generated bar (waybar.nix) and both whole-swap themes
(summer-day/night waybar.{jsonc,css}) for parity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 19:13:20 +01:00
Bernardo Magri
c2281dbc61 fix(waybar): launch from exec-once, not a systemd user service
Waybar vanished after a logout→login (no reboot): the bar's systemd user
service landed in `failed`. HM's Hyprland integration cycles
hyprland-session.target on every launch (exec-once stop && start), which
re-pulls WantedBy units — including waybar.service — very early, before
Hyprland's IPC socket is ready. Waybar's hyprland/workspaces & window
modules can't connect, so it exits non-zero and is never retried. A cold
boot's extra latency hid the race, so it only bit on warm relogins. The
non-IPC session services (swaync/hypridle/hyprsunset) survive the same
cycling fine, which is why night-light kept working.

Launch waybar from Hyprland's own exec-once instead (systemd.enable =
false). exec-once entries are dispatched once the compositor is up, so the
IPC socket is ready — the same reliable pattern awww-daemon, the wallpaper
sync and the keyboard watcher already use across relogins. Since the unit
no longer exists to be restarted on a home-manager switch, nudge the
running bar to re-read its rebuilt config/style with SIGUSR2 from
nomarchy-theme-sync so theme switches still reapply the bar live.

Found on the Latitude 5410 hardware sweep (systemctl --user status showed
waybar.service loaded/failed; manual start worked).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 19:00:39 +01:00
Bernardo Magri
b4fe52261b fix(keyboard): isolate new-keyboard layout to its own device
The interactive new-keyboard picker applied the chosen layout with
`hyprctl switchxkblayout <device> <index>`, which flips the *shared*
layout for every keyboard without its own per-device config — so picking
a layout for an external board dragged the built-in keyboard along, and
the change stuck after unplug (it's global state, not device-bound).
Compounding it, the candidate `keyboard.layouts` pool was merged into
`input.kb_layout`, loading those layouts onto the session keyboard too.

Apply the choice as a per-device `hyprctl keyword device[<name>]:kb_layout`
instead — the runtime twin of the declarative `keyboard.devices` blocks —
so it isolates that one keyboard and never touches the built-in board.
Keep the candidate pool out of `input.kb_layout` (session keyboard carries
only `keyboard.layout`); the pool is for external boards, applied per-device.

Also put `nomarchy-keyboard-watch` on PATH when enabled (it was store-path
only via exec-once, so undiscoverable) for debugging.

Found on the Latitude 5410 hardware sweep; picker itself verified working.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 17:47:19 +01:00
47 changed files with 1679 additions and 126 deletions

View File

@@ -190,7 +190,8 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
| `nomarchy.keyboard.variant` | `""` | XKB variant for the session | | `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.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.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.nightlight.enable` | `false` | Scheduled blue-light filter (hyprsunset) — warm at night (`.temperature`, default 4000K) between `.sunset`/`.sunrise`, no shift by day; toggle it on/off from the menu (System Night light) or the Waybar moon/sun indicator |
| `nomarchy.updates.enable` | `false` | Passive update awareness: a background check (`.interval`, default daily) that flags when flake inputs (nixpkgs, the Nomarchy input, …) are behind upstream — and, with Flatpak on, when apps have updates (`.flatpak`) — via a Waybar indicator + notification. Never changes anything; click the indicator to run the upgrade flow |
| `nomarchy.hyprland.enable` | `true` | Nomarchy's Hyprland config | | `nomarchy.hyprland.enable` | `true` | Nomarchy's Hyprland config |
| `nomarchy.waybar.enable` | `true` | Nomarchy's Waybar | | `nomarchy.waybar.enable` | `true` | Nomarchy's Waybar |
| `nomarchy.rofi.enable` | `true` | Themed rofi launcher + `nomarchy-menu` dispatcher | | `nomarchy.rofi.enable` | `true` | Themed rofi launcher + `nomarchy-menu` dispatcher |
@@ -219,6 +220,14 @@ 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.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.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.system.power.batteryChargeLimit` | `null` | Stop charging at this % (e.g. `80`) where the hardware supports it; needs `power.laptop` |
| `nomarchy.hardware.intel.enable` | `false` | Intel enablement above nixos-hardware (GuC/HuC via `i915.enable_guc=3`); the installer sets it on an Intel CPU/GPU |
| `nomarchy.hardware.intel.computeRuntime` | `false` | Opt-in: Intel GPU compute — OpenCL/Level-Zero (`intel-compute-runtime`) + oneVPL (`vpl-gpu-rt`) |
| `nomarchy.hardware.amd.enable` | `false` | AMD enablement above nixos-hardware (amd-pstate EPP + radeonsi VA-API); installer-set on an AMD CPU/GPU |
| `nomarchy.hardware.amd.rocm.enable` | `false` | Opt-in: ROCm HIP/OpenCL GPU compute (multi-GB); pair with `.gfxOverride` (e.g. `"11.0.0"`) for an unlisted iGPU |
| `nomarchy.hardware.fingerprint.enable` | `false` | fprintd for a detected fingerprint reader (installer-set); enroll with `fprintd-enroll` |
| `nomarchy.hardware.fingerprint.pam` | `false` | Opt-in: use the fingerprint for login + sudo (PAM) |
| `nomarchy.hardware.npu.enable` | `false` | Opt-in/experimental: load the on-die NPU driver (`amdxdna`/`intel_vpu`); userspace runtime is BYO |
| `nomarchy.hardware.latestKernel` | `false` | Opt-in: ship `linuxPackages_latest` instead of the default kernel — for very new hardware whose drivers landed recently |
| `nomarchy.services.tailscale.enable` | `false` | Opt-in: Tailscale mesh VPN (then `sudo tailscale up`) | | `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.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.podman.enable` | `false` | Opt-in: rootless Podman (`docker` aliased to it) |

View File

@@ -51,6 +51,11 @@ how to override it. Items marked ✓ are shipped.
one-off — any new feature that earns a menu entry must be placed in the 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 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. direct `SUPER+CTRL+<mnemonic>` bind and self-gating as applicable.
-**Back everywhere:** every list menu now ends with a `↩ Back` entry that
returns one level up (power/theme/power-profile/clipboard/files/capture,
not just Tools/System), so you never have to Esc out and reopen. A `back`
helper + a shared `BACK` label keep it uniform; matched exactly so it can't
collide with clipboard/filename content. Esc still quits instantly.
-**Menu modules from rofi plugins:** the old `calc` flow committed the -**Menu modules from rofi plugins:** the old `calc` flow committed the
expression blind (result only in the *next* menu's `-mesg`) and `qalc -t` 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- misparsed common phrasings (`15% of 200``rem(15, 1 B)`, the natural-
@@ -67,25 +72,104 @@ how to override it. Items marked ✓ are shipped.
launcher, and yazi (SUPER+E) already covers real browsing. launcher, and yazi (SUPER+E) already covers real browsing.
- **More menu modules from rofi tools:** the script-based counterparts - **More menu modules from rofi tools:** the script-based counterparts
(run via `rofi -dmenu`, like the hand-rolled modules), each a deliberate (run via `rofi -dmenu`, like the hand-rolled modules), each a deliberate
replacement of an existing flow: **rofi-network-manager** (a keyboard replacement of an existing flow.
wifi/VPN picker vs today's `nmtui`-in-terminal `network`), **rofi-rbw / -**Network** (`networkmanager_dmenu`): a native rofi wifi/VPN picker
rofi-pass** (a secrets module — Bitwarden via rbw, or `pass` — pairs with replacing the old `nmtui`-in-terminal `network` flow. Configured (xdg
`keys.nix`), and **rofi-pulse-select** (an audio sink/source switcher). `networkmanager-dmenu/config.ini`) to drive rofi with `rofi_highlight`
Decide per-module whether it earns replacing the current path. for connected/available rows; editing a connection drops to nmtui in the
terminal. Inherits the generated theme like every other module.
-**Audio** (`rofi-pulse-select`): a PipeWire/pulse sink/source switcher
under System → Audio (Output/Input), self-gated on the pulse socket.
- Deferred: **rofi-rbw / rofi-pass** secrets module — no Bitwarden/`pass`
setup here today (secrets are gpg/ssh + gnome-keyring, see `keys.nix`),
so it'd mean adopting a new secret manager. Revisit if that changes.
- Also: menu search is now **case-insensitive fuzzy** (`matching = fuzzy`,
`sorting-method = fzf`) across every module + the launcher.
-**Printer setup menu module:** a `nomarchy-menu printers` entry in the
**System** submenu opens **system-config-printer** (the CUPS admin GUI —
discovery, drivers/PPDs, options, test page), mirroring the bluetooth/blueman
pattern: the printing service ships the package (`environment.systemPackages`)
and the menu execs it, self-gated on the `system-config-printer` binary so
the entry appears only when `nomarchy.services.printing` is on. No direct
`SUPER+CTRL` bind — printer setup is a rare one-off, not a frequent utility
(left out deliberately; easy to add). Chose the GUI over a rofi-native
`lpadmin` flow (driver/PPD picking is impractical in rofi) and the CUPS web
UI (an unthemed browser page). Validated: the generated menu script's
`bash -n` build check passes, and an eval confirms printing-on puts
system-config-printer in `systemPackages`. Pending an on-machine check.
- **Theme parity with legacy:** summer-day/night now carry their legacy - **Theme parity with legacy:** summer-day/night now carry their legacy
bar layouts as `waybar.jsonc` whole-swaps (adapted: dead legacy script bar layouts as `waybar.jsonc` whole-swaps (adapted: dead legacy script
modules dropped, Nerd-Fonts-v2 codepoints remapped to FontAwesome/v3, modules dropped, Nerd-Fonts-v2 codepoints remapped to FontAwesome/v3,
logo button opens nomarchy-menu); the other four identity themes are logo button opens nomarchy-menu); the other four identity themes are
palette recolors and already match. Remaining: a visual pass over all palette recolors and already match. Remaining: a visual pass over all
six on the live ISO six on the live ISO
- **Per-theme rofi identity:** the `themes/<slug>/rofi.rasi` whole-swap - **Per-theme rofi identity:** the `themes/<slug>/rofi.rasi` whole-swap
ships, and summer-day/night carry their legacy designs (inverted window, ships, and all six identity themes now carry a designed `.rasi`. summer-day/
green inputbar, yellow bottom-border). Remaining: author `.rasi` night keep their legacy ports (inverted window, green inputbar, yellow
identities for the other four ported themes if/when they want one (the bottom-border); the other four were authored from each theme's character
generated palette theme is the default and looks fine) (no legacy layout to port — their waybar whole-swaps are palette-only):
- **Faster switches:** move `backgrounds/` out of the flake source (the 86 MB **nord** a soft rounded "frost panel" (frost border, brighter-frost
re-copy on every state write is the main eval tax), then pre-built theme selection, aurora-purple prompt); **retro-82** a sharp CRT terminal (square
variants if still needed corners, amber-on-navy, a teal scanline underline, mono); **lumon** a
clinical cyan "bezel" (thick cyan frame, a *framed*-not-filled readout
inputbar, mono); **kanagawa** ink-and-paper (warm washi-paper frame, a
deeper ink-well inputbar, crystal-blue wave reserved for the selection).
Each is self-contained (it replaces the generated theme) and keeps the
element structure the theme-grid picker's per-invocation `-theme-str`
layers onto; all four parse clean under `rofi -dump-theme`. The generated
palette theme stays the default for the other 15 presets. Remaining: a
visual pass over the four on hardware (the parse check confirms syntax, not
aesthetics).
-**Visual theme picker (preview thumbnails):** `nomarchy-menu theme` is now
a rofi **icon grid of real desktop previews** instead of a plain-text list —
each theme a screenshot of its themed desktop (waybar + floating terminal),
pretty name beneath, **grouped dark-first then light** in one scrollable grid
(the previews make the mode obvious, so no light/dark submenu split), the
active theme marked `✓`, ending in `↩ Back`. The grid, the Name→slug map and
the active mark are all **generated at eval time** from the preset JSONs in
`rofi.nix` (`builtins.readDir` + `fromJSON`); "active" is just `t.slug`, since
every switch rebuilds the menu. Grid layout via a per-invocation `-theme-str`
(`listview { columns: 3; flow: horizontal; }` so Down scrolls row-by-row, +
vertical `element` cards, name centred below). **Sizing gotcha (learned the
hard way):** rofi's `element-icon` `size` is a **single value → a square
cell** (a two-value `WxH` is silently collapsed), and the icon is *contained*
in that square. So a 16:9 preview letterboxes (theme-coloured bands top/
bottom), and a cell can't be shorter-than-square without the bands returning.
The fix: a build-time imagemagick step **centre-crops each preview to a
square** so it fills the cell edge-to-edge; the window width is derived so a
column is exactly the icon side (no slack margins). One knob, `themeGridIconW`
(240px), drives both icon and window. **Workflow:** Bernardo captures previews
on real hardware and commits them as `themes/<slug>/preview.png`, **already
downscaled to 480×270** (~2.4 MB total for all 21, vs ~28 MB full-res) — the
source stays 16:9 and untouched; only the *displayed* thumb is squared at
build, so the crop is reversible. Graceful fallback when a theme has no
`preview.png`: a plain-name row, so it degrades cleanly.
(A headless VM-render route — `runNixOSTest` + software-GL Hyprland
(`LIBGL_ALWAYS_SOFTWARE` on virtio-gpu) + `machine.screenshot()` QMP
framebuffer dump — was prototyped 2026-06-19 and **works** (themed waybar
rendered + captured); real-hardware capture was chosen for fidelity +
simplicity, with the VM route as a documented fallback if hand-capturing all
themes gets tedious.)
- **Faster switches:** move `backgrounds/` out of the flake source. Diagnosis
(confirmed): `themes/` is 86 MB and `backgrounds/` is **all** of it — the
palette JSONs + per-theme overrides are only ~208 KB, and the wallpapers are
**never read at Nix eval** (only the Python `swww` path uses them). But
`theme-state.json` is git-tracked, so every `apply` rewrites it → the flake
tree changes → Nix re-copies the whole 86 MB source before `home-manager
switch` can evaluate. You pay an 86 MB copy to change a 1 KB file.
**Decided approach (deferred — don't want the extra moving part yet):**
Option 1, a **separate pinned wallpapers artifact** — a `Nomarchy-wallpapers`
repo or release tarball, pulled once via a flake input / `fetchurl` (pinned
by hash → content-addressed, never re-copied on a state write), with
`nomarchy-theme-sync` reading wallpapers from that stable store path (the
`NOMARCHY_DEFAULT_THEMES` env hook already anticipates external theme
assets). Keeps eval **pure**; the live ISO still bakes them in (fetched at
build). A state write then re-copies only ~208 KB. Rejected alternative:
moving `theme-state.json` out to `~/.config` + `--impure` eval (one repo, no
second artifact, but trades away the in-tree-pinned-state reproducibility).
Follow-on if `home-manager switch` itself is still the bottleneck after the
copy is gone: **pre-built theme variants** (build each theme's generation
ahead of time so a switch just activates a cached one).
- Greeter (tuigreet/SDDM) theming from the same JSON (Plymouth ships since - Greeter (tuigreet/SDDM) theming from the same JSON (Plymouth ships since
v1: `nomarchy.system.plymouth.*`, background tinted from the state file) v1: `nomarchy.system.plymouth.*`, background tinted from the state file)
- Installer round 2: multi-disk BTRFS RAID, impermanence, BIOS/legacy - Installer round 2: multi-disk BTRFS RAID, impermanence, BIOS/legacy
@@ -263,6 +347,31 @@ how to override it. Items marked ✓ are shipped.
Complements nixos-hardware (model quirks) rather than replacing it; keep Complements nixos-hardware (model quirks) rather than replacing it; keep
the detection in the installer's `hardware-db` so an installed machine the detection in the installer's `hardware-db` so an installed machine
bakes the right defaults, all overridable through `nomarchy.hardware.*`. bakes the right defaults, all overridable through `nomarchy.hardware.*`.
-**Mechanism + broad seed shipped** (`modules/nixos/hardware.nix`): a
vendor-keyed `nomarchy.hardware.*` surface (`intel`, `amd`, `fingerprint`,
`npu`), `hardware-db.sh` extended to detect Intel/AMD, a fingerprint reader
(libfprint USB vendor IDs) and an NPU (Intel VPU / AMD XDNA PCI IDs), and
the installer bakes the matching toggles into `system.nix` — safe defaults
active (GuC/HuC, amd-pstate, AMD VA-API env, fprintd), heavy/experimental
opt-ins commented (Intel compute-runtime, ROCm, fingerprint PAM, NPU
driver). Audited against the nixos-hardware commons so it only fills the
gap above them — microcode / the media-driver VA-API stack / weekly fstrim
already come from `common-cpu-*` / `common-gpu-*` / `common-pc-ssd`.
Exercised live on the dev machine's detection (an AMD Ryzen-AI laptop: AMD
+ fingerprint + NPU all detected) and a toggles-on build (`amd_pstate=active`
+ `i915.enable_guc=3` in kernel-params, `fprintd.service` present).
**Remaining: on-hardware verification of the runtime bits (AMD/NPU/Intel
GPU-compute) — none are testable in CI or on the Intel Latitudes for the
AMD paths.** SSD TRIM is intentionally left to `common-pc-ssd`.
-**Hardening (driver-gen + kernel awareness):** `intel.guc` emits the
*i915* param, so the installer turns it off when the GPU is on the newer
`xe` driver (Lunar Lake / Battlemage / Panther Lake — GuC is default-on
there); the NPU detector matches the PCI *accelerator class* (not just a
device-ID list) so new gens are caught without a code change; and a
`nomarchy.hardware.latestKernel` escape hatch (+ a build-time warning when
`npu.enable` predates the shipped kernel) covers very-new hardware whose
drivers only just landed. A config-assertion VM test guards the wiring
(`checks.hardware-toggles`: kernel cmdline + fprintd + PAM, booted in a VM).
Worked example — ThinkPad T14s (Ryzen 7 PRO 7840U + Radeon 780M): Worked example — ThinkPad T14s (Ryzen 7 PRO 7840U + Radeon 780M):
- **GPU compute — ROCm:** the 780M is an RDNA3 iGPU (gfx1103) ROCm doesn't - **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 officially list, so it needs `HSA_OVERRIDE_GFX_VERSION=11.0.0`. Multi-GB
@@ -353,10 +462,26 @@ how to override it. Items marked ✓ are shipped.
warm at night, identity (no shift) by day. `modules/home/nightlight.nix` warm at night, identity (no shift) by day. `modules/home/nightlight.nix`
drives the HM `services.hyprsunset` with two time-based profiles drives the HM `services.hyprsunset` with two time-based profiles
(`sunrise` → identity, `sunset``temperature`), so hyprsunset handles the (`sunrise` → identity, `sunset``temperature`), so hyprsunset handles the
schedule and the on-login state. Needs an on-hardware check that hyprsunset schedule and the on-login state. Active-profile-at-session-start was
applies the active profile at session start. Remaining (optional): a menu + confirmed on hardware (2026-06-18). ✓ **Menu + Waybar toggle:**
Waybar toggle to force it on/off, and geo (lat/long) auto sunset/sunrise `nomarchy-nightlight` (in `nightlight.nix`, on PATH) starts/stops the
(would mean wlsunset, which schedules by location). hyprsunset *service*`systemctl --user is-active` is the single source of
truth, the screen restores to true colours when stopped, and it forces the
filter off for colour-sensitive work and back on without touching config. A
`nomarchy-menu nightlight` entry (System submenu, self-gated on the unit
existing) and a self-gating Waybar `custom/nightlight` indicator (moon = on,
sun = off; in the generated bar and both summer whole-swaps) drive it.
**Persistent off:** a manual `off` now drops a marker
(`~/.local/state/nomarchy/nightlight-off`) and the unit refuses to (re)start
while it exists (`Unit.ConditionPathExists=!%h/…`), so an off survives
logout/reboot until toggled back on; `on`/toggle-on remove the marker first
(a failed condition would otherwise no-op the start). Without the marker the
service stays `WantedBy=graphical-session.target` and resumes its schedule at
login as before — the session-scoped behaviour is now the *default*, with
persistence opt-in via the toggle. Remaining (optional): geo (lat/long) auto
sunset/sunrise (would mean wlsunset, which schedules by location). Pending an
on-machine check (off persisting across reboot, and that stopping hyprsunset
cleanly restores the gamma).
- **Keyboard layouts (per-device + switching):** - **Keyboard layouts (per-device + switching):**
-**Per-device declarative layout:** `nomarchy.keyboard.devices` -**Per-device declarative layout:** `nomarchy.keyboard.devices`
(`{ "<hyprctl-device-name>" = { layout; variant; }; }`) generates Hyprland (`{ "<hyprctl-device-name>" = { layout; variant; }; }`) generates Hyprland
@@ -372,15 +497,20 @@ how to override it. Items marked ✓ are shipped.
watch` daemon runs (exec-once): it polls `hyprctl devices`, and when a watch` daemon runs (exec-once): it polls `hyprctl devices`, and when a
keyboard connects *after* login that isn't in `keyboard.devices` and keyboard connects *after* login that isn't in `keyboard.devices` and
hasn't been chosen before, it pops a rofi layout picker, applies the hasn't been chosen before, it pops a rofi layout picker, applies the
choice with `hyprctl switchxkblayout` (an index into the candidate set, choice as a **per-device** `hyprctl keyword device[<name>]:kb_layout` (the
which `input.kb_layout` carries), and remembers it per-device in runtime twin of the declarative `device{}` blocks — so it isolates that
`~/.local/state/nomarchy/keyboard-layouts` — re-applied silently on later one keyboard and never touches the built-in board), and remembers it
reconnects. The boot-time set (incl. the built-in keyboard) is never per-device in `~/.local/state/nomarchy/keyboard-layouts` — re-applied
prompted. The runtime-remember complement to the declarative silently on later reconnects. The boot-time set (incl. the built-in
`keyboard.devices`; a stateful runtime piece by design. **Pending an keyboard) is never prompted. The runtime-remember complement to the
on-hardware test** (hotplug isn't verifiable in CI). Remaining (bonus): declarative `keyboard.devices`; a stateful runtime piece by design.
offer to write a choice into `keyboard.devices` so it graduates to the **Picker verified on hardware (2026-06-18)**; the first cut applied the
reproducible config. choice with `switchxkblayout` (a global layout-index flip) and merged the
candidate pool into `input.kb_layout`, so a selection leaked onto the
laptop keyboard and stuck after unplug — fixed by the per-device keyword +
keeping the pool out of the session layout. Remaining (bonus): offer to
write a choice into `keyboard.devices` so it graduates to the reproducible
config; **re-verify the per-device isolation on hardware**.
- Remaining: a multi-layout cycle bind (`hyprctl switchxkblayout` / xkb - Remaining: a multi-layout cycle bind (`hyprctl switchxkblayout` / xkb
`grp:` options) for switching layouts on one keyboard; add the `grp:` options) for switching layouts on one keyboard; add the
`hyprland/language` module to the summer whole-swap themes for parity `hyprland/language` module to the summer whole-swap themes for parity
@@ -410,15 +540,57 @@ how to override it. Items marked ✓ are shipped.
artifact and will crash on hardware too). The menu wiring is correct and 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 app is installed, but the GUI doesn't open. The snapshot *backend*
(the actual snapshots) is unaffected. Fix path: a nixpkgs override aligning (the actual snapshots) is unaffected. Fix path: a nixpkgs override aligning
its libbtrfsutil, or wait for an upstream bump; meanwhile the menu entry its libbtrfsutil, or wait for an upstream bump. ✓ **Fallback shipped:**
is a no-op when the binary crashes. Fallback if it lingers: a rofi-based the menu now launches `nomarchy-snapshots` (system-side, gated on
snapshot menu (no btrfs-assistant dependency). `nomarchy.system.snapper`) instead — a keyboard-driven snapper
- Remaining (optional): a keyboard-driven rofi browse for quick glances; browser/restore with no btrfs-assistant dependency. snapper is root-only
boot-from-snapshot needs a systemd-boot equivalent of grub-btrfs. here (no `ALLOW_USERS`) and rofi can't run as root under Wayland, so it
- **Update awareness:** updates are manual today (`sys-update`/`home-update`); runs in a terminal via `sudo` (one password prompt) and uses `fzf` to pick:
add a Waybar indicator / notification when flake inputs are stale or a new browse/diff (read-only), restore files (`undochange`), or roll back (root
nixpkgs rev is available (optionally with a pending-change count). Augments, config only) — each behind a typed-`yes` confirmation, which is *safer*
never replaces, the explicit rebuild flow. than the fat-fingerable one-click rofi rollback this entry originally
avoided. btrfs-assistant stays installed for when nixpkgs fixes it.
**Pending an on-machine check of the restore/rollback paths.**
- Remaining (optional): boot-from-snapshot needs a systemd-boot equivalent
of grub-btrfs.
-**Update awareness:** `nomarchy.updates.enable` (opt-in, `modules/home/
updates.nix`) — a `nomarchy-updates` checker on a systemd user timer
(`.interval`, default daily) compares each **direct** branch-tracking flake
input (nixpkgs, the Nomarchy input, home-manager, stylix … via `git
ls-remote` vs the locked rev; offline → skipped, no false alarm) and, when
the `flatpak` CLI is present (`.flatpak`), counts Flatpak updates. A
self-gating Waybar `custom/updates` indicator (hidden until something's
available; accent `󰚰 N`, signal-refreshed) + a notification that fires only
when the count *grows* (so a daily timer never nags). Click → the upgrade
flow in a terminal (`sys-update` / `flatpak update`, each confirmed). Purely
passive — it never changes anything, augmenting the explicit rebuild flow.
Indicator is in the generated bar and both summer whole-swaps. **Pending an
on-machine check** (the ls-remote/notify/timer path needs a live session).
- **Automated upstream lock bumps (maintainer CI):** the upstream twin of the
user-side checker above — automate advancing Nomarchy's *own* `flake.lock`,
which is the entire delivery channel (a downstream takes a single input,
`?ref=v1`, so nixpkgs/home-manager/stylix/nixos-hardware reach it
*transitively pinned* through that lock; users can't bump them independently).
**Tiered by what CI can't do:** the hardware-QA gate (the Latitude 5410 + the
AMD dev box — AMD/NPU/Intel-GPU-compute runtime bits aren't testable in CI or
on the Intel Latitudes for the AMD paths) can't be automated, so a bot drives
`main` and a human still promotes `main → v1`. A scheduled job (Forgejo
Actions, or Renovate's Nix manager opening a reviewable lock-diff PR) runs
`nix flake update` → `nix flake check` + the existing `runNixOSTest` suite +
builds `system.build.toplevel`, and on green lands on `main` — which nobody
tracks, so a bad bump costs a debug session, not a user outage; `v1` stays
the manual on-hardware gate (it only ever fast-forwards, never force-pushed,
or a user's locked rev can vanish from history). **Cadence weekly**
(release-26.05 doesn't move fast; nightly is just churn), and `nix flake
update` stays *within* the pinned release branches — no surprise major bump
(a 26.11 jump is a deliberate `v2`, hand-edited in flake.nix). Side benefit:
shrinks the security-patch latency the single-input model otherwise imposes
(users are gated on the maintainer for nixpkgs CVEs), since a fix is usually
already eval/build/VM-tested and one hardware promotion away from `v1` —
worth fast-laning lock-only/security bumps ahead of feature batches. The repo
has **no CI today** (manual `nix flake update` only). Explicitly *not* a
binary cache: compile-from-source is a deliberate values call (Gentoo-style),
so this automates the *config/lock* channel, not artifact distribution.
- **"nomarchy" control center:** a single TUI/GUI front-end over the common - **"nomarchy" control center:** a single TUI/GUI front-end over the common
toggles — theme, power profile, opt-in services, display, DND — built on toggles — theme, power profile, opt-in services, display, DND — built on
the same `nomarchy-theme-sync` / `nomarchy.*` surface the menu already the same `nomarchy-theme-sync` / `nomarchy.*` surface the menu already

View File

@@ -134,6 +134,31 @@
downstream.nixosConfigurations.default.config.system.build.toplevel; downstream.nixosConfigurations.default.config.system.build.toplevel;
downstream-template-home = downstream-template-home =
downstream.homeConfigurations.me.activationPackage; downstream.homeConfigurations.me.activationPackage;
# Config-level VM assertions for the nomarchy.hardware.* toggles —
# what they SET (kernel cmdline, fprintd, PAM). Real hardware
# behaviour (firmware/driver/device) needs bare metal and is out of
# scope here. Imports only hardware.nix, so the VM stays minimal.
hardware-toggles = pkgs.testers.runNixOSTest {
name = "nomarchy-hardware-toggles";
nodes.machine = { ... }: {
imports = [ ./modules/nixos/hardware.nix ];
nomarchy.hardware = {
intel.enable = true;
amd.enable = true;
fingerprint = { enable = true; pam = true; };
npu.enable = true;
};
};
testScript = ''
machine.wait_for_unit("multi-user.target")
cmdline = machine.succeed("cat /proc/cmdline")
assert "amd_pstate=active" in cmdline, cmdline
assert "i915.enable_guc=3" in cmdline, cmdline
machine.succeed("systemctl cat fprintd.service")
machine.succeed("grep -q pam_fprintd /etc/pam.d/sudo")
'';
};
}; };
# ─── Reference host ──────────────────────────────────────────── # ─── Reference host ────────────────────────────────────────────

View File

@@ -1,7 +1,7 @@
# Nomarchy — Home Manager entry point. # Nomarchy — Home Manager entry point.
# Consume this via homeModules.nomarchy (flake.nix), which also pulls in # Consume this via homeModules.nomarchy (flake.nix), which also pulls in
# the stylix home module that stylix.nix configures. # the stylix home module that stylix.nix configures.
{ config, pkgs, ... }: { config, lib, pkgs, ... }:
{ {
imports = [ imports = [
@@ -18,6 +18,7 @@
./yazi.nix # flagship TUI file manager, themed + plugins ./yazi.nix # flagship TUI file manager, themed + plugins
./osd.nix # swayosd volume/brightness OSD, themed ./osd.nix # swayosd volume/brightness OSD, themed
./nightlight.nix # scheduled blue-light filter (hyprsunset), opt-in ./nightlight.nix # scheduled blue-light filter (hyprsunset), opt-in
./updates.nix # passive update-awareness indicator + notification, opt-in
./shell.nix # zsh + starship + bat/eza/zoxide, themed ./shell.nix # zsh + starship + bat/eza/zoxide, themed
./keys.nix # gpg-agent (fronts SSH) + pinentry-qt ./keys.nix # gpg-agent (fronts SSH) + pinentry-qt
./fastfetch.nix # system info with the themed Nomarchy logo ./fastfetch.nix # system info with the themed Nomarchy logo
@@ -32,6 +33,16 @@
services.network-manager-applet.enable = true; services.network-manager-applet.enable = true;
xsession.preferStatusNotifierItems = true; xsession.preferStatusNotifierItems = true;
# Standard XDG user directories (Downloads, Documents, Pictures, Music,
# Videos, Desktop, Public, Templates): written to user-dirs.dirs so file
# pickers/browsers resolve them, and created on activation so a fresh
# install lands with them present (not just on first app use). mkDefault
# so a downstream home.nix can flip it off or remap individual paths.
xdg.userDirs = {
enable = lib.mkDefault true;
createDirectories = lib.mkDefault true;
};
home.stateVersion = "26.05"; home.stateVersion = "26.05";
home.packages = with pkgs; [ home.packages = with pkgs; [

View File

@@ -55,10 +55,14 @@ let
++ lib.optional (m.extra != "") m.extra ++ lib.optional (m.extra != "") m.extra
); );
# Keyboard layout candidates: the primary nomarchy.keyboard.layout (split on # Candidate layouts offered by the new-keyboard picker: the session
# commas) plus any nomarchy.keyboard.layouts. Drives input.kb_layout so all # layout(s) (nomarchy.keyboard.layout, comma-split for a multi-layout
# of them are switchable, and feeds the watcher's picker. # session) plus the extra nomarchy.keyboard.layouts pool. The pool is
kbLayouts = lib.unique ( # deliberately NOT merged into input.kb_layout — those candidates are for
# *external* keyboards and get applied per-device by the watcher's apply().
# Loading them onto the session keyboard is what let a global switch flip
# the built-in board to the wrong layout.
pickerLayouts = lib.unique (
(lib.splitString "," config.nomarchy.keyboard.layout) (lib.splitString "," config.nomarchy.keyboard.layout)
++ config.nomarchy.keyboard.layouts ++ config.nomarchy.keyboard.layouts
); );
@@ -68,18 +72,20 @@ let
# keyboard.devices, not already remembered), ask for a layout and persist it # keyboard.devices, not already remembered), ask for a layout and persist it
# per-device, re-applying silently on later reconnects. Stateful runtime # per-device, re-applying silently on later reconnects. Stateful runtime
# piece — the complement to the declarative keyboard.devices. Reliable # piece — the complement to the declarative keyboard.devices. Reliable
# primitives only: poll hyprctl devices, apply via switchxkblayout (an index # primitives only: poll hyprctl devices, apply with a per-device
# into kbLayouts). NOTE: needs an on-hardware test (hotplug isn't verifiable # `device[<name>]:kb_layout` keyword — the runtime twin of the declarative
# in CI). # device blocks, so it isolates that one keyboard and never disturbs the
# built-in board (switchxkblayout flipped the *shared* layout, which leaked
# onto the laptop and stuck after unplug). NOTE: needs an on-hardware test
# (hotplug isn't verifiable in CI).
keyboardWatch = pkgs.writeShellScriptBin "nomarchy-keyboard-watch" '' keyboardWatch = pkgs.writeShellScriptBin "nomarchy-keyboard-watch" ''
set -u set -u
layouts="${lib.concatStringsSep " " kbLayouts}" layouts="${lib.concatStringsSep " " pickerLayouts}"
declared="${lib.concatStringsSep " " (builtins.attrNames config.nomarchy.keyboard.devices)}" declared="${lib.concatStringsSep " " (builtins.attrNames config.nomarchy.keyboard.devices)}"
state="''${XDG_STATE_HOME:-$HOME/.local/state}/nomarchy/keyboard-layouts" state="''${XDG_STATE_HOME:-$HOME/.local/state}/nomarchy/keyboard-layouts"
mkdir -p "$(dirname "$state")"; [ -f "$state" ] || : > "$state" 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() { hyprctl keyword "device[$1]:kb_layout" "$2" >/dev/null 2>&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'; } keyboards() { hyprctl devices -j | jq -r '.keyboards[].name'; }
is_declared() { case " $declared " in *" $1 "*) return 0 ;; *) return 1 ;; esac; } 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"; } saved_for() { while IFS='=' read -r k v; do [ "$k" = "$1" ] && { printf '%s' "$v"; return; }; done < "$state"; }
@@ -146,7 +152,13 @@ in
# Paint the wallpaper as soon as the session is up (waits for # Paint the wallpaper as soon as the session is up (waits for
# the daemon internally). # the daemon internally).
"nomarchy-theme-sync wallpaper" "nomarchy-theme-sync wallpaper"
] ++ lib.optional kbAutoSwitch "${keyboardWatch}/bin/nomarchy-keyboard-watch"; # Waybar is launched here rather than as a systemd user service: bound
# to graphical-session.target the unit raced Hyprland's IPC on relogin
# and landed in `failed`; exec-once only fires once the compositor is
# up. Reloaded on theme switch via SIGUSR2 (nomarchy-theme-sync). See
# waybar.nix.
] ++ lib.optional config.nomarchy.waybar.enable "waybar"
++ lib.optional kbAutoSwitch "${keyboardWatch}/bin/nomarchy-keyboard-watch";
# ── Theme-driven look ────────────────────────────────────────── # ── Theme-driven look ──────────────────────────────────────────
# These flow from theme-state.json and stay at NORMAL priority: # These flow from theme-state.json and stay at NORMAL priority:
@@ -201,8 +213,10 @@ in
input = { input = {
# kb_layout/variant come from nomarchy.keyboard.* — set that # kb_layout/variant come from nomarchy.keyboard.* — set that
# option (the installer writes it; it also drives tty/LUKS). # option (the installer writes it; it also drives tty/LUKS). Only the
kb_layout = lib.concatStringsSep "," kbLayouts; # session layout(s) land here; the keyboard.layouts pool is applied
# per-device to external boards, never onto the session keyboard.
kb_layout = config.nomarchy.keyboard.layout;
kb_variant = config.nomarchy.keyboard.variant; kb_variant = config.nomarchy.keyboard.variant;
follow_mouse = lib.mkDefault 1; follow_mouse = lib.mkDefault 1;
touchpad.natural_scroll = lib.mkDefault true; touchpad.natural_scroll = lib.mkDefault true;
@@ -270,7 +284,10 @@ in
# writes a config file). A helper to find values for nomarchy.monitors — # 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 # the declarative config stays the source of truth (we don't source its
# output). Gated on its toggle; pointless without the Hyprland session. # output). Gated on its toggle; pointless without the Hyprland session.
home.packages = lib.optionals home.packages =
(config.nomarchy.hyprland.enable && config.nomarchy.displays.enable) lib.optionals (config.nomarchy.hyprland.enable && config.nomarchy.displays.enable)
[ pkgs.nwg-displays ]; [ pkgs.nwg-displays ]
# The new-keyboard watcher on PATH (when enabled) so it's discoverable and
# runnable by name for debugging — Hyprland still exec-once's it by store path.
++ lib.optional (config.nomarchy.hyprland.enable && kbAutoSwitch) keyboardWatch;
} }

View File

@@ -5,14 +5,57 @@
# #
# The hyprsunset HM service module is provided by home-manager; this only # The hyprsunset HM service module is provided by home-manager; this only
# configures it. Override anything with plain services.hyprsunset.* options. # configures it. Override anything with plain services.hyprsunset.* options.
{ config, lib, ... }: #
# nomarchy-nightlight is the menu/Waybar toggle: it starts/stops the hyprsunset
# *service* (so `systemctl --user is-active` is the single source of truth and
# the screen restores to true colours when stopped), letting you force the
# filter off for colour-sensitive work and back on without touching the config.
{ config, lib, pkgs, ... }:
let let
cfg = config.nomarchy.nightlight; cfg = config.nomarchy.nightlight;
# Toggle/status helper, on PATH so the menu and both the generated and
# whole-swap Waybars can drive it by name. Self-gates: when the unit doesn't
# exist (night-light not enabled) `status` prints nothing, so the Waybar
# module hides itself — same pattern as the power-profile indicator.
nomarchy-nightlight = pkgs.writeShellScriptBin "nomarchy-nightlight" ''
unit=hyprsunset.service
# Persistent-off marker mirrors the unit's ConditionPathExists below.
# Present => night-light stays off across logout/reboot until toggled on.
# Path is literal (not XDG_STATE_HOME) so it matches the unit's %h check.
marker="$HOME/.local/state/nomarchy/nightlight-off"
mark_off() { mkdir -p "$(dirname "$marker")"; : > "$marker"; }
mark_on() { rm -f "$marker"; }
if ! systemctl --user cat "$unit" >/dev/null 2>&1; then
[ "''${1:-}" = status ] && exit 0
notify-send "Night light" "Not enabled set nomarchy.nightlight.enable."
exit 0
fi
case "''${1:-toggle}" in
status)
if systemctl --user is-active --quiet "$unit"; then
printf '{"text":"󰖔","tooltip":"Night light on warm on schedule (click to disable)","class":"on"}\n'
else
printf '{"text":"󰖙","tooltip":"Night light off (click to enable)","class":"off"}\n'
fi ;;
on) mark_on; systemctl --user start "$unit" ;;
off) mark_off; systemctl --user stop "$unit" ;;
toggle)
if systemctl --user is-active --quiet "$unit"; then
mark_off; systemctl --user stop "$unit"
notify-send "Night light off" "Screen back to true colours (stays off until re-enabled)."
else
mark_on; systemctl --user start "$unit"
notify-send "Night light on" "Warm filter follows your schedule."
fi ;;
*) echo "usage: nomarchy-nightlight [toggle|status|on|off]" >&2; exit 64 ;;
esac
'';
in in
{ {
config = lib.mkIf cfg.enable { config = {
services.hyprsunset = { services.hyprsunset = lib.mkIf cfg.enable {
enable = true; enable = true;
settings.profile = [ settings.profile = [
# Daytime: identity = no colour change. # Daytime: identity = no colour change.
@@ -21,5 +64,16 @@ in
{ time = cfg.sunset; temperature = cfg.temperature; } { time = cfg.sunset; temperature = cfg.temperature; }
]; ];
}; };
# Persistent off: a manual `off` drops ~/.local/state/nomarchy/nightlight-off,
# and this Condition makes the unit refuse to (re)start while it exists — so
# an off survives logout/reboot. `nomarchy-nightlight on` removes it first
# (a failed condition would otherwise no-op the start). %h = $HOME.
systemd.user.services.hyprsunset.Unit.ConditionPathExists =
lib.mkIf cfg.enable "!%h/.local/state/nomarchy/nightlight-off";
# Always on PATH (it self-gates at runtime), so a static whole-swap Waybar
# can exec it even when night-light is off without the call failing.
home.packages = [ nomarchy-nightlight ];
}; };
} }

View File

@@ -105,10 +105,11 @@ in
picker. When non-empty, a small watcher runs in the session: when a picker. When non-empty, a small watcher runs in the session: when a
keyboard connects that isn't covered by nomarchy.keyboard.devices and keyboard connects that isn't covered by nomarchy.keyboard.devices and
hasn't been chosen before, it pops a rofi picker (these layouts plus hasn't been chosen before, it pops a rofi picker (these layouts plus
the primary nomarchy.keyboard.layout), applies the choice, and the primary nomarchy.keyboard.layout), applies the choice to that
remembers it per-device (~/.local/state) re-applying automatically keyboard only (a per-device kb_layout, so the built-in board is left
on later reconnects. The runtime-remember complement to the alone), and remembers it per-device (~/.local/state) re-applying
declarative keyboard.devices; a stateful runtime piece by design. automatically on later reconnects. The runtime-remember complement to
the declarative keyboard.devices; a stateful runtime piece by design.
''; '';
}; };
@@ -184,6 +185,32 @@ in
}; };
}; };
updates = {
enable = lib.mkEnableOption ''
passive update awareness: a background check (systemd user timer) that
compares the flake's locked inputs (nixpkgs, the Nomarchy input, )
against upstream and when Flatpak is enabled counts Flatpak
updates, surfacing a Waybar indicator + a notification when something
is available. It never changes anything; you still run sys-update /
home-update / flatpak update yourself'';
interval = lib.mkOption {
type = lib.types.str;
default = "daily";
example = "6h";
description = "How often to check, as a systemd OnCalendar expression.";
};
flatpak = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Also count available Flatpak updates when the `flatpak` CLI is
present (i.e. nomarchy.services.flatpak is on). No effect otherwise.
'';
};
};
monitors = lib.mkOption { monitors = lib.mkOption {
type = lib.types.listOf monitorType; type = lib.types.listOf monitorType;
default = [ ]; default = [ ];

View File

@@ -19,6 +19,92 @@ let
rasiOverride = cfg.themesDir + "/${t.slug}/rofi.rasi"; rasiOverride = cfg.themesDir + "/${t.slug}/rofi.rasi";
hasRasiOverride = builtins.pathExists rasiOverride; hasRasiOverride = builtins.pathExists rasiOverride;
# ── Visual theme picker ──────────────────────────────────────────────
# Every preset (themes/<slug>.json) is read at eval time to build a grid
# of real desktop previews. Grouped dark-first then light — the previews
# make the mode obvious at a glance, so no light/dark submenu split — and
# the active theme is marked ✓. "Active" is just t.slug: every switch
# rebuilds this script, so the baked-in mark always tracks the live theme.
themeFiles = lib.filterAttrs
(n: ty: ty == "regular" && lib.hasSuffix ".json" n)
(builtins.readDir cfg.themesDir);
themeData = lib.mapAttrsToList (fname: _:
let
j = builtins.fromJSON (builtins.readFile (cfg.themesDir + "/${fname}"));
slug = j.slug or (lib.removeSuffix ".json" fname);
in {
inherit slug;
name = j.name or slug;
mode = j.mode or "dark";
hasPreview = builtins.pathExists (cfg.themesDir + "/${slug}/preview.png");
}) themeFiles;
byName = lib.sort (a: b: a.name < b.name);
orderedThemes =
byName (lib.filter (th: th.mode != "light") themeData) # dark group
++ byName (lib.filter (th: th.mode == "light") themeData); # then light
# rofi's element-icon cells are SQUARE, so a 16:9 preview would letterbox
# (theme-coloured bands top/bottom). Build-time crop each committed 480×270
# preview to a centred square so it fills the cell edge-to-edge. The source
# preview.png stays 480×270 (untouched) — only the displayed thumb is square.
themeThumbs = pkgs.runCommand "nomarchy-theme-thumbs"
{ nativeBuildInputs = [ pkgs.imagemagick ]; } ''
mkdir -p $out
${lib.concatMapStringsSep "\n"
(th: lib.optionalString th.hasPreview ''
magick ${cfg.themesDir + "/${th.slug}/preview.png"} \
-resize 360x360^ -gravity center -extent 360x360 -strip $out/${th.slug}.png
'')
orderedThemes}
'';
# One grid row per theme (square thumb + name, ✓ on the active one), and a
# Name→slug map so the picked label resolves back to the preset that
# nomarchy-theme-sync applies (pretty names ≠ slugs, hence the map). Themes
# with no preview degrade to a plain-name row.
themeRows = lib.concatMapStringsSep "\n" (th:
let label = th.name + lib.optionalString (th.slug == t.slug) " ";
in if th.hasPreview
then "row ${lib.escapeShellArg label} ${themeThumbs}/${th.slug}.png"
else "printf '%s\\n' ${lib.escapeShellArg label}")
orderedThemes;
themeSlugMap = lib.concatMapStringsSep "\n" (th:
" [${lib.escapeShellArg th.name}]=${lib.escapeShellArg th.slug}")
orderedThemes;
# Per-invocation grid layout (cards: a 16:9 preview above a centered name),
# overriding the single-column list theme just for this menu.
# · The icon box is sized 16:9 (rofi 2.0 takes a two-value `size`) to
# match the 480×270 previews exactly — they fill it with no letterbox.
# · The window is sized to the *content* (3 × icon + paddings), not a
# percentage, so a column is no wider than its card — otherwise the
# selection highlight balloons out around the image.
# · flow: horizontal lays cards out row-by-row (rofi's default Vertical
# fills column-by-column, so Down at a column's foot jumped to the top
# of the next column instead of scrolling the page).
# Grid dial — the previews are as big as the icon px allows; screen *height*
# caps it, so showing 2 rows (not 3) lets each card grow a lot. Scrolling
# reaches the rest. Tune themeGridIconW for size; the window resizes to fit.
# rofi's element-icon `size` is a SINGLE value (the manual only documents
# one) — a two-value "WxH" is silently collapsed, which is why the preview
# used to render tiny inside a too-wide column. So: size = the card *width*,
# rofi fits the 16:9 image tight to it (no square letterbox), and the window
# is sized so a column is exactly that width — the preview fills the cell.
themeGridCols = 3;
themeGridLines = 3;
themeGridIconW = 240; # square preview card side (px) — the size knob
themeGridPad = 0; # margin around the preview inside its cell (px)
themeGridGap = 8; # gap between cards (px)
themeGridThemeStr = lib.escapeShellArg (lib.concatStringsSep " " [
# window width = cards + their padding + inter-card gaps + chrome (~20px)
"window { width: ${toString (themeGridCols * (themeGridIconW + 2 * themeGridPad) + (themeGridCols - 1) * themeGridGap + 20)}px; }"
"listview { columns: ${toString themeGridCols}; lines: ${toString themeGridLines}; spacing: ${toString themeGridGap}px; flow: horizontal; }"
"element { orientation: vertical; padding: ${toString themeGridPad}px; spacing: 2px; }"
"element-icon { size: ${toString themeGridIconW}px; }"
"element-text { horizontal-align: 0.5; }"
]);
# Keybindings cheatsheet (SUPER+? → the `keybinds` menu module). Built # Keybindings cheatsheet (SUPER+? → the `keybinds` menu module). Built
# from the SAME ./keybinds.nix that hyprland.nix binds, so it can never # from the SAME ./keybinds.nix that hyprland.nix binds, so it can never
# drift from the live shortcuts. "$mod" reads as SUPER; rows are padded # drift from the live shortcuts. "$mod" reads as SUPER; rows are padded
@@ -64,6 +150,14 @@ let
# rofi has show-icons (on globally; passed explicitly on the icon menus). # rofi has show-icons (on globally; passed explicitly on the icon menus).
row() { printf '%s\0icon\x1f%s\n' "$1" "$2"; } row() { printf '%s\0icon\x1f%s\n' "$1" "$2"; }
# General menu convention: every list menu ends with a " Back" entry that
# returns one level up (so you never have to Esc out and reopen). `back`
# emits it with an icon; plain pick-lists append the bare label instead.
# Always matched EXACTLY (" Back") so it can't collide with clipboard or
# filename content. The arrow + go-previous icon read as "go back".
BACK=" Back"
back() { row "$BACK" go-previous; }
case "''${1:-}" in case "''${1:-}" in
power) power)
choice=$( { choice=$( {
@@ -73,8 +167,10 @@ let
row "Hibernate" system-hibernate row "Hibernate" system-hibernate
row "Reboot" system-reboot row "Reboot" system-reboot
row "Shutdown" system-shutdown row "Shutdown" system-shutdown
back
} | rofi -dmenu -show-icons -p Power) || exit 0 } | rofi -dmenu -show-icons -p Power) || exit 0
case "$choice" in case "$choice" in
"$BACK") exec "$0" ;;
*Lock) loginctl lock-session ;; *Lock) loginctl lock-session ;;
*Logout) hyprctl dispatch exit ;; *Logout) hyprctl dispatch exit ;;
*Suspend) systemctl suspend ;; *Suspend) systemctl suspend ;;
@@ -92,17 +188,30 @@ let
exit 0 exit 0
fi fi
cur=$(powerprofilesctl get 2>/dev/null) cur=$(powerprofilesctl get 2>/dev/null)
choice=$(powerprofilesctl list 2>/dev/null \ choice=$( { powerprofilesctl list 2>/dev/null | sed -nE 's/^[* ] ([a-z-]+):$/\1/p'; printf '%s\n' "$BACK"; } \
| sed -nE 's/^[* ] ([a-z-]+):$/\1/p' \
| rofi -dmenu -p "profile (now: $cur)") || exit 0 | rofi -dmenu -p "profile (now: $cur)") || exit 0
[ "$choice" = "$BACK" ] && exec "$0" system
[ -n "$choice" ] && powerprofilesctl set "$choice" ;; [ -n "$choice" ] && powerprofilesctl set "$choice" ;;
theme) theme)
choice=$(nomarchy-theme-sync list | rofi -dmenu -p theme) || exit 0 # Visual picker: a grid of real desktop previews (rows + Nameslug
[ -n "$choice" ] && exec nomarchy-theme-sync apply "$choice" ;; # map generated from the presets at build time). Pick a card apply
# that slug. The grid layout is a per-invocation -theme-str override.
declare -A THEME_SLUG=(
${themeSlugMap}
)
choice=$( {
${themeRows}
back
} | rofi -dmenu -show-icons -p Theme -theme-str ${themeGridThemeStr}) || exit 0
[ "$choice" = "$BACK" ] && exec "$0"
choice="''${choice% }" # drop the active marker if present
slug="''${THEME_SLUG[$choice]:-}"
[ -n "$slug" ] && exec nomarchy-theme-sync apply "$slug" ;;
clipboard) clipboard)
sel=$(cliphist list | rofi -dmenu -p clip) || exit 0 sel=$( { cliphist list; printf '%s\n' "$BACK"; } | rofi -dmenu -p clip) || exit 0
[ "$sel" = "$BACK" ] && exec "$0" tools
printf '%s' "$sel" | cliphist decode | wl-copy ;; printf '%s' "$sel" | cliphist decode | wl-copy ;;
calc) calc)
@@ -114,10 +223,10 @@ let
-calc-command "echo -n '{result}' | wl-copy" ;; -calc-command "echo -n '{result}' | wl-copy" ;;
files) files)
sel=$(fd . "$HOME" --type f --hidden --exclude .git --exclude .cache 2>/dev/null \ sel=$( { fd . "$HOME" --type f --hidden --exclude .git --exclude .cache 2>/dev/null \
| head -n 50000 \ | head -n 50000 | sed "s|^$HOME/||"; printf '%s\n' "$BACK"; } \
| sed "s|^$HOME/||" \
| rofi -dmenu -p file) || exit 0 | rofi -dmenu -p file) || exit 0
[ "$sel" = "$BACK" ] && exec "$0" tools
[ -n "$sel" ] && exec xdg-open "$HOME/$sel" ;; [ -n "$sel" ] && exec xdg-open "$HOME/$sel" ;;
emoji) emoji)
@@ -132,23 +241,52 @@ let
exec xdg-open "https://www.google.com/search?q=$(urlencode "$q")" ;; exec xdg-open "https://www.google.com/search?q=$(urlencode "$q")" ;;
network) network)
# nmtui in a terminal (NetworkManager is the system network stack). # Native rofi wifi/VPN picker (networkmanager_dmenu) over the system
exec ${cfg.terminal} -e nmtui ;; # NetworkManager replaces the old nmtui-in-a-terminal flow. Reads
# its config from xdg.configFile below (told to drive rofi).
exec networkmanager_dmenu ;;
bluetooth) bluetooth)
# blueman-manager GUI (services.blueman.enable, system-side). # blueman-manager GUI (services.blueman.enable, system-side).
exec blueman-manager ;; exec blueman-manager ;;
printers)
# system-config-printer GUI (the CUPS admin app), installed by
# nomarchy.services.printing. Self-gated in the System menu; guard
# here too in case it's invoked directly.
command -v system-config-printer >/dev/null 2>&1 \
&& exec system-config-printer
notify-send "Printers" "Not available (nomarchy.services.printing off?)."; exit 0 ;;
audio)
# PipeWire (pulse) sink/source switcher via rofi-pulse-select. Self-
# gated in the System menu on the pulse socket; guarded here too.
if [ ! -e "''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/pulse/native" ]; then
notify-send "Audio" "No PipeWire/PulseAudio socket (nomarchy.audio off?)."; exit 0
fi
choice=$( {
row "Output device" audio-volume-high
row "Input device" audio-input-microphone
back
} | rofi -dmenu -show-icons -p Audio) || exit 0
case "$choice" in
"$BACK") exec "$0" system ;;
*Output*) exec rofi-pulse-select sink ;;
*Input*) exec rofi-pulse-select source ;;
esac ;;
capture) capture)
choice=$( { choice=$( {
row "Region clipboard" applets-screenshooter row "Region clipboard" applets-screenshooter
row "Region file" applets-screenshooter row "Region file" applets-screenshooter
row "Full screen clipboard" camera-photo row "Full screen clipboard" camera-photo
row "Full screen file" camera-photo row "Full screen file" camera-photo
back
} | rofi -dmenu -show-icons -p Capture) || exit 0 } | rofi -dmenu -show-icons -p Capture) || exit 0
dir="$HOME/Pictures/Screenshots" dir="$HOME/Pictures/Screenshots"
file="$dir/$(date +%Y%m%d-%H%M%S).png" file="$dir/$(date +%Y%m%d-%H%M%S).png"
case "$choice" in case "$choice" in
"$BACK") exec "$0" tools ;;
*"Region clipboard"*) grim -g "$(slurp)" - | wl-copy ;; *"Region clipboard"*) grim -g "$(slurp)" - | wl-copy ;;
*"Region file"*) mkdir -p "$dir"; grim -g "$(slurp)" "$file" \ *"Region file"*) mkdir -p "$dir"; grim -g "$(slurp)" "$file" \
&& notify-send "Screenshot saved" "$file" ;; && notify-send "Screenshot saved" "$file" ;;
@@ -178,13 +316,23 @@ let
&& notify-send "Do Not Disturb off" "Notifications resumed." && notify-send "Do Not Disturb off" "Notifications resumed."
exit 0 ;; exit 0 ;;
nightlight)
# Force the scheduled blue-light filter on/off for the session by
# starting/stopping hyprsunset (nomarchy-nightlight, from nightlight.nix).
# Self-gated in the menu; guard here too in case it's invoked directly.
command -v nomarchy-nightlight >/dev/null 2>&1 \
&& exec nomarchy-nightlight toggle
notify-send "Night light" "Not available (nomarchy.nightlight off?)."; exit 0 ;;
snapshot) snapshot)
# btrfs-assistant: snapshot browse / diff / restore / rollback over # btrfs-assistant 2.2 segfaults on 26.05 (libbtrfsutil ABI), so launch
# snapper, elevating via polkit. Shipped system-side with # the keyboard-driven snapper browser instead: a terminal running it
# nomarchy.system.snapper; self-gate so the entry no-ops elsewhere. # via sudo (snapper is root-only here one password prompt), with
command -v btrfs-assistant >/dev/null 2>&1 \ # browse/diff, restore (undochange) and rollback, each confirmed.
|| { notify-send "Snapshots" "btrfs-assistant isn't installed (BTRFS snapshots off?)."; exit 0; } # Self-gated to nomarchy.system.snapper, so it no-ops elsewhere.
exec btrfs-assistant ;; command -v nomarchy-snapshots >/dev/null 2>&1 \
|| { notify-send "Snapshots" "Snapshot tools unavailable (nomarchy.system.snapper off?)."; exit 0; }
exec ${cfg.terminal} -e sudo nomarchy-snapshots ;;
tools) tools)
# Tools submenu utilities you invoke. Each leaf is also reachable # Tools submenu utilities you invoke. Each leaf is also reachable
@@ -197,9 +345,10 @@ let
row "Web search" system-search row "Web search" system-search
row "Capture" applets-screenshooter row "Capture" applets-screenshooter
row "Ask Claude" internet-chat row "Ask Claude" internet-chat
row "Back" go-previous back
} | rofi -dmenu -show-icons -p Tools) || exit 0 } | rofi -dmenu -show-icons -p Tools) || exit 0
case "$choice" in case "$choice" in
"$BACK") exec "$0" ;;
*Calc*) exec "$0" calc ;; *Calc*) exec "$0" calc ;;
*Clipboard*) exec "$0" clipboard ;; *Clipboard*) exec "$0" clipboard ;;
*Emoji*) exec "$0" emoji ;; *Emoji*) exec "$0" emoji ;;
@@ -207,7 +356,6 @@ let
*Web*) exec "$0" web ;; *Web*) exec "$0" web ;;
*Capture*) exec "$0" capture ;; *Capture*) exec "$0" capture ;;
*Ask*) exec "$0" ask ;; *Ask*) exec "$0" ask ;;
*Back*) exec "$0" ;;
esac ;; esac ;;
system) system)
@@ -218,20 +366,28 @@ let
choice=$( { choice=$( {
row "Network" network-wireless row "Network" network-wireless
row "Bluetooth" bluetooth row "Bluetooth" bluetooth
[ -e "''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/pulse/native" ] \
&& row "Audio" audio-volume-high
command -v system-config-printer >/dev/null 2>&1 && row "Printers" printer
row "Do Not Disturb" notification-disabled row "Do Not Disturb" notification-disabled
command -v btrfs-assistant >/dev/null 2>&1 && row "Snapshots" timeshift systemctl --user cat hyprsunset.service >/dev/null 2>&1 \
&& row "Night light" weather-clear-night
command -v nomarchy-snapshots >/dev/null 2>&1 && row "Snapshots" timeshift
if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then
row "Power profile" preferences-system-power row "Power profile" preferences-system-power
fi fi
row "Back" go-previous back
} | rofi -dmenu -show-icons -p System) || exit 0 } | rofi -dmenu -show-icons -p System) || exit 0
case "$choice" in case "$choice" in
"$BACK") exec "$0" ;;
*Network*) exec "$0" network ;; *Network*) exec "$0" network ;;
*Bluetooth*) exec "$0" bluetooth ;; *Bluetooth*) exec "$0" bluetooth ;;
*Audio*) exec "$0" audio ;;
*Printers*) exec "$0" printers ;;
*"Do Not Disturb"*) exec "$0" dnd ;; *"Do Not Disturb"*) exec "$0" dnd ;;
*"Night light"*) exec "$0" nightlight ;;
*Snapshots*) exec "$0" snapshot ;; *Snapshots*) exec "$0" snapshot ;;
*"Power profile"*) exec "$0" power-profile ;; *"Power profile"*) exec "$0" power-profile ;;
*Back*) exec "$0" ;;
esac ;; esac ;;
"") "")
@@ -256,7 +412,7 @@ let
esac ;; esac ;;
*) *)
echo "usage: nomarchy-menu [tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|capture|keybinds|ask|dnd|snapshot]" >&2 echo "usage: nomarchy-menu [tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|printers|capture|keybinds|ask|dnd|nightlight|snapshot]" >&2
exit 64 ;; exit 64 ;;
esac esac
''; '';
@@ -268,8 +424,31 @@ in
pkgs.fd # files module (fuzzy search over $HOME) pkgs.fd # files module (fuzzy search over $HOME)
pkgs.xdg-utils # xdg-open for the files + web modules pkgs.xdg-utils # xdg-open for the files + web modules
pkgs.nodejs # npx, for the Ask Claude module (claude-code from npm) pkgs.nodejs # npx, for the Ask Claude module (claude-code from npm)
pkgs.networkmanager_dmenu # network module: rofi wifi/VPN picker
pkgs.rofi-pulse-select # audio module: sink/source switcher
]; ];
# networkmanager_dmenu drives rofi (not bare dmenu) and uses rofi's
# active/urgent row styling for the connected/available networks, so it
# inherits the generated theme like every other menu module. Editing a
# connection drops to nmtui in the configured terminal. force: the tool
# writes a default config on first run, so an unmanaged file may already
# sit at this path — we fully own it, so overwrite rather than abort.
xdg.configFile."networkmanager-dmenu/config.ini" = {
force = true;
text = ''
[dmenu]
dmenu_command = rofi
rofi_highlight = True
compact = False
wifi_chars =
[editor]
terminal = ${cfg.terminal}
gui_if_available = False
'';
};
programs.rofi = { programs.rofi = {
enable = true; enable = true;
terminal = cfg.terminal; terminal = cfg.terminal;
@@ -292,6 +471,14 @@ in
# reserving `lines` (8) — so a 6-entry menu doesn't leave empty space, # reserving `lines` (8) — so a 6-entry menu doesn't leave empty space,
# while the app launcher still fills + scrolls past `lines`. # while the app launcher still fills + scrolls past `lines`.
fixed-num-lines = false; fixed-num-lines = false;
# Search: case-insensitive fuzzy across every menu + the launcher, so
# "system" finds "System" and "fzr" finds "Firefox". fzf sorting ranks
# the closest match to the top (the per-keystroke modules — calc/emoji
# — opt out per-invocation with -no-sort).
matching = "fuzzy";
case-sensitive = false;
sort = true;
sorting-method = "fzf";
}; };
# Whole-swap themes bring their own rofi.rasi; otherwise the theme # Whole-swap themes bring their own rofi.rasi; otherwise the theme

141
modules/home/updates.nix Normal file
View File

@@ -0,0 +1,141 @@
# Update awareness (opt-in, nomarchy.updates.enable) — a passive background
# check that surfaces a Waybar indicator + a notification when updates are
# available, without ever changing anything (you still run sys-update /
# home-update / flatpak update). It counts:
# • flake inputs whose locked rev is behind upstream (nixpkgs, the Nomarchy
# input, home-manager …) — via `git ls-remote` on each branch-tracking
# github/git input in flake.lock; offline → skipped, never a false alarm.
# • Flatpak updates, when the `flatpak` CLI is present (services.flatpak on).
#
# nomarchy-updates is always on PATH and self-gates (status prints nothing
# until the timer has found something), so the Waybar module — generated and
# whole-swap — can exec it by name even when the feature is off.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy.updates;
nomarchy-updates = pkgs.writeShellScriptBin "nomarchy-updates" ''
set -u
# System + user profiles, so flatpak / sys-update resolve from a timer-run
# service too (build-time tools below use absolute store paths regardless).
export PATH="$PATH:/run/current-system/sw/bin:/etc/profiles/per-user/$USER/bin"
GIT=${pkgs.git}/bin/git
JQ=${pkgs.jq}/bin/jq
cache="''${XDG_CACHE_HOME:-$HOME/.cache}/nomarchy"
state="$cache/updates.json"
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
mkdir -p "$cache"
count_nix() {
local lock="$flake/flake.lock" n=0 name kind owner repo url ref locked giturl up
[ -f "$lock" ] || { echo 0; return; }
while IFS=$'\t' read -r name kind owner repo url ref locked; do
[ -n "$locked" ] || continue
case "$kind" in
github) giturl="https://github.com/$owner/$repo" ;;
git) giturl="$url" ;;
*) continue ;;
esac
up=$("$GIT" ls-remote "$giturl" "$ref" 2>/dev/null | ${pkgs.gawk}/bin/awk 'NR==1{print $1}')
[ -n "$up" ] || continue # offline / unknown skip (no false alarm)
[ "$up" != "$locked" ] && n=$((n + 1))
done < <(
# Only the flake's DIRECT inputs (root.inputs) not the transitive
# closure so a deep dependency bump doesn't nag as an "update".
"$JQ" -r '
.nodes as $nodes
| ($nodes.root.inputs | [ .[] | if type == "array" then .[0] else . end ]) as $direct
| $nodes | to_entries[]
| .key as $k | .value as $v
| select($direct | index($k))
| select(($v.original.type? == "github") or ($v.original.type? == "git"))
| select(($v.original.rev? // "") == "") # branch-tracking only
| [ $k, $v.original.type,
($v.original.owner? // ""), ($v.original.repo? // ""),
($v.original.url? // ""), ($v.original.ref? // "HEAD"),
($v.locked.rev? // "") ] | @tsv
' "$lock"
)
echo "$n"
}
count_flatpak() {
${lib.optionalString cfg.flatpak ''
if command -v flatpak >/dev/null 2>&1; then
flatpak remote-ls --updates --columns=application 2>/dev/null | ${pkgs.gnugrep}/bin/grep -c . || true
return
fi
''}
echo 0
}
refresh_bar() { ${pkgs.procps}/bin/pkill -RTMIN+9 -x waybar 2>/dev/null || true; }
case "''${1:-status}" in
check)
nix=$(count_nix); fp=$(count_flatpak); total=$((nix + fp))
prev=$("$JQ" -r '.total // 0' "$state" 2>/dev/null || echo 0)
printf '{"nix":%d,"flatpak":%d,"total":%d,"ts":%d}\n' \
"$nix" "$fp" "$total" "$(${pkgs.coreutils}/bin/date +%s)" > "$state"
# Notify only when NEW updates appear, so a daily timer doesn't nag.
if [ "$total" -gt 0 ] && [ "$total" -gt "$prev" ]; then
msg="$nix flake input(s)"
[ "$fp" -gt 0 ] && msg="$msg · $fp Flatpak(s)"
${pkgs.libnotify}/bin/notify-send -a Nomarchy "Updates available" \
"$msg click the bar icon, or run sys-update."
fi
refresh_bar ;;
status)
total=$("$JQ" -r '.total // 0' "$state" 2>/dev/null || echo 0)
[ "$total" -gt 0 ] 2>/dev/null || exit 0 # up to date / unchecked hide
nix=$("$JQ" -r '.nix // 0' "$state"); fp=$("$JQ" -r '.flatpak // 0' "$state")
tip="Updates available"
[ "$nix" -gt 0 ] && tip="$tip\n $nix flake input(s) sys-update"
[ "$fp" -gt 0 ] && tip="$tip\n $fp Flatpak(s) flatpak update"
printf '{"text":"󰚰 %d","tooltip":"%s","class":"available"}\n' "$total" "$tip" ;;
upgrade)
echo "Checking"; "$0" check
nix=$("$JQ" -r '.nix // 0' "$state" 2>/dev/null || echo 0)
fp=$("$JQ" -r '.flatpak // 0' "$state" 2>/dev/null || echo 0)
echo "Pending: $nix flake input(s), $fp Flatpak(s)."
if [ "$nix" -gt 0 ] && command -v sys-update >/dev/null 2>&1; then
read -rp "Run sys-update (flake update + system rebuild)? [y/N] " a
[ "$a" = y ] && sys-update
fi
if [ "$fp" -gt 0 ] && command -v flatpak >/dev/null 2>&1; then
read -rp "Run flatpak update? [y/N] " a
[ "$a" = y ] && flatpak update
fi
"$0" check
echo "Done press enter."; read -r _ || true ;;
*) echo "usage: nomarchy-updates [check|status|upgrade]" >&2; exit 64 ;;
esac
'';
in
{
config = lib.mkMerge [
# Always on PATH so the Waybar module (incl. the static whole-swap themes)
# can exec it; it self-gates at runtime.
{ home.packages = [ nomarchy-updates ]; }
(lib.mkIf cfg.enable {
systemd.user.services.nomarchy-updates = {
Unit.Description = "Check for Nomarchy / nixpkgs / Flatpak updates";
Service = {
Type = "oneshot";
ExecStart = "${nomarchy-updates}/bin/nomarchy-updates check";
};
};
systemd.user.timers.nomarchy-updates = {
Unit.Description = "Periodic update-awareness check";
Timer = {
OnStartupSec = "2min";
OnCalendar = cfg.interval;
Persistent = true;
};
Install.WantedBy = [ "timers.target" ];
};
})
];
}

View File

@@ -77,9 +77,9 @@ let
modules-left = [ "hyprland/workspaces" "hyprland/window" ]; modules-left = [ "hyprland/workspaces" "hyprland/window" ];
modules-center = [ "clock" ]; modules-center = [ "clock" ];
modules-right = [ "tray" "pulseaudio" "network" "cpu" "memory" "custom/powerprofile" ] modules-right = [ "tray" "pulseaudio" "cpu" "memory" "custom/powerprofile" "custom/nightlight" ]
++ lib.optional showLanguage "hyprland/language" ++ lib.optional showLanguage "hyprland/language"
++ [ "battery" "custom/notification" ]; ++ [ "battery" "custom/updates" "custom/notification" ];
"hyprland/workspaces" = { "hyprland/workspaces" = {
format = "{icon}"; format = "{icon}";
@@ -111,15 +111,8 @@ let
on-click = "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; on-click = "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
}; };
network = { # No network module: nm-applet lives in the tray (the GUI path), so the
format-wifi = "󰤨 {essid}"; # bar's wifi/ethernet indicator would just duplicate it.
format-ethernet = "󰈀";
format-disconnected = "󰤭";
tooltip-format = "{ipaddr} via {gwaddr}";
# nm-applet sits in the tray for the GUI path; this is the
# keyboard-friendly one.
on-click = "${config.nomarchy.terminal} -e nmtui";
};
cpu.format = "󰍛 {usage}%"; cpu.format = "󰍛 {usage}%";
memory.format = "󰾆 {percentage}%"; memory.format = "󰾆 {percentage}%";
@@ -138,6 +131,28 @@ let
on-click = "nomarchy-powerprofile-cycle"; on-click = "nomarchy-powerprofile-cycle";
}; };
# Night-light (hyprsunset) toggle + state. Self-gates: hidden unless
# nomarchy.nightlight is enabled (the status helper prints nothing then).
# Click starts/stops the schedule; moon = on, sun = off.
"custom/nightlight" = {
exec = "nomarchy-nightlight status";
return-type = "json";
interval = 3;
on-click = "nomarchy-nightlight toggle";
};
# Update awareness. Self-gates: hidden unless nomarchy.updates is enabled
# AND the periodic check found something (the helper prints nothing then).
# signal 9 lets the checker refresh it instantly; click opens the upgrade
# flow in a terminal.
"custom/updates" = {
exec = "nomarchy-updates status";
return-type = "json";
interval = 1800;
signal = 9;
on-click = "${config.nomarchy.terminal} -e nomarchy-updates upgrade";
};
# swaync notification bell + Do-Not-Disturb state. `-swb` streams JSON # swaync notification bell + Do-Not-Disturb state. `-swb` streams JSON
# (text/tooltip/class) on every change, so it tracks count and DND with # (text/tooltip/class) on every change, so it tracks count and DND with
# no polling. Left-click toggles the panel; right-click toggles DND. # no polling. Left-click toggles the panel; right-click toggles DND.
@@ -209,11 +224,21 @@ let
font-weight: bold; font-weight: bold;
} }
#tray, #pulseaudio, #network, #cpu, #memory, #custom-powerprofile, #language, #battery, #custom-notification { #tray, #pulseaudio, #cpu, #memory, #custom-powerprofile, #custom-nightlight, #custom-updates, #language, #battery, #custom-notification {
color: @subtext; color: @subtext;
padding: 0 10px; padding: 0 10px;
} }
/* Night-light active warm tone, matching the filter it represents. */
#custom-nightlight.on { color: @warn; }
/* Updates pending accent, to draw the eye. */
#custom-updates.available { color: @accent; }
/* The power-profile speedometer glyph renders small in its em box
size it up so it reads at a glance like the other indicators. */
#custom-powerprofile { font-size: ${toString (t.fonts.size + 3)}pt; }
/* notifications waiting accent; Do-Not-Disturb muted bell-off */ /* notifications waiting accent; Do-Not-Disturb muted bell-off */
#custom-notification.notification { color: @accent; } #custom-notification.notification { color: @accent; }
#custom-notification.dnd-none, #custom-notification.dnd-none,
@@ -228,7 +253,14 @@ in
{ {
programs.waybar = lib.mkIf config.nomarchy.waybar.enable { programs.waybar = lib.mkIf config.nomarchy.waybar.enable {
enable = true; enable = true;
systemd.enable = true; # started/stopped with graphical-session.target # Launched from Hyprland's exec-once (hyprland.nix), NOT a systemd user
# service. Bound to graphical-session.target the unit raced Hyprland's IPC
# on a warm relogin — it started before the socket was up, exited, landed
# in `failed`, and was never retried, so the bar vanished. exec-once only
# fires once Hyprland is up, dodging the race; theme switches reload the
# running bar via SIGUSR2 (nomarchy-theme-sync). No uwsm here to manage the
# session target, so we don't depend on its lifecycle.
systemd.enable = false;
# mkDefault so downstream can replace the whole bar config/style with # mkDefault so downstream can replace the whole bar config/style with
# a plain home.nix assignment. For per-theme identity, prefer the # a plain home.nix assignment. For per-theme identity, prefer the

View File

@@ -22,7 +22,7 @@ let
''; '';
in in
{ {
imports = [ ./options.nix ./plymouth.nix ./file-manager.nix ./power.nix ./services.nix ]; imports = [ ./options.nix ./plymouth.nix ./file-manager.nix ./power.nix ./services.nix ./hardware.nix ];
config = { config = {
# The safe half of distro branding: distroName flows into # The safe half of distro branding: distroName flows into
@@ -69,6 +69,11 @@ in
console.earlySetup = lib.mkDefault true; console.earlySetup = lib.mkDefault true;
boot.initrd.systemd.enable = lib.mkDefault true; boot.initrd.systemd.enable = lib.mkDefault true;
# Nomarchy roots are BTRFS, not ZFS, so adopt the 26.11 default early and
# silence the eval warning the old `true` default emits. mkDefault, so a
# genuine ZFS-root downstream can still force it back on.
boot.zfs.forceImportRoot = lib.mkDefault false;
# ── Wayland session: Hyprland ──────────────────────────────────── # ── Wayland session: Hyprland ────────────────────────────────────
# Installs the binary, registers the session, wires up # Installs the binary, registers the session, wires up
# xdg-desktop-portal-hyprland. Configuration is Home Manager's job. # xdg-desktop-portal-hyprland. Configuration is Home Manager's job.
@@ -320,8 +325,69 @@ in
nixos-rebuild switch --flake /etc/nixos#default "$@" nixos-rebuild switch --flake /etc/nixos#default "$@"
'') '')
# The desktop snapshot manager (browse / diff / restore / rollback over # The desktop snapshot manager (browse / diff / restore / rollback over
# snapper, elevating via polkit) — what `nomarchy-menu snapshot` launches. # snapper, elevating via polkit). Kept for when nixpkgs fixes its crash.
++ lib.optional cfg.snapper.enable pkgs.btrfs-assistant; ++ lib.optional cfg.snapper.enable pkgs.btrfs-assistant
# Keyboard-driven snapper browser/restore — the fallback `nomarchy-menu
# snapshot` actually launches while btrfs-assistant 2.2 segfaults on 26.05
# (libbtrfsutil ABI). Runs as root (snapper is root-only here; the menu
# opens it in a terminal via sudo, one password prompt), fzf to pick, with
# browse/diff (read-only) and typed-`yes` confirmation before any write.
++ lib.optional cfg.snapper.enable (pkgs.writeShellApplication {
name = "nomarchy-snapshots";
runtimeInputs = with pkgs; [ snapper fzf gawk gnugrep less coreutils systemd ];
text = ''
if [ "$(id -u)" -ne 0 ]; then
echo "nomarchy-snapshots must run as root (snapper needs it) use sudo." >&2
exit 1
fi
mapfile -t configs < <(snapper list-configs | awk 'NR>2 {print $1}' | grep .)
if [ "''${#configs[@]}" -eq 0 ]; then
echo "No snapper configs found." >&2; exit 1
elif [ "''${#configs[@]}" -eq 1 ]; then
config="''${configs[0]}"
else
config=$(printf '%s\n' "''${configs[@]}" | fzf --prompt="snapper config> ") || exit 0
fi
while :; do
snap=$(snapper -c "$config" list \
| fzf --header-lines=2 --prompt="[$config] pick a snapshot (Esc quits)> ") || exit 0
num=$(awk '{print $1}' <<<"$snap")
case "$num" in ""|*[!0-9]*) continue ;; esac
action=$(printf '%s\n' \
"Browse changes since #$num (read-only)" \
"Restore changed files to #$num (undochange)" \
"Roll the system back to #$num (reboot)" \
" Back to the snapshot list" \
| fzf --prompt="snapshot #$num> ") || exit 0
case "$action" in
Browse*)
snapper -c "$config" status "$num..0" | less -R || true ;;
Restore*)
read -rp "Revert files in '$config' to snapshot #$num? Type yes to confirm: " ans || continue
if [ "$ans" = yes ]; then
snapper -c "$config" undochange "$num..0"
echo "Files restored. Press enter."; read -r _ || true
fi ;;
Roll*)
if [ "$config" != root ]; then
echo "Rollback applies to the 'root' config only; use Restore for '$config'. Press enter."
read -r _ || true
else
read -rp "Roll the SYSTEM back to #$num and REBOOT now? Type yes to confirm: " ans || continue
if [ "$ans" = yes ]; then
snapper -c root rollback "$num"
echo "Rolled back rebooting"; systemctl reboot
fi
fi ;;
*) continue ;;
esac
done
'';
});
# Don't let boot entries fill the ESP over the years. # Don't let boot entries fill the ESP over the years.
boot.loader.systemd-boot.configurationLimit = lib.mkDefault 10; boot.loader.systemd-boot.configurationLimit = lib.mkDefault 10;

195
modules/nixos/hardware.nix Normal file
View File

@@ -0,0 +1,195 @@
# Hardware enablement beyond nixos-hardware.
#
# The nixos-hardware "common-*" profiles the installer selects cover the
# BASICS (microcode, the Intel/AMD VA-API media stack, weekly fstrim), and
# power.nix adds thermald + power-profiles-daemon. This module fills the GAP
# above those: broadly-beneficial bits that default ON when the installer
# detects the vendor (opt-OUT), and heavier/experimental bits behind opt-IN
# toggles. The installer's hardware-db.sh probes what's present and writes the
# matching nomarchy.hardware.* into the generated system.nix.
#
# Audited against the commons so we don't double-set: we add GuC/HuC, the
# amd-pstate governor, the AMD VA-API env, GPU-compute runtimes, fprintd, and
# the NPU driver — none of which the commons turn on.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy.hardware;
in
{
options.nomarchy.hardware = {
intel = {
enable = lib.mkEnableOption ''
Intel CPU/GPU enablement (the installer turns this on when it detects
an Intel CPU or GPU). Complements nixos-hardware's common-gpu-intel'';
guc = lib.mkOption {
type = lib.types.bool;
default = cfg.intel.enable;
defaultText = lib.literalExpression "config.nomarchy.hardware.intel.enable";
description = ''
Load the GPU's GuC/HuC firmware via the i915 param
(i915.enable_guc=3) better power management and HuC-accelerated
media. On by default with intel.enable. NOTE: this is the *i915*
driver's param; the newer `xe` driver (Lunar Lake / Battlemage /
Panther Lake and other recent Xe GPUs) enables GuC by default and
ignores it, so the installer turns this off on xe-driver hardware.
'';
};
computeRuntime = lib.mkEnableOption ''
Intel GPU compute: the OpenCL / Level Zero (intel-compute-runtime) and
oneVPL (vpl-gpu-rt) runtimes for GPU compute and transcode. Opt-in (a
few hundred MB) the Intel counterpart to AMD ROCm'';
};
amd = {
enable = lib.mkEnableOption ''
AMD CPU/GPU enablement (installer-set on an AMD CPU or GPU).
Complements nixos-hardware's common-cpu-amd / common-gpu-amd'';
pstate = lib.mkOption {
type = lib.types.bool;
default = cfg.amd.enable;
defaultText = lib.literalExpression "config.nomarchy.hardware.amd.enable";
description = ''
Use the amd-pstate EPP driver (amd_pstate=active) the modern Zen
power/perf governor that power-profiles-daemon drives per profile.
On by default with amd.enable (broadly beneficial on Zen 2+).
'';
};
vaapi = lib.mkOption {
type = lib.types.bool;
default = cfg.amd.enable;
defaultText = lib.literalExpression "config.nomarchy.hardware.amd.enable";
description = ''
Point VA-API at mesa's radeonsi (LIBVA_DRIVER_NAME=radeonsi) for
hardware video decode/encode. On by default with amd.enable.
'';
};
rocm = {
enable = lib.mkEnableOption ''
AMD ROCm: the HIP / OpenCL GPU-compute stack (multi-GB closure).
Opt-in unlocks GPU PyTorch / Ollama on Radeon'';
gfxOverride = lib.mkOption {
type = lib.types.str;
default = "";
example = "11.0.0";
description = ''
HSA_OVERRIDE_GFX_VERSION for GPUs ROCm doesn't officially list
(e.g. an RDNA3 780M iGPU, gfx1103, needs "11.0.0"). Empty = no
override.
'';
};
};
};
fingerprint = {
enable = lib.mkEnableOption ''
a fingerprint reader via fprintd (the installer turns this on when it
detects a known reader). Enroll with `fprintd-enroll`'';
pam = lib.mkEnableOption ''
using the fingerprint for login and sudo (PAM). Opt-in password-only
stays the default for the cautious; enroll a finger first'';
};
npu.enable = lib.mkEnableOption ''
the on-die NPU (AI accelerator) kernel driver amdxdna on AMD (Ryzen
AI), intel_vpu on Intel (Core Ultra and newer). Opt-in and experimental:
this loads the in-kernel driver only; the userspace runtime (AMD XRT /
oneAPI Level Zero NPU) is yours to add. Needs a recent kernel (see
latestKernel)'';
latestKernel = lib.mkEnableOption ''
the latest mainline kernel (pkgs.linuxPackages_latest) instead of the
distro default for very new hardware whose drivers (a fresh NPU, the
`xe` GPU driver, new-platform enablement) only landed recently. Off by
default; the default kernel already carries amd-pstate and amdxdna (6.14+)'';
};
config = lib.mkMerge [
# ── Intel ──────────────────────────────────────────────────────────
(lib.mkIf cfg.intel.guc {
boot.kernelParams = [ "i915.enable_guc=3" ];
})
(lib.mkIf cfg.intel.computeRuntime {
hardware.graphics.extraPackages = with pkgs; [ intel-compute-runtime vpl-gpu-rt ];
})
# ── AMD ────────────────────────────────────────────────────────────
(lib.mkIf cfg.amd.pstate {
boot.kernelParams = [ "amd_pstate=active" ];
})
(lib.mkIf cfg.amd.vaapi {
# radeonsi itself comes from mesa (via common-gpu-amd); this just steers
# libva at it. mkDefault so a hand-set value or another module wins.
environment.sessionVariables.LIBVA_DRIVER_NAME = lib.mkDefault "radeonsi";
hardware.graphics.extraPackages = [ pkgs.libva ];
})
(lib.mkIf cfg.amd.rocm.enable {
hardware.graphics.extraPackages = [ pkgs.rocmPackages.clr pkgs.rocmPackages.clr.icd ];
environment.sessionVariables = lib.optionalAttrs (cfg.amd.rocm.gfxOverride != "") {
HSA_OVERRIDE_GFX_VERSION = cfg.amd.rocm.gfxOverride;
};
})
# ── Fingerprint ────────────────────────────────────────────────────
(lib.mkIf cfg.fingerprint.enable {
services.fprintd.enable = true;
})
(lib.mkIf (cfg.fingerprint.enable && cfg.fingerprint.pam) {
security.pam.services.login.fprintAuth = true;
security.pam.services.sudo.fprintAuth = true;
})
# ── Newest kernel for very-new hardware (opt-in escape hatch) ──────
(lib.mkIf cfg.latestKernel {
boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
})
# ── NPU (in-kernel driver only; userspace runtime is BYO) ──────────
# The driver has to actually be in the running kernel — warn (don't fail)
# when it predates the shipped one, pointing at latestKernel.
(lib.mkIf (cfg.npu.enable && cfg.amd.enable) {
boot.kernelModules = [ "amdxdna" ];
warnings = lib.optional
(!lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.14")
''
nomarchy.hardware.npu: the amdxdna driver needs kernel >= 6.14, but
this config ships ${config.boot.kernelPackages.kernel.version}. Set
nomarchy.hardware.latestKernel = true.'';
})
(lib.mkIf (cfg.npu.enable && cfg.intel.enable) {
boot.kernelModules = [ "intel_vpu" ];
warnings = lib.optional
(!lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.11")
''
nomarchy.hardware.npu: the intel_vpu driver (especially for newer
NPUs) wants a recent kernel, but this config ships
${config.boot.kernelPackages.kernel.version}. Consider
nomarchy.hardware.latestKernel = true.'';
})
# ── Sanity ─────────────────────────────────────────────────────────
{
assertions = [
{
assertion = cfg.amd.rocm.enable -> cfg.amd.enable;
message = "nomarchy.hardware.amd.rocm.enable needs nomarchy.hardware.amd.enable.";
}
{
assertion = cfg.intel.computeRuntime -> cfg.intel.enable;
message = "nomarchy.hardware.intel.computeRuntime needs nomarchy.hardware.intel.enable.";
}
{
assertion = cfg.npu.enable -> (cfg.amd.enable || cfg.intel.enable);
message = "nomarchy.hardware.npu.enable needs a detected Intel or AMD platform.";
}
];
}
];
}

View File

@@ -75,7 +75,8 @@ in
printing.enable = lib.mkEnableOption '' printing.enable = lib.mkEnableOption ''
CUPS printing with Avahi/mDNS, so network printers are auto-discovered CUPS printing with Avahi/mDNS, so network printers are auto-discovered
(add vendor drivers via `services.printing.drivers`)''; (add vendor drivers via `services.printing.drivers`); the menu's
System Printers entry opens the system-config-printer GUI'';
openrgb.enable = lib.mkEnableOption '' openrgb.enable = lib.mkEnableOption ''
the OpenRGB daemon and GUI for controlling RGB lighting on peripherals the OpenRGB daemon and GUI for controlling RGB lighting on peripherals
@@ -246,6 +247,9 @@ in
nssmdns4 = true; nssmdns4 = true;
openFirewall = true; openFirewall = true;
}; };
# The CUPS admin GUI — the menu's System ▸ Printers entry execs it
# (self-gated on this binary), so it ships with the printing service.
environment.systemPackages = [ pkgs.system-config-printer ];
}) })
(lib.mkIf cfg.openrgb.enable { (lib.mkIf cfg.openrgb.enable {

View File

@@ -112,6 +112,7 @@ HARDWARE_DB=(
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
nomarchy_detect_hw() { nomarchy_detect_hw() {
local sys_vendor product_name cpu_vendor local sys_vendor product_name cpu_vendor
local nvidia=0 amdgpu=0 intelgpu=0
sys_vendor=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || echo "") sys_vendor=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || echo "")
product_name=$(cat /sys/class/dmi/id/product_name 2>/dev/null || echo "") product_name=$(cat /sys/class/dmi/id/product_name 2>/dev/null || echo "")
cpu_vendor=$(lscpu 2>/dev/null | awk -F: '/Vendor ID/{gsub(/ /,"",$2); print $2; exit}') cpu_vendor=$(lscpu 2>/dev/null | awk -F: '/Vendor ID/{gsub(/ /,"",$2); print $2; exit}')
@@ -126,7 +127,7 @@ nomarchy_detect_hw() {
# GPU (lspci may list several; report all) # GPU (lspci may list several; report all)
if command -v lspci >/dev/null 2>&1; then if command -v lspci >/dev/null 2>&1; then
local gpu_line nvidia=0 amdgpu=0 intelgpu=0 local gpu_line
while IFS= read -r gpu_line; do while IFS= read -r gpu_line; do
case "$gpu_line" in case "$gpu_line" in
*"[10de:"*|*"NVIDIA"*) nvidia=1 ;; *"[10de:"*|*"NVIDIA"*) nvidia=1 ;;
@@ -140,6 +141,67 @@ nomarchy_detect_hw() {
(( intelgpu )) && { echo "MODULE common-gpu-intel"; echo "DETAIL gpu: Intel"; detected=1; } (( intelgpu )) && { echo "MODULE common-gpu-intel"; echo "DETAIL gpu: Intel"; detected=1; }
fi fi
# ── nomarchy.hardware.* enablement — the gap ABOVE the nixos-hardware
# commons (GuC/HuC, amd-pstate, the AMD VA-API env, GPU-compute runtimes,
# fprintd, the NPU driver). Emitted as NOMARCHY lines the installer turns
# into nomarchy.hardware.* in system.nix; safe bits active, heavy opt-ins
# commented.
if [[ "$cpu_vendor" == "GenuineIntel" || $intelgpu -eq 1 ]]; then
echo "NOMARCHY hardware.intel.enable=true"
echo "DETAIL nomarchy.hardware.intel: GuC/HuC on; GPU-compute runtime opt-in"
# i915.enable_guc applies to the i915 driver only. The newer `xe` driver
# (Lunar Lake / Battlemage / Panther Lake, recent Xe GPUs) enables GuC by
# default and ignores the param — so turn the toggle off when xe is bound.
if lspci -k 2>/dev/null | grep -qiE 'driver in use: xe\b'; then
echo "NOMARCHY hardware.intel.guc=false"
echo "DETAIL Intel GPU on the xe driver → GuC default-on (i915 param skipped)"
fi
fi
if [[ "$cpu_vendor" == "AuthenticAMD" || $amdgpu -eq 1 ]]; then
echo "NOMARCHY hardware.amd.enable=true"
echo "DETAIL nomarchy.hardware.amd: amd-pstate + VA-API on; ROCm opt-in"
fi
# Fingerprint reader — libfprint's common USB vendor IDs (Goodix,
# Synaptics/Validity, Elan, EgisTec, Upek, AuthenTec, FocalTech, NB).
local have_fp=0
if command -v lsusb >/dev/null 2>&1; then
local vid
for vid in 27c6 06cb 138a 04f3 1c7a 147e 08ff 2808 1fae; do
lsusb 2>/dev/null | grep -qiE "ID ${vid}:" && { have_fp=1; break; }
done
else
local f
for f in /sys/bus/usb/devices/*/idVendor; do
[[ -e "$f" ]] || continue
case "$(cat "$f" 2>/dev/null)" in
27c6|06cb|138a|04f3|1c7a|147e|08ff|2808|1fae) have_fp=1; break ;;
esac
done
fi
if [[ $have_fp -eq 1 ]]; then
echo "NOMARCHY hardware.fingerprint.enable=true"
echo "DETAIL fingerprint reader detected → fprintd (PAM login/sudo opt-in)"
fi
# NPU (detect-only → a commented, experimental opt-in). Match the PCI
# "Processing accelerators" class [1200] (or accelerator keywords) and
# attribute by vendor — future-proof vs a per-device-ID list, so new gens
# (e.g. Panther Lake) are caught without a code change. Known IDs kept for
# reference: Intel VPU 8086:{7d1d,643e,ad1d,b03e}, AMD XDNA 1022:1502.
if command -v lspci >/dev/null 2>&1; then
local npu_line
npu_line=$(lspci -nn 2>/dev/null \
| grep -iE '\[1200\]|processing accelerat|neural|\[8086:(7d1d|643e|ad1d|b03e)\]|\[1022:1502\]' \
| head -1)
case "$npu_line" in
*"[8086:"*|*Intel*)
echo "NOMARCHY-NPU intel"; echo "DETAIL Intel NPU detected (opt-in, experimental)" ;;
*"[1022:"*|*"Advanced Micro Devices"*|*AMD*)
echo "NOMARCHY-NPU amd"; echo "DETAIL AMD XDNA NPU detected (opt-in, experimental)" ;;
esac
fi
# Chassis (glob test, not compgen — nixpkgs' non-interactive bash # Chassis (glob test, not compgen — nixpkgs' non-interactive bash
# is built without the completion builtins) # is built without the completion builtins)
local bats=(/sys/class/power_supply/BAT*) local bats=(/sys/class/power_supply/BAT*)

View File

@@ -54,6 +54,15 @@ if [[ $EUID -ne 0 ]]; then
exec sudo --preserve-env "$0" "$@" exec sudo --preserve-env "$0" "$@"
fi fi
# `sudo --preserve-env` (needed to carry the NOMARCHY_* vars) also drags in
# the live session user's HOME=/home/nomarchy. Root-run `nix` calls below
# would then scribble an eval cache + .nix-defexpr into /home/nomarchy —
# and the in-chroot one lands on the TARGET disk as a stray, orphaned
# /home/nomarchy (no such user on the installed system). Pin root's own
# HOME so every root nix invocation stays in /root; the user activation
# sets HOME=/home/$USERNAME explicitly and is unaffected.
export HOME=/root
header "Nomarchy installer" "NixOS, themed and ready to go." header "Nomarchy installer" "NixOS, themed and ready to go."
[[ -d /sys/firmware/efi ]] \ [[ -d /sys/firmware/efi ]] \
@@ -210,6 +219,8 @@ section "Hardware detection"
source "$SHARE/hardware-db.sh" source "$SHARE/hardware-db.sh"
HW_PROFILES=() HW_PROFILES=()
HW_NOMARCHY=() # NOMARCHY hardware.* assignments from detection
NPU_VENDOR="" # "intel" | "amd" if an NPU was detected (commented opt-in)
hw_mode="${NOMARCHY_HW:-auto}" hw_mode="${NOMARCHY_HW:-auto}"
if [[ "$hw_mode" == "none" ]]; then if [[ "$hw_mode" == "none" ]]; then
info "Hardware profiles skipped." info "Hardware profiles skipped."
@@ -221,6 +232,8 @@ else
while IFS= read -r line; do while IFS= read -r line; do
case "$line" in case "$line" in
MODULE\ *) HW_PROFILES+=("${line#MODULE }") ;; MODULE\ *) HW_PROFILES+=("${line#MODULE }") ;;
NOMARCHY-NPU\ *) NPU_VENDOR="${line#NOMARCHY-NPU }" ;;
NOMARCHY\ *) HW_NOMARCHY+=("${line#NOMARCHY }") ;;
DETAIL\ *) info "${line#DETAIL }" ;; DETAIL\ *) info "${line#DETAIL }" ;;
esac esac
done <<< "$detection" done <<< "$detection"
@@ -428,6 +441,57 @@ NIX
) )
fi fi
# Hardware enablement (nomarchy.hardware.*): what hardware-db.sh detected
# above the nixos-hardware commons. Safe defaults active; the heavier or
# experimental opt-ins written commented for the user to flip on.
HARDWARE_CONFIG=""
if [[ ${#HW_NOMARCHY[@]} -gt 0 || -n "$NPU_VENDOR" ]]; then
has_intel=0; has_amd=0; has_fp=0; intel_guc_off=0
if [[ ${#HW_NOMARCHY[@]} -gt 0 ]]; then
for nm in "${HW_NOMARCHY[@]}"; do
case "$nm" in
hardware.intel.enable=true) has_intel=1 ;;
hardware.intel.guc=false) intel_guc_off=1 ;;
hardware.amd.enable=true) has_amd=1 ;;
hardware.fingerprint.enable=true) has_fp=1 ;;
esac
done
fi
hw_lines=" # Hardware enablement (auto-detected). Safe defaults are active;
# the heavier opt-ins are commented — uncomment to turn them on."
if [[ $has_intel -eq 1 ]]; then
hw_lines+="
nomarchy.hardware.intel.enable = true; # GuC/HuC firmware (i915.enable_guc=3)"
if [[ $intel_guc_off -eq 1 ]]; then
hw_lines+="
nomarchy.hardware.intel.guc = false; # GPU on the xe driver → GuC is default-on"
fi
hw_lines+="
# nomarchy.hardware.intel.computeRuntime = true; # OpenCL/oneVPL GPU compute (opt-in)"
fi
if [[ $has_amd -eq 1 ]]; then
hw_lines+="
nomarchy.hardware.amd.enable = true; # amd-pstate EPP + radeonsi VA-API
# nomarchy.hardware.amd.rocm.enable = true; # ROCm GPU compute (multi-GB, opt-in)
# nomarchy.hardware.amd.rocm.gfxOverride = \"\"; # e.g. \"11.0.0\" for an unlisted iGPU"
fi
if [[ $has_fp -eq 1 ]]; then
hw_lines+="
nomarchy.hardware.fingerprint.enable = true; # fprintd (enroll: fprintd-enroll)
# nomarchy.hardware.fingerprint.pam = true; # use it for login + sudo (opt-in)"
fi
if [[ -n "$NPU_VENDOR" ]]; then
hw_lines+="
# nomarchy.hardware.npu.enable = true; # $NPU_VENDOR NPU driver (experimental; userspace runtime BYO)
# nomarchy.hardware.latestKernel = true; # newest kernel if the NPU driver isn't in the shipped one"
fi
HARDWARE_CONFIG=$(cat <<NIX
$hw_lines
NIX
)
fi
# initialHashedPassword is safe to template: mkpasswd's alphabet is # initialHashedPassword is safe to template: mkpasswd's alphabet is
# [a-zA-Z0-9./$] — no Nix string metacharacters. # [a-zA-Z0-9./$] — no Nix string metacharacters.
cat > "$FLAKE_DIR/system.nix" <<EOF cat > "$FLAKE_DIR/system.nix" <<EOF
@@ -459,7 +523,7 @@ cat > "$FLAKE_DIR/system.nix" <<EOF
extraGroups = [ "wheel" "networkmanager" "video" "input" ]; extraGroups = [ "wheel" "networkmanager" "video" "input" ];
initialHashedPassword = "$HASHED_PASSWORD"; initialHashedPassword = "$HASHED_PASSWORD";
}; };
$AUTOLOGIN_CONFIG$POWER_CONFIG$RESUME_CONFIG $AUTOLOGIN_CONFIG$POWER_CONFIG$HARDWARE_CONFIG$RESUME_CONFIG
# Hourly/daily BTRFS timeline snapshots + nixos-rebuild-snap. # Hourly/daily BTRFS timeline snapshots + nixos-rebuild-snap.
nomarchy.system.snapper.enable = true; nomarchy.system.snapper.enable = true;
@@ -573,6 +637,8 @@ cat > /mnt/root/nomarchy-hm-activate.sh <<EOF
set -ex set -ex
exec > /var/log/nomarchy-hm-preactivate.log 2>&1 exec > /var/log/nomarchy-hm-preactivate.log 2>&1
export PATH=/run/current-system/sw/bin:\$PATH export PATH=/run/current-system/sw/bin:\$PATH
# Keep root's nix state in /root, not a stray /home/nomarchy on the target.
export HOME=/root
# Normally pre-built in the live env and copied over; the in-chroot # Normally pre-built in the live env and copied over; the in-chroot
# build (default substituters — the live-side build already proved the # build (default substituters — the live-side build already proved the
# no-network case) is a last-resort fallback. # no-network case) is a last-resort fallback.

View File

@@ -171,6 +171,11 @@ def run_switch() -> None:
notify("Theme rebuild FAILED — see terminal / journal", urgency="critical") notify("Theme rebuild FAILED — see terminal / journal", urgency="critical")
die("rebuild failed (state file already updated; fix and re-run)") die("rebuild failed (state file already updated; fix and re-run)")
notify("Theme applied ✓") notify("Theme applied ✓")
# Waybar runs from Hyprland's exec-once (not a systemd unit HM would restart
# on switch), so nudge the running bar to re-read its freshly rebuilt
# config/style. SIGUSR2 = reload; harmless no-op if waybar isn't running.
if shutil.which("pkill"):
subprocess.run(["pkill", "--signal", "SIGUSR2", "-x", "waybar"], check=False)
# ─── Theme assets (wallpapers) ──────────────────────────────────────────── # ─── Theme assets (wallpapers) ────────────────────────────────────────────

View File

@@ -22,6 +22,12 @@
# sunrise = "07:00"; # sunrise = "07:00";
# }; # };
# #
# nomarchy.updates = { # passive update-awareness indicator + notification
# enable = true;
# interval = "daily"; # how often to check (systemd OnCalendar)
# flatpak = true; # also count Flatpak updates when flatpak is on
# };
#
# nomarchy.monitors = [ # declarative per-output layout, hotplug-applied # nomarchy.monitors = [ # declarative per-output layout, hotplug-applied
# { name = "eDP-1"; position = "0x0"; } # { name = "eDP-1"; position = "0x0"; }
# { name = "HDMI-A-1"; position = "auto-right"; } # { name = "HDMI-A-1"; position = "auto-right"; }

View File

@@ -31,6 +31,18 @@
# thermal.enable = true; # thermald (Intel CPUs) # thermal.enable = true; # thermald (Intel CPUs)
# }; # };
# #
# nomarchy.hardware = { # enablement above nixos-hardware (installer-detected)
# intel.enable = true; # GuC/HuC firmware; installer sets this on Intel
# intel.computeRuntime = true; # OpenCL/oneVPL GPU compute (opt-in)
# amd.enable = true; # amd-pstate + radeonsi VA-API; installer-set on AMD
# amd.rocm.enable = true; # ROCm GPU compute (multi-GB, opt-in)
# amd.rocm.gfxOverride = "11.0.0"; # HSA override for an unlisted iGPU
# fingerprint.enable = true; # fprintd; installer-set when a reader is detected
# fingerprint.pam = true; # use the fingerprint for login + sudo
# npu.enable = true; # on-die NPU driver (experimental; userspace runtime BYO)
# latestKernel = true; # newest kernel for very-new hardware (drivers not yet in the default)
# };
#
# nomarchy.services.tailscale.enable = true; # mesh VPN — then `sudo tailscale up` # 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.syncthing.enable = true; # file sync — GUI at http://127.0.0.1:8384
# nomarchy.services.podman.enable = true; # rootless containers (docker → podman) # nomarchy.services.podman.enable = true; # rootless containers (docker → podman)

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
themes/ethereal/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
themes/gruvbox/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
themes/kanagawa/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

107
themes/kanagawa/rofi.rasi Normal file
View File

@@ -0,0 +1,107 @@
/**
* Nomarchy Kanagawa — rofi whole-swap (themes/<slug>/rofi.rasi).
* Ink-and-paper identity: a sumi-ink canvas inside a warm washi-paper frame
* (the same text-toned border the theme uses on windows — the only warm
* frame of the four), the inputbar a deeper ink well, and the crystal-blue
* wave reserved for the selected row. Soft rounding, an unhurried feel.
* The `configuration {}` block is omitted on purpose — modi/terminal/
* show-icons come from modules/home/rofi.nix.
*/
* {
sumi: #1f1f28; /* sumi ink — the canvas */
well: #090618; /* deeper ink — inputbar + zebra */
paper: #dcd7ba; /* washi paper — text + frame */
parch: #c8c093; /* parchment — the prompt */
grey: #727169;
wave: #7e9cd8; /* crystal-blue wave — the selection */
font: "JetBrainsMono Nerd Font 14";
background-color: transparent;
text-color: @paper;
}
window {
background-color: @sumi;
border: 2px;
border-color: @paper; /* the warm paper frame (kanagawa's border tone) */
border-radius: 10px;
width: 40%;
location: center;
anchor: center;
padding: 8px;
}
mainbox {
background-color: transparent;
children: [ inputbar, message, listview ];
spacing: 0px;
}
inputbar {
children: [ prompt, entry ];
background-color: @well; /* the ink well */
text-color: @paper;
border-radius: 8px;
padding: 10px 12px;
spacing: 8px;
margin: 0px 0px 8px 0px;
}
prompt { text-color: @parch; }
entry {
text-color: @paper;
cursor: text;
placeholder: "search…";
placeholder-color: @grey;
}
listview {
background-color: transparent;
columns: 1;
lines: 8;
dynamic: true;
scrollbar: false;
spacing: 4px;
padding: 8px 0px 0px 0px;
}
element {
background-color: transparent;
text-color: @paper;
orientation: horizontal;
border-radius: 8px;
padding: 10px 14px;
spacing: 12px;
}
element alternate.normal {
background-color: @well;
}
element selected.normal {
background-color: @wave;
text-color: @sumi;
}
element-icon {
background-color: transparent;
size: 36px;
cursor: inherit;
}
element-text {
background-color: transparent;
text-color: inherit;
highlight: bold;
vertical-align: 0.5;
}
message { padding: 8px 0px 0px 0px; }
textbox {
background-color: @well;
text-color: @paper;
border-radius: 8px;
padding: 8px 12px;
}
scrollbar { width: 0px; }

BIN
themes/lumon/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

110
themes/lumon/rofi.rasi Normal file
View File

@@ -0,0 +1,110 @@
/**
* Nomarchy Lumon — rofi whole-swap (themes/<slug>/rofi.rasi).
* Clinical-terminal identity (the MDR readout): a thick cyan bezel around a
* cold teal screen, the inputbar FRAMED rather than filled (a data field,
* not a button), near-white headings, cyan selection. Faintly softened
* corners keep it boxy without going razor-sharp like Retro 82.
* The `configuration {}` block is omitted on purpose — modi/terminal/
* show-icons come from modules/home/rofi.nix.
*/
* {
base: #16242d;
surface: #1b2d40;
overlay: #304860;
text: #d6e2ee;
ice: #f2fcff; /* near-white — the prompt */
cyan: #8bc9eb; /* the Lumon glow — bezel + select */
font: "JetBrainsMono Nerd Font 13";
background-color: transparent;
text-color: @text;
}
window {
background-color: @base;
border: 3px; /* a screen bezel */
border-color: @cyan;
border-radius: 4px; /* boxy, faintly softened */
width: 40%;
location: center;
anchor: center;
padding: 10px;
}
mainbox {
background-color: transparent;
children: [ inputbar, message, listview ];
spacing: 0px;
}
/* Framed, not filled — a readout field rather than a button. */
inputbar {
children: [ prompt, entry ];
background-color: transparent;
text-color: @text;
border: 1px;
border-color: @overlay;
border-radius: 4px;
padding: 10px 12px;
spacing: 8px;
margin: 0px 0px 8px 0px;
}
prompt { text-color: @ice; }
entry {
text-color: @text;
cursor: text;
placeholder: "refine…";
placeholder-color: @overlay;
}
listview {
background-color: transparent;
columns: 1;
lines: 8;
dynamic: true;
scrollbar: false;
spacing: 4px;
padding: 8px 0px 0px 0px;
}
element {
background-color: transparent;
text-color: @text;
orientation: horizontal;
border-radius: 4px;
padding: 10px 14px;
spacing: 12px;
}
element alternate.normal {
background-color: @surface;
}
element selected.normal {
background-color: @cyan;
text-color: @base;
}
element-icon {
background-color: transparent;
size: 36px;
cursor: inherit;
}
element-text {
background-color: transparent;
text-color: inherit;
highlight: bold;
vertical-align: 0.5;
}
message { padding: 8px 0px 0px 0px; }
textbox {
background-color: @surface;
text-color: @text;
border-radius: 4px;
padding: 8px 12px;
}
scrollbar { width: 0px; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

BIN
themes/miasma/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
themes/nord/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

110
themes/nord/rofi.rasi Normal file
View File

@@ -0,0 +1,110 @@
/**
* Nomarchy Nord — rofi whole-swap (themes/<slug>/rofi.rasi).
* Frost identity: a soft, rounded "frost panel". The window wears a calm
* frost border; the selected row lifts to the brighter frost, and the
* prompt picks up the aurora purple for the one point of warmth.
* The `configuration {}` block is omitted on purpose — modi/terminal/
* show-icons come from modules/home/rofi.nix. Element structure mirrors
* the generated theme so the theme-grid picker's per-invocation
* -theme-str (vertical cards) still lays out correctly.
*/
* {
base: #2e3440;
surface: #3b4252;
overlay: #4c566a;
text: #d8dee9;
frost1: #81a1c1; /* calm frost — the border */
frost2: #88c0d0; /* bright frost — the selection */
aurora: #b48ead; /* the single accent pop — prompt */
font: "JetBrainsMono Nerd Font 13";
background-color: transparent;
text-color: @text;
}
window {
background-color: @base;
border: 2px;
border-color: @frost1;
border-radius: 14px; /* Nord is soft and rounded */
width: 40%;
location: center;
anchor: center;
padding: 8px;
}
mainbox {
background-color: transparent;
children: [ inputbar, message, listview ];
spacing: 0px;
}
inputbar {
children: [ prompt, entry ];
background-color: @surface;
text-color: @text;
border-radius: 10px;
padding: 10px 12px;
spacing: 8px;
margin: 0px 0px 8px 0px;
}
prompt { text-color: @aurora; }
entry {
text-color: @text;
cursor: text;
placeholder: "Search…";
placeholder-color: @overlay;
}
listview {
background-color: transparent;
columns: 1;
lines: 8;
dynamic: true;
scrollbar: false;
spacing: 4px;
padding: 8px 0px 0px 0px;
}
element {
background-color: transparent;
text-color: @text;
orientation: horizontal;
border-radius: 8px;
padding: 10px 14px;
spacing: 12px;
}
/* Faint frost-grey zebra; the selected row below still wins. */
element alternate.normal {
background-color: @surface;
}
element selected.normal {
background-color: @frost2;
text-color: @base;
}
element-icon {
background-color: transparent;
size: 36px;
cursor: inherit;
}
element-text {
background-color: transparent;
text-color: inherit;
highlight: bold;
vertical-align: 0.5;
}
message { padding: 8px 0px 0px 0px; }
textbox {
background-color: @surface;
text-color: @text;
border-radius: 8px;
padding: 8px 12px;
}
scrollbar { width: 0px; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
themes/retro-82/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

109
themes/retro-82/rofi.rasi Normal file
View File

@@ -0,0 +1,109 @@
/**
* Nomarchy Retro 82 — rofi whole-swap (themes/<slug>/rofi.rasi).
* CRT-terminal identity: SQUARE corners (the giveaway against every rounded
* theme), amber-on-navy phosphor, a monospace face. The window wears a hard
* amber frame; the inputbar sits over a teal underline (a stray scanline),
* and the prompt + selected row burn amber.
* The `configuration {}` block is omitted on purpose — modi/terminal/
* show-icons come from modules/home/rofi.nix.
*/
* {
navy: #05182e;
panel: #00172e;
overlay: #134e5a;
sand: #f6dcac; /* phosphor cream — the foreground */
teal: #3f8f8a; /* the underline scanline */
amber: #faa968; /* the glow — frame, prompt, select */
font: "JetBrainsMono Nerd Font 13";
background-color: transparent;
text-color: @sand;
}
window {
background-color: @navy;
border: 2px;
border-color: @amber;
border-radius: 0px; /* sharp: this is a terminal */
width: 40%;
location: center;
anchor: center;
padding: 10px;
}
mainbox {
background-color: transparent;
children: [ inputbar, message, listview ];
spacing: 0px;
}
inputbar {
children: [ prompt, entry ];
background-color: @panel;
text-color: @sand;
border-bottom: 2px; /* a teal scanline under the field */
border-color: @teal;
border-radius: 0px;
padding: 10px 12px;
spacing: 8px;
margin: 0px 0px 8px 0px;
}
prompt { text-color: @amber; }
entry {
text-color: @sand;
cursor: text;
placeholder: "search…";
placeholder-color: @overlay;
}
listview {
background-color: transparent;
columns: 1;
lines: 8;
dynamic: true;
scrollbar: false;
spacing: 2px;
padding: 8px 0px 0px 0px;
}
element {
background-color: transparent;
text-color: @sand;
orientation: horizontal;
border-radius: 0px; /* square rows too */
padding: 10px 14px;
spacing: 12px;
}
element alternate.normal {
background-color: @panel;
}
element selected.normal {
background-color: @amber;
text-color: @navy;
}
element-icon {
background-color: transparent;
size: 36px;
cursor: inherit;
}
element-text {
background-color: transparent;
text-color: inherit;
highlight: bold;
vertical-align: 0.5;
}
message { padding: 8px 0px 0px 0px; }
textbox {
background-color: @panel;
text-color: @sand;
border-radius: 0px;
padding: 8px 12px;
}
scrollbar { width: 0px; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -47,8 +47,9 @@ window#waybar {
#clock-date, #clock-date,
#workspaces, #workspaces,
#pulseaudio, #pulseaudio,
#network,
#custom-powerprofile, #custom-powerprofile,
#custom-nightlight,
#custom-updates,
#battery, #battery,
#tray, #tray,
#custom-notification, #custom-notification,
@@ -71,6 +72,11 @@ window#waybar {
padding: 0px; padding: 0px;
} }
/* The power-profile speedometer glyph renders small in its em box. */
#custom-powerprofile {
font-size: 20px;
}
#workspaces button.active { #workspaces button.active {
background-color: @blue; background-color: @blue;
color: @bg0; color: @bg0;

View File

@@ -11,7 +11,7 @@
"modules-left": ["custom/launcher", "clock", "clock#date"], "modules-left": ["custom/launcher", "clock", "clock#date"],
"modules-center": ["hyprland/workspaces"], "modules-center": ["hyprland/workspaces"],
"modules-right": ["pulseaudio", "network", "custom/powerprofile", "battery", "tray", "custom/notification", "custom/powermenu"], "modules-right": ["pulseaudio", "custom/powerprofile", "custom/nightlight", "custom/updates", "battery", "tray", "custom/notification", "custom/powermenu"],
"hyprland/workspaces": { "hyprland/workspaces": {
"disable-scroll": true, "disable-scroll": true,
@@ -46,14 +46,6 @@
"tooltip-format": "Playing at {volume}%" "tooltip-format": "Playing at {volume}%"
}, },
"network": {
"format-wifi": " {signalStrength}%",
"format-ethernet": "",
"format-disconnected": "󰤭",
"on-click": "sh -c '$TERMINAL -e nmtui'",
"tooltip-format": "{ipaddr} via {gwaddr}"
},
"battery": { "battery": {
"interval": 60, "interval": 60,
"states": { "warning": 30, "critical": 15 }, "states": { "warning": 30, "critical": 15 },
@@ -87,6 +79,21 @@
"on-click": "nomarchy-powerprofile-cycle" "on-click": "nomarchy-powerprofile-cycle"
}, },
"custom/nightlight": {
"return-type": "json",
"interval": 3,
"exec": "nomarchy-nightlight status",
"on-click": "nomarchy-nightlight toggle"
},
"custom/updates": {
"return-type": "json",
"interval": 1800,
"signal": 9,
"exec": "nomarchy-updates status",
"on-click": "sh -c '$TERMINAL -e nomarchy-updates upgrade'"
},
"custom/notification": { "custom/notification": {
"format": "{icon}", "format": "{icon}",
"return-type": "json", "return-type": "json",

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -53,8 +53,9 @@ window#waybar {
#clock.date, #clock.date,
#workspaces, #workspaces,
#pulseaudio, #pulseaudio,
#network,
#custom-powerprofile, #custom-powerprofile,
#custom-nightlight,
#custom-updates,
#idle_inhibitor, #idle_inhibitor,
#battery, #battery,
#custom-notification, #custom-notification,
@@ -75,6 +76,11 @@ window#waybar {
color: @fg; color: @fg;
} }
/* The power-profile speedometer glyph renders small in its em box. */
#custom-powerprofile {
font-size: 17px;
}
#tray > .needs-attention { #tray > .needs-attention {
color: @green; color: @green;
} }

View File

@@ -11,7 +11,7 @@
"modules-left": ["custom/nomarchy", "clock", "clock#date"], "modules-left": ["custom/nomarchy", "clock", "clock#date"],
"modules-center": ["hyprland/workspaces"], "modules-center": ["hyprland/workspaces"],
"modules-right": ["idle_inhibitor", "pulseaudio", "network", "custom/powerprofile", "battery", "tray", "custom/notification", "custom/powermenu"], "modules-right": ["idle_inhibitor", "pulseaudio", "custom/powerprofile", "custom/nightlight", "custom/updates", "battery", "tray", "custom/notification", "custom/powermenu"],
"hyprland/workspaces": { "hyprland/workspaces": {
"disable-scroll": true, "disable-scroll": true,
@@ -55,14 +55,6 @@
"tooltip-format": "{desc} | {volume}%" "tooltip-format": "{desc} | {volume}%"
}, },
"network": {
"format-wifi": " {essid}",
"format-ethernet": "",
"format-disconnected": "󰤭",
"on-click": "sh -c '$TERMINAL -e nmtui'",
"tooltip-format": "{ipaddr} via {gwaddr}"
},
"battery": { "battery": {
"interval": 30, "interval": 30,
"states": { "warning": 25, "critical": 10 }, "states": { "warning": 25, "critical": 10 },
@@ -94,6 +86,21 @@
"on-click": "nomarchy-powerprofile-cycle" "on-click": "nomarchy-powerprofile-cycle"
}, },
"custom/nightlight": {
"return-type": "json",
"interval": 3,
"exec": "nomarchy-nightlight status",
"on-click": "nomarchy-nightlight toggle"
},
"custom/updates": {
"return-type": "json",
"interval": 1800,
"signal": 9,
"exec": "nomarchy-updates status",
"on-click": "sh -c '$TERMINAL -e nomarchy-updates upgrade'"
},
"custom/notification": { "custom/notification": {
"format": "{icon}", "format": "{icon}",
"return-type": "json", "return-type": "json",

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
themes/white/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB