Compare commits
45 Commits
v1
...
f70838c5b5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f70838c5b5 | ||
|
|
1e4427f6af | ||
|
|
a47aa3aff5 | ||
|
|
baab2d3b88 | ||
|
|
5ea4f0c9ac | ||
|
|
938753273d | ||
|
|
46af2f0632 | ||
|
|
bc4e8e1410 | ||
|
|
4c2ad38656 | ||
|
|
6d70bba8e6 | ||
|
|
9726ba3b2f | ||
|
|
4024da791f | ||
|
|
aac678335c | ||
|
|
7d6d74fd7f | ||
|
|
cdd1897b14 | ||
|
|
9976ea06f5 | ||
|
|
cdfe92a089 | ||
|
|
431af618cc | ||
|
|
47526ae6e2 | ||
|
|
86802f244e | ||
|
|
f3325385c1 | ||
|
|
aed41793f8 | ||
|
|
685126ab47 | ||
|
|
c8d0b09044 | ||
|
|
c2f90c7d0a | ||
|
|
5747dc9839 | ||
|
|
d1344712b8 | ||
|
|
37204f5f45 | ||
| 97b5944dc1 | |||
| 0c483f9512 | |||
| dfb57c2e34 | |||
| 995810927d | |||
| e1cf190dd2 | |||
|
|
70334e68bb | ||
|
|
6a4af69b0f | ||
|
|
d9466d6555 | ||
|
|
0e42763aea | ||
|
|
a6d6860054 | ||
|
|
5c43a93285 | ||
|
|
09c308b93c | ||
|
|
bdf20f2d8e | ||
|
|
c57d26864e | ||
|
|
019fdfc8bb | ||
|
|
c2281dbc61 | ||
|
|
b4fe52261b |
101
.gitea/workflows/check.yml
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Nomarchy CI — the always-on net under direct-to-main pushes.
|
||||||
|
#
|
||||||
|
# Scope (deliberate): the EVAL tier only. The runner behind this Gitea
|
||||||
|
# instance is act_runner + docker containers (the legacy repo's check.yml
|
||||||
|
# ran 57 times on it) — no systemd, no /dev/kvm — so the checks.* VM
|
||||||
|
# tests and full toplevel builds can't run here. `nix flake check
|
||||||
|
# --no-build` still catches most breakage (type errors, missing options,
|
||||||
|
# bad merges, template/mkFlake drift — see docs/TESTING.md §1); the VM
|
||||||
|
# suite stays a local/promotion gate until a KVM-capable NixOS runner
|
||||||
|
# exists (see the commented vm-checks job at the bottom).
|
||||||
|
#
|
||||||
|
# Inherited-from-legacy gotchas (learned over 57 runs, kept verbatim):
|
||||||
|
# - Single-user Nix (--no-daemon): no systemd in the container. The
|
||||||
|
# installer runs as root and honours build-users-group=nixbld from
|
||||||
|
# its bundled nix.conf, aborting unless the group exists AND has
|
||||||
|
# members — create nixbld + users first.
|
||||||
|
# - sandbox=false: Stylix/base16.nix do import-from-derivation; the
|
||||||
|
# single-user Nix in this container can't set up the build sandbox
|
||||||
|
# (no user namespaces), which otherwise surfaces as
|
||||||
|
# "path '…-source' is not valid".
|
||||||
|
# - Pin the Nix version: 2.34's lazy-trees git cache doesn't
|
||||||
|
# materialise flake-input `-source` paths into the store, breaking
|
||||||
|
# the same IFD reads. 2.31.5 matches what wrote flake.lock.
|
||||||
|
# - Plain-shell Nix install, not a JS action: act_runner's bundled act
|
||||||
|
# tops out at node20; a `run:` step has no node-runtime coupling.
|
||||||
|
|
||||||
|
name: Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, v1]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
eval:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
env:
|
||||||
|
NIX_CONFIG: |
|
||||||
|
experimental-features = nix-command flakes
|
||||||
|
sandbox = false
|
||||||
|
NIX_SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
run: |
|
||||||
|
NIX_VERSION=2.31.5
|
||||||
|
groupadd -r nixbld 2>/dev/null || true
|
||||||
|
for i in $(seq 1 10); do
|
||||||
|
useradd -r -g nixbld -G nixbld -d /var/empty \
|
||||||
|
-s /usr/sbin/nologin -c "Nix build user $i" "nixbld$i" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
curl -L "https://releases.nixos.org/nix/nix-${NIX_VERSION}/install" | sh -s -- --no-daemon
|
||||||
|
echo "$HOME/.nix-profile/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: nix flake check (eval only)
|
||||||
|
# Full module-system evaluation of every output — both nixos
|
||||||
|
# configs, the home config, the checks.* derivations (instantiated,
|
||||||
|
# not run) and the downstream template through lib.mkFlake.
|
||||||
|
run: nix flake check --no-build
|
||||||
|
|
||||||
|
- name: Python syntax (nomarchy-theme-sync)
|
||||||
|
# Via nix shell so the step doesn't depend on the runner image
|
||||||
|
# preinstalling python3.
|
||||||
|
run: |
|
||||||
|
nix shell nixpkgs#python3 --command \
|
||||||
|
python3 -m py_compile pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py
|
||||||
|
|
||||||
|
- name: Shell syntax (tracked scripts)
|
||||||
|
# The distro's user-facing scripts are generated by Nix (their
|
||||||
|
# syntax is exercised by the eval + local builds); this covers the
|
||||||
|
# hand-written .sh files: installer helpers and maintainer tools.
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
fail=0
|
||||||
|
while IFS= read -r script; do
|
||||||
|
head -1 "$script" | grep -qE '^#!.*\b(bash|sh)\b' || continue
|
||||||
|
if ! bash -n "$script"; then
|
||||||
|
echo "::error file=$script::bash syntax error"
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done < <(git ls-files '*.sh')
|
||||||
|
exit "$fail"
|
||||||
|
|
||||||
|
# ── vm-checks (DISABLED until a KVM runner exists) ────────────────────
|
||||||
|
# The real prize: running the checks.* VM suite + a toplevel build in
|
||||||
|
# CI. Needs a runner on a NixOS (or at least nix + /dev/kvm) host —
|
||||||
|
# register one with a dedicated label, then uncomment and set runs-on
|
||||||
|
# to that label. Do NOT enable this against a label that doesn't
|
||||||
|
# exist: Gitea queues such jobs forever instead of failing.
|
||||||
|
#
|
||||||
|
# vm-checks:
|
||||||
|
# runs-on: nix-kvm
|
||||||
|
# timeout-minutes: 120
|
||||||
|
# steps:
|
||||||
|
# - uses: actions/checkout@v4
|
||||||
|
# - run: nix flake check # builds + runs the VM tests
|
||||||
|
# - run: nix build .#nixosConfigurations.nomarchy.config.system.build.toplevel --no-link
|
||||||
|
# - run: nix build .#homeConfigurations.nomarchy.activationPackage --no-link
|
||||||
25
CLAUDE.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Nomarchy — agent entry point
|
||||||
|
|
||||||
|
Nomarchy is a NixOS-based distro: rock-stable, fully reproducible, themed
|
||||||
|
from one JSON, configured through a menu that writes into the user's own
|
||||||
|
flake checkout. Read the README for the architecture.
|
||||||
|
|
||||||
|
## If you're here to work autonomously (the loop)
|
||||||
|
Follow **`agent/LOOP.md`** — it defines one iteration: orient
|
||||||
|
(GOALS/BACKLOG/JOURNAL/MEMORY), take the top actionable BACKLOG item,
|
||||||
|
verify up the V0–V3 ladder, commit+push on `main`, record. All loop state
|
||||||
|
is git-tracked in `agent/`.
|
||||||
|
|
||||||
|
## Rules that apply to every session, loop or not
|
||||||
|
- **Honesty rule** (docs/TESTING.md): for visual/interactive changes,
|
||||||
|
evaluation is not rendering — state exactly what you verified and at
|
||||||
|
which tier. Cheap check first, always: `nix flake check --no-build`.
|
||||||
|
- **Conventions** (`agent/CONVENTIONS.md`): in-flake state, menu
|
||||||
|
placement, Waybar parity with the summer whole-swaps, toggle-vs-package
|
||||||
|
discipline, commented template examples for opt-ins.
|
||||||
|
- **Git:** direct commits on `main`, pushed; `v1` is the human-only
|
||||||
|
release pointer — never touch it. Never run `nix flake update` unless
|
||||||
|
the task is a lock bump. No formatter — match the aligned
|
||||||
|
hand-formatting.
|
||||||
|
- Machine-specifics live in `hosts/`, the distro in `modules/`, data in
|
||||||
|
`themes/`, code in `pkgs/`, maintainer tools in `tools/`.
|
||||||
72
README.md
@@ -76,7 +76,9 @@ Flat on purpose. Two module trees, one options file each, no hidden layers.
|
|||||||
├── templates/downstream/ # `nix flake init -t` starter for users
|
├── templates/downstream/ # `nix flake init -t` starter for users
|
||||||
├── docs/TESTING.md # how to verify changes (incl. AI-agent rules)
|
├── docs/TESTING.md # how to verify changes (incl. AI-agent rules)
|
||||||
├── docs/OVERRIDES.md # how downstream users override defaults
|
├── docs/OVERRIDES.md # how downstream users override defaults
|
||||||
├── docs/ROADMAP.md # forward-looking plans + shipped-fixes log
|
├── docs/ROADMAP.md # design/decision records + shipped-fixes log
|
||||||
|
├── agent/ # autonomous-agent loop: protocol (LOOP.md),
|
||||||
|
│ # prioritized BACKLOG, journal, memory
|
||||||
└── tools/ # maintainer-only
|
└── tools/ # maintainer-only
|
||||||
├── import-palettes.py # converts old-distro themes → JSON + assets
|
├── import-palettes.py # converts old-distro themes → JSON + assets
|
||||||
├── test-live-iso.sh # build the ISO + boot it in QEMU
|
├── test-live-iso.sh # build the ISO + boot it in QEMU
|
||||||
@@ -182,6 +184,13 @@ behaviour (input/misc/monitor/chrome) is `mkDefault` so a plain `home.nix`
|
|||||||
assignment wins, and bind/exec-once lists concatenate. Full guide with
|
assignment wins, and bind/exec-once lists concatenate. Full guide with
|
||||||
examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
||||||
|
|
||||||
|
**Where each option goes.** `nomarchy.system.*`, `nomarchy.hardware.*`, and
|
||||||
|
`nomarchy.services.*` are NixOS options — set them in `system.nix`. Everything
|
||||||
|
else under `nomarchy.*` is a Home Manager option — set it in `home.nix`. The
|
||||||
|
two tables below are split along exactly that line.
|
||||||
|
|
||||||
|
**`home.nix`** (Home Manager — the desktop):
|
||||||
|
|
||||||
| Option | Default | Purpose |
|
| Option | Default | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `nomarchy.stateFile` | — (required) | Path to your theme-state.json |
|
| `nomarchy.stateFile` | — (required) | Path to your theme-state.json |
|
||||||
@@ -190,7 +199,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; off by default — enable it from the menu (System › Night light; the first enable rebuilds), then toggle on/off instantly from the menu or the Waybar moon indicator (writes `settings.nightlight.on` in your flake, no rebuild) and it survives reboot, so it stays reproducible |
|
||||||
|
| `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 |
|
||||||
@@ -207,6 +217,11 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
|||||||
| `nomarchy.displays.enable` | `true` | nwg-displays interactive monitor arranger (helper for `nomarchy.monitors`) |
|
| `nomarchy.displays.enable` | `true` | nwg-displays interactive monitor arranger (helper for `nomarchy.monitors`) |
|
||||||
| `nomarchy.monitors` | `[]` | Declarative per-output layout → Hyprland `monitor` rules (applied on hotplug); `,preferred,auto,1` wildcard kept as fallback |
|
| `nomarchy.monitors` | `[]` | Declarative per-output layout → Hyprland `monitor` rules (applied on hotplug); `,preferred,auto,1` wildcard kept as fallback |
|
||||||
| `nomarchy.themesDir` | Nomarchy's `themes/` | Where per-theme app overrides are probed |
|
| `nomarchy.themesDir` | Nomarchy's `themes/` | Where per-theme app overrides are probed |
|
||||||
|
|
||||||
|
**`system.nix`** (NixOS — the machine):
|
||||||
|
|
||||||
|
| Option | Default | Purpose |
|
||||||
|
|---|---|---|
|
||||||
| `nomarchy.system.plymouth.enable` | `true` | Branded boot splash, background from the theme JSON (recolors on system rebuilds) |
|
| `nomarchy.system.plymouth.enable` | `true` | Branded boot splash, background from the theme JSON (recolors on system rebuilds) |
|
||||||
| `nomarchy.system.fileManager.enable` | `true` | Thunar GUI + gvfs/tumbler/udisks2 (the "open folder" handler) |
|
| `nomarchy.system.fileManager.enable` | `true` | Thunar GUI + gvfs/tumbler/udisks2 (the "open folder" handler) |
|
||||||
| `nomarchy.system.greeter.enable` | `true` | greetd/tuigreet |
|
| `nomarchy.system.greeter.enable` | `true` | greetd/tuigreet |
|
||||||
@@ -219,6 +234,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) |
|
||||||
@@ -238,11 +261,16 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
|||||||
| `nomarchy.services.restic.enable` | `false` | Opt-in: scheduled daily restic backup (set `.repository` + `.passwordFile`; 7/4/6 retention; list/restore via the `restic-nomarchy` wrapper) |
|
| `nomarchy.services.restic.enable` | `false` | Opt-in: scheduled daily restic backup (set `.repository` + `.passwordFile`; 7/4/6 retention; list/restore via the `restic-nomarchy` wrapper) |
|
||||||
|
|
||||||
Beyond the `nomarchy.*` surface, the system layer turns on the usual
|
Beyond the `nomarchy.*` surface, the system layer turns on the usual
|
||||||
desktop services with `lib.mkDefault` (override natively). One worth
|
desktop services with `lib.mkDefault` (override natively). Two worth
|
||||||
calling out: **`services.fwupd.enable`** is on by default for firmware
|
calling out: **`services.fwupd.enable`** is on by default for firmware
|
||||||
updates via LVFS — it only refreshes metadata, never flashes on its own,
|
updates via LVFS — it only refreshes metadata, never flashes on its own,
|
||||||
so run `fwupdmgr update` to apply. Disable with `services.fwupd.enable =
|
so run `fwupdmgr update` to apply; disable with `services.fwupd.enable =
|
||||||
false;` on machines without real firmware (VMs/headless).
|
false;` on machines without real firmware (VMs/headless). And
|
||||||
|
**`services.earlyoom`** is on by default so running out of memory kills
|
||||||
|
the offending process (with a desktop notification) instead of freezing
|
||||||
|
the desktop — process-level on purpose, since a Hyprland session is one
|
||||||
|
cgroup and systemd-oomd would kill all of it (oomd is disabled
|
||||||
|
accordingly). Opt out with `services.earlyoom.enable = false;`.
|
||||||
|
|
||||||
## 4. How theming works
|
## 4. How theming works
|
||||||
|
|
||||||
@@ -271,6 +299,31 @@ imperative; nothing in Nix consumes the path): applied at session start and
|
|||||||
after every switch via a tiny activation hook, cycled instantly with
|
after every switch via a tiny activation hook, cycled instantly with
|
||||||
`bg next`.
|
`bg next`.
|
||||||
|
|
||||||
|
### Config the menu writes (not just themes)
|
||||||
|
|
||||||
|
The in-flake-state model isn't only for appearance. **Feature toggles you flip
|
||||||
|
from the menu are written into a `settings.*` section of the *same* state file**
|
||||||
|
— git-tracked, reproducible, never stashed in `~/.local/state`. The menu is just
|
||||||
|
an ergonomic writer for your flake, so version-controlling your downstream
|
||||||
|
reproduces the machine, settings and all. Where a toggle can take effect without
|
||||||
|
a rebuild it does: the menu writes the key (`--no-switch`) and flips the running
|
||||||
|
service, which reads the *live* flake state at session start, so the choice is
|
||||||
|
both instant and survives reboot.
|
||||||
|
|
||||||
|
**Night light** is the first to use this — enable it from the menu (System ›
|
||||||
|
Night light; the first enable rebuilds to install hyprsunset), then on/off is
|
||||||
|
instant. Internally it's two keys: `settings.nightlight.installed` (sticky —
|
||||||
|
gates the unit, the first enable rebuilds) and `settings.nightlight.on` (the
|
||||||
|
instant on/off). Expect more `nomarchy.*` toggles to migrate to this pattern.
|
||||||
|
|
||||||
|
**Auto-commit (opt-in):** System › Auto-commit makes every `apply`/`set`
|
||||||
|
mutation also `git commit` theme-state.json in your flake — *only* that
|
||||||
|
file, so unrelated dirty work is never swept up — turning your settings
|
||||||
|
history into `git log`. Off by default; the toggle is instant (nothing in
|
||||||
|
Nix consumes the flag), the off-write is itself committed so history stays
|
||||||
|
consistent, and wallpaper cycling (`bg next`) is deliberately excluded —
|
||||||
|
the current wallpaper rides along with the next real commit.
|
||||||
|
|
||||||
### Per-theme app assets (`themes/<slug>/`)
|
### Per-theme app assets (`themes/<slug>/`)
|
||||||
|
|
||||||
Recoloring covers 95% of theming; the rest is one optional assets directory
|
Recoloring covers 95% of theming; the rest is one optional assets directory
|
||||||
@@ -357,6 +410,9 @@ reload # exec zsh (reload the shell)
|
|||||||
|
|
||||||
## Roadmap & known issues
|
## Roadmap & known issues
|
||||||
|
|
||||||
See **[docs/ROADMAP.md](docs/ROADMAP.md)** — forward-looking plans plus the
|
The prioritized queue of what's next lives in
|
||||||
log of shipped fixes. Kept out of the README so this stays a focused entry
|
**[agent/BACKLOG.md](agent/BACKLOG.md)**; the detailed design/decision
|
||||||
point.
|
records and the log of shipped fixes stay in
|
||||||
|
**[docs/ROADMAP.md](docs/ROADMAP.md)**. Development runs on an
|
||||||
|
agent-driven loop — see **[agent/LOOP.md](agent/LOOP.md)**. Kept out of
|
||||||
|
the README so this stays a focused entry point.
|
||||||
|
|||||||
254
agent/BACKLOG.md
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
# Backlog — the prioritized task queue
|
||||||
|
|
||||||
|
The forward-looking half of the old docs/ROADMAP.md, reworked as a queue
|
||||||
|
agents can execute. Detailed design notes and decision records stay in
|
||||||
|
`docs/ROADMAP.md` (referenced as "ROADMAP § <item>"); this file is *what's
|
||||||
|
next, in what order*.
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- Agents take the topmost actionable item (see LOOP.md). Finished items
|
||||||
|
are **deleted** here — the journal + git log are the record; durable
|
||||||
|
design notes get a ✓-entry in docs/ROADMAP.md if worth keeping.
|
||||||
|
- Item numbers are **stable IDs** — never renumbered or reused. A gap in
|
||||||
|
the sequence means shipped (or dropped) work; new items take the next
|
||||||
|
free number regardless of tier.
|
||||||
|
- Tags: `[blocked:hw]` needs real hardware (see HARDWARE-QUEUE.md) ·
|
||||||
|
`[human]` needs Bernardo · `[stuck]` two failed attempts, needs help ·
|
||||||
|
`[big]` must be split before starting.
|
||||||
|
- Agents may append to **PROPOSED** and **Decisions** freely; only
|
||||||
|
Bernardo moves items *out* of PROPOSED into the tiers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NOW
|
||||||
|
|
||||||
|
*Items 21–24: real-hardware QA findings from the Latitude 5410
|
||||||
|
(Bernardo, 2026-07-04) — bugs found on metal outrank the queue.*
|
||||||
|
|
||||||
|
### 21. Waybar crash on theme switch — no respawn
|
||||||
|
Reported: after a theme switch on the Latitude, Waybar crashed and
|
||||||
|
never came back (exec-once = no supervisor; a crash orphans the session
|
||||||
|
until relogin). Two halves: (a) **resilience** — Waybar must respawn on
|
||||||
|
crash regardless of cause. exec-once a small supervisor loop (restart
|
||||||
|
with backoff; must not fight `pkill -x waybar` intentional stops), or
|
||||||
|
re-attempt the systemd user unit with an IPC-readiness gate (the naive
|
||||||
|
unit raced Hyprland IPC on relogin — see waybar.nix's comment). (b)
|
||||||
|
**root cause** — the switch path both flips `style.css` (Waybar's
|
||||||
|
`reload_style_on_change` picks it up) *and* sends SIGUSR2
|
||||||
|
(`nomarchy-theme-sync`), a double-reload during the config/css symlink
|
||||||
|
flip; suspect the race crashes Waybar's reload. Consider: drop the
|
||||||
|
SIGUSR2 when `reload_style_on_change` is on, or replace reload with a
|
||||||
|
clean restart on switch. **Verify:** V2 (kill waybar in the themed-VM
|
||||||
|
recipe → respawns; switch theme → bar survives); V3 re-check on the
|
||||||
|
Latitude.
|
||||||
|
|
||||||
|
### 22. Network menu shows nameless/weird entries
|
||||||
|
Reported: `networkmanager_dmenu` lists entries with no names (likely
|
||||||
|
hidden-SSID APs rendered as blank rows, possibly other adapters/
|
||||||
|
duplicate BSSIDs). Investigate its config.ini surface for filtering,
|
||||||
|
or patch the list source; blank rows in a themed picker read as
|
||||||
|
breakage. **Verify:** V1 (config renders) — behavior needs Wi-Fi
|
||||||
|
hardware → V3 on the Latitude.
|
||||||
|
|
||||||
|
### 23. `sys-rebuild` — rebuild without updating the lock
|
||||||
|
Reported: there's no way to rebuild the system *without* `sys-update`'s
|
||||||
|
`nix flake update` (home-update already skips the lock; the system side
|
||||||
|
lacks the twin). Add `sys-rebuild` (snapshot-first like sys-update,
|
||||||
|
rebuild against the current lock, no update) alongside `sys-update`;
|
||||||
|
document both in README §5 + the motd cheat sheet if it lists them.
|
||||||
|
**Verify:** V1 + eval that both scripts land on PATH.
|
||||||
|
|
||||||
|
### 24. "Back" audit — every list menu ends in ↩ Back
|
||||||
|
Reported: some submenu items/tools still lack a Back option. The "Back
|
||||||
|
everywhere" pass covered the hand-rolled dmenu lists at the time; audit
|
||||||
|
what's been added since (VPN, Display, power-profile, printers…) and
|
||||||
|
the external/plugin modules (networkmanager_dmenu, rofi-pulse-select,
|
||||||
|
rofi-calc/emoji modi) — hand-rolled lists get the shared `back` helper;
|
||||||
|
external modi that can't take an injected row get documented as such
|
||||||
|
(and Esc always works). **Verify:** V1 + a grep-the-generated-script
|
||||||
|
audit listing which menus have/lack Back; V3 spot-check.
|
||||||
|
|
||||||
|
### 6. Full docs review & restructure
|
||||||
|
ROADMAP § Full docs review. The roadmap/backlog split is done (this
|
||||||
|
file); remaining: reconcile every README option table against the live
|
||||||
|
`nomarchy.*` surface; drift pass over docs/OVERRIDES.md, docs/TESTING.md,
|
||||||
|
templates/downstream/README.md; read the install/first-run story end to
|
||||||
|
end; add a short **recovery runbook** (desktop won't start → boot an
|
||||||
|
older generation / `nomarchy-snapshots` / greetd journal — the pieces
|
||||||
|
exist, the story isn't written). Site-vs-markdown is a Decision (below).
|
||||||
|
**Verify:** V0 + a mechanical option-surface diff (eval the options,
|
||||||
|
compare to the tables — consider making that a permanent check).
|
||||||
|
|
||||||
|
## NEXT
|
||||||
|
|
||||||
|
### 8. Complete-workstation viewers + default applications
|
||||||
|
New. The template ships mpv but **no PDF viewer and no image viewer**,
|
||||||
|
and nothing sets xdg-mime defaults — so "open a PDF/photo" falls to
|
||||||
|
whatever GTK guesses (GIMP for images). Two halves: (a) add a themed,
|
||||||
|
lightweight PDF viewer + image viewer to the template's active
|
||||||
|
`home.packages` (candidates: zathura or GNOME Papers; imv or loupe —
|
||||||
|
prefer what Stylix themes well); (b) a mime-defaults module setting
|
||||||
|
`xdg.mimeApps` associations (browser/PDF/image/video/text, `mkDefault`)
|
||||||
|
— real config, so unlike bare packages this *is* module territory, but
|
||||||
|
it must degrade gracefully when the user deletes a package from the
|
||||||
|
suite. **Verify:** V1 + assert the rendered mimeapps.list; V3 queue an
|
||||||
|
open-a-file smoke.
|
||||||
|
|
||||||
|
### 9. Update & rollback UX
|
||||||
|
New; extends the update-awareness story to the other half: what changed,
|
||||||
|
and how do I get back. (a) `sys-update` prints a human diff of what the
|
||||||
|
update changed (`nvd diff` between system generations, or `nix store
|
||||||
|
diff-closures`); (b) a **System → Rollback** menu flow: desktop = pick a
|
||||||
|
recent `home-manager generations` entry and activate it (the theme
|
||||||
|
history is already generations); system = point at the boot-menu
|
||||||
|
generation flow + `nomarchy-snapshots` rather than reimplementing it —
|
||||||
|
destructive steps keep the typed-`yes` gate. **Why:** informative +
|
||||||
|
rock-stable means undo is one menu away, not a Nix lesson. **Verify:**
|
||||||
|
V1; V2 for the generation-activate path in a VM.
|
||||||
|
|
||||||
|
### 10. `nomarchy-doctor` — one-shot health check
|
||||||
|
New. A single command (+ System-menu entry) that checks the things that
|
||||||
|
actually break user machines and prints a themed pass/fail sheet:
|
||||||
|
failed systemd units (system + user), disk space (/, /boot, the nix
|
||||||
|
store), state-file parses + is git-tracked, downstream flake dirty/
|
||||||
|
diverged, last rebuild generation age, snapper timeline running (when
|
||||||
|
enabled). Read-only, no auto-fixing; each failure prints the one command
|
||||||
|
that fixes it. Optionally later: a self-gating Waybar warning fed by the
|
||||||
|
same script. **Verify:** V2 — a VM check with an induced failure.
|
||||||
|
|
||||||
|
### 11. State-file validation & friendly errors
|
||||||
|
New; "the user never has to master Nix" must include *error messages*.
|
||||||
|
A hand-edited theme-state.json (trailing comma, wrong type, unknown
|
||||||
|
theme slug) today surfaces as a raw Nix eval stack. Add (a) `nomarchy-
|
||||||
|
theme-sync validate` + validate-before-write on every set/apply; (b) an
|
||||||
|
eval-time schema assertion in `theme.nix` whose message says the field,
|
||||||
|
the problem, and the fix in plain language. **Verify:** V1 + a corpus of
|
||||||
|
broken-state fixtures round-tripped through validate; V2 if cheap.
|
||||||
|
|
||||||
|
### 12. Screen recording in the Capture submenu
|
||||||
|
New; screenshots ship, recording doesn't — a standard workstation need.
|
||||||
|
Extend `nomarchy-menu capture`: record region/full → `wl-screenrec`
|
||||||
|
(fallback `wf-recorder`), saved next to Screenshots, with a self-gating
|
||||||
|
Waybar recording indicator that stops the recording on click (the only
|
||||||
|
sane "stop" surface). Optional audio toggle. Parity rule applies.
|
||||||
|
**Verify:** V1 + `bash -n`; V2 headless if the software-GL recipe
|
||||||
|
supports it, else V3 queue.
|
||||||
|
|
||||||
|
### 13. Small niceties batch (one slice per iteration)
|
||||||
|
Each is a small, self-contained polish item in the existing patterns:
|
||||||
|
- **Idle-inhibit (caffeine) toggle:** Waybar `idle_inhibitor` module
|
||||||
|
(blocks hypridle lock/suspend during video/presentations) in the
|
||||||
|
GENERATED bar — summer-night already has one (reverse parity gap
|
||||||
|
found 2026-07-04); add to summer-day too + cheatsheet mention.
|
||||||
|
- **Low-battery notifications:** the bar colors at 25/10% but nothing
|
||||||
|
*notifies*; gate on `power.laptop` (poweralertd, or a small upower
|
||||||
|
watcher consistent with how the bar reads state).
|
||||||
|
- **Color picker:** `hyprpicker` → clipboard as a Tools › entry +
|
||||||
|
`SUPER+CTRL` bind; pairs naturally with theme work.
|
||||||
|
|
||||||
|
### 14. Automated upstream lock bumps (maintainer CI, slices b+c) `[big]`
|
||||||
|
ROADMAP § Automated upstream lock bumps — the scheduled half (the
|
||||||
|
checks-on-push workflow shipped; see item 20 for the runner status):
|
||||||
|
weekly job runs `nix flake update` (within pinned release branches) →
|
||||||
|
full check suite → lands on `main` on green; `v1` promotion stays
|
||||||
|
human. Plus the fast-lane note for security bumps. Note the current
|
||||||
|
runner is eval-only (no KVM) — a green bump run guards eval, not the VM
|
||||||
|
suite, until item 20's stretch runner exists.
|
||||||
|
|
||||||
|
### 20. KVM runner → VM suite in CI `[human]`
|
||||||
|
The remaining stretch of the CI item — checks-on-push is live and
|
||||||
|
**green** (run #58; runner = gitea/act_runner docker, eval tier).
|
||||||
|
Register a second runner on a host with `/dev/kvm` + nix (host-mode
|
||||||
|
label `nix-kvm`), then an agent uncomments the workflow's `vm-checks`
|
||||||
|
job: the `checks.*` VM suite + real toplevel/HM builds on every push
|
||||||
|
(also upgrades item 14's bump gate from eval-only to the full suite).
|
||||||
|
|
||||||
|
### 15. Display profiles — docked/undocked switching
|
||||||
|
ROADMAP § Display / monitor management, remaining: true profile
|
||||||
|
switching of the *same* outputs (multi-layout toggles, beyond
|
||||||
|
per-output hotplug rules), optionally workspace-to-monitor binding.
|
||||||
|
Design first (state-file shape, menu surface) — write the plan into
|
||||||
|
this entry before coding.
|
||||||
|
|
||||||
|
### 16. Greeter theming from the JSON
|
||||||
|
ROADMAP § Greeter. tuigreet themed from theme-state.json at system
|
||||||
|
rebuild (same model as Plymouth's tint). Scope: tuigreet only (no SDDM).
|
||||||
|
|
||||||
|
### 17. Launch-or-focus UX scripts
|
||||||
|
ROADMAP § launch-or-focus. Hyprland dispatch scripts: a bind launches an
|
||||||
|
app or focuses its existing window. Curate which apps get binds;
|
||||||
|
cheatsheet entries via keybinds.nix.
|
||||||
|
|
||||||
|
### 18. "nomarchy" control center + first-boot welcome `[big]`
|
||||||
|
ROADMAP § control center. A single front-end over the common toggles on
|
||||||
|
the same `nomarchy-theme-sync` surface, + a first-boot guided
|
||||||
|
"pick your theme / essentials" flow. Needs a design pass (TUI vs GUI)
|
||||||
|
→ write options into Decisions before implementing. Note: items 9–11
|
||||||
|
(rollback menu, doctor, validation) are natural panels of it — design
|
||||||
|
them as composable commands, not dead ends.
|
||||||
|
|
||||||
|
### 19. Look & Feel menu category
|
||||||
|
ROADMAP § Menu system, remaining: group Theme + night-light (+ wallpaper
|
||||||
|
cycle, future appearance toggles) under a Look & Feel submenu once it
|
||||||
|
earns ≥3 entries — keep the root at six.
|
||||||
|
|
||||||
|
## LATER
|
||||||
|
|
||||||
|
- **Wallpapers artifact split** (ROADMAP § Faster switches — decided,
|
||||||
|
deferred): pinned `Nomarchy-wallpapers` input so a state write stops
|
||||||
|
re-copying 86 MB. Follow-on: pre-built theme variants if switches are
|
||||||
|
still slow after.
|
||||||
|
- **Installer round 2** (ROADMAP § Installer): multi-disk BTRFS RAID,
|
||||||
|
impermanence, BIOS/legacy boot.
|
||||||
|
- **Boot-from-snapshot**: a systemd-boot equivalent of grub-btrfs.
|
||||||
|
- **Night-light geo mode**: lat/long auto sunset/sunrise (means wlsunset).
|
||||||
|
- **Per-theme icon overrides** / more icon packs (ROADMAP § Icon themes).
|
||||||
|
- **MIPI/IPU software-ISP camera** support (no-UVC machines).
|
||||||
|
- **OCR screenshot-to-text**: a Capture › entry (grim region → tesseract
|
||||||
|
→ clipboard) — cheap once recording (#12) reshapes the submenu.
|
||||||
|
- **Auto-timezone Waybar tooltip** showing the detected zone (optional).
|
||||||
|
- **VPN exit-node richer display** (country/city) (optional).
|
||||||
|
- **NixOS release bump → v2** `[human]`: deliberate, hand-edited, never
|
||||||
|
automated; the previous attempt was discarded (2026-06-22) over a
|
||||||
|
Hyprland OOM blocker — see MEMORY.md before retrying (NOW#3 should
|
||||||
|
also soften that blocker class).
|
||||||
|
|
||||||
|
## PROPOSED (agent suggestions — await human triage)
|
||||||
|
|
||||||
|
*Agents: append here with a one-paragraph pitch (what/why/cost). Do not
|
||||||
|
implement. Bernardo moves accepted items into a tier.*
|
||||||
|
|
||||||
|
- **Portal/Flatpak camera picker still lists the internal IR sensor**
|
||||||
|
(ROADMAP § Webcam follow-up). The shipped IR-hide is a *WirePlumber
|
||||||
|
v4l2* rule, but Flatpak/portal apps consume cameras via the
|
||||||
|
**libcamera** path, where both sensors remain visible — a Flatpak
|
||||||
|
Zoom user can still pick the black IR "camera". Options, roughly
|
||||||
|
ascending cost: (a) do nothing — document it (portal camera support
|
||||||
|
is still rare in practice); (b) a WirePlumber *libcamera* monitor
|
||||||
|
rule disabling GREY-only nodes — needs verifying that libcamera
|
||||||
|
monitor rules can match early enough (the v4l2 investigation found
|
||||||
|
only `device.api` binds pre-rule, which is why surgical scoping
|
||||||
|
failed before — same wall likely applies); (c) a libcamera
|
||||||
|
configuration/udev quirk hiding the IR sensor at the libcamera layer
|
||||||
|
itself. Cost: (b)/(c) need a T14s-style RGB+IR machine to verify →
|
||||||
|
pairs with a hardware-queue session. Recommend (a) now, (b)
|
||||||
|
investigated when the T14s is next available.
|
||||||
|
|
||||||
|
## Decisions `[human]`
|
||||||
|
|
||||||
|
Open calls only Bernardo can make; agents add options/evidence but never
|
||||||
|
decide.
|
||||||
|
|
||||||
|
- **Formatter adoption:** repo deliberately has none; `nixfmt-rfc-style`
|
||||||
|
would flatten the aligned hand-formatting of ~33 files. Adopt or
|
||||||
|
declare never?
|
||||||
|
- **Docs site vs Markdown-in-repo** (from the docs-review item).
|
||||||
|
- **Control center form factor:** TUI (gum/ratatui-style) vs GUI vs
|
||||||
|
"the rofi menu *is* the control center, just add a first-boot flow".
|
||||||
|
- **zram swap:** faster under pressure and pairs with NOW#3, but it
|
||||||
|
interacts with the hibernation-swapfile story (resume device/priority
|
||||||
|
ordering) — adopt, adopt-with-hibernation-guard, or skip?
|
||||||
|
- **Default browser:** the template comments Firefox out; item #8's mime
|
||||||
|
defaults need *something* to point `text/html` at — ship a browser
|
||||||
|
active in the suite, or leave browserless-by-default and let mime
|
||||||
|
defaults degrade?
|
||||||
70
agent/CONVENTIONS.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Conventions — how Nomarchy code is written
|
||||||
|
|
||||||
|
The standing rules an agent must follow while coding. GOALS.md says what
|
||||||
|
we're building; this says how. Details/rationale live in docs/ROADMAP.md's
|
||||||
|
decision records and the README.
|
||||||
|
|
||||||
|
## Repo layout (the rule of thumb)
|
||||||
|
`modules/` is the distro (reusable, no machine specifics) · `hosts/` is a
|
||||||
|
machine · `themes/` is data · `pkgs/` is code · `tools/` is maintainer-only
|
||||||
|
· `agent/` is loop state. If a new file doesn't obviously belong to one of
|
||||||
|
those, it probably shouldn't exist.
|
||||||
|
|
||||||
|
## Nix style
|
||||||
|
- **No formatter.** Files use deliberate aligned hand-formatting — match
|
||||||
|
the surrounding style exactly; never reflow a file you're only touching
|
||||||
|
a line of.
|
||||||
|
- Distro defaults use `lib.mkDefault` so a plain downstream assignment
|
||||||
|
wins; bind/exec lists concatenate. Behaviour options overridable,
|
||||||
|
appearance flows from the state JSON.
|
||||||
|
- Options live in the existing surfaces: `nomarchy.system.*` /
|
||||||
|
`nomarchy.hardware.*` / `nomarchy.services.*` (NixOS, `system.nix`),
|
||||||
|
everything else `nomarchy.*` (HM, `home.nix`). Update the README tables
|
||||||
|
when the surface changes.
|
||||||
|
|
||||||
|
## Feature design
|
||||||
|
- **In-flake state:** any user-settable config gets a menu writer that
|
||||||
|
lands it in `theme-state.json` (`settings.*`), git-tracked. No
|
||||||
|
`~/.local/state`, no side files. Instant-effect where possible
|
||||||
|
(`--no-switch` + flip the service; the service reads the *live* working
|
||||||
|
tree at start — the night-light `ExecCondition` pattern). Rebuild-baked
|
||||||
|
values graduate via `mkDefault` reads of the settings key.
|
||||||
|
- **Toggle vs package:** a `nomarchy.*` toggle only when there's real
|
||||||
|
config behind it (units, groups, udev, firewall). A bare package goes
|
||||||
|
in the downstream template's `home.packages` — opt-out is deleting the
|
||||||
|
line.
|
||||||
|
- **Opt-in features** ship a commented example in
|
||||||
|
`templates/downstream/home.nix` or `system.nix`.
|
||||||
|
- **Menu:** new entries go in the right submenu (Tools › / System ›; root
|
||||||
|
stays six entries), end lists with the shared `↩ Back`, self-gate on
|
||||||
|
the feature's availability, and add the direct
|
||||||
|
`SUPER+CTRL+<mnemonic>` bind in `keybinds.nix` (single source — it
|
||||||
|
feeds both Hyprland and the SUPER+? cheatsheet).
|
||||||
|
- **Waybar:** new indicators self-gate (hidden when irrelevant), use
|
||||||
|
named `writeShellScriptBin`s on PATH (so static configs can exec them
|
||||||
|
by bare name), and are added to **both** the generated `waybar.nix`
|
||||||
|
config **and** the summer-day/night `waybar.jsonc` whole-swaps (the
|
||||||
|
parity rule).
|
||||||
|
- **Theming:** every new visual surface consumes the palette from the
|
||||||
|
state JSON. There is no second renderer to keep in sync — add the key
|
||||||
|
to the JSON, consume it in the module.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- docs/TESTING.md is canonical; LOOP.md's ladder (V0–V3) sets the
|
||||||
|
required tier. Cheap first: `nix flake check --no-build`, `bash -n`,
|
||||||
|
`py_compile`.
|
||||||
|
- Prefer a permanent `checks.*` runNixOSTest over a one-off manual poke;
|
||||||
|
reusable recipes (headless Hyprland with software GL, QMP screenshots,
|
||||||
|
udev-event fakes) are indexed in MEMORY.md and demonstrated by the
|
||||||
|
existing checks (`distro-id`, `hardware-toggles`,
|
||||||
|
`battery-charge-limit`).
|
||||||
|
- The honesty rule: report exactly what you verified and at which tier.
|
||||||
|
|
||||||
|
## Git
|
||||||
|
- `main` is development (direct commits, pushed); `v1` is the release
|
||||||
|
pointer — **human-only, fast-forward-only, never touched by agents**.
|
||||||
|
- Commit style: `feat|fix|test|docs|chore(scope): summary`, body with
|
||||||
|
what/why + verification tier + what remains. Bookkeeping (`agent/`
|
||||||
|
updates) rides in the same commit as the change.
|
||||||
|
- `flake.lock` moves only when the task *is* a lock bump, and only within
|
||||||
|
the pinned release branches.
|
||||||
58
agent/GOALS.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Goals — what "done" looks like for Nomarchy
|
||||||
|
|
||||||
|
The north star every loop iteration serves. When two options conflict,
|
||||||
|
the earlier pillar wins.
|
||||||
|
|
||||||
|
## The four pillars (in priority order)
|
||||||
|
|
||||||
|
1. **Rock-stable.** A workstation you never fight. Everything is a NixOS/HM
|
||||||
|
generation — atomic, rollbackable, never partial. `nix flake check` is
|
||||||
|
green at every commit on `main`. Regressions are caught by the VM-test
|
||||||
|
suite (`checks.*`), not by users. `v1` only ever advances after human
|
||||||
|
on-hardware QA.
|
||||||
|
2. **Reproducible, with zero hidden state.** The downstream flake checkout
|
||||||
|
*is* the machine. All user-settable config is menu-writable into the
|
||||||
|
git-tracked state file (`theme-state.json` `settings.*`) — never
|
||||||
|
`~/.local/state`, never `~/.config` side files. Re-cloning your flake
|
||||||
|
reproduces the machine, settings and all.
|
||||||
|
3. **Effortless to configure.** The user never has to learn Nix. Every
|
||||||
|
common knob is reachable from `nomarchy-menu` (SUPER+M); the menu is an
|
||||||
|
ergonomic writer for the flake. Where a toggle can take effect without a
|
||||||
|
rebuild, it must (`--no-switch` + flip the running service).
|
||||||
|
4. **Beautiful.** One JSON themes the entire desktop coherently — Hyprland,
|
||||||
|
Waybar, Ghostty, btop, rofi, GTK/Qt, boot splash, greeter. Every new
|
||||||
|
surface follows the palette. Informative, self-gating Waybar modules
|
||||||
|
(they hide when irrelevant). No unthemed corner survives contact with
|
||||||
|
the theme switcher.
|
||||||
|
|
||||||
|
## Quality bars (non-negotiable)
|
||||||
|
|
||||||
|
- **The honesty rule** (docs/TESTING.md): for anything visual, "the Nix
|
||||||
|
evaluates" is not "it renders". Verify at the highest tier you can reach
|
||||||
|
(see LOOP.md's verification ladder) and *state the tier you reached*.
|
||||||
|
Never claim a check you didn't run.
|
||||||
|
- **Parity rule:** any module added to the generated Waybar config must
|
||||||
|
also be added to the summer-day/night `waybar.jsonc` whole-swaps.
|
||||||
|
- **Menu placement:** new menu entries go in the right category submenu
|
||||||
|
(Tools › / System ›), with a direct `SUPER+CTRL+<mnemonic>` bind and
|
||||||
|
self-gating where applicable. The root picker stays six entries.
|
||||||
|
- **Opt-in features** ship a commented example in
|
||||||
|
`templates/downstream/{home,system}.nix`.
|
||||||
|
- **Option surface discipline:** a toggle exists only when there is real
|
||||||
|
config behind it. Bare package installs go in the template's
|
||||||
|
`home.packages` (opt-out = delete the line), never a `nomarchy.apps.*`.
|
||||||
|
|
||||||
|
## Non-goals (do not drift into these)
|
||||||
|
|
||||||
|
- **No binary cache.** Compile-from-source is a deliberate values call;
|
||||||
|
automation targets the config/lock channel, not artifact distribution.
|
||||||
|
- **No second theming pipeline.** The dispatcher owns menu structure; the
|
||||||
|
renderer (rofi) stays swappable. No GTK4 launcher.
|
||||||
|
- **No nixpkgs major bump on `main`.** Lock updates stay within the pinned
|
||||||
|
release branch; a release jump is a deliberate `v2`, hand-edited by the
|
||||||
|
maintainer (the last attempt was discarded over a Hyprland OOM — see
|
||||||
|
agent/MEMORY.md).
|
||||||
|
- **No repo-wide reformat.** The `.nix` files use deliberate aligned
|
||||||
|
hand-formatting; adopting a formatter is an open maintainer decision
|
||||||
|
(BACKLOG.md § Decisions), not a cleanup.
|
||||||
|
- **No multi-DE.** Hyprland is the desktop.
|
||||||
85
agent/HARDWARE-QUEUE.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Hardware queue — V3 checks only a human can run
|
||||||
|
|
||||||
|
Everything shipped at V1/V2 whose final verification needs real hardware.
|
||||||
|
Agents **append** (newest at the bottom of a section) with exact steps;
|
||||||
|
Bernardo runs them and either checks off (`[x]` + date + verdict) or files
|
||||||
|
the failure as a NOW bug in BACKLOG.md. Machines: the **AMD dev box**
|
||||||
|
(Ryzen AI laptop: AMD + fingerprint + NPU), the **Latitude 5410** (Intel
|
||||||
|
QA machine), the **T14s** (webcam case).
|
||||||
|
|
||||||
|
## Any machine (dev box is fine)
|
||||||
|
- [ ] **Battery charge limit re-apply on unplug** — `sudo nixos-rebuild
|
||||||
|
switch` with `power.batteryChargeLimit = 80`, physically unplug/replug
|
||||||
|
AC, confirm `charge_control_end_threshold` re-reads 80. (udev trigger
|
||||||
|
already VM-verified.)
|
||||||
|
- [ ] **SSH_AUTH_SOCK for GUI clients** — after relogin, launch a GUI git
|
||||||
|
client (or `rofi`-launched terminal-less app) and confirm it reaches
|
||||||
|
gpg-agent's SSH socket without an interactive shell parent.
|
||||||
|
- [ ] **Night-light full cycle** — first menu enable rebuilds + hyprsunset
|
||||||
|
starts; later toggles are instant (no rebuild); an *off* survives
|
||||||
|
reboot (ExecCondition); stopping hyprsunset restores gamma.
|
||||||
|
- [ ] **Auto-timezone** — enable from the menu; confirm geoclue finds the
|
||||||
|
zone, `/etc/localtime` updates, and the Waybar clock refreshes
|
||||||
|
(SIGUSR2 watcher) — also after a manual `timedatectl set-timezone`.
|
||||||
|
- [ ] **Keyboard layout cycle bind** — with a comma layout (e.g.
|
||||||
|
`nomarchy.keyboard.layout = "us,de"`), SUPER+SHIFT+K cycles the
|
||||||
|
focused keyboard's layout, the Waybar `` indicator follows, and
|
||||||
|
the row shows in the SUPER+? cheatsheet.
|
||||||
|
- [ ] **Keyboard hotplug picker (re-verify after in-flake graduation)** —
|
||||||
|
plug an external keyboard post-login, pick a layout in rofi, confirm
|
||||||
|
it applies per-device only, persists in `settings.keyboard.devices`,
|
||||||
|
and graduates into a `device{}` block on the next rebuild.
|
||||||
|
- [ ] **Snapshots restore/rollback** — ⚠ PRECONDITION: update to main ≥
|
||||||
|
a47aa3a and RELOGIN first (the polkit agent starts with the session).
|
||||||
|
Bernardo's 2026-07-04 Latitude findings 5/6 ("btrfs-assistant still
|
||||||
|
crashes", "menu snapshots shows nothing") match the PRE-fix behavior
|
||||||
|
exactly: no agent → pkexec fails silently (= menu does nothing), and
|
||||||
|
a direct unprivileged run crashes (= the libbtrfsutil bug). If either
|
||||||
|
still reproduces ON THE FIXED BUILD after relogin, reopen BACKLOG
|
||||||
|
item 4 as [stuck]. Then: `nomarchy-menu snapshot` now opens
|
||||||
|
the **btrfs-assistant GUI**: a *themed* polkit prompt must appear
|
||||||
|
(hyprpolkitagent — first on-hardware outing) and the GUI must open
|
||||||
|
as root. Also exercise the fzf fallback in a terminal
|
||||||
|
(`sudo nomarchy-snapshots`): browse/diff, restore a file
|
||||||
|
(`undochange`), and a root-config rollback behind the typed-`yes`
|
||||||
|
gate. Bonus check: any other pkexec flow now prompts instead of
|
||||||
|
silently failing.
|
||||||
|
- [ ] **Update awareness** — with `nomarchy.updates.enable`, let the timer
|
||||||
|
fire (or start the unit): indicator appears only when inputs are
|
||||||
|
behind, notification only on count growth, click opens the upgrade
|
||||||
|
flow.
|
||||||
|
- [ ] **VPN menu live paths** — import a real WireGuard `.conf` and an
|
||||||
|
`.ovpn` via System → VPN, toggle up/down (● / ○ state), and the
|
||||||
|
Tailscale block: up/down + exit-node without sudo (operator grant).
|
||||||
|
- [ ] **Printer menu** — with `nomarchy.services.printing`, System →
|
||||||
|
Printers opens system-config-printer; add a printer, test page.
|
||||||
|
- [ ] **GRUB UEFI ISO theme render** — boot the ISO on UEFI hardware:
|
||||||
|
composed splash background, palette menu in the lower third, accent
|
||||||
|
timeout bar.
|
||||||
|
- [ ] **Visual theme pass** — live ISO: all six identity themes (bars) +
|
||||||
|
the four authored rofi `.rasi` (nord/retro-82/lumon/kanagawa) look
|
||||||
|
right, not just parse.
|
||||||
|
- [ ] **Auto-commit on a real machine** — System › Auto-commit toggles on
|
||||||
|
(row shows state, notification fires); a theme apply from SUPER+T then
|
||||||
|
shows a `nomarchy: apply theme …` commit in `~/.nomarchy` (`git log`);
|
||||||
|
unrelated dirty files in the checkout stay uncommitted; toggle off is
|
||||||
|
itself the last commit. Also: the "Auto timezone (on/off)" row label
|
||||||
|
now reflects the real state (the `= true` comparison fix).
|
||||||
|
|
||||||
|
## AMD dev box only
|
||||||
|
- [ ] **AMD runtime bits** — VA-API (`vainfo` → radeonsi), amd-pstate EPP
|
||||||
|
active and PPD switching governors; opt-ins: ROCm (`rocminfo`, a GPU
|
||||||
|
PyTorch/Ollama smoke) and the XDNA NPU driver loading.
|
||||||
|
- [ ] **Fingerprint** — `fprintd-enroll` + (opt-in PAM) login/sudo.
|
||||||
|
|
||||||
|
## Latitude 5410 only
|
||||||
|
- [ ] **Media keys + gestures** (from dccceb4) — volume/brightness keys
|
||||||
|
drive the OSD; touchpad gestures work.
|
||||||
|
- [ ] **v1 QA batch on-hardware pass** (583708d batch was QEMU-verified) —
|
||||||
|
general smoke before the next `main → v1` promotion.
|
||||||
|
|
||||||
|
## T14s only
|
||||||
|
- [ ] **Webcam IR-hide end-to-end on Nomarchy** — installer detects the
|
||||||
|
RGB+IR pair, bakes `hardware.camera.hideIrSensor`; `wpctl status`
|
||||||
|
shows one colour source; an app picker lists one camera; Howdy-style
|
||||||
|
direct `/dev/video2` reads still work.
|
||||||
179
agent/JOURNAL.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# Journal — append-only iteration log
|
||||||
|
|
||||||
|
One entry per iteration, **newest first**. This is the loop's short-term
|
||||||
|
memory: the next session reads the last 3–5 entries to orient. Keep an
|
||||||
|
entry under ~15 lines; durable lessons go to MEMORY.md, not here.
|
||||||
|
|
||||||
|
Template:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## YYYY-MM-DD — <task title> (<commit sha, or "no commit">)
|
||||||
|
- **Task:** which BACKLOG item (or QA sweep / bootstrap / escalation)
|
||||||
|
- **Did:** what changed, in 2–4 lines
|
||||||
|
- **Verified:** tier reached (V0/V1/V2) + the actual commands/checks run
|
||||||
|
- **Pending:** V3 queued? follow-ups filed? anything [stuck]?
|
||||||
|
- **Next suggestion:** what the following iteration should probably take
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-04 — Keyboard cycle bind + parity (iteration #6) + Latitude triage
|
||||||
|
- **Task:** BACKLOG NOW#5; mid-iteration Bernardo delivered Latitude
|
||||||
|
5410 hardware-QA findings (the machine got a real session!).
|
||||||
|
- **Did:** `multiLayoutBinds` in keybinds.nix (SUPER+SHIFT+K →
|
||||||
|
`switchxkblayout current next`), gated in hyprland.nix AND the rofi
|
||||||
|
cheatsheet on the same comma-in-layout condition; `hyprland/language`
|
||||||
|
+ `#language` CSS added to both summer whole-swaps (shows even
|
||||||
|
single-layout there — static JSON can't gate; deliberate). Reverse
|
||||||
|
parity gap found (summer-night has idle_inhibitor, generated bar
|
||||||
|
doesn't) → folded into item 13. **Triage:** four new NOW items from
|
||||||
|
hardware: #21 Waybar crash on theme switch + no respawn (top),
|
||||||
|
#22 nameless network-menu entries, #23 `sys-rebuild` (no-update
|
||||||
|
rebuild), #24 Back-everywhere audit.
|
||||||
|
- **Verified:** V0 (JSON + flake check) + V1 both directions
|
||||||
|
(single-layout build: no bind; `us,de` via extendModules: bind + the
|
||||||
|
cheatsheet row render; summer JSONs parse).
|
||||||
|
- **Pending:** V3 cycle-bind check queued; items 21/22/24 carry V3
|
||||||
|
re-checks on the Latitude.
|
||||||
|
- **Next suggestion:** #21 (waybar crash — stability pillar, user-hit).
|
||||||
|
|
||||||
|
## 2026-07-04 — Webcam follow-ups (loop iteration #5, quick item)
|
||||||
|
- **Task:** BACKLOG NOW#7 (Bernardo asked for "a quick item").
|
||||||
|
- **Did:** template `home.packages` gains a commented "Webcam tuning"
|
||||||
|
pair (cameractrls GUI, v4l-utils CLI — bare packages, so template not
|
||||||
|
toggle, per the option-surface rule); the portal/Flatpak libcamera IR
|
||||||
|
gap is written up as a PROPOSED item with three options (recommend:
|
||||||
|
document-only now, libcamera-rule investigation when the T14s is
|
||||||
|
available). CI runs #60/#61 confirmed green (earlyoom + snapshot fix).
|
||||||
|
- **Verified:** V0 (flake check — template evals through mkFlake;
|
||||||
|
changes are comments + agent docs).
|
||||||
|
- **Next suggestion:** NOW#5 (keyboard cycle bind + summer parity) —
|
||||||
|
the last code item in NOW besides the docs pass.
|
||||||
|
|
||||||
|
## 2026-07-04 — Snapshot GUI un-broken + polkit agent (loop iteration #4)
|
||||||
|
- **Task:** BACKLOG NOW#4 — btrfs-assistant segfault.
|
||||||
|
- **Did:** re-diagnosed instead of overriding. gdb: crash is in
|
||||||
|
libbtrfsutil's *unprivileged* subvolume iteration (btrfs-progs 6.17.1;
|
||||||
|
upstream fix `886571653` post-6.17.1; symbol versions verified — not
|
||||||
|
ABI drift). VM A/B: root works (exit 0), user crashes (139). The
|
||||||
|
launcher runs it as root → GUI fine *if* pkexec can prompt — and the
|
||||||
|
distro shipped **no polkit agent** (all pkexec silently failed; the
|
||||||
|
actual root cause of "GUI is dead"). Shipped: hyprpolkitagent
|
||||||
|
(exec-once, Stylix-themed Qt), menu re-pointed to the launcher with
|
||||||
|
the fzf flow as fallback, `checks.snapshot-gui` canary (root path +
|
||||||
|
offscreen event loop on real btrfs). No btrfs-progs patch — root-side
|
||||||
|
flows don't hit the bug; the fix arrives with a lock bump.
|
||||||
|
- **Verified:** V0 + V2 (snapshot-gui check, run for real) + V1 (HM
|
||||||
|
generation renders the agent exec-once; menu bash -n).
|
||||||
|
- **Pending:** V3 queued — first on-hardware themed polkit prompt.
|
||||||
|
- **Next suggestion:** NOW#5 (keyboard cycle bind + summer parity).
|
||||||
|
|
||||||
|
## 2026-07-04 — CI is green (run #58) — item 20 main half closed
|
||||||
|
- **Task:** monitor follow-up, no code. Run #58 (the .gitea path fix)
|
||||||
|
**succeeded** — first green CI: the container recipe works on the
|
||||||
|
live runner, eval tier confirmed end to end.
|
||||||
|
- **Did:** BACKLOG item 20 reduced to its stretch (KVM runner → enable
|
||||||
|
vm-checks), moved to NEXT. Run #59 (earlyoom push) monitored.
|
||||||
|
- **Next suggestion:** NOW#4 (btrfs-assistant override attempt).
|
||||||
|
|
||||||
|
## 2026-07-04 — Memory-pressure protection shipped (loop iteration #3)
|
||||||
|
- **Task:** BACKLOG NOW#3 — default-on userspace OOM killer.
|
||||||
|
- **Did:** `modules/nixos/oom.nix`: earlyoom (mkDefault on, desktop
|
||||||
|
notifications, session-plumbing `--avoid` list, no `--prefer`) +
|
||||||
|
`systemd.oomd.enable = false`. Choice rationale: Hyprland session is
|
||||||
|
one cgroup scope → oomd's cgroup-level kill would nuke the desktop;
|
||||||
|
earlyoom is process-level. Found en route: nixpkgs default-enables
|
||||||
|
oomd *inert* (enable=true, all slice toggles false). README "beyond
|
||||||
|
the surface" note added.
|
||||||
|
- **Verified:** V0 (flake check) + **V2** — new `checks.oom-protection`
|
||||||
|
runNixOSTest, run for real: hog (chunked allocator, 686 MB peak in a
|
||||||
|
1 GB VM) SIGTERM'd by earlyoom in 0.1 s, bystander unit survived,
|
||||||
|
oomd asserted inactive. 25 s test, cheap enough for the suite.
|
||||||
|
- **Pending:** nothing hardware-specific (behavior fully VM-provable).
|
||||||
|
- **Next suggestion:** NOW#4 (btrfs-assistant override attempt). Also:
|
||||||
|
CI run #58 (the .gitea fix) was *running* at commit time — check its
|
||||||
|
outcome next tick; this push triggers #59.
|
||||||
|
|
||||||
|
## 2026-07-04 — CI fix: wrong workflow dir (correction to iteration #2)
|
||||||
|
- **Task:** Bernardo reported no run despite an active runner + shared
|
||||||
|
his compose file: the server is **Gitea** (gitea/act_runner), not
|
||||||
|
Forgejo — I'd shipped the workflow to `.forgejo/workflows/`, which
|
||||||
|
Gitea never reads. The legacy repo's `.gitea/workflows/` path was the
|
||||||
|
clue I under-weighted.
|
||||||
|
- **Did:** `git mv` → `.gitea/workflows/check.yml`; corrected the
|
||||||
|
Forgejo references in the workflow header, TESTING.md §1b, BACKLOG
|
||||||
|
item 20 (now: watch the first run; runner was never the problem);
|
||||||
|
MEMORY.md gains the Gitea-not-Forgejo line.
|
||||||
|
- **Verified:** the push of this commit is itself the test — the run
|
||||||
|
must appear; monitoring.
|
||||||
|
- **Pending:** first green run (item 20); runner-container memory
|
||||||
|
headroom is the watch-out.
|
||||||
|
|
||||||
|
## 2026-07-04 — CI checks-on-push shipped (loop iteration #2)
|
||||||
|
- **Task:** BACKLOG NOW#2 — Forgejo Actions workflow.
|
||||||
|
- **Did:** `.forgejo/workflows/check.yml` (push to main/v1 + dispatch):
|
||||||
|
flake check --no-build, theme-sync py_compile, bash -n over tracked
|
||||||
|
.sh. Scoped to the **eval tier** deliberately — API probing +
|
||||||
|
archaeology of the legacy `.gitea/workflows/check.yml` (57 runs on
|
||||||
|
this instance) showed the runner is an act_runner docker container
|
||||||
|
(no KVM/systemd), so the VM suite can't run there; a commented
|
||||||
|
`vm-checks` job documents the KVM-runner upgrade path. Legacy's
|
||||||
|
hard-won container gotchas (nixbld setup, sandbox=false for Stylix
|
||||||
|
IFD, Nix 2.31.5 pin) carried over verbatim. TESTING.md §1b added.
|
||||||
|
- **Verified:** V0 locally (same commands the workflow runs, minus the
|
||||||
|
container Nix install) + yq YAML parse. The real green run needs the
|
||||||
|
runner to be alive — **not confirmable from here** (runners API is
|
||||||
|
401 unauthenticated); watching the run after push.
|
||||||
|
- **Pending:** new `[human]` item 20 — confirm/re-register the runner;
|
||||||
|
stretch: a KVM runner unlocks the vm-checks job.
|
||||||
|
- **Next suggestion:** NOW#3 (memory-pressure protection).
|
||||||
|
|
||||||
|
## 2026-07-04 — Opt-in auto-commit shipped (loop iteration #1)
|
||||||
|
- **Task:** BACKLOG NOW#1 — auto-commit of state mutations (in-flake
|
||||||
|
state Phase 4). First autonomous /loop iteration.
|
||||||
|
- **Did:** `settings.autoCommit` live-read by nomarchy-theme-sync;
|
||||||
|
pathspec-limited commit of theme-state.json on apply/set (fires if flag
|
||||||
|
on before OR after the write → the off-toggle commits too); identity
|
||||||
|
fallback; same-value no-op; `bg` excluded. Menu: System › Auto-commit
|
||||||
|
(instant, self-gated on `.git`). Rider: `get` prints booleans as JSON
|
||||||
|
`true`/`false`, fixing the stuck "Auto timezone (on/off)" menu label.
|
||||||
|
BACKLOG numbering switched to stable IDs (no renumbering).
|
||||||
|
- **Verified:** V0 (py_compile, flake check) + V1 (HM generation builds;
|
||||||
|
generated menu `bash -n`) + a 7-assertion sandbox git-repo round-trip
|
||||||
|
(enable/set/same-value/disable/post-disable/apply/identity-fallback).
|
||||||
|
- **Pending:** V3 queued in HARDWARE-QUEUE (on-machine toggle + apply).
|
||||||
|
- **Next suggestion:** NOW#2 (CI checks-on-push) — but its runner half
|
||||||
|
may need Bernardo to register a Forgejo runner; deliver the workflow
|
||||||
|
regardless.
|
||||||
|
|
||||||
|
## 2026-07-04 — Backlog revision pass (human-requested)
|
||||||
|
- **Task:** Bernardo asked for a philosophy-aligned revision of BACKLOG.md
|
||||||
|
("I'm sure I'm forgetting important items").
|
||||||
|
- **Did:** verified guessed gaps against the tree first (GC/optimise/boot
|
||||||
|
limit + all 21 previews already ship — dropped those ideas). Added the
|
||||||
|
confirmed gaps: CI-on-push promoted to NOW#2 (agents push to main now),
|
||||||
|
memory-pressure protection NOW#3 (nothing mitigates OOM; the release
|
||||||
|
bump died to one), viewers+mime defaults #8, update/rollback UX #9,
|
||||||
|
nomarchy-doctor #10, state-file validation #11, screen recording #12,
|
||||||
|
niceties batch #13 (idle-inhibit, low-battery notify, hyprpicker), OCR
|
||||||
|
in LATER; recovery runbook folded into the docs item; zram + default
|
||||||
|
browser added to Decisions.
|
||||||
|
- **Verified:** V0 (docs-only).
|
||||||
|
- **Pending:** Bernardo to sanity-check the new NOW ordering, esp. CI at
|
||||||
|
#2 and the two new Decisions.
|
||||||
|
- **Next suggestion:** unchanged — NOW#1 (opt-in auto-commit).
|
||||||
|
|
||||||
|
## 2026-07-04 — Bootstrap the loop infrastructure (this commit)
|
||||||
|
- **Task:** create `agent/` (human-requested): LOOP/GOALS/BACKLOG/
|
||||||
|
CONVENTIONS/MEMORY/HARDWARE-QUEUE/JOURNAL, root CLAUDE.md, README +
|
||||||
|
ROADMAP pointers.
|
||||||
|
- **Did:** reworked the forward half of docs/ROADMAP.md into the tiered
|
||||||
|
BACKLOG (5 NOW / 6 NEXT / LATER / Decisions); collected every pending
|
||||||
|
on-hardware check into HARDWARE-QUEUE.md; seeded MEMORY.md from the
|
||||||
|
ROADMAP's decision records.
|
||||||
|
- **Verified:** V0 (docs-only change; `nix flake check --no-build` run to
|
||||||
|
confirm the tree still evaluates).
|
||||||
|
- **Pending:** Bernardo to sanity-read GOALS.md pillars + BACKLOG tiering
|
||||||
|
— reorder freely, the order *is* the instruction.
|
||||||
|
- **Next suggestion:** BACKLOG NOW#1 (opt-in auto-commit — Phase 4 of
|
||||||
|
in-flake state, already the agreed next step).
|
||||||
118
agent/LOOP.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# The loop — autonomous iteration protocol
|
||||||
|
|
||||||
|
How an AI agent works on Nomarchy unattended. One **iteration** = pick one
|
||||||
|
task, do it, verify it, commit it, record it. The protocol is
|
||||||
|
runner-agnostic; the same iteration works under any of:
|
||||||
|
|
||||||
|
- **Interactive `/loop`** in a Claude Code session in this repo — the agent
|
||||||
|
self-paces iterations until stopped.
|
||||||
|
- **Headless** (`claude -p`, cron/systemd-timer) — one invocation runs one
|
||||||
|
iteration (or a small fixed number) and exits.
|
||||||
|
- **A fresh manual session** — a human says "do a loop iteration"; the
|
||||||
|
files below carry all the state, so any session can pick up where the
|
||||||
|
last left off.
|
||||||
|
|
||||||
|
All loop state lives in this directory, git-tracked. There is no state
|
||||||
|
outside the checkout (the distro's own philosophy, applied to its agents).
|
||||||
|
|
||||||
|
## The files
|
||||||
|
|
||||||
|
| File | Role | Who writes it |
|
||||||
|
|---|---|---|
|
||||||
|
| `GOALS.md` | North star + quality bars + non-goals | Human (agents propose edits) |
|
||||||
|
| `BACKLOG.md` | Prioritized task queue (NOW/NEXT/LATER/PROPOSED/DECISIONS) | Both — see its header rules |
|
||||||
|
| `JOURNAL.md` | Append-only iteration log | Agents |
|
||||||
|
| `MEMORY.md` | Curated durable lessons/gotchas | Agents (curated, not append-only) |
|
||||||
|
| `HARDWARE-QUEUE.md` | Pending on-hardware checks only Bernardo can run | Agents append, human checks off |
|
||||||
|
| `CONVENTIONS.md` | Repo/design conventions to follow while coding | Human (agents propose edits) |
|
||||||
|
|
||||||
|
## One iteration, step by step
|
||||||
|
|
||||||
|
### 0. Orient
|
||||||
|
1. Read `GOALS.md`, `CONVENTIONS.md`, `MEMORY.md`, the **last 3–5 entries**
|
||||||
|
of `JOURNAL.md`, and `BACKLOG.md`.
|
||||||
|
2. `git pull --ff-only` (skip silently if offline). Confirm you are on
|
||||||
|
`main` with a clean tree. **A dirty tree you didn't create → stop and
|
||||||
|
report; never stash or discard someone else's work.**
|
||||||
|
3. Sanity baseline: if the last journal entry reports a red
|
||||||
|
`nix flake check`, or you have any reason to suspect breakage, run
|
||||||
|
`nix flake check --no-build` first. **A red baseline preempts the
|
||||||
|
backlog — fixing it *is* this iteration's task.**
|
||||||
|
|
||||||
|
### 1. Pick exactly one task
|
||||||
|
- Take the **topmost actionable** item: NOW before NEXT; never LATER
|
||||||
|
unless NOW and NEXT are empty or all blocked.
|
||||||
|
- *Actionable* means: not `[blocked:hw]` (those wait in
|
||||||
|
`HARDWARE-QUEUE.md`), not `[human]` (decisions), and small enough to
|
||||||
|
finish + verify in one iteration. If the top item is too big, **split
|
||||||
|
it in BACKLOG.md** (that edit is part of the iteration) and take the
|
||||||
|
first slice.
|
||||||
|
- Never implement anything from **PROPOSED** — those await human triage.
|
||||||
|
- If nothing is actionable, do a **QA sweep** instead: run the full check
|
||||||
|
suite, hunt drift (README option tables vs the live `nomarchy.*`
|
||||||
|
surface, template drift, dead code), deepen a VM test, or research and
|
||||||
|
write up a PROPOSED item. An iteration that only improves the backlog
|
||||||
|
is a valid iteration. If even that yields nothing, journal it and stop
|
||||||
|
— do not manufacture churn.
|
||||||
|
|
||||||
|
### 2. Work
|
||||||
|
- Keep the diff focused on the task. Unrelated fixes you trip over become
|
||||||
|
PROPOSED/NOW entries, not scope creep.
|
||||||
|
- Follow `CONVENTIONS.md`. Match the surrounding hand-formatting; never
|
||||||
|
run a formatter.
|
||||||
|
- New gotcha discovered the hard way → one line in `MEMORY.md` now, while
|
||||||
|
it's fresh.
|
||||||
|
|
||||||
|
### 3. Verify — the ladder
|
||||||
|
Climb as high as the change warrants and your environment allows; **record
|
||||||
|
the tier reached** in the commit body and journal entry.
|
||||||
|
|
||||||
|
| Tier | What | When required |
|
||||||
|
|---|---|---|
|
||||||
|
| **V0** | `nix flake check --no-build` (+ `bash -n` / `py_compile` for scripts) | Every change, no exceptions |
|
||||||
|
| **V1** | Build the touched output: `system.build.toplevel`, the HM generation, the ISO, or the package | Anything beyond docs/comments |
|
||||||
|
| **V2** | VM: a `checks.*` runNixOSTest (add one if the change is guardable), or boot `tools/test-live-iso.sh` / `tools/test-install.sh` | Behavioral changes — services, boot, installer, session |
|
||||||
|
| **V3** | Real hardware | Cannot be done by the agent → append to `HARDWARE-QUEUE.md` with exact test steps |
|
||||||
|
|
||||||
|
The honesty rule governs: a visual/interactive change verified only to V1
|
||||||
|
is **not done** — it ships as "V1-verified, V2/V3 pending" with the pending
|
||||||
|
check queued. Prefer *adding a permanent `checks.*` test* over a one-off
|
||||||
|
manual VM poke when the behavior is testable headlessly (see MEMORY.md for
|
||||||
|
the reusable recipes).
|
||||||
|
|
||||||
|
### 4. Commit + push
|
||||||
|
- Style: match the log — `feat(scope): …`, `fix(scope): …`,
|
||||||
|
`test(scope): …`, `docs(scope): …`. Body explains what/why + the
|
||||||
|
verification tier reached and what remains.
|
||||||
|
- Include the `agent/` bookkeeping updates (backlog/journal/memory/queue)
|
||||||
|
**in the same commit** as the change they describe.
|
||||||
|
- Commit directly on `main` and `git push` (Bernardo's standing workflow).
|
||||||
|
- **Never:** force-push; touch the `v1` branch or any branch/tag you
|
||||||
|
didn't create; commit secrets or binaries; run `nix flake update`
|
||||||
|
unless the task is explicitly a lock bump; delete themes, wallpapers,
|
||||||
|
or user-facing assets without the backlog saying so.
|
||||||
|
|
||||||
|
### 5. Record
|
||||||
|
1. Mark the task in `BACKLOG.md` (move to its ✓ line or delete, per that
|
||||||
|
file's rules).
|
||||||
|
2. Append a `JOURNAL.md` entry (template in that file).
|
||||||
|
3. Queue any V3 checks in `HARDWARE-QUEUE.md`.
|
||||||
|
|
||||||
|
### 6. Pace (self-paced runners only)
|
||||||
|
Under `/loop`, continue to the next iteration while tasks remain
|
||||||
|
actionable and checks stay green. Stop the loop when: nothing is
|
||||||
|
actionable, the same task has failed twice (journal the failure analysis
|
||||||
|
and mark the item `[stuck]`), or a `[human]` decision blocks everything
|
||||||
|
remaining.
|
||||||
|
|
||||||
|
## Stop-and-escalate conditions (any runner)
|
||||||
|
|
||||||
|
Write a journal entry + a BACKLOG note, then stop, when:
|
||||||
|
- A fix would require touching `v1`, force-pushing, or a nixpkgs release
|
||||||
|
jump.
|
||||||
|
- The working tree contains uncommitted work you didn't create.
|
||||||
|
- A task turns out to need a design decision Bernardo hasn't made → move
|
||||||
|
it to **Decisions** in BACKLOG.md with the options laid out.
|
||||||
|
- Two consecutive iterations failed on the same task (`[stuck]`).
|
||||||
|
- Anything would delete or rewrite user data, git history, or the state
|
||||||
|
file schema in a non-backward-compatible way.
|
||||||
73
agent/MEMORY.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Memory — durable lessons, learned the hard way
|
||||||
|
|
||||||
|
Curated, not append-only: one line per fact, newest at the top of its
|
||||||
|
section; delete entries that stop being true. Details usually live in a
|
||||||
|
docs/ROADMAP.md decision record — pointer given as (§ item). Add a fact
|
||||||
|
here the moment a debugging session teaches you something a future
|
||||||
|
iteration would otherwise rediscover.
|
||||||
|
|
||||||
|
## Testing & VM recipes
|
||||||
|
- CI (`.gitea/workflows/check.yml`) is **eval-tier only**: the act_runner
|
||||||
|
is a docker container (no systemd, no /dev/kvm). Container gotchas are
|
||||||
|
documented in the workflow header (single-user Nix + nixbld users,
|
||||||
|
`sandbox=false` for Stylix IFD, Nix pinned 2.31.5 vs lazy-trees, no JS
|
||||||
|
actions past node20) — learned over the legacy repo's 57 runs; read
|
||||||
|
them before touching the workflow.
|
||||||
|
- The git server is **Gitea** (gitea/act_runner via docker-compose), NOT
|
||||||
|
Forgejo — workflows are read from `.gitea/workflows/` (or `.github/`),
|
||||||
|
never `.forgejo/workflows/` (a whole push cycle was lost to that).
|
||||||
|
- Reusable headless VM harness: `checks.*` via runNixOSTest — existing
|
||||||
|
examples to crib from: `distro-id` (boots + `switch-to-configuration
|
||||||
|
dry-activate`), `hardware-toggles` (kernel cmdline/PAM assertions),
|
||||||
|
`battery-charge-limit` (fake Mains adapter via `test_power`, real udev
|
||||||
|
uevent, `InvocationID` change proves the restart).
|
||||||
|
- Themed-desktop screenshots work headlessly: software-GL Hyprland
|
||||||
|
(`LIBGL_ALWAYS_SOFTWARE` on virtio-gpu) + `machine.screenshot()` QMP
|
||||||
|
dump — prototyped 2026-06-19, kept as the fallback for theme previews
|
||||||
|
(§ Visual theme picker).
|
||||||
|
- Hyprland/Ghostty need guest GL (`virtio-vga-gl`, `gl=on`) in
|
||||||
|
interactive QEMU or the session won't start; black screen ≈ missing GL
|
||||||
|
(docs/TESTING.md § gotchas).
|
||||||
|
- No KVM = slow, not broken; don't read slowness as failure.
|
||||||
|
|
||||||
|
## Known-broken / watchlist
|
||||||
|
- **btrfs-assistant "segfault" was unprivileged-only** (re-diagnosed
|
||||||
|
2026-07-04): libbtrfsutil's unprivileged subvolume iteration crashes on
|
||||||
|
btrfs-progs 6.17.1 (upstream-fixed after); **as root it works**, and the
|
||||||
|
pkexec launcher runs it as root. The real distro bug was **no polkit
|
||||||
|
agent in the session** (every pkexec failed silently) — hyprpolkitagent
|
||||||
|
now ships (hyprland.nix exec-once). `checks.snapshot-gui` guards the
|
||||||
|
root path. Lesson: before "app X is broken", check WHO it runs as — and
|
||||||
|
whether polkit prompts can render at all (§ Snapshot browse/restore).
|
||||||
|
- **NixOS release bump is a trap:** the discarded attempt
|
||||||
|
(branch deleted 2026-06-22) hit a Hyprland OOM blocker; a redo is a
|
||||||
|
deliberate `v2`, never part of routine lock bumps.
|
||||||
|
- `theme-state.json` is git-tracked inside an 86 MB flake tree, so every
|
||||||
|
state write re-copies the source before eval — the wallpapers-artifact
|
||||||
|
split (BACKLOG LATER) is the decided fix (§ Faster switches).
|
||||||
|
|
||||||
|
## Gotchas (cost a debugging session once)
|
||||||
|
- Never kill a Wayland session-lock client (hyprlock): its crash
|
||||||
|
failsafe drops to a tty instead of unlocking (§ Hibernate
|
||||||
|
double-unlock).
|
||||||
|
- rofi `element-icon size` is one value = a square cell; `WxH` silently
|
||||||
|
collapses and non-square icons letterbox — pre-crop images square at
|
||||||
|
build (§ Visual theme picker).
|
||||||
|
- WirePlumber 0.5 monitor rules can only early-match `device.api`;
|
||||||
|
`device.product.name` etc. bind *after* the rule runs — surgical
|
||||||
|
libcamera scoping is impossible (§ Webcam).
|
||||||
|
- `hyprctl switchxkblayout` is a *global* layout flip; per-device isolation
|
||||||
|
needs `device[<name>]:kb_layout` keywords (§ Keyboard layouts).
|
||||||
|
- Waybar's clock captures the timezone at construction — a zone change
|
||||||
|
needs SIGUSR2 (watcher in `timezone.nix`) (§ Automatic timezone).
|
||||||
|
- Waybar `persistent_workspaces` (underscore) is dead syntax silently
|
||||||
|
ignored; the hyphen form is honoured and renders phantom workspaces
|
||||||
|
(§ Waybar shows non-existent workspaces).
|
||||||
|
- GTK4/libadwaita/Qt6 read light/dark from the portal's
|
||||||
|
`org.freedesktop.appearance color-scheme` (dconf), not Stylix polarity
|
||||||
|
(§ GTK/Qt ignore the theme's mode).
|
||||||
|
- Update order matters downstream: `sys-update` (lock) before
|
||||||
|
`home-update`, or desktop changes are silently skipped against the old
|
||||||
|
lock (README § 3).
|
||||||
|
- grub `loadfont`s every `.pf2` in a theme dir — reuse a bundled DejaVu
|
||||||
|
rather than shipping fonts (§ Distro branding).
|
||||||
541
docs/ROADMAP.md
@@ -5,6 +5,12 @@ Forward-looking plans, plus a running log of shipped fixes (the
|
|||||||
stays a focused entry point — what Nomarchy is, how to install it, and
|
stays a focused entry point — what Nomarchy is, how to install it, and
|
||||||
how to override it. Items marked ✓ are shipped.
|
how to override it. Items marked ✓ are shipped.
|
||||||
|
|
||||||
|
> **The live, prioritized queue now lives in [`agent/BACKLOG.md`](../agent/BACKLOG.md)**
|
||||||
|
> (part of the autonomous-agent loop, `agent/LOOP.md`). This file remains
|
||||||
|
> the detailed design/decision record and the shipped log — backlog items
|
||||||
|
> reference it as "ROADMAP § <item>". When a backlog item ships, its
|
||||||
|
> lasting design notes get a ✓ entry here.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
- **Menu system** (apps launcher + theme switching + system actions), built
|
- **Menu system** (apps launcher + theme switching + system actions), built
|
||||||
on rofi 2.0 (native Wayland on 26.05) — its `.rasi` theme is baked from
|
on rofi 2.0 (native Wayland on 26.05) — its `.rasi` theme is baked from
|
||||||
@@ -51,6 +57,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 +78,136 @@ 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.
|
||||||
|
- ✓ **VPN setup & management menu:** a dedicated **System → VPN** flow
|
||||||
|
(`nomarchy-vpn`, `modules/home/rofi.nix`) that goes past the Network module
|
||||||
|
(`networkmanager_dmenu` only connects/disconnects *existing* VPNs) to a guided
|
||||||
|
setup + management surface across the three common kinds:
|
||||||
|
- **WireGuard:** import a `.conf` into NetworkManager (`nmcli connection import
|
||||||
|
type wireguard file …` — NM handles wg tunnels natively, no plugin) and
|
||||||
|
toggle it up/down.
|
||||||
|
- **OpenVPN:** import an `.ovpn` (`nmcli connection import type openvpn file …`);
|
||||||
|
the `networkmanager-openvpn` plugin ships system-side
|
||||||
|
(`networking.networkmanager.plugins`, mkDefault) so the openvpn type is
|
||||||
|
available — import type is chosen by file extension.
|
||||||
|
- **Tailscale:** status (read-only) + `up`/`down` + **exit-node** selection. It
|
||||||
|
lives outside NetworkManager, so the menu drives the `tailscale` CLI
|
||||||
|
directly, **self-gated** on the CLI being present (= `nomarchy.services.tailscale`,
|
||||||
|
which makes the login user the **operator** via `extraSetFlags`). So
|
||||||
|
up/down/exit-node run **inline without sudo**, falling back to a sudo terminal
|
||||||
|
only if the operator grant is absent; the first interactive login uses a
|
||||||
|
terminal (the auth URL is visible).
|
||||||
|
Shape: a `nomarchy-menu vpn` rofi submenu under **System** — NM
|
||||||
|
VPN/WireGuard connections shown ● active / ○ inactive and toggled on select
|
||||||
|
(networkmanager-group users need no sudo), Import via the Files/`fd` picker
|
||||||
|
(`*.conf`/`*.ovpn`), the Tailscale block when present — ending in `↩ Back`. A
|
||||||
|
self-gating Waybar **`custom/vpn`** shield (`nomarchy-vpn-status`: shown only
|
||||||
|
while a NM tunnel or Tailscale is up; `@good` tone; click opens the submenu),
|
||||||
|
wired into the generated bar **and the summer whole-swaps**. Secrets stay in
|
||||||
|
the connection manager's own store (NetworkManager / Tailscale) — no new secret
|
||||||
|
manager (cf. the deferred rofi-rbw/pass note above). **Decided: import-first**
|
||||||
|
— a from-scratch WireGuard keypair/peer editor is too much rofi surface, so
|
||||||
|
creation is deferred to `nm-connection-editor`. Eval + build green; **pending
|
||||||
|
an on-machine check** (the nmcli import/up-down + Tailscale paths need a live
|
||||||
|
session with real configs). Remaining (optional): richer exit-node display
|
||||||
|
(country/city).
|
||||||
- **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
|
||||||
@@ -99,10 +221,28 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
✓ `isoImage.splashImage` — the vendored vector logo
|
✓ `isoImage.splashImage` — the vendored vector logo
|
||||||
(`modules/nixos/branding/logo.svg`, from legacy) recolored to the palette
|
(`modules/nixos/branding/logo.svg`, from legacy) recolored to the palette
|
||||||
accent on the theme base, built at ISO-build time (`hosts/live.nix`).
|
accent on the theme base, built at ISO-build time (`hosts/live.nix`).
|
||||||
Remaining: `isoImage.grubTheme` so UEFI boot matches the isolinux splash
|
✓ **`isoImage.grubTheme` (UEFI boot matches BIOS):** `hosts/live.nix` now
|
||||||
(needs a full grub theme dir), and the `distroId` question (it changes
|
builds a `nomarchyGrubTheme` dir whose background is the *same* composed
|
||||||
`DEFAULT_HOSTNAME` and upstream `isNixos` checks — needs a test pass;
|
splash image as the isolinux splash (accent logo on base), with a
|
||||||
nixos-* CLI names stay regardless)
|
palette-coloured boot menu in the lower third (clear of the centred logo)
|
||||||
|
and an accent timeout bar. Derived from `nixos-grub2-theme` only to reuse
|
||||||
|
its bundled DejaVu `.pf2` (grub `loadfont`s every `.pf2` in the dir); the
|
||||||
|
stock NixOS `logo.png` is dropped since ours is in the background. Built +
|
||||||
|
structure-verified (theme.txt palette colours, 1920×1080 background, font
|
||||||
|
present). Remaining: a UEFI ISO-boot render check on hardware (the file
|
||||||
|
wiring is confirmed; the visual is not CI-testable, same as the splash).
|
||||||
|
✓ **`distroId = "nomarchy"`:** os-release is now honest — `ID=nomarchy`,
|
||||||
|
`ID_LIKE=nixos` (the standard derivative-distro lineage marker, cf.
|
||||||
|
Ubuntu→debian), `DEFAULT_HOSTNAME=nomarchy`, lsb `DISTRIB_ID`/`CPE_NAME`
|
||||||
|
follow. **Verified safe:** `switch-to-configuration` builds its "is this
|
||||||
|
NixOS?" guard from the *configured* distroId (and `/etc/NIXOS` remains as
|
||||||
|
the fallback), so rebuilds keep working — a new `checks.distro-id`
|
||||||
|
VM-test boots such a system and runs `switch-to-configuration dry-activate`
|
||||||
|
green; nixos-* CLI tools are package names, untouched. The one side effect
|
||||||
|
(isNixos→false blanks the upstream nixos.org URLs) is handled by
|
||||||
|
`extraOSReleaseArgs` restoring `HOME_URL`/`DOCUMENTATION_URL`/`SUPPORT_URL`/
|
||||||
|
`BUG_REPORT_URL` to the project. os-release output eval-verified from the
|
||||||
|
real downstream config.
|
||||||
- ✓ **fastfetch branding:** `modules/home/fastfetch.nix`
|
- ✓ **fastfetch branding:** `modules/home/fastfetch.nix`
|
||||||
(`nomarchy.fastfetch.enable`) — the vendored vector logo, recolored to
|
(`nomarchy.fastfetch.enable`) — the vendored vector logo, recolored to
|
||||||
the palette accent and rendered to compact block-art via chafa at build
|
the palette accent and rendered to compact block-art via chafa at build
|
||||||
@@ -176,8 +316,17 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
comes from the agent's zsh integration; cache TTLs (30 min / 2 h) govern
|
comes from the agent's zsh integration; cache TTLs (30 min / 2 h) govern
|
||||||
re-prompting. gnome-keyring stays the Secret Service (modern versions run
|
re-prompting. gnome-keyring stays the Secret Service (modern versions run
|
||||||
no SSH agent, so no socket contention); screen lock doesn't flush the
|
no SSH agent, so no socket contention); screen lock doesn't flush the
|
||||||
cache. Remaining (optional): a session-level `SSH_AUTH_SOCK` export so GUI
|
cache. ✓ **session-level `SSH_AUTH_SOCK`:** besides the zsh integration,
|
||||||
clients launched outside a shell also see the agent.
|
a `home.sessionVariables` export now covers GUI clients launched outside a
|
||||||
|
shell (rofi launcher, autostarted apps) that never inherit the interactive
|
||||||
|
shell's copy — resolved with `gpgconf --list-dirs agent-ssh-socket` at
|
||||||
|
session-init (the same lookup the shell integration uses, so the two can't
|
||||||
|
drift), reaching GUI apps via the login shell that starts Hyprland the same
|
||||||
|
way `NIXOS_OZONE_WL` does. Verified by building the home generation and
|
||||||
|
inspecting the rendered `hm-session-vars.sh` — it carries
|
||||||
|
`export SSH_AUTH_SOCK="$(…/gpgconf --list-dirs agent-ssh-socket)"` with the
|
||||||
|
command substitution intact (unescaped, resolved at session-init). Pending
|
||||||
|
an on-machine check that a GUI git/ssh client picks up the agent.
|
||||||
- **Sanitize & organize the repo:** a housekeeping pass for consistency
|
- **Sanitize & organize the repo:** a housekeeping pass for consistency
|
||||||
and clarity.
|
and clarity.
|
||||||
- ✓ pruned now-redundant config: the installer no longer writes
|
- ✓ pruned now-redundant config: the installer no longer writes
|
||||||
@@ -231,7 +380,21 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
turns it on for a `GenuineIntel` CPU.
|
turns it on for a `GenuineIntel` CPU.
|
||||||
- ✓ **longevity:** `power.batteryChargeLimit` (e.g. 80) — a backend-
|
- ✓ **longevity:** `power.batteryChargeLimit` (e.g. 80) — a backend-
|
||||||
independent sysfs oneshot writes `charge_control_end_threshold`. Off by
|
independent sysfs oneshot writes `charge_control_end_threshold`. Off by
|
||||||
default; the installer scaffolds it commented-out on laptops.
|
default; the installer scaffolds it commented-out on laptops. ✓ the
|
||||||
|
threshold is now **re-applied on AC state changes** (a `services.udev`
|
||||||
|
rule on `SUBSYSTEM=="power_supply", ATTR{type}=="Mains"` restarts the
|
||||||
|
oneshot via `systemctl --no-block`), closing the firmware-resets-on-
|
||||||
|
unplug gap — the match is by adapter *type*, not kernel name
|
||||||
|
(AC/AC0/ADP1/ACAD vary), and `restart` (not `try-restart`) re-applies
|
||||||
|
even if the boot run was inactive. Eval-verified both ways (rule present
|
||||||
|
with charge limit on / absent off), and **VM-verified the trigger**
|
||||||
|
(`checks.battery-charge-limit`: the `test_power` module fakes a Mains
|
||||||
|
adapter, toggling `ac_online` emits a real `power_supply` uevent, and the
|
||||||
|
udev rule restarts the oneshot — confirmed by a changed `InvocationID`).
|
||||||
|
The sysfs *write* itself needs a real `charge_control_end_threshold`, so
|
||||||
|
a final on-hardware check (a `sudo nixos-rebuild` + a physical unplug)
|
||||||
|
remains — the dev box has both an `AC` Mains adapter and a `BAT0`
|
||||||
|
`charge_control_end_threshold`, so it can exercise it.
|
||||||
- ✓ **installer:** writes `power.laptop = true` (battery probe) and
|
- ✓ **installer:** writes `power.laptop = true` (battery probe) and
|
||||||
`power.thermal.enable` (Intel) into the generated `system.nix`.
|
`power.thermal.enable` (Intel) into the generated `system.nix`.
|
||||||
- ✓ **idle cohesion:** `modules/home/idle.nix` now suspends only on
|
- ✓ **idle cohesion:** `modules/home/idle.nix` now suspends only on
|
||||||
@@ -241,8 +404,6 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
logind's lid handling is left at its defaults (suspend on lid close,
|
logind's lid handling is left at its defaults (suspend on lid close,
|
||||||
ignore when docked) — an explicit "I'm done" that coheres with the
|
ignore when docked) — an explicit "I'm done" that coheres with the
|
||||||
idle behaviour.
|
idle behaviour.
|
||||||
- Remaining: a boot-only→event-driven charge-limit re-apply (udev) if a
|
|
||||||
firmware resets the threshold on unplug.
|
|
||||||
- ✓ **Waybar parity:** the `custom/powerprofile` indicator now shows in the
|
- ✓ **Waybar parity:** the `custom/powerprofile` indicator now shows in the
|
||||||
summer-day/night whole-swap themes too. `powerProfileStatus`/`Cycle` are
|
summer-day/night whole-swap themes too. `powerProfileStatus`/`Cycle` are
|
||||||
named `writeShellScriptBin`s on PATH (`waybar.nix` home.packages), so the
|
named `writeShellScriptBin`s on PATH (`waybar.nix` home.packages), so the
|
||||||
@@ -263,6 +424,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
|
||||||
@@ -287,6 +473,91 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
distro-wide regardless).
|
distro-wide regardless).
|
||||||
Sub-items here can graduate into their own roadmap entries as they're
|
Sub-items here can graduate into their own roadmap entries as they're
|
||||||
scoped; the unifying work is the detection + `nomarchy.hardware.*` surface.
|
scoped; the unifying work is the detection + `nomarchy.hardware.*` surface.
|
||||||
|
- **Webcam support & tuning:** improve out-of-the-box webcam behaviour.
|
||||||
|
Motivating case — on the ThinkPad T14s AMD Gen 4 the camera shows a dark,
|
||||||
|
low-quality image. **Diagnosed on hardware (2026-06-26)**, and it's *not* what
|
||||||
|
the first cut of this item guessed (UVC default-controls tuning vs a MIPI/
|
||||||
|
libcamera gap):
|
||||||
|
- The camera is a **USB UVC** module (Chicony `04f2:b7c0`, `uvcvideo`) and is
|
||||||
|
**dual-sensor** — `video0` Color (MJPG, up to 2592×1944 / 1080p) + `video2`
|
||||||
|
**IR** (8-bit `GREY` only, the face-unlock sensor). The AMD IPU `[1022:1502]`
|
||||||
|
is present on PCI but **unused** (the camera enumerates over USB, not the
|
||||||
|
MIPI/ISP path), so the libcamera-software-ISP branch is moot on this machine.
|
||||||
|
- The **raw color capture is fine**: at factory defaults with auto-exposure,
|
||||||
|
`/dev/video0` measures luma ≈130/255 *from the first frame* (no AE ramp, not
|
||||||
|
dark); forcing manual exposure made it *worse*. So there is **no v4l2 control
|
||||||
|
default to bake** — the speculated `v4l2-ctl`-tuning / udev-oneshot fix is the
|
||||||
|
wrong tree.
|
||||||
|
- **Real cause is the consumption path.** PipeWire/WirePlumber exposes the
|
||||||
|
device through **both** backends at once — two `[v4l2]` sources (node 144 =
|
||||||
|
`/dev/video0` Color, node 146 = `/dev/video2` **IR**) **plus** two
|
||||||
|
`[libcamera]` nodes (Color + IR) — and the IR sensor is presented as an
|
||||||
|
**indistinguishable** "Integrated Camera". So an app's camera picker shows up
|
||||||
|
to *four* identical entries, and choosing the IR one yields a black/dark
|
||||||
|
monochrome frame; the libcamera path can also negotiate the low-res `YUYV`
|
||||||
|
640×480 mode ("bad quality"). The default source is the color node, so apps
|
||||||
|
that don't let you choose are fine — the breakage is selecting (or an app
|
||||||
|
auto-selecting) the wrong node.
|
||||||
|
- **Fix — validated live on hardware (2026-06-26).** Two WirePlumber 0.5
|
||||||
|
drop-ins collapse the four entries to one clean color camera, confirmed via
|
||||||
|
`wpctl status` (before: 2 v4l2 + 2 libcamera incl. both IR nodes → after:
|
||||||
|
**1** v4l2 source = `/dev/video0` color, **0** libcamera):
|
||||||
|
1. **Hide the IR node** — `monitor.v4l2.rules` matching the IR sensor →
|
||||||
|
`node.disabled = true`. (Tested by card name `~.*Integrated I`; the
|
||||||
|
**shipping** match should key on the more robust, vendor-neutral heuristic
|
||||||
|
of a **`GREY`-only / no-color-format** node, since other vendors' IR cards
|
||||||
|
are named differently.)
|
||||||
|
2. **Drop the duplicate backend** — `wireplumber.profiles.main.monitor.libcamera
|
||||||
|
= disabled`, since a plain UVC cam is fully covered by v4l2 (also kills the
|
||||||
|
libcamera low-res-`YUYV` path).
|
||||||
|
**Drawbacks / design constraints for the module:**
|
||||||
|
- Rule 2 (libcamera off) is only safe **when a UVC camera exists** — on a
|
||||||
|
MIPI/IPU-only machine (no UVC fallback) it would kill the camera entirely,
|
||||||
|
so it **must be conditional on detection**, not blanket. Rule 1 (IR-hide)
|
||||||
|
is broadly safe. Exactly the `nomarchy.hardware.*`-gated targeting the
|
||||||
|
parent item calls for.
|
||||||
|
- **Face-unlock is *not* broken:** `node.disabled` only hides the PipeWire
|
||||||
|
node; the kernel `/dev/video2` stays openable, so Howdy (which reads the IR
|
||||||
|
device directly, bypassing PipeWire) still works.
|
||||||
|
A true MIPI/IPU software-ISP camera with no UVC fallback stays a separate
|
||||||
|
future item.
|
||||||
|
- **Shipped (2026-06-27):** `nomarchy.hardware.camera.hideIrSensor` (+ an
|
||||||
|
overridable `irMatch` regex) in `modules/nixos/hardware.nix` emits rule 1 via
|
||||||
|
`services.pipewire.wireplumber.extraConfig`; the installer's `hardware-db.sh`
|
||||||
|
auto-detects a paired RGB+IR webcam (from `/sys/class/video4linux/*/name`)
|
||||||
|
and bakes the toggle into `system.nix`, with a commented example in the
|
||||||
|
downstream template. **Decision: v4l2 IR-hide only — libcamera is left
|
||||||
|
untouched** so an external camera you plug in is never affected. Surgical
|
||||||
|
internal-only libcamera scoping proved impossible: the distinguishing device
|
||||||
|
props (`api.libcamera.location`, `device.product.name`) bind *after* the
|
||||||
|
monitor rule runs, and the only early-matchable prop (`device.api`) is
|
||||||
|
all-or-nothing — so a broad libcamera-off was the only option and was rejected
|
||||||
|
as the blunt instrument it is (external USB cams are UVC and keep working via
|
||||||
|
v4l2 regardless). Verified: installer detection fires on the T14s; the
|
||||||
|
generated drop-in's serialized content is valid WP-0.5 config; and the exact
|
||||||
|
shipped `irMatch` was re-confirmed live (1 V4L2 source = the colour
|
||||||
|
`/dev/video0`, libcamera untouched, IR node disabled in the WirePlumber log).
|
||||||
|
Remaining: an on-Nomarchy end-to-end check; optional `v4l-utils` +
|
||||||
|
`cameractrls` for the rare genuine-tuning case; and a follow-up for
|
||||||
|
portal/Flatpak apps that consume the libcamera path (where the internal IR is
|
||||||
|
still listed, since libcamera stays on).
|
||||||
|
- ✓ **Memory-pressure protection (earlyoom, default-on):**
|
||||||
|
`modules/nixos/oom.nix` — running out of memory kills the offending
|
||||||
|
process instead of freezing the desktop. **earlyoom over systemd-oomd,
|
||||||
|
deliberately:** oomd kills whole cgroups, and a Hyprland session is ONE
|
||||||
|
scope (no per-app systemd scopes here, unlike GNOME) — under pressure it
|
||||||
|
would take out the entire desktop; earlyoom kills the single largest
|
||||||
|
process before the thrash point. nixpkgs default-enables oomd *inert*
|
||||||
|
(no slices monitored) — disabled outright so there's one owner. Session
|
||||||
|
plumbing is `--avoid`-listed (Hyprland/hyprlock/greetd/waybar/pipewire/
|
||||||
|
wireplumber/Xwayland/nix-daemon/systemd — unanchored, since NixOS
|
||||||
|
wrappers rename comm to `.foo-wrapped`); no `--prefer` tuning (largest-
|
||||||
|
RSS selection already finds the hog); kills raise a desktop notification
|
||||||
|
(systembus-notify). All `mkDefault` — opt out with
|
||||||
|
`services.earlyoom.enable = false`. **VM-verified**
|
||||||
|
(`checks.oom-protection`): a chunked allocator peaked at ~686 MB in a
|
||||||
|
1 GB VM, earlyoom SIGTERM'd it in 0.1 s, a bystander unit survived, and
|
||||||
|
oomd is asserted off.
|
||||||
- **Opt-in services & integrations:** the counterpart to the opt-*out*
|
- **Opt-in services & integrations:** the counterpart to the opt-*out*
|
||||||
application suite above — heavier or more personal integrations shipped
|
application suite above — heavier or more personal integrations shipped
|
||||||
**off by default**, each a `nomarchy.services.<name>.enable` toggle a
|
**off by default**, each a `nomarchy.services.<name>.enable` toggle a
|
||||||
@@ -344,19 +615,74 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
(no kanshi — it fights Hyprland's own output management). `nwg-displays`
|
(no kanshi — it fights Hyprland's own output management). `nwg-displays`
|
||||||
ships behind `nomarchy.displays.enable` as an interactive arranger (a
|
ships behind `nomarchy.displays.enable` as an interactive arranger (a
|
||||||
helper to find values; the declarative config stays the source of truth —
|
helper to find values; the declarative config stays the source of truth —
|
||||||
its output file isn't sourced). Remaining: true docked/undocked **profile
|
its output file isn't sourced). The System ▸ **Display** menu picks an
|
||||||
switching** of the *same* outputs (Hyprland's per-output rules cover the
|
output's resolution from its advertised modes: applied live via `hyprctl
|
||||||
common "external connects → arrange" case, not multi-layout toggles), and
|
keyword monitor` (current position + scale kept) and persisted to the
|
||||||
optionally workspace-to-monitor binding.
|
in-flake state (`settings.monitors.<name>`, no rebuild), overlaid onto
|
||||||
|
`nomarchy.monitors` by name so the next rebuild bakes it in — the monitor
|
||||||
|
twin of the keyboard-layout graduation. Remaining: true docked/undocked
|
||||||
|
**profile switching** of the *same* outputs (Hyprland's per-output rules
|
||||||
|
cover the common "external connects → arrange" case, not multi-layout
|
||||||
|
toggles), and optionally workspace-to-monitor binding.
|
||||||
- ✓ **Night light / blue-light filter:** `nomarchy.nightlight` (opt-in) —
|
- ✓ **Night light / blue-light filter:** `nomarchy.nightlight` (opt-in) —
|
||||||
a scheduled colour-temperature shift via `hyprsunset` (Hyprland-native),
|
a scheduled colour-temperature shift via `hyprsunset` (Hyprland-native),
|
||||||
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 (opt-in,
|
||||||
Waybar toggle to force it on/off, and geo (lat/long) auto sunset/sunrise
|
instant, in-flake state):** two git-tracked keys in the state file, both
|
||||||
(would mean wlsunset, which schedules by location).
|
menu-written (exposed via the new `nomarchy.settings` option) —
|
||||||
|
`settings.nightlight.installed` (**sticky**: gates the hyprsunset unit;
|
||||||
|
`nomarchy.nightlight.enable` `mkDefault`-reads it) and
|
||||||
|
`settings.nightlight.on` (runtime on/off). **Off by default.** The *first*
|
||||||
|
enable from the menu writes `installed` and rebuilds to create the unit (the
|
||||||
|
one accepted rebuild); **every toggle after is instant** —
|
||||||
|
`nomarchy-theme-sync set settings.nightlight.on … --no-switch` (atomic +
|
||||||
|
`git add -N`, no rebuild) plus a `systemctl` start/stop. Splitting the sticky
|
||||||
|
flag from the on/off is what avoids a **decay** bug: an instant-off writes
|
||||||
|
only `on`, so an unrelated later rebuild (e.g. `sys-update`) never drops the
|
||||||
|
unit and "on" stays instant. Persistence across logout/reboot comes from an
|
||||||
|
`ExecCondition` on the unit (`nomarchy-nightlight should-start`) that reads the
|
||||||
|
**live** working-tree on/off at start time — *not* the eval-frozen store copy
|
||||||
|
— so an off survives a reboot with no rebuild; a later rebuild bakes the same
|
||||||
|
value. No more `~/.local/state` marker, no marker `ConditionPathExists`. The
|
||||||
|
`nomarchy-menu` System-submenu entry is always shown (current on/off) so the
|
||||||
|
feature can be enabled from the menu; the self-gating Waybar
|
||||||
|
`custom/nightlight` indicator shows the moon while running and hides otherwise.
|
||||||
|
This is the first cut of a broader principle — **any user-settable config
|
||||||
|
gets a menu writer that lands it in the downstream flake; no state lives
|
||||||
|
outside the checkout** (Phase 1 night-light + Phase 2 the per-device
|
||||||
|
keyboard-layout memory below are done — both now in the git-tracked state
|
||||||
|
file; next target: opt-in auto-commit of the flake on each mutation, owned
|
||||||
|
files only). Remaining (optional): geo (lat/long) auto
|
||||||
|
sunset/sunrise (would mean wlsunset, which schedules by location). Pending an
|
||||||
|
on-machine check (first enable rebuilds + comes on; later toggles instant; off
|
||||||
|
persists across reboot via ExecCondition; stopping hyprsunset restores gamma).
|
||||||
|
- ✓ **Automatic timezone (location-following clock):** `nomarchy.system.autoTimezone`
|
||||||
|
(opt-in, off by default) — geoclue + `services.automatic-timezoned` drive
|
||||||
|
`/etc/localtime` from your location, so travelling to another zone updates the
|
||||||
|
Waybar clock on its own. **Menu-driven, in-flake state** (the night-light /
|
||||||
|
keyboard philosophy): the flag is `settings.autoTimezone` in theme-state.json,
|
||||||
|
git-tracked; the System-menu entry (`nomarchy-menu autotimezone` →
|
||||||
|
`nomarchy-autotimezone`) writes it and rebuilds. **Not instant like
|
||||||
|
night-light** — it's a *system* service (not a user unit you can start/stop),
|
||||||
|
so the toggle drives a `sudo nixos-rebuild` (bakes the service + the
|
||||||
|
`time.timeZone` override) plus a `home-manager switch` (the Waybar-refresh
|
||||||
|
watcher), both off the one flag. **time.timeZone handling:** a runtime zone
|
||||||
|
needs `/etc/localtime` writable; automatic-timezoned sets `time.timeZone = null`
|
||||||
|
itself, but the installer's static value would collide (a hard eval error), so
|
||||||
|
the module forces it null (`mkForce`) over the installer's value when enabled,
|
||||||
|
and reverts to it when disabled. **Live clock refresh:** Waybar's clock module
|
||||||
|
captures the zone at construction, so `modules/home/timezone.nix` runs a tiny
|
||||||
|
user service that watches timedate1's change signal and reloads Waybar
|
||||||
|
(SIGUSR2) on a real zone change (also catches a manual `timedatectl
|
||||||
|
set-timezone`). Eval-verified both ways (on: geoclue+daemon on, tz forced null
|
||||||
|
over a static installer value, watcher present; off: static tz intact, no
|
||||||
|
service/watcher). **Pending an on-hardware check** — geoclue detection and the
|
||||||
|
SIGUSR2 clock refresh aren't testable in CI (if SIGUSR2 proves insufficient,
|
||||||
|
fall back to restarting waybar). Remaining (optional): a Waybar tooltip line
|
||||||
|
showing the detected zone.
|
||||||
- **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,20 +698,46 @@ 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. The
|
||||||
reconnects. The boot-time set (incl. the built-in keyboard) is never
|
boot-time set (incl. the built-in keyboard) is never prompted. The
|
||||||
prompted. The runtime-remember complement to the declarative
|
runtime-remember complement to the declarative `keyboard.devices`.
|
||||||
`keyboard.devices`; a stateful runtime piece by design. **Pending an
|
**Picker verified on hardware (2026-06-18)**; the first cut applied the
|
||||||
on-hardware test** (hotplug isn't verifiable in CI). Remaining (bonus):
|
choice with `switchxkblayout` (a global layout-index flip) and merged the
|
||||||
offer to write a choice into `keyboard.devices` so it graduates to the
|
candidate pool into `input.kb_layout`, so a selection leaked onto the
|
||||||
reproducible config.
|
laptop keyboard and stuck after unplug — fixed by the per-device keyword +
|
||||||
- Remaining: a multi-layout cycle bind (`hyprctl switchxkblayout` / xkb
|
keeping the pool out of the session layout.
|
||||||
`grp:` options) for switching layouts on one keyboard; add the
|
- ✓ **In-flake state + graduation (2026-06-23):** the remembered picks moved
|
||||||
`hyprland/language` module to the summer whole-swap themes for parity
|
out of `~/.local/state/nomarchy/keyboard-layouts` into the git-tracked
|
||||||
(the gating doesn't translate to their static JSON). Keep the system
|
state file (`settings.keyboard.devices`, a device-name → layout map) —
|
||||||
(console/initrd) and session layouts in sync (the LUKS-keymap work).
|
Phase 2 of the in-flake-state principle, the same path night-light took.
|
||||||
|
The watcher reads the **live** working tree (a pick is honoured at once)
|
||||||
|
and writes instantly with `--no-switch` (no rebuild), merging the whole map
|
||||||
|
back at the dot-free parent path so a device name containing a dot can't
|
||||||
|
corrupt the dotted set-path. Each remembered device **graduates** into
|
||||||
|
`nomarchy.keyboard.devices` on the next rebuild (a generated Hyprland
|
||||||
|
`device{}` block, `mkDefault` so a hand-written entry still wins), after
|
||||||
|
which the watcher sees it as declared and steps back. Eval-verified
|
||||||
|
(graduation + precedence) and the writer round-trips; **still needs the
|
||||||
|
on-hardware hotplug re-verify** (the picker path isn't testable in CI).
|
||||||
|
- ✓ **Multi-layout cycle bind + summer parity (2026-07-04):**
|
||||||
|
`SUPER+SHIFT+K` → `hyprctl switchxkblayout current next`, rendered
|
||||||
|
only when the session layout has a comma (same gate as the Waybar
|
||||||
|
language indicator) — data lives in `keybinds.nix` as a separate
|
||||||
|
`multiLayoutBinds` list, so hyprland.nix and the cheatsheet consume
|
||||||
|
one source and gate identically (verified both ways: absent on a
|
||||||
|
single layout; bind + cheatsheet row render under `us,de` via
|
||||||
|
extendModules). `current` targets the focused keyboard, so a
|
||||||
|
per-device-overridden board (one layout) is a no-op, never a leak.
|
||||||
|
`hyprland/language` (` {short}`) added to both summer
|
||||||
|
`waybar.jsonc` whole-swaps + `#language` in their CSS — static JSON
|
||||||
|
can't eval-gate, so on summer themes the module shows even with one
|
||||||
|
layout (a deliberate parity-over-minimalism call; it's small).
|
||||||
|
Noticed en route: summer-night carries an `idle_inhibitor` the
|
||||||
|
generated bar lacks — folded into the idle-inhibit backlog item.
|
||||||
|
- Remaining: keep the system (console/initrd) and session layouts in
|
||||||
|
sync (the LUKS-keymap work).
|
||||||
- ✓ **Do-Not-Disturb:** swaync DND toggle wired into the menu
|
- ✓ **Do-Not-Disturb:** swaync DND toggle wired into the menu
|
||||||
(`nomarchy-menu dnd`, in the picker, SUPER+CTRL+D) and a Waybar bell
|
(`nomarchy-menu dnd`, in the picker, SUPER+CTRL+D) and a Waybar bell
|
||||||
indicator (`custom/notification` via `swaync-client -swb`: shows the
|
indicator (`custom/notification` via `swaync-client -swb`: shows the
|
||||||
@@ -404,21 +756,92 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
change). **The backend is VM-verified** (a full BTRFS+LUKS install: both
|
change). **The backend is VM-verified** (a full BTRFS+LUKS install: both
|
||||||
`root`+`home` configs, a `/home` timeline snapshot taken, `/home/.snapshots`
|
`root`+`home` configs, a `/home` timeline snapshot taken, `/home/.snapshots`
|
||||||
a real subvolume, the oneshot `Result=success`).
|
a real subvolume, the oneshot `Result=success`).
|
||||||
- ⚠ **Known bug — btrfs-assistant 2.2 segfaults on launch** in nixpkgs
|
- ✓ **Resolved — the "btrfs-assistant 2.2 segfault" was unprivileged-only**
|
||||||
26.05: it crashes inside `libbtrfsutil.so.1.4.0` (a library ABI mismatch,
|
(re-diagnosed 2026-07-04): gdb puts the crash in `btrfs_util_subvolume_
|
||||||
confirmed in the VM regardless of GL/Qt platform — so it's not a VM/GL
|
iterator_next()` — libbtrfsutil's *unprivileged* subvolume-iteration path
|
||||||
artifact and will crash on hardware too). The menu wiring is correct and
|
in btrfs-progs 6.17.1 (upstream-fixed after 6.17.1, kdave/btrfs-progs
|
||||||
the app is installed, but the GUI doesn't open. The snapshot *backend*
|
`886571653` "re-enable tree search v2 ioctl"; symbol versions were checked
|
||||||
(the actual snapshots) is unaffected. Fix path: a nixpkgs override aligning
|
and match, so not an ABI/link issue). **As root it runs fine** — VM-proven
|
||||||
its libbtrfsutil, or wait for an upstream bump; meanwhile the menu entry
|
(root `--version` exits 0, unprivileged exits 139) — and the pkexec
|
||||||
is a no-op when the binary crashes. Fallback if it lingers: a rofi-based
|
launcher runs it as root, so the GUI was never actually broken *when
|
||||||
snapshot menu (no btrfs-assistant dependency).
|
launched right*. What WAS missing distro-wide: **no polkit authentication
|
||||||
- Remaining (optional): a keyboard-driven rofi browse for quick glances;
|
agent**, so every pkexec prompt in the session failed silently — that's
|
||||||
boot-from-snapshot needs a systemd-boot equivalent of grub-btrfs.
|
the root cause of "the GUI doesn't open". Fixes: `hyprpolkitagent`
|
||||||
- **Update awareness:** updates are manual today (`sys-update`/`home-update`);
|
(Hyprland's Qt agent, Stylix-themed) now ships via exec-once in
|
||||||
add a Waybar indicator / notification when flake inputs are stale or a new
|
`hyprland.nix`; the menu prefers `btrfs-assistant-launcher` again with
|
||||||
nixpkgs rev is available (optionally with a pending-change count). Augments,
|
`nomarchy-snapshots` as fallback; `checks.snapshot-gui` guards the root
|
||||||
never replaces, the explicit rebuild flow.
|
path on a real btrfs volume (and the GUI event loop offscreen) against
|
||||||
|
lock-bump regressions. No btrfs-progs patch: our flows are all root-side,
|
||||||
|
so the unprivileged fix rides in with a future lock bump. Original
|
||||||
|
diagnosis kept below for the record. ✓ **Fallback shipped:**
|
||||||
|
the menu now launches `nomarchy-snapshots` (system-side, gated on
|
||||||
|
`nomarchy.system.snapper`) instead — a keyboard-driven snapper
|
||||||
|
browser/restore with no btrfs-assistant dependency. snapper is root-only
|
||||||
|
here (no `ALLOW_USERS`) 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, which is *safer*
|
||||||
|
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.
|
||||||
|
- ✓ **Opt-in auto-commit of state mutations (in-flake state, Phase 4):**
|
||||||
|
`settings.autoCommit` (menu: **System › Auto-commit**, self-gated on the
|
||||||
|
flake being a git repo) makes every `apply`/`set` also `git commit`
|
||||||
|
theme-state.json in the downstream flake. Design decisions: the flag is
|
||||||
|
**live-read by the tool** on each write — nothing in Nix consumes it, so
|
||||||
|
the toggle is instant, no rebuild, no option-surface addition; the commit
|
||||||
|
is **pathspec-limited** (`git commit -- theme-state.json`) so unrelated
|
||||||
|
dirty files are never swept up; it fires when the flag is on before *or*
|
||||||
|
after the write, so the disable-toggle itself lands in history instead of
|
||||||
|
staying forever-dirty; a same-value `set` no-ops (diff against HEAD); a
|
||||||
|
missing git identity falls back to `Nomarchy <nomarchy@localhost>`;
|
||||||
|
`bg next` is deliberately excluded (runtime wallpaper churn — the path
|
||||||
|
rides along with the next real commit). Verified: V1 (HM generation
|
||||||
|
builds, generated menu `bash -n` green) + a 7-assertion sandbox-repo
|
||||||
|
round-trip incl. the apply path; on-machine check queued. Rider fix:
|
||||||
|
`get` now prints booleans JSON-style (`true`, not Python's `True`) —
|
||||||
|
which also un-sticks the System menu's "Auto timezone (on/off)" label,
|
||||||
|
whose `= true` comparison could never match before.
|
||||||
- **"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
|
||||||
|
|||||||
@@ -22,6 +22,17 @@ bad merges — most breakage stops here. It also evaluates the downstream
|
|||||||
template through `lib.mkFlake` (including a real nixos-hardware profile),
|
template through `lib.mkFlake` (including a real nixos-hardware profile),
|
||||||
so template/wrapper drift fails fast too.
|
so template/wrapper drift fails fast too.
|
||||||
|
|
||||||
|
## 1b. CI (automatic on push)
|
||||||
|
|
||||||
|
Every push to `main`/`v1` runs `.gitea/workflows/check.yml`: the §1
|
||||||
|
cheap checks (flake eval, Python + shell syntax) on the Gitea instance.
|
||||||
|
That's the **eval tier only** — the runner is a docker container without
|
||||||
|
KVM, so the `checks.*` VM suite and real builds stay local (this file)
|
||||||
|
until a KVM-capable runner is registered; the workflow carries a
|
||||||
|
commented `vm-checks` job ready for that day. A green CI run is *not* "it
|
||||||
|
renders" (the honesty rule below still applies) — it means "nobody broke
|
||||||
|
evaluation".
|
||||||
|
|
||||||
## 2. Build and boot the live ISO
|
## 2. Build and boot the live ISO
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
24
flake.lock
generated
@@ -145,11 +145,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1781063120,
|
"lastModified": 1782704057,
|
||||||
"narHash": "sha256-1UIF/mDJluwJQjmmcZ2j1L2+mjYsefe82QCLj0TYSOg=",
|
"narHash": "sha256-G1I1gd32F7mp9LAe1DaZ4ZL7NX5gyiKwdCMwro1Vrck=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "baa46aeb6d02e0ba13de67cd35e3d57aedfacf01",
|
"rev": "868d0a692de703c2de98fab61968e4e310b7c28e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -166,11 +166,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1781020964,
|
"lastModified": 1782562157,
|
||||||
"narHash": "sha256-fS7xTi2j2iso5Hj7RNZLv/acDlCT+fgMVkVk40A7Uco=",
|
"narHash": "sha256-a7+T6QSeowynwZ1ZJJbP8T8ntAytvrui8kFGJmIZt2c=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixos-hardware",
|
"repo": "nixos-hardware",
|
||||||
"rev": "32c2cd9e46286c4eced3dc6b613c659126bf3cca",
|
"rev": "a9cf7546a938c737b079e738de73934a13de9784",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -182,11 +182,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1780902259,
|
"lastModified": 1782691344,
|
||||||
"narHash": "sha256-q8yYEC5f1mFlQO9RGna4LTc9QrcvWunX6FYp83munkQ=",
|
"narHash": "sha256-i5nw9BYYsMDAaOC4J+JmTof6b2GhlyH076awYRNrTV8=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "bd0ff2d3eac24699c3664d5966b9ef36f388e2ca",
|
"rev": "1f01958ffb5b3545c96d9ef2f4e24c5e5e1eb846",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -249,11 +249,11 @@
|
|||||||
"tinted-zed": "tinted-zed"
|
"tinted-zed": "tinted-zed"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1780702455,
|
"lastModified": 1782770679,
|
||||||
"narHash": "sha256-+srjPGNy67nKytYwdlepycL51IG6S34sS4MKRZXK8G0=",
|
"narHash": "sha256-+8RpmHKn5n2tYmoRCwiKJ6PeU85q15qnXzGQ2WGMn9Q=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "stylix",
|
"repo": "stylix",
|
||||||
"rev": "54fa19702f4f2c7f6a981a92850678933588af9a",
|
"rev": "3ed763829fc06d32cab3c1f31672379a1f53450e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
182
flake.nix
@@ -134,6 +134,188 @@
|
|||||||
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")
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Runtime VM check for the battery-charge-limit re-apply: the udev
|
||||||
|
# rule must restart the oneshot on an AC state change (firmware can
|
||||||
|
# clear the threshold on unplug). The `test_power` module fakes a
|
||||||
|
# Mains adapter we can toggle to emit a real power_supply uevent —
|
||||||
|
# so this exercises the trigger, not just the config wiring. (The
|
||||||
|
# sysfs *write* needs a real charge_control_end_threshold and stays
|
||||||
|
# an on-hardware check.) Imports options.nix (where the power
|
||||||
|
# options live) + power.nix, so the VM stays minimal.
|
||||||
|
battery-charge-limit = pkgs.testers.runNixOSTest {
|
||||||
|
name = "nomarchy-battery-charge-limit";
|
||||||
|
nodes.machine = { ... }: {
|
||||||
|
imports = [ ./modules/nixos/options.nix ./modules/nixos/power.nix ];
|
||||||
|
boot.kernelModules = [ "test_power" ]; # fake Mains + battery
|
||||||
|
nomarchy.system.power = {
|
||||||
|
enable = true;
|
||||||
|
laptop = true;
|
||||||
|
batteryChargeLimit = 80;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
testScript = ''
|
||||||
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
|
||||||
|
# The oneshot exists and the fake Mains adapter is present.
|
||||||
|
machine.succeed("systemctl cat nomarchy-battery-charge-limit.service")
|
||||||
|
machine.succeed("udevadm settle")
|
||||||
|
machine.succeed("grep -lx Mains /sys/class/power_supply/*/type")
|
||||||
|
|
||||||
|
# Let any add-event restart settle, then sample the invocation.
|
||||||
|
machine.succeed("sleep 2")
|
||||||
|
before = machine.succeed(
|
||||||
|
"systemctl show -p InvocationID --value "
|
||||||
|
"nomarchy-battery-charge-limit.service"
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
# Simulate an AC unplug → 'change' uevent on the Mains device.
|
||||||
|
machine.succeed("echo off > /sys/module/test_power/parameters/ac_online")
|
||||||
|
machine.succeed("udevadm settle")
|
||||||
|
|
||||||
|
# The udev rule must have restarted the oneshot (new InvocationID).
|
||||||
|
machine.wait_until_succeeds(
|
||||||
|
'test "$(systemctl show -p InvocationID --value '
|
||||||
|
'nomarchy-battery-charge-limit.service)" != "' + before + '"',
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Memory-pressure protection (oom.nix): earlyoom must kill a
|
||||||
|
# runaway allocator BEFORE the kernel thrash point — and must not
|
||||||
|
# take anything else with it. A bystander unit survives the kill,
|
||||||
|
# proving the process-level granularity that motivated earlyoom
|
||||||
|
# over cgroup-level systemd-oomd (which would kill the whole
|
||||||
|
# Hyprland session scope). Imports only oom.nix so the VM stays
|
||||||
|
# minimal.
|
||||||
|
oom-protection = pkgs.testers.runNixOSTest {
|
||||||
|
name = "nomarchy-oom-protection";
|
||||||
|
nodes.machine = { ... }: {
|
||||||
|
imports = [ ./modules/nixos/oom.nix ];
|
||||||
|
environment.systemPackages = [ pkgs.python3 ];
|
||||||
|
virtualisation.memorySize = 1024;
|
||||||
|
};
|
||||||
|
testScript = ''
|
||||||
|
machine.wait_for_unit("earlyoom.service")
|
||||||
|
|
||||||
|
# One owner: oomd (nixpkgs default-on, inert) is disabled.
|
||||||
|
machine.fail("systemctl is-active --quiet systemd-oomd.service")
|
||||||
|
# The session avoid-list made it into the daemon invocation.
|
||||||
|
machine.succeed("systemctl cat earlyoom.service | grep -q -- --avoid")
|
||||||
|
|
||||||
|
# A bystander that must survive (stand-in for the session).
|
||||||
|
machine.succeed("systemd-run --unit=bystander sleep infinity")
|
||||||
|
|
||||||
|
# The hog: allocate in 20 MB chunks with a short sleep, so
|
||||||
|
# earlyoom's poll wins the race against the kernel OOM killer.
|
||||||
|
machine.succeed(
|
||||||
|
"systemd-run --unit=hog python3 -c 'import time; "
|
||||||
|
"exec(\"a=[]\\nwhile True:\\n a.append(bytearray(20*1024*1024)); time.sleep(0.05)\")'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# earlyoom (not the kernel) pulled the trigger…
|
||||||
|
machine.wait_until_succeeds(
|
||||||
|
"journalctl -u earlyoom.service | grep -Eiq 'sending SIG(TERM|KILL)'",
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
# …the hog is gone…
|
||||||
|
machine.wait_until_succeeds(
|
||||||
|
'test "$(systemctl show -p ActiveState --value hog.service)" != "active"',
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
# …and the bystander is untouched.
|
||||||
|
machine.succeed("systemctl is-active --quiet bystander.service")
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Snapshot GUI canary: btrfs-assistant crashes in libbtrfsutil's
|
||||||
|
# UNPRIVILEGED subvolume iteration (btrfs-progs 6.17.1, fixed
|
||||||
|
# upstream after 6.17.1) but runs fine as root — which is how the
|
||||||
|
# menu launches it (pkexec launcher). Guard the root path on a
|
||||||
|
# real btrfs volume so a lock bump that breaks it fails here, not
|
||||||
|
# on a user's machine. The GUI event loop is exercised offscreen
|
||||||
|
# (survives a 5s timeout = no startup crash past the Btrfs scan).
|
||||||
|
snapshot-gui = pkgs.testers.runNixOSTest {
|
||||||
|
name = "nomarchy-snapshot-gui";
|
||||||
|
nodes.machine = { pkgs, ... }: {
|
||||||
|
environment.systemPackages = [ pkgs.btrfs-assistant pkgs.btrfs-progs ];
|
||||||
|
virtualisation.emptyDiskImages = [ 512 ];
|
||||||
|
};
|
||||||
|
testScript = ''
|
||||||
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
machine.succeed(
|
||||||
|
"mkfs.btrfs /dev/vdb && mkdir -p /mnt && mount /dev/vdb /mnt "
|
||||||
|
"&& btrfs subvolume create /mnt/sub1"
|
||||||
|
)
|
||||||
|
# Root (the launcher's effective context): must work.
|
||||||
|
machine.succeed("QT_QPA_PLATFORM=offscreen btrfs-assistant-bin --version")
|
||||||
|
# The GUI proper survives startup (timeout kill = alive → 124).
|
||||||
|
machine.succeed(
|
||||||
|
"set +e; timeout 5 env QT_QPA_PLATFORM=offscreen btrfs-assistant-bin; "
|
||||||
|
"[ $? -eq 124 ]"
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# The distroId question (roadmap "Distro branding"): set
|
||||||
|
# distroId = "nomarchy" so /etc/os-release is honest (ID=nomarchy,
|
||||||
|
# ID_LIKE=nixos). The risk is switch-to-configuration's "is this
|
||||||
|
# NixOS?" guard — but it builds the check from the *configured*
|
||||||
|
# distroId (and /etc/NIXOS still exists), so a rebuild must still
|
||||||
|
# work. This boots such a system and runs `switch-to-configuration
|
||||||
|
# dry-activate` to prove the guard passes. Inline (not the full
|
||||||
|
# module) so the VM stays minimal — the real wiring in default.nix
|
||||||
|
# is covered by the downstream-template-system build.
|
||||||
|
distro-id = pkgs.testers.runNixOSTest {
|
||||||
|
name = "nomarchy-distro-id";
|
||||||
|
nodes.machine = { ... }: {
|
||||||
|
system.nixos.distroName = "Nomarchy";
|
||||||
|
system.nixos.distroId = "nomarchy";
|
||||||
|
# Test base omits switch-to-configuration by default; we need it
|
||||||
|
# to exercise the "is this NixOS?" guard.
|
||||||
|
system.switch.enable = true;
|
||||||
|
};
|
||||||
|
testScript = { nodes, ... }: ''
|
||||||
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
|
||||||
|
# os-release reflects the rebrand, with the nixos lineage marker.
|
||||||
|
machine.succeed("grep -q '^ID=nomarchy$' /etc/os-release")
|
||||||
|
machine.succeed("grep -q '^ID_LIKE=nixos$' /etc/os-release")
|
||||||
|
machine.succeed("grep -q '^NAME=Nomarchy$' /etc/os-release")
|
||||||
|
|
||||||
|
# The rebuild path must still recognise this as a NixOS install
|
||||||
|
# (the guard is built from the configured distroId, not "nixos").
|
||||||
|
machine.succeed(
|
||||||
|
"${nodes.machine.system.build.toplevel}/bin/switch-to-configuration dry-activate"
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# ─── Reference host ────────────────────────────────────────────
|
# ─── Reference host ────────────────────────────────────────────
|
||||||
|
|||||||
@@ -4,26 +4,85 @@
|
|||||||
{ lib, pkgs, username, nomarchySrc, ... }:
|
{ lib, pkgs, username, nomarchySrc, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
# ISO boot splash: the Nomarchy monogram recolored to the palette accent,
|
# ISO boot branding: the Nomarchy monogram recolored to the palette accent,
|
||||||
# centred on the theme base. Built from the vendored vector logo and the
|
# centred on the theme base. Built from the vendored vector logo and the
|
||||||
# live theme-state.json (tokyo-night by default). Shows on the isolinux
|
# live theme-state.json (tokyo-night by default). The same composed image
|
||||||
# (BIOS) boot menu; UEFI/GRUB still uses the stock theme (see roadmap).
|
# backs both the isolinux (BIOS) splash and the GRUB (UEFI) theme below, so
|
||||||
|
# the two boot paths match.
|
||||||
state = builtins.fromJSON (builtins.readFile ../theme-state.json);
|
state = builtins.fromJSON (builtins.readFile ../theme-state.json);
|
||||||
isoColor = key: fallback: lib.removePrefix "#" ((state.colors or { }).${key} or fallback);
|
isoColor = key: fallback: lib.removePrefix "#" ((state.colors or { }).${key} or fallback);
|
||||||
|
accent = isoColor "accent" "7aa2f7";
|
||||||
|
base = isoColor "base" "1a1b26";
|
||||||
|
subtext = isoColor "subtext" "787c99";
|
||||||
|
|
||||||
isoSplash = pkgs.runCommand "nomarchy-iso-splash.png"
|
isoSplash = pkgs.runCommand "nomarchy-iso-splash.png"
|
||||||
{ nativeBuildInputs = [ pkgs.imagemagick pkgs.librsvg ]; } ''
|
{ nativeBuildInputs = [ pkgs.imagemagick pkgs.librsvg ]; } ''
|
||||||
rsvg-convert -h 320 ${../modules/nixos/branding/logo.svg} > logo.png
|
rsvg-convert -h 320 ${../modules/nixos/branding/logo.svg} > logo.png
|
||||||
magick logo.png -fill "#${isoColor "accent" "7aa2f7"}" -colorize 100 logo-c.png
|
magick logo.png -fill "#${accent}" -colorize 100 logo-c.png
|
||||||
magick -size 1920x1080 xc:"#${isoColor "base" "1a1b26"}" \
|
magick -size 1920x1080 xc:"#${base}" \
|
||||||
logo-c.png -gravity center -composite $out
|
logo-c.png -gravity center -composite $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
# GRUB (UEFI) theme matched to the BIOS splash: the same accent-logo-on-base
|
||||||
|
# image as the background, plus a palette-coloured boot menu in the lower
|
||||||
|
# third (clear of the centred logo). Derived from nixos-grub2-theme only to
|
||||||
|
# reuse its bundled DejaVu .pf2 font — we overwrite the background and
|
||||||
|
# theme.txt and drop the stock NixOS logo (ours is in the background). grub
|
||||||
|
# loads every .pf2 in the dir, so "DejaVu Regular" resolves.
|
||||||
|
grubThemeTxt = pkgs.writeText "nomarchy-grub-theme.txt" ''
|
||||||
|
title-text: ""
|
||||||
|
desktop-image: "background.png"
|
||||||
|
desktop-color: "#${base}"
|
||||||
|
|
||||||
|
message-font: "DejaVu Regular"
|
||||||
|
message-color: "#${subtext}"
|
||||||
|
terminal-font: "DejaVu Regular"
|
||||||
|
|
||||||
|
+ boot_menu {
|
||||||
|
left = 50%-300
|
||||||
|
width = 600
|
||||||
|
top = 64%
|
||||||
|
height = 26%
|
||||||
|
item_font = "DejaVu Regular"
|
||||||
|
item_color = "#${subtext}"
|
||||||
|
item_height = 36
|
||||||
|
item_spacing = 6
|
||||||
|
selected_item_font = "DejaVu Regular"
|
||||||
|
selected_item_color = "#${accent}"
|
||||||
|
scrollbar = false
|
||||||
|
}
|
||||||
|
|
||||||
|
+ progress_bar {
|
||||||
|
id = "__timeout__"
|
||||||
|
left = 50%-300
|
||||||
|
top = 92%
|
||||||
|
width = 600
|
||||||
|
height = 16
|
||||||
|
show_text = true
|
||||||
|
text = "@TIMEOUT_NOTIFICATION_MIDDLE@"
|
||||||
|
font = "DejaVu Regular"
|
||||||
|
text_color = "#${subtext}"
|
||||||
|
border_color = "#${accent}"
|
||||||
|
bg_color = "#${base}"
|
||||||
|
fg_color = "#${accent}"
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
|
||||||
|
nomarchyGrubTheme = pkgs.runCommand "nomarchy-grub-theme" { } ''
|
||||||
|
cp -r ${pkgs.nixos-grub2-theme} $out
|
||||||
|
chmod -R u+w $out
|
||||||
|
cp ${isoSplash} $out/background.png
|
||||||
|
cp ${grubThemeTxt} $out/theme.txt
|
||||||
|
rm -f $out/logo.png
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
networking.hostName = "nomarchy-live";
|
networking.hostName = "nomarchy-live";
|
||||||
|
|
||||||
isoImage.volumeID = lib.mkForce "NOMARCHY_LIVE";
|
isoImage.volumeID = lib.mkForce "NOMARCHY_LIVE";
|
||||||
isoImage.edition = lib.mkForce "live";
|
isoImage.edition = lib.mkForce "live";
|
||||||
isoImage.splashImage = isoSplash;
|
isoImage.splashImage = isoSplash; # isolinux / BIOS
|
||||||
|
isoImage.grubTheme = nomarchyGrubTheme; # GRUB / UEFI
|
||||||
|
|
||||||
# The minimal-CD profile slims the image for a CONSOLE installer; this
|
# The minimal-CD profile slims the image for a CONSOLE installer; this
|
||||||
# ISO is the desktop, so re-enable what it strips. Above all
|
# ISO is the desktop, so re-enable what it strips. Above all
|
||||||
|
|||||||
@@ -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,8 @@
|
|||||||
./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
|
||||||
|
./timezone.nix # keep the Waybar clock in step with auto-timezone changes
|
||||||
|
./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 +34,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; [
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ let
|
|||||||
c = t.colors;
|
c = t.colors;
|
||||||
inherit (config.nomarchy.lib) rgb rgba;
|
inherit (config.nomarchy.lib) rgb rgba;
|
||||||
|
|
||||||
|
# nomarchy-theme-sync — the in-flake state writer the keyboard watcher uses
|
||||||
|
# to remember per-device layouts (same path night-light's toggle takes).
|
||||||
|
sync = lib.getExe config.nomarchy.package;
|
||||||
|
|
||||||
# swayosd's `--input-volume mute-toggle` draws the OSD but never flips the
|
# swayosd's `--input-volume mute-toggle` draws the OSD but never flips the
|
||||||
# source mute (verified on hardware: wpctl toggles the state, swayosd's
|
# source mute (verified on hardware: wpctl toggles the state, swayosd's
|
||||||
# input-mute path is a no-op — unlike its working output-mute path). Do the
|
# input-mute path is a no-op — unlike its working output-mute path). Do the
|
||||||
@@ -55,10 +59,40 @@ let
|
|||||||
++ lib.optional (m.extra != "") m.extra
|
++ lib.optional (m.extra != "") m.extra
|
||||||
);
|
);
|
||||||
|
|
||||||
# Keyboard layout candidates: the primary nomarchy.keyboard.layout (split on
|
# Menu-remembered per-output resolutions (settings.monitors: output-name ->
|
||||||
# commas) plus any nomarchy.keyboard.layouts. Drives input.kb_layout so all
|
# "WxH@R", written instantly by the Display menu — see rofi.nix) overlaid onto
|
||||||
# of them are switchable, and feeds the watcher's picker.
|
# the declarative nomarchy.monitors by name: a declared output keeps its
|
||||||
kbLayouts = lib.unique (
|
# position/scale/etc and only its resolution changes; an output covered solely
|
||||||
|
# by the `,preferred,auto,1` wildcard (e.g. the laptop's built-in panel)
|
||||||
|
# becomes a new rule with default position/scale. The menu writes the in-flake
|
||||||
|
# state with --no-switch (no rebuild) and applies the mode live via hyprctl;
|
||||||
|
# this bakes the same choice on the next rebuild — the monitor twin of the
|
||||||
|
# keyboard graduation. The menu pick wins over a hand-set resolution (it's the
|
||||||
|
# explicit live action); other declared fields are untouched.
|
||||||
|
resOverrides = config.nomarchy.settings.monitors;
|
||||||
|
# Skeleton for an output that's only menu-set (mirrors the monitorType defaults).
|
||||||
|
monitorDefaults = {
|
||||||
|
resolution = "preferred"; position = "auto"; scale = 1;
|
||||||
|
transform = null; mirror = null; bitdepth = null; vrr = null; extra = "";
|
||||||
|
};
|
||||||
|
declaredNames = map (m: m.name) config.nomarchy.monitors;
|
||||||
|
resolvedMonitors =
|
||||||
|
map (m: if resOverrides ? ${m.name}
|
||||||
|
then m // { resolution = resOverrides.${m.name}; }
|
||||||
|
else m)
|
||||||
|
config.nomarchy.monitors
|
||||||
|
++ lib.mapAttrsToList
|
||||||
|
(name: res: monitorDefaults // { inherit name; resolution = res; })
|
||||||
|
(lib.filterAttrs (name: _: ! lib.elem name declaredNames) resOverrides);
|
||||||
|
|
||||||
|
# Candidate layouts offered by the new-keyboard picker: the session
|
||||||
|
# layout(s) (nomarchy.keyboard.layout, comma-split for a multi-layout
|
||||||
|
# session) plus the extra nomarchy.keyboard.layouts pool. The pool is
|
||||||
|
# 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,21 +102,41 @@ 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).
|
||||||
|
#
|
||||||
|
# The remembered choices live in the git-tracked in-flake state
|
||||||
|
# (settings.keyboard.devices), NOT ~/.local/state — read live from the
|
||||||
|
# working tree (so a pick this session is honoured at once) and written
|
||||||
|
# instantly with `--no-switch` (no rebuild). A later rebuild graduates them
|
||||||
|
# into nomarchy.keyboard.devices (generated device{} blocks), after which the
|
||||||
|
# watcher treats them as declared and steps back. 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"
|
sync=${sync}
|
||||||
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"; }
|
|
||||||
|
# The remembered map (device-name -> layout) from the LIVE in-flake state;
|
||||||
|
# an absent key (sparse state) reads as an empty object.
|
||||||
|
saved_map() { "$sync" get settings.keyboard.devices 2>/dev/null || echo '{}'; }
|
||||||
|
saved_for() { saved_map | jq -r --arg k "$1" '.[$k] // empty'; }
|
||||||
|
|
||||||
|
# Persist a pick INSTANTLY: merge it into the map and write the whole object
|
||||||
|
# back at the dot-free parent path, so a device name containing a dot can't
|
||||||
|
# corrupt the dotted set-path. --no-switch = write only, no rebuild.
|
||||||
|
remember() {
|
||||||
|
map=$(saved_map | jq -c --arg k "$1" --arg v "$2" '. + {($k): $v}') || return
|
||||||
|
"$sync" --quiet set settings.keyboard.devices "$map" --no-switch
|
||||||
|
}
|
||||||
|
|
||||||
# Startup: re-apply remembered layouts, never prompt — so the built-in
|
# Startup: re-apply remembered layouts, never prompt — so the built-in
|
||||||
# keyboard just stays on the session default.
|
# keyboard just stays on the session default.
|
||||||
@@ -106,7 +160,7 @@ let
|
|||||||
apply "$kb" "$s"
|
apply "$kb" "$s"
|
||||||
else
|
else
|
||||||
choice=$(printf '%s\n' $layouts | rofi -dmenu -p "Layout · $kb")
|
choice=$(printf '%s\n' $layouts | rofi -dmenu -p "Layout · $kb")
|
||||||
[ -n "$choice" ] && { printf '%s=%s\n' "$kb" "$choice" >> "$state"; apply "$kb" "$choice"; }
|
[ -n "$choice" ] && { remember "$kb" "$choice"; apply "$kb" "$choice"; }
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
prev="$cur"
|
prev="$cur"
|
||||||
@@ -135,7 +189,7 @@ in
|
|||||||
# and Hyprland applies them on hotplug. mkDefault so a downstream
|
# and Hyprland applies them on hotplug. mkDefault so a downstream
|
||||||
# `monitor = [...]` replaces the lot (the live ISO uses mkForce too).
|
# `monitor = [...]` replaces the lot (the live ISO uses mkForce too).
|
||||||
monitor = lib.mkDefault
|
monitor = lib.mkDefault
|
||||||
([ ",preferred,auto,1" ] ++ map monitorRule config.nomarchy.monitors);
|
([ ",preferred,auto,1" ] ++ map monitorRule resolvedMonitors);
|
||||||
|
|
||||||
# exec-once is a list at normal priority, so downstream additions
|
# exec-once is a list at normal priority, so downstream additions
|
||||||
# CONCATENATE (your autostart runs alongside ours) rather than
|
# CONCATENATE (your autostart runs alongside ours) rather than
|
||||||
@@ -146,7 +200,19 @@ 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";
|
# Polkit authentication agent — without one, EVERY pkexec/polkit
|
||||||
|
# prompt in the session fails silently (btrfs-assistant-launcher
|
||||||
|
# was the discovery case). hyprpolkitagent is Hyprland's own Qt
|
||||||
|
# agent, so the prompt is Stylix-themed like every other Qt
|
||||||
|
# surface. Its binary lives in libexec (not bin), hence the path.
|
||||||
|
"${pkgs.hyprpolkitagent}/libexec/hyprpolkitagent"
|
||||||
|
# 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 +267,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;
|
||||||
@@ -236,8 +304,13 @@ in
|
|||||||
# Rendered from ./keybinds.nix (the cheatsheet reads the same list),
|
# Rendered from ./keybinds.nix (the cheatsheet reads the same list),
|
||||||
# plus the generated per-workspace binds. Like exec-once above this
|
# plus the generated per-workspace binds. Like exec-once above this
|
||||||
# stays a normal-priority list, so a downstream `bind = [...]`
|
# stays a normal-priority list, so a downstream `bind = [...]`
|
||||||
# concatenates rather than replaces.
|
# concatenates rather than replaces. The layout-cycle bind only
|
||||||
bind = map mkBind keybinds.binds ++ workspaceBinds;
|
# exists when there is something to cycle (same comma condition
|
||||||
|
# the Waybar language indicator gates on).
|
||||||
|
bind = map mkBind (keybinds.binds
|
||||||
|
++ lib.optionals (lib.hasInfix "," config.nomarchy.keyboard.layout)
|
||||||
|
keybinds.multiLayoutBinds)
|
||||||
|
++ workspaceBinds;
|
||||||
|
|
||||||
# Media keys via swayosd-client: it performs the action AND shows
|
# Media keys via swayosd-client: it performs the action AND shows
|
||||||
# the on-screen display (the nomarchy.osd module). e = repeat,
|
# the on-screen display (the nomarchy.osd module). e = repeat,
|
||||||
@@ -266,11 +339,24 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Runtime-remembered per-device layouts (settings.keyboard.devices, written
|
||||||
|
# by the watcher into the in-flake state) graduate into the declarative
|
||||||
|
# keyboard.devices: each becomes a generated Hyprland device{} block —
|
||||||
|
# reproducible and applied natively on hotplug — after which the watcher sees
|
||||||
|
# it as declared and steps back. mkDefault so a hand-written
|
||||||
|
# keyboard.devices.<name> in home.nix still wins for the same keyboard.
|
||||||
|
nomarchy.keyboard.devices = lib.mapAttrs
|
||||||
|
(_name: layout: { layout = lib.mkDefault layout; })
|
||||||
|
config.nomarchy.settings.keyboard.devices;
|
||||||
|
|
||||||
# nwg-displays: interactive monitor arranger (applies live via hyprctl and
|
# nwg-displays: interactive monitor arranger (applies live via hyprctl and
|
||||||
# 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,15 @@
|
|||||||
{ mods = ""; key = "Print"; action = "exec, grim -g \"$(slurp)\" - | wl-copy"; desc = "Screenshot region → clipboard"; }
|
{ mods = ""; key = "Print"; action = "exec, grim -g \"$(slurp)\" - | wl-copy"; desc = "Screenshot region → clipboard"; }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# Rendered only when the session has >1 layout (a comma in
|
||||||
|
# nomarchy.keyboard.layout) — hyprland.nix and rofi.nix both gate on
|
||||||
|
# that same condition, so the bind and its cheatsheet row stay in
|
||||||
|
# step. `current` targets the focused keyboard, so a board with its
|
||||||
|
# own per-device layout (a single one) is a no-op, never a leak.
|
||||||
|
multiLayoutBinds = [
|
||||||
|
{ mods = "$mod SHIFT"; key = "K"; action = "exec, hyprctl switchxkblayout current next"; desc = "Cycle keyboard layout"; }
|
||||||
|
];
|
||||||
|
|
||||||
extra = [
|
extra = [
|
||||||
{ keys = "SUPER + 1-9"; desc = "Switch to workspace 1-9"; }
|
{ keys = "SUPER + 1-9"; desc = "Switch to workspace 1-9"; }
|
||||||
{ keys = "SUPER + SHIFT + 1-9"; desc = "Move window to workspace 1-9"; }
|
{ keys = "SUPER + SHIFT + 1-9"; desc = "Move window to workspace 1-9"; }
|
||||||
|
|||||||
@@ -8,10 +8,12 @@
|
|||||||
# GNOME session to lean on) and themed by Stylix's Qt config, so the
|
# GNOME session to lean on) and themed by Stylix's Qt config, so the
|
||||||
# passphrase dialog tracks the palette.
|
# passphrase dialog tracks the palette.
|
||||||
#
|
#
|
||||||
# SSH_AUTH_SOCK is exported by the agent's shell integration (zsh, on via
|
# SSH_AUTH_SOCK reaches both terminal and GUI clients: the agent's zsh
|
||||||
# home.shell.enableZshIntegration in shell.nix). Terminal git/ssh is the
|
# integration sets it in interactive shells (home.shell.enableZshIntegration
|
||||||
# supported path; a GUI client launched outside a shell won't inherit the
|
# in shell.nix), and a session-level home.sessionVariables export (below)
|
||||||
# socket — revisit with a session-level export if that's ever wanted.
|
# covers GUI clients launched outside a shell — e.g. from the rofi launcher —
|
||||||
|
# which never inherit the interactive shell's copy. Both resolve the same
|
||||||
|
# socket via gpgconf, so they can't drift.
|
||||||
#
|
#
|
||||||
# gnome-keyring (system side) stays the Secret Service for application
|
# gnome-keyring (system side) stays the Secret Service for application
|
||||||
# secrets; modern gnome-keyring no longer runs an SSH agent, so there is no
|
# secrets; modern gnome-keyring no longer runs an SSH agent, so there is no
|
||||||
@@ -38,5 +40,16 @@ in
|
|||||||
defaultCacheTtlSsh = 1800;
|
defaultCacheTtlSsh = 1800;
|
||||||
maxCacheTtlSsh = 7200;
|
maxCacheTtlSsh = 7200;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Session-level SSH_AUTH_SOCK so GUI clients launched outside a shell
|
||||||
|
# (rofi launcher, autostarted apps) reach the agent — the shell
|
||||||
|
# integration only covers interactive shells. Resolved with gpgconf at
|
||||||
|
# session-init time (the same lookup the shell integration uses), so it
|
||||||
|
# tracks the agent's real socket rather than a hardcoded path; valid
|
||||||
|
# before first use since the socket is systemd-activated. Reaches GUI
|
||||||
|
# apps the way NIXOS_OZONE_WL does — sourced into the login shell that
|
||||||
|
# starts Hyprland, so every spawned client inherits it.
|
||||||
|
home.sessionVariables.SSH_AUTH_SOCK =
|
||||||
|
"$(${pkgs.gnupg}/bin/gpgconf --list-dirs agent-ssh-socket)";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,75 @@
|
|||||||
# Night light — a scheduled blue-light filter via hyprsunset (Hyprland's own
|
# Night light — a scheduled blue-light filter via hyprsunset (Hyprland's own
|
||||||
# gamma/temperature tool). Warm at night, identity (no shift) by day;
|
# gamma/temperature tool). Warm at night, identity (no shift) by day; the
|
||||||
# hyprsunset's time-based `profile` entries handle the schedule and pick the
|
# schedule (temperature/sunrise/sunset) is tuned via nomarchy.nightlight.* in
|
||||||
# right state on session start. Opt-in via nomarchy.nightlight.enable.
|
# home.nix and baked into the unit's time-based `profile`.
|
||||||
#
|
#
|
||||||
# The hyprsunset HM service module is provided by home-manager; this only
|
# Off by default and opt-in. Two git-tracked flags in the state file, both
|
||||||
# configures it. Override anything with plain services.hyprsunset.* options.
|
# menu-written (no ~/.local/state):
|
||||||
{ config, lib, ... }:
|
# settings.nightlight.installed — does the hyprsunset unit exist? Sticky; the
|
||||||
|
# option mkDefault-reads it, so the FIRST enable from the menu rebuilds (to
|
||||||
|
# create the unit) and an instant-off is never undone by a later rebuild.
|
||||||
|
# settings.nightlight.on — runtime on/off. Toggled INSTANTLY (write + systemctl,
|
||||||
|
# no rebuild); read by the unit's ExecCondition (should-start) at session
|
||||||
|
# start so the choice survives logout/reboot via the *live* state, not the
|
||||||
|
# eval-frozen store copy. A later rebuild bakes the same value (no divergence).
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.nomarchy.nightlight;
|
cfg = config.nomarchy.nightlight;
|
||||||
|
s = config.nomarchy.settings.nightlight;
|
||||||
|
sync = lib.getExe config.nomarchy.package;
|
||||||
|
# Runtime-on default for when the `on` key hasn't been written yet (e.g. right
|
||||||
|
# after the first enable). Baked at eval; only used when the live key is absent.
|
||||||
|
onDefault = lib.boolToString s.on;
|
||||||
|
|
||||||
|
nomarchy-nightlight = pkgs.writeShellScriptBin "nomarchy-nightlight" ''
|
||||||
|
unit=hyprsunset.service
|
||||||
|
# Instant runtime on/off: write the in-flake state WITHOUT a rebuild.
|
||||||
|
write_on() { ${sync} --quiet set settings.nightlight.on "$1" --no-switch; }
|
||||||
|
# First enable: mark the feature installed and REBUILD to create the unit
|
||||||
|
# (the one rebuild we accept; every toggle after is instant).
|
||||||
|
install_feature() { ${sync} --quiet set settings.nightlight.installed true; }
|
||||||
|
# Read the LIVE working-tree on/off (~/.nomarchy via $NOMARCHY_PATH), not the
|
||||||
|
# store copy baked into this generation; fall back to the eval-time default
|
||||||
|
# when absent. Normalise Python's True/False bool rendering.
|
||||||
|
is_on() {
|
||||||
|
v=$(${sync} get settings.nightlight.on 2>/dev/null) || v=${onDefault}
|
||||||
|
case "$v" in true|True) return 0 ;; *) return 1 ;; esac
|
||||||
|
}
|
||||||
|
installed() { systemctl --user cat "$unit" >/dev/null 2>&1; }
|
||||||
|
start() { systemctl --user start "$unit" 2>/dev/null || true; }
|
||||||
|
stop() { systemctl --user stop "$unit" 2>/dev/null || true; }
|
||||||
|
case "''${1:-toggle}" in
|
||||||
|
should-start) is_on ;; # ExecCondition gate (login/reboot)
|
||||||
|
status)
|
||||||
|
# Waybar (polls every 3s): moon while running; print nothing otherwise so
|
||||||
|
# the module self-hides — enable / re-enable from the System menu.
|
||||||
|
systemctl --user is-active --quiet "$unit" \
|
||||||
|
&& printf '{"text":"","tooltip":"Night light on — warm on schedule (click to disable)","class":"on"}\n'
|
||||||
|
exit 0 ;;
|
||||||
|
on)
|
||||||
|
if installed; then write_on true; start; else install_feature; start; fi ;;
|
||||||
|
off) write_on false; stop ;;
|
||||||
|
toggle)
|
||||||
|
if systemctl --user is-active --quiet "$unit"; then
|
||||||
|
write_on false; stop # on -> off (instant)
|
||||||
|
elif installed; then
|
||||||
|
write_on true; start # installed, off -> on (instant)
|
||||||
|
else
|
||||||
|
install_feature; start # first enable (rebuilds)
|
||||||
|
fi ;;
|
||||||
|
*) echo "usage: nomarchy-nightlight [toggle|status|on|off|should-start]" >&2; exit 64 ;;
|
||||||
|
esac
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.enable {
|
config = {
|
||||||
services.hyprsunset = {
|
# Unit presence tracks the sticky `installed` flag the menu writes (first
|
||||||
|
# enable rebuilds). mkDefault so a hand-set nomarchy.nightlight.enable in
|
||||||
|
# home.nix also works as a declarative opt-in.
|
||||||
|
nomarchy.nightlight.enable = lib.mkDefault s.installed;
|
||||||
|
|
||||||
|
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 +78,13 @@ in
|
|||||||
{ time = cfg.sunset; temperature = cfg.temperature; }
|
{ time = cfg.sunset; temperature = cfg.temperature; }
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Gate the unit on the LIVE on/off state at start time (login/reboot), not
|
||||||
|
# at eval time — so a menu toggle (written without a rebuild) is honoured on
|
||||||
|
# the next session. A failed condition skips the unit (inactive, not failed).
|
||||||
|
systemd.user.services.hyprsunset.Service.ExecCondition =
|
||||||
|
lib.mkIf cfg.enable "${nomarchy-nightlight}/bin/nomarchy-nightlight should-start";
|
||||||
|
|
||||||
|
home.packages = [ nomarchy-nightlight ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,10 +105,14 @@ 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 in the git-tracked in-flake state
|
||||||
declarative keyboard.devices; a stateful runtime piece by design.
|
(settings.keyboard.devices, not ~/.local/state) — re-applied
|
||||||
|
automatically on later reconnects and across reboots. Each remembered
|
||||||
|
choice graduates into nomarchy.keyboard.devices on the next rebuild
|
||||||
|
(a generated device block). The runtime-remember complement to the
|
||||||
|
declarative keyboard.devices.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,7 +139,9 @@ in
|
|||||||
nomarchy.keyboard.layout for that keyboard only — e.g. an external
|
nomarchy.keyboard.layout for that keyboard only — e.g. an external
|
||||||
keyboard that's physically a different layout than the laptop's
|
keyboard that's physically a different layout than the laptop's
|
||||||
built-in one. Hyprland applies it automatically whenever that
|
built-in one. Hyprland applies it automatically whenever that
|
||||||
keyboard is connected.
|
keyboard is connected. The interactive watcher
|
||||||
|
(nomarchy.keyboard.layouts) also writes its remembered picks here on
|
||||||
|
the next rebuild, so a runtime choice graduates into reproducible config.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -184,6 +190,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 = [ ];
|
||||||
@@ -228,6 +260,17 @@ in
|
|||||||
description = "The parsed theme state (stateFile merged over defaults).";
|
description = "The parsed theme state (stateFile merged over defaults).";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
settings = lib.mkOption {
|
||||||
|
type = lib.types.attrs;
|
||||||
|
readOnly = true;
|
||||||
|
description = ''
|
||||||
|
Parsed feature settings — the `settings` section of the state file,
|
||||||
|
what the menu/Waybar toggles write (e.g. settings.nightlight.enable).
|
||||||
|
Feature options mkDefault-read from here, so a menu toggle lands in the
|
||||||
|
in-flake state (git-tracked, reproducible) rather than ~/.local/state.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
lib = lib.mkOption {
|
lib = lib.mkOption {
|
||||||
type = lib.types.attrs;
|
type = lib.types.attrs;
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -33,7 +119,9 @@ let
|
|||||||
key = lib.replaceStrings [ "question" "slash" ] [ "?" "/" ] b.key;
|
key = lib.replaceStrings [ "question" "slash" ] [ "?" "/" ] b.key;
|
||||||
in prefix + key;
|
in prefix + key;
|
||||||
cheatRows =
|
cheatRows =
|
||||||
map (b: padRight 22 (prettyKeys b) + b.desc) keybinds.binds
|
map (b: padRight 22 (prettyKeys b) + b.desc) (keybinds.binds
|
||||||
|
++ lib.optionals (lib.hasInfix "," cfg.keyboard.layout)
|
||||||
|
keybinds.multiLayoutBinds)
|
||||||
++ map (e: padRight 22 e.keys + e.desc) keybinds.extra;
|
++ map (e: padRight 22 e.keys + e.desc) keybinds.extra;
|
||||||
cheatsheetFile = pkgs.writeText "nomarchy-keybinds.txt"
|
cheatsheetFile = pkgs.writeText "nomarchy-keybinds.txt"
|
||||||
(lib.concatStringsSep "\n" cheatRows + "\n");
|
(lib.concatStringsSep "\n" cheatRows + "\n");
|
||||||
@@ -64,6 +152,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 +169,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 +190,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 + Name→slug
|
||||||
[ -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 +225,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 +243,107 @@ 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 ;;
|
||||||
|
|
||||||
|
vpn)
|
||||||
|
# VPN setup + management (nomarchy-vpn): NM VPN/WireGuard connect +
|
||||||
|
# config import + Tailscale. Distinct from `network` (the wifi picker).
|
||||||
|
exec nomarchy-vpn ;;
|
||||||
|
|
||||||
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 ;;
|
||||||
|
|
||||||
|
display)
|
||||||
|
# Per-output resolution picker. Applies the chosen mode INSTANTLY via
|
||||||
|
# `hyprctl keyword monitor` — keeping the output's current position and
|
||||||
|
# scale, so only the resolution changes — and PERSISTS it to the in-flake
|
||||||
|
# state (settings.monitors.<name>, --no-switch = no rebuild), so the
|
||||||
|
# choice survives logout/reboot. A later rebuild bakes the same
|
||||||
|
# resolution into the generated monitor rule (hyprland.nix overlays
|
||||||
|
# settings.monitors onto nomarchy.monitors by name). Self-gated to the
|
||||||
|
# Hyprland session; guard here too in case it's invoked directly.
|
||||||
|
command -v hyprctl >/dev/null 2>&1 \
|
||||||
|
|| { notify-send "Display" "Hyprland is not running."; exit 0; }
|
||||||
|
mons=$(hyprctl monitors -j)
|
||||||
|
|
||||||
|
# Choose the output (skip the chooser when only one is connected).
|
||||||
|
if [ "$(printf '%s' "$mons" | jq -r '.[].name' | grep -c .)" -gt 1 ]; then
|
||||||
|
name=$( {
|
||||||
|
printf '%s' "$mons" | jq -r '.[] | "\(.name) · \(.width)x\(.height)@\(.refreshRate|round)Hz"'
|
||||||
|
printf '%s\n' "$BACK"
|
||||||
|
} | rofi -dmenu -p Display ) || exit 0
|
||||||
|
[ "$name" = "$BACK" ] && exec "$0" system
|
||||||
|
name=''${name%% ·*} # strip the " · WxH@R" hint
|
||||||
|
else
|
||||||
|
name=$(printf '%s' "$mons" | jq -r '.[0].name')
|
||||||
|
fi
|
||||||
|
[ -n "$name" ] || exit 0
|
||||||
|
|
||||||
|
# Pick a mode: a few Hyprland resolution tokens, then the output's
|
||||||
|
# advertised modes (rounded refresh rate, deduped, highest first). The
|
||||||
|
# prompt shows the current mode.
|
||||||
|
cur=$(printf '%s' "$mons" | jq -r --arg n "$name" \
|
||||||
|
'.[]|select(.name==$n)|"\(.width)x\(.height)@\(.refreshRate|round)"')
|
||||||
|
mode=$( {
|
||||||
|
printf 'preferred\nhighres\nhighrr\n'
|
||||||
|
printf '%s' "$mons" | jq -r --arg n "$name" '.[]|select(.name==$n)|.availableModes[]' \
|
||||||
|
| sed 's/Hz$//' | awk -F@ '{ printf "%s@%d\n", $1, $2 + 0.5 }' | sort -rV -u
|
||||||
|
printf '%s\n' "$BACK"
|
||||||
|
} | rofi -dmenu -p "$name (now $cur)" ) || exit 0
|
||||||
|
[ "$mode" = "$BACK" ] && exec "$0" display
|
||||||
|
[ -n "$mode" ] || exit 0
|
||||||
|
|
||||||
|
# Apply live (keep the output's current position + scale), then persist.
|
||||||
|
pos=$(printf '%s' "$mons" | jq -r --arg n "$name" '.[]|select(.name==$n)|"\(.x)x\(.y)"')
|
||||||
|
scale=$(printf '%s' "$mons" | jq -r --arg n "$name" '.[]|select(.name==$n)|.scale')
|
||||||
|
if hyprctl keyword monitor "$name,$mode,$pos,$scale" >/dev/null 2>&1; then
|
||||||
|
nomarchy-theme-sync --quiet set "settings.monitors.$name" "$mode" --no-switch
|
||||||
|
notify-send "Display" "$name → $mode"
|
||||||
|
else
|
||||||
|
notify-send "Display" "Could not set $name to $mode."
|
||||||
|
fi ;;
|
||||||
|
|
||||||
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 +373,53 @@ 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 ;;
|
||||||
|
|
||||||
|
autotimezone)
|
||||||
|
# Toggle automatic timezone detection (geoclue + automatic-timezoned).
|
||||||
|
# A SYSTEM service, so flipping it writes the in-flake flag and runs a
|
||||||
|
# system rebuild (sudo) + a home switch — in a terminal, like Snapshots.
|
||||||
|
command -v nomarchy-autotimezone >/dev/null 2>&1 \
|
||||||
|
|| { notify-send "Auto timezone" "Unavailable on this machine."; exit 0; }
|
||||||
|
exec ${cfg.terminal} -e nomarchy-autotimezone toggle ;;
|
||||||
|
|
||||||
|
autocommit)
|
||||||
|
# Toggle opt-in auto-commit: every menu/theme mutation also commits
|
||||||
|
# theme-state.json (that file only) in the downstream flake, so
|
||||||
|
# settings history is `git log`. Nothing in Nix consumes the flag —
|
||||||
|
# the tool reads the live state on each write — so the toggle is
|
||||||
|
# instant, no rebuild. The off-write commits too (the tool fires
|
||||||
|
# when the flag was on before OR after), keeping history consistent.
|
||||||
|
if [ "$(nomarchy-theme-sync get settings.autoCommit 2>/dev/null)" = true ]; then
|
||||||
|
nomarchy-theme-sync --quiet set settings.autoCommit false --no-switch
|
||||||
|
notify-send "Auto-commit" "Off — menu changes stay uncommitted in your flake."
|
||||||
|
else
|
||||||
|
nomarchy-theme-sync --quiet set settings.autoCommit true --no-switch
|
||||||
|
notify-send "Auto-commit" "On — each change commits theme-state.json in your flake."
|
||||||
|
fi
|
||||||
|
exit 0 ;;
|
||||||
|
|
||||||
snapshot)
|
snapshot)
|
||||||
# btrfs-assistant: snapshot browse / diff / restore / rollback over
|
# Snapshot browse/restore — the btrfs-assistant GUI via its pkexec
|
||||||
# snapper, elevating via polkit. Shipped system-side with
|
# launcher. The "2.2 segfaults on 26.05" diagnosis was HALF right:
|
||||||
# nomarchy.system.snapper; self-gate so the entry no-ops elsewhere.
|
# only UNPRIVILEGED runs crash (libbtrfsutil unprivileged subvolume
|
||||||
command -v btrfs-assistant >/dev/null 2>&1 \
|
# iteration, btrfs-progs 6.17.1; fixed upstream after) — as root it
|
||||||
|| { notify-send "Snapshots" "btrfs-assistant isn't installed (BTRFS snapshots off?)."; exit 0; }
|
# runs fine, and the launcher runs it as root. The prompt needs the
|
||||||
exec btrfs-assistant ;;
|
# session polkit agent (hyprpolkitagent, exec-once in hyprland.nix).
|
||||||
|
# Fallback: nomarchy-snapshots, the keyboard-driven fzf browser in a
|
||||||
|
# terminal (kept installed — also nice over SSH).
|
||||||
|
# Self-gated to nomarchy.system.snapper, so it no-ops elsewhere.
|
||||||
|
command -v btrfs-assistant-launcher >/dev/null 2>&1 \
|
||||||
|
&& exec btrfs-assistant-launcher
|
||||||
|
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 +432,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 +443,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)
|
||||||
@@ -217,21 +452,47 @@ let
|
|||||||
bats=(/sys/class/power_supply/BAT*)
|
bats=(/sys/class/power_supply/BAT*)
|
||||||
choice=$( {
|
choice=$( {
|
||||||
row "Network" network-wireless
|
row "Network" network-wireless
|
||||||
|
row "VPN" network-vpn
|
||||||
row "Bluetooth" bluetooth
|
row "Bluetooth" bluetooth
|
||||||
|
[ -e "''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/pulse/native" ] \
|
||||||
|
&& row "Audio" audio-volume-high
|
||||||
|
row "Display" video-display
|
||||||
|
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
|
if systemctl --user is-active --quiet hyprsunset.service 2>/dev/null
|
||||||
|
then row "Night light (on)" weather-clear-night
|
||||||
|
else row "Night light (off)" weather-clear-night
|
||||||
|
fi
|
||||||
|
if [ "$(nomarchy-theme-sync get settings.autoTimezone 2>/dev/null)" = true ]
|
||||||
|
then row "Auto timezone (on)" preferences-system-time
|
||||||
|
else row "Auto timezone (off)" preferences-system-time
|
||||||
|
fi
|
||||||
|
if [ -e "''${NOMARCHY_PATH:-$HOME/.nomarchy}/.git" ]; then
|
||||||
|
if [ "$(nomarchy-theme-sync get settings.autoCommit 2>/dev/null)" = true ]
|
||||||
|
then row "Auto-commit (on)" git
|
||||||
|
else row "Auto-commit (off)" git
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
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 ;;
|
||||||
|
*VPN*) exec "$0" vpn ;;
|
||||||
*Bluetooth*) exec "$0" bluetooth ;;
|
*Bluetooth*) exec "$0" bluetooth ;;
|
||||||
|
*Audio*) exec "$0" audio ;;
|
||||||
|
*Display*) exec "$0" display ;;
|
||||||
|
*Printers*) exec "$0" printers ;;
|
||||||
*"Do Not Disturb"*) exec "$0" dnd ;;
|
*"Do Not Disturb"*) exec "$0" dnd ;;
|
||||||
|
*"Night light"*) exec "$0" nightlight ;;
|
||||||
|
*"Auto timezone"*) exec "$0" autotimezone ;;
|
||||||
|
*"Auto-commit"*) exec "$0" autocommit ;;
|
||||||
*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,20 +517,142 @@ 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|display|printers|capture|keybinds|ask|dnd|nightlight|autotimezone|autocommit|vpn|snapshot]" >&2
|
||||||
exit 64 ;;
|
exit 64 ;;
|
||||||
esac
|
esac
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
# VPN setup + management — a dedicated System submenu (and the custom/vpn
|
||||||
|
# Waybar indicator's click target). Goes past networkmanager_dmenu (which
|
||||||
|
# only connects existing VPNs) to importing configs and driving Tailscale:
|
||||||
|
# · NetworkManager VPN/WireGuard connections, ● active / ○ inactive, toggled
|
||||||
|
# up/down (networkmanager-group users need no sudo).
|
||||||
|
# · Import a WireGuard .conf or OpenVPN .ovpn (nmcli import; OpenVPN needs the
|
||||||
|
# networkmanager-openvpn plugin, shipped system-side).
|
||||||
|
# · Tailscale (self-gated on the CLI being present = nomarchy.services.tailscale,
|
||||||
|
# which makes the login user the operator): status is read-only; up/down/
|
||||||
|
# exit-node run inline without sudo thanks to the operator grant, falling
|
||||||
|
# back to a sudo terminal if it's absent. First login goes to a terminal.
|
||||||
|
# Secrets stay in NetworkManager's / Tailscale's own store — no new manager.
|
||||||
|
nomarchy-vpn = pkgs.writeShellScriptBin "nomarchy-vpn" ''
|
||||||
|
set -u
|
||||||
|
BACK="↩ Back"
|
||||||
|
|
||||||
|
conn_rows() {
|
||||||
|
nmcli -t -f NAME,TYPE,STATE connection show 2>/dev/null \
|
||||||
|
| awk -F: '$2=="vpn"||$2=="wireguard"{ printf "%s %s\n", ($3=="activated"?"●":"○"), $1 }'
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle_conn() {
|
||||||
|
mark=''${1%% *}; name=''${1#* }
|
||||||
|
if [ "$mark" = "●" ]; then
|
||||||
|
nmcli connection down "$name" >/dev/null 2>&1 \
|
||||||
|
&& notify-send "VPN" "Disconnected $name" || notify-send "VPN" "Could not disconnect $name"
|
||||||
|
else
|
||||||
|
nmcli connection up "$name" >/dev/null 2>&1 \
|
||||||
|
&& notify-send "VPN" "Connected $name" || notify-send "VPN" "Could not connect $name (auth or bad config?)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
import_config() {
|
||||||
|
file=$( { fd -e conf -e ovpn . "$HOME" --type f --hidden \
|
||||||
|
--exclude .git --exclude .cache 2>/dev/null \
|
||||||
|
| sed "s|^$HOME/||"; printf '%s\n' "$BACK"; } \
|
||||||
|
| rofi -dmenu -p "Import VPN config" ) || return
|
||||||
|
{ [ -z "$file" ] || [ "$file" = "$BACK" ]; } && return
|
||||||
|
case "$file" in *.ovpn) type=openvpn ;; *) type=wireguard ;; esac
|
||||||
|
if nmcli connection import type "$type" file "$HOME/$file" >/dev/null 2>&1; then
|
||||||
|
notify-send "VPN" "Imported $type config: $(basename "$file")"
|
||||||
|
else
|
||||||
|
notify-send "VPN" "Import failed ($type plugin missing, or bad file?)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ts_state() { tailscale status --json 2>/dev/null | jq -r '.BackendState // "Unknown"'; }
|
||||||
|
# Run a privileged tailscale subcommand. nomarchy.services.tailscale makes
|
||||||
|
# the login user the operator, so this runs inline (no sudo); if the operator
|
||||||
|
# grant is absent (downstream dropped it) it falls back to a sudo terminal.
|
||||||
|
ts_priv() {
|
||||||
|
if tailscale "$@" >/dev/null 2>&1; then notify-send "Tailscale" "tailscale $*"
|
||||||
|
else ${cfg.terminal} -e sudo tailscale "$@"; fi
|
||||||
|
}
|
||||||
|
ts_exit_node() {
|
||||||
|
node=$( { printf 'none\n'; tailscale exit-node list 2>/dev/null | awk 'NR>1 && $2 {print $2}'; \
|
||||||
|
printf '%s\n' "$BACK"; } | rofi -dmenu -p "Exit node" ) || return
|
||||||
|
{ [ -z "$node" ] || [ "$node" = "$BACK" ]; } && return
|
||||||
|
[ "$node" = none ] && node=""
|
||||||
|
ts_priv set --exit-node="$node"
|
||||||
|
}
|
||||||
|
tailscale_menu() {
|
||||||
|
while :; do
|
||||||
|
st=$(ts_state)
|
||||||
|
choice=$( {
|
||||||
|
if [ "$st" = Running ]; then printf 'Disconnect\n'; else printf 'Connect\n'; fi
|
||||||
|
printf 'Exit node…\n'
|
||||||
|
printf '%s\n' "$BACK"
|
||||||
|
} | rofi -dmenu -p "Tailscale ($st)" ) || return
|
||||||
|
case "$choice" in
|
||||||
|
"$BACK"|"") return ;;
|
||||||
|
# Authed-but-down → bring it up inline; otherwise it needs an
|
||||||
|
# interactive login, so show it in a terminal (URL visible).
|
||||||
|
Connect) if [ "$st" = Stopped ]; then ts_priv up; else ${cfg.terminal} -e sudo tailscale up; fi ;;
|
||||||
|
Disconnect) ts_priv down ;;
|
||||||
|
"Exit node…") ts_exit_node ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
have_ts=""
|
||||||
|
command -v tailscale >/dev/null 2>&1 && have_ts=1
|
||||||
|
choice=$( {
|
||||||
|
conn_rows
|
||||||
|
printf 'Import config…\n'
|
||||||
|
[ -n "$have_ts" ] && printf 'Tailscale ›\n'
|
||||||
|
printf '%s\n' "$BACK"
|
||||||
|
} | rofi -dmenu -p VPN ) || exit 0
|
||||||
|
case "$choice" in
|
||||||
|
"$BACK"|"") exec nomarchy-menu system ;;
|
||||||
|
"Import config…") import_config ;;
|
||||||
|
"Tailscale ›") tailscale_menu ;;
|
||||||
|
"● "*|"○ "*) toggle_conn "$choice" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.rofi.enable {
|
config = lib.mkIf cfg.rofi.enable {
|
||||||
home.packages = [
|
home.packages = [
|
||||||
nomarchy-menu
|
nomarchy-menu
|
||||||
|
nomarchy-vpn # VPN submenu (NM connect/import + Tailscale)
|
||||||
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 +675,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
|
||||||
|
|||||||
@@ -54,6 +54,27 @@ let
|
|||||||
# tone for kanagawa/summer-*). Each preset declares its own so a theme
|
# tone for kanagawa/summer-*). Each preset declares its own so a theme
|
||||||
# switch always replaces it (deep_merge would otherwise leave it stuck).
|
# switch always replaces it (deep_merge would otherwise leave it stuck).
|
||||||
border = { active = "accent"; inactive = "overlay"; };
|
border = { active = "accent"; inactive = "overlay"; };
|
||||||
|
|
||||||
|
# Non-appearance feature settings the menu/watchers write into this same
|
||||||
|
# in-flake state. nomarchy.nightlight: `installed` (sticky — gates the unit,
|
||||||
|
# so the first enable rebuilds) and `on` (instant runtime on/off).
|
||||||
|
# settings.keyboard.devices: per-device layouts the new-keyboard watcher
|
||||||
|
# remembers (device-name -> XKB layout), git-tracked instead of
|
||||||
|
# ~/.local/state; they graduate into nomarchy.keyboard.devices on the next
|
||||||
|
# rebuild. settings.monitors: per-output resolutions the Display menu
|
||||||
|
# remembers (output-name -> "WxH@R"), overlaid onto nomarchy.monitors by
|
||||||
|
# name in hyprland.nix — the monitor twin of the keyboard graduation.
|
||||||
|
# Defaulted so a sparse/older state file still evaluates; nomarchy.settings
|
||||||
|
# exposes them.
|
||||||
|
settings = {
|
||||||
|
nightlight = { installed = false; on = true; };
|
||||||
|
keyboard.devices = { };
|
||||||
|
monitors = { };
|
||||||
|
# Automatic timezone detection (nomarchy.system.autoTimezone): a system
|
||||||
|
# service, but the flag lives here so both sides read one source — the
|
||||||
|
# home side gates the Waybar-refresh watcher (timezone.nix) on it.
|
||||||
|
autoTimezone = false;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
parsed = lib.recursiveUpdate defaults themeState;
|
parsed = lib.recursiveUpdate defaults themeState;
|
||||||
@@ -78,6 +99,11 @@ in
|
|||||||
config = {
|
config = {
|
||||||
nomarchy.theme = parsed // { inherit iconTheme border; };
|
nomarchy.theme = parsed // { inherit iconTheme border; };
|
||||||
|
|
||||||
|
# Feature toggles the menu writes (settings.nightlight.enable, …), exposed
|
||||||
|
# alongside the appearance state. Feature modules mkDefault-read from here
|
||||||
|
# so a menu toggle lands in the flake instead of in ~/.local/state.
|
||||||
|
nomarchy.settings = parsed.settings;
|
||||||
|
|
||||||
nomarchy.lib = {
|
nomarchy.lib = {
|
||||||
# "#7aa2f7" -> "rgb(7aa2f7)" (Hyprland color syntax)
|
# "#7aa2f7" -> "rgb(7aa2f7)" (Hyprland color syntax)
|
||||||
rgb = c: "rgb(${lib.removePrefix "#" c})";
|
rgb = c: "rgb(${lib.removePrefix "#" c})";
|
||||||
|
|||||||
44
modules/home/timezone.nix
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Auto-timezone, home side: keep the Waybar clock in step with the system
|
||||||
|
# timezone. Waybar's clock module captures the zone once at construction, so a
|
||||||
|
# runtime timezone change (automatic-timezoned, nomarchy.system.autoTimezone)
|
||||||
|
# would NOT show until a relogin. A tiny watcher subscribes to timedate1's
|
||||||
|
# change signal and reloads Waybar (SIGUSR2 = the same reload theme-sync uses),
|
||||||
|
# so the clock follows your location live. Also catches a manual
|
||||||
|
# `timedatectl set-timezone`.
|
||||||
|
#
|
||||||
|
# Gated on the same in-flake flag the system side reads (settings.autoTimezone,
|
||||||
|
# exposed via nomarchy.settings) — so it only runs when the feature is on. The
|
||||||
|
# menu toggle (nomarchy-autotimezone) rebuilds both sides off that one flag.
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.nomarchy;
|
||||||
|
enabled = cfg.waybar.enable && (cfg.settings.autoTimezone or false);
|
||||||
|
|
||||||
|
tzWatch = pkgs.writeShellScript "nomarchy-tz-watch" ''
|
||||||
|
last=$(${pkgs.systemd}/bin/timedatectl show -p Timezone --value 2>/dev/null || true)
|
||||||
|
${pkgs.dbus}/bin/dbus-monitor --system \
|
||||||
|
"type='signal',interface='org.freedesktop.DBus.Properties',path='/org/freedesktop/timedate1',member='PropertiesChanged'" \
|
||||||
|
2>/dev/null |
|
||||||
|
while read -r _; do
|
||||||
|
cur=$(${pkgs.systemd}/bin/timedatectl show -p Timezone --value 2>/dev/null || true)
|
||||||
|
[ "$cur" = "$last" ] && continue
|
||||||
|
last=$cur
|
||||||
|
${pkgs.procps}/bin/pkill -SIGUSR2 -x waybar 2>/dev/null || true
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
systemd.user.services.nomarchy-tz-watch = lib.mkIf enabled {
|
||||||
|
Unit = {
|
||||||
|
Description = "Reload Waybar on timezone change (auto-timezone)";
|
||||||
|
PartOf = [ "graphical-session.target" ];
|
||||||
|
After = [ "graphical-session.target" ];
|
||||||
|
};
|
||||||
|
Service = {
|
||||||
|
ExecStart = "${tzWatch}";
|
||||||
|
Restart = "on-failure";
|
||||||
|
};
|
||||||
|
Install.WantedBy = [ "graphical-session.target" ];
|
||||||
|
};
|
||||||
|
}
|
||||||
141
modules/home/updates.nix
Normal 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" ];
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -51,6 +51,19 @@ let
|
|||||||
[ -n "$next" ] && powerprofilesctl set "$next"
|
[ -n "$next" ] && powerprofilesctl set "$next"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
# VPN indicator — shows a shield when a NetworkManager VPN/WireGuard
|
||||||
|
# connection is active OR Tailscale is up; prints nothing otherwise so the
|
||||||
|
# module self-hides (like nightlight/updates). Click opens the VPN submenu.
|
||||||
|
vpnStatus = pkgs.writeShellScriptBin "nomarchy-vpn-status" ''
|
||||||
|
active=$(nmcli -t -f TYPE connection show --active 2>/dev/null | grep -Exm1 'vpn|wireguard')
|
||||||
|
ts=""
|
||||||
|
if command -v tailscale >/dev/null 2>&1; then
|
||||||
|
[ "$(tailscale status --json 2>/dev/null | jq -r '.BackendState // empty')" = Running ] && ts=1
|
||||||
|
fi
|
||||||
|
[ -n "$active" ] || [ -n "$ts" ] || exit 0
|
||||||
|
printf '{"text":"","tooltip":"VPN active (click to manage)","class":"on"}\n'
|
||||||
|
'';
|
||||||
|
|
||||||
# Per-theme override probe.
|
# Per-theme override probe.
|
||||||
assetDir = config.nomarchy.themesDir + "/${t.slug}";
|
assetDir = config.nomarchy.themesDir + "/${t.slug}";
|
||||||
styleOverride = assetDir + "/waybar.css";
|
styleOverride = assetDir + "/waybar.css";
|
||||||
@@ -77,9 +90,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" "custom/vpn" "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 +124,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 +144,39 @@ let
|
|||||||
on-click = "nomarchy-powerprofile-cycle";
|
on-click = "nomarchy-powerprofile-cycle";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# VPN shield — self-hides unless a NM VPN/WireGuard tunnel or Tailscale is
|
||||||
|
# up. Click opens the VPN submenu (nomarchy-vpn). 5s poll like powerprofile.
|
||||||
|
"custom/vpn" = {
|
||||||
|
exec = "nomarchy-vpn-status";
|
||||||
|
return-type = "json";
|
||||||
|
interval = 5;
|
||||||
|
on-click = "nomarchy-vpn";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Night-light (hyprsunset) indicator. Self-gates: the moon shows only while
|
||||||
|
# the schedule runs; otherwise the status helper prints nothing => hidden
|
||||||
|
# (enable / re-enable from the System menu). Click toggles instantly — writes
|
||||||
|
# the in-flake on/off (settings.nightlight.on, no rebuild) and flips the unit
|
||||||
|
# with systemctl, so the choice lands in the flake and survives reboot.
|
||||||
|
"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 +248,24 @@ 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, #custom-vpn, #language, #battery, #custom-notification {
|
||||||
color: @subtext;
|
color: @subtext;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* VPN active → accent green, reading as "connected / protected". */
|
||||||
|
#custom-vpn.on { color: @good; }
|
||||||
|
|
||||||
|
/* 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 +280,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
|
||||||
@@ -251,5 +310,5 @@ in
|
|||||||
# The power-profile helpers on PATH, so both the generated bar and the
|
# The power-profile helpers on PATH, so both the generated bar and the
|
||||||
# whole-swap themes' static waybar.jsonc can exec them by name.
|
# whole-swap themes' static waybar.jsonc can exec them by name.
|
||||||
home.packages = lib.optionals config.nomarchy.waybar.enable
|
home.packages = lib.optionals config.nomarchy.waybar.enable
|
||||||
[ powerProfileStatus powerProfileCycle ];
|
[ powerProfileStatus powerProfileCycle vpnStatus ];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,14 +22,28 @@ 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 ./timezone.nix ./oom.nix ];
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
# The safe half of distro branding: distroName flows into
|
# Distro branding. distroName flows into /etc/os-release PRETTY_NAME,
|
||||||
# /etc/os-release PRETTY_NAME, systemd-boot entry titles and the
|
# systemd-boot entry titles and the ISO boot-menu label; distroId is the
|
||||||
# ISO boot-menu label. distroId stays "nixos" on purpose — it feeds
|
# machine-readable ID (DEFAULT_HOSTNAME, lsb DISTRIB_ID, CPE name).
|
||||||
# DEFAULT_HOSTNAME and upstream isNixos checks (see roadmap).
|
# distroId = "nomarchy" makes os-release honest — ID=nomarchy with
|
||||||
|
# ID_LIKE=nixos, the standard derivative-distro lineage marker (cf.
|
||||||
|
# Ubuntu→debian) — and is safe: switch-to-configuration builds its
|
||||||
|
# "is this NixOS?" guard from the *configured* distroId (and /etc/NIXOS
|
||||||
|
# still exists as the fallback), so rebuilds keep working; the nixos-*
|
||||||
|
# CLI tools are package names, untouched. The one side effect is that
|
||||||
|
# isNixos goes false and blanks the upstream nixos.org URLs, so we
|
||||||
|
# restore them pointing at the project instead.
|
||||||
system.nixos.distroName = lib.mkDefault "Nomarchy";
|
system.nixos.distroName = lib.mkDefault "Nomarchy";
|
||||||
|
system.nixos.distroId = lib.mkDefault "nomarchy";
|
||||||
|
system.nixos.extraOSReleaseArgs = lib.mkDefault {
|
||||||
|
HOME_URL = "https://git.bemagri.xyz/bernardo/Nomarchy";
|
||||||
|
DOCUMENTATION_URL = "https://git.bemagri.xyz/bernardo/Nomarchy";
|
||||||
|
SUPPORT_URL = "https://git.bemagri.xyz/bernardo/Nomarchy";
|
||||||
|
BUG_REPORT_URL = "https://git.bemagri.xyz/bernardo/Nomarchy/issues";
|
||||||
|
};
|
||||||
|
|
||||||
# MOTD on TTY/SSH login (the desktop auto-logs into Hyprland, so this
|
# MOTD on TTY/SSH login (the desktop auto-logs into Hyprland, so this
|
||||||
# is mostly seen over SSH or on a bare console). Branded, and doubles
|
# is mostly seen over SSH or on a bare console). Branded, and doubles
|
||||||
@@ -69,6 +83,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.
|
||||||
@@ -122,6 +141,11 @@ in
|
|||||||
services.fwupd.enable = lib.mkDefault true;
|
services.fwupd.enable = lib.mkDefault true;
|
||||||
|
|
||||||
networking.networkmanager.enable = lib.mkDefault true;
|
networking.networkmanager.enable = lib.mkDefault true;
|
||||||
|
# OpenVPN support for the VPN menu's import/connect flow (nomarchy-vpn).
|
||||||
|
# WireGuard needs no plugin (NetworkManager imports wg .conf natively);
|
||||||
|
# this adds the openvpn type so `nmcli connection import type openvpn` works.
|
||||||
|
# mkDefault so a downstream can drop it to slim the closure.
|
||||||
|
networking.networkmanager.plugins = lib.mkDefault [ pkgs.networkmanager-openvpn ];
|
||||||
|
|
||||||
# No double-unlock on hibernate. Locking the session before sleep is
|
# No double-unlock on hibernate. Locking the session before sleep is
|
||||||
# right for suspend (resumes from RAM, no other gate), but an encrypted
|
# right for suspend (resumes from RAM, no other gate), but an encrypted
|
||||||
@@ -320,8 +344,73 @@ 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) — the primary `nomarchy-menu snapshot`
|
||||||
++ lib.optional cfg.snapper.enable pkgs.btrfs-assistant;
|
# target. The "2.2 segfault" is unprivileged-only (libbtrfsutil
|
||||||
|
# unprivileged subvolume iteration, btrfs-progs 6.17.1, fixed upstream
|
||||||
|
# after); the pkexec launcher runs it as root, where it works —
|
||||||
|
# VM-proven, guarded by checks.snapshot-gui.
|
||||||
|
++ lib.optional cfg.snapper.enable pkgs.btrfs-assistant
|
||||||
|
# Keyboard-driven snapper browser/restore — the menu's fallback when the
|
||||||
|
# GUI is absent, and handy over SSH. 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;
|
||||||
|
|||||||
242
modules/nixos/hardware.nix
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
# 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+)'';
|
||||||
|
|
||||||
|
camera = {
|
||||||
|
hideIrSensor = lib.mkEnableOption ''
|
||||||
|
hiding a dual-sensor webcam's IR (face-unlock) node from PipeWire so
|
||||||
|
apps only ever see the colour camera. Such modules (common on recent
|
||||||
|
ThinkPads) expose the IR sensor as a SECOND, identically-named
|
||||||
|
"Integrated Camera"; selecting it gives a dark, 8-bit-greyscale image —
|
||||||
|
the classic "my webcam is dark" symptom. The installer turns this on
|
||||||
|
when it detects a paired RGB+IR webcam. Only the PipeWire node is
|
||||||
|
disabled — the kernel /dev/video* device stays open, so face-unlock
|
||||||
|
(Howdy) still works. Acts on the V4L2 path only; the libcamera monitor
|
||||||
|
is left untouched, so an external camera you plug in is never affected.
|
||||||
|
See irMatch'';
|
||||||
|
|
||||||
|
irMatch = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "~.*(Integrated I|IR Camera|Infrared).*";
|
||||||
|
description = ''
|
||||||
|
WirePlumber regex (matched against a V4L2 node's api.v4l2.cap.card)
|
||||||
|
selecting the IR sensor to hide. The default catches the common
|
||||||
|
dual-sensor naming ("… Integrated I", "IR Camera", "Infrared"); set it
|
||||||
|
to your camera's IR card name if it differs — find it with
|
||||||
|
`v4l2-ctl --list-devices` or `wpctl inspect`. Only consulted when
|
||||||
|
camera.hideIrSensor is on.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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.'';
|
||||||
|
})
|
||||||
|
|
||||||
|
# ── Webcam: hide a dual-sensor module's IR node ────────────────────
|
||||||
|
# A built-in RGB+IR webcam exposes its IR (face-unlock) sensor as a second,
|
||||||
|
# identically-named camera; an app that picks it gets a dark greyscale
|
||||||
|
# image. Disable that node on the V4L2 PipeWire path so only the colour
|
||||||
|
# camera is offered. Matched by card name (irMatch). libcamera is left
|
||||||
|
# alone on purpose — an external camera may rely on it, and surgical
|
||||||
|
# internal-only libcamera scoping isn't possible (the distinguishing
|
||||||
|
# device props bind after the monitor rule runs). The kernel /dev/video*
|
||||||
|
# stays open, so Howdy face-unlock still reads the IR sensor directly.
|
||||||
|
(lib.mkIf (cfg.camera.hideIrSensor && config.services.pipewire.wireplumber.enable) {
|
||||||
|
services.pipewire.wireplumber.extraConfig."90-nomarchy-hide-ir-camera" = {
|
||||||
|
"monitor.v4l2.rules" = [
|
||||||
|
{
|
||||||
|
matches = [ { "api.v4l2.cap.card" = cfg.camera.irMatch; } ];
|
||||||
|
actions."update-props"."node.disabled" = 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.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
42
modules/nixos/oom.nix
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Memory-pressure protection: keep the desktop alive when RAM runs out.
|
||||||
|
#
|
||||||
|
# A workstation that compiles from source WILL exhaust memory eventually —
|
||||||
|
# a big `nix build`, a runaway eval, a browser tab. The kernel's own OOM
|
||||||
|
# killer acts only after the system has thrashed itself unresponsive
|
||||||
|
# (often minutes of frozen desktop) and then picks by badness score,
|
||||||
|
# which can land on the compositor.
|
||||||
|
#
|
||||||
|
# earlyoom over systemd-oomd — a deliberate choice: oomd kills whole
|
||||||
|
# cgroups, and a Hyprland session runs as ONE scope (nothing spawns
|
||||||
|
# per-app systemd scopes here, unlike GNOME), so under pressure oomd
|
||||||
|
# would take out the entire desktop to save it. earlyoom kills a single
|
||||||
|
# process (highest oom_score ≈ the hog) BEFORE the thrash point — "kill
|
||||||
|
# the build step, keep the session". nixpkgs default-enables oomd in an
|
||||||
|
# inert state (no slices monitored); it's disabled outright below so
|
||||||
|
# there is exactly one owner of the OOM story.
|
||||||
|
{ lib, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
services.earlyoom = {
|
||||||
|
enable = lib.mkDefault true;
|
||||||
|
|
||||||
|
# Desktop toast when something is killed (relayed via
|
||||||
|
# systembus-notify), so a vanished build/tab is explained rather
|
||||||
|
# than mysterious.
|
||||||
|
enableNotifications = lib.mkDefault true;
|
||||||
|
|
||||||
|
# Never pick the session plumbing: losing the compositor or the lock
|
||||||
|
# screen IS the outage this module exists to prevent (and killing a
|
||||||
|
# Wayland session-lock client trips its go-to-a-tty failsafe). No
|
||||||
|
# --prefer tuning: highest-memory selection already targets the hog.
|
||||||
|
# Matched unanchored — NixOS wrappers rename comm to ".foo-wrapped".
|
||||||
|
extraArgs = lib.mkDefault [
|
||||||
|
"--avoid"
|
||||||
|
"(Hyprland|hyprlock|greetd|waybar|pipewire|wireplumber|Xwayland|nix-daemon|systemd)"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# One owner (see header): oomd ships default-on but inert; make the
|
||||||
|
# earlyoom choice explicit and total.
|
||||||
|
systemd.oomd.enable = lib.mkDefault false;
|
||||||
|
}
|
||||||
@@ -46,6 +46,16 @@
|
|||||||
audio.enable = lib.mkEnableOption "the Pipewire audio stack" // { default = true; };
|
audio.enable = lib.mkEnableOption "the Pipewire audio stack" // { default = true; };
|
||||||
bluetooth.enable = lib.mkEnableOption "Bluetooth support with blueman" // { default = true; };
|
bluetooth.enable = lib.mkEnableOption "Bluetooth support with blueman" // { default = true; };
|
||||||
|
|
||||||
|
autoTimezone.enable = lib.mkEnableOption ''
|
||||||
|
automatic timezone detection (geoclue + automatic-timezoned): the
|
||||||
|
system timezone — and so the Waybar clock — follows your location, so
|
||||||
|
travelling to another zone updates the time on its own. Off by default
|
||||||
|
(it's a location service and needs the network); toggle it from the
|
||||||
|
System menu, which lands the choice in the in-flake state file. Enabling
|
||||||
|
it unsets the static time.timeZone for you (a runtime timezone needs
|
||||||
|
/etc/localtime writable), so the menu toggle drives a system rebuild''
|
||||||
|
// { default = false; };
|
||||||
|
|
||||||
snapper.enable = lib.mkEnableOption ''
|
snapper.enable = lib.mkEnableOption ''
|
||||||
hourly/daily BTRFS timeline snapshots of / via snapper, plus the
|
hourly/daily BTRFS timeline snapshots of / via snapper, plus the
|
||||||
`nixos-rebuild-snap` pre-rebuild-snapshot helper. No-op unless the
|
`nixos-rebuild-snap` pre-rebuild-snapshot helper. No-op unless the
|
||||||
@@ -99,8 +109,9 @@
|
|||||||
where the hardware exposes a charge threshold
|
where the hardware exposes a charge threshold
|
||||||
(/sys/class/power_supply/BAT*/charge_control_end_threshold).
|
(/sys/class/power_supply/BAT*/charge_control_end_threshold).
|
||||||
null leaves charging at the firmware default. Backend-independent
|
null leaves charging at the firmware default. Backend-independent
|
||||||
(a small systemd unit writes the sysfs knob at boot), so it
|
(a small systemd unit writes the sysfs knob, re-applied on AC
|
||||||
works under PPD too; needs nomarchy.system.power.laptop.
|
state changes), so it works under PPD too; needs
|
||||||
|
nomarchy.system.power.laptop.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ in
|
|||||||
|
|
||||||
# Battery charge limit via sysfs. PPD can't cap charge at all, and
|
# Battery charge limit via sysfs. PPD can't cap charge at all, and
|
||||||
# TLP's own knob only applies under TLP — so a tiny oneshot writes
|
# TLP's own knob only applies under TLP — so a tiny oneshot writes
|
||||||
# the threshold directly, independent of the backend. Boot-time only:
|
# the threshold directly, independent of the backend. Re-applied on
|
||||||
# some firmwares reset the threshold on unplug; revisit with a udev
|
# AC state changes by the udev rule below (some firmwares reset the
|
||||||
# hook if that bites. `-` paths that don't exist are skipped, so this
|
# threshold when the charger is unplugged). `[ -w ]` skips paths that
|
||||||
# is a clean no-op on hardware without the control.
|
# don't exist, so this is a clean no-op on hardware without the control.
|
||||||
systemd.services.nomarchy-battery-charge-limit = lib.mkIf chargeLimit {
|
systemd.services.nomarchy-battery-charge-limit = lib.mkIf chargeLimit {
|
||||||
description = "Cap battery charging at ${toString cfg.batteryChargeLimit}%";
|
description = "Cap battery charging at ${toString cfg.batteryChargeLimit}%";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
@@ -56,5 +56,16 @@ in
|
|||||||
exit 0
|
exit 0
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Re-apply the threshold whenever the mains adapter changes state: the
|
||||||
|
# boot oneshot above runs once, but some firmwares clear the limit when
|
||||||
|
# the charger is unplugged, so it must be re-asserted on the event.
|
||||||
|
# Matched by ATTR{type}=="Mains" (vendor-neutral — the kernel name
|
||||||
|
# AC/AC0/ADP1/ACAD varies); --no-block so the RUN+= returns at once
|
||||||
|
# (systemd kills long-running udev workers); restart (not try-restart)
|
||||||
|
# so it re-applies even if the boot run was inactive.
|
||||||
|
services.udev.extraRules = lib.mkIf chargeLimit ''
|
||||||
|
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="${config.systemd.package}/bin/systemctl --no-block restart nomarchy-battery-charge-limit.service"
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ in
|
|||||||
{
|
{
|
||||||
options.nomarchy.services = {
|
options.nomarchy.services = {
|
||||||
tailscale.enable = lib.mkEnableOption ''
|
tailscale.enable = lib.mkEnableOption ''
|
||||||
Tailscale, the mesh VPN — ships the daemon; authenticate once with
|
Tailscale, the mesh VPN — ships the daemon and makes the login user its
|
||||||
`sudo tailscale up`'';
|
operator, so `tailscale up/down/set` and the System → VPN menu work
|
||||||
|
without sudo. Authenticate once (the menu's Connect, or `tailscale up`)'';
|
||||||
|
|
||||||
syncthing.enable = lib.mkEnableOption ''
|
syncthing.enable = lib.mkEnableOption ''
|
||||||
Syncthing continuous file sync, running as the login user — add
|
Syncthing continuous file sync, running as the login user — add
|
||||||
@@ -75,7 +76,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
|
||||||
@@ -124,6 +126,12 @@ in
|
|||||||
config = lib.mkMerge [
|
config = lib.mkMerge [
|
||||||
(lib.mkIf cfg.tailscale.enable {
|
(lib.mkIf cfg.tailscale.enable {
|
||||||
services.tailscale.enable = true;
|
services.tailscale.enable = true;
|
||||||
|
# Let the login user drive tailscale (up/down/set — and so the VPN menu's
|
||||||
|
# Tailscale controls) without sudo. It's already in wheel, so the operator
|
||||||
|
# grant is no real new privilege, just skips the password prompt. The
|
||||||
|
# module's tailscaled-set unit applies this after the daemon starts.
|
||||||
|
# mkDefault so a downstream can drop or replace it (e.g. extraSetFlags = []).
|
||||||
|
services.tailscale.extraSetFlags = lib.mkDefault [ "--operator=${args.username}" ];
|
||||||
})
|
})
|
||||||
|
|
||||||
(lib.mkIf cfg.syncthing.enable {
|
(lib.mkIf cfg.syncthing.enable {
|
||||||
@@ -246,6 +254,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 {
|
||||||
|
|||||||
87
modules/nixos/timezone.nix
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Automatic timezone detection (opt-in) — the system timezone, and so the
|
||||||
|
# Waybar clock, follows your location, so travelling to another zone updates
|
||||||
|
# the time on its own. Geoclue feeds `automatic-timezoned`, which drives
|
||||||
|
# /etc/localtime at runtime.
|
||||||
|
#
|
||||||
|
# In-flake state, menu-driven (the keyboard/night-light philosophy): the on/off
|
||||||
|
# flag lives in the same theme-state.json under `settings.autoTimezone`
|
||||||
|
# (git-tracked, reproducible), written by the System-menu toggle
|
||||||
|
# (nomarchy-autotimezone). Because this is a SYSTEM service — not a user unit it
|
||||||
|
# can start/stop instantly like night-light — the toggle drives a system rebuild
|
||||||
|
# (plus a home switch for the Waybar-refresh watcher in timezone.nix home-side).
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.nomarchy.system;
|
||||||
|
|
||||||
|
# Read the same state file the rest of the system side uses (Plymouth too),
|
||||||
|
# wired by lib.mkFlake. The flag defaults off when the file is absent/sparse.
|
||||||
|
state =
|
||||||
|
if cfg.stateFile != null
|
||||||
|
then builtins.fromJSON (builtins.readFile cfg.stateFile)
|
||||||
|
else { };
|
||||||
|
stateEnabled = (state.settings or { }).autoTimezone or false;
|
||||||
|
|
||||||
|
sync = lib.getExe pkgs.nomarchy-theme-sync;
|
||||||
|
|
||||||
|
# Menu/CLI toggle. Runs as the normal user (it owns the flake checkout +
|
||||||
|
# writes the state); sudos only the system switch, like sys-update. Writes
|
||||||
|
# the in-flake flag, then rebuilds: the system rebuild bakes the service +
|
||||||
|
# the time.timeZone override, the home switch installs/removes the Waybar
|
||||||
|
# refresh watcher — both read the same flag we just wrote.
|
||||||
|
nomarchy-autotimezone = pkgs.writeShellScriptBin "nomarchy-autotimezone" ''
|
||||||
|
set -e
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
|
echo "nomarchy-autotimezone: run as your normal user (it sudos the rebuild itself)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
|
||||||
|
|
||||||
|
cur=$(${sync} get settings.autoTimezone 2>/dev/null) || cur=false
|
||||||
|
case "''${1:-toggle}" in
|
||||||
|
on) new=true ;;
|
||||||
|
off) new=false ;;
|
||||||
|
toggle) case "$cur" in true|True) new=false ;; *) new=true ;; esac ;;
|
||||||
|
status) echo "$cur"; exit 0 ;;
|
||||||
|
*) echo "usage: nomarchy-autotimezone [toggle|on|off|status]" >&2; exit 64 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
${sync} --quiet set settings.autoTimezone "$new" --no-switch
|
||||||
|
|
||||||
|
if [ "$new" = true ]; then
|
||||||
|
notify-send "Auto timezone" "Enabling — rebuilding the system…" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
notify-send "Auto timezone" "Disabling — rebuilding the system…" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo nixos-rebuild switch --flake "$flake#default"
|
||||||
|
home-manager switch --flake "$flake"
|
||||||
|
|
||||||
|
if [ "$new" = true ]; then
|
||||||
|
notify-send "Auto timezone on" "The clock now follows your location." 2>/dev/null || true
|
||||||
|
else
|
||||||
|
notify-send "Auto timezone off" "Back to the fixed timezone in system.nix." 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = lib.mkMerge [
|
||||||
|
{
|
||||||
|
# Shipped unconditionally so the menu can enable the feature even while
|
||||||
|
# it's off. Track the in-flake flag; mkDefault so a hand-set
|
||||||
|
# nomarchy.system.autoTimezone.enable in system.nix still wins.
|
||||||
|
environment.systemPackages = [ nomarchy-autotimezone ];
|
||||||
|
nomarchy.system.autoTimezone.enable = lib.mkDefault stateEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
(lib.mkIf cfg.autoTimezone.enable {
|
||||||
|
services.geoclue2.enable = true;
|
||||||
|
services.automatic-timezoned.enable = true;
|
||||||
|
# A runtime timezone needs /etc/localtime writable. automatic-timezoned
|
||||||
|
# sets time.timeZone = null itself, but the installer writes a static
|
||||||
|
# value at normal priority, which would collide (a hard eval error) —
|
||||||
|
# mkForce null overrides both and resolves cleanly.
|
||||||
|
time.timeZone = lib.mkForce null;
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -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,92 @@ 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
|
||||||
|
|
||||||
|
# Dual-sensor webcam (RGB + IR face-unlock). Such modules expose the IR
|
||||||
|
# sensor as a SECOND, identically-named "Integrated Camera"; an app that
|
||||||
|
# picks it shows a dark, 8-bit-greyscale image. When an IR-companion node
|
||||||
|
# sits alongside a normal camera node, enable hiding the IR one from
|
||||||
|
# PipeWire (the colour camera stays; the kernel /dev/video* is untouched, so
|
||||||
|
# Howdy can still read the IR sensor directly). Read from /sys — no
|
||||||
|
# v4l2-ctl needed in the installer env.
|
||||||
|
local cam_name have_ir_cam=0 have_color_cam=0 vf
|
||||||
|
local ir_re='Integrated I|IR Camera|Infrared'
|
||||||
|
for vf in /sys/class/video4linux/video*/name; do
|
||||||
|
[[ -e "$vf" ]] || continue
|
||||||
|
cam_name=$(cat "$vf" 2>/dev/null)
|
||||||
|
shopt -s nocasematch
|
||||||
|
if [[ "$cam_name" =~ $ir_re ]]; then
|
||||||
|
have_ir_cam=1
|
||||||
|
elif [[ "$cam_name" =~ (Camera|Webcam) ]]; then
|
||||||
|
have_color_cam=1
|
||||||
|
fi
|
||||||
|
shopt -u nocasematch
|
||||||
|
done
|
||||||
|
if [[ $have_ir_cam -eq 1 && $have_color_cam -eq 1 ]]; then
|
||||||
|
echo "NOMARCHY hardware.camera.hideIrSensor=true"
|
||||||
|
echo "DETAIL dual-sensor webcam (RGB+IR) → hide the IR node so apps get the colour camera"
|
||||||
|
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*)
|
||||||
|
|||||||
@@ -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,62 @@ 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; has_camera_ir=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 ;;
|
||||||
|
hardware.camera.hideIrSensor=true) has_camera_ir=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 [[ $has_camera_ir -eq 1 ]]; then
|
||||||
|
hw_lines+="
|
||||||
|
nomarchy.hardware.camera.hideIrSensor = true; # dual-sensor webcam: hide the IR node (color cam only)"
|
||||||
|
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 +528,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 +642,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.
|
||||||
|
|||||||
@@ -116,6 +116,39 @@ def write_state(state: dict) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def auto_commit_enabled(state: dict) -> bool:
|
||||||
|
return bool((state.get("settings") or {}).get("autoCommit"))
|
||||||
|
|
||||||
|
|
||||||
|
def auto_commit(message: str) -> None:
|
||||||
|
"""Opt-in (settings.autoCommit): commit theme-state.json — and nothing
|
||||||
|
else — after a mutation, so settings history is `git log`. The pathspec
|
||||||
|
keeps unrelated dirty files out of the commit; a missing git identity
|
||||||
|
falls back to a Nomarchy one so a fresh machine never errors. Callers
|
||||||
|
fire this when the flag is on before OR after the write, so the
|
||||||
|
disable-toggle itself lands in history instead of staying dirty.
|
||||||
|
`bg` is deliberately excluded (runtime wallpaper churn); the wallpaper
|
||||||
|
path rides along with the next apply/set commit."""
|
||||||
|
if not (FLAKE_DIR / ".git").exists() or shutil.which("git") is None:
|
||||||
|
return
|
||||||
|
git = ["git", "-C", str(FLAKE_DIR)]
|
||||||
|
# No-op when the file already matches HEAD (a `set` to the same value).
|
||||||
|
# On a repo with no commits yet this diff errors — then just commit.
|
||||||
|
if subprocess.run(git + ["diff", "--quiet", "HEAD", "--", "theme-state.json"],
|
||||||
|
capture_output=True).returncode == 0:
|
||||||
|
return
|
||||||
|
if not subprocess.run(git + ["config", "user.email"],
|
||||||
|
capture_output=True, text=True).stdout.strip():
|
||||||
|
git += ["-c", "user.name=Nomarchy", "-c", "user.email=nomarchy@localhost"]
|
||||||
|
result = subprocess.run(
|
||||||
|
git + ["commit", "--quiet", "-m", message, "--", "theme-state.json"],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
log(f"auto-committed: {message}")
|
||||||
|
else:
|
||||||
|
log(f"auto-commit skipped: {(result.stderr or result.stdout).strip()}")
|
||||||
|
|
||||||
|
|
||||||
def deep_merge(base: dict, override: dict) -> dict:
|
def deep_merge(base: dict, override: dict) -> dict:
|
||||||
out = dict(base)
|
out = dict(base)
|
||||||
for k, v in override.items():
|
for k, v in override.items():
|
||||||
@@ -165,12 +198,17 @@ def run_switch() -> None:
|
|||||||
# Persistent (timeout 0): stays up for the whole rebuild — replaced
|
# Persistent (timeout 0): stays up for the whole rebuild — replaced
|
||||||
# in place by the success/failure notification below — so a multi-
|
# in place by the success/failure notification below — so a multi-
|
||||||
# minute switch never looks like it silently failed.
|
# minute switch never looks like it silently failed.
|
||||||
notify("Applying theme — rebuilding the desktop…", persistent=True)
|
notify("Applying changes — rebuilding the desktop…", persistent=True)
|
||||||
result = subprocess.run(argv) # stream output to the caller's terminal
|
result = subprocess.run(argv) # stream output to the caller's terminal
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
notify("Theme rebuild FAILED — see terminal / journal", urgency="critical")
|
notify("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("Changes 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) ────────────────────────────────────────────
|
||||||
@@ -259,8 +297,11 @@ def cmd_apply(args) -> None:
|
|||||||
preset = json.loads(preset_path.read_text())
|
preset = json.loads(preset_path.read_text())
|
||||||
# Merge over current state: presets define palette/name/wallpaper,
|
# Merge over current state: presets define palette/name/wallpaper,
|
||||||
# user tweaks (gaps, fonts) outside the preset survive.
|
# user tweaks (gaps, fonts) outside the preset survive.
|
||||||
state = deep_merge(load_state(), preset)
|
old = load_state()
|
||||||
|
state = deep_merge(old, preset)
|
||||||
write_state(state)
|
write_state(state)
|
||||||
|
if auto_commit_enabled(old) or auto_commit_enabled(state):
|
||||||
|
auto_commit(f"nomarchy: apply theme {state.get('name', args.theme)}")
|
||||||
log(f"theme: {state.get('name', args.theme)}")
|
log(f"theme: {state.get('name', args.theme)}")
|
||||||
check_fonts(state)
|
check_fonts(state)
|
||||||
if not args.no_switch:
|
if not args.no_switch:
|
||||||
@@ -275,6 +316,7 @@ def cmd_set(args) -> None:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
value = args.value # bare string ("#7aa2f7", font names, paths)
|
value = args.value # bare string ("#7aa2f7", font names, paths)
|
||||||
|
|
||||||
|
was_on = auto_commit_enabled(state)
|
||||||
node = state
|
node = state
|
||||||
keys = args.path.split(".")
|
keys = args.path.split(".")
|
||||||
for key in keys[:-1]:
|
for key in keys[:-1]:
|
||||||
@@ -284,11 +326,17 @@ def cmd_set(args) -> None:
|
|||||||
node[keys[-1]] = value
|
node[keys[-1]] = value
|
||||||
|
|
||||||
write_state(state)
|
write_state(state)
|
||||||
|
if was_on or auto_commit_enabled(state):
|
||||||
|
auto_commit(f"nomarchy: set {args.path} = {json.dumps(value)}")
|
||||||
log(f"set {args.path} = {value!r}")
|
log(f"set {args.path} = {value!r}")
|
||||||
if args.path.startswith("fonts."):
|
if args.path.startswith("fonts."):
|
||||||
check_fonts(state)
|
check_fonts(state)
|
||||||
if not args.no_switch:
|
if not args.no_switch:
|
||||||
run_switch()
|
run_switch()
|
||||||
|
# Only wallpaper/theme keys change what swww shows; skip the re-apply
|
||||||
|
# (and its transition) for unrelated keys like ui.* or settings.* so a
|
||||||
|
# gaps tweak or a feature toggle doesn't flash the background.
|
||||||
|
if args.path.split(".")[0] in ("wallpaper", "slug"):
|
||||||
apply_wallpaper(state)
|
apply_wallpaper(state)
|
||||||
|
|
||||||
|
|
||||||
@@ -301,7 +349,10 @@ def cmd_get(args) -> None:
|
|||||||
node = node[key]
|
node = node[key]
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
die(f"no such key: {args.path}")
|
die(f"no such key: {args.path}")
|
||||||
print(json.dumps(node, indent=2) if isinstance(node, (dict, list)) else node)
|
# Booleans print JSON-style (true/false, not Python's True/False) so
|
||||||
|
# shell consumers can compare against the same literal they `set`.
|
||||||
|
print(json.dumps(node, indent=2)
|
||||||
|
if isinstance(node, (dict, list, bool)) else node)
|
||||||
else:
|
else:
|
||||||
print(json.dumps(state, indent=2))
|
print(json.dumps(state, indent=2))
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,19 @@
|
|||||||
|
|
||||||
# ── Opt-in features — uncomment and tweak to enable ─────────────────
|
# ── Opt-in features — uncomment and tweak to enable ─────────────────
|
||||||
# nomarchy.nightlight = { # scheduled blue-light filter (hyprsunset)
|
# nomarchy.nightlight = { # scheduled blue-light filter (hyprsunset)
|
||||||
# enable = true;
|
# enable = true; # declarative opt-in (or just enable it from
|
||||||
|
# # the menu: System › Night light)
|
||||||
# temperature = 4000; # night warmth in K — lower is warmer
|
# temperature = 4000; # night warmth in K — lower is warmer
|
||||||
# sunset = "20:00";
|
# sunset = "20:00";
|
||||||
# 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"; }
|
||||||
@@ -31,8 +38,9 @@
|
|||||||
# "keychron-keychron-k2" = { layout = "de"; };
|
# "keychron-keychron-k2" = { layout = "de"; };
|
||||||
# };
|
# };
|
||||||
#
|
#
|
||||||
# nomarchy.keyboard.layouts = [ "de" "fr" ]; # rofi-prompt for a layout when a
|
# nomarchy.keyboard.layouts = [ "de" "fr" ]; # rofi-prompt for a layout when a new
|
||||||
# # new keyboard connects + remember it
|
# # keyboard connects + remember it in the
|
||||||
|
# # flake state (graduates to .devices above)
|
||||||
|
|
||||||
# ── Application suite ───────────────────────────────────────────────
|
# ── Application suite ───────────────────────────────────────────────
|
||||||
# A starter complete-workstation set, installed for your user — yours to
|
# A starter complete-workstation set, installed for your user — yours to
|
||||||
@@ -105,6 +113,11 @@
|
|||||||
# kdePackages.kdenlive # video editor (pulls in KDE libs)
|
# kdePackages.kdenlive # video editor (pulls in KDE libs)
|
||||||
# handbrake # transcoder
|
# handbrake # transcoder
|
||||||
|
|
||||||
|
# Webcam tuning (rarely needed — the distro's IR-hide covers the
|
||||||
|
# common dual-sensor problem; these are for genuine tuning cases)
|
||||||
|
# cameractrls # GUI for exposure/focus/zoom, saves per-camera presets
|
||||||
|
# v4l-utils # v4l2-ctl CLI: list formats, poke controls directly
|
||||||
|
|
||||||
# Media
|
# Media
|
||||||
# vlc
|
# vlc
|
||||||
# spotify # (unfree)
|
# spotify # (unfree)
|
||||||
|
|||||||
@@ -31,7 +31,27 @@
|
|||||||
# thermal.enable = true; # thermald (Intel CPUs)
|
# thermal.enable = true; # thermald (Intel CPUs)
|
||||||
# };
|
# };
|
||||||
#
|
#
|
||||||
# nomarchy.services.tailscale.enable = true; # mesh VPN — then `sudo tailscale up`
|
# nomarchy.system.autoTimezone.enable = true; # clock follows your location
|
||||||
|
# # (geoclue); travelling updates the
|
||||||
|
# # time on its own. Unsets time.timeZone
|
||||||
|
# # above. Easier toggled from System menu.
|
||||||
|
#
|
||||||
|
# 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)
|
||||||
|
# camera.hideIrSensor = true; # dual-sensor webcam: hide the IR node so apps get the color cam
|
||||||
|
# # (installer-set when a paired RGB+IR webcam is detected)
|
||||||
|
# };
|
||||||
|
#
|
||||||
|
# nomarchy.services.tailscale.enable = true; # mesh VPN — connect from System › VPN
|
||||||
|
# # (login user is operator, no sudo)
|
||||||
# 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)
|
||||||
# nomarchy.services.flatpak.enable = true; # Flatpak + the Flathub remote
|
# nomarchy.services.flatpak.enable = true; # Flatpak + the Flathub remote
|
||||||
|
|||||||
@@ -55,5 +55,6 @@
|
|||||||
"terminalOpacity": 0.96,
|
"terminalOpacity": 0.96,
|
||||||
"blur": true,
|
"blur": true,
|
||||||
"shadow": true
|
"shadow": true
|
||||||
}
|
},
|
||||||
|
"settings": {}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
themes/catppuccin-latte/preview.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
themes/catppuccin/preview.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
themes/ethereal/preview.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
themes/everforest/preview.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
themes/flexoki-light/preview.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
themes/gruvbox/preview.png
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
themes/hackerman/preview.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
themes/kanagawa/preview.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
107
themes/kanagawa/rofi.rasi
Normal 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
|
After Width: | Height: | Size: 30 KiB |
110
themes/lumon/rofi.rasi
Normal 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; }
|
||||||
BIN
themes/matte-black/preview.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
themes/miasma/preview.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
themes/nord/preview.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
110
themes/nord/rofi.rasi
Normal 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; }
|
||||||
BIN
themes/osaka-jade/preview.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
themes/retro-82/preview.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
109
themes/retro-82/rofi.rasi
Normal 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; }
|
||||||
BIN
themes/ristretto/preview.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
themes/rose-pine/preview.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
themes/summer-day/preview.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
@@ -47,8 +47,10 @@ window#waybar {
|
|||||||
#clock-date,
|
#clock-date,
|
||||||
#workspaces,
|
#workspaces,
|
||||||
#pulseaudio,
|
#pulseaudio,
|
||||||
#network,
|
|
||||||
#custom-powerprofile,
|
#custom-powerprofile,
|
||||||
|
#custom-nightlight,
|
||||||
|
#language,
|
||||||
|
#custom-updates,
|
||||||
#battery,
|
#battery,
|
||||||
#tray,
|
#tray,
|
||||||
#custom-notification,
|
#custom-notification,
|
||||||
@@ -71,6 +73,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;
|
||||||
|
|||||||
@@ -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", "hyprland/language", "custom/updates", "battery", "tray", "custom/vpn", "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,33 @@
|
|||||||
"on-click": "nomarchy-powerprofile-cycle"
|
"on-click": "nomarchy-powerprofile-cycle"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"custom/vpn": {
|
||||||
|
"return-type": "json",
|
||||||
|
"interval": 5,
|
||||||
|
"exec": "nomarchy-vpn-status",
|
||||||
|
"on-click": "nomarchy-vpn"
|
||||||
|
},
|
||||||
|
|
||||||
|
"hyprland/language": {
|
||||||
|
"format": " {short}",
|
||||||
|
"tooltip": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"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",
|
||||||
|
|||||||
BIN
themes/summer-night/preview.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
@@ -53,8 +53,10 @@ window#waybar {
|
|||||||
#clock.date,
|
#clock.date,
|
||||||
#workspaces,
|
#workspaces,
|
||||||
#pulseaudio,
|
#pulseaudio,
|
||||||
#network,
|
|
||||||
#custom-powerprofile,
|
#custom-powerprofile,
|
||||||
|
#custom-nightlight,
|
||||||
|
#language,
|
||||||
|
#custom-updates,
|
||||||
#idle_inhibitor,
|
#idle_inhibitor,
|
||||||
#battery,
|
#battery,
|
||||||
#custom-notification,
|
#custom-notification,
|
||||||
@@ -75,6 +77,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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", "hyprland/language", "custom/updates", "battery", "tray", "custom/vpn", "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,33 @@
|
|||||||
"on-click": "nomarchy-powerprofile-cycle"
|
"on-click": "nomarchy-powerprofile-cycle"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"custom/vpn": {
|
||||||
|
"return-type": "json",
|
||||||
|
"interval": 5,
|
||||||
|
"exec": "nomarchy-vpn-status",
|
||||||
|
"on-click": "nomarchy-vpn"
|
||||||
|
},
|
||||||
|
|
||||||
|
"hyprland/language": {
|
||||||
|
"format": " {short}",
|
||||||
|
"tooltip": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"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",
|
||||||
|
|||||||
BIN
themes/tokyo-night/preview.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
themes/vantablack/preview.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
themes/white/preview.png
Normal file
|
After Width: | Height: | Size: 49 KiB |