Compare commits
213 Commits
v1
...
5b93b97191
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b93b97191 | ||
|
|
ad6b76e1eb | ||
|
|
3a874dccc8 | ||
|
|
82776d7da4 | ||
|
|
34362d6a92 | ||
|
|
a640de4fd4 | ||
|
|
bd6d94f973 | ||
|
|
3d324982b9 | ||
|
|
61d9ee1577 | ||
|
|
cb659ebb4c | ||
|
|
6b488d69ef | ||
|
|
1d6b25b96a | ||
|
|
14558ad296 | ||
|
|
5f3124d160 | ||
|
|
a102dff508 | ||
|
|
e81529c1bb | ||
|
|
2ef0a8b710 | ||
|
|
35699f170f | ||
|
|
a504f35b41 | ||
|
|
c962d07841 | ||
|
|
09388196a9 | ||
|
|
f09042adbe | ||
|
|
11d6a3df0f | ||
|
|
9aa8f250d6 | ||
|
|
be4efd38ea | ||
|
|
d8a796b6ee | ||
|
|
f606c78fcf | ||
|
|
c40c74e640 | ||
|
|
f94772de2c | ||
|
|
40c38dc4f5 | ||
|
|
def6e9dcbe | ||
|
|
4a99b64532 | ||
|
|
75d76fabd7 | ||
|
|
f6975a9797 | ||
|
|
7a5284b15a | ||
|
|
e01303851d | ||
|
|
97bf26a23f | ||
|
|
1ee17f6799 | ||
|
|
eba7924b0f | ||
|
|
c89cace149 | ||
|
|
41cd350a52 | ||
| 7d52d4b5e4 | |||
|
|
ed7fd93e16 | ||
|
|
5c875542d0 | ||
|
|
351b7adb8e | ||
|
|
70501b566a | ||
|
|
208b8d4444 | ||
|
|
02d7baeb7c | ||
|
|
caaac88da9 | ||
|
|
896b41faa3 | ||
|
|
d09c978b9b | ||
|
|
5273493c20 | ||
|
|
9c21aa64b1 | ||
|
|
6b7f2b4ce8 | ||
|
|
2ef56eae88 | ||
|
|
d6b5b344fa | ||
|
|
60c7878a6a | ||
|
|
8a5714f330 | ||
|
|
05c7c7b54f | ||
|
|
3322db7caf | ||
|
|
fbd4e0503e | ||
|
|
f97acda158 | ||
|
|
ff76781a97 | ||
|
|
856445c505 | ||
|
|
c6b759e19e | ||
|
|
eb8f9fad88 | ||
|
|
d41e5c18d2 | ||
|
|
452bb9d75f | ||
|
|
2bad7c524f | ||
|
|
1d8c1a4314 | ||
|
|
3dcbb2b0b6 | ||
|
|
b1a9d2ea66 | ||
|
|
db48bb85d5 | ||
|
|
6bd03747c9 | ||
|
|
850dc310df | ||
|
|
565f66372a | ||
|
|
be8d8a7d9b | ||
|
|
2954283e23 | ||
|
|
ce83a8e655 | ||
|
|
1143e67f95 | ||
|
|
714fbd1daf | ||
|
|
b18980f642 | ||
|
|
b1cf10ff06 | ||
| 60c6f14c08 | |||
| a8391c381d | |||
| 28a28e05d3 | |||
| 239c3c4551 | |||
| 92b3c1a1e3 | |||
| 8961fd6936 | |||
| 1928cd94f6 | |||
| 21c0c58ea2 | |||
| 926be22fd4 | |||
| cd5d3b51e2 | |||
| 6c61b51d55 | |||
| 28cbaf6f5e | |||
| d078ba2a82 | |||
| ce89fddab6 | |||
| cfecb612a6 | |||
| be5888d354 | |||
| f848e7390f | |||
| 907d3123ea | |||
| ddf9b186e4 | |||
|
|
175b877f95 | ||
|
|
7b599c5786 | ||
|
|
f924d92f1b | ||
|
|
f2c815ddbd | ||
|
|
16275947ec | ||
| 7f10a12ec8 | |||
| 825b7e25a4 | |||
| 801ffa4e24 | |||
| ccd896c8dc | |||
| 51e319d357 | |||
| 7569b678db | |||
| 28e21af206 | |||
| eecc214ca9 | |||
| 93521c8617 | |||
| 3f15f6451f | |||
|
|
8fded63b10 | ||
|
|
edd0bd38ce | ||
|
|
096440a7d7 | ||
|
|
7d5f091c29 | ||
|
|
90a5104f94 | ||
|
|
39cfe0fb12 | ||
|
|
01ee847490 | ||
|
|
808a3febdd | ||
|
|
4ad564bd80 | ||
|
|
3e49481d46 | ||
|
|
8d54eecd67 | ||
|
|
4e3acbe89c | ||
|
|
8f2e047f4a | ||
|
|
8874a22a37 | ||
|
|
310614bdd2 | ||
|
|
fb75c3dedf | ||
|
|
44d5516191 | ||
|
|
47c8b6f997 | ||
|
|
abc953ea84 | ||
|
|
d1d6a09d9d | ||
|
|
a360bc87ca | ||
|
|
1921839e0e | ||
|
|
9df18261f9 | ||
|
|
e02b4d8200 | ||
|
|
5eadf0cff6 | ||
|
|
26e393b65d | ||
|
|
fb78c814cc | ||
|
|
821032e81c | ||
|
|
b7b51e9354 | ||
|
|
0fe46221ea | ||
|
|
bfb80cb60d | ||
|
|
352c681f48 | ||
|
|
d2ac131b75 | ||
|
|
2a23e82169 | ||
|
|
1b8eccbdca | ||
|
|
3f5e414341 | ||
|
|
774bdad6e4 | ||
|
|
e05e3647e6 | ||
|
|
472d7502b4 | ||
|
|
b0b8a9a09b | ||
|
|
56f1cc3fa9 | ||
|
|
9cd6f5e30c | ||
|
|
a83edb0d36 | ||
|
|
fd6e5f60e9 | ||
|
|
a643391d3d | ||
|
|
63136a8cb1 | ||
|
|
0d80ab272f | ||
|
|
397dd5991a | ||
|
|
274ffc25e1 | ||
|
|
0e6c678835 | ||
|
|
18b854563b | ||
|
|
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 |
28
.claude/agents/nomarchy-runner.md
Normal file
28
.claude/agents/nomarchy-runner.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: nomarchy-runner
|
||||
description: Mechanical execution of the Nomarchy headless test harness. Use PROACTIVELY when a build or VM run just needs to be executed and its artifacts collected — nix build, tools/test-live-iso.sh, tools/test-install.sh, screenshot capture via tools/vm/vncshot.py and tools/vm/qmp.py, and the scripted checks (check-theme-contrast.py, audit-theme-design.py, check-option-docs.py). Never use for interpreting ambiguous failures, judging visual quality, or deciding what to test.
|
||||
tools: Read, Bash, Glob, Grep
|
||||
model: haiku
|
||||
---
|
||||
|
||||
You are a deterministic test executor for the Nomarchy repository. You run
|
||||
exactly the commands you are asked to run, headlessly, and hand back the
|
||||
evidence. You do not decide what to test, do not interpret ambiguous
|
||||
results, and do not judge whether something "looks fine".
|
||||
|
||||
Rules:
|
||||
- Run everything headless and unattended. Never launch a graphical VM
|
||||
window; never wait on human input. Wrap long-running commands in
|
||||
timeouts as instructed by the caller, and treat a timeout as a recorded
|
||||
failure, not something to silently retry.
|
||||
- Collect artifacts to the working directory the caller specifies: full
|
||||
command lines, exit codes, the tail of stdout/stderr (plus the complete
|
||||
logs as files), serial console output, and screenshot file paths.
|
||||
- Report format: for each command — command, exit code, wall time,
|
||||
artifact paths, and verbatim error lines if the exit code was nonzero.
|
||||
Nothing else. No summaries of what the results "mean".
|
||||
- Never mark anything as passed or verified. You return evidence; the
|
||||
caller makes the verification claim.
|
||||
- If a command fails in a way that prevents collecting artifacts, report
|
||||
exactly what happened and stop — do not improvise recovery steps beyond
|
||||
what the caller authorized.
|
||||
20
.claude/agents/nomarchy-scout.md
Normal file
20
.claude/agents/nomarchy-scout.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: nomarchy-scout
|
||||
description: Read-only reconnaissance inside the Nomarchy repo. Use PROACTIVELY for any task that is pure information gathering — locating where an option/module/theme is defined, mapping which files touch a subsystem, scanning build logs or serial-console output for errors, checking docs against code for drift, summarizing a directory. Never use for anything requiring judgment about design, correctness, or visual quality.
|
||||
tools: Read, Grep, Glob
|
||||
model: haiku
|
||||
---
|
||||
|
||||
You are a fast, cheap scout for the Nomarchy NixOS distribution repository.
|
||||
Your only job is to find things and report facts. You never edit, never
|
||||
judge design quality, and never draw conclusions beyond what the files
|
||||
literally say.
|
||||
|
||||
Rules:
|
||||
- Report file paths and line numbers, quote the minimum relevant snippet.
|
||||
- If asked to scan logs, extract the error/warning lines verbatim with
|
||||
enough surrounding context to locate them, and note timestamps.
|
||||
- If you cannot find something, say so plainly — never guess or infer that
|
||||
something "probably" exists.
|
||||
- Keep output terse and structured: the caller pays for every token you
|
||||
produce. Facts first, no prose padding, no recommendations.
|
||||
88
.claude/settings.json
Normal file
88
.claude/settings.json
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Read",
|
||||
"Edit",
|
||||
"Write",
|
||||
"Glob",
|
||||
"Grep",
|
||||
|
||||
"Bash(ls *)",
|
||||
"Bash(cat *)",
|
||||
"Bash(head *)",
|
||||
"Bash(tail *)",
|
||||
"Bash(grep *)",
|
||||
"Bash(rg *)",
|
||||
"Bash(find *)",
|
||||
"Bash(tree *)",
|
||||
"Bash(wc *)",
|
||||
"Bash(file *)",
|
||||
"Bash(stat *)",
|
||||
"Bash(du *)",
|
||||
"Bash(df *)",
|
||||
"Bash(which *)",
|
||||
"Bash(diff *)",
|
||||
"Bash(jq *)",
|
||||
"Bash(sha256sum *)",
|
||||
|
||||
"Bash(mkdir *)",
|
||||
"Bash(cp *)",
|
||||
"Bash(mv *)",
|
||||
"Bash(touch *)",
|
||||
"Bash(ln -s *)",
|
||||
"Bash(tar *)",
|
||||
|
||||
"Bash(nix *)",
|
||||
"Bash(nix-store *)",
|
||||
"Bash(nix-instantiate *)",
|
||||
|
||||
"Bash(./tools/test-live-iso.sh *)",
|
||||
"Bash(./tools/test-install.sh *)",
|
||||
"Bash(./tools/audit-theme-design.py *)",
|
||||
"Bash(./tools/check-option-docs.py *)",
|
||||
"Bash(./tools/check-theme-contrast.py *)",
|
||||
"Bash(./tools/import-palettes.py *)",
|
||||
"Bash(./tools/vm/qmp.py *)",
|
||||
"Bash(./tools/vm/vncshot.py *)",
|
||||
"Bash(./tools/vm/gap-analysis.py *)",
|
||||
"Bash(python3 tools/*)",
|
||||
"Bash(bash tools/*)",
|
||||
|
||||
"Bash(qemu-system-x86_64 *)",
|
||||
"Bash(qemu-img *)",
|
||||
|
||||
"Bash(git status *)",
|
||||
"Bash(git diff *)",
|
||||
"Bash(git log *)",
|
||||
"Bash(git show *)",
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit *)",
|
||||
"Bash(git branch *)",
|
||||
"Bash(git switch *)",
|
||||
"Bash(git checkout *)",
|
||||
"Bash(git restore *)",
|
||||
"Bash(git stash *)",
|
||||
"Bash(git worktree *)",
|
||||
|
||||
"Bash(systemctl --user status *)",
|
||||
"Bash(journalctl --user *)",
|
||||
"Bash(pgrep *)",
|
||||
"Bash(pkill -f qemu*)"
|
||||
],
|
||||
"ask": [
|
||||
"Bash(git push *)",
|
||||
"Bash(rm *)",
|
||||
"Bash(git reset --hard *)",
|
||||
"Bash(git clean *)",
|
||||
"Bash(nix-collect-garbage *)",
|
||||
"Bash(curl *)",
|
||||
"Bash(wget *)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(sudo *)",
|
||||
"Bash(git push --force *)",
|
||||
"Bash(git push -f *)"
|
||||
],
|
||||
"defaultMode": "default"
|
||||
}
|
||||
}
|
||||
272
.claude/skills/nomarchy/SKILL.md
Normal file
272
.claude/skills/nomarchy/SKILL.md
Normal file
@@ -0,0 +1,272 @@
|
||||
---
|
||||
name: nomarchy
|
||||
description: Development and maintenance workflow for the Nomarchy NixOS-based distribution (this repository). Use this skill for ANY change to this repo — new features, bug fixes, theming or visual work (Hyprland, Waybar, rofi, wallpapers, palettes), NixOS/Home-Manager module changes, flake lock bumps, theme imports, docs updates, backlog grooming, or loop iterations. Trigger even for "small" or "obvious" changes; the whole point of this skill is that no change ships without climbing the verification ladder, and small changes are where verification gets skipped.
|
||||
---
|
||||
|
||||
# Nomarchy Development Skill
|
||||
|
||||
Nomarchy's promise to its user: a rock-stable, fully functional, beautiful
|
||||
workstation that is reproducible, easy to recover, and never requires the
|
||||
user to become a Nix expert. Every rule below exists to protect that promise.
|
||||
A change that works but degrades stability, aesthetics, or user-simplicity is
|
||||
a regression, not a feature.
|
||||
|
||||
## 0. Read the authoritative docs first
|
||||
|
||||
This skill is an enforcer, not the workflow itself. The workflow lives in the
|
||||
repo and is the single source of truth:
|
||||
|
||||
- `CLAUDE.md` — entry point, project conventions
|
||||
- `agent/LOOP.md` — the iteration loop and the V0–V3 verification ladder
|
||||
- `agent/` (remaining files) — philosophy, conventions, memory, backlog
|
||||
- `docs/TESTING.md` — VM instructions, the regression checklist, known
|
||||
environment gotchas
|
||||
|
||||
At the start of any work session in this repo, read `CLAUDE.md` and
|
||||
`agent/LOOP.md` before touching code. Read `docs/TESTING.md` before running
|
||||
any VM test. If this skill and those docs ever disagree, the repo docs win —
|
||||
then fix the discrepancy in the same or a follow-up commit so they can't
|
||||
disagree twice.
|
||||
|
||||
## 1. Preflight (once per session)
|
||||
|
||||
Before starting work, establish what verification tier this environment can
|
||||
reach, so you never promise verification you can't deliver:
|
||||
|
||||
1. Linux x86_64 host? `/dev/kvm` present and readable?
|
||||
2. Enough free disk for an image/ISO build (multi-GB)?
|
||||
3. Network access for a cold Nix store?
|
||||
|
||||
If the environment cannot reach V2 (no KVM, no disk, etc.): say so
|
||||
immediately, do the V0/V1 work honestly, mark the change **"V2 pending"**
|
||||
exactly as you would mark a hardware-blocked change "V3 pending" (see §4),
|
||||
and stop short of claiming the change is done. Never simulate, guess, or
|
||||
describe what a VM test "would" show.
|
||||
|
||||
## 2. The verification ladder (enforcement rules)
|
||||
|
||||
Climb the V0–V3 ladder as defined in `agent/LOOP.md`. This skill adds three
|
||||
non-negotiable enforcement rules on top:
|
||||
|
||||
1. **V2 is mandatory for anything user-visible.** If a user of the installed
|
||||
system could perceive the change — behavior, layout, colors, keybinds,
|
||||
timing, error messages — it must be exercised in the local VM before
|
||||
commit. Docs-only, comment-only, or agent-notes changes may stop at the
|
||||
tier LOOP.md assigns them; user-visible changes may not.
|
||||
2. **Every "done" report names the tier reached and shows the evidence.**
|
||||
Evidence means: the command run and its relevant output, the checklist
|
||||
items exercised, and for visual work the screenshots viewed (§3). "It
|
||||
builds" is a V1 claim, not a V2 claim. Never let a report imply a higher
|
||||
tier than was actually reached.
|
||||
3. **A failed or flaky test is a result, not an obstacle.** Distinguish real
|
||||
failures from environment flakes using the known-gotchas section of
|
||||
`docs/TESTING.md` (e.g. no-KVM slowness, missing guest GL). If you cannot
|
||||
confidently classify a failure, report it as unresolved — do not retry
|
||||
until green and report only the green run.
|
||||
4. **VM runs are headless and unattended.** Use the repo's headless
|
||||
harness — `tools/test-live-iso.sh` and `tools/test-install.sh` for
|
||||
boot/install runs, `tools/vm/qmp.py` for programmatic VM control and
|
||||
`tools/vm/vncshot.py` for screen capture — never a graphical VM window
|
||||
or any flow that needs a human at the console. The human is not part of
|
||||
the test loop: do not pause mid-run to ask them to look at the VM, click
|
||||
something, or confirm what is on screen. A run must complete on its own
|
||||
and leave auditable artifacts behind (logs, serial console output, exit
|
||||
codes, screenshots), with every wait bounded by a timeout so a hang
|
||||
becomes a recorded failure instead of a stalled session. Prefer scripted
|
||||
assertions (process up, file exists, service/D-Bus state, the checks in
|
||||
`tools/`) over eyeballing; where judgment is genuinely needed — visual
|
||||
quality — *you* view the captured screenshots (§3), not the human. The
|
||||
human reviews evidence in the final report, never the live run.
|
||||
|
||||
### Regression scope after a change
|
||||
|
||||
Re-running the full checklist for every change wastes VM time; running
|
||||
nothing invites regressions. Default rule:
|
||||
|
||||
- Always: the session-sanity items (boot to session, bar renders).
|
||||
- Plus: every checklist item touching the layer you changed.
|
||||
- Plus: the theming end-to-end item whenever theming plumbing changed,
|
||||
even indirectly (palette generation, symlinks, reload hooks).
|
||||
- Lock bumps and toolchain changes: run the full checklist — their blast
|
||||
radius is unknowable by construction.
|
||||
|
||||
## 3. Visual verification protocol
|
||||
|
||||
Visual quality is a core feature of Nomarchy, so "it probably looks fine" is
|
||||
never verification. A visual/UI change is not V2-verified until all of the
|
||||
following are true:
|
||||
|
||||
1. **Before/after screenshots** of the changed surface were captured
|
||||
headlessly — `tools/theme-shot.nix` for reproducible theme renders,
|
||||
`tools/vm/vncshot.py` (driven via `tools/vm/qmp.py`) for captures from a
|
||||
running VM. No VM window, no human interaction. Capture the "before"
|
||||
from the base branch or prior generation, not from memory.
|
||||
2. **Scripted checks first**: run `tools/check-theme-contrast.py` and
|
||||
`tools/audit-theme-design.py` against the affected theme(s) before any
|
||||
eyeballing — machine-checkable legibility/design violations should never
|
||||
survive to the human-judgment stage. Use `tools/vm/gap-analysis.py`
|
||||
where it applies.
|
||||
3. **Two themes**: repeat the "after" capture under at least two themes, one
|
||||
with a generated palette and one whole-swap theme (e.g. summer-night).
|
||||
These exercise different code paths in the bar/launcher theming; a change
|
||||
that looks right under one can silently break the other.
|
||||
4. **You actually viewed the images** — open the screenshot files and look
|
||||
at them. State concretely what you inspected: alignment, spacing,
|
||||
contrast/legibility against the palette, icon rendering, no clipped or
|
||||
overlapping elements, and that the change looks intentional next to the
|
||||
"before".
|
||||
5. Keep the screenshots in the run's working area and reference their paths
|
||||
in the report, so the human can audit the same evidence.
|
||||
|
||||
If the VM cannot render the surface faithfully (known GL/compositor gaps in
|
||||
the guest — see `docs/TESTING.md`), that specific visual aspect is
|
||||
hardware-blocked: verify everything the VM *can* show, and queue the rest
|
||||
per §4.
|
||||
|
||||
## 4. Hardware-blocked checks
|
||||
|
||||
Some checks genuinely require real hardware (GPU behavior, multi-monitor
|
||||
hotplug, audio devices, power/suspend, firmware). For those:
|
||||
|
||||
1. Add an entry to `agent/HARDWARE-QUEUE.md` with: what changed, **exact**
|
||||
reproduction steps a human can follow verbatim, the expected observation
|
||||
(what "pass" looks like), and the commit hash once known.
|
||||
2. Mark the commit body **"V3 pending: <one-line summary>"**.
|
||||
3. Say it plainly in your report. A hardware-blocked check is not a failure
|
||||
and not something to hide — hiding it is the failure.
|
||||
4. When the human reports back, close the queue entry in the next commit and
|
||||
record the outcome; if it failed on hardware, that's a new bug at the top
|
||||
of the backlog.
|
||||
|
||||
Do not use the hardware queue as an escape hatch: if a check *can* be done
|
||||
in the VM, it must be. "The VM is slow" does not qualify.
|
||||
|
||||
## 5. Maintenance work
|
||||
|
||||
Maintenance is in scope for this skill and follows the same ladder:
|
||||
|
||||
- **Flake lock bumps**: treat as maximum-blast-radius changes. Build,
|
||||
boot the VM, run the full regression checklist, and do a visual
|
||||
spot-check of the session (themes can shift with upstream package
|
||||
changes). Never merge a lock bump on "it evaluates".
|
||||
- **Theme imports / new themes**: import via `tools/import-palettes.py`,
|
||||
then the full §3 visual protocol; additionally verify the theme-switch
|
||||
round trip (into the new theme and back out).
|
||||
- **Docs drift**: run `tools/check-option-docs.py` after any change that
|
||||
adds or modifies options, and fix drift in the same commit as the code
|
||||
change that created it. Doc-only fixes are V0 — but verify any command
|
||||
you document by actually running it.
|
||||
- **Backlog grooming / agent-notes**: V0; keep entries consistent with the
|
||||
conventions in `agent/`.
|
||||
|
||||
## 6. Guarding the philosophy
|
||||
|
||||
Before committing, check the change against the distro's promises:
|
||||
|
||||
- **User is not a Nix expert.** If the change requires the user to write or
|
||||
read Nix to use the feature day-to-day, redesign it. Configuration the
|
||||
user touches must stay in the simple, documented surface the repo defines.
|
||||
- **Rock-stable and recoverable.** Prefer boring, reproducible mechanisms
|
||||
over clever ones. Any change that could break boot or the session must
|
||||
have an obvious rollback story (NixOS generations count, but say so).
|
||||
- **Aesthetics are load-bearing.** A functionally correct but visually
|
||||
regressive change fails review by definition — that's what §3 is for.
|
||||
|
||||
When a requested change conflicts with these promises, stop and raise the
|
||||
conflict instead of implementing it quietly.
|
||||
|
||||
## 6.5 Token economy: delegate machinery, keep judgment
|
||||
|
||||
Not every step needs the smartest model. Use subagents to route mechanical
|
||||
work to cheap models and keep expensive reasoning where it pays. The repo
|
||||
defines two in `.claude/agents/`:
|
||||
|
||||
- **nomarchy-scout** (haiku, read-only): finding where things are defined,
|
||||
mapping which files touch a subsystem, scanning build logs and serial
|
||||
output for error lines, docs-vs-code drift sweeps. Use it for any pure
|
||||
information-gathering step instead of reading files into the main
|
||||
context yourself — it keeps the main context small, which matters more
|
||||
than the token price on long loops.
|
||||
- **nomarchy-runner** (haiku, executes the harness): builds, VM boots,
|
||||
screenshot capture, the scripted `tools/` checks. It runs commands and
|
||||
returns exit codes, logs, and artifact paths — nothing more.
|
||||
|
||||
The dividing line is **evidence vs. judgment**. Cheap models gather
|
||||
evidence; they never make verification claims. The following always stay
|
||||
with the main (smart) model and are never delegated downward:
|
||||
|
||||
- deciding what to test and what the regression scope is (§2)
|
||||
- interpreting an ambiguous or flaky failure (§2 rule 3)
|
||||
- viewing screenshots and judging visual quality (§3) — aesthetic judgment
|
||||
is exactly the kind of work small models do badly, and it's load-bearing
|
||||
for this distro
|
||||
- writing non-trivial Nix (module structure, overlays, cross-cutting
|
||||
refactors)
|
||||
- the final review of the diff and the §7 report
|
||||
|
||||
Practical guidance: delegation is not free — each subagent has its own
|
||||
context and the tokens multiply — so delegate when the work is mechanical
|
||||
*or* would pollute the main context with bulk (log files, wide file
|
||||
scans), not reflexively for every small step. A one-file read is cheaper
|
||||
done directly. When a scout/runner result surprises you, spot-check it
|
||||
yourself before building on it: cheap models are allowed to be wrong about
|
||||
hard things, which is precisely why they're not allowed to make claims.
|
||||
|
||||
### Fanning out worktree agents (parallel V0/V1 work)
|
||||
|
||||
When several NEXT items are independent and don't need the VM, spread them
|
||||
across worktree-isolated subagents that run in parallel — but keep the
|
||||
token cost down with these habits (each is a real lever, not a nicety):
|
||||
|
||||
- **Match the model to the task, not the tier.** A backlog item whose spec
|
||||
is already written (exact files, exact fixes) is *well-specified* →
|
||||
**sonnet**, not opus. Reserve opus for genuine multi-step reasoning
|
||||
(novel Nix, cross-cutting design, an ambiguous failure). Haiku for bulk
|
||||
mechanical. Picking sonnet over opus for a spec'd task is the single
|
||||
biggest saving.
|
||||
- **Point at the spec; don't restate it.** The brief should say "the spec
|
||||
is in `agent/BACKLOG.md` #NN — implement it, don't re-derive," plus only
|
||||
the *constraints* (scope files, branch, no-VM, no-push). A cold agent
|
||||
re-reading the whole design into its own context is the tokens you're
|
||||
trying to avoid.
|
||||
- **Disjoint file lanes.** Partition the items so no two agents touch the
|
||||
same file (map the touched files first). Zero shared files = zero merge
|
||||
conflicts at landing = no reconciliation tokens. If two items must share
|
||||
a file (README, flake.nix, rofi.nix), give both to one agent or keep one
|
||||
for yourself.
|
||||
- **Isolation + you own landing.** Run each agent with `isolation:
|
||||
worktree`; it commits to its own branch and **never pushes or touches
|
||||
`main`/`v1`**. You review each diff, cherry-pick onto `main`, and do the
|
||||
bookkeeping. Parallel pushers race on `main`; a single landing agent
|
||||
doesn't. (Clean up: `git worktree remove --force` + `git branch -D` the
|
||||
branch and its `worktree-…` sibling after landing.)
|
||||
- **Lean on scriptable checks as the primary evidence.** Where a
|
||||
deterministic `tools/` check or a `checks.*` guard already proves the
|
||||
property (e.g. `check-theme-contrast.py` for a palette fix,
|
||||
`option-docs` for a doc row), that near-free run *is* the V0/V1
|
||||
evidence — don't spend a VM render to re-confirm what the script already
|
||||
asserts.
|
||||
- **Batch V2 at the end, once.** Visual/behavioural items delegated at
|
||||
V0/V1 come back "V2 pending." Don't boot the VM per item — collect the
|
||||
landed changes and do **one** `theme-shot`/`test-install` pass covering
|
||||
all of them. The VM render + screenshot review is the most expensive
|
||||
token sink in the loop; amortise it.
|
||||
- **Re-verify on `main`, but leanly.** After cherry-picking, confirm the
|
||||
agent's V0/V1 on the merged tree — but a tiny, obviously-correct diff
|
||||
needs a targeted build, not a full re-run of everything. Trust-but-spot-
|
||||
check scales; blind re-running doesn't.
|
||||
|
||||
The judgment that never delegates (§ above) still holds: *you* review every
|
||||
diff before it lands, and *you* make the product calls (a "finish vs.
|
||||
quarantine" decision is yours or Bernardo's, never a cheap agent's).
|
||||
|
||||
## 7. Reporting format
|
||||
|
||||
End every unit of work with a short report containing:
|
||||
|
||||
1. What changed (one paragraph, plain language).
|
||||
2. Verification tier reached, with evidence (commands + key output,
|
||||
checklist items run, screenshot paths viewed).
|
||||
3. Anything pending: "V2 pending" (environment) or "V3 pending" (hardware,
|
||||
with queue entry reference).
|
||||
4. Follow-ups added to the backlog, if any.
|
||||
21
.claude/skills/theme/SKILL.md
Normal file
21
.claude/skills/theme/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Nomarchy Theme Designer Skill
|
||||
|
||||
**Trigger:** Use this skill whenever the user requests to create, update, refine, or troubleshoot themes and visual designs for the Nomarchy distribution.
|
||||
|
||||
## System Prompt / Instructions
|
||||
|
||||
You are an elite UI/UX designer and Linux ricing expert specializing in Wayland environments. Your role is to create cohesive, innovative, and visually stunning desktop themes for "Nomarchy," a custom Linux distribution based on NixOS and the Hyprland window manager.
|
||||
|
||||
### Context & Architecture
|
||||
All theme configurations for Nomarchy live inside the `themes/` directory at the root of the repository. Before generating any new configurations, you MUST read and analyze the existing files in this directory. Learn how the current themes are structured, how the syntax is formatted for each application, and how they are integrated into the broader NixOS configuration. Always match this established architectural pattern.
|
||||
|
||||
### Your Design Responsibilities
|
||||
1. **Holistic Design:** Develop unified themes that span across Hyprland (borders, shadows, animations), Waybar, Ghostty terminal, btop, fastfetch, Rofi/fuzzel, and desktop wallpapers.
|
||||
2. **Color Theory & Aesthetics:** Create or adapt advanced color palettes. You may draw inspiration from established aesthetics (e.g., Everforest, Nord, Gruvbox) or r/unixporn trends, but you should innovate. Ensure perfect harmony between background, foreground, accents, and warning/error colors.
|
||||
3. **Typography & Iconography:** Select and pair fonts (UI vs. Monospace) and icon themes that match the specific vibe of the color palette.
|
||||
4. **Accessibility:** Follow best practices for UI design. Ensure high contrast for text readability and avoid eye strain.
|
||||
|
||||
### Process
|
||||
1. Read the `themes/` directory to understand the codebase.
|
||||
2. Explain the "vibe," primary color palette (with hex codes), and typography choices of your proposed design.
|
||||
3. Implement the theme by generating or updating the necessary files within the `themes/` structure.
|
||||
97
.gitea/workflows/bump.yml
Normal file
97
.gitea/workflows/bump.yml
Normal file
@@ -0,0 +1,97 @@
|
||||
# Nomarchy scheduled lock bump — the automated half of "rock-stable
|
||||
# without rotting": once a week, update flake.lock (inputs track pinned
|
||||
# release branches, so this never jumps a NixOS release — that's a
|
||||
# deliberate hand-edited v2, see agent/GOALS.md), gate it, and land it
|
||||
# on main only on green. `v1` promotion stays human, always.
|
||||
#
|
||||
# Fast lane: workflow_dispatch. For a security fix upstream, run this
|
||||
# workflow manually from the Actions tab instead of waiting for Monday.
|
||||
#
|
||||
# Gate scope: `nix flake check --no-build` (eval tier) followed by a V1 build
|
||||
# (home-manager activation package + nixos toplevel) to catch compilation errors.
|
||||
# (this runner has no KVM; see item 20 for the VM-suite
|
||||
# upgrade path). The py_compile/bash -n steps are skipped on purpose:
|
||||
# they don't read flake.lock, so a lock bump cannot break them. The
|
||||
# bump commit's own push then triggers check.yml as a second net.
|
||||
# A green bump therefore guards evaluation and compilation, not VM behaviour —
|
||||
# the checks.* suite still runs locally / at promotion time.
|
||||
#
|
||||
# Failure mode: a red gate fails this run visibly and pushes nothing;
|
||||
# next schedule retries. A push race (someone landed on main mid-run)
|
||||
# also just fails the final push — rerun or wait a week.
|
||||
#
|
||||
# Container recipe (nixbld users, sandbox=false, pinned Nix, plain-shell
|
||||
# install) inherited from check.yml — the gotchas are documented there.
|
||||
|
||||
name: Lock bump
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '17 5 * * 1' # Mondays 05:17 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
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: Update flake.lock
|
||||
# Inputs track release branches (nixos-26.05, release-26.05), so
|
||||
# the update stays within them by construction. The human-readable
|
||||
# change list goes into the commit body below.
|
||||
run: |
|
||||
nix flake update 2>&1 | tee /tmp/bump-log
|
||||
if git diff --quiet flake.lock; then
|
||||
echo "Lock already up to date — nothing to do."
|
||||
echo "changed=0" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=1" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
id: update
|
||||
|
||||
- name: Gate — nix flake check (eval only)
|
||||
if: steps.update.outputs.changed == '1'
|
||||
run: nix flake check --no-build
|
||||
|
||||
- name: Gate — V1 build (toplevel + home-manager)
|
||||
if: steps.update.outputs.changed == '1'
|
||||
run: |
|
||||
nix build .#homeConfigurations.nomarchy.activationPackage --no-link
|
||||
nix build .#nixosConfigurations.nomarchy.config.system.build.toplevel --no-link
|
||||
|
||||
- name: Commit + push on green
|
||||
if: steps.update.outputs.changed == '1'
|
||||
# The checkout step persists the Actions token, so this push
|
||||
# authenticates as the workflow; its push triggers check.yml as
|
||||
# the second net. Only flake.lock is committed — pathspec-limited,
|
||||
# nothing else can ride along.
|
||||
run: |
|
||||
{
|
||||
echo "chore(lock): scheduled upstream bump"
|
||||
echo
|
||||
grep -E '^(• | )' /tmp/bump-log || true
|
||||
echo
|
||||
echo "Gate: nix flake check --no-build (eval tier — see bump.yml header)."
|
||||
} > /tmp/bump-msg
|
||||
git -c user.name="nomarchy-bump" -c user.email="actions@git.bemagri.xyz" \
|
||||
commit --only flake.lock -F /tmp/bump-msg
|
||||
git push origin HEAD:main
|
||||
103
.gitea/workflows/check.yml
Normal file
103
.gitea/workflows/check.yml
Normal file
@@ -0,0 +1,103 @@
|
||||
# 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 (all tracked scripts)
|
||||
# Every tracked *.py — theme-sync, the installer composers
|
||||
# (compose-lock, patch-template) and the maintainer tools
|
||||
# (tools/, tools/vm/). Via nix shell so the step doesn't depend on
|
||||
# the runner image preinstalling python3.
|
||||
run: |
|
||||
nix shell nixpkgs#python3 --command \
|
||||
bash -c 'git ls-files "*.py" | xargs -r python3 -m py_compile'
|
||||
|
||||
- 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
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ __pycache__/
|
||||
|
||||
# Claude Code machine-local settings (permissions etc.)
|
||||
.claude/settings.local.json
|
||||
# .claude/skills/
|
||||
|
||||
52
CLAUDE.md
Normal file
52
CLAUDE.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# 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.
|
||||
|
||||
## Where things live (read this first)
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| **`agent/`** | Loop state — **only executable queue is `BACKLOG.md`** |
|
||||
| **`docs/VISION.md`** | Product themes toward **v1.0** (not a queue) |
|
||||
| **`docs/ROADMAP.md`** | Design history + shipped log |
|
||||
| **`docs/README.md`** | Full documentation map |
|
||||
| **`agent/README.md`** | Map of loop files |
|
||||
| **`.claude/`** | Claude Code only (permissions + subagents) — not the backlog |
|
||||
|
||||
## If you're here to work autonomously (the loop)
|
||||
Follow **`agent/LOOP.md`** — one iteration: orient → pick one BACKLOG
|
||||
item → work → verify → 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 the tier you reached. Cheap first:
|
||||
`nix flake check --no-build`.
|
||||
- **Conventions** (`agent/CONVENTIONS.md`): in-flake state, menu
|
||||
placement, Waybar parity with whole-swaps, toggle-vs-package discipline,
|
||||
template as SoT for opt-in comments (`templates/downstream`).
|
||||
- **Git:** direct commits on `main`, pushed; **`v1` is human-only** —
|
||||
never touch it. Never `nix flake update` unless the task is a lock bump.
|
||||
No formatter — match aligned hand-formatting.
|
||||
- Layout: `hosts/` machine · `modules/` distro · `themes/` data ·
|
||||
`pkgs/` code · `tools/` maintainer · `agent/` loop · `docs/` human docs.
|
||||
|
||||
## Delegation
|
||||
|
||||
Push mechanical work down; keep judgment. Brief every child cold.
|
||||
|
||||
| Model | Best for | Delegate? | Effort |
|
||||
|---|---|---|---|
|
||||
| Haiku | bulk mechanical | never | low |
|
||||
| Sonnet | scoped research | when it helps | medium |
|
||||
| Opus 4.8 | multi-step reasoning | on clear benefit | xhigh |
|
||||
| Fable 5 | judgment, taste | by default | medium |
|
||||
|
||||
Fable goes xhigh only for the hardest calls. Skip high.
|
||||
|
||||
## Escalation
|
||||
|
||||
An Opus parent can spawn a Fable child for one hard call. Work above your
|
||||
tier? Return it — don't burn tokens.
|
||||
162
README.md
162
README.md
@@ -36,7 +36,7 @@ Flat on purpose. Two module trees, one options file each, no hidden layers.
|
||||
├── flake.nix # inputs + the downstream API (exports below)
|
||||
├── lib.nix # nomarchy.lib.mkFlake — one-call downstream wrapper
|
||||
├── theme-state.json # ★ THE single source of truth (git-tracked!)
|
||||
├── themes/ # 21 presets: <slug>.json + optional <slug>/ assets
|
||||
├── themes/ # 24 presets: <slug>.json + optional <slug>/ assets
|
||||
│ ├── nord.json # palette (required, works alone)
|
||||
│ └── nord/ # assets (optional, fixed filenames)
|
||||
│ ├── backgrounds/ # wallpapers (auto-picked, SUPER+SHIFT+T cycles)
|
||||
@@ -62,6 +62,7 @@ Flat on purpose. Two module trees, one options file each, no hidden layers.
|
||||
│ ├── keybinds.nix # single source: Hyprland binds + SUPER+? sheet
|
||||
│ ├── swaync.nix # notifications, same JSON
|
||||
│ ├── idle.nix # hyprlock + hypridle, same JSON
|
||||
│ ├── battery-notify.nix # low-battery toasts at the bar's thresholds
|
||||
│ ├── yazi.nix # flagship TUI file manager + plugins
|
||||
│ ├── osd.nix # swayosd volume/brightness OSD
|
||||
│ ├── shell.nix # zsh + starship + bat/eza/zoxide
|
||||
@@ -72,11 +73,20 @@ Flat on purpose. Two module trees, one options file each, no hidden layers.
|
||||
│ └── live.nix # bootable live ISO (try the distro, no install)
|
||||
├── pkgs/
|
||||
│ ├── nomarchy-theme-sync/ # state writer + rebuild dispatcher (Python)
|
||||
│ └── nomarchy-install/ # live-ISO installer (gum + disko + mkFlake)
|
||||
├── templates/downstream/ # `nix flake init -t` starter for users
|
||||
├── docs/TESTING.md # how to verify changes (incl. AI-agent rules)
|
||||
├── docs/OVERRIDES.md # how downstream users override defaults
|
||||
├── docs/ROADMAP.md # forward-looking plans + shipped-fixes log
|
||||
│ ├── nomarchy-install/ # live-ISO installer (gum + disko + mkFlake)
|
||||
│ ├── nomarchy-doctor/ # read-only health sheet, one command per failure
|
||||
│ ├── nomarchy-control-center/ # TUI settings front-end over nomarchy-theme-sync (gum)
|
||||
│ └── nomarchy-battery-notify/ # low-battery toast watcher backing battery-notify.nix
|
||||
├── templates/downstream/ # machine flake SoT (`flake init` + installer copy/patch)
|
||||
├── docs/ # human docs — map: docs/README.md
|
||||
│ ├── VISION.md # product themes toward v1.0 (not a queue)
|
||||
│ ├── ROADMAP.md # design/decision records + shipped log
|
||||
│ ├── HARDWARE.md # firmware, profiles, drivers
|
||||
│ ├── TESTING.md · RECOVERY.md · OVERRIDES.md · MIGRATION.md
|
||||
├── agent/ # agent loop state — map: agent/README.md
|
||||
│ # BACKLOG (executable queue), LOOP, GOALS, …
|
||||
├── CLAUDE.md # agent harness entry (points at agent/ + docs/)
|
||||
├── .claude/ # Claude Code only: permissions + subagents
|
||||
└── tools/ # maintainer-only
|
||||
├── import-palettes.py # converts old-distro themes → JSON + assets
|
||||
├── test-live-iso.sh # build the ISO + boot it in QEMU
|
||||
@@ -109,7 +119,8 @@ Like what you see? **`nomarchy-install`** (in a terminal) walks you through
|
||||
installing to disk: pick a disk, LUKS2 full-disk encryption **by default**
|
||||
(in exchange the desktop logs in passwordless — the passphrase already
|
||||
gates the machine), user + hostname + timezone, hardware autodetection
|
||||
(DMI → nixos-hardware profile), a hibernation-ready swapfile sized to RAM,
|
||||
(DMI → nixos-hardware profile — see **[docs/HARDWARE.md](docs/HARDWARE.md)**),
|
||||
a hibernation-ready swapfile sized to RAM,
|
||||
then disko partitions (GPT + ESP + BTRFS subvolumes incl. `@snapshots` —
|
||||
snapper timeline snapshots are on) and `nixos-install` runs — **without a
|
||||
network** when the ISO was built from a clean tree (the target's
|
||||
@@ -128,6 +139,16 @@ mkdir my-machine && cd my-machine
|
||||
nix flake init -t "git+https://git.bemagri.xyz/bernardo/nomarchy.git?ref=v1"
|
||||
```
|
||||
|
||||
The template's own README then walks the first-run steps: a real
|
||||
`hardware-configuration.nix`, `git init` (flakes only see tracked
|
||||
files), the two first rebuilds, and landing the checkout at
|
||||
`~/.nomarchy` so the theme CLI finds it. (The installer does all of
|
||||
this for you.)
|
||||
|
||||
Already running NixOS on this machine? No reinstall needed — adopt
|
||||
Nomarchy in place, reusing your existing `hardware-configuration.nix`:
|
||||
**[docs/MIGRATION.md](docs/MIGRATION.md)**.
|
||||
|
||||
You own two files day-to-day: `system.nix` and `home.nix` (plus
|
||||
`theme-state.json`, written by the CLI). Your `flake.nix` is set up once —
|
||||
later by the installer — and never hand-edited; it's a single call:
|
||||
@@ -162,6 +183,8 @@ Day-to-day you'll use the shipped shortcuts instead:
|
||||
|
||||
```sh
|
||||
sys-update # nix flake update + system rebuild (BTRFS snapshot first when available)
|
||||
sys-rebuild # system rebuild against the CURRENT lock (config changes only, no update)
|
||||
# …both end with a package-level diff of what the rebuild changed (nvd)
|
||||
home-update # home-manager switch (no flake update, no sudo)
|
||||
```
|
||||
|
||||
@@ -182,6 +205,13 @@ behaviour (input/misc/monitor/chrome) is `mkDefault` so a plain `home.nix`
|
||||
assignment wins, and bind/exec-once lists concatenate. Full guide with
|
||||
examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
||||
|
||||
**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 |
|
||||
|---|---|---|
|
||||
| `nomarchy.stateFile` | — (required) | Path to your theme-state.json |
|
||||
@@ -190,12 +220,14 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
||||
| `nomarchy.keyboard.variant` | `""` | XKB variant for the session |
|
||||
| `nomarchy.keyboard.devices` | `{}` | Per-device layout overrides (Hyprland `device` blocks keyed by `hyprctl devices` name) — e.g. an external keyboard with its own layout/variant |
|
||||
| `nomarchy.keyboard.layouts` | `[]` | Extra candidate layouts; when set, a watcher prompts (rofi) for a layout on a newly-connected keyboard and remembers it per-device |
|
||||
| `nomarchy.nightlight.enable` | `false` | Scheduled blue-light filter (hyprsunset) — warm at night (`.temperature`, default 4000K) between `.sunset`/`.sunrise`, no shift by day |
|
||||
| `nomarchy.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.waybar.enable` | `true` | Nomarchy's Waybar |
|
||||
| `nomarchy.rofi.enable` | `true` | Themed rofi launcher + `nomarchy-menu` dispatcher |
|
||||
| `nomarchy.swaync.enable` | `true` | swaync notifications, themed |
|
||||
| `nomarchy.idle.enable` | `true` | hyprlock + hypridle (idle lock 5 min, display off 10, suspend 30) |
|
||||
| `nomarchy.batteryNotify.enable` | `true` | Low-battery toasts at the bar's thresholds — 25% low, 10% critical (stays up until dismissed); silent no-op on machines without a battery |
|
||||
| `nomarchy.idle.enable` | `true` | hyprlock + hypridle (idle lock 5 min, display off 10, suspend 15 min — battery-only) |
|
||||
| `nomarchy.yazi.enable` | `true` | yazi TUI file manager, themed + curated plugins |
|
||||
| `nomarchy.osd.enable` | `true` | swayosd on-screen display for volume/brightness/mute |
|
||||
| `nomarchy.shell.enable` | `true` | zsh + starship prompt + bat/eza/zoxide (zsh is the default login shell) |
|
||||
@@ -205,21 +237,52 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
||||
| `nomarchy.fastfetch.enable` | `true` | fastfetch fronted by the themed Nomarchy logo |
|
||||
| `nomarchy.keys.enable` | `true` | gpg-agent fronting SSH + pinentry-qt |
|
||||
| `nomarchy.displays.enable` | `true` | nwg-displays interactive monitor arranger (helper for `nomarchy.monitors`) |
|
||||
| `nomarchy.viewers.enable` | `true` | Document/image viewers: zathura (Stylix-themed PDF) + imv |
|
||||
| `nomarchy.mime.enable` | `true` | Default "open with" associations (PDF/image/video/text/browser/directory); entries for apps you removed are skipped, so it degrades with the suite |
|
||||
| `nomarchy.monitors` | `[]` | Declarative per-output layout → Hyprland `monitor` rules (applied on hotplug); `,preferred,auto,1` wildcard kept as fallback |
|
||||
| `nomarchy.displayProfiles` | `{}` | Named layouts for the same outputs (docked/undocked/…), switched from System › Display › Profiles: instant via hyprctl, persisted in-flake (`settings.displayProfile`), baked over `nomarchy.monitors` at the next rebuild. The menu's Auto-switch row (`settings.displayProfileAuto`) makes plugging/unplugging outputs apply the matching profile instantly (driven by a dedicated Hyprland IPC socket watcher, no polling). A profile can also pin workspaces to outputs (`workspaces = { "1" = "DP-3"; }`) — moved instantly on switch, baked as Hyprland `workspace` rules |
|
||||
| `nomarchy.launchOrFocus` | `[]` | Launch-or-focus binds: `SUPER+<key>` focuses the app's window (case-insensitive class match) or launches it; entries land in the SUPER+? cheatsheet, and a bind whose app was removed notifies instead of failing silently |
|
||||
| `nomarchy.themesDir` | Nomarchy's `themes/` | Where per-theme app overrides are probed |
|
||||
| `nomarchy.package` | overlay's `nomarchy-theme-sync` | The theme/state tool package, overridable if you fork it |
|
||||
|
||||
**Always-on, no toggle by design:** `services.cliphist`, `services.udiskie`
|
||||
(automount + safe-removal toasts) and `services.easyeffects` (mic noise
|
||||
cancellation) ship unconditionally — small, low-risk pieces with no
|
||||
Nomarchy-specific config behind them (the toggle-vs-package rule in
|
||||
`agent/CONVENTIONS.md`). Disable one from `home.nix` with the plain HM
|
||||
option, e.g. `services.easyeffects.enable = lib.mkForce false;` — see
|
||||
**[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
||||
|
||||
**`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.stateFile` | `null` | theme-state.json for the system-side consumers (the Plymouth tint); `lib.mkFlake` wires it for you — set it only when composing the modules by hand |
|
||||
| `nomarchy.system.fileManager.enable` | `true` | Thunar GUI + gvfs/tumbler/udisks2 (the "open folder" handler) |
|
||||
| `nomarchy.system.greeter.enable` | `true` | greetd/tuigreet |
|
||||
| `nomarchy.system.greeter.autoLogin` | `null` | Auto-login this user into Hyprland (installer sets it on LUKS machines) |
|
||||
| `nomarchy.system.audio.enable` | `true` | Pipewire stack |
|
||||
| `nomarchy.system.bluetooth.enable` | `true` | Bluetooth + blueman |
|
||||
| `nomarchy.system.autoTimezone.enable` | `false` | Automatic timezone (geoclue + automatic-timezoned) — the clock follows your location; toggle from System › Auto timezone (a menu enable rebuilds: it has to unset the static `time.timeZone`) |
|
||||
| `nomarchy.system.snapper.enable` | `false` | Hourly/daily BTRFS timeline snapshots + `nixos-rebuild-snap` (installer enables it; no-op unless root is BTRFS) |
|
||||
| `nomarchy.system.power.enable` | `true` | Active power management (see below) |
|
||||
| `nomarchy.system.power.backend` | `"ppd"` | `"ppd"` (power-profiles-daemon + menu/Waybar switcher) or `"tlp"` (deeper battery tuning, no switcher) — mutually exclusive |
|
||||
| `nomarchy.system.power.laptop` | `false` | Marks a laptop, gating battery-only features; the installer sets it when a battery is present |
|
||||
| `nomarchy.system.power.thermal.enable` | `false` | thermald (Intel-only); the installer enables it on a GenuineIntel CPU |
|
||||
| `nomarchy.system.power.batteryChargeLimit` | `null` | Stop charging at this % (e.g. `80`) where the hardware supports it; needs `power.laptop` |
|
||||
| `nomarchy.services.tailscale.enable` | `false` | Opt-in: Tailscale mesh VPN (then `sudo tailscale up`) |
|
||||
| `nomarchy.hardware.intel.enable` | `false` | Intel enablement above nixos-hardware (GuC/HuC firmware via `i915.enable_guc=3` — `.guc` toggles just that; the installer unsets it on `xe`-driver GPUs); 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, each toggleable via `.pstate` / `.vaapi`); 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.hardware.camera.hideIrSensor` | `false` | Hide a dual-sensor webcam's IR node from PipeWire's **v4l2** path so apps only ever see the colour camera (the "second, dark Integrated Camera"); installer-set on a paired RGB+IR webcam. `/dev/video*` stays open, so Howdy-style face unlock still works; `.irMatch` overrides the IR-name regex. Does **not** hide IR from libcamera / portal / Flatpak pickers — see [HARDWARE.md §7](docs/HARDWARE.md) |
|
||||
| `nomarchy.hardware.i2c.enable` | `false` | I2C devices support — access to `/dev/i2c-*` (RGB controllers, sensors, DDC/CI monitor control) |
|
||||
| `nomarchy.hardware.i2c.ddcci` | `false` (distro default: **`true`**) | the ddcci-driver kernel module, exposing external monitors as standard backlight devices via DDC/CI so brightness keys and swayosd natively control them |
|
||||
| `nomarchy.services.tailscale.enable` | `false` | Opt-in: Tailscale mesh VPN — the login user is made the operator, so `tailscale up/down/set` and the System › VPN menu work without sudo |
|
||||
| `nomarchy.services.syncthing.enable` | `false` | Opt-in: Syncthing file sync as the login user (GUI at `127.0.0.1:8384`) |
|
||||
| `nomarchy.services.podman.enable` | `false` | Opt-in: rootless Podman (`docker` aliased to it) |
|
||||
| `nomarchy.services.flatpak.enable` | `false` | Opt-in: Flatpak + the Flathub remote |
|
||||
@@ -235,14 +298,25 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
||||
| `nomarchy.services.ollama.enable` | `false` | Opt-in: Ollama local LLM runtime on `127.0.0.1:11434` (CPU; GPU via `services.ollama.acceleration`) |
|
||||
| `nomarchy.services.printing.enable` | `false` | Opt-in: CUPS + Avahi/mDNS network printer discovery |
|
||||
| `nomarchy.services.openrgb.enable` | `false` | Opt-in: OpenRGB daemon for RGB peripheral/motherboard lighting |
|
||||
| `nomarchy.services.restic.enable` | `false` | Opt-in: scheduled daily restic backup (set `.repository` + `.passwordFile`; 7/4/6 retention; list/restore via the `restic-nomarchy` wrapper) |
|
||||
| `nomarchy.services.restic.enable` | `false` | Opt-in: scheduled daily restic backup (set `.repository` + `.passwordFile`; backs up `.paths`, default `/home`; 7/4/6 retention; list/restore via the `restic-nomarchy` wrapper) |
|
||||
|
||||
Beyond the `nomarchy.*` surface, the system layer turns on the usual
|
||||
desktop services with `lib.mkDefault` (override natively). One worth
|
||||
calling out: **`services.fwupd.enable`** is on by default for firmware
|
||||
desktop services with `lib.mkDefault` (override natively) and enforces
|
||||
baseline security/stability defaults. A few worth calling out:
|
||||
**`security.apparmor.enable`** is on by default for mandatory access
|
||||
control confinement. **Kernel panics** are configured to auto-reboot the
|
||||
machine after 10 seconds (`panic=10 oops=panic`) rather than hanging
|
||||
indefinitely. **`services.fwupd.enable`** is on by default for firmware
|
||||
updates via LVFS — it only refreshes metadata, never flashes on its own,
|
||||
so run `fwupdmgr update` to apply. Disable with `services.fwupd.enable =
|
||||
false;` on machines without real firmware (VMs/headless).
|
||||
so run `fwupdmgr update` to apply; disable with `services.fwupd.enable =
|
||||
false;` on machines without real firmware (VMs/headless). Full hardware
|
||||
story (profiles, fingerprint, NVIDIA, unsupported machines, contributing
|
||||
a DMI line): **[docs/HARDWARE.md](docs/HARDWARE.md)**. 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
|
||||
|
||||
@@ -271,6 +345,31 @@ imperative; nothing in Nix consumes the path): applied at session start and
|
||||
after every switch via a tiny activation hook, cycled instantly with
|
||||
`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>/`)
|
||||
|
||||
Recoloring covers 95% of theming; the rest is one optional assets directory
|
||||
@@ -284,29 +383,35 @@ per theme — a single place to look, unlike the old distro's split:
|
||||
| `waybar.jsonc` | whole-swap for the bar *layout* (must be plain JSON) |
|
||||
| `rofi.rasi` | **whole-swap**: replaces the generated launcher/menu theme entirely |
|
||||
|
||||
Six ported themes ship a `waybar.css` identity (catppuccin, lumon, nord,
|
||||
retro-82, summer-day, summer-night). Custom user themes can live in
|
||||
Four themes ship a `waybar.css` identity (summer-day, summer-night,
|
||||
executive-slate, boreal). Custom user themes can live in
|
||||
`$NOMARCHY_PATH/themes/` (preset lookup) and `nomarchy.themesDir` (eval-time
|
||||
asset probe).
|
||||
|
||||
## 5. Day-to-day
|
||||
|
||||
```sh
|
||||
nomarchy-theme-sync list # 21 presets (nord, gruvbox, rose-pine, …)
|
||||
nomarchy-theme-sync list # 24 presets (nord, gruvbox, rose-pine, …)
|
||||
nomarchy-theme-sync apply kanagawa # whole desktop, one generation (~a switch)
|
||||
nomarchy-theme-sync set ui.gapsOut 16 # tweak one knob (also a switch)
|
||||
nomarchy-theme-sync bg next # cycle wallpapers — instant, no rebuild
|
||||
nomarchy-theme-sync bg auto # back to the theme's default wallpaper
|
||||
nomarchy-theme-sync get colors.accent
|
||||
sys-update # update inputs + rebuild the system (snapshots first)
|
||||
sys-rebuild # rebuild the system, current lock (no update)
|
||||
home-update # rebuild just the desktop layer
|
||||
nomarchy-doctor # read-only health sheet (also: menu › System › Doctor)
|
||||
```
|
||||
|
||||
Something broke anyway? Every rebuild is a generation and (on BTRFS)
|
||||
every hour is a snapshot — the undo story, from a bad theme to a
|
||||
machine that won't boot, is **[docs/RECOVERY.md](docs/RECOVERY.md)**.
|
||||
|
||||
Keybinds: `SUPER+Return` terminal · `SUPER+D` launcher · `SUPER+T` theme
|
||||
picker · `SUPER+SHIFT+T` next wallpaper · `SUPER+X` power menu ·
|
||||
`SUPER+E` file manager (yazi) · `SUPER+N` notifications · `SUPER+CTRL+V`
|
||||
clipboard history · `SUPER+Q`
|
||||
close · `SUPER+1..9` workspaces · `Print` region screenshot.
|
||||
clipboard history · `SUPER+SHIFT+C` color picker (hyprpicker) · `SUPER+Q`
|
||||
close · `SUPER+1..9` workspaces · `SUPER+ALT+arrow` move workspace to monitor · `Print` region screenshot.
|
||||
|
||||
Shell aliases (zsh, gated on `nomarchy.shell.enable`) — `alias` lists them
|
||||
all; the curated set:
|
||||
@@ -341,7 +446,10 @@ reload # exec zsh (reload the shell)
|
||||
- **New themed value:** add the key to `theme-state.json` and consume it in
|
||||
the Nix modules. One place — there is no second renderer to keep in sync.
|
||||
- **Importing more old-distro palettes:**
|
||||
`tools/import-palettes.py <palettes-dir> themes/`.
|
||||
`tools/import-palettes.py <palettes-dir> themes/` (roles are first-class:
|
||||
when ANSI color0==color8 the tool derives an overlay step; light themes
|
||||
do not use dark ANSI black as surface — hand-tune after import; do not
|
||||
bulk-reimport shipped JSON without a hierarchy pass).
|
||||
- **New opt-in feature (convention):** when a feature is off by default and
|
||||
needs the user to set `nomarchy.*` options (e.g. night light, per-device
|
||||
keyboard layouts, monitor layout, power management), ship a **commented**
|
||||
@@ -357,6 +465,12 @@ reload # exec zsh (reload the shell)
|
||||
|
||||
## Roadmap & known issues
|
||||
|
||||
See **[docs/ROADMAP.md](docs/ROADMAP.md)** — forward-looking plans plus the
|
||||
log of shipped fixes. Kept out of the README so this stays a focused entry
|
||||
point.
|
||||
| Doc | Role |
|
||||
|-----|------|
|
||||
| **[agent/BACKLOG.md](agent/BACKLOG.md)** | What to do next (agent queue) |
|
||||
| **[docs/VISION.md](docs/VISION.md)** | Product themes toward **v1.0** |
|
||||
| **[docs/ROADMAP.md](docs/ROADMAP.md)** | Design history + shipped log |
|
||||
| **[docs/README.md](docs/README.md)** · **[agent/README.md](agent/README.md)** | Maps |
|
||||
| **[agent/LOOP.md](agent/LOOP.md)** | Autonomous iteration protocol |
|
||||
|
||||
Kept out of the README body so this stays a focused entry point.
|
||||
|
||||
184
agent/BACKLOG.md
Normal file
184
agent/BACKLOG.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Backlog — the prioritized task queue
|
||||
|
||||
**This is the only executable work list for agents.** Product themes and
|
||||
v1.0 intent live in [`docs/VISION.md`](../docs/VISION.md); design history
|
||||
in [`docs/ROADMAP.md`](../docs/ROADMAP.md); map in
|
||||
[`docs/README.md`](../docs/README.md) and [`agent/README.md`](README.md).
|
||||
|
||||
**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 (and/or a note in VISION)
|
||||
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 (include
|
||||
`VISION § …` or `ROADMAP § …` when relevant); only Bernardo moves items
|
||||
*out* of PROPOSED into the tiers.
|
||||
|
||||
---
|
||||
|
||||
## NOW
|
||||
|
||||
*(empty — NEXT's top item is the queue head)*
|
||||
|
||||
## NEXT
|
||||
|
||||
### 74. Unattended-install test matrix
|
||||
`test-install.sh` always LUKS+swap; add (or document) no-swap and
|
||||
explicit no-LUKS paths via env flags only. Cost: script + KVM time; V2.
|
||||
|
||||
### 75. Installer template SoT — V2 install after ISO rebuild
|
||||
HM pre-activate now pulls starter packages (larger closure). Run
|
||||
`test-install.sh` (or equivalent) on a rebuilt live ISO and confirm
|
||||
first boot still themed. Cost: ISO + KVM; V2.
|
||||
|
||||
### 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
|
||||
that later upgrades the bump gate from eval-only to the full suite.
|
||||
|
||||
### 41. Floating-window audit — residual GTK portal class `[blocked:hw]`
|
||||
(Raised by Bernardo, 2026-07-07 — item 11.) Conservative cut + 2026-07-10
|
||||
slice shipped: mixer, blueman, system-config-printer, calendar,
|
||||
**hyprpolkitagent** + **pinentry-qt** (classes from package app-id
|
||||
strings; softGL capture harness could not materialize dialogs).
|
||||
**Still open:** GTK file-chooser portal (`xdg-desktop-portal-gtk`) —
|
||||
class not discoverable headlessly. On hardware: open a portal file
|
||||
picker, `hyprctl clients`, append float/center rules if needed; confirm
|
||||
polkit/pinentry/blueman actually float.
|
||||
|
||||
## 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).
|
||||
- **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.*
|
||||
|
||||
*Open work only. Shipped exam/A–C items (#47–#63, #14, #52 theme
|
||||
high-ROI, etc.) live in the journal + ROADMAP — not here.*
|
||||
|
||||
### Product / day-2
|
||||
|
||||
- **Battery charge-limit — make the toggle instant** `[blocked:hw]`
|
||||
Menu/CC write `settings.power.batteryChargeLimit` but need a rebuild.
|
||||
Instant path: udev group-writable `charge_control_end_threshold` on
|
||||
system batteries + live `echo` alongside state persist. Decide first
|
||||
whether a local user may set the charge cap. Cost: medium; hw confirm.
|
||||
|
||||
_(Portal/Flatpak IR camera → **#71** docs (a); (b)/(c) still need T14s.)_
|
||||
|
||||
_(Post-install hardware hints residual → **#73**.)_
|
||||
|
||||
- **Look & Feel submenu** (ROADMAP optional) — night-light, wallpaper
|
||||
cycle, blur/gaps with Theme; root stays six entries. Product call.
|
||||
|
||||
- **NVIDIA first-class options** `[big]` `[blocked:hw]` — thin
|
||||
`nomarchy.hardware.nvidia.*` only with hybrid maintainer + queue.
|
||||
Prefer shipped #59 commented guidance until then.
|
||||
|
||||
### Installer / template
|
||||
|
||||
_(Installer ↔ template SoT CI → **#72**; V2 install → **#75**.)_
|
||||
_(Installer chown flake dir → **#65**.)_
|
||||
_(MIGRATION.md snapshot layout → **#64**.)_
|
||||
_(Unattended-install test matrix → **#74**.)_
|
||||
_(Friendlier mkFlake theme-state errors → **#66**.)_
|
||||
|
||||
### Theme polish
|
||||
|
||||
_(#52 residual ANSI → **#68**.)_
|
||||
_(Fidelity / hierarchy nits → **#67**.)_
|
||||
_(Import pipeline hierarchy → **#70**.)_
|
||||
_(audit-theme-design identity exemptions → **#69**.)_
|
||||
|
||||
- **summer-day / summer-night pair polish** — waybar CSS vs JSON dual
|
||||
SoT; night `subtext`=`muted`=`overlay`; day/night font + battery
|
||||
threshold + VPN pill drift. Pick JSON as SoT (or document EF split).
|
||||
Cost: CSS/JSON; V3.
|
||||
|
||||
- **summer-\* CSS status module states** — missing `.on` / `.available`
|
||||
/ `.recording` polish vs boreal/generated. Cost: CSS; V3.
|
||||
|
||||
- **theme preview recapture nicety** — executive-slate + neon-glass
|
||||
previews are bare desktop+bar, not the floating-terminal composition
|
||||
of the other 21; optional identity `btop.theme`s. Cost: asset/V3.
|
||||
|
||||
- **Identity optional taste retunes** — white / lumon / hackerman /
|
||||
matte-black / miasma hierarchy or status L-steps only (no traffic-light
|
||||
forced on identity themes). Product call per theme.
|
||||
|
||||
### v1.0 pointer
|
||||
|
||||
See **VISION**. Remaining PROPOSED: charge-limit instant, Look & Feel,
|
||||
NVIDIA first-class, summer-* / preview / identity taste polish.
|
||||
|
||||
|
||||
## 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).
|
||||
|
||||
- **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. The shipped
|
||||
mime defaults (item 8, done) point `text/html`/http(s) at
|
||||
`firefox.desktop` as *inert* entries — they activate the moment
|
||||
Firefox is installed and are skipped otherwise, so the remaining call
|
||||
is only: ship a browser active in the suite, or stay
|
||||
browserless-by-default?
|
||||
|
||||
- **Default power backend — PPD vs TLP:** (raised by Bernardo 2026-07-08:
|
||||
would TLP be more power-efficient, and should it be the default?)
|
||||
TLP does get more *idle* battery life, but only from device-level knobs
|
||||
PPD deliberately omits — PCIe ASPM, disk/NVMe link PM, USB autosuspend,
|
||||
runtime PM — which are the same knobs behind NVMe/USB flakiness that
|
||||
pillar 1 (rock-stable, never fight your machine) guards against.
|
||||
**Evidence (2026-07-08):** no credible hard-watt benchmarks exist — TLP's
|
||||
own maintainer declines to quote any ("measure on your hardware") and says
|
||||
PPD's power-saver gives *similar* savings under load; TLP's edge is
|
||||
idle-only (linrunner.de/tlp/faq/ppd.html — PPD covers a *subset* of TLP,
|
||||
"no settings to reduce consumption when the CPU is idle"). Our sibling
|
||||
distro **Omarchy** ran this exact experiment — a "replace Power Profiles
|
||||
with TLP for battery" guide — and the author **reverted to PPD** ("more
|
||||
headaches and weird issues"); Omarchy shipped PPD auto-switching instead
|
||||
(basecamp/omarchy#3907). PPD 3.4.0 now does AC/battery auto-switching,
|
||||
closing part of TLP's UX gap. **Cost of TLP-default:** TLP has no live
|
||||
D-Bus profile API, so the `powermgmt` menu + Waybar profile indicator +
|
||||
low-battery auto power-saver (all `powerprofilesctl`) would need a rework —
|
||||
it's not a one-line backend swap. **Agent rec (evidence, not a decision):
|
||||
keep PPD default;** chase battery via targeted low-risk tweaks (PCIe ASPM
|
||||
policy, battery-side EPP, the charge *start* threshold, PPD auto-switching)
|
||||
and keep TLP the documented one-line opt-in it already is
|
||||
(`nomarchy.system.power.backend = "tlp"`). Options: (i) status quo +
|
||||
targeted tweaks [rec]; (ii) TLP default (reworks the profile UX);
|
||||
(iii) nothing.
|
||||
72
agent/CONVENTIONS.md
Normal file
72
agent/CONVENTIONS.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# 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`. That template is the
|
||||
single source of truth for machine files: `nomarchy-install` copies it
|
||||
and patches install-time values only (never a thinner second catalog).
|
||||
- **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** every `waybar.jsonc` whole-swap — summer-day, summer-night,
|
||||
executive-slate and boreal (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.
|
||||
60
agent/GOALS.md
Normal file
60
agent/GOALS.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Goals — what "done" looks like for Nomarchy
|
||||
|
||||
The north star every loop iteration serves. When two options conflict,
|
||||
the earlier pillar wins. Product *themes* toward a **v1.0** ship (day-2
|
||||
confidence, default identity, release bar) live in
|
||||
[`docs/VISION.md`](../docs/VISION.md) — still subordinate to these pillars.
|
||||
|
||||
## 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.
|
||||
384
agent/HARDWARE-QUEUE.md
Normal file
384
agent/HARDWARE-QUEUE.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# 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)
|
||||
- [ ] **#41 float classes (polkit / pinentry / portal / blueman)** — after
|
||||
rebuild: (1) `pkexec true` or mount a disk → polkit dialog should
|
||||
float+center; if not, note `hyprctl clients` class while open.
|
||||
(2) `gpg --sign` or any pinentry prompt → same. (3) App file picker
|
||||
via portal → capture class for residual rule. (4) blueman-manager /
|
||||
system-config-printer still float (`.…-wrapped` tolerance).
|
||||
- [ ] **#55 fingerprint enroll on real reader** — with
|
||||
`nomarchy.hardware.fingerprint.enable` and a physical reader: System ›
|
||||
Fingerprint › Enroll a finger; List shows it; Verify succeeds; optional
|
||||
Use for login (on) → `sys-rebuild` → login/sudo accept fingerprint.
|
||||
Menu surface is V1/V2 without a reader (self-gate + dry paths).
|
||||
- [ ] **#60 non-BAT* battery name (if available)** — on a machine whose
|
||||
system battery is **not** named `BAT*` (e.g. `CMB0`): confirm
|
||||
charge-limit oneshot writes the threshold, System › Battery limit
|
||||
and Power profile rows appear, Waybar power-profile module shows,
|
||||
doctor charge-limit section runs. On `BAT0`-only machines this is
|
||||
a no-op (already covered by existing charge-limit re-apply item).
|
||||
- [ ] **btop theme fidelity (#52 residual)** — softGL theme-shot cannot
|
||||
open Ghostty, so the hand `btop.theme` assets were only guest-file
|
||||
asserted (main_bg / inactive_fg keys) + desktop/bar rendered for
|
||||
rose-pine, everforest, summer-night, vantablack, catppuccin. On a
|
||||
real session: `nomarchy-theme-sync apply <slug>` then `btop` and
|
||||
confirm backgrounds/text match the theme (esp. rose-pine Dawn light
|
||||
bg `#faf4ed`, vantablack near-black `#0d0d0d`, catppuccin Mocha
|
||||
`#1E1E2E`). One pass cycling those five is enough.
|
||||
- [ ] **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.
|
||||
- [ ] **Notification inhibited-state glyph** (item 28 color-only sweep,
|
||||
iteration #69) — have an app hold a notification inhibitor (e.g. run
|
||||
a fullscreen video, or `busctl --user call org.freedesktop.Notifications
|
||||
… Inhibit`); the Waybar bell must switch to the muted **bell-off**
|
||||
(same glyph as DND), NOT the normal bell /. Then release it → the
|
||||
bell returns. Repeat on a whole-swap bar (summer-day/night use their
|
||||
own bell-off glyph, executive-slate/boreal use ). The glyph itself
|
||||
already renders (DND uses it); this only confirms swaync emits the
|
||||
`inhibited-*` class and the bar routes it.
|
||||
- [x] **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.
|
||||
— 2026-07-04 Bernardo: PASS enable/disable on hardware
|
||||
(off-survives-reboot + gamma-restore not explicitly checked).
|
||||
- [x] **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`.
|
||||
— 2026-07-04 Bernardo: PASS — enabled from the menu, `timedatectl`
|
||||
shows Europe/London (the manual set-timezone re-trigger wasn't
|
||||
exercised).
|
||||
- [ ] **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. — 2026-07-04 attempt:
|
||||
no comma layout was configured, so the bind wasn't rendered (the
|
||||
gate working as designed, not a failure) — retest after setting a
|
||||
comma layout + rebuild + relogin. The SUPER+? no-op found en
|
||||
route IS a real bug → BACKLOG item 26 (fix shipped — see the
|
||||
dedicated SUPER+? entry below).
|
||||
- [ ] **SUPER+? opens the cheatsheet** (item 32 re-fix — supersedes the
|
||||
item 26 attempt) — the bind is now `$mod SHIFT, slash` (the BASE
|
||||
keysym: Hyprland 0.55 resolves the sym with Shift consumed, so the
|
||||
old `question` keysym never matched while Shift was down — same
|
||||
pattern as the working `$mod SHIFT, 1` workspace binds). After
|
||||
`home-update` + relogin (or `hyprctl reload`), SUPER+? must open the
|
||||
keybindings cheatsheet, whose row still reads `SUPER + ?` (not
|
||||
`SUPER + SHIFT + ?`).
|
||||
- [ ] **swaync readable on summer-day** (item 25 fix) — on summer-day
|
||||
after `home-update`: `notify-send "title" "body text"` shows
|
||||
readable body text; open the control centre (SUPER+N), hover a
|
||||
notification row and check the Clear button — all text legible
|
||||
(body/buttons now @text on tinted chips, not subtext/surface).
|
||||
- [ ] **Bar + rofi legible on flexoki-light** (item 27 fix) — switch to
|
||||
flexoki-light: every Waybar module readable (window title, tray
|
||||
row, dimmed inactive workspaces / muted volume visible-but-dim),
|
||||
rofi inputbar/alternate rows show text. Spot-check one dark theme
|
||||
(e.g. tokyo-night) for no visual regression in the same spots.
|
||||
- [ ] **Stub-bar themes got the generated bar** (item 28, 90a5104) —
|
||||
switch to catppuccin, lumon, nord and retro-82: each now shows
|
||||
the generated styled bar in its own palette (workspace pill,
|
||||
padded right cluster) instead of the old raw default Waybar.
|
||||
Their rofi menus should look unchanged.
|
||||
- [ ] **Back-audit spot-check** (item 24) — the keybinds cheatsheet
|
||||
(SUPER+? / root menu › Keybindings) now ends in ↩ Back and
|
||||
returns to the root picker; spot a couple of submenus (Display,
|
||||
VPN › Tailscale) still Back correctly after the audit pass.
|
||||
- [ ] **Network menu has no blank rows** (item 22 fix, needs Wi-Fi) —
|
||||
after `home-update`, open Tools › Network: no empty separator
|
||||
rows between the ethernet/wifi/VPN sections (compact = True).
|
||||
KNOWN RESIDUAL: if exactly one nameless wifi row remains showing
|
||||
only security+bars, that's a hidden-SSID AP — config can't filter
|
||||
it; report it and the follow-up is a small source patch skipping
|
||||
empty-name APs in create_ap_list.
|
||||
- [ ] **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.
|
||||
- [x] **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. — 2026-07-04 Bernardo: PASS on the fixed build —
|
||||
polkit prompt appears, GUI opens as root, fzf fallback works.
|
||||
Restore/rollback not exercised → residual item below.
|
||||
- [ ] **Snapshots restore + rollback exercise** (residual from the item
|
||||
above — the GUI/polkit half passed 2026-07-04): in
|
||||
`sudo nomarchy-snapshots`, restore a single file (`undochange`)
|
||||
and walk a root-config rollback up to (or through) the
|
||||
typed-`yes` gate.
|
||||
- [ ] **Caffeine toggle (item 13 slice)** — click in the bar → turns
|
||||
(warm tint on the generated bar) and hypridle must NOT lock /
|
||||
blank while it's on (wait past the 5-min lock); click again
|
||||
releases. Also present on both summer bars now.
|
||||
- [ ] **Screen recording (item 12)** — Tools › Capture: Record region
|
||||
(slurp) and Record screen produce a playable .mp4 in
|
||||
~/Videos/Recordings; the red ⏺ REC appears in the bar instantly
|
||||
and CLICKING IT stops + notifies the path; the + audio variants
|
||||
carry sound. While recording, the Capture menu offers only
|
||||
"■ Stop recording". On the AMD box wl-screenrec (VAAPI) should
|
||||
do the work; if it dies at start the wf-recorder fallback kicks
|
||||
in silently — check the notification says which. Summer bars
|
||||
have the same ⏺ (parity).
|
||||
- [ ] **Doctor (item 10)** — menu › System › Doctor opens the sheet in
|
||||
a terminal; `nomarchy-doctor` over SSH shows the same minus user
|
||||
units. On a healthy machine everything is ✔/– (dev-box run
|
||||
2026-07-04 correctly flagged a genuinely failed user unit).
|
||||
- [ ] **Rollback menu (item 9b)** — after a couple of theme changes,
|
||||
menu › System › Rollback: recent desktop generations listed
|
||||
(newest marked current); picking an older one opens a terminal,
|
||||
activates it, and the theme visibly reverts; picking the newer one
|
||||
again rolls forward. The two System rows: Snapshots opens the
|
||||
snapshot flow, "boot an older generation (how)" fires an
|
||||
instruction notification.
|
||||
- [ ] **Open-a-file smoke (viewers + mime defaults, item 8)** — after
|
||||
`home-update`: from yazi/Thunar, open a PDF (→ zathura, themed to
|
||||
the palette), an image (→ imv, NOT GIMP), a video (→ mpv);
|
||||
`xdg-open .` on a directory → Thunar. With no browser installed,
|
||||
clicking a link must still fall through to *something sane* (the
|
||||
firefox.desktop entries are inert); if you've uncommented a
|
||||
browser, links go there.
|
||||
- [ ] **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.
|
||||
- [x] **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).
|
||||
— 2026-07-04 Bernardo: PASS — theme changes committed and the
|
||||
toggle self-committed.
|
||||
- [ ] **Display profiles, slice a (item 15)** — declare two
|
||||
`nomarchy.displayProfiles` (e.g. docked disables eDP-1 + arranges
|
||||
the externals; undocked re-enables it), `home-update`, then:
|
||||
System › Display › Profiles lists them (● marks active); applying
|
||||
is instant (no rebuild) and survives a relogin (the baked
|
||||
overlay); "Base layout" restores; a menu resolution pick made
|
||||
earlier must NOT re-enable a profile-disabled panel after the
|
||||
next rebuild.
|
||||
- [ ] **Display profiles workspace pins, slice c (item 15)** — give the
|
||||
docked profile `workspaces = { "1" = "<ext>"; "9" = "eDP-1"; }`:
|
||||
applying the profile moves open workspaces 1/9 to those outputs
|
||||
immediately, new visits land there too, and the pins survive a
|
||||
relogin (baked `workspace` rules). Switching to a profile without
|
||||
pins leaves workspaces where they are (stale session pins are
|
||||
expected until reload/rebuild — noted in the applier).
|
||||
- [ ] **Display profiles auto-switch, slice b (item 15)** — same setup,
|
||||
then menu › Profiles › Auto-switch on: plugging the dock/monitor
|
||||
applies the matching profile within ~3s (toast names it);
|
||||
unplugging switches back; Auto-switch off stops that; with two
|
||||
profiles naming the same outputs, no flapping (ties do nothing).
|
||||
- [ ] **P2 retunes eyeball (item 28b)** — spot-check on top of the P1
|
||||
entry below: gruvbox/nord/lumon/everforest/retro-82/white dimmed
|
||||
text (inactive workspaces, muted volume) now visible-but-dim;
|
||||
latte + rose-pine(dawn) + summer-day warn/battery tint reads on
|
||||
the light bases; flexoki-light statuses are the deeper canonical
|
||||
600s; latte selected-row/alt-accent pink darker. Anything that
|
||||
lost its "dim" feel → reopen 28b.
|
||||
- [ ] **Retuned palettes eyeball (item 28b P1)** — switch through
|
||||
summer-day, flexoki-light, kanagawa, miasma: chips/menus/toasts
|
||||
now draw on a *raised* surface (summer-day's was slate-on-cream,
|
||||
kanagawa's near-black, miasma's pure black), secondary text
|
||||
(tooltips, fastfetch labels) is visible on summer-day +
|
||||
flexoki-light, kanagawa floats are lighter than the bg (upstream
|
||||
sumiInk4). Anything that reads worse than before → reopen 28b.
|
||||
- [ ] **Clock zone tooltip (LATER item)** — hover the bar clock: the
|
||||
tooltip's first line shows the zone ("BST (UTC+0100)") above the
|
||||
calendar, on the generated AND summer bars; with auto-timezone
|
||||
on, a zone change updates it (the SIGUSR2 reload).
|
||||
- [ ] **Doctor bar tripwire (LATER item)** — with everything healthy
|
||||
the bar shows nothing; `systemctl --user start doomed`-style
|
||||
induced failure → within ~5 min a red appears (tooltip lists
|
||||
the ✖ lines), clicking opens the sheet in a terminal;
|
||||
reset-failed → it disappears on the next poll. Also visible on
|
||||
both summer bars.
|
||||
- [ ] **OCR region (LATER item)** — Tools › Capture › "OCR region →
|
||||
clipboard": select a region with visible text → toast reports
|
||||
the word count and `wl-paste` yields the text; Esc in slurp
|
||||
cancels silently; a text-free region toasts "No text recognized"
|
||||
without clobbering the clipboard.
|
||||
- [ ] **Look & Feel submenu (item 19)** — root menu: "Look & Feel"
|
||||
(replacing Theme, root still six rows) → Theme grid opens, Next
|
||||
wallpaper cycles instantly, Night light toggles (and is GONE
|
||||
from System). SUPER+T / SUPER+SHIFT+T direct binds unchanged.
|
||||
- [ ] **Launch-or-focus (item 17)** — uncomment the template's
|
||||
`nomarchy.launchOrFocus` firefox example (with firefox
|
||||
installed), `home-update` + reload: SUPER+B launches firefox
|
||||
when closed, FOCUSES the existing window when open (also from
|
||||
another workspace); with firefox removed the bind fires a
|
||||
"not installed" toast; the row shows under SUPER+?.
|
||||
- [ ] **Themed greeter + console (item 16)** — after `sys-rebuild` +
|
||||
logout: tuigreet renders in theme colors (dark container, accent
|
||||
border, themed prompt/time) and a raw tty (CTRL+ALT+F2) shows the
|
||||
theme's ANSI palette; on a LUKS machine the passphrase prompt
|
||||
follows too. Then switch theme + `sys-rebuild` + logout → the
|
||||
greeter follows the new palette. (tuigreet can't run under the
|
||||
VM harness — see MEMORY.md — so rendering is hardware-tier.)
|
||||
- [ ] **Color picker (item 13, final slice)** — SUPER+CTRL+P (or Tools ›
|
||||
Color picker): hyprpicker's zoom loupe appears; click a pixel →
|
||||
toast shows the hex and `wl-paste` yields it; Esc cancels with no
|
||||
toast. Row present in the SUPER+? cheatsheet.
|
||||
- [ ] **Low-battery toasts (item 13 slice)** — on battery, drain past
|
||||
25%: one "Battery low" toast; past 10%: a critical "Battery
|
||||
critical" toast that stays up until dismissed (swaync); plug in,
|
||||
drain again → it re-notifies; no repeat toasts while it just keeps
|
||||
draining. (Crossing logic VM-verified — this checks the real
|
||||
swaync rendering in a session.)
|
||||
|
||||
- [ ] **Battery charge-limit toggle** (iteration #55) — control-center
|
||||
(menu › System › Control Center › System Toggles › Battery Limit):
|
||||
the preset picker (Off / 80% / 90% / 60% / Custom…) writes the value
|
||||
(`nomarchy-theme-sync get settings.power.batteryChargeLimit` reflects
|
||||
the pick); after a `sys-rebuild` the sysfs
|
||||
`charge_control_end_threshold` reads it. (UX-only over the already
|
||||
VM-verified writer — a session sanity pass, not a deep check.)
|
||||
- [ ] **Waybar hides under fullscreen video** (item 30) — after
|
||||
`home-update` + relogin, put a browser video (YouTube) into
|
||||
fullscreen (F): the bar is now *covered* by the video, not drawn
|
||||
on top; exit fullscreen → the bar returns. Normal tiling
|
||||
unchanged (the bar still reserves its space). Accepted trade-off
|
||||
of `layer: bottom`: a floating window dragged over the top strip
|
||||
can now overlap the bar — confirm that's the only regression.
|
||||
- [ ] **Battery limit in rofi System** (iteration #64, item 36a) — after
|
||||
`home-update`: menu › System shows a "Battery limit" row (this laptop
|
||||
exposes the threshold node); picking a preset (80/90/60/Off/Custom)
|
||||
writes it (`nomarchy-theme-sync get settings.power.batteryChargeLimit`
|
||||
reflects the pick) and after a `sys-rebuild` the sysfs
|
||||
`charge_control_end_threshold` matches. The gum control-center no
|
||||
longer lists Battery Limit (moved, not duplicated).
|
||||
- [ ] **Config dialogs float** (iteration #63, item 41 cut) — after
|
||||
`home-update` + relogin: open Bluetooth (blueman-manager) and, if
|
||||
printing is on, System ▸ Printers (system-config-printer) — each
|
||||
opens floating + centered, not tiled. If either still tiles, run
|
||||
`hyprctl clients` while it's open, read the real `class`, and fix the
|
||||
regex. While there, capture the polkit prompt's and a GTK file
|
||||
dialog's class for the next 41 slice.
|
||||
- [ ] **Right-click volume → floating mixer** (iteration #62, item 35) —
|
||||
after `home-update` + relogin: right-click the Waybar volume module
|
||||
opens pwvucontrol as a floating, centered window (not tiled);
|
||||
left-click still mutes. Verify across a whole-swap theme too
|
||||
(summer/boreal/executive-slate) since the jsoncs got the same wiring.
|
||||
- [ ] **Window focus on arrows** (iteration #61) — after `home-update` +
|
||||
relogin: SUPER+←/→/↑/↓ move focus between tiled windows; SUPER+H/J/K/L
|
||||
no longer move focus; SUPER+? cheatsheet shows the arrow glyphs.
|
||||
- [ ] **Audio opens in Amberol** (iteration #60, item 37) — after
|
||||
`home-update`: double-click an mp3/flac/ogg (or `xdg-open song.mp3`)
|
||||
→ it opens in Amberol, not mpv; video files still open in mpv.
|
||||
(`xdg-mime query default audio/mpeg` → io.bassi.Amberol.desktop.)
|
||||
- [ ] **Capture-to-file keybinds** (iteration #59, item 38) — after
|
||||
`home-update` + relogin: SHIFT+Print prompts a region select then
|
||||
saves a PNG under ~/Pictures/Screenshots (toast shows the path);
|
||||
CTRL+Print saves the whole screen the same way; bare Print still
|
||||
copies a region to the clipboard. Both new rows appear in SUPER+?.
|
||||
Also (iteration #66): Tools ▸ Capture now shows the matching dim key
|
||||
hint on the Region → clipboard / Region → file / Full screen → file
|
||||
rows (Print / SHIFT + Print / CTRL + Print) — pango renders, no raw
|
||||
`<span>` leaks.
|
||||
- [ ] **rofi menu polish** (iteration #58, item 39 + #56/#57 34/40) —
|
||||
after `home-update`: (a) menu › System › Power profile shows a
|
||||
colored icon per profile (performance/balanced/power-saver), not
|
||||
just text — icons *render* (not blank); (b) every submenu's ↩ Back
|
||||
shows a single arrow, no double; (c) on this single-monitor box,
|
||||
System › Display → pick a resolution → Back returns to System
|
||||
(not back into the same resolution list).
|
||||
- [ ] **Combined power menu** (iteration #67, item 36b) — after
|
||||
`home-update` + relogin: clicking EITHER the Waybar battery icon OR
|
||||
the power-profile icon opens one "Power" menu listing Profile: rows
|
||||
+ Charge limit: rows; picking a profile calls `powerprofilesctl set`
|
||||
(verify `powerprofilesctl get` changes), picking a charge preset
|
||||
writes `settings.power.batteryChargeLimit` (applies next rebuild).
|
||||
The prompt shows the current profile + limit. System ▸ Power profile
|
||||
/ Battery limit still work directly; the whole-swap bars (summer-day/
|
||||
night, executive-slate, boreal) behave the same and their shutdown
|
||||
power-button (if present) still opens the lock/logout menu.
|
||||
|
||||
- [ ] **Calendar on the clock click** (iteration #68, item 42) — after
|
||||
`home-update` + relogin: left-click the Waybar date/clock → calcurse
|
||||
opens in a **floating, centered** ghostty window (~60×65% of screen),
|
||||
showing the month calendar; closing calcurse (`q`) closes the window.
|
||||
The `--class=com.nomarchy.calendar` must match the windowrule (if it
|
||||
tiles, run `hyprctl clients` while open and check the real app-id).
|
||||
Hover the clock → tooltip shows the long date + zone + month grid.
|
||||
Same on a whole-swap bar (summer/boreal/executive-slate). If calcurse
|
||||
was removed from home.packages, the click toasts "calcurse isn't
|
||||
installed" instead of doing nothing.
|
||||
- [ ] **Screenshot annotation (satty)** (iteration #73) — after `home-update`:
|
||||
hit SUPER+SHIFT+Print (or Tools ▸ Capture ▸ Annotate region) → a region
|
||||
select (slurp) appears, then the `satty` UI opens in fullscreen with the
|
||||
screenshot loaded. Check that the UI draws on the current theme palette
|
||||
(tools colored properly) and hitting save places the screenshot in
|
||||
`~/Pictures/Screenshots/` while hitting copy places it in the clipboard.
|
||||
|
||||
## 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.
|
||||
- [ ] **System ▸ Firmware menu on real LVFS hardware** (item #43,
|
||||
`nomarchy-menu firmware`) — on a machine whose firmware/SSD/dock is
|
||||
on LVFS: open Menu ▸ System ▸ **Firmware**; confirm the terminal runs
|
||||
`fwupdmgr refresh` → lists real `get-updates` → the y/N confirm gates
|
||||
correctly → `fwupdmgr update` applies and prints the reboot note when a
|
||||
capsule needs one. Verify **no** flash happens on "N"/cancel. (VM only
|
||||
proves the menu row + flow renders; a real capsule write is
|
||||
hardware-only.)
|
||||
|
||||
## Latitude 5410 only
|
||||
- [x] **Waybar theme-switch resilience** (finding #1 re-test, needs main ≥
|
||||
the supervisor commit + relogin) — switch themes repeatedly (incl.
|
||||
summer-day/night whole-swaps): the bar restarts cleanly each time;
|
||||
if anything kills it, it's back within ~a second. `pgrep -f
|
||||
nomarchy-waybar` shows the supervisor. — 2026-07-04 Bernardo:
|
||||
PASS — theme switches clean; pkill respawn so fast the bar never
|
||||
visibly disappears.
|
||||
- [x] **Media keys + gestures** (from dccceb4) — volume/brightness keys
|
||||
drive the OSD; touchpad gestures work. — 2026-07-04 Bernardo:
|
||||
PASS, volume/brightness keys and touchpad gestures all working.
|
||||
- [ ] **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.
|
||||
- [ ] **Portal/Flatpak libcamera IR (b)/(c)** — after #71 docs: on hardware,
|
||||
confirm a Flatpak/portal picker still lists the internal IR node;
|
||||
only then investigate (b) WirePlumber libcamera GREY-only monitor
|
||||
rule or (c) libcamera/udev-layer hide. Do not ship either without a
|
||||
live dual-sensor check (see HARDWARE.md §7 / ROADMAP § Webcam).
|
||||
1993
agent/JOURNAL.md
Normal file
1993
agent/JOURNAL.md
Normal file
File diff suppressed because it is too large
Load Diff
151
agent/LOOP.md
Normal file
151
agent/LOOP.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# 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) |
|
||||
|
||||
## Model & token economy
|
||||
|
||||
Spend expensive tokens on judgment, not mechanics.
|
||||
|
||||
- **Plan and reason on the strong model.** Orientation, task selection,
|
||||
design, debugging, Nix eval semantics, verification judgment, and
|
||||
anything that would land in a commit unreviewed stay with the
|
||||
top-tier model running the loop (Fable 5).
|
||||
- **Delegate mechanical subtasks to cheaper models.** When a subtask is
|
||||
fully specified and needs no design judgment — grep/audit sweeps,
|
||||
README-option-table reconciliation, a repeated edit applied across
|
||||
files, summarizing long logs or check output — hand it to a subagent
|
||||
with a `model` override: `haiku` for search/summarize/audit, `sonnet`
|
||||
for routine well-specified edits. The strong model writes the spec,
|
||||
reviews the result, and owns the commit. Only delegate when writing
|
||||
the spec is cheaper than doing the work — a spawned agent starts cold
|
||||
and must re-derive context.
|
||||
- **Read narrowly.** Step 0's list is the whole orientation read (the
|
||||
*last 3–5 entries* of the journal, never the full file). Read large
|
||||
files by section, don't re-read what's already in context, and tail
|
||||
build/check logs instead of dumping them.
|
||||
- **Write tersely.** Journal entries follow the template and no more;
|
||||
commit bodies state what/why/tier, not a narrative.
|
||||
- **Headless runners** may run whole low-stakes iterations (QA sweeps,
|
||||
docs-drift passes) on a cheaper `--model`; iterations touching
|
||||
`modules/` or `pkgs/` behavior keep the strong model.
|
||||
|
||||
## 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`. If the top task is product-shaped
|
||||
(UX, release bar, day-2 confidence), also read the matching section of
|
||||
**`docs/VISION.md`** — do not invent work from VISION; only execute
|
||||
BACKLOG items. Map of docs vs agent state: `docs/README.md`,
|
||||
`agent/README.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.
|
||||
- Mechanical, fully-specified sub-steps go to a cheaper model
|
||||
(see *Model & token economy* above); design and review stay here.
|
||||
- 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.
|
||||
141
agent/MEMORY.md
Normal file
141
agent/MEMORY.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 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
|
||||
- **theme-shot softGL cannot start Ghostty** — `btop.png` is best-effort
|
||||
(usually identical to desktop). Guest asserts on
|
||||
`~/.config/btop/themes/nomarchy.theme` prove baking; the TUI look is
|
||||
hardware/GL tier (HARDWARE-QUEUE).
|
||||
- **Waybar `custom/doctor` tripwire:** status helper must invoke
|
||||
`nomarchy-doctor` by **absolute store path** (waybar's env can miss
|
||||
system PATH → `command -v … || exit 0` self-hides forever); empty
|
||||
`"text"` also self-hides; strip ANSI before packing the tooltip; use
|
||||
signal 10 + `format = "{}"`. theme-shot asserts class:bad + glyph and
|
||||
pokes RTMIN+10 before the desktop shot.
|
||||
- **tuigreet dies silently under runNixOSTest** (even bare, no theme
|
||||
flag: greetd sits as "(greetd)" with no child, nothing in the
|
||||
journal — its stderr goes to the VT) — nixpkgs' own greetd test uses
|
||||
agreety instead. Greeter *rendering* is interactive-ISO/hardware
|
||||
tier; don't burn another session on a checks.greeter VM test.
|
||||
- In VM tests `pgrep -f PATTERN` can match the test backdoor's own
|
||||
`bash -c` wrapper (the pattern is in its cmdline) — use `pgrep -x`
|
||||
or a `[t]uigreet`-style bracket pattern.
|
||||
- A checks.* fixture CANNOT be a writeText/toFile state file read at
|
||||
eval time ("path … is not valid" — flake check's eval store won't
|
||||
realise it): extract the logic into a pure importable file and
|
||||
unit-test THAT (monitor-rules.nix / checks.display-profiles is the
|
||||
pattern).
|
||||
- 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 Gitea instance is **1.25.4** — `on: schedule` workflows are
|
||||
supported; bump.yml assumes the Actions token can push to `main`
|
||||
(standard Gitea behaviour, but unconfirmed until the first run lands).
|
||||
- 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).
|
||||
- **Friendly theme-state load** (`modules/theme-state-read.nix`, #66):
|
||||
`builtins.tryEval` does **not** catch `readFile`/`fromJSON` failures —
|
||||
gate with `pathExists` + empty/non-object checks before `fromJSON`.
|
||||
Subtle JSON syntax errors still surface from nlohmann (line/col);
|
||||
field schema stays in `theme.nix`. mkFlake must `builtins.seq` the
|
||||
check onto the whole return set or lazy attr access skips it.
|
||||
|
||||
## Design invariants
|
||||
- **Waybar status is never color-only** (item 28 sweep, iteration #69):
|
||||
every status module must distinguish its states by SHAPE (glyph) or
|
||||
presence (self-hide), never color alone — good/warn/bad collapse under
|
||||
color-blindness. When adding a state, give it a distinct glyph or gate
|
||||
the module on it; a new `class` that only recolors an existing glyph is
|
||||
a regression. Suppressed notification states (DND *and* app-inhibited)
|
||||
all use the bell-off glyph + @muted.
|
||||
- **Identity themes are not traffic lights** (#69): white, vantablack,
|
||||
lumon, hackerman, matte-black, miasma — monochrome / mono-hue / earthy
|
||||
status by design. `audit-theme-design.py` tags their hue/CVD/ANSI-family
|
||||
findings `[identity]`; do not "fix" them into R/Y/G.
|
||||
- **Import hierarchy ≠ ANSI** (#70): `import-palettes.py` must not set
|
||||
surface==overlay when color0==color8; light color0 is often ANSI black
|
||||
(not a chip). Roles are first-class — never bulk-reimport shipped JSON
|
||||
without a hierarchy pass.
|
||||
|
||||
## Gotchas (cost a debugging session once)
|
||||
- Waybar `layer: top` renders above **even real-fullscreen windows** — the
|
||||
bar draws over a fullscreen video. `layer: bottom` lets the fullscreen
|
||||
surface cover it while the exclusive zone still reserves the bar's space
|
||||
in normal tiling (trade-off: floating windows can now overlap the bar
|
||||
strip). Set in both waybar.nix and every whole-swap jsonc (item 30).
|
||||
- Hyprland binds match the exact modmask: a shifted keysym (`question`)
|
||||
needs SHIFT in `mods` or the bind never fires — the keypress falls
|
||||
through to the focused window (§ item 26; caught on hardware, invisible
|
||||
to eval-tier tests).
|
||||
- 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).
|
||||
- Hyprland 0.53 rewrote window rules: `windowrulev2` is a hard error and
|
||||
the old rule-first `float, class:^…$` no longer parses — both surface a
|
||||
red config-error banner on the default desktop. Hyprlang legacy form is
|
||||
now `<effect> <value>, match:<prop> ^…$` (e.g. `float 1, match:class ^…$`);
|
||||
effects carry a value, matchers take `match:` (§ windowrule migration).
|
||||
- grub `loadfont`s every `.pf2` in a theme dir — reuse a bundled DejaVu
|
||||
rather than shipping fonts (§ Distro branding).
|
||||
- `.claude/skills/*/SKILL.md` are now **tracked** (the `.gitignore`
|
||||
`.claude/skills/` line was un-commented→removed, 7d52d4b) — commit skill
|
||||
edits like any repo doc. Still never `git add -A` blindly: check
|
||||
`git status --short` for genuine strangers first (`settings.local.json`,
|
||||
harness-dropped files) and commit with explicit pathspecs (§ loop hygiene).
|
||||
42
agent/README.md
Normal file
42
agent/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Agent loop state
|
||||
|
||||
Git-tracked state for autonomous and assisted work on Nomarchy.
|
||||
Protocol: **[LOOP.md](LOOP.md)**. Entry for most harnesses: repo-root
|
||||
**[CLAUDE.md](../CLAUDE.md)**.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Who writes | Role |
|
||||
|------|------------|------|
|
||||
| [LOOP.md](LOOP.md) | Human | One-iteration protocol (orient → pick → work → verify → commit → record) |
|
||||
| [GOALS.md](GOALS.md) | Human (agents propose) | Pillars, quality bars, non-goals |
|
||||
| [CONVENTIONS.md](CONVENTIONS.md) | Human (agents propose) | How to write code/menu/state while shipping |
|
||||
| [BACKLOG.md](BACKLOG.md) | Both | **Prioritized queue** — only executable work list |
|
||||
| [JOURNAL.md](JOURNAL.md) | Agents | Append-only iteration log (read last 3–5 entries) |
|
||||
| [MEMORY.md](MEMORY.md) | Agents | Curated durable gotchas |
|
||||
| [HARDWARE-QUEUE.md](HARDWARE-QUEUE.md) | Agents append, human checks | On-hardware V3 tests only Bernardo can run |
|
||||
|
||||
## Product / design docs (not a queue)
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| [../docs/VISION.md](../docs/VISION.md) | v1.0 product themes — agents slice into BACKLOG PROPOSED |
|
||||
| [../docs/ROADMAP.md](../docs/ROADMAP.md) | Design history + shipped log |
|
||||
| [../docs/README.md](../docs/README.md) | Full docs map |
|
||||
|
||||
## Claude Code only
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| [../.claude/settings.json](../.claude/settings.json) | Tool permissions |
|
||||
| [../.claude/agents/](../.claude/agents/) | `nomarchy-scout` / `nomarchy-runner` subagent defs |
|
||||
|
||||
Do **not** put backlog items or vision text under `.claude/` — it is not
|
||||
shared with other agent runners.
|
||||
|
||||
## Rules of thumb
|
||||
|
||||
1. **Execute** from BACKLOG only (NOW → NEXT; never PROPOSED without human triage).
|
||||
2. **Orient** with GOALS + CONVENTIONS + MEMORY + last journal + BACKLOG; when the task is product-shaped, also read the relevant **VISION §**.
|
||||
3. **Record** lasting design in ROADMAP ✓ when something ships that future humans should know; delete the BACKLOG line.
|
||||
4. **v1 branch** is human-only — never advance from an agent session.
|
||||
436
docs/HARDWARE.md
Normal file
436
docs/HARDWARE.md
Normal file
@@ -0,0 +1,436 @@
|
||||
# Hardware support
|
||||
|
||||
How Nomarchy enables CPUs, GPUs, laptops, firmware, and peripherals — and
|
||||
what to do when your machine is not in the happy path.
|
||||
|
||||
> **Queue:** [`agent/BACKLOG.md`](../agent/BACKLOG.md) (PROPOSED › Hardware
|
||||
> product). Product framing: [`VISION.md`](VISION.md) § A. Design history:
|
||||
> [`ROADMAP.md`](ROADMAP.md). Docs map: [`README.md`](README.md).
|
||||
> Migration: [`MIGRATION.md`](MIGRATION.md).
|
||||
|
||||
## 1. Architecture (three layers)
|
||||
|
||||
Nomarchy does **not** reimplement [nixos-hardware](https://github.com/NixOS/nixos-hardware).
|
||||
It stacks:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ A. Always-on desktop floor │
|
||||
│ modules/nixos/default.nix + power.nix + audio/BT │
|
||||
│ redistributable firmware · fwupd · NM · PipeWire · PPD · ddcci │
|
||||
└───────────────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────────────▼─────────────────────────────────────┐
|
||||
│ B. nixos-hardware profiles (via mkFlake hardwareProfile) │
|
||||
│ common-cpu-* · common-gpu-* · common-pc(-laptop|-ssd) · model │
|
||||
│ microcode · GPU media · fstrim · vendor/model quirks │
|
||||
└───────────────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────────────▼─────────────────────────────────────┐
|
||||
│ C. nomarchy.hardware.* (modules/nixos/hardware.nix) │
|
||||
│ gap above commons: GuC · amd-pstate · VA-API env · fprintd · │
|
||||
│ IR-webcam hide · ROCm/NPU/latestKernel · I2C/DDC/CI │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| Layer | Who turns it on | Where it lives |
|
||||
|-------|-----------------|----------------|
|
||||
| **A** | Distro defaults (`mkDefault`) | Any machine importing `nomarchy.nixosModules.nomarchy` |
|
||||
| **B** | Installer DMI/`lspci` → `hardwareProfile = [ … ]` | Downstream `flake.nix` (`mkFlake`) |
|
||||
| **C** | Installer probes → `system.nix`, or hand-edit | Downstream `system.nix` |
|
||||
|
||||
`hardware-configuration.nix` (from `nixos-generate-config`) still owns
|
||||
initrd modules, filesystems, and the usual generate-config flags. The
|
||||
installer writes a real one; the template ships a placeholder you must
|
||||
replace.
|
||||
|
||||
Unknown `hardwareProfile` names **fail at eval** with Levenshtein
|
||||
suggestions (`lib.nix`) — never silently ignored.
|
||||
|
||||
## 2. What works without thinking about it
|
||||
|
||||
These ship on every Nomarchy system unless you override them:
|
||||
|
||||
| Capability | Default | Notes |
|
||||
|------------|---------|--------|
|
||||
| Wi‑Fi / BT firmware blobs | `hardware.enableRedistributableFirmware` | iwlwifi, ath, rtw, brcm, SOF, … |
|
||||
| NetworkManager | on | Wi‑Fi UI: System › Network |
|
||||
| PipeWire + WirePlumber | on | |
|
||||
| Bluetooth + blueman | on | System › Bluetooth |
|
||||
| power-profiles-daemon | on | Menu + Waybar; TLP via `nomarchy.system.power.backend = "tlp"` |
|
||||
| External monitor brightness | `nomarchy.hardware.i2c.ddcci` **on** | DDC/CI → backlight devices |
|
||||
| Firmware updates (LVFS) | `services.fwupd.enable` | **Never auto-flashes** — see §4 |
|
||||
| SMART / UPower / pcscd (FIDO) | on | |
|
||||
| earlyoom | on | Process-level OOM, not cgroup kill of the whole session |
|
||||
|
||||
**Not** default-on (need detect or uncomment): fprintd, GuC/HuC,
|
||||
amd-pstate, ROCm, NPU, thermald, charge limit, snapper (installer enables
|
||||
snapper), `latestKernel`.
|
||||
|
||||
## 3. Install path (best experience)
|
||||
|
||||
`nomarchy-install` sources `pkgs/nomarchy-install/hardware-db.sh` and:
|
||||
|
||||
1. Probes CPU vendor, GPUs (`lspci`), battery, SSD, fingerprint USB VIDs,
|
||||
RGB+IR webcam names, NPU PCI class.
|
||||
2. Looks up DMI `sys_vendor` × `product_name` in a ~60-entry model table
|
||||
(Framework, Dell XPS/Latitude, ThinkPad, Surface, ASUS ROG, Apple T2,
|
||||
System76).
|
||||
3. Emits `MODULE …` lines → `hardwareProfile` list in `flake.nix`.
|
||||
4. Emits `NOMARCHY hardware.*=…` → active + commented blocks in
|
||||
`system.nix`.
|
||||
5. Asks you to confirm profiles (or pick manually from the full
|
||||
nixos-hardware attr list baked into the ISO). Override with
|
||||
`NOMARCHY_HW=auto|none|"mod1 mod2"`.
|
||||
|
||||
**Live ISO** is intentionally *generic* (no model profile): broad GPU
|
||||
modules as *available*, redistributable firmware on, no baked
|
||||
`nomarchy.hardware.*`. Tuning happens at install time.
|
||||
|
||||
Out of scope for the installer today: aarch64 (Pi, Snapdragon X), Steam
|
||||
Deck (Jovian), Apple Silicon (T2 Intel Macs only in the DB).
|
||||
|
||||
## 4. Firmware updates (deep dive)
|
||||
|
||||
### What we ship
|
||||
|
||||
```nix
|
||||
# modules/nixos/default.nix
|
||||
services.fwupd.enable = lib.mkDefault true;
|
||||
```
|
||||
|
||||
fwupd talks to the [LVFS](https://fwupd.org/): UEFI capsule (BIOS), some
|
||||
SSDs, docks, Thunderbolt controllers, peripherals. It **only refreshes
|
||||
metadata** on its own. Applying an update is always an explicit user
|
||||
action.
|
||||
|
||||
### What you do today (CLI)
|
||||
|
||||
```sh
|
||||
# After first boot (and occasionally later):
|
||||
fwupdmgr refresh # optional; daemon often has metadata
|
||||
fwupdmgr get-devices # what LVFS can see
|
||||
fwupdmgr get-updates # pending
|
||||
fwupdmgr update # apply (may need reboot / battery / AC)
|
||||
```
|
||||
|
||||
Disable on VMs/headless: `services.fwupd.enable = false;` in `system.nix`.
|
||||
|
||||
### Why this is under-discovered
|
||||
|
||||
| Present | Missing |
|
||||
|---------|---------|
|
||||
| Daemon + package | Doctor check (“updates available”) |
|
||||
| README one-liner | Waybar / updates panel integration |
|
||||
| **System ▸ Firmware menu** (`nomarchy-menu firmware`) | |
|
||||
| MOTD + first-boot tip (#43) | |
|
||||
|
||||
**System ▸ Firmware** (shipped): a self-gated System-submenu row (present
|
||||
whenever `fwupdmgr` is on PATH, i.e. `services.fwupd.enable`, default-on)
|
||||
that opens a terminal and runs `fwupdmgr refresh` → `get-updates` →
|
||||
confirm → `fwupdmgr update`. It never auto-flashes — `fwupdmgr update`
|
||||
confirms each device and prompts for the reboot a capsule needs; pillar 1
|
||||
(rock-stable) forbids silent BIOS writes.
|
||||
|
||||
**Hints (#43 / #73):** MOTD cheat-sheet line + `nomarchy-control-center
|
||||
--first-boot` tip when `fwupdmgr` is on PATH. Fingerprint / doctor tips
|
||||
follow the same pattern (fingerprint MOTD only when
|
||||
`nomarchy.hardware.fingerprint.enable`).
|
||||
|
||||
**Still queued:** a Doctor “updates available” check, and
|
||||
Waybar/updates-panel integration.
|
||||
|
||||
### Thunderbolt
|
||||
|
||||
Firmware for TB devices may appear via LVFS. There is **no** first-class
|
||||
`services.hardware.bolt` / security-level UI in Nomarchy — use plain
|
||||
NixOS options if you need bolt.
|
||||
|
||||
### Microcode / kernel firmware
|
||||
|
||||
- **CPU microcode:** from nixos-hardware `common-cpu-*` and
|
||||
`nixos-generate-config` once redistributable firmware is on — not a
|
||||
separate `nomarchy.*` toggle.
|
||||
- **GPU GuC/HuC (Intel i915):** `nomarchy.hardware.intel.guc` →
|
||||
`i915.enable_guc=3`. Installer turns this **off** when the bound driver
|
||||
is `xe` (GuC is default there; the param is ignored).
|
||||
- **`hardware.enableAllFirmware`:** not set. Oddball non-redistributable
|
||||
Wi‑Fi still needs a manual NixOS fix (or a broader policy decision).
|
||||
|
||||
## 5. Fingerprint (deep dive)
|
||||
|
||||
### Detect → enable
|
||||
|
||||
Installer scans USB vendor IDs common to libfprint (Goodix, Synaptics,
|
||||
Elan, …) via `lsusb` or `/sys/bus/usb/.../idVendor`. On hit:
|
||||
|
||||
```nix
|
||||
nomarchy.hardware.fingerprint.enable = true; # services.fprintd
|
||||
# nomarchy.hardware.fingerprint.pam = true; # login + sudo (opt-in)
|
||||
```
|
||||
|
||||
### Enroll (menu or CLI)
|
||||
|
||||
**Shipped #55:** System › Fingerprint (self-gated when `fprintd-list` is
|
||||
on PATH) — Enroll / List / Verify / Delete all, plus **Use for login**
|
||||
which writes `settings.fingerprint.pam` and applies on the next
|
||||
`sys-rebuild` (option default follows theme-state.json).
|
||||
|
||||
**Hints (#73):** MOTD line when `fingerprint.enable` is on; first-boot
|
||||
tip when `fprintd-list` is on PATH (`SUPER+M → System › Fingerprint` /
|
||||
`fprintd-enroll`). No permanent nag without a reader.
|
||||
|
||||
```sh
|
||||
# CLI still works:
|
||||
fprintd-enroll
|
||||
fprintd-list "$USER"
|
||||
# or: System › Fingerprint › Use for login (on) → sys-rebuild
|
||||
```
|
||||
|
||||
PAM stays opt-in on purpose: password-only remains the cautious default
|
||||
until a finger is enrolled. Full enroll on a real reader is V3/hardware.
|
||||
|
||||
### Doctor
|
||||
|
||||
`nomarchy-doctor` reports fprintd unit + enroll status when present.
|
||||
|
||||
## 6. NVIDIA (deep dive)
|
||||
|
||||
### What happens at install
|
||||
|
||||
```
|
||||
lspci → NVIDIA ⇒ MODULE common-gpu-nvidia
|
||||
```
|
||||
|
||||
That is the **entire** Nomarchy surface. Unfree packages are allowed
|
||||
(`allowUnfree = true` in the module and in `mkFlake`), so the proprietary
|
||||
path from nixos-hardware can evaluate.
|
||||
|
||||
There is **no** `nomarchy.hardware.nvidia.*` for:
|
||||
|
||||
- hybrid / PRIME (Intel+NVIDIA, AMD+NVIDIA)
|
||||
- open vs closed kernel module
|
||||
- power management / udev / suspend quirks
|
||||
- CUDA / container toolkit packages
|
||||
|
||||
### What a hybrid laptop user must do today
|
||||
|
||||
1. Confirm `common-gpu-nvidia` (and often `common-gpu-intel` or
|
||||
`common-gpu-amd`) are in `hardwareProfile`.
|
||||
2. Read the relevant nixos-hardware module and [NixOS NVIDIA wiki](https://wiki.nixos.org/wiki/Nvidia).
|
||||
3. Add plain NixOS options in `system.nix`, for example (illustrative —
|
||||
hardware-specific):
|
||||
|
||||
```nix
|
||||
# Example only — verify against your generation and nixos-hardware module.
|
||||
# hardware.nvidia.prime = { ... };
|
||||
# hardware.nvidia.powerManagement.enable = true;
|
||||
# hardware.nvidia.open = false; # or true for open modules on newer cards
|
||||
```
|
||||
|
||||
4. Rebuild the system. Home Manager does not own the driver.
|
||||
|
||||
### Live ISO note
|
||||
|
||||
The ISO lists `nouveau` among *available* initrd modules and does **not**
|
||||
force-load every GPU driver (avoids multi-driver panics). Proprietary
|
||||
NVIDIA on the live session is not the product focus; installed systems
|
||||
use the profile.
|
||||
|
||||
**Product direction (shipped #59):** when `common-gpu-nvidia` is in
|
||||
`hardwareProfile`, the installer emits a **commented** `system.nix` block
|
||||
with PRIME / power / open-module pointers (same pattern as ROCm / NPU
|
||||
comments). A full `nomarchy.hardware.nvidia.*` stack stays optional and
|
||||
needs a maintainer + hardware-queue coverage.
|
||||
|
||||
## 7. Day-2: “my laptop is mostly fine, make it fully fine”
|
||||
|
||||
| Goal | How | Rebuild? |
|
||||
|------|-----|----------|
|
||||
| Power profile (perf/balanced/saver) | System menu / Waybar | No (PPD D-Bus) |
|
||||
| Charge limit 80% | Menu / `settings.power.batteryChargeLimit` | System (sysfs apply is separate backlog) |
|
||||
| Thermald (Intel) | Installer on for Intel laptops; else `power.thermal.enable` | System |
|
||||
| Fingerprint enroll | `fprintd-enroll` | No |
|
||||
| Fingerprint login | `fingerprint.pam = true` | System |
|
||||
| ROCm / Intel compute | Uncomment opt-ins in `system.nix` | System (large) |
|
||||
| NPU driver | Uncomment `npu` (+ often `latestKernel`) | System; userspace BYO |
|
||||
| Newer kernel for brand-new silicon | `nomarchy.hardware.latestKernel = true` | System |
|
||||
| Hide IR “dark camera” | Installer or `camera.hideIrSensor` | System |
|
||||
| Firmware | `fwupdmgr update` | Reboot if capsule requires |
|
||||
| Model quirks missing | Set better `hardwareProfile` (see §8) | System |
|
||||
| OpenRGB / printing / Steam | `nomarchy.services.*` | System |
|
||||
|
||||
### Dual-sensor webcam / IR hide
|
||||
|
||||
Many dual-sensor modules (e.g. ThinkPad T14s) expose colour + IR as two
|
||||
identically named “Integrated Camera” nodes. Picking the IR node yields a
|
||||
black/dark greyscale frame — the classic “my webcam is dark” symptom.
|
||||
|
||||
`nomarchy.hardware.camera.hideIrSensor` (installer-on when RGB+IR names
|
||||
are detected; overridable `irMatch`) drops the IR node from **WirePlumber’s
|
||||
v4l2 monitor** so native PipeWire pickers only offer the colour camera.
|
||||
The kernel `/dev/video*` node stays open, so Howdy-style face unlock still
|
||||
works. Design history: [ROADMAP § Webcam](ROADMAP.md).
|
||||
|
||||
**What it does not cover:** apps that list cameras via **libcamera** or
|
||||
the **xdg-desktop-portal** / **Flatpak** camera path still see both
|
||||
sensors — a Flatpak Zoom (or similar) user can still pick the black IR
|
||||
“camera”. That is intentional: a blanket libcamera disable would risk
|
||||
external USB cams that need the libcamera path, and surgical internal-only
|
||||
libcamera rules could not match early enough (only `device.api` binds
|
||||
before the WirePlumber monitor rule).
|
||||
|
||||
**Workaround:** prefer the colour device in the picker (or any non-IR
|
||||
name). Apps that default to the first v4l2 colour source without a picker
|
||||
are fine.
|
||||
|
||||
**Further engineering (needs T14s-class hardware):** (b) a WirePlumber
|
||||
*libcamera* monitor rule disabling GREY-only nodes; (c) a libcamera/udev
|
||||
quirk at the libcamera layer. Neither is implemented — the recommended
|
||||
path is document-only until those can be verified on real dual-sensor
|
||||
hardware (see `agent/HARDWARE-QUEUE.md` › T14s).
|
||||
|
||||
Theme switches never touch drivers (Home Manager only).
|
||||
|
||||
## 8. Unsupported or unlisted machines
|
||||
|
||||
### Still good floor
|
||||
|
||||
Even with **no** DMI hit you usually get:
|
||||
|
||||
- `common-cpu-{intel,amd}`
|
||||
- `common-gpu-{intel,amd,nvidia}` as applicable
|
||||
- `common-pc` or `common-pc-laptop` + optional `common-pc-ssd`
|
||||
- `nomarchy.hardware.intel` or `.amd` when vendor matches
|
||||
- Layer A (firmware, audio, BT, PPD, fwupd)
|
||||
|
||||
You **lose** model-specific EC/suspend/keyboard/audio quirks that only
|
||||
exist in a named nixos-hardware module.
|
||||
|
||||
### Pick a profile after install
|
||||
|
||||
1. List candidates:
|
||||
|
||||
```sh
|
||||
nix eval github:NixOS/nixos-hardware#nixosModules \
|
||||
--apply builtins.attrNames
|
||||
# or: the ISO ships hardware-modules.txt for the same list
|
||||
```
|
||||
|
||||
2. Set in your flake (installer already uses a list):
|
||||
|
||||
```nix
|
||||
hardwareProfile = [
|
||||
"common-cpu-amd"
|
||||
"common-gpu-amd"
|
||||
"common-pc-laptop"
|
||||
"common-pc-ssd"
|
||||
"lenovo-thinkpad-t14-amd-gen3" # if it matches
|
||||
];
|
||||
```
|
||||
|
||||
3. `sudo nixos-rebuild switch --flake ~/.nomarchy#default`
|
||||
|
||||
### Template / migration (no installer)
|
||||
|
||||
See [MIGRATION.md](MIGRATION.md): reuse `hardware-configuration.nix`, set
|
||||
`hardwareProfile`, uncomment `nomarchy.hardware` / power in `system.nix`.
|
||||
Post-install re-probe (**shipped #58**):
|
||||
|
||||
```sh
|
||||
nomarchy-detect-hw # human report + suggested snippets
|
||||
nomarchy-detect-hw --raw # MODULE / NOMARCHY / DETAIL protocol lines
|
||||
```
|
||||
|
||||
Prints suggested `hardwareProfile` and `system.nix` lines; **does not**
|
||||
rewrite the flake. Paste after review, then `sudo nixos-rebuild switch`.
|
||||
|
||||
## 9. Adding your model to the distro
|
||||
|
||||
For contributors (and power users who will PR):
|
||||
|
||||
1. On the machine:
|
||||
|
||||
```sh
|
||||
cat /sys/class/dmi/id/sys_vendor
|
||||
cat /sys/class/dmi/id/product_name
|
||||
```
|
||||
|
||||
2. Find a matching attr in
|
||||
[nixos-hardware](https://github.com/NixOS/nixos-hardware) (or add one
|
||||
upstream first).
|
||||
|
||||
3. Append to `pkgs/nomarchy-install/hardware-db.sh` in `HARDWARE_DB`:
|
||||
|
||||
```bash
|
||||
"VendorRegex|Product regex|nixos-hardware-module-name"
|
||||
```
|
||||
|
||||
First match wins — put specific lines above broad fallbacks.
|
||||
|
||||
4. Verify the name exists in the **pinned** nixos-hardware input
|
||||
(`mkFlake` will throw if not). A CI check that every DB name ∈
|
||||
`nixosModules` is queued so lock bumps cannot silently break installs.
|
||||
|
||||
5. Optional: note on-hardware QA steps in `agent/HARDWARE-QUEUE.md`.
|
||||
|
||||
## 10. Doctor and hardware health (current vs target)
|
||||
|
||||
**Today** (`nomarchy-doctor`): failed units, disk space, theme-state
|
||||
validity/git, generation age, snapper timer. **No** Wi‑Fi, GPU, fwupd,
|
||||
or fingerprint checks.
|
||||
|
||||
**Target checks** (queued, all read-only, each failure prints one fix
|
||||
command — same doctor contract):
|
||||
|
||||
| Check | Pass condition | Suggested fix line |
|
||||
|-------|----------------|--------------------|
|
||||
| NetworkManager | device connected or wifi radio on | open System › Network |
|
||||
| Audio sink | default sink exists | System › Audio |
|
||||
| GPU accel | `glxinfo`/`vainfo` smoke (if installed) | check `hardwareProfile` / drivers |
|
||||
| Fingerprint | if USB VID matched / fprintd unit | `fprintd-enroll` or enable option |
|
||||
| fwupd | daemon active; optional “updates pending” warn | `fwupdmgr get-updates` |
|
||||
| Battery threshold | if laptop + limit set, sysfs writable | power docs |
|
||||
|
||||
## 11. Option quick reference
|
||||
|
||||
Full tables: [README § options](../README.md). Hardware-shaped surface:
|
||||
|
||||
| Option | Role |
|
||||
|--------|------|
|
||||
| `mkFlake.hardwareProfile` | nixos-hardware name or list |
|
||||
| `nomarchy.hardware.intel.enable` / `.guc` / `.computeRuntime` | Intel gap layer |
|
||||
| `nomarchy.hardware.amd.enable` / `.pstate` / `.vaapi` / `.rocm.*` | AMD gap layer |
|
||||
| `nomarchy.hardware.fingerprint.enable` / `.pam` | fprintd + PAM |
|
||||
| `nomarchy.hardware.npu.enable` | in-kernel NPU only |
|
||||
| `nomarchy.hardware.latestKernel` | `linuxPackages_latest` |
|
||||
| `nomarchy.hardware.camera.hideIrSensor` / `.irMatch` | dual-sensor webcams (v4l2 only; §7) |
|
||||
| `nomarchy.hardware.i2c.enable` / `.ddcci` | I2C + external backlight |
|
||||
| `nomarchy.system.power.*` | PPD/TLP, laptop, thermald, charge limit |
|
||||
| `nomarchy.system.bluetooth.enable` | BT stack |
|
||||
| `services.fwupd.enable` | LVFS (native NixOS, default on) |
|
||||
|
||||
## 12. Ease summary
|
||||
|
||||
| Situation | Effort |
|
||||
|-----------|--------|
|
||||
| Known model in DB (Framework, many ThinkPads, …) | Low — install and go |
|
||||
| Unknown modern Intel/AMD laptop | Low–medium — commons cover a lot |
|
||||
| Firmware updates | Medium — CLI only until product work lands |
|
||||
| Fingerprint for login | Medium — enroll CLI + PAM uncomment |
|
||||
| Hybrid NVIDIA | High — plain NixOS/wiki territory |
|
||||
| ROCm / NPU userspace | High — opt-in + expert |
|
||||
| Contribute a new DMI line | Medium for someone who can PR |
|
||||
|
||||
## 13. Related files
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| `modules/nixos/hardware.nix` | `nomarchy.hardware.*` |
|
||||
| `modules/nixos/power.nix` | PPD/TLP, charge limit, thermald |
|
||||
| `modules/nixos/default.nix` | firmware, fwupd, ddcci default |
|
||||
| `pkgs/nomarchy-install/hardware-db.sh` | DMI DB + probes |
|
||||
| `pkgs/nomarchy-install/nomarchy-install.sh` | writes flake + system.nix |
|
||||
| `lib.nix` | `hardwareProfile` resolution |
|
||||
| `templates/downstream/system.nix` | commented opt-ins |
|
||||
| `agent/HARDWARE-QUEUE.md` | on-machine V3 checks |
|
||||
314
docs/MIGRATION.md
Normal file
314
docs/MIGRATION.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# Migrating an existing NixOS machine to Nomarchy (no reinstall)
|
||||
|
||||
If your machine already runs NixOS, you do **not** need the installer or a
|
||||
reformat to adopt Nomarchy. Nomarchy is a flake; "installing" it onto an
|
||||
existing NixOS box means pointing `nixos-rebuild` at a Nomarchy‑based flake
|
||||
that reuses your current `hardware-configuration.nix`. Nothing repartitions,
|
||||
and your `/home` is never written to by an activation.
|
||||
|
||||
This guide is written generically, with **TuringMachine** — a Lenovo AMD
|
||||
Ryzen 7840U / Radeon 780M laptop, LUKS + btrfs, systemd‑boot — as the
|
||||
concrete worked example. Substitute your own values where called out.
|
||||
|
||||
> **The promise:** every step below is reversible. You keep three
|
||||
> independent rollback nets (NixOS generations, a btrfs snapshot, and the
|
||||
> fact that `nixos-rebuild test` never changes the boot default), and your
|
||||
> files live on a separate subvolume that no config switch touches.
|
||||
|
||||
---
|
||||
|
||||
## 0. Is your machine a good candidate?
|
||||
|
||||
Migration is cleanest when your machine already matches Nomarchy's
|
||||
assumptions. Check each:
|
||||
|
||||
| Nomarchy expects | Check it | If it differs |
|
||||
|---|---|---|
|
||||
| **systemd‑boot** | `bootctl status` → "Product: systemd‑boot" | GRUB works too; keep your loader config in `system.nix` |
|
||||
| **btrfs** with `@`/`@home` subvolumes | `findmnt -t btrfs` | ext4/xfs boot fine — you just don't get snapshots |
|
||||
| `@snapshots` + `@home-snapshots` subvols | in `findmnt` output | snapper features need them; create them or skip snapshots |
|
||||
| **LUKS** (optional, themed prompt) | `lsblk -f` shows `crypto_LUKS` | none — LUKS is optional |
|
||||
| Already a **flake** config | `test -f /etc/nixos/flake.nix` | fine either way; you'll write a fresh flake regardless |
|
||||
|
||||
**Installer vs migration snapshot layout.** A fresh Nomarchy install
|
||||
(disko) creates a top-level `@snapshots` subvolume mounted at
|
||||
`/.snapshots`, then a first-boot oneshot makes a *nested*
|
||||
`/home/.snapshots` under `@home` for snapper's home timeline — it does
|
||||
**not** create a separate top-level `@home-snapshots`. Migration machines
|
||||
(e.g. TuringMachine) may already use top-level `@snapshots` **and**
|
||||
`@home-snapshots`; that is fine. Snapper only needs a `.snapshots` path
|
||||
under each tracked subvolume (`/` and `/home`), so either layout works —
|
||||
reuse what you have, or create the missing pieces if you want snapper
|
||||
without reformatting.
|
||||
|
||||
TuringMachine matches all of these, including the `@snapshots` /
|
||||
`@home-snapshots` subvolumes — so snapper works with zero disk work.
|
||||
|
||||
**Version note.** Nomarchy pins `nixos-26.05`. If you're on an older release
|
||||
(TuringMachine is on **25.11**), the migration folds a one‑release upgrade
|
||||
into the switch. That's normal and supported — read the
|
||||
[NixOS 26.05 release notes](https://nixos.org/manual/nixos/stable/release-notes)
|
||||
for option/package renames, and lean on generations if something regresses.
|
||||
|
||||
---
|
||||
|
||||
## 1. The two rules that protect your data
|
||||
|
||||
1. **Never bump `system.stateVersion`.** It is an on‑disk/service
|
||||
compatibility marker tied to when the machine was *first installed* — not
|
||||
the nixpkgs version. Nomarchy's template ships `26.05`; you **must** change
|
||||
it back to your machine's original value. Find yours:
|
||||
|
||||
```console
|
||||
$ nixos-option system.stateVersion # or: nix eval .#nixosConfigurations.<host>.config.system.stateVersion
|
||||
"24.11"
|
||||
```
|
||||
|
||||
*(TuringMachine: `24.11`.)*
|
||||
|
||||
2. **`/home` is never touched by an activation.** A `nixos-rebuild switch`
|
||||
swaps the system generation; your data subvolume is untouched. The Phase 0
|
||||
snapshot is belt‑and‑suspenders, not a necessity for file safety.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Safety net (nothing changes yet)
|
||||
|
||||
```bash
|
||||
# Read-only btrfs snapshots of root and home — instant rollback targets.
|
||||
sudo btrfs subvolume snapshot -r / /.snapshots/pre-nomarchy-root
|
||||
sudo btrfs subvolume snapshot -r /home /home/.snapshots/pre-nomarchy-home
|
||||
|
||||
# Freeze your current config as a clean git baseline.
|
||||
cd /etc/nixos && git add -A && git commit -m "pre-nomarchy baseline" || true
|
||||
```
|
||||
|
||||
You are currently booted in a known‑good generation; it remains in the
|
||||
systemd‑boot menu throughout. Worst case at any later step: reboot and pick
|
||||
it.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Build the Nomarchy flake alongside (no switch)
|
||||
|
||||
Stand the new config up in a working directory and **build** it without
|
||||
activating. This is the real safety line: iterate here until it builds green
|
||||
before anything touches the running system.
|
||||
|
||||
```bash
|
||||
git clone https://git.bemagri.xyz/bernardo/nomarchy.git ~/nomarchy-migrate
|
||||
cd ~/nomarchy-migrate
|
||||
# Or start from the downstream template:
|
||||
# nix flake init -t "git+https://git.bemagri.xyz/bernardo/nomarchy.git?ref=v1"
|
||||
# (produces flake.nix/system.nix/home.nix/…)
|
||||
```
|
||||
|
||||
A Nomarchy downstream flake owns exactly five files. Assemble them:
|
||||
|
||||
### `hardware-configuration.nix` — reuse yours unchanged
|
||||
|
||||
Copy your **existing** hardware config in verbatim. This is what preserves
|
||||
your disks, LUKS, btrfs subvolumes and swap — the reason no reinstall is
|
||||
needed.
|
||||
|
||||
```bash
|
||||
cp /etc/nixos/hosts/TuringMachine/hardware-configuration.nix ./hardware-configuration.nix
|
||||
```
|
||||
|
||||
### `flake.nix` — one `mkFlake` call (from the template)
|
||||
|
||||
```nix
|
||||
{
|
||||
description = "TuringMachine — Nomarchy";
|
||||
inputs.nomarchy.url = "git+https://git.bemagri.xyz/bernardo/nomarchy.git?ref=v1";
|
||||
|
||||
outputs = { nomarchy, ... }:
|
||||
nomarchy.lib.mkFlake {
|
||||
src = ./.;
|
||||
username = "bernardo"; # <- your login name
|
||||
# Optional nixos-hardware profile(s) for your model. For an AMD laptop:
|
||||
# hardwareProfile = [ "common-cpu-amd-pstate" "common-gpu-amd" "common-pc-laptop-ssd" ];
|
||||
# Names: https://github.com/NixOS/nixos-hardware (verify before use)
|
||||
# Full hardware story (firmware, fingerprint, unsupported machines):
|
||||
# docs/HARDWARE.md in the Nomarchy repo
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### `system.nix` — machine specifics + your decisions
|
||||
|
||||
This is where your three migration decisions land: **PPD power (no
|
||||
ryzenadj)**, **no Secure Boot**, and the **stateVersion override**.
|
||||
|
||||
```nix
|
||||
{ pkgs, username, ... }:
|
||||
|
||||
{
|
||||
# Plain systemd-boot — no lanzaboote / Secure Boot.
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
networking.hostName = "TuringMachine";
|
||||
time.timeZone = "Europe/London"; # your zone
|
||||
i18n.defaultLocale = "en_US.UTF-8";
|
||||
|
||||
users.users.${username} = {
|
||||
isNormalUser = true;
|
||||
extraGroups = [ "wheel" "networkmanager" "video" "input" ];
|
||||
};
|
||||
|
||||
# ── Power: Nomarchy's PPD (drops all custom ryzenadj/TLP tuning) ──────
|
||||
nomarchy.system.power = {
|
||||
enable = true;
|
||||
backend = "ppd"; # power-profiles-daemon
|
||||
laptop = true;
|
||||
batteryChargeLimit = 80; # optional longevity cap
|
||||
};
|
||||
|
||||
# ── AMD 7840U / Radeon 780M ──────────────────────────────────────────
|
||||
nomarchy.hardware.amd.enable = true; # amd-pstate + radeonsi VA-API
|
||||
# nomarchy.hardware.amd.rocm.enable = true; # opt-in GPU compute (multi-GB)
|
||||
|
||||
# CRITICAL: keep your ORIGINAL install's value — never Nomarchy's 26.05.
|
||||
system.stateVersion = "24.11";
|
||||
}
|
||||
```
|
||||
|
||||
> **Dropped on purpose (decisions 1 & 2):** your old
|
||||
> `modules/services/power-management.nix` ryzenadj stack, the `lanzaboote`
|
||||
> input, and the `power-max`/`power-stealth` scripts. PPD's
|
||||
> performance/balanced/power‑saver profiles (switchable from the Waybar
|
||||
> battery/profile icons and `nomarchy-menu`) replace them.
|
||||
|
||||
### `home.nix` — your apps on top of Nomarchy's desktop
|
||||
|
||||
Start from the template's `home.nix` (it ships the default app set) and add
|
||||
your personal packages/config. Carry over anything you still want (e.g. your
|
||||
emacs setup).
|
||||
|
||||
```nix
|
||||
{ pkgs, lib, ... }:
|
||||
|
||||
{
|
||||
# Nomarchy hardcodes home.stateVersion = "26.05". Moving home-manager's
|
||||
# stateVersion is low-risk, but if you want to pin your original:
|
||||
home.stateVersion = lib.mkForce "24.11";
|
||||
|
||||
home.packages = with pkgs; [
|
||||
# your extras — e.g. emacs, language toolchains, …
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### `theme-state.json`
|
||||
|
||||
Copy the template's `theme-state.json` (or let `nomarchy-menu theme` write
|
||||
it after the switch). Your old `nomarchy-state.nix` prototype (schema
|
||||
`nomarchy.theme = "nord"` …) is **retired** — the current distro uses this
|
||||
JSON. `nord` is a shipped Nomarchy theme, so you lose nothing.
|
||||
|
||||
### The build gate
|
||||
|
||||
```bash
|
||||
nixos-rebuild build --flake ~/nomarchy-migrate#default
|
||||
```
|
||||
|
||||
Zero activation — this only evaluates and builds the system closure. Fix any
|
||||
eval/build error here, in isolation, before touching the running machine.
|
||||
Expect to resolve a few 25.11→26.05 option renames.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Reversible activation
|
||||
|
||||
```bash
|
||||
# Activates now, but does NOT become the boot default. If the session
|
||||
# breaks, REBOOT and you are back in your old generation, untouched.
|
||||
sudo nixos-rebuild test --flake ~/nomarchy-migrate#default
|
||||
|
||||
# Bring the desktop (home-manager) up:
|
||||
home-manager switch --flake ~/nomarchy-migrate#bernardo
|
||||
```
|
||||
|
||||
Log into Hyprland and sanity‑check: Waybar renders, `SUPER+M` opens the
|
||||
menu, theming is coherent, `SUPER+?` shows the cheatsheet. Confirm the
|
||||
machine‑specific things you care about still work — suspend/hibernate, the
|
||||
AMD GPU (`vainfo` → radeonsi), display brightness.
|
||||
|
||||
If anything is wrong: **reboot → old generation.** Nothing is committed as
|
||||
default yet.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Reconcile
|
||||
|
||||
- **Power:** verify `powerprofilesctl get` works and the Waybar battery /
|
||||
power‑profile icons open the power menu. Your ryzenadj scripts are gone;
|
||||
if you miss a specific TDP behaviour, that's a follow‑up, not a blocker.
|
||||
- **Theme:** `nomarchy-menu theme` → pick **nord** (writes
|
||||
`theme-state.json`).
|
||||
- **Snapshots:** `nomarchy-menu` → System → Snapshots should see your
|
||||
existing `@snapshots` subvolume.
|
||||
- **Secrets/services:** if you relied on agenix‑managed secrets for a
|
||||
service, layer `agenix` back into `system.nix` as a machine‑specific
|
||||
import (Nomarchy doesn't manage secrets). If you don't need them, leave
|
||||
them out — this is a full cutover.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Cutover
|
||||
|
||||
Once a test boot is solid:
|
||||
|
||||
```bash
|
||||
# Move the flake to its canonical home and make it the boot default.
|
||||
mv ~/nomarchy-migrate ~/.nomarchy
|
||||
sudo nixos-rebuild switch --flake ~/.nomarchy#default
|
||||
home-manager switch --flake ~/.nomarchy#bernardo
|
||||
|
||||
# Point /etc/nixos at the flake (optional but conventional).
|
||||
sudo mv /etc/nixos /etc/nixos.pre-nomarchy
|
||||
sudo ln -s ~/.nomarchy /etc/nixos
|
||||
```
|
||||
|
||||
From here you're on the standard Nomarchy update flow: `sys-update` (lock +
|
||||
system) then `home-update` (desktop) — always in that order (a lock bump
|
||||
before the home switch, or desktop changes are skipped against the old
|
||||
lock).
|
||||
|
||||
---
|
||||
|
||||
## Rollback, at any point
|
||||
|
||||
| Net | How |
|
||||
|---|---|
|
||||
| **NixOS generation** | Reboot → pick the previous entry in the systemd‑boot menu. |
|
||||
| **`nixos-rebuild test`** | Never sets the boot default; a reboot reverts it. |
|
||||
| **btrfs snapshot** | Restore `/.snapshots/pre-nomarchy-root` (see `docs/RECOVERY.md`). |
|
||||
| **git baseline** | `/etc/nixos.pre-nomarchy` (and the pre‑nomarchy commit) is your old config verbatim. |
|
||||
|
||||
`/home` is untouched by all of the above.
|
||||
|
||||
---
|
||||
|
||||
## Post‑migration cleanup (once you're confident)
|
||||
|
||||
- Delete the safety snapshots: `sudo btrfs subvolume delete /.snapshots/pre-nomarchy-root` (and the home one).
|
||||
- Remove `/etc/nixos.pre-nomarchy` and the old per‑host modules you cut
|
||||
(ryzenadj power‑management, lanzaboote, the `nomarchy-state.nix`
|
||||
prototype).
|
||||
- Prune old generations: `sudo nix-collect-garbage -d`.
|
||||
|
||||
---
|
||||
|
||||
## TuringMachine — the decisions, at a glance
|
||||
|
||||
| Item | Choice |
|
||||
|---|---|
|
||||
| Power | Nomarchy **PPD** (`backend = "ppd"`); **all ryzenadj/TLP tuning dropped** |
|
||||
| Secure Boot | **Off** — plain systemd‑boot, `lanzaboote` dropped |
|
||||
| Scope | **Full cutover** to Nomarchy's structure |
|
||||
| `system.stateVersion` | **`24.11`** (preserved from original install) |
|
||||
| Hardware | `nomarchy.hardware.amd.enable = true` (7840U / Radeon 780M) |
|
||||
| Bootloader | systemd‑boot (unchanged — already matched) |
|
||||
| Filesystem | LUKS + btrfs, existing subvolumes reused (incl. snapshot subvols) |
|
||||
@@ -39,7 +39,7 @@ in your `home.nix` wins — no `mkForce` needed:
|
||||
input.follow_mouse = 0; # was 1
|
||||
input.touchpad.natural_scroll = false;
|
||||
misc.disable_splash_rendering = false;
|
||||
monitor = [ "DP-1,2560x1440@144,0x0,1" ]; # replaces the default rule
|
||||
monitor = [ "DP-1,2560x1440@144,0x0,1" ]; # raw rule — replaces the default
|
||||
animations.enabled = false;
|
||||
};
|
||||
|
||||
@@ -50,6 +50,11 @@ in your `home.nix` wins — no `mkForce` needed:
|
||||
}
|
||||
```
|
||||
|
||||
For monitor layout, prefer the friendlier **`nomarchy.monitors`** (a list of
|
||||
per-output submodules — resolution/position/scale/rotation — turned into
|
||||
Hyprland rules and applied on hotplug; run `nwg-displays` to find the values
|
||||
interactively). Assigning `settings.monitor` directly, as above, replaces it.
|
||||
|
||||
### Adding vs. overriding lists
|
||||
|
||||
`bind`, `bindel`, `bindl`, `bindm` and `exec-once` are lists kept at normal
|
||||
@@ -61,7 +66,7 @@ autostarts run *alongside* the defaults:
|
||||
wayland.windowManager.hyprland.settings = {
|
||||
bind = [
|
||||
"$mod, B, exec, firefox"
|
||||
"$mod SHIFT, S, exec, grim -g \"$(slurp)\" - | swappy -f -"
|
||||
"$mod SHIFT, S, exec, grim -g \"$(slurp)\" - | satty --filename -"
|
||||
];
|
||||
exec-once = [ "nm-applet --indicator" ];
|
||||
};
|
||||
@@ -140,6 +145,7 @@ value is theme-owned at normal priority — either change it via the CLI (§1) o
|
||||
|---|---|
|
||||
| Change gaps / colors / rounding / fonts | `nomarchy-theme-sync set …` or `apply` |
|
||||
| Change input / misc / monitor / animations / terminal chrome | plain assignment in `home.nix` |
|
||||
| Arrange monitors declaratively | `nomarchy.monitors` (values via `nwg-displays`) |
|
||||
| Add keybinds / autostarts | add to the `bind` / `exec-once` list (concatenates) |
|
||||
| Replace all keybinds | `bind = lib.mkForce [ … ]` |
|
||||
| Hardcode an appearance value against the theme | `lib.mkForce` in `home.nix` |
|
||||
|
||||
40
docs/README.md
Normal file
40
docs/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Docs map
|
||||
|
||||
Where human and agent documentation lives. **Do not** invent a third
|
||||
tree for the same facts.
|
||||
|
||||
| Path | Audience | Role |
|
||||
|------|----------|------|
|
||||
| [../README.md](../README.md) | Everyone | What Nomarchy is, install, options tables |
|
||||
| [VISION.md](VISION.md) | Maintainers + agents | Product north star toward **v1.0** and beyond — themes, not a task queue |
|
||||
| [ROADMAP.md](ROADMAP.md) | Maintainers + agents | Design/decision records + shipped log (historical ✓) |
|
||||
| [HARDWARE.md](HARDWARE.md) | Users + agents | Firmware, profiles, drivers, unsupported machines |
|
||||
| [TESTING.md](TESTING.md) | Maintainers + agents | Verification ladder, honesty rule, ISO/VM recipes |
|
||||
| [RECOVERY.md](RECOVERY.md) | Users | Broken theme/desktop/boot → undo |
|
||||
| [OVERRIDES.md](OVERRIDES.md) | Users | Downstream Nix overrides |
|
||||
| [MIGRATION.md](MIGRATION.md) | Users | Existing NixOS → Nomarchy without reinstall |
|
||||
|
||||
## Related (not under `docs/`)
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| [../agent/README.md](../agent/README.md) | **Executable** agent loop state: BACKLOG, LOOP, MEMORY, … |
|
||||
| [../CLAUDE.md](../CLAUDE.md) | Agent entry point (any harness that reads it) |
|
||||
| [../.claude/](../.claude/) | Claude Code only: permissions + subagent defs |
|
||||
|
||||
## How work flows
|
||||
|
||||
```
|
||||
VISION (what we want the product to feel like)
|
||||
│
|
||||
▼ human triages slices into…
|
||||
BACKLOG (what's next, ordered — agents execute only this)
|
||||
│
|
||||
▼ lasting design notes after ship →
|
||||
ROADMAP ✓ entries
|
||||
```
|
||||
|
||||
Agents **do not** implement directly from VISION or ROADMAP. They take
|
||||
the top actionable item in `agent/BACKLOG.md` (see `agent/LOOP.md`).
|
||||
They **may** append PROPOSED pitches that reference `VISION § …` or
|
||||
`ROADMAP § …`.
|
||||
92
docs/RECOVERY.md
Normal file
92
docs/RECOVERY.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Recovery runbook — when something breaks
|
||||
|
||||
Ordered from "the desktop looks wrong" to "the machine won't boot".
|
||||
Everything here already ships on an installed machine — you don't need a
|
||||
live USB until the last resort. The theme is always the same: **nothing
|
||||
in Nomarchy is destroyed by a bad change** — every rebuild is a NixOS /
|
||||
Home Manager generation you can step back to, and (on BTRFS installs)
|
||||
snapper keeps file-level history on top.
|
||||
|
||||
If the graphical session is unusable, a text console is one keystroke
|
||||
away: **Ctrl+Alt+F2** gives a TTY login (the session itself runs on
|
||||
tty1); log in with your normal user.
|
||||
|
||||
## 1. A theme or desktop change broke the session
|
||||
|
||||
Theme applies and `home-update` are Home Manager switches — one
|
||||
generation each, so the previous desktop is still on disk:
|
||||
|
||||
```sh
|
||||
home-manager generations # newest first, one per theme/HM change
|
||||
/nix/store/…-home-manager-generation/activate # run the one you want
|
||||
```
|
||||
|
||||
The same picker lives in the menu: **System › Rollback** lists the
|
||||
recent desktop generations — pick one and it activates.
|
||||
|
||||
Or simply apply a theme you know is good: `nomarchy-theme-sync apply
|
||||
boreal` (or any preset). If a switch failed halfway, the state file is written
|
||||
*before* the rebuild — fix the cause and re-run
|
||||
`home-manager switch --flake ~/.nomarchy` (or `home-update`).
|
||||
|
||||
Your flake checkout is a git repo, and with auto-commit enabled every
|
||||
apply is a commit: `git -C ~/.nomarchy log` to see what changed,
|
||||
`git revert` the culprit, then `home-update`.
|
||||
|
||||
## 2. The desktop won't start at all
|
||||
|
||||
Greeter loops, black screen after the password, session exits straight
|
||||
back to tuigreet — from the Ctrl+Alt+F2 TTY:
|
||||
|
||||
```sh
|
||||
journalctl -b -u greetd # did the session command launch?
|
||||
journalctl --user -b # Hyprland + the user services
|
||||
```
|
||||
|
||||
- Rolling back the *desktop* half is §1 (works from the TTY).
|
||||
- If greetd/tuigreet itself is broken, that's system-side → §3.
|
||||
- In a **VM**, a black screen is almost always missing guest OpenGL,
|
||||
not your config — see docs/TESTING.md §5.
|
||||
- First boot after an install came up *unthemed*: read
|
||||
`/var/log/nomarchy-hm-preactivate.log` on the installed system.
|
||||
|
||||
## 3. A system change broke it — boot an older generation
|
||||
|
||||
Reboot and pick an older **NixOS generation** in the systemd-boot menu
|
||||
(hold a key during firmware handoff if the menu doesn't linger; the
|
||||
last 10 generations are kept). That boots yesterday's system unchanged.
|
||||
|
||||
Booting an old generation is temporary — the default entry is still the
|
||||
broken one. Make the fix stick from the working boot: revert the change
|
||||
in `~/.nomarchy` (`git -C ~/.nomarchy revert …` or edit `system.nix`
|
||||
back), then `sys-rebuild`.
|
||||
|
||||
## 4. Files went missing or wrong — snapshots (BTRFS installs)
|
||||
|
||||
With `nomarchy.system.snapper.enable` (the installer's default on
|
||||
BTRFS), the root filesystem has hourly/daily history, and
|
||||
`nixos-rebuild-snap` leaves a snapshot right before a rebuild:
|
||||
|
||||
- **GUI:** menu › System › Snapshots (btrfs-assistant; expects a polkit
|
||||
password prompt).
|
||||
- **Terminal/SSH:** `sudo nomarchy-snapshots` — browse a snapshot's
|
||||
diff, **restore changed files** (snapper `undochange`), or **roll the
|
||||
whole root back** to a snapshot and reboot. Both destructive actions
|
||||
sit behind a typed-`yes` confirmation.
|
||||
|
||||
Snapshots are the undo for *data on disk*; the Nix config model is
|
||||
undone by generations (§1/§3) — use each for its half.
|
||||
|
||||
## 5. Last resort — from the outside
|
||||
|
||||
Boot the Nomarchy ISO (any NixOS ISO works), then:
|
||||
|
||||
```sh
|
||||
sudo mount /dev/<root> /mnt # + /mnt/boot; LUKS: cryptsetup open first
|
||||
sudo nixos-enter --root /mnt # chroot with nix available
|
||||
```
|
||||
|
||||
From there you have the full toolbox: `nixos-rebuild boot --flake
|
||||
/home/<you>/.nomarchy#default` after fixing the flake, or snapper from
|
||||
§4. If you get this far with something Nomarchy shipped broken, please
|
||||
file it.
|
||||
642
docs/ROADMAP.md
642
docs/ROADMAP.md
@@ -1,9 +1,20 @@
|
||||
# Roadmap & changelog
|
||||
|
||||
Forward-looking plans, plus a running log of shipped fixes (the
|
||||
"Known issues & follow-ups" section). Split out of the README so that
|
||||
stays a focused entry point — what Nomarchy is, how to install it, and
|
||||
how to override it. Items marked ✓ are shipped.
|
||||
Design/decision records and a running log of shipped work (items marked
|
||||
✓). Split out of the README so that stays a focused entry point.
|
||||
|
||||
> **Where to look**
|
||||
>
|
||||
> | Need | File |
|
||||
> |------|------|
|
||||
> | Product themes toward **v1.0** (not a queue) | [`docs/VISION.md`](VISION.md) |
|
||||
> | **What agents execute next** | [`agent/BACKLOG.md`](../agent/BACKLOG.md) |
|
||||
> | Docs map | [`docs/README.md`](README.md) |
|
||||
> | Agent loop protocol | [`agent/LOOP.md`](../agent/LOOP.md) |
|
||||
>
|
||||
> Backlog items may reference this file as **ROADMAP § \<item\>**. When
|
||||
> something ships, lasting design notes get a ✓ entry here; the BACKLOG
|
||||
> line is deleted.
|
||||
|
||||
## Roadmap
|
||||
- **Menu system** (apps launcher + theme switching + system actions), built
|
||||
@@ -51,6 +62,11 @@ how to override it. Items marked ✓ are shipped.
|
||||
one-off — any new feature that earns a menu entry must be placed in the
|
||||
right submenu (don't let the root creep back to a flat list), with its
|
||||
direct `SUPER+CTRL+<mnemonic>` bind and self-gating as applicable.
|
||||
- ✓ **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
|
||||
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-
|
||||
@@ -67,25 +83,136 @@ how to override it. Items marked ✓ are shipped.
|
||||
launcher, and yazi (SUPER+E) already covers real browsing.
|
||||
- **More menu modules from rofi tools:** the script-based counterparts
|
||||
(run via `rofi -dmenu`, like the hand-rolled modules), each a deliberate
|
||||
replacement of an existing flow: **rofi-network-manager** (a keyboard
|
||||
wifi/VPN picker vs today's `nmtui`-in-terminal `network`), **rofi-rbw /
|
||||
rofi-pass** (a secrets module — Bitwarden via rbw, or `pass` — pairs with
|
||||
`keys.nix`), and **rofi-pulse-select** (an audio sink/source switcher).
|
||||
Decide per-module whether it earns replacing the current path.
|
||||
replacement of an existing flow.
|
||||
- ✓ **Network** (`networkmanager_dmenu`): a native rofi wifi/VPN picker
|
||||
replacing the old `nmtui`-in-terminal `network` flow. Configured (xdg
|
||||
`networkmanager-dmenu/config.ini`) to drive rofi with `rofi_highlight`
|
||||
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
|
||||
bar layouts as `waybar.jsonc` whole-swaps (adapted: dead legacy script
|
||||
modules dropped, Nerd-Fonts-v2 codepoints remapped to FontAwesome/v3,
|
||||
logo button opens nomarchy-menu); the other four identity themes are
|
||||
palette recolors and already match. Remaining: a visual pass over all
|
||||
six on the live ISO
|
||||
- **Per-theme rofi identity:** the `themes/<slug>/rofi.rasi` whole-swap
|
||||
ships, and summer-day/night carry their legacy designs (inverted window,
|
||||
green inputbar, yellow bottom-border). Remaining: author `.rasi`
|
||||
identities for the other four ported themes if/when they want one (the
|
||||
generated palette theme is the default and looks fine)
|
||||
- **Faster switches:** move `backgrounds/` out of the flake source (the 86 MB
|
||||
re-copy on every state write is the main eval tax), then pre-built theme
|
||||
variants if still needed
|
||||
- ✓ **Per-theme rofi identity:** the `themes/<slug>/rofi.rasi` whole-swap
|
||||
ships, and all six identity themes now carry a designed `.rasi`. summer-day/
|
||||
night keep their legacy ports (inverted window, green inputbar, yellow
|
||||
bottom-border); the other four were authored from each theme's character
|
||||
(no legacy layout to port — their waybar whole-swaps are palette-only):
|
||||
**nord** a soft rounded "frost panel" (frost border, brighter-frost
|
||||
selection, aurora-purple prompt); **retro-82** a sharp CRT terminal (square
|
||||
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
|
||||
v1: `nomarchy.system.plymouth.*`, background tinted from the state file)
|
||||
- Installer round 2: multi-disk BTRFS RAID, impermanence, BIOS/legacy
|
||||
@@ -99,10 +226,28 @@ how to override it. Items marked ✓ are shipped.
|
||||
✓ `isoImage.splashImage` — the vendored vector logo
|
||||
(`modules/nixos/branding/logo.svg`, from legacy) recolored to the palette
|
||||
accent on the theme base, built at ISO-build time (`hosts/live.nix`).
|
||||
Remaining: `isoImage.grubTheme` so UEFI boot matches the isolinux splash
|
||||
(needs a full grub theme dir), and the `distroId` question (it changes
|
||||
`DEFAULT_HOSTNAME` and upstream `isNixos` checks — needs a test pass;
|
||||
nixos-* CLI names stay regardless)
|
||||
✓ **`isoImage.grubTheme` (UEFI boot matches BIOS):** `hosts/live.nix` now
|
||||
builds a `nomarchyGrubTheme` dir whose background is the *same* composed
|
||||
splash image as the isolinux splash (accent logo on base), with a
|
||||
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`
|
||||
(`nomarchy.fastfetch.enable`) — the vendored vector logo, recolored to
|
||||
the palette accent and rendered to compact block-art via chafa at build
|
||||
@@ -176,8 +321,17 @@ how to override it. Items marked ✓ are shipped.
|
||||
comes from the agent's zsh integration; cache TTLs (30 min / 2 h) govern
|
||||
re-prompting. gnome-keyring stays the Secret Service (modern versions run
|
||||
no SSH agent, so no socket contention); screen lock doesn't flush the
|
||||
cache. Remaining (optional): a session-level `SSH_AUTH_SOCK` export so GUI
|
||||
clients launched outside a shell also see the agent.
|
||||
cache. ✓ **session-level `SSH_AUTH_SOCK`:** besides the zsh integration,
|
||||
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
|
||||
and clarity.
|
||||
- ✓ pruned now-redundant config: the installer no longer writes
|
||||
@@ -204,18 +358,15 @@ how to override it. Items marked ✓ are shipped.
|
||||
hand-formatting, so `nixfmt-rfc-style --check` flags ~33 files. Adopting a
|
||||
formatter would be a one-time repo-wide reformat that flattens that
|
||||
alignment — a maintainer call, not a silent cleanup, so left untouched.
|
||||
- **Full docs review & restructure:** a dedicated pass over all the prose,
|
||||
not just the code-adjacent cleanup the repo-sanitize item covers. The
|
||||
README has grown to ~540 lines with a ~200-line roadmap inline — the
|
||||
biggest single move is likely **splitting the roadmap out** (e.g.
|
||||
`ROADMAP.md` / `docs/`) so the README stays a focused "what it is / how
|
||||
to install / how to override" entry point. Also: reconcile every option
|
||||
table against the live `nomarchy.*` surface (snapper and others are
|
||||
missing); a pass over `docs/OVERRIDES.md` + `docs/TESTING.md` and the
|
||||
`templates/downstream/README.md` for drift; check the install/first-run
|
||||
story reads cleanly end to end; and decide whether anything wants a real
|
||||
docs site vs. staying Markdown-in-repo. Pairs with the first-boot welcome
|
||||
and control-center items (shared "how do I…" surface).
|
||||
- ✓ **Full docs review & restructure** (completed 2026-07-04 in four
|
||||
slices): the roadmap/backlog split (this file + agent/BACKLOG.md);
|
||||
option tables reconciled with the live surface and guarded permanently
|
||||
by `checks.option-docs`; drift pass over OVERRIDES/TESTING/template
|
||||
README (claims held; two gaps fixed); the install/first-run story now
|
||||
hands off README §3 → template README explicitly; and a new
|
||||
**docs/RECOVERY.md** runbook (generations → journals → older boot
|
||||
generation → snapper → nixos-enter). Still open, deliberately: the
|
||||
docs-site-vs-Markdown call (BACKLOG § Decisions).
|
||||
- **Laptop power / battery management:** a real power story behind a
|
||||
`nomarchy.system.power.*` surface (`modules/nixos/power.nix`), replacing the
|
||||
old "only `services.upower` for Waybar reporting" baseline:
|
||||
@@ -231,7 +382,21 @@ how to override it. Items marked ✓ are shipped.
|
||||
turns it on for a `GenuineIntel` CPU.
|
||||
- ✓ **longevity:** `power.batteryChargeLimit` (e.g. 80) — a backend-
|
||||
independent sysfs oneshot writes `charge_control_end_threshold`. Off by
|
||||
default; the installer scaffolds it commented-out on laptops.
|
||||
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
|
||||
`power.thermal.enable` (Intel) into the generated `system.nix`.
|
||||
- ✓ **idle cohesion:** `modules/home/idle.nix` now suspends only on
|
||||
@@ -241,8 +406,6 @@ how to override it. Items marked ✓ are shipped.
|
||||
logind's lid handling is left at its defaults (suspend on lid close,
|
||||
ignore when docked) — an explicit "I'm done" that coheres with the
|
||||
idle behaviour.
|
||||
- Remaining: a boot-only→event-driven charge-limit re-apply (udev) if a
|
||||
firmware resets the threshold on unplug.
|
||||
- ✓ **Waybar parity:** the `custom/powerprofile` indicator now shows in the
|
||||
summer-day/night whole-swap themes too. `powerProfileStatus`/`Cycle` are
|
||||
named `writeShellScriptBin`s on PATH (`waybar.nix` home.packages), so the
|
||||
@@ -263,6 +426,31 @@ how to override it. Items marked ✓ are shipped.
|
||||
Complements nixos-hardware (model quirks) rather than replacing it; keep
|
||||
the detection in the installer's `hardware-db` so an installed machine
|
||||
bakes the right defaults, all overridable through `nomarchy.hardware.*`.
|
||||
- ✓ **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):
|
||||
- **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
|
||||
@@ -287,6 +475,96 @@ how to override it. Items marked ✓ are shipped.
|
||||
distro-wide regardless).
|
||||
Sub-items here can graduate into their own roadmap entries as they're
|
||||
scoped; the unifying work is the detection + `nomarchy.hardware.*` surface.
|
||||
- **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 (HARDWARE-QUEUE › T14s);
|
||||
optional `v4l-utils` + `cameractrls` already commented in the template.
|
||||
- ✓ **Portal/Flatpak IR gap documented (#71, docs only):** the shipped
|
||||
v4l2 IR-hide does **not** cover libcamera / xdg-desktop-portal /
|
||||
Flatpak camera pickers (IR can still appear there). User-facing note
|
||||
in [`HARDWARE.md` §7](HARDWARE.md). Engineering options (b) WirePlumber
|
||||
libcamera GREY-only rule and (c) libcamera/udev-layer hide stay
|
||||
deferred — both need a T14s-class dual-sensor machine to verify; no
|
||||
libcamera rules in-tree.
|
||||
- ✓ **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*
|
||||
application suite above — heavier or more personal integrations shipped
|
||||
**off by default**, each a `nomarchy.services.<name>.enable` toggle a
|
||||
@@ -344,19 +622,74 @@ how to override it. Items marked ✓ are shipped.
|
||||
(no kanshi — it fights Hyprland's own output management). `nwg-displays`
|
||||
ships behind `nomarchy.displays.enable` as an interactive arranger (a
|
||||
helper to find values; the declarative config stays the source of truth —
|
||||
its output file isn't sourced). Remaining: true docked/undocked **profile
|
||||
switching** of the *same* outputs (Hyprland's per-output rules cover the
|
||||
common "external connects → arrange" case, not multi-layout toggles), and
|
||||
optionally workspace-to-monitor binding.
|
||||
its output file isn't sourced). The System ▸ **Display** menu picks an
|
||||
output's resolution from its advertised modes: applied live via `hyprctl
|
||||
keyword monitor` (current position + scale kept) and persisted to the
|
||||
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) —
|
||||
a scheduled colour-temperature shift via `hyprsunset` (Hyprland-native),
|
||||
warm at night, identity (no shift) by day. `modules/home/nightlight.nix`
|
||||
drives the HM `services.hyprsunset` with two time-based profiles
|
||||
(`sunrise` → identity, `sunset` → `temperature`), so hyprsunset handles the
|
||||
schedule and the on-login state. Needs an on-hardware check that hyprsunset
|
||||
applies the active profile at session start. Remaining (optional): a menu +
|
||||
Waybar toggle to force it on/off, and geo (lat/long) auto sunset/sunrise
|
||||
(would mean wlsunset, which schedules by location).
|
||||
schedule and the on-login state. Active-profile-at-session-start was
|
||||
confirmed on hardware (2026-06-18). ✓ **Menu + Waybar toggle (opt-in,
|
||||
instant, in-flake state):** two git-tracked keys in the state file, both
|
||||
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):**
|
||||
- ✓ **Per-device declarative layout:** `nomarchy.keyboard.devices`
|
||||
(`{ "<hyprctl-device-name>" = { layout; variant; }; }`) generates Hyprland
|
||||
@@ -372,20 +705,46 @@ how to override it. Items marked ✓ are shipped.
|
||||
watch` daemon runs (exec-once): it polls `hyprctl devices`, and when a
|
||||
keyboard connects *after* login that isn't in `keyboard.devices` and
|
||||
hasn't been chosen before, it pops a rofi layout picker, applies the
|
||||
choice with `hyprctl switchxkblayout` (an index into the candidate set,
|
||||
which `input.kb_layout` carries), and remembers it per-device in
|
||||
`~/.local/state/nomarchy/keyboard-layouts` — re-applied silently on later
|
||||
reconnects. The boot-time set (incl. the built-in keyboard) is never
|
||||
prompted. The runtime-remember complement to the declarative
|
||||
`keyboard.devices`; a stateful runtime piece by design. **Pending an
|
||||
on-hardware test** (hotplug isn't verifiable in CI). Remaining (bonus):
|
||||
offer to write a choice into `keyboard.devices` so it graduates to the
|
||||
reproducible config.
|
||||
- Remaining: a multi-layout cycle bind (`hyprctl switchxkblayout` / xkb
|
||||
`grp:` options) for switching layouts on one keyboard; add the
|
||||
`hyprland/language` module to the summer whole-swap themes for parity
|
||||
(the gating doesn't translate to their static JSON). Keep the system
|
||||
(console/initrd) and session layouts in sync (the LUKS-keymap work).
|
||||
choice as a **per-device** `hyprctl keyword device[<name>]:kb_layout` (the
|
||||
runtime twin of the declarative `device{}` blocks — so it isolates that
|
||||
one keyboard and never touches the built-in board), and remembers it. The
|
||||
boot-time set (incl. the built-in keyboard) is never prompted. The
|
||||
runtime-remember complement to the declarative `keyboard.devices`.
|
||||
**Picker verified on hardware (2026-06-18)**; the first cut applied the
|
||||
choice with `switchxkblayout` (a global layout-index flip) and merged the
|
||||
candidate pool into `input.kb_layout`, so a selection leaked onto the
|
||||
laptop keyboard and stuck after unplug — fixed by the per-device keyword +
|
||||
keeping the pool out of the session layout.
|
||||
- ✓ **In-flake state + graduation (2026-06-23):** the remembered picks moved
|
||||
out of `~/.local/state/nomarchy/keyboard-layouts` into the git-tracked
|
||||
state file (`settings.keyboard.devices`, a device-name → layout map) —
|
||||
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
|
||||
(`nomarchy-menu dnd`, in the picker, SUPER+CTRL+D) and a Waybar bell
|
||||
indicator (`custom/notification` via `swaync-client -swb`: shows the
|
||||
@@ -404,21 +763,92 @@ how to override it. Items marked ✓ are shipped.
|
||||
change). **The backend is VM-verified** (a full BTRFS+LUKS install: both
|
||||
`root`+`home` configs, a `/home` timeline snapshot taken, `/home/.snapshots`
|
||||
a real subvolume, the oneshot `Result=success`).
|
||||
- ⚠ **Known bug — btrfs-assistant 2.2 segfaults on launch** in nixpkgs
|
||||
26.05: it crashes inside `libbtrfsutil.so.1.4.0` (a library ABI mismatch,
|
||||
confirmed in the VM regardless of GL/Qt platform — so it's not a VM/GL
|
||||
artifact and will crash on hardware too). The menu wiring is correct and
|
||||
the app is installed, but the GUI doesn't open. The snapshot *backend*
|
||||
(the actual snapshots) is unaffected. Fix path: a nixpkgs override aligning
|
||||
its libbtrfsutil, or wait for an upstream bump; meanwhile the menu entry
|
||||
is a no-op when the binary crashes. Fallback if it lingers: a rofi-based
|
||||
snapshot menu (no btrfs-assistant dependency).
|
||||
- Remaining (optional): a keyboard-driven rofi browse for quick glances;
|
||||
boot-from-snapshot needs a systemd-boot equivalent of grub-btrfs.
|
||||
- **Update awareness:** updates are manual today (`sys-update`/`home-update`);
|
||||
add a Waybar indicator / notification when flake inputs are stale or a new
|
||||
nixpkgs rev is available (optionally with a pending-change count). Augments,
|
||||
never replaces, the explicit rebuild flow.
|
||||
- ✓ **Resolved — the "btrfs-assistant 2.2 segfault" was unprivileged-only**
|
||||
(re-diagnosed 2026-07-04): gdb puts the crash in `btrfs_util_subvolume_
|
||||
iterator_next()` — libbtrfsutil's *unprivileged* subvolume-iteration path
|
||||
in btrfs-progs 6.17.1 (upstream-fixed after 6.17.1, kdave/btrfs-progs
|
||||
`886571653` "re-enable tree search v2 ioctl"; symbol versions were checked
|
||||
and match, so not an ABI/link issue). **As root it runs fine** — VM-proven
|
||||
(root `--version` exits 0, unprivileged exits 139) — and the pkexec
|
||||
launcher runs it as root, so the GUI was never actually broken *when
|
||||
launched right*. What WAS missing distro-wide: **no polkit authentication
|
||||
agent**, so every pkexec prompt in the session failed silently — that's
|
||||
the root cause of "the GUI doesn't open". Fixes: `hyprpolkitagent`
|
||||
(Hyprland's Qt agent, Stylix-themed) now ships via exec-once in
|
||||
`hyprland.nix`; the menu prefers `btrfs-assistant-launcher` again with
|
||||
`nomarchy-snapshots` as fallback; `checks.snapshot-gui` guards the root
|
||||
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
|
||||
toggles — theme, power profile, opt-in services, display, DND — built on
|
||||
the same `nomarchy-theme-sync` / `nomarchy.*` surface the menu already
|
||||
@@ -426,6 +856,57 @@ how to override it. Items marked ✓ are shipped.
|
||||
your theme / essentials" flow (ties into the branding work).
|
||||
|
||||
## Known issues & follow-ups
|
||||
- ✓ **README option tables drift from the live surface** (docs review
|
||||
slice (a), 2026-07-04): a fresh reconcile found 9 undocumented options
|
||||
(autoTimezone, camera.hideIrSensor/irMatch, intel.guc, amd.pstate/
|
||||
vaapi, package, system.stateFile, restic.paths) — rows/mentions added,
|
||||
and the diff is now permanent: `checks.option-docs` walks the option
|
||||
names out of the four option-declaring modules at eval time (their
|
||||
lazy `config` halves never evaluate, so dummy args suffice) and
|
||||
requires each a table row — or a backticked `.leaf` mention for
|
||||
non-enable sub-knobs — plus flags table rows whose option no longer
|
||||
exists (tools/check-option-docs.py).
|
||||
- ✓ **swaync text invisible on summer-day** (Latitude QA, 2026-07-04):
|
||||
the themed CSS paired @subtext on @base, but palette roles are not
|
||||
uniform across themes — summer-day/flexoki-light use subtext as
|
||||
text-*on*-surface (== base) and surface as a dark chip (== text), so
|
||||
body text and widget buttons self-colored. Fix: swaync uses only
|
||||
pairings safe in every palette — @text on @base for text (survey:
|
||||
worst ratio 5.18 across 21 themes) and alpha(@text, 0.1) tints for
|
||||
chips/hovers — guarded permanently by `checks.theme-contrast`
|
||||
(tools/check-theme-contrast.py, WCAG ratios over themes/*.json).
|
||||
The same audit found generated waybar/rofi CSS shared the bug class,
|
||||
exposed on flexoki-light (no whole-swap) — fixed in the follow-on:
|
||||
waybar dim/secondary shades are alpha(@text) tints, rofi @dim/@surface
|
||||
are fg-derived #RRGGBBAA tints, and the contrast check's pairings now
|
||||
cover all three generated surfaces. Status glyph accents
|
||||
(good/warn/bad on base, 2.0–2.7 in some palettes) were left as-is —
|
||||
raising them is palette design, a human call.
|
||||
- ✓ **Identity audit exemptions + import hierarchy** (#69/#70):
|
||||
`tools/audit-theme-design.py` tags expected hue/CVD/ANSI-family noise
|
||||
on white/vantablack/lumon/hackerman/matte-black/miasma as `[identity]`
|
||||
(do not retune into traffic lights). `tools/import-palettes.py` keeps
|
||||
surface≠overlay when ANSI color0==color8, derives light chips from base
|
||||
instead of ANSI black, and documents that roles are first-class — no
|
||||
bulk re-import of shipped themes without a hierarchy pass.
|
||||
- ✓ **Waybar crash on theme switch left the session bar-less** (Latitude
|
||||
hardware QA, 2026-07-04): exec-once has no supervisor, so any crash
|
||||
orphaned the session until relogin — and the switch path itself was
|
||||
crash-prone: `reload_style_on_change` (inotify on style.css) and
|
||||
theme-sync's SIGUSR2 both fired while the HM symlinks flipped, a
|
||||
double-reload race in waybar's in-place reload. Fix: (1) the bar runs
|
||||
under a `nomarchy-waybar` supervisor (waybar.nix) — ANY exit respawns
|
||||
it, with a crash-loop guard (5 fast exits → critical notification,
|
||||
stop); (2) theme-sync now prefers a clean `pkill -x waybar` when it
|
||||
sees the supervisor (a restart with fresh config+style — no reload
|
||||
code path at all), keeping SIGUSR2 only as the fallback for
|
||||
unsupervised/custom bars; `reload_style_on_change` stays for manual
|
||||
`home-update` restyles. VM-verified on the headless software-GL
|
||||
desktop: SIGKILL → new pid; clean kill → respawn; SIGUSR2 → alive.
|
||||
- ✓ **`sys-rebuild`** (Latitude QA request): the no-update twin of
|
||||
`sys-update` — snapshot-first system rebuild against the *current*
|
||||
lock, for config-only changes; `home-update` was already lock-free.
|
||||
README §3/§5 + the motd list all three.
|
||||
- ✓ **Yazi TOML parse error on startup:** yazi 26.x made fetchers'
|
||||
`group` field required, so `[[plugin.prepend_fetchers]]` failed to parse
|
||||
and yazi fell back to presets. Fixed by adding `group = "git"` to both
|
||||
@@ -471,3 +952,14 @@ how to override it. Items marked ✓ are shipped.
|
||||
`color-scheme` reads `1` on a dark theme and flips to `2` after switching
|
||||
to a light one (catppuccin-latte) — so xdg-desktop-portal-gtk does relay
|
||||
the dconf value GTK4/Qt apps read.
|
||||
- ✓ **Laptop docking & Multi-monitor UX**:
|
||||
- Rewrote the display watcher (`nomarchy-display-profile-watch`) to listen immediately to Hyprland's IPC socket via `socat`, removing the 3-second polling delay.
|
||||
- Added a fallback to the `base` layout when unplugging an external monitor to guarantee the laptop lid turns back on.
|
||||
- Added explicit `SUPER+ALT+arrow` keybindings for moving active workspaces across physical monitors.
|
||||
- ✓ **Workstation Polish Pack (Security & Stability)**:
|
||||
- Enabled `security.apparmor.enable` system-wide for defense-in-depth confinement.
|
||||
- Added `boot.kernelParams = [ "panic=10" "oops=panic" ]` to auto-reboot upon catastrophic kernel/driver failures.
|
||||
- Integrated `hyprpicker` (`SUPER+SHIFT+C`) for instant Wayland-native color picking.
|
||||
- ✓ **Theme UI Review completed**:
|
||||
- Preserved the full-color Papirus icon set for Rofi menus to provide a colorful layer independent of the Waybar's monochrome style.
|
||||
- Enforced correct typography scales across `swaync` (notifications) and Rofi menus, natively binding to the active theme's `t.fonts.size` and `t.fonts.ui` rather than hardcoding sizes.
|
||||
|
||||
@@ -13,7 +13,8 @@ than claiming success. "All Nix files parse" is not "the bar renders."
|
||||
```sh
|
||||
nix flake check --no-build # full module-system evaluation, no builds
|
||||
nix-instantiate --parse <file> # syntax-only, works even on macOS
|
||||
python3 -m py_compile pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py
|
||||
git ls-files '*.py' | xargs python3 -m py_compile # all tracked Python
|
||||
bash -n <script>.sh # shell syntax (tools/, installer bits)
|
||||
```
|
||||
|
||||
`nix flake check` needs a Linux machine (or a Linux builder) since all
|
||||
@@ -22,6 +23,17 @@ bad merges — most breakage stops here. It also evaluates the downstream
|
||||
template through `lib.mkFlake` (including a real nixos-hardware profile),
|
||||
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
|
||||
|
||||
```sh
|
||||
@@ -53,13 +65,13 @@ Work through these in order; each one exercises a different layer.
|
||||
|
||||
| # | Check | Verifies |
|
||||
|---|---|---|
|
||||
| 1 | Boots to Hyprland with Tokyo Night wallpaper visible | greetd autologin, awww, session start |
|
||||
| 1 | Boots to Hyprland with the default theme wallpaper visible (Boreal) | greetd autologin, awww, session start |
|
||||
| 2 | Waybar shows at top, themed (blue accent on dark) | HM waybar module, palette baking |
|
||||
| 3 | `SUPER+Return` opens Ghostty with Tokyo Night colors | terminal default, ANSI palette |
|
||||
| 3 | `SUPER+Return` opens Ghostty with the default theme colors (Boreal) | terminal default, ANSI palette |
|
||||
| 4 | `btop` in the terminal is themed | per-theme asset baking |
|
||||
| 5 | `nomarchy-theme-sync list` prints 21 presets | package, baked themes dir |
|
||||
| 5 | `nomarchy-theme-sync list` prints 24 presets | package, baked themes dir |
|
||||
| 6 | `nomarchy-theme-sync apply gruvbox` → state written, `home-manager switch` runs, desktop re-themes, wallpaper changes | the whole engine: state write, pure eval, HM rebuild, wallpaper hook |
|
||||
| 7 | `SUPER+SHIFT+T` cycles wallpapers instantly (try `tokyo-night`: 4 of them) | the runtime wallpaper path |
|
||||
| 7 | `SUPER+SHIFT+T` cycles wallpapers instantly (try `tokyo-night` for 4, or `boreal` for its set) | the runtime wallpaper path |
|
||||
| 8 | `nomarchy-theme-sync apply summer-night` → after the switch the bar has its own identity (light bar, different styling) | whole-swap waybar.css assets |
|
||||
| 9 | Open a GTK app — dark theme matching the palette | Stylix layer |
|
||||
| 10 | `home-manager generations` lists one generation per theme change; activating an older one rolls the theme back | atomicity / rollback story |
|
||||
|
||||
167
docs/VISION.md
Normal file
167
docs/VISION.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Product vision — toward Nomarchy v1.0
|
||||
|
||||
North star for **what the product should feel like**, not a task queue.
|
||||
Pillars stay in [`agent/GOALS.md`](../agent/GOALS.md). Executable work
|
||||
lives only in [`agent/BACKLOG.md`](../agent/BACKLOG.md).
|
||||
|
||||
**How agents use this file**
|
||||
|
||||
1. Read the relevant section when orienting on product work.
|
||||
2. Slice concrete work into **PROPOSED** (or wait for human triage into
|
||||
NOW/NEXT) with a one-paragraph pitch and `VISION § <heading>`.
|
||||
3. Do **not** implement multi-week themes in one iteration — split.
|
||||
4. When a slice ships, leave a ✓ note here or in ROADMAP if the design
|
||||
decision should outlive the backlog line.
|
||||
|
||||
**Philosophy reminder:** opinionated and stable over option sprawl. Prefer
|
||||
safer defaults, one golden path, and menu/doctor surfaces over install-time
|
||||
questionnaires and `nomarchy.apps.*` toggles for bare packages.
|
||||
|
||||
---
|
||||
|
||||
## North-star user (v1 window)
|
||||
|
||||
> **Framework / modern AMD-or-Intel laptop, little or no Nix, wants a
|
||||
> beautiful desktop that never bricks, configured from the menu.**
|
||||
|
||||
Work that does not serve this user for the v1.0 window should stay LATER
|
||||
or PROPOSED unless it unblocks stability.
|
||||
|
||||
---
|
||||
|
||||
## v1.0 release bar
|
||||
|
||||
Ship the **v1** branch pointer only when these are honestly true (human
|
||||
judgment; agents prepare, do not advance `v1`):
|
||||
|
||||
| Bar | Done looks like |
|
||||
|-----|-----------------|
|
||||
| **Install golden path** | Offline (or documented online) install → first boot themed desktop; swap=0 and unattended LUKS contracts correct |
|
||||
| **Default identity** | Boreal is the seed theme (shipped 2026-07-09); picker should have `preview.png` |
|
||||
| **Day-2 confidence** | Doctor covers real failure modes; firmware and fingerprint are discoverable without reading the README |
|
||||
| **Hardware story** | `docs/HARDWARE.md` matches code; option-docs green; i2c/ddcci documented |
|
||||
| **Honesty** | No “done” claims past verified tier; HARDWARE-QUEUE cleared or consciously deferred |
|
||||
| **Docs map** | README + `docs/README.md` + agent README agree on where things live |
|
||||
|
||||
Not required for v1.0: dual-boot, Secure Boot, multi-disk RAID, aarch64,
|
||||
Steam Deck, docs website, binary cache.
|
||||
|
||||
---
|
||||
|
||||
## Theme A — Day-2 confidence (highest product ROI)
|
||||
|
||||
The install is already strong. The gap is **after** first boot.
|
||||
|
||||
| Idea | Intent | Likely home |
|
||||
|------|--------|-------------|
|
||||
| **System › Firmware** | fwupd is on but CLI-only; menu: refresh → list → confirm → update (never auto-flash) | BACKLOG Hardware product |
|
||||
| **Fingerprint menu** | Enroll/list when fprintd present; optional PAM + rebuild note | BACKLOG Hardware product |
|
||||
| **Doctor hardware section** | NM, sink, optional GPU smoke, fprintd, fwupd pending, charge threshold | BACKLOG Hardware product |
|
||||
| **Machine health entry** | One System row → doctor (not five submenus) | Menu / doctor |
|
||||
| **Human rebuild errors** | On failed switch, point at last log lines + `nomarchy-doctor` | pkgs / menu |
|
||||
| **HM pre-activate fail flag** | Durable recovery one-liner on target if bake failed | Installer |
|
||||
|
||||
Reference: [HARDWARE.md](HARDWARE.md) §4–§5, §10.
|
||||
|
||||
---
|
||||
|
||||
## Theme B — First week experience
|
||||
|
||||
| Idea | Intent | Notes |
|
||||
|------|--------|-------|
|
||||
| **Silent first-week card** | One dismissible “you’re set” (menu, theme, wallpaper, network) — not a wizard | Prefer notify or doctor “first boot” section |
|
||||
| **Boreal as default** | Identity on first boot | BACKLOG PROPOSED already |
|
||||
| **Generation readability** | “What changed last rebuild” in plain language | Rollback exists; story is incomplete |
|
||||
| **Post-install hints** | One-shot MOTD/notify for fwupd / fprintd when relevant | Avoid permanent nag |
|
||||
|
||||
---
|
||||
|
||||
## Theme C — Laptop daily driver
|
||||
|
||||
| Idea | Intent | Notes |
|
||||
|------|--------|-------|
|
||||
| **Dock life** | Lid closed + external only, wake, default audio sink on undock | Display profiles exist — polish edge cases |
|
||||
| **Hibernate/sleep confidence** | Doctor: resume device, swap size, clean suspend journal | Trust > new power UI |
|
||||
| **Battery health readout** | Cycles / charge limit where sysfs allows | Report-only |
|
||||
| **Charge-limit instant apply** | Already PROPOSED `[blocked:hw]` | Keep privilege tradeoff explicit |
|
||||
|
||||
---
|
||||
|
||||
## Theme D — Beauty without theme sprawl
|
||||
|
||||
| Idea | Intent | Notes |
|
||||
|------|--------|-------|
|
||||
| **Fewer themes, sharper** | Finish or quarantine neon-glass; invest in whole-swap quality | Contrast stays gated |
|
||||
| **Time-of-day pair** | Auto light/dark (e.g. summer-day ↔ summer-night) from schedule | Still one theme engine |
|
||||
| **Theme switch speed** | Wallpapers artifact split (LATER) if switches still feel slow | GOALS: no second pipeline |
|
||||
|
||||
---
|
||||
|
||||
## Theme E — Menu as the product
|
||||
|
||||
| Idea | Intent | Notes |
|
||||
|------|--------|-------|
|
||||
| Root stays six entries | New features go Tools › / System › | CONVENTIONS |
|
||||
| Secrets module | Only if a vault story is adopted (rofi-rbw deferred) | Don’t tease |
|
||||
| Look & Feel group | Night-light, wallpaper, blur — when enough toggles exist | ROADMAP optional |
|
||||
|
||||
---
|
||||
|
||||
## Theme F — Narrative & community
|
||||
|
||||
| Idea | Intent | Notes |
|
||||
|------|--------|-------|
|
||||
| **Omarchy migrant one-pager** | Bindings/theme/install map | `docs/` short guide |
|
||||
| **Hardware hall of fame** | Install-tested models; invite DMI PRs | Ties to HARDWARE.md §9 |
|
||||
| **60s demo** | Live → install → theme → menu | Outside repo OK |
|
||||
|
||||
---
|
||||
|
||||
## Theme G — Deliberate non-sprawl
|
||||
|
||||
| Idea | Intent |
|
||||
|------|--------|
|
||||
| Starter apps: curate yearly, don’t grow `nomarchy.apps.*` | Template SoT already |
|
||||
| Gaming/creator: comment blocks in template, not new modules | Steam/OBS already services |
|
||||
| Installer stays golden-path | No dual-boot/partition wizard for v1 |
|
||||
|
||||
---
|
||||
|
||||
## Theme H — Quality bar when ideas run dry
|
||||
|
||||
1. Burn down [HARDWARE-QUEUE.md](../agent/HARDWARE-QUEUE.md) (V3 only you can close).
|
||||
2. Fix install contracts (swap=0, unattended LUKS fail-closed).
|
||||
3. Visual ritual: default-theme screenshots before each `v1` fast-forward.
|
||||
4. option-docs + theme-contrast always green on `main`.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope (reaffirm)
|
||||
|
||||
From GOALS non-goals and installer audits — do not “fill the roadmap” with:
|
||||
|
||||
- Binary cache as a product
|
||||
- Multi-DE / GTK4 launcher / second theming pipeline
|
||||
- Repo-wide formatter without a Decision
|
||||
- nixpkgs major bump on `main` (that’s a deliberate `v2`)
|
||||
- Option sprawl for bare package installs
|
||||
|
||||
---
|
||||
|
||||
## Suggested agent slices (promote via PROPOSED → NEXT)
|
||||
|
||||
Small enough for one iteration each; reference this file:
|
||||
|
||||
1. `VISION § v1.0` — default theme → boreal (seed state + fallbacks)
|
||||
2. `VISION § A` — System › Firmware (fwupd wrapper)
|
||||
3. `VISION § A` — doctor hardware section (read-only checks)
|
||||
4. `VISION § A` — fingerprint enroll menu (self-gated)
|
||||
5. `VISION § B` — first-boot dismissible tips (one surface)
|
||||
6. `VISION § A` — human-facing rebuild failure hint
|
||||
7. `VISION § v1.0` — install P0 contracts (swap / unattended LUKS)
|
||||
8. `VISION § D` — neon-glass quarantine or finish
|
||||
9. `VISION § F` — Omarchy migrant doc (short)
|
||||
10. `VISION § H` — HARDWARE-QUEUE session notes only Bernardo runs
|
||||
|
||||
Larger themes (dock life, time-of-day theme, wallpapers split) stay
|
||||
`[big]` until split.
|
||||
24
flake.lock
generated
24
flake.lock
generated
@@ -145,11 +145,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1781063120,
|
||||
"narHash": "sha256-1UIF/mDJluwJQjmmcZ2j1L2+mjYsefe82QCLj0TYSOg=",
|
||||
"lastModified": 1783221248,
|
||||
"narHash": "sha256-ESQnuNHEDChsB4IxoLRhscVahqkDWkTb+qdIz8euYt4=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "baa46aeb6d02e0ba13de67cd35e3d57aedfacf01",
|
||||
"rev": "af2beae5f0fae0a4310cc0e6aef2572f56090353",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -166,11 +166,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1781020964,
|
||||
"narHash": "sha256-fS7xTi2j2iso5Hj7RNZLv/acDlCT+fgMVkVk40A7Uco=",
|
||||
"lastModified": 1783370751,
|
||||
"narHash": "sha256-E+3MIMvKuo9k+K+qLQ9YXzsBzkgHuyVLnsEbN2DFfuc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixos-hardware",
|
||||
"rev": "32c2cd9e46286c4eced3dc6b613c659126bf3cca",
|
||||
"rev": "662bd6e312d2c8b212e32cb377abaee190749320",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -182,11 +182,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1780902259,
|
||||
"narHash": "sha256-q8yYEC5f1mFlQO9RGna4LTc9QrcvWunX6FYp83munkQ=",
|
||||
"lastModified": 1783148766,
|
||||
"narHash": "sha256-uslt2pqShTIXDdAHRHv2QkYLsVdY8Oqwz0EA48/RSM8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "bd0ff2d3eac24699c3664d5966b9ef36f388e2ca",
|
||||
"rev": "a50de1b7d8a586adc18d2395c19de7d6058e6030",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -249,11 +249,11 @@
|
||||
"tinted-zed": "tinted-zed"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1780702455,
|
||||
"narHash": "sha256-+srjPGNy67nKytYwdlepycL51IG6S34sS4MKRZXK8G0=",
|
||||
"lastModified": 1783359251,
|
||||
"narHash": "sha256-HUiCnEVlJ4n+qJlZojiz/zv+P0cqM5zsg1dxpz2J7Mg=",
|
||||
"owner": "nix-community",
|
||||
"repo": "stylix",
|
||||
"rev": "54fa19702f4f2c7f6a981a92850678933588af9a",
|
||||
"rev": "e602ad042f00409f33c8ad2829cd8d59ba345c7e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
584
flake.nix
584
flake.nix
@@ -83,6 +83,10 @@
|
||||
# works on machines that don't check out this repo.
|
||||
themesDir = ./themes;
|
||||
};
|
||||
nomarchy-doctor = final.callPackage ./pkgs/nomarchy-doctor { };
|
||||
nomarchy-control-center = final.callPackage ./pkgs/nomarchy-control-center { };
|
||||
nomarchy-battery-notify = final.callPackage ./pkgs/nomarchy-battery-notify { };
|
||||
nomarchy-detect-hw = final.callPackage ./pkgs/nomarchy-detect-hw { };
|
||||
};
|
||||
|
||||
nixosModules.nomarchy = {
|
||||
@@ -112,6 +116,12 @@
|
||||
packages.${system} = {
|
||||
nomarchy-theme-sync = pkgs.nomarchy-theme-sync;
|
||||
nomarchy-install = nomarchyInstall;
|
||||
# Overlay tools exported for `nix build .#nomarchy-doctor` etc.
|
||||
# (maintainer/CI convenience — modules install them via the overlay).
|
||||
nomarchy-doctor = pkgs.nomarchy-doctor;
|
||||
nomarchy-control-center = pkgs.nomarchy-control-center;
|
||||
nomarchy-battery-notify = pkgs.nomarchy-battery-notify;
|
||||
nomarchy-detect-hw = pkgs.nomarchy-detect-hw;
|
||||
default = pkgs.nomarchy-theme-sync;
|
||||
};
|
||||
|
||||
@@ -134,6 +144,580 @@
|
||||
downstream.nixosConfigurations.default.config.system.build.toplevel;
|
||||
downstream-template-home =
|
||||
downstream.homeConfigurations.me.activationPackage;
|
||||
|
||||
# Every hex-on-hex text pairing the generated swaync CSS uses
|
||||
# must contrast in EVERY palette — summer-day's body text was
|
||||
# invisible on hardware (item 25: subtext==base there). Cheap
|
||||
# (no VM); extend the script's PAIRINGS as more generated
|
||||
# surfaces move to palette-safe roles (item 27: waybar/rofi).
|
||||
theme-contrast = pkgs.runCommand "nomarchy-theme-contrast"
|
||||
{ nativeBuildInputs = [ pkgs.python3 ]; }
|
||||
''
|
||||
python3 ${./tools/check-theme-contrast.py} ${./themes}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
# Whole-swap waybar.css is read raw (no palette prepend) — every
|
||||
# @color it references must be @define-color'd in the same file
|
||||
# (neon-glass shipped zero defines; BACKLOG #62). Also gate
|
||||
# preview.png + non-empty backgrounds/ per preset.
|
||||
theme-wholeswap = pkgs.runCommand "nomarchy-theme-wholeswap"
|
||||
{ nativeBuildInputs = [ pkgs.python3 ]; }
|
||||
''
|
||||
python3 ${./tools/check-theme-wholeswap.py} ${./themes}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
# The README option tables must track the live `nomarchy.*`
|
||||
# surface. The surface is walked at eval time from the four
|
||||
# option-declaring modules (their lazy `config` halves stay
|
||||
# unevaluated), so the live side of the diff can't go stale;
|
||||
# the script wants a table row per option (inline `.leaf`
|
||||
# mentions ok for non-enable sub-knobs) and flags stale rows.
|
||||
option-docs =
|
||||
let
|
||||
lib = nixpkgs.lib;
|
||||
names = prefix: opts: lib.concatLists (lib.mapAttrsToList
|
||||
(n: v:
|
||||
if lib.isOption v
|
||||
then lib.optional (!(v.readOnly or false)) "${prefix}${n}"
|
||||
else names "${prefix}${n}." v)
|
||||
opts);
|
||||
surface = lib.concatLists [
|
||||
(names "nomarchy."
|
||||
((import ./modules/home/options.nix { inherit lib pkgs; config = { }; }).options.nomarchy))
|
||||
(names "nomarchy.system."
|
||||
((import ./modules/nixos/options.nix { inherit lib; config = { }; }).options.nomarchy.system))
|
||||
(names "nomarchy.hardware."
|
||||
((import ./modules/nixos/hardware.nix { inherit lib pkgs; config = { }; }).options.nomarchy.hardware))
|
||||
(names "nomarchy.services."
|
||||
((import ./modules/nixos/services.nix { inherit lib pkgs; config = { }; }).options.nomarchy.services))
|
||||
];
|
||||
in
|
||||
pkgs.runCommand "nomarchy-option-docs"
|
||||
{ nativeBuildInputs = [ pkgs.python3 ]; }
|
||||
''
|
||||
python3 ${./tools/check-option-docs.py} \
|
||||
${pkgs.writeText "option-surface.txt"
|
||||
(lib.concatStringsSep "\n" surface + "\n")} \
|
||||
${./README.md}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
# The state-file friendly-errors contract (item 11): validate
|
||||
# reports broken fixtures by field with a fix, good state
|
||||
# passes, and an invalid `set` is refused BEFORE the write —
|
||||
# the on-disk file must be byte-identical after the attempt.
|
||||
theme-sync-validate = pkgs.runCommand "nomarchy-theme-sync-validate"
|
||||
{ nativeBuildInputs = [ pkgs.python3 pkgs.jq ]; }
|
||||
''
|
||||
set -eu
|
||||
tool=${./pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py}
|
||||
export NOMARCHY_DEFAULT_THEMES=${./themes}
|
||||
|
||||
mkdir good && cp ${./templates/downstream/theme-state.json} good/theme-state.json
|
||||
chmod +w good/theme-state.json
|
||||
NOMARCHY_PATH=$PWD/good python3 "$tool" validate
|
||||
|
||||
mkdir syntax && printf '{ "slug": "x", }' > syntax/theme-state.json
|
||||
if NOMARCHY_PATH=$PWD/syntax python3 "$tool" validate 2>err; then
|
||||
echo "FAIL: syntax fixture accepted"; exit 1; fi
|
||||
grep -q "syntax error at line" err
|
||||
|
||||
mkdir badhex && jq '.colors.accent = "7aa2f7"' \
|
||||
good/theme-state.json > badhex/theme-state.json
|
||||
if NOMARCHY_PATH=$PWD/badhex python3 "$tool" validate 2>err; then
|
||||
echo "FAIL: bad hex accepted"; exit 1; fi
|
||||
grep -q "colors.accent" err && grep -q "fix:" err
|
||||
|
||||
mkdir badtype && jq '.ui.gapsOut = "12"' \
|
||||
good/theme-state.json > badtype/theme-state.json
|
||||
if NOMARCHY_PATH=$PWD/badtype python3 "$tool" validate 2>err; then
|
||||
echo "FAIL: bad type accepted"; exit 1; fi
|
||||
grep -q "ui.gapsOut" err
|
||||
|
||||
mkdir unknown && jq '.extraKey = 1' \
|
||||
good/theme-state.json > unknown/theme-state.json
|
||||
NOMARCHY_PATH=$PWD/unknown python3 "$tool" validate \
|
||||
| grep -q "unknown top-level key"
|
||||
|
||||
cp good/theme-state.json before.json
|
||||
if NOMARCHY_PATH=$PWD/good python3 "$tool" --quiet \
|
||||
set ui.gapsOut '"12"' --no-switch 2>err; then
|
||||
echo "FAIL: invalid set was written"; exit 1; fi
|
||||
grep -q "refusing to write" err
|
||||
cmp good/theme-state.json before.json
|
||||
|
||||
touch $out
|
||||
'';
|
||||
|
||||
# Installer hardware DB ↔ nixos-hardware (item 49): every module
|
||||
# name the DB table and the CPU/GPU/chassis autodetect reference
|
||||
# must exist in the PINNED nixos-hardware.nixosModules — a lock
|
||||
# bump can rename a module upstream (the X1 Carbon → x1-Nth-gen
|
||||
# churn) and break profiled installs on just the matching DMI,
|
||||
# invisible to any VM. The available-set is generated here from
|
||||
# attrNames — the same value fed to nomarchy-install — so it
|
||||
# tracks the lock automatically.
|
||||
hardware-db-modules = pkgs.runCommand "nomarchy-hardware-db-modules"
|
||||
{ nativeBuildInputs = [ pkgs.python3 ]; }
|
||||
''
|
||||
python3 ${./tools/check-hardware-db.py} \
|
||||
${./pkgs/nomarchy-install/hardware-db.sh} \
|
||||
${pkgs.writeText "nixos-hardware-modules.txt"
|
||||
(nixpkgs.lib.concatStringsSep "\n"
|
||||
(builtins.attrNames nixos-hardware.nixosModules))}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
# compose-lock.py offline-composer contract (item 49): the lock
|
||||
# the installer writes on the target with no network must root at
|
||||
# nomarchy, carry every upstream node one level down with its
|
||||
# `follows` paths rebased, and fail closed on a missing narHash or
|
||||
# a pre-existing nomarchy node. Pure — runs the script on fixtures.
|
||||
installer-compose-lock = pkgs.runCommand "nomarchy-installer-compose-lock"
|
||||
{ nativeBuildInputs = [ pkgs.python3 ]; }
|
||||
''
|
||||
python3 ${./tools/check-compose-lock.py} \
|
||||
${./pkgs/nomarchy-install/compose-lock.py}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
# Installer safety guards (item 54): offline compose-lock fail-closed
|
||||
# (no network flake lock), disk-signature warn regex, pre-activate
|
||||
# recovery hint path, password floors. Pure source+regex contract.
|
||||
installer-safety = pkgs.runCommand "nomarchy-installer-safety"
|
||||
{ nativeBuildInputs = [ pkgs.python3 ]; }
|
||||
''
|
||||
python3 ${./tools/check-install-safety.py} \
|
||||
${./pkgs/nomarchy-install/nomarchy-install.sh}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
# Installer share ↔ templates/downstream SoT (item 72): the four
|
||||
# files default.nix copies into share/.../template/ must stay
|
||||
# byte-identical to the checkout template (single machine-seed
|
||||
# SoT; no thinner second catalog). Cheap package build + cmp.
|
||||
template-sot = pkgs.runCommand "nomarchy-template-sot"
|
||||
{ nativeBuildInputs = [ pkgs.python3 ]; }
|
||||
''
|
||||
python3 ${./tools/check-template-sot.py} \
|
||||
${nomarchyInstall}/share/nomarchy-install/template \
|
||||
${./templates/downstream}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
# The installer's disko arg contract (item 49): swapSize "0"/"0G"
|
||||
# (bare "0" is what the swap=0 install passes) must yield NO @swap
|
||||
# subvolume, a sized value a correctly-sized one, and withLuks must
|
||||
# gate the LUKS wrapper around the BTRFS root. Pure eval of
|
||||
# disko-config.nix (a plain function) — no disko, no VM.
|
||||
installer-disko =
|
||||
let
|
||||
diskoFor = args: import ./pkgs/nomarchy-install/disko-config.nix
|
||||
({ mainDrive = "/dev/vda"; } // args);
|
||||
rootContent = args:
|
||||
(diskoFor args).disko.devices.disk.main.content.partitions.root.content;
|
||||
btrfsOf = c: if c.type == "luks" then c.content else c;
|
||||
subvols = args: (btrfsOf (rootContent args)).subvolumes;
|
||||
hasSwap = args: builtins.hasAttr "@swap" (subvols args);
|
||||
expect = cond: msg: nixpkgs.lib.assertMsg cond "installer-disko: ${msg}";
|
||||
ok =
|
||||
expect (! hasSwap { swapSize = "0"; })
|
||||
''swapSize "0" produced an @swap subvol''
|
||||
&& expect (! hasSwap { swapSize = "0G"; })
|
||||
''swapSize "0G" produced an @swap subvol''
|
||||
&& expect (hasSwap { swapSize = "2G"; })
|
||||
''swapSize "2G" produced no @swap subvol''
|
||||
&& expect ((subvols { swapSize = "2G"; })."@swap".swap.swapfile.size == "2G")
|
||||
''the @swap swapfile size is not "2G"''
|
||||
&& expect ((rootContent { withLuks = true; }).type == "luks")
|
||||
"withLuks=true did not wrap root in luks"
|
||||
&& expect ((rootContent { withLuks = true; }).name == "crypted")
|
||||
"the luks device is not named 'crypted'"
|
||||
&& expect ((rootContent { withLuks = false; }).type == "btrfs")
|
||||
"withLuks=false did not put btrfs directly on root"
|
||||
&& expect (builtins.hasAttr "@" (subvols { withLuks = false; }))
|
||||
"the no-luks root lost its @ subvolume";
|
||||
in
|
||||
assert ok; pkgs.runCommand "nomarchy-installer-disko" { } "touch $out";
|
||||
|
||||
# Hyprland windowrule dead-syntax guard (item 49): build the
|
||||
# GENERATED hyprland.conf (the ISO's HM output) and fail on a
|
||||
# regression to the pre-0.53 grammar — the `windowrulev2` keyword,
|
||||
# rule-first order (`float, class:^…$`), or a bare `<prop>:`
|
||||
# matcher. That syntax shipped a red config-error banner on every
|
||||
# boot (fixed in ed7fd93); this locks the correct form. Building
|
||||
# one config file is a pure derivation, not a VM.
|
||||
windowrule-syntax =
|
||||
let
|
||||
hyprconf = self.nixosConfigurations.nomarchy-live.config
|
||||
.home-manager.users.${username}.xdg.configFile."hypr/hyprland.conf".source;
|
||||
in
|
||||
pkgs.runCommand "nomarchy-windowrule-syntax"
|
||||
{ nativeBuildInputs = [ pkgs.python3 ]; }
|
||||
''
|
||||
python3 ${./tools/check-windowrule-syntax.py} ${hyprconf}
|
||||
touch $out
|
||||
'';
|
||||
|
||||
# nomarchy-doctor's contract: an induced failed unit flips the
|
||||
# sheet to ✖/exit-1 and names the unit; with the failure
|
||||
# cleared it reports healthy/exit-0. Minimal node (just the
|
||||
# package) — the disk/flake/snapper checks self-skip in a VM.
|
||||
# Live-ISO install affordance (BACKLOG #57): desktop entry baked
|
||||
# into the live host's HM generation + installer on PATH + Tools
|
||||
# menu gate in nomarchy-menu. Builds the real nomarchy-live HM
|
||||
# package (not a full ISO) — V2 file-level smoke of the same
|
||||
# artifacts the ISO ships.
|
||||
live-install-entry =
|
||||
let
|
||||
liveHm =
|
||||
self.nixosConfigurations.nomarchy-live.config.home-manager.users.${username};
|
||||
gen = liveHm.home.activationPackage;
|
||||
in
|
||||
pkgs.runCommand "nomarchy-live-install-entry"
|
||||
{
|
||||
nativeBuildInputs = [ pkgs.gnugrep pkgs.findutils ];
|
||||
passAsFile = [ ];
|
||||
}
|
||||
''
|
||||
set -euo pipefail
|
||||
desk=$(find ${gen}/home-path/share/applications \
|
||||
-name nomarchy-install.desktop | head -1)
|
||||
test -n "$desk"
|
||||
grep -q '^Name=Install Nomarchy$' "$desk"
|
||||
grep -q '^Terminal=true$' "$desk"
|
||||
grep -q '^Exec=nomarchy-install$' "$desk"
|
||||
test -x ${nomarchyInstall}/bin/nomarchy-install
|
||||
# Tools › Install Nomarchy self-gate lives in nomarchy-menu
|
||||
# (rofi.nix), which is on the live HM path.
|
||||
menu=$(find ${gen}/home-path/bin -name nomarchy-menu | head -1)
|
||||
test -n "$menu"
|
||||
grep -q 'Install Nomarchy' "$menu"
|
||||
echo "live-install-entry: ok desk=$desk menu=$menu"
|
||||
touch $out
|
||||
'';
|
||||
|
||||
doctor = pkgs.testers.runNixOSTest {
|
||||
name = "nomarchy-doctor";
|
||||
nodes.machine = { ... }: {
|
||||
environment.systemPackages = [ pkgs.nomarchy-doctor ];
|
||||
systemd.services.doomed = {
|
||||
description = "deliberately failing unit (doctor test)";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.coreutils}/bin/false";
|
||||
};
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
};
|
||||
};
|
||||
testScript = ''
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
machine.wait_until_succeeds("systemctl is-failed doomed.service")
|
||||
out = machine.fail("nomarchy-doctor 2>&1")
|
||||
assert "doomed.service" in out, f"doctor did not name the failed unit:\n{out}"
|
||||
assert "journalctl" in out, f"doctor did not print a fix:\n{out}"
|
||||
machine.succeed("systemctl reset-failed")
|
||||
# The healthy run also exercises the #44 hardware section: in a
|
||||
# bare VM every probe (nmcli/wpctl/vainfo/fprintd/fwupd) is
|
||||
# absent, so each check must self-gate to a skip row and the
|
||||
# sheet must still exit 0 — i.e. the section runs without
|
||||
# erroring under `set -euo pipefail`.
|
||||
healthy = machine.succeed("nomarchy-doctor 2>&1")
|
||||
for probe in ["NetworkManager", "fingerprint", "fwupd", "VA-API"]:
|
||||
assert probe in healthy, f"doctor hardware section missing a self-gated '{probe}' row:\n{healthy}"
|
||||
'';
|
||||
};
|
||||
|
||||
# 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,
|
||||
)
|
||||
'';
|
||||
};
|
||||
|
||||
# Unit test of the monitor-rule overlay (modules/home/
|
||||
# monitor-rules.nix — pure, imported directly, no HM eval): the
|
||||
# active profile must replace base entries whole by name, leave
|
||||
# unnamed base outputs alone, and the disable-guard must stop a
|
||||
# stale menu resolution pick (settings.monitors) resurrecting a
|
||||
# panel the profile disables. Junk in settings.displayProfile
|
||||
# degrades to base config, never an eval error.
|
||||
display-profiles =
|
||||
let
|
||||
inherit (nixpkgs) lib;
|
||||
monitorLib = import ./modules/home/monitor-rules.nix { inherit lib; };
|
||||
entry = a: monitorLib.defaults // a;
|
||||
fixture = {
|
||||
base = [
|
||||
(entry { name = "eDP-1"; position = "0x0"; })
|
||||
(entry { name = "HDMI-A-1"; position = "auto-right"; })
|
||||
];
|
||||
profiles.docked = [
|
||||
(entry { name = "eDP-1"; resolution = "disable"; })
|
||||
(entry { name = "DP-3"; position = "0x0"; })
|
||||
];
|
||||
resOverrides."eDP-1" = "1920x1080@60"; # stale pick — must stay dead
|
||||
};
|
||||
rulesFor = active: map monitorLib.rule
|
||||
(monitorLib.resolve (fixture // { inherit active; }));
|
||||
docked = rulesFor "docked";
|
||||
junk = rulesFor 42; # hand-edited state (validator only warns)
|
||||
expect = cond: msg: lib.assertMsg cond
|
||||
"display-profiles: ${msg} (rendered: ${builtins.toJSON docked})";
|
||||
ok =
|
||||
expect (lib.elem "eDP-1, disable" docked)
|
||||
"the profile did not disable eDP-1"
|
||||
&& expect (! lib.any (lib.hasInfix "1920x1080") docked)
|
||||
"a stale resolution pick resurrected the disabled panel"
|
||||
&& expect (lib.elem "DP-3, preferred, 0x0, 1" docked)
|
||||
"the profile-only output's rule is missing"
|
||||
&& expect (lib.elem "HDMI-A-1, preferred, auto-right, 1" docked)
|
||||
"an unnamed base output lost its rule"
|
||||
&& expect (lib.elem "eDP-1, 1920x1080@60, 0x0, 1" junk)
|
||||
"junk active profile must degrade to base config (with the pick applied)"
|
||||
&& expect (monitorLib.workspaceRule "1" "DP-3" == "1, monitor:DP-3")
|
||||
"workspace pin did not render as a Hyprland workspace rule";
|
||||
in
|
||||
assert ok; pkgs.runCommand "nomarchy-display-profiles" { } "touch $out";
|
||||
|
||||
# The low-battery watcher's crossing logic: one toast per downward
|
||||
# crossing (normal at 25%, critical at 10%), re-armed by charging.
|
||||
# test_power fakes a battery whose capacity/status we script;
|
||||
# notify-send is shimmed to a log via PATH (the package resolves
|
||||
# it from PATH for exactly this reason). What stays on-hardware:
|
||||
# the real toast rendering through swaync in a session.
|
||||
battery-notify = pkgs.testers.runNixOSTest {
|
||||
name = "nomarchy-battery-notify";
|
||||
nodes.machine = { ... }: {
|
||||
boot.kernelModules = [ "test_power" ]; # fake battery + Mains
|
||||
environment.systemPackages = [ pkgs.nomarchy-battery-notify ];
|
||||
};
|
||||
testScript = ''
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# notify-send shim: log the calls instead of needing a session bus.
|
||||
machine.succeed(
|
||||
"mkdir -p /shim && printf '#!/bin/sh\necho \"$*\" >> /tmp/notifications\n'"
|
||||
" > /shim/notify-send && chmod +x /shim/notify-send"
|
||||
)
|
||||
|
||||
# Self-gate: with the fake battery gone, the watcher exits 0 at once.
|
||||
machine.succeed("modprobe -r test_power")
|
||||
machine.succeed("timeout 5 nomarchy-battery-notify 1")
|
||||
machine.succeed("modprobe test_power")
|
||||
|
||||
# Healthy level while discharging: silence.
|
||||
machine.succeed("echo discharging > /sys/module/test_power/parameters/battery_status")
|
||||
machine.succeed("echo 80 > /sys/module/test_power/parameters/battery_capacity")
|
||||
machine.succeed(
|
||||
"systemd-run --unit=batwatch --setenv=PATH=/shim:/run/current-system/sw/bin"
|
||||
" /run/current-system/sw/bin/nomarchy-battery-notify 1"
|
||||
)
|
||||
machine.sleep(3)
|
||||
machine.fail("test -s /tmp/notifications")
|
||||
|
||||
# Crossing 25% fires the low toast exactly once, not per poll.
|
||||
machine.succeed("echo 20 > /sys/module/test_power/parameters/battery_capacity")
|
||||
machine.wait_until_succeeds("grep -q 'Battery low' /tmp/notifications")
|
||||
machine.succeed("echo 15 > /sys/module/test_power/parameters/battery_capacity")
|
||||
machine.sleep(3)
|
||||
assert machine.succeed("grep -c 'Battery low' /tmp/notifications").strip() == "1"
|
||||
|
||||
# Crossing 10% escalates to a critical-urgency toast, once.
|
||||
machine.succeed("echo 5 > /sys/module/test_power/parameters/battery_capacity")
|
||||
machine.wait_until_succeeds("grep -q 'Battery critical' /tmp/notifications")
|
||||
assert "critical" in machine.succeed("grep 'Battery critical' /tmp/notifications")
|
||||
machine.sleep(3)
|
||||
assert machine.succeed("grep -c 'Battery critical' /tmp/notifications").strip() == "1"
|
||||
|
||||
# Back on the charger → re-armed: the next drain notifies again.
|
||||
machine.succeed("echo charging > /sys/module/test_power/parameters/battery_status")
|
||||
machine.succeed("echo 80 > /sys/module/test_power/parameters/battery_capacity")
|
||||
machine.sleep(3)
|
||||
machine.succeed("echo discharging > /sys/module/test_power/parameters/battery_status")
|
||||
machine.succeed("echo 20 > /sys/module/test_power/parameters/battery_capacity")
|
||||
machine.wait_until_succeeds(
|
||||
"test \"$(grep -c 'Battery low' /tmp/notifications)\" = 2"
|
||||
)
|
||||
'';
|
||||
};
|
||||
|
||||
# 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 ────────────────────────────────────────────
|
||||
|
||||
@@ -1,29 +1,89 @@
|
||||
# Live ISO host — boot the full Nomarchy desktop from a USB stick or QEMU
|
||||
# without touching the disk. No installer yet (see roadmap); this target
|
||||
# exists to test the distro end-to-end on real hardware.
|
||||
# without touching the disk. The live session bundles nomarchy-install
|
||||
# (gum + disko + mkFlake) for a real install; this target also lets you
|
||||
# test the distro end-to-end on real hardware before committing to disk.
|
||||
{ lib, pkgs, username, nomarchySrc, ... }:
|
||||
|
||||
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
|
||||
# live theme-state.json (tokyo-night by default). Shows on the isolinux
|
||||
# (BIOS) boot menu; UEFI/GRUB still uses the stock theme (see roadmap).
|
||||
# live theme-state.json (Boreal by default). The same composed image
|
||||
# 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);
|
||||
isoColor = key: fallback: lib.removePrefix "#" ((state.colors or { }).${key} or fallback);
|
||||
accent = isoColor "accent" "B79BE8";
|
||||
base = isoColor "base" "21272F";
|
||||
subtext = isoColor "subtext" "97A3B2";
|
||||
|
||||
isoSplash = pkgs.runCommand "nomarchy-iso-splash.png"
|
||||
{ nativeBuildInputs = [ pkgs.imagemagick pkgs.librsvg ]; } ''
|
||||
rsvg-convert -h 320 ${../modules/nixos/branding/logo.svg} > logo.png
|
||||
magick logo.png -fill "#${isoColor "accent" "7aa2f7"}" -colorize 100 logo-c.png
|
||||
magick -size 1920x1080 xc:"#${isoColor "base" "1a1b26"}" \
|
||||
magick logo.png -fill "#${accent}" -colorize 100 logo-c.png
|
||||
magick -size 1920x1080 xc:"#${base}" \
|
||||
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
|
||||
{
|
||||
networking.hostName = "nomarchy-live";
|
||||
|
||||
isoImage.volumeID = lib.mkForce "NOMARCHY_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
|
||||
# ISO is the desktop, so re-enable what it strips. Above all
|
||||
@@ -112,6 +172,20 @@ in
|
||||
# regression). Installed systems keep idle management.
|
||||
nomarchy.idle.enable = false;
|
||||
|
||||
# Durable install affordance (BACKLOG #57): always-visible desktop
|
||||
# entry, not only the 3s toast / getty helpLine / tribal knowledge.
|
||||
# Tools › Install Nomarchy is self-gated on this package in rofi.nix.
|
||||
xdg.desktopEntries.nomarchy-install = {
|
||||
name = "Install Nomarchy";
|
||||
genericName = "Installer";
|
||||
comment = "Install Nomarchy to this machine's disk";
|
||||
exec = "nomarchy-install";
|
||||
terminal = true;
|
||||
icon = "system-software-install";
|
||||
categories = [ "System" "Settings" ];
|
||||
startupNotify = true;
|
||||
};
|
||||
|
||||
wayland.windowManager.hyprland.settings = {
|
||||
# QEMU (and some panels) report a tiny "preferred" mode; ask for
|
||||
# the highest resolution instead.
|
||||
@@ -119,7 +193,7 @@ in
|
||||
# Welcome toast once the session is up (concatenated onto the
|
||||
# base exec-once list).
|
||||
exec-once = [
|
||||
"sh -c 'sleep 3; notify-send -a Nomarchy \"Welcome to Nomarchy\" \"SUPER+Return terminal · SUPER+T themes · install with nomarchy-install\"'"
|
||||
"sh -c 'sleep 3; notify-send -a Nomarchy \"Welcome to Nomarchy\" \"SUPER+Return terminal · SUPER+T themes · Install Nomarchy app or nomarchy-install\"'"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
26
lib.nix
26
lib.nix
@@ -5,7 +5,16 @@
|
||||
# in flake.nix remain the escape hatch for power users.
|
||||
{ nixpkgs, home-manager, nixos-hardware, nomarchy }:
|
||||
|
||||
let
|
||||
inherit (nixpkgs) lib;
|
||||
|
||||
# Shared pure reader (modules/theme-state-read.nix) — re-exported so
|
||||
# power users composing without mkFlake can call it too.
|
||||
readThemeState = import ./modules/theme-state-read.nix { inherit lib; };
|
||||
in
|
||||
{
|
||||
inherit readThemeState;
|
||||
|
||||
mkFlake =
|
||||
{ src # the downstream flake directory (./.)
|
||||
, username # login name; also the homeConfigurations attr
|
||||
@@ -13,8 +22,6 @@
|
||||
, system ? "x86_64-linux"
|
||||
}:
|
||||
let
|
||||
inherit (nixpkgs) lib;
|
||||
|
||||
# One profile or several (the installer's autodetection emits a few
|
||||
# common-* modules alongside the model-specific one).
|
||||
profileNames =
|
||||
@@ -51,8 +58,17 @@
|
||||
# and the standalone HM desktop see the same package set.
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
|
||||
# Early fail-closed gate: missing/empty/non-object theme-state.json
|
||||
# throws here with a template + validate pointer, before module
|
||||
# evaluation buries a raw readFile/fromJSON stack. Field-level
|
||||
# schema still runs in modules/home/theme.nix after defaults merge.
|
||||
# Forced via seq on the whole return set — attrNames alone must not
|
||||
# skip the check (Nix is lazy on unused let bindings and attr values).
|
||||
statePath = src + "/theme-state.json";
|
||||
_themeState = readThemeState statePath;
|
||||
in
|
||||
{
|
||||
builtins.seq _themeState {
|
||||
# System layer — rebuilt rarely:
|
||||
# sudo nixos-rebuild switch --flake .#default
|
||||
nixosConfigurations.default = nixpkgs.lib.nixosSystem {
|
||||
@@ -67,7 +83,7 @@
|
||||
{ environment.systemPackages = [ home-manager.packages.${system}.home-manager ]; }
|
||||
# System-side theme consumers (Plymouth splash background)
|
||||
# read the same JSON the desktop does.
|
||||
{ nomarchy.system.stateFile = src + "/theme-state.json"; }
|
||||
{ nomarchy.system.stateFile = statePath; }
|
||||
]
|
||||
++ hardwareModules
|
||||
++ [
|
||||
@@ -87,7 +103,7 @@
|
||||
{
|
||||
# Written by nomarchy-theme-sync; reading it is pure — the
|
||||
# file is part of the downstream flake's source.
|
||||
nomarchy.stateFile = src + "/theme-state.json";
|
||||
nomarchy.stateFile = statePath;
|
||||
home = {
|
||||
inherit username;
|
||||
homeDirectory = "/home/${username}";
|
||||
|
||||
29
modules/home/battery-notify.nix
Normal file
29
modules/home/battery-notify.nix
Normal file
@@ -0,0 +1,29 @@
|
||||
# Low-battery notifications (nomarchy.batteryNotify) — the bar colors the
|
||||
# battery @warn/@bad at 25/10% (waybar.nix battery.states) but nothing
|
||||
# *notified*; this watcher fires a toast at those same thresholds. Session-
|
||||
# side and self-gating on a battery being present (the powerprofile-script
|
||||
# pattern — no system→home coupling), so it's a silent no-op on desktops.
|
||||
# swaync shows critical toasts until dismissed (timeout-critical = 0), so
|
||||
# the 10% one can't slip by unseen. Logic lives in pkgs/nomarchy-battery-
|
||||
# notify (overlay), where checks.battery-notify exercises it.
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
config = lib.mkIf config.nomarchy.batteryNotify.enable {
|
||||
systemd.user.services.nomarchy-battery-notify = {
|
||||
Unit = {
|
||||
Description = "Low-battery notifications (25/10%, the bar's thresholds)";
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
After = [ "graphical-session.target" ];
|
||||
};
|
||||
Service = {
|
||||
# The script resolves notify-send from PATH (that's what the VM
|
||||
# check shims); here libnotify provides the real one.
|
||||
Environment = "PATH=${lib.makeBinPath [ pkgs.libnotify ]}";
|
||||
ExecStart = "${pkgs.nomarchy-battery-notify}/bin/nomarchy-battery-notify";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
# Nomarchy — Home Manager entry point.
|
||||
# Consume this via homeModules.nomarchy (flake.nix), which also pulls in
|
||||
# the stylix home module that stylix.nix configures.
|
||||
{ config, pkgs, ... }:
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
@@ -18,25 +18,49 @@
|
||||
./yazi.nix # flagship TUI file manager, themed + plugins
|
||||
./osd.nix # swayosd volume/brightness OSD, themed
|
||||
./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
|
||||
./keys.nix # gpg-agent (fronts SSH) + pinentry-qt
|
||||
./fastfetch.nix # system info with the themed Nomarchy logo
|
||||
./viewers.nix # zathura (Stylix-themed) + imv — PDF/image viewing
|
||||
./mime.nix # default applications (mimeapps.list), degrades with the suite
|
||||
./recording.nix # nomarchy-record: screen recording behind Capture + the bar ⏺
|
||||
./battery-notify.nix # low-battery toasts at the bar's 25/10% thresholds
|
||||
./satty.nix # satty screenshot annotation tool, themed
|
||||
];
|
||||
|
||||
# Clipboard history (wl-paste watcher); browsed via the SUPER+CTRL+V
|
||||
# menu module.
|
||||
services.cliphist.enable = true;
|
||||
|
||||
# Automount removable media (USB drives) and provide safe-removal notifications.
|
||||
services.udiskie.enable = true;
|
||||
|
||||
# Microphone noise cancellation (rnnoise) and audio EQ.
|
||||
services.easyeffects.enable = true;
|
||||
|
||||
# Wifi from the bar: nm-applet lives in waybar's tray (SNI flag via
|
||||
# preferStatusNotifierItems — without it there is no tray icon).
|
||||
services.network-manager-applet.enable = true;
|
||||
xsession.preferStatusNotifierItems = true;
|
||||
|
||||
home.stateVersion = "26.05";
|
||||
# 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 = lib.mkDefault "26.05";
|
||||
|
||||
home.packages = with pkgs; [
|
||||
awww # wallpaper daemon with animated transitions (the swww fork)
|
||||
libnotify
|
||||
hyprpicker
|
||||
];
|
||||
|
||||
home.sessionVariables = {
|
||||
|
||||
@@ -8,11 +8,16 @@ let
|
||||
c = t.colors;
|
||||
in
|
||||
{
|
||||
programs.ghostty = lib.mkIf config.nomarchy.ghostty.enable {
|
||||
# Ghostty is ALWAYS installed — it's Nomarchy's default terminal and is
|
||||
# load-bearing (SUPER+E file manager, the calendar launcher's classed
|
||||
# window, etc.), so the distro enforces it. `nomarchy.ghostty.enable` now
|
||||
# gates only whether Nomarchy's theming/config is applied — a user can
|
||||
# keep ghostty but drop our config, they just can't remove ghostty itself.
|
||||
programs.ghostty = {
|
||||
enable = true;
|
||||
enableBashIntegration = true;
|
||||
|
||||
settings = {
|
||||
settings = lib.mkIf config.nomarchy.ghostty.enable {
|
||||
# ── Typography (from theme-state.json) ────────────────────────
|
||||
font-family = t.fonts.mono;
|
||||
font-size = t.fonts.size;
|
||||
|
||||
@@ -8,6 +8,10 @@ let
|
||||
c = t.colors;
|
||||
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
|
||||
# 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
|
||||
@@ -27,6 +31,34 @@ let
|
||||
${pkgs.swayosd}/bin/swayosd-client --custom-icon "$icon" --custom-progress "$2"
|
||||
'';
|
||||
|
||||
# Launch-or-focus (nomarchy.launchOrFocus, item 17): focus the app's
|
||||
# window if one exists (case-insensitive class match), launch it
|
||||
# otherwise. Self-gates: an uninstalled command notifies instead of
|
||||
# failing silently, so a bind survives the app being removed from the
|
||||
# suite. rofi.nix renders the same entries into the SUPER+? cheatsheet.
|
||||
focusOrLaunch = pkgs.writeShellScriptBin "nomarchy-focus-or-launch" ''
|
||||
class="''${1:-}"; shift || true
|
||||
[ -n "$class" ] && [ $# -gt 0 ] || {
|
||||
echo "usage: nomarchy-focus-or-launch <class> <command...>" >&2; exit 64
|
||||
}
|
||||
if hyprctl clients -j 2>/dev/null \
|
||||
| ${pkgs.jq}/bin/jq -e --arg c "$class" \
|
||||
'any(.[]; (.class | ascii_downcase) == ($c | ascii_downcase))' >/dev/null; then
|
||||
exec hyprctl dispatch focuswindow "class:(?i)^$class\$"
|
||||
fi
|
||||
command -v "$1" >/dev/null 2>&1 \
|
||||
|| { notify-send "Launch" "$1 is not installed."; exit 0; }
|
||||
exec "$@"
|
||||
'';
|
||||
lofEntries = map
|
||||
(e: e // {
|
||||
command = if e.command == "" then lib.toLower e.class else e.command;
|
||||
})
|
||||
config.nomarchy.launchOrFocus;
|
||||
lofBinds = map
|
||||
(e: "${e.mods}, ${e.key}, exec, ${focusOrLaunch}/bin/nomarchy-focus-or-launch ${e.class} ${e.command}")
|
||||
lofEntries;
|
||||
|
||||
# SUPER+1..9 / SUPER+SHIFT+1..9 workspace binds, generated.
|
||||
workspaceBinds = builtins.concatLists (builtins.genList
|
||||
(i:
|
||||
@@ -42,23 +74,169 @@ let
|
||||
keybinds = import ./keybinds.nix;
|
||||
mkBind = b: "${b.mods}, ${b.key}, ${b.action}";
|
||||
|
||||
# A nomarchy.monitors entry -> a Hyprland `monitor` rule. Unset optional
|
||||
# fields are omitted; `resolution = "disable"` collapses to the short form.
|
||||
monitorRule = m:
|
||||
if m.resolution == "disable" then "${m.name}, disable"
|
||||
else lib.concatStringsSep ", " (
|
||||
[ m.name m.resolution (toString m.position) (toString m.scale) ]
|
||||
++ lib.optionals (m.transform != null) [ "transform" (toString m.transform) ]
|
||||
++ lib.optionals (m.mirror != null) [ "mirror" m.mirror ]
|
||||
++ lib.optionals (m.bitdepth != null) [ "bitdepth" (toString m.bitdepth) ]
|
||||
++ lib.optionals (m.vrr != null) [ "vrr" (toString m.vrr) ]
|
||||
++ lib.optional (m.extra != "") m.extra
|
||||
);
|
||||
# Monitor-rule composition (rendering + the display-profile/resolution
|
||||
# overlays) lives in ./monitor-rules.nix — pure, so
|
||||
# checks.display-profiles unit-tests the overlay semantics directly.
|
||||
# The layering story (profiles replace whole-by-name, menu resolution
|
||||
# picks apply field-level, disable-guard) is documented there.
|
||||
monitorLib = import ./monitor-rules.nix { inherit lib; };
|
||||
monitorRule = monitorLib.rule;
|
||||
|
||||
# Keyboard layout candidates: the primary nomarchy.keyboard.layout (split on
|
||||
# commas) plus any nomarchy.keyboard.layouts. Drives input.kb_layout so all
|
||||
# of them are switchable, and feeds the watcher's picker.
|
||||
kbLayouts = lib.unique (
|
||||
# The night-light pattern: nomarchy-display-profile applies rules live
|
||||
# via hyprctl and writes settings.displayProfile with --no-switch;
|
||||
# resolve bakes the same choice here at the next rebuild.
|
||||
# Profiles normalized to { monitors; workspaces; } — the option also
|
||||
# accepts the original bare-list-of-monitors shape.
|
||||
profiles = lib.mapAttrs
|
||||
(_: p: if builtins.isList p then { monitors = p; workspaces = { }; } else p)
|
||||
config.nomarchy.displayProfiles;
|
||||
resolvedMonitors = monitorLib.resolve {
|
||||
base = config.nomarchy.monitors;
|
||||
profiles = lib.mapAttrs (_: p: p.monitors) profiles;
|
||||
active = config.nomarchy.settings.displayProfile or "";
|
||||
resOverrides = config.nomarchy.settings.monitors;
|
||||
};
|
||||
|
||||
# The active profile's workspace→output pins, baked as `workspace`
|
||||
# rules below (same rebuild path as the monitor overlay; the applier
|
||||
# covers the instant half). Same junk-state guard as resolve.
|
||||
activeProfile =
|
||||
let a = config.nomarchy.settings.displayProfile or "";
|
||||
in profiles.${if builtins.isString a then a else ""} or { workspaces = { }; };
|
||||
profileWorkspaceRules =
|
||||
lib.mapAttrsToList monitorLib.workspaceRule activeProfile.workspaces;
|
||||
|
||||
# Profile applier — on PATH only when profiles are declared (the menu
|
||||
# row self-gates on it). apply: the profile's rules live via hyprctl +
|
||||
# the in-flake state write; base: clear the state, then hyprctl reload
|
||||
# re-applies the generated config's rules.
|
||||
displayProfileTool = pkgs.writeShellScriptBin "nomarchy-display-profile" ''
|
||||
case "''${1:-}" in
|
||||
list)
|
||||
${lib.concatMapStringsSep "\n" (n: " echo ${lib.escapeShellArg n}") (lib.attrNames profiles)} ;;
|
||||
active)
|
||||
cur=$(${sync} get settings.displayProfile 2>/dev/null) || cur=
|
||||
echo "''${cur:-none}" ;;
|
||||
apply)
|
||||
case "''${2:-}" in
|
||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList
|
||||
(name: p:
|
||||
" ${lib.escapeShellArg name})\n"
|
||||
+ lib.concatMapStringsSep "\n"
|
||||
(m: " hyprctl keyword monitor ${lib.escapeShellArg (monitorRule m)} >/dev/null 2>&1")
|
||||
p.monitors
|
||||
# Workspace pins: the keyword sets the session rule, the dispatch
|
||||
# moves an already-open workspace over. Pins from a previously
|
||||
# applied profile linger until reload/rebuild (hyprctl can only
|
||||
# add) — harmless, a rule naming an absent output is inert.
|
||||
+ lib.concatStrings (lib.mapAttrsToList
|
||||
(ws: out:
|
||||
"\n hyprctl keyword workspace ${lib.escapeShellArg (monitorLib.workspaceRule ws out)} >/dev/null 2>&1"
|
||||
+ "\n hyprctl dispatch moveworkspacetomonitor ${lib.escapeShellArg ws} ${lib.escapeShellArg out} >/dev/null 2>&1")
|
||||
p.workspaces)
|
||||
+ " ;;")
|
||||
profiles)}
|
||||
*) echo "unknown profile '$2' — try: nomarchy-display-profile list" >&2; exit 64 ;;
|
||||
esac
|
||||
${sync} --quiet set settings.displayProfile "$2" --no-switch
|
||||
notify-send "Display profile" "Applied: $2" ;;
|
||||
base)
|
||||
${sync} --quiet set settings.displayProfile "" --no-switch
|
||||
hyprctl reload >/dev/null 2>&1
|
||||
notify-send "Display profile" "Base layout restored." ;;
|
||||
match)
|
||||
# Which profile fits these connected outputs? (args = output names,
|
||||
# e.g. `match $(hyprctl monitors all -j | jq -r '.[].name')`.)
|
||||
# An exact set match wins; else the largest profile whose named
|
||||
# outputs are all connected; any tie prints nothing (exit 1) — the
|
||||
# auto-switch watcher must never flap between ambiguous layouts.
|
||||
shift
|
||||
[ "$#" -gt 0 ] || exit 1
|
||||
con=" $* " ncon=$#
|
||||
exact= exactdup= best= bestn=0 dup=
|
||||
while IFS=: read -r p names; do
|
||||
[ -n "$p" ] || continue
|
||||
n=0 ok=1
|
||||
for o in $names; do
|
||||
case "$con" in *" $o "*) n=$((n+1)) ;; *) ok=; break ;; esac
|
||||
done
|
||||
[ -n "$ok" ] || continue
|
||||
if [ "$n" -eq "$ncon" ]; then
|
||||
if [ -n "$exact" ]; then exactdup=1; else exact=$p; fi
|
||||
fi
|
||||
if [ "$n" -gt "$bestn" ]; then best=$p bestn=$n dup=
|
||||
elif [ "$n" -eq "$bestn" ] && [ "$n" -gt 0 ]; then dup=1
|
||||
fi
|
||||
done < <(printf '%s\n' ${lib.concatMapStringsSep " " lib.escapeShellArg
|
||||
(lib.mapAttrsToList
|
||||
(name: p: "${name}:${lib.concatMapStringsSep " " (m: m.name) p.monitors}")
|
||||
profiles)})
|
||||
if [ -n "$exact" ] && [ -z "$exactdup" ]; then echo "$exact"; exit 0; fi
|
||||
if [ -n "$best" ] && [ -z "$dup" ]; then echo "$best"; exit 0; fi
|
||||
exit 1 ;;
|
||||
*)
|
||||
echo "usage: nomarchy-display-profile [list|active|apply <name>|base|match <output>...]" >&2
|
||||
exit 64 ;;
|
||||
esac
|
||||
'';
|
||||
|
||||
# Hotplug auto-switch (opt-in via the menu's Auto-switch row →
|
||||
# settings.displayProfileAuto, read LIVE on every output change so the
|
||||
# toggle is instant, no rebuild). Polls like the keyboard watcher —
|
||||
# exec-once, not a systemd unit (graphical-session-bound units raced
|
||||
# Hyprland's IPC on relogin; see the waybar note below). Only reacts to
|
||||
# CHANGES after session start: the baked config already encodes the
|
||||
# last choice, and login must not fight a deliberate manual pick.
|
||||
# `match` picks deterministically (exact set, else unambiguous largest
|
||||
# subset); applying via the tool persists the concrete profile, so the
|
||||
# next rebuild bakes what auto chose.
|
||||
displayProfileWatch = pkgs.writeShellScriptBin "nomarchy-display-profile-watch" ''
|
||||
set -u
|
||||
auto_on() {
|
||||
v=$(${sync} get settings.displayProfileAuto 2>/dev/null) || v=false
|
||||
case "$v" in true|True) return 0 ;; *) return 1 ;; esac
|
||||
}
|
||||
outputs() { hyprctl monitors all -j 2>/dev/null | jq -r '.[].name' | sort; }
|
||||
|
||||
check_monitors() {
|
||||
cur=$(outputs)
|
||||
[ "$cur" = "$1" ] && return
|
||||
[ -n "$cur" ] || return
|
||||
auto_on || return
|
||||
|
||||
target=$(nomarchy-display-profile match $cur) || target="base"
|
||||
|
||||
if [ "$target" = "base" ]; then
|
||||
[ "$(nomarchy-display-profile active)" = "none" ] && return
|
||||
nomarchy-display-profile base
|
||||
else
|
||||
[ "$target" = "$(nomarchy-display-profile active)" ] && return
|
||||
nomarchy-display-profile apply "$target"
|
||||
fi
|
||||
echo "$cur"
|
||||
}
|
||||
|
||||
prev=$(outputs)
|
||||
|
||||
# Listen to Hyprland's IPC socket for instant hotplug reaction
|
||||
${pkgs.socat}/bin/socat -U - UNIX-CONNECT:$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock | while read -r line; do
|
||||
case "$line" in
|
||||
monitoradded*|monitorremoved*)
|
||||
res=$(check_monitors "$prev")
|
||||
[ -n "$res" ] && prev="$res"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
'';
|
||||
|
||||
# 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)
|
||||
++ config.nomarchy.keyboard.layouts
|
||||
);
|
||||
@@ -68,21 +246,41 @@ let
|
||||
# keyboard.devices, not already remembered), ask for a layout and persist it
|
||||
# per-device, re-applying silently on later reconnects. Stateful runtime
|
||||
# piece — the complement to the declarative keyboard.devices. Reliable
|
||||
# primitives only: poll hyprctl devices, apply via switchxkblayout (an index
|
||||
# into kbLayouts). NOTE: needs an on-hardware test (hotplug isn't verifiable
|
||||
# in CI).
|
||||
# primitives only: poll hyprctl devices, apply with a per-device
|
||||
# `device[<name>]:kb_layout` keyword — the runtime twin of the declarative
|
||||
# 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" ''
|
||||
set -u
|
||||
layouts="${lib.concatStringsSep " " kbLayouts}"
|
||||
layouts="${lib.concatStringsSep " " pickerLayouts}"
|
||||
declared="${lib.concatStringsSep " " (builtins.attrNames config.nomarchy.keyboard.devices)}"
|
||||
state="''${XDG_STATE_HOME:-$HOME/.local/state}/nomarchy/keyboard-layouts"
|
||||
mkdir -p "$(dirname "$state")"; [ -f "$state" ] || : > "$state"
|
||||
sync=${sync}
|
||||
|
||||
layout_index() { i=0; for l in $layouts; do [ "$l" = "$1" ] && { printf %s "$i"; return; }; i=$((i + 1)); done; printf %s -1; }
|
||||
apply() { idx=$(layout_index "$2"); [ "$idx" -ge 0 ] && hyprctl switchxkblayout "$1" "$idx" >/dev/null 2>&1; }
|
||||
apply() { hyprctl keyword "device[$1]:kb_layout" "$2" >/dev/null 2>&1; }
|
||||
keyboards() { hyprctl devices -j | jq -r '.keyboards[].name'; }
|
||||
is_declared() { case " $declared " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
|
||||
saved_for() { while IFS='=' read -r k v; do [ "$k" = "$1" ] && { printf '%s' "$v"; return; }; done < "$state"; }
|
||||
|
||||
# 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
|
||||
# keyboard just stays on the session default.
|
||||
@@ -106,7 +304,7 @@ let
|
||||
apply "$kb" "$s"
|
||||
else
|
||||
choice=$(printf '%s\n' $layouts | rofi -dmenu -p "Layout · $kb")
|
||||
[ -n "$choice" ] && { printf '%s=%s\n' "$kb" "$choice" >> "$state"; apply "$kb" "$choice"; }
|
||||
[ -n "$choice" ] && { remember "$kb" "$choice"; apply "$kb" "$choice"; }
|
||||
fi
|
||||
done
|
||||
prev="$cur"
|
||||
@@ -135,7 +333,11 @@ in
|
||||
# and Hyprland applies them on hotplug. mkDefault so a downstream
|
||||
# `monitor = [...]` replaces the lot (the live ISO uses mkForce too).
|
||||
monitor = lib.mkDefault
|
||||
([ ",preferred,auto,1" ] ++ map monitorRule config.nomarchy.monitors);
|
||||
([ ",preferred,auto,1" ] ++ map monitorRule resolvedMonitors);
|
||||
|
||||
# The active display profile's workspace→output pins (a list, so
|
||||
# downstream `workspace = [...]` rules concatenate).
|
||||
workspace = profileWorkspaceRules;
|
||||
|
||||
# exec-once is a list at normal priority, so downstream additions
|
||||
# CONCATENATE (your autostart runs alongside ours) rather than
|
||||
@@ -146,7 +348,21 @@ in
|
||||
# Paint the wallpaper as soon as the session is up (waits for
|
||||
# the daemon internally).
|
||||
"nomarchy-theme-sync wallpaper"
|
||||
] ++ lib.optional kbAutoSwitch "${keyboardWatch}/bin/nomarchy-keyboard-watch";
|
||||
# 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. Via the nomarchy-waybar supervisor (waybar.nix): a crash — or
|
||||
# the clean pkill a theme switch now does — respawns the bar instead
|
||||
# of orphaning the session bar-less.
|
||||
] ++ lib.optional config.nomarchy.waybar.enable "nomarchy-waybar"
|
||||
++ lib.optional kbAutoSwitch "${keyboardWatch}/bin/nomarchy-keyboard-watch"
|
||||
++ lib.optional (profiles != { }) "${displayProfileWatch}/bin/nomarchy-display-profile-watch";
|
||||
|
||||
# ── Theme-driven look ──────────────────────────────────────────
|
||||
# These flow from theme-state.json and stay at NORMAL priority:
|
||||
@@ -201,8 +417,10 @@ in
|
||||
|
||||
input = {
|
||||
# kb_layout/variant come from nomarchy.keyboard.* — set that
|
||||
# option (the installer writes it; it also drives tty/LUKS).
|
||||
kb_layout = lib.concatStringsSep "," kbLayouts;
|
||||
# option (the installer writes it; it also drives tty/LUKS). Only the
|
||||
# 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;
|
||||
follow_mouse = lib.mkDefault 1;
|
||||
touchpad.natural_scroll = lib.mkDefault true;
|
||||
@@ -231,13 +449,67 @@ in
|
||||
disable_hyprland_logo = lib.mkDefault true;
|
||||
disable_splash_rendering = lib.mkDefault true;
|
||||
force_default_wallpaper = lib.mkDefault 0;
|
||||
# The backdrop when no wallpaper is painted (awww still starting,
|
||||
# a slow first paint, or a wallpaper-less setup): the theme's base
|
||||
# instead of compositor black — on light themes the black flash
|
||||
# reads as a glitch (item 28c, seen in the capture harness).
|
||||
background_color = rgb c.base;
|
||||
};
|
||||
|
||||
# Window rules — float + center small config/utility dialogs that
|
||||
# tile awkwardly. Normal-priority list like `bind`/`exec-once` below,
|
||||
# so a downstream `windowrule = [...]` concatenates rather than
|
||||
# replaces. Syntax is the post-0.53 rule rewrite (hyprlang legacy
|
||||
# parser, `handleWindowrule`): each comma field is `<keyword>
|
||||
# <value>`, so effects carry a value (`float 1`, `center 1`, `size
|
||||
# W H`) and matchers take a `match:` prefix (`match:class ^…$`).
|
||||
# The old rule-first form (`float, class:^…$`) and the
|
||||
# `windowrulev2` key are both hard errors now (red config-error
|
||||
# banner on every session start — seen in the capture harness).
|
||||
# Class regexes use `(?i)` and tolerate the XWayland
|
||||
# `.…-wrapped` binary-name form.
|
||||
#
|
||||
# Conservative set (item 41): only dialogs we ship or can name from
|
||||
# package strings / desktop files. SoftGL capture harness could not
|
||||
# materialize polkit/pinentry windows (empty hyprctl clients) — classes
|
||||
# below are from binary/app-id strings (hyprpolkitagent, pinentry-qt).
|
||||
# Remaining: GTK file-chooser portal — class still unknown headlessly;
|
||||
# HARDWARE-QUEUE for live hyprctl clients confirmation.
|
||||
windowrule = [
|
||||
# Audio mixer (item 35): right-click the Waybar volume module.
|
||||
"float 1, match:class ^(com\\.saivert\\.pwvucontrol|org\\.pulseaudio\\.pavucontrol|pavucontrol)$"
|
||||
"center 1, match:class ^(com\\.saivert\\.pwvucontrol|org\\.pulseaudio\\.pavucontrol|pavucontrol)$"
|
||||
|
||||
# Bluetooth manager (blueman) + CUPS printer admin.
|
||||
"float 1, match:class (?i)^(\\.?blueman-(manager|adapters)(-wrapped)?|\\.?system-config-printer(-wrapped)?)$"
|
||||
"center 1, match:class (?i)^(\\.?blueman-(manager|adapters)(-wrapped)?|\\.?system-config-printer(-wrapped)?)$"
|
||||
|
||||
# Calendar popup (item 42): the Waybar clock's on-click runs
|
||||
# nomarchy-calendar → calcurse in a ghostty window tagged with a
|
||||
# distinct --class, so it floats centered instead of tiling.
|
||||
"float 1, match:class ^(com\\.nomarchy\\.calendar)$"
|
||||
"size 60% 65%, match:class ^(com\\.nomarchy\\.calendar)$"
|
||||
"center 1, match:class ^(com\\.nomarchy\\.calendar)$"
|
||||
|
||||
# Polkit auth (hyprpolkitagent — app id / binary name in the package)
|
||||
# and pinentry-qt (keys.nix default; org.gnupg.pinentry-qt desktop).
|
||||
"float 1, match:class (?i)^(hyprpolkitagent|\\.hyprpolkitagent-wrapped)$"
|
||||
"center 1, match:class (?i)^(hyprpolkitagent|\\.hyprpolkitagent-wrapped)$"
|
||||
"float 1, match:class (?i)^(pinentry(-qt)?|org\\.gnupg\\.pinentry-qt)$"
|
||||
"center 1, match:class (?i)^(pinentry(-qt)?|org\\.gnupg\\.pinentry-qt)$"
|
||||
];
|
||||
|
||||
# Rendered from ./keybinds.nix (the cheatsheet reads the same list),
|
||||
# plus the generated per-workspace binds. Like exec-once above this
|
||||
# stays a normal-priority list, so a downstream `bind = [...]`
|
||||
# concatenates rather than replaces.
|
||||
bind = map mkBind keybinds.binds ++ workspaceBinds;
|
||||
# concatenates rather than replaces. The layout-cycle bind only
|
||||
# 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
|
||||
++ lofBinds;
|
||||
|
||||
# Media keys via swayosd-client: it performs the action AND shows
|
||||
# the on-screen display (the nomarchy.osd module). e = repeat,
|
||||
@@ -266,11 +538,28 @@ 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
|
||||
# writes a config file). A helper to find values for nomarchy.monitors —
|
||||
# the declarative config stays the source of truth (we don't source its
|
||||
# output). Gated on its toggle; pointless without the Hyprland session.
|
||||
home.packages = lib.optionals
|
||||
(config.nomarchy.hyprland.enable && config.nomarchy.displays.enable)
|
||||
[ pkgs.nwg-displays ];
|
||||
home.packages =
|
||||
lib.optionals (config.nomarchy.hyprland.enable && config.nomarchy.displays.enable)
|
||||
[ 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
|
||||
# The display-profile switcher (the menu's Profiles row gates on it)
|
||||
# + its hotplug watcher, on PATH for debugging like the keyboard one.
|
||||
++ lib.optionals (config.nomarchy.hyprland.enable && profiles != { })
|
||||
[ displayProfileTool displayProfileWatch ];
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
{
|
||||
binds = [
|
||||
{ mods = "$mod"; key = "Return"; action = "exec, $terminal"; desc = "Open terminal"; }
|
||||
{ mods = "$mod"; key = "Space"; action = "exec, rofi -show drun"; desc = "Quick launch (apps)"; }
|
||||
{ mods = "$mod"; key = "D"; action = "exec, rofi -show drun"; desc = "App launcher"; }
|
||||
{ mods = "$mod"; key = "Space"; action = "exec, rofi -show drun -theme launcher"; desc = "Quick launch (apps)"; }
|
||||
{ mods = "$mod"; key = "D"; action = "exec, rofi -show drun -theme launcher"; desc = "App launcher"; }
|
||||
{ mods = "$mod"; key = "M"; action = "exec, nomarchy-menu"; desc = "Main menu"; }
|
||||
{ mods = "$mod"; key = "E"; action = "exec, $terminal -e yazi"; desc = "File manager (yazi)"; }
|
||||
{ mods = "$mod"; key = "Q"; action = "killactive"; desc = "Close window"; }
|
||||
@@ -33,8 +33,13 @@
|
||||
{ mods = "$mod"; key = "X"; action = "exec, nomarchy-menu power"; desc = "Power menu"; }
|
||||
|
||||
{ mods = "$mod"; key = "N"; action = "exec, swaync-client -t"; desc = "Notification centre"; }
|
||||
# SUPER+? (the "question" keysym already implies Shift on most layouts).
|
||||
{ mods = "$mod"; key = "question"; action = "exec, nomarchy-menu keybinds"; desc = "Keybindings cheatsheet"; }
|
||||
# SUPER+? — ? is Shift+/. SHIFT stays in the modmask (you hold it), but
|
||||
# the keysym must be the BASE key `slash`, not `question`: Hyprland
|
||||
# resolves the sym with Shift consumed, so `question` never matches while
|
||||
# Shift is down — same as the `$mod SHIFT, 1` workspace binds. (item 26
|
||||
# fixed the modmask but kept the shifted keysym → still dead; item 32.)
|
||||
# The cheatsheet still renders this row as SUPER + ? (rofi.nix).
|
||||
{ mods = "$mod SHIFT"; key = "slash"; action = "exec, nomarchy-menu keybinds"; desc = "Keybindings cheatsheet"; }
|
||||
|
||||
# Menu functions — SUPER+CTRL+<mnemonic> jumps straight to a
|
||||
# nomarchy-menu module (all also reachable from the SUPER+M picker).
|
||||
@@ -43,20 +48,43 @@
|
||||
{ mods = "$mod CTRL"; key = "W"; action = "exec, nomarchy-menu web"; desc = "Web search"; }
|
||||
{ mods = "$mod CTRL"; key = "F"; action = "exec, nomarchy-menu files"; desc = "File search"; }
|
||||
{ mods = "$mod CTRL"; key = "E"; action = "exec, nomarchy-menu emoji"; desc = "Emoji picker"; }
|
||||
{ mods = "$mod CTRL"; key = "N"; action = "exec, nomarchy-menu network"; desc = "Network (nmtui)"; }
|
||||
{ mods = "$mod CTRL"; key = "N"; action = "exec, nomarchy-menu network"; desc = "Network (networkmanager_dmenu)"; }
|
||||
{ mods = "$mod CTRL"; key = "B"; action = "exec, nomarchy-menu bluetooth"; desc = "Bluetooth"; }
|
||||
{ mods = "$mod CTRL"; key = "S"; action = "exec, nomarchy-menu capture"; desc = "Screenshot / capture"; }
|
||||
{ mods = "$mod CTRL"; key = "P"; action = "exec, nomarchy-menu colorpicker"; desc = "Color picker (→ clipboard)"; }
|
||||
{ mods = "$mod CTRL"; key = "A"; action = "exec, nomarchy-menu ask"; desc = "Ask Claude"; }
|
||||
{ mods = "$mod CTRL"; key = "D"; action = "exec, nomarchy-menu dnd"; desc = "Do Not Disturb toggle"; }
|
||||
{ mods = "$mod SHIFT"; key = "C"; action = "exec, hyprpicker -a"; desc = "Color picker (→ clipboard)"; }
|
||||
|
||||
# Focus
|
||||
{ mods = "$mod"; key = "H"; action = "movefocus, l"; desc = "Focus left"; }
|
||||
{ mods = "$mod"; key = "L"; action = "movefocus, r"; desc = "Focus right"; }
|
||||
{ mods = "$mod"; key = "K"; action = "movefocus, u"; desc = "Focus up"; }
|
||||
{ mods = "$mod"; key = "J"; action = "movefocus, d"; desc = "Focus down"; }
|
||||
# Focus — SUPER + arrow keys.
|
||||
{ mods = "$mod"; key = "left"; action = "movefocus, l"; desc = "Focus left"; }
|
||||
{ mods = "$mod"; key = "right"; action = "movefocus, r"; desc = "Focus right"; }
|
||||
{ mods = "$mod"; key = "up"; action = "movefocus, u"; desc = "Focus up"; }
|
||||
{ mods = "$mod"; key = "down"; action = "movefocus, d"; desc = "Focus down"; }
|
||||
|
||||
# Screenshot region to clipboard (the menu's Capture module has more).
|
||||
# Multi-monitor workspace movement — SUPER + ALT + arrow keys.
|
||||
{ mods = "$mod ALT"; key = "left"; action = "movecurrentworkspacetomonitor, l"; desc = "Move workspace to left monitor"; }
|
||||
{ mods = "$mod ALT"; key = "right"; action = "movecurrentworkspacetomonitor, r"; desc = "Move workspace to right monitor"; }
|
||||
{ mods = "$mod ALT"; key = "up"; action = "movecurrentworkspacetomonitor, u"; desc = "Move workspace to upper monitor"; }
|
||||
{ mods = "$mod ALT"; key = "down"; action = "movecurrentworkspacetomonitor, d"; desc = "Move workspace to lower monitor"; }
|
||||
|
||||
# Screenshots (the menu's Capture module has the rest: OCR, recording).
|
||||
# Bare Print → region to clipboard; the two → file binds save a
|
||||
# timestamped PNG under ~/Pictures/Screenshots and toast the path, the
|
||||
# same plumbing the Capture menu's "→ file" rows use.
|
||||
{ mods = ""; key = "Print"; action = "exec, grim -g \"$(slurp)\" - | wl-copy"; desc = "Screenshot region → clipboard"; }
|
||||
{ mods = "SHIFT"; key = "Print"; action = "exec, f=$HOME/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png; mkdir -p $HOME/Pictures/Screenshots && grim -g \"$(slurp)\" \"$f\" && notify-send \"Screenshot saved\" \"$f\""; desc = "Screenshot region → file"; }
|
||||
{ mods = "CTRL"; key = "Print"; action = "exec, f=$HOME/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png; mkdir -p $HOME/Pictures/Screenshots && grim \"$f\" && notify-send \"Screenshot saved\" \"$f\""; desc = "Screenshot screen → file"; }
|
||||
{ mods = "$mod SHIFT"; key = "Print"; action = "exec, f=$HOME/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png; mkdir -p $HOME/Pictures/Screenshots && grim -g \"$(slurp)\" - | satty --filename - --fullscreen --output-filename \"$f\""; desc = "Annotate region"; }
|
||||
];
|
||||
|
||||
# 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 = [
|
||||
@@ -64,5 +92,6 @@
|
||||
{ keys = "SUPER + SHIFT + 1-9"; desc = "Move window to workspace 1-9"; }
|
||||
{ keys = "SUPER + drag"; desc = "Move (LMB) / resize (RMB) window"; }
|
||||
{ keys = "Volume / Brightness"; desc = "Hardware keys, shown via the OSD"; }
|
||||
{ keys = "Bar: click"; desc = "Caffeine — hold the screen awake (idle inhibitor)"; }
|
||||
];
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
# GNOME session to lean on) and themed by Stylix's Qt config, so the
|
||||
# passphrase dialog tracks the palette.
|
||||
#
|
||||
# SSH_AUTH_SOCK is exported by the agent's shell integration (zsh, on via
|
||||
# home.shell.enableZshIntegration in shell.nix). Terminal git/ssh is the
|
||||
# supported path; a GUI client launched outside a shell won't inherit the
|
||||
# socket — revisit with a session-level export if that's ever wanted.
|
||||
# SSH_AUTH_SOCK reaches both terminal and GUI clients: the agent's zsh
|
||||
# integration sets it in interactive shells (home.shell.enableZshIntegration
|
||||
# in shell.nix), and a session-level home.sessionVariables export (below)
|
||||
# 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
|
||||
# secrets; modern gnome-keyring no longer runs an SSH agent, so there is no
|
||||
@@ -38,5 +40,16 @@ in
|
||||
defaultCacheTtlSsh = 1800;
|
||||
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)";
|
||||
};
|
||||
}
|
||||
|
||||
62
modules/home/mime.nix
Normal file
62
modules/home/mime.nix
Normal file
@@ -0,0 +1,62 @@
|
||||
# Default applications (xdg mimeapps.list) — without this, "open a
|
||||
# PDF/photo" falls to whatever GTK guesses first (GIMP for images).
|
||||
# Every association is mkDefault AND degrades gracefully by design: an
|
||||
# entry whose .desktop file isn't installed is skipped by GIO/xdg-open,
|
||||
# which then falls through to whatever else claims the type — so
|
||||
# deleting an app from the template suite (or never uncommenting the
|
||||
# browser) leaves no broken "open" behaviour, just the old guessing.
|
||||
{ config, lib, ... }:
|
||||
|
||||
lib.mkIf config.nomarchy.mime.enable {
|
||||
xdg.mimeApps = {
|
||||
enable = lib.mkDefault true;
|
||||
defaultApplications = lib.mapAttrs (_: v: lib.mkDefault v) {
|
||||
"application/pdf" = "org.pwmt.zathura.desktop";
|
||||
|
||||
"image/png" = "imv.desktop";
|
||||
"image/jpeg" = "imv.desktop";
|
||||
"image/gif" = "imv.desktop";
|
||||
"image/webp" = "imv.desktop";
|
||||
"image/avif" = "imv.desktop";
|
||||
"image/bmp" = "imv.desktop";
|
||||
"image/tiff" = "imv.desktop";
|
||||
"image/svg+xml" = "imv.desktop";
|
||||
|
||||
# Video → mpv (the template's media player).
|
||||
"video/mp4" = "mpv.desktop";
|
||||
"video/webm" = "mpv.desktop";
|
||||
"video/x-matroska" = "mpv.desktop";
|
||||
"video/quicktime" = "mpv.desktop";
|
||||
|
||||
# Audio → Amberol (the template's GTK4 music player). Amberol
|
||||
# registers these types itself, so this only sets the preference over
|
||||
# mpv, which also claims them. Both x- and canonical names because
|
||||
# files report either. Degrades to mpv/whatever if Amberol is dropped.
|
||||
"audio/mpeg" = "io.bassi.Amberol.desktop";
|
||||
"audio/flac" = "io.bassi.Amberol.desktop";
|
||||
"audio/x-flac" = "io.bassi.Amberol.desktop";
|
||||
"audio/ogg" = "io.bassi.Amberol.desktop";
|
||||
"audio/x-vorbis+ogg" = "io.bassi.Amberol.desktop";
|
||||
"audio/opus" = "io.bassi.Amberol.desktop";
|
||||
"audio/wav" = "io.bassi.Amberol.desktop";
|
||||
"audio/x-wav" = "io.bassi.Amberol.desktop";
|
||||
"audio/mp4" = "io.bassi.Amberol.desktop";
|
||||
"audio/x-m4a" = "io.bassi.Amberol.desktop";
|
||||
"audio/aac" = "io.bassi.Amberol.desktop";
|
||||
|
||||
# The template's active editor; degrades if you drop vscode.
|
||||
"text/plain" = "code.desktop";
|
||||
|
||||
# The system-side Thunar (nomarchy.system.fileManager).
|
||||
"inode/directory" = "thunar.desktop";
|
||||
|
||||
# The template ships no browser by default (open Decision) — these
|
||||
# are inert until one is installed; firefox is the template's first
|
||||
# suggestion. A different browser registers its own handler and
|
||||
# wins once these entries stay dead.
|
||||
"text/html" = "firefox.desktop";
|
||||
"x-scheme-handler/http" = "firefox.desktop";
|
||||
"x-scheme-handler/https" = "firefox.desktop";
|
||||
};
|
||||
};
|
||||
}
|
||||
72
modules/home/monitor-rules.nix
Normal file
72
modules/home/monitor-rules.nix
Normal file
@@ -0,0 +1,72 @@
|
||||
# Pure monitor-rule composition, split out of hyprland.nix so
|
||||
# checks.display-profiles can exercise the overlay semantics directly —
|
||||
# no Home Manager evaluation, no state-file fixture.
|
||||
{ lib }:
|
||||
|
||||
rec {
|
||||
# Skeleton for an entry built outside the option type (mirrors the
|
||||
# monitorType defaults in options.nix).
|
||||
defaults = {
|
||||
resolution = "preferred"; position = "auto"; scale = 1;
|
||||
transform = null; mirror = null; bitdepth = null; vrr = null; extra = "";
|
||||
};
|
||||
|
||||
# An entry -> a Hyprland `monitor` rule. Unset optional fields are
|
||||
# omitted; `resolution = "disable"` collapses to the short form.
|
||||
rule = m:
|
||||
if m.resolution == "disable" then "${m.name}, disable"
|
||||
else lib.concatStringsSep ", " (
|
||||
[ m.name m.resolution (toString m.position) (toString m.scale) ]
|
||||
++ lib.optionals (m.transform != null) [ "transform" (toString m.transform) ]
|
||||
++ lib.optionals (m.mirror != null) [ "mirror" m.mirror ]
|
||||
++ lib.optionals (m.bitdepth != null) [ "bitdepth" (toString m.bitdepth) ]
|
||||
++ lib.optionals (m.vrr != null) [ "vrr" (toString m.vrr) ]
|
||||
++ lib.optional (m.extra != "") m.extra
|
||||
);
|
||||
|
||||
# A profile's workspace pinning ({ "1" = "DP-3"; }) -> a Hyprland
|
||||
# `workspace` rule. Only the active profile's pins are rendered
|
||||
# (hyprland.nix); a workspace rule naming an absent output is inert.
|
||||
workspaceRule = ws: out: "${ws}, monitor:${out}";
|
||||
|
||||
# The full overlay, three layers:
|
||||
#
|
||||
# 1. base (nomarchy.monitors) with the ACTIVE display profile's entries
|
||||
# (settings.displayProfile naming a nomarchy.displayProfiles key)
|
||||
# replacing base entries WHOLE, by name — a profile entry is a
|
||||
# complete statement about that output. Outputs a profile doesn't
|
||||
# name keep their base rules; a cleared/unknown/non-string value
|
||||
# ("" after `nomarchy-display-profile base`, or hand-edited junk —
|
||||
# the validator only warns) means base config only.
|
||||
#
|
||||
# 2. Menu-remembered per-output resolutions (settings.monitors:
|
||||
# output-name -> "WxH@R", written instantly by the Display menu)
|
||||
# overlaid field-level by name: a declared output keeps its
|
||||
# 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 pick wins over a hand-set resolution
|
||||
# (it's the explicit live action) — EXCEPT onto disabled entries: a
|
||||
# stale pick, made while the output was enabled, must not resurrect
|
||||
# a panel the active profile (or the base config) disables.
|
||||
#
|
||||
# 3. Anything still uncovered falls to the wildcard (hyprland.nix
|
||||
# prepends it).
|
||||
resolve = { base, profiles, active, resOverrides }:
|
||||
let
|
||||
activeName = if builtins.isString active then active else "";
|
||||
profileEntries = profiles.${activeName} or [ ];
|
||||
profileNames = map (m: m.name) profileEntries;
|
||||
baseMonitors =
|
||||
lib.filter (m: ! lib.elem m.name profileNames) base
|
||||
++ profileEntries;
|
||||
declaredNames = map (m: m.name) baseMonitors;
|
||||
in
|
||||
map (m: if resOverrides ? ${m.name} && m.resolution != "disable"
|
||||
then m // { resolution = resOverrides.${m.name}; }
|
||||
else m)
|
||||
baseMonitors
|
||||
++ lib.mapAttrsToList
|
||||
(name: res: defaults // { inherit name; resolution = res; })
|
||||
(lib.filterAttrs (name: _: ! lib.elem name declaredNames) resOverrides);
|
||||
}
|
||||
@@ -1,18 +1,75 @@
|
||||
# Night light — a scheduled blue-light filter via hyprsunset (Hyprland's own
|
||||
# gamma/temperature tool). Warm at night, identity (no shift) by day;
|
||||
# hyprsunset's time-based `profile` entries handle the schedule and pick the
|
||||
# right state on session start. Opt-in via nomarchy.nightlight.enable.
|
||||
# gamma/temperature tool). Warm at night, identity (no shift) by day; the
|
||||
# schedule (temperature/sunrise/sunset) is tuned via nomarchy.nightlight.* in
|
||||
# home.nix and baked into the unit's time-based `profile`.
|
||||
#
|
||||
# The hyprsunset HM service module is provided by home-manager; this only
|
||||
# configures it. Override anything with plain services.hyprsunset.* options.
|
||||
{ config, lib, ... }:
|
||||
# Off by default and opt-in. Two git-tracked flags in the state file, both
|
||||
# menu-written (no ~/.local/state):
|
||||
# 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
|
||||
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
|
||||
{
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.hyprsunset = {
|
||||
config = {
|
||||
# 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;
|
||||
settings.profile = [
|
||||
# Daytime: identity = no colour change.
|
||||
@@ -21,5 +78,13 @@ in
|
||||
{ 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 ];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# User-level `nomarchy.*` options — the full surface downstream users
|
||||
# configure in their home.nix. Kept small on purpose.
|
||||
{ lib, pkgs, ... }:
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
# One output's layout — turned into a Hyprland `monitor` rule in
|
||||
@@ -55,6 +55,33 @@ let
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# One display profile: a monitor layout plus optional workspace→output
|
||||
# pinning. A bare list of monitor entries still works (the original
|
||||
# shape) — hyprland.nix normalizes it to { monitors = […]; }.
|
||||
# (either, not coercedTo: coercedTo refuses list-of-submodule sources.)
|
||||
displayProfileType = lib.types.either
|
||||
(lib.types.listOf monitorType)
|
||||
(lib.types.submodule {
|
||||
options = {
|
||||
monitors = lib.mkOption {
|
||||
type = lib.types.listOf monitorType;
|
||||
default = [ ];
|
||||
description = "nomarchy.monitors-style entries; each replaces the base entry for the same output whole.";
|
||||
};
|
||||
workspaces = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.str;
|
||||
default = { };
|
||||
example = { "1" = "DP-3"; "9" = "eDP-1"; };
|
||||
description = ''
|
||||
Workspace → output pinning while this profile is active
|
||||
(Hyprland `workspace = <ws>, monitor:<output>` rules).
|
||||
Applied instantly on profile switch (existing workspaces are
|
||||
moved over) and baked at the next rebuild.
|
||||
'';
|
||||
};
|
||||
};
|
||||
});
|
||||
in
|
||||
{
|
||||
options.nomarchy = {
|
||||
@@ -73,13 +100,13 @@ in
|
||||
# ── Preferences ────────────────────────────────────────────────
|
||||
terminal = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "ghostty";
|
||||
default = config.nomarchy.settings.terminal or "ghostty";
|
||||
description = "Terminal emulator command, used by keybinds and $TERMINAL.";
|
||||
};
|
||||
|
||||
keyboard.layout = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "us";
|
||||
default = config.nomarchy.settings.keyboard.layout or "us";
|
||||
example = "de";
|
||||
description = ''
|
||||
XKB layout for the Hyprland session. The console (and the LUKS
|
||||
@@ -105,10 +132,14 @@ in
|
||||
picker. When non-empty, a small watcher runs in the session: when a
|
||||
keyboard connects that isn't covered by nomarchy.keyboard.devices and
|
||||
hasn't been chosen before, it pops a rofi picker (these layouts plus
|
||||
the primary nomarchy.keyboard.layout), applies the choice, and
|
||||
remembers it per-device (~/.local/state) — re-applying automatically
|
||||
on later reconnects. The runtime-remember complement to the
|
||||
declarative keyboard.devices; a stateful runtime piece by design.
|
||||
the primary nomarchy.keyboard.layout), applies the choice to that
|
||||
keyboard only (a per-device kb_layout, so the built-in board is left
|
||||
alone), and remembers it in the git-tracked in-flake state
|
||||
(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 +166,9 @@ in
|
||||
nomarchy.keyboard.layout for that keyboard only — e.g. an external
|
||||
keyboard that's physically a different layout than the laptop's
|
||||
built-in one. Hyprland applies it automatically whenever that
|
||||
keyboard is connected.
|
||||
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 +217,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'' // { default = config.nomarchy.settings.updates.enable or false; };
|
||||
|
||||
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 {
|
||||
type = lib.types.listOf monitorType;
|
||||
default = [ ];
|
||||
@@ -205,11 +264,88 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
launchOrFocus = lib.mkOption {
|
||||
type = lib.types.listOf (lib.types.submodule {
|
||||
options = {
|
||||
key = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "B";
|
||||
description = "The key (with `mods`) that focuses-or-launches the app.";
|
||||
};
|
||||
mods = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "$mod";
|
||||
description = "Modifier string, Hyprland syntax (\"$mod\", \"$mod SHIFT\").";
|
||||
};
|
||||
class = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "firefox";
|
||||
description = "Window class to focus (case-insensitive; see `hyprctl clients`).";
|
||||
};
|
||||
command = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "";
|
||||
description = "Command to launch when no window matches (defaults to the class).";
|
||||
};
|
||||
desc = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "";
|
||||
description = "Cheatsheet label (defaults to \"Focus or launch <command>\").";
|
||||
};
|
||||
};
|
||||
});
|
||||
default = [ ];
|
||||
example = lib.literalExpression ''
|
||||
[
|
||||
{ key = "B"; class = "firefox"; }
|
||||
{ key = "O"; class = "obsidian"; }
|
||||
]
|
||||
'';
|
||||
description = ''
|
||||
Launch-or-focus binds: the key focuses the app's existing window
|
||||
(case-insensitive class match) or launches it if none is open. Each
|
||||
entry generates a Hyprland bind AND a SUPER+? cheatsheet row. The
|
||||
launcher self-gates: a bind whose command isn't installed notifies
|
||||
instead of failing silently.
|
||||
'';
|
||||
};
|
||||
|
||||
displayProfiles = lib.mkOption {
|
||||
type = lib.types.attrsOf displayProfileType;
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
docked = {
|
||||
monitors = [
|
||||
{ name = "eDP-1"; resolution = "disable"; }
|
||||
{ name = "DP-3"; position = "0x0"; }
|
||||
{ name = "DP-4"; position = "auto-right"; }
|
||||
];
|
||||
workspaces = { "1" = "DP-3"; "9" = "DP-4"; };
|
||||
};
|
||||
undocked = [ { name = "eDP-1"; position = "0x0"; } ];
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Named display layouts for the same outputs (docked, undocked, …):
|
||||
a list of nomarchy.monitors-style entries, or an attrset with
|
||||
`monitors` plus optional `workspaces` (workspace → output pinning
|
||||
while the profile is active). Switch from the menu (System ›
|
||||
Display › Profiles) or `nomarchy-display-profile apply <name>`:
|
||||
the profile's rules apply instantly via hyprctl and the choice
|
||||
persists in the in-flake state (settings.displayProfile), so the
|
||||
next rebuild bakes the active profile's entries over
|
||||
nomarchy.monitors by name. Outputs a profile doesn't name keep
|
||||
their base rules.
|
||||
'';
|
||||
};
|
||||
|
||||
# ── Component toggles ──────────────────────────────────────────
|
||||
hyprland.enable = lib.mkEnableOption "Nomarchy's Hyprland configuration" // { default = true; };
|
||||
waybar.enable = lib.mkEnableOption "Nomarchy's Waybar configuration" // { default = true; };
|
||||
rofi.enable = lib.mkEnableOption "Nomarchy's themed rofi launcher + the nomarchy-menu dispatcher" // { default = true; };
|
||||
swaync.enable = lib.mkEnableOption "swaync notifications, themed from the state file" // { default = true; };
|
||||
batteryNotify.enable = lib.mkEnableOption "low-battery notifications at the bar's thresholds (25% low, 10% critical — that one stays up until dismissed); self-gating, a silent no-op on machines without a battery" // { default = true; };
|
||||
idle.enable = lib.mkEnableOption "hyprlock + hypridle (idle lock, display off, suspend)" // { default = true; };
|
||||
yazi.enable = lib.mkEnableOption "the yazi TUI file manager, themed with a curated plugin set" // { default = true; };
|
||||
osd.enable = lib.mkEnableOption "swayosd on-screen display for volume/brightness/mute" // { default = true; };
|
||||
@@ -220,6 +356,8 @@ in
|
||||
btop.enable = lib.mkEnableOption "btop with the per-theme nomarchy theme" // { default = true; };
|
||||
stylix.enable = lib.mkEnableOption "Stylix theming for the long tail of apps (GTK, Qt, cursors)" // { default = true; };
|
||||
displays.enable = lib.mkEnableOption "the nwg-displays interactive monitor arranger (a helper to find nomarchy.monitors values; the declarative config stays the source of truth)" // { default = true; };
|
||||
viewers.enable = lib.mkEnableOption "the document/image viewers (zathura, Stylix-themed, + imv)" // { default = true; };
|
||||
mime.enable = lib.mkEnableOption "default file associations (xdg mimeapps.list: PDF/image/video/text/browser/directory); entries for absent apps are skipped, so it degrades with the package suite" // { default = true; };
|
||||
|
||||
# ── Computed (read-only) ───────────────────────────────────────
|
||||
theme = lib.mkOption {
|
||||
@@ -228,6 +366,17 @@ in
|
||||
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 {
|
||||
type = lib.types.attrs;
|
||||
readOnly = true;
|
||||
|
||||
99
modules/home/recording.nix
Normal file
99
modules/home/recording.nix
Normal file
@@ -0,0 +1,99 @@
|
||||
# Screen recording (menu › Tools › Capture). One helper owns the whole
|
||||
# lifecycle: `start` launches wl-screenrec (VAAPI hardware encode) and
|
||||
# falls back to wf-recorder (software x264) if it dies on the spot —
|
||||
# e.g. no usable VAAPI device; `status` feeds the self-gating Waybar
|
||||
# indicator (waybar.nix, signal 8), whose click is the ONE stop surface
|
||||
# (`stop` SIGINTs the recorder so it finalizes the file cleanly). State
|
||||
# is a runtime pidfile — nothing persists across sessions.
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
lib.mkIf config.nomarchy.rofi.enable {
|
||||
home.packages = [
|
||||
pkgs.wl-screenrec
|
||||
pkgs.wf-recorder
|
||||
|
||||
(pkgs.writeShellScriptBin "nomarchy-record" ''
|
||||
run="''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||
pidfile="$run/nomarchy-record.pid"
|
||||
filefile="$run/nomarchy-record.file"
|
||||
|
||||
alive() { [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile")" 2>/dev/null; }
|
||||
poke_bar() { pkill -RTMIN+8 waybar 2>/dev/null || true; }
|
||||
|
||||
case "''${1:-}" in
|
||||
start)
|
||||
if alive; then
|
||||
notify-send "Screen recording" "Already recording — stop it via the ⏺ in the bar."
|
||||
exit 1
|
||||
fi
|
||||
target="''${2:-screen}"; audio="''${3:-}"
|
||||
geo=""
|
||||
if [ "$target" = region ]; then
|
||||
geo=$(slurp) || exit 0 # Esc in slurp = cancel, silently
|
||||
fi
|
||||
dir="$HOME/Videos/Recordings"
|
||||
mkdir -p "$dir"
|
||||
file="$dir/$(date +%Y%m%d-%H%M%S).mp4"
|
||||
|
||||
launch() { # launch <recorder> -> 0 if it survived startup
|
||||
case "$1" in
|
||||
wl-screenrec)
|
||||
setsid wl-screenrec ''${geo:+-g "$geo"} \
|
||||
''${audio:+--audio} -f "$file" >/dev/null 2>&1 & ;;
|
||||
wf-recorder)
|
||||
setsid wf-recorder ''${geo:+-g "$geo"} \
|
||||
''${audio:+-a} -f "$file" >/dev/null 2>&1 & ;;
|
||||
esac
|
||||
echo $! > "$pidfile"
|
||||
sleep 0.7
|
||||
alive
|
||||
}
|
||||
|
||||
started=""
|
||||
if command -v wl-screenrec >/dev/null 2>&1 && launch wl-screenrec; then
|
||||
started=wl-screenrec
|
||||
elif command -v wf-recorder >/dev/null 2>&1 && launch wf-recorder; then
|
||||
started=wf-recorder # software fallback (no VAAPI device)
|
||||
fi
|
||||
if [ -z "$started" ]; then
|
||||
rm -f "$pidfile"
|
||||
notify-send "Screen recording" "Recorder failed to start (no wl-screenrec/wf-recorder able to run)."
|
||||
exit 1
|
||||
fi
|
||||
printf '%s' "$file" > "$filefile"
|
||||
poke_bar
|
||||
notify-send "Screen recording" "Recording ($started)''${audio:+ with audio} — click the ⏺ in the bar to stop." ;;
|
||||
|
||||
stop)
|
||||
if ! alive; then
|
||||
rm -f "$pidfile" "$filefile"; poke_bar
|
||||
notify-send "Screen recording" "No recording is running."
|
||||
exit 0
|
||||
fi
|
||||
pid=$(cat "$pidfile")
|
||||
kill -INT "$pid" 2>/dev/null # graceful: both recorders finalize on INT
|
||||
for _ in $(seq 1 50); do kill -0 "$pid" 2>/dev/null || break; sleep 0.1; done
|
||||
kill -0 "$pid" 2>/dev/null && kill "$pid" 2>/dev/null
|
||||
file=$(cat "$filefile" 2>/dev/null || echo "?")
|
||||
rm -f "$pidfile" "$filefile"
|
||||
poke_bar
|
||||
notify-send "Screen recording saved" "$file" ;;
|
||||
|
||||
active)
|
||||
alive ;;
|
||||
|
||||
status)
|
||||
# Waybar JSON while recording; nothing => module hidden.
|
||||
if alive; then
|
||||
file=$(cat "$filefile" 2>/dev/null || echo "")
|
||||
printf '{"text":"⏺ REC","class":"recording","tooltip":"Recording to %s — click to stop"}\n' "''${file##*/}"
|
||||
fi
|
||||
exit 0 ;;
|
||||
|
||||
*)
|
||||
echo "usage: nomarchy-record start [region|screen] [audio] | stop | active | status" >&2
|
||||
exit 2 ;;
|
||||
esac
|
||||
'')
|
||||
];
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
31
modules/home/satty.nix
Normal file
31
modules/home/satty.nix
Normal file
@@ -0,0 +1,31 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.nomarchy;
|
||||
t = cfg.theme;
|
||||
c = t.colors;
|
||||
in
|
||||
{
|
||||
config = lib.mkIf (pkgs.lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.satty) {
|
||||
home.packages = [ pkgs.satty ];
|
||||
|
||||
xdg.configFile."satty/config.toml".text = ''
|
||||
[general]
|
||||
# Set the default tool to pointer or arrow?
|
||||
initial-tool = "arrow"
|
||||
copy-command = "wl-copy"
|
||||
# Hitting copy also saves? Let the user hit save if they want to save.
|
||||
# save-after-copy = false
|
||||
|
||||
[color-palette]
|
||||
palette = [
|
||||
"${c.accent}ff",
|
||||
"${c.bad}ff",
|
||||
"${c.warn}ff",
|
||||
"${c.good}ff",
|
||||
"${c.text}ff",
|
||||
"${c.base}ff"
|
||||
]
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -47,6 +47,8 @@ in
|
||||
targets = {
|
||||
gtk.enable = true;
|
||||
qt.enable = true;
|
||||
# No-op unless programs.zathura is on (viewers.nix enables it).
|
||||
zathura.enable = true;
|
||||
};
|
||||
|
||||
cursor = {
|
||||
|
||||
@@ -29,11 +29,14 @@ in
|
||||
};
|
||||
|
||||
style = ''
|
||||
/* Palette baked from theme-state.json */
|
||||
/* Palette baked from theme-state.json. Only roles guaranteed to
|
||||
contrast @base in EVERY palette are used: subtext/surface mean
|
||||
"on-surface" in some light themes (summer-day: subtext==base,
|
||||
surface==text — body text was invisible on hardware, item 25).
|
||||
Chips/hovers are alpha(@text) tints — contrast by construction.
|
||||
tools/check-theme-contrast.py guards the hex-on-hex pairings. */
|
||||
@define-color base ${c.base};
|
||||
@define-color surface ${c.surface};
|
||||
@define-color text ${c.text};
|
||||
@define-color subtext ${c.subtext};
|
||||
@define-color accent ${c.accent};
|
||||
@define-color bad ${c.bad};
|
||||
|
||||
@@ -42,6 +45,8 @@ in
|
||||
border: ${toString t.ui.borderSize}px solid alpha(@accent, 0.4);
|
||||
border-radius: ${r}px;
|
||||
color: @text;
|
||||
font-family: "${t.fonts.ui}", "${t.fonts.mono}";
|
||||
font-size: ${toString t.fonts.size}pt;
|
||||
}
|
||||
|
||||
.notification-content .summary {
|
||||
@@ -50,7 +55,7 @@ in
|
||||
}
|
||||
|
||||
.notification-content .body {
|
||||
color: @subtext;
|
||||
color: @text;
|
||||
}
|
||||
|
||||
.notification.critical {
|
||||
@@ -62,11 +67,13 @@ in
|
||||
border: ${toString t.ui.borderSize}px solid alpha(@accent, 0.4);
|
||||
border-radius: ${r}px;
|
||||
color: @text;
|
||||
font-family: "${t.fonts.ui}", "${t.fonts.mono}";
|
||||
font-size: ${toString t.fonts.size}pt;
|
||||
}
|
||||
|
||||
.control-center .notification-row:focus,
|
||||
.control-center .notification-row:hover {
|
||||
background: alpha(@surface, 0.6);
|
||||
background: alpha(@text, 0.1);
|
||||
border-radius: ${r}px;
|
||||
}
|
||||
|
||||
@@ -76,7 +83,7 @@ in
|
||||
}
|
||||
|
||||
.widget-title > button {
|
||||
background: @surface;
|
||||
background: alpha(@text, 0.1);
|
||||
color: @text;
|
||||
border: none;
|
||||
border-radius: ${r}px;
|
||||
|
||||
@@ -18,14 +18,18 @@
|
||||
let
|
||||
cfg = config.nomarchy;
|
||||
|
||||
themeState = builtins.fromJSON (builtins.readFile cfg.stateFile);
|
||||
# Fail-closed load: missing / empty / non-object get a short pointer at
|
||||
# the template + `nomarchy-theme-sync validate`, not a raw readFile stack
|
||||
# from deep inside a consumer (nightlight, hyprland, …). Field-level
|
||||
# checks below still run on the merged result.
|
||||
themeState = import ../theme-state-read.nix { inherit lib; } cfg.stateFile;
|
||||
|
||||
# Defaults guarantee evaluation succeeds on a sparse or older state
|
||||
# file (e.g. one written before a schema field was added). The shipped
|
||||
# Tokyo Night preset provides the color/ansi fallbacks; the path is
|
||||
# Boreal preset provides the color/ansi fallbacks; the path is
|
||||
# relative to this module, so it resolves inside Nomarchy's own flake
|
||||
# source even when consumed downstream.
|
||||
preset = builtins.fromJSON (builtins.readFile ../../themes/tokyo-night.json);
|
||||
preset = builtins.fromJSON (builtins.readFile ../../themes/boreal.json);
|
||||
|
||||
defaults = preset // {
|
||||
fonts = {
|
||||
@@ -54,16 +58,103 @@ let
|
||||
# tone for kanagawa/summer-*). Each preset declares its own so a theme
|
||||
# switch always replaces it (deep_merge would otherwise leave it stuck).
|
||||
border = { active = "accent"; inactive = "overlay"; };
|
||||
|
||||
# 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.
|
||||
# settings.displayProfile / displayProfileAuto: active named layout +
|
||||
# auto-switch on plug events (hyprland.nix / Display menu).
|
||||
# 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;
|
||||
displayProfile = "";
|
||||
displayProfileAuto = false;
|
||||
};
|
||||
};
|
||||
|
||||
parsed = lib.recursiveUpdate defaults themeState;
|
||||
|
||||
# ── Friendly eval-time validation ───────────────────────────────────
|
||||
# The same schema nomarchy-theme-sync enforces before every write. A
|
||||
# HAND-edited state file (the one path that bypasses the tool) must
|
||||
# fail with the field, the problem, and the fix — not a Nix stack
|
||||
# trace deep in some consumer. Checks run on `parsed` (after the
|
||||
# defaults), so missing fields are fine; only wrong values throw.
|
||||
isHex = v: builtins.isString v && builtins.match "#[0-9a-fA-F]{6}" v != null;
|
||||
isNum = v: builtins.isInt v || builtins.isFloat v;
|
||||
colorRoles = [ "base" "mantle" "surface" "overlay" "text" "subtext"
|
||||
"muted" "accent" "accentAlt" "good" "warn" "bad" ];
|
||||
got = v: "got: ${builtins.toJSON v}";
|
||||
problems = lib.concatLists [
|
||||
(map (k: "colors.${k} must be \"#RRGGBB\" (${got (parsed.colors.${k} or null)})")
|
||||
(builtins.filter (k: !isHex (parsed.colors.${k} or null)) colorRoles))
|
||||
(lib.optional (!(parsed.mode == "dark" || parsed.mode == "light"))
|
||||
"mode must be \"dark\" or \"light\" (${got parsed.mode})")
|
||||
(map (k: "ui.${k} must be a non-negative whole number (${got (parsed.ui.${k} or null)})")
|
||||
(builtins.filter
|
||||
(k: let v = parsed.ui.${k} or null; in !(builtins.isInt v && v >= 0))
|
||||
[ "gapsIn" "gapsOut" "borderSize" "rounding" "iconSize" ]))
|
||||
(map (k: "ui.${k} must be a number between 0 and 1 (${got (parsed.ui.${k} or null)})")
|
||||
(builtins.filter
|
||||
(k: let v = parsed.ui.${k} or null; in !(isNum v && v >= 0 && v <= 1))
|
||||
[ "activeOpacity" "inactiveOpacity" "terminalOpacity" ]))
|
||||
(map (k: "ui.${k} must be true or false (${got (parsed.ui.${k} or null)})")
|
||||
(builtins.filter (k: !builtins.isBool (parsed.ui.${k} or null))
|
||||
[ "blur" "shadow" ]))
|
||||
(map (k: "fonts.${k} must be a font-family string (${got (parsed.fonts.${k} or null)})")
|
||||
(builtins.filter (k: !builtins.isString (parsed.fonts.${k} or null))
|
||||
[ "mono" "ui" ]))
|
||||
(lib.optional (!(isNum (parsed.fonts.size or null) && parsed.fonts.size > 0))
|
||||
"fonts.size must be a positive number (${got (parsed.fonts.size or null)})")
|
||||
(lib.optional (!(builtins.isList parsed.ansi
|
||||
&& builtins.length parsed.ansi == 16
|
||||
&& lib.all isHex parsed.ansi))
|
||||
"ansi must be a list of exactly 16 \"#RRGGBB\" strings")
|
||||
(map (k: "border.${k} must be a palette role or \"#RRGGBB\" (${got (parsed.border.${k} or null)})")
|
||||
(builtins.filter
|
||||
(k: let v = parsed.border.${k} or null;
|
||||
in !(builtins.isString v && (builtins.elem v colorRoles || isHex v)))
|
||||
[ "active" "inactive" ]))
|
||||
];
|
||||
checked =
|
||||
if problems == [ ] then parsed
|
||||
else throw ''
|
||||
|
||||
Nomarchy: your theme-state.json is invalid:
|
||||
${lib.concatMapStrings (p: " ✖ ${p}\n") problems}
|
||||
Fix the named field(s) in the theme-state.json of your flake
|
||||
checkout (usually ~/.nomarchy/theme-state.json — the store copy at
|
||||
${toString cfg.stateFile} is a snapshot of it). The tool prints
|
||||
the same report with per-field fixes: `nomarchy-theme-sync
|
||||
validate`. Re-applying any preset resets all appearance fields:
|
||||
`nomarchy-theme-sync apply boreal`.'';
|
||||
|
||||
# A border value is a palette key (look it up in colors) unless it's
|
||||
# already a literal hex; unknown keys fall through to the raw string.
|
||||
resolveColor = v: if lib.hasPrefix "#" v then v else parsed.colors.${v} or v;
|
||||
# already a literal hex. Unknown roles are rejected by the field checks
|
||||
# above; resolve only runs on a validated state.
|
||||
resolveColor = v:
|
||||
if lib.hasPrefix "#" v then v
|
||||
else parsed.colors.${v} or (throw ''
|
||||
|
||||
Nomarchy: border role "${v}" is not in the palette (colors.*).
|
||||
Use one of: ${lib.concatStringsSep ", " colorRoles}
|
||||
or a literal "#RRGGBB". Validate with: nomarchy-theme-sync validate'');
|
||||
border = {
|
||||
active = resolveColor parsed.border.active;
|
||||
inactive = resolveColor parsed.border.inactive;
|
||||
active = resolveColor checked.border.active;
|
||||
inactive = resolveColor checked.border.inactive;
|
||||
};
|
||||
|
||||
# Resolve the icon theme once and expose it on nomarchy.theme so both
|
||||
@@ -76,7 +167,12 @@ let
|
||||
in
|
||||
{
|
||||
config = {
|
||||
nomarchy.theme = parsed // { inherit iconTheme border; };
|
||||
nomarchy.theme = checked // { 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 = checked.settings;
|
||||
|
||||
nomarchy.lib = {
|
||||
# "#7aa2f7" -> "rgb(7aa2f7)" (Hyprland color syntax)
|
||||
|
||||
44
modules/home/timezone.nix
Normal file
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
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" ];
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
15
modules/home/viewers.nix
Normal file
15
modules/home/viewers.nix
Normal file
@@ -0,0 +1,15 @@
|
||||
# Document & image viewers — the "open a PDF / open a photo" half of a
|
||||
# complete workstation. zathura goes through its HM module (not a bare
|
||||
# package) because Stylix themes it via programs.zathura.options — the
|
||||
# reason it's a component toggle here rather than a template package
|
||||
# line. imv rides along: wayland-native, and an image viewer is a
|
||||
# borderless dark surface — nothing to theme. The heavier editors
|
||||
# (GIMP, Inkscape) stay template packages; mime.nix points the default
|
||||
# associations at these viewers.
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
lib.mkIf config.nomarchy.viewers.enable {
|
||||
programs.zathura.enable = lib.mkDefault true;
|
||||
|
||||
home.packages = [ pkgs.imv ];
|
||||
}
|
||||
@@ -28,8 +28,46 @@ let
|
||||
# Named writeShellScriptBins (put on PATH via home.packages below) rather
|
||||
# than bare writeShellScript store paths, so the whole-swap themes' static
|
||||
# waybar.jsonc can exec them by name too — same as the swaync bell.
|
||||
# Waybar supervisor — exec-once has no restart, so a crashed bar used to
|
||||
# leave the session bar-less until relogin (seen on hardware: a theme
|
||||
# switch crashed waybar mid-reload). Respawns on ANY exit — a plain
|
||||
# `pkill -x waybar` is now a clean restart, which nomarchy-theme-sync
|
||||
# uses instead of the crash-prone in-place SIGUSR2 reload when it sees
|
||||
# this supervisor running. Crash-loop guard: 5 exits within 10s of
|
||||
# their start → give up with a critical notification instead of
|
||||
# spinning. Stop the bar for real: pkill -f nomarchy-waybar (TERM is
|
||||
# trapped to take the child down too).
|
||||
waybarSupervisor = pkgs.writeShellScriptBin "nomarchy-waybar" ''
|
||||
child=
|
||||
trap '[ -n "$child" ] && kill "$child" 2>/dev/null; exit 0' TERM INT
|
||||
fails=0
|
||||
while :; do
|
||||
start=$(date +%s)
|
||||
waybar & child=$!
|
||||
wait "$child"; code=$?
|
||||
if [ $(( $(date +%s) - start )) -lt 10 ]; then
|
||||
fails=$((fails + 1))
|
||||
if [ "$fails" -ge 5 ]; then
|
||||
notify-send -u critical "Waybar" \
|
||||
"Crashing on start (exit $code) — check ~/.config/waybar. Giving up." 2>/dev/null
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
fails=0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
'';
|
||||
|
||||
powerProfileStatus = pkgs.writeShellScriptBin "nomarchy-powerprofile-status" ''
|
||||
case "$(ls /sys/class/power_supply/ 2>/dev/null)" in *BAT*) ;; *) exit 0 ;; esac
|
||||
# Name-agnostic system battery (BAT0, CMB0, …) — BACKLOG #60.
|
||||
has_bat=
|
||||
for d in /sys/class/power_supply/*/; do
|
||||
[ "$(cat "$d/type" 2>/dev/null)" = Battery ] || continue
|
||||
[ "$(cat "$d/scope" 2>/dev/null || echo System)" = Device ] && continue
|
||||
has_bat=1; break
|
||||
done
|
||||
[ -n "$has_bat" ] || exit 0
|
||||
command -v powerprofilesctl >/dev/null 2>&1 || exit 0
|
||||
prof=$(powerprofilesctl get 2>/dev/null) || exit 0
|
||||
case "$prof" in
|
||||
@@ -51,6 +89,54 @@ let
|
||||
[ -n "$next" ] && powerprofilesctl set "$next"
|
||||
'';
|
||||
|
||||
# Opens calcurse (a lightweight TUI calendar, month view by default) in a
|
||||
# floating, centered ghostty window — bound to the Waybar clock's on-click.
|
||||
# A distinct --class gives the window a matchable app-id for the
|
||||
# float+center+size windowrule (hyprland.nix); --gtk-single-instance=false
|
||||
# forces a fresh standalone window (ghostty defaults single-instance on).
|
||||
# calcurse ships uncommented in the downstream template (opt-out), so
|
||||
# self-gate with a helpful notify if it's been removed. ghostty is always
|
||||
# installed (ghostty.nix), so it can be relied on for the --class window.
|
||||
calendarLauncher = pkgs.writeShellScriptBin "nomarchy-calendar" ''
|
||||
if ! command -v calcurse >/dev/null 2>&1; then
|
||||
notify-send "Calendar" "calcurse isn't installed (removed from home.packages?)." 2>/dev/null
|
||||
exit 0
|
||||
fi
|
||||
exec ghostty --class=com.nomarchy.calendar --gtk-single-instance=false -e calcurse
|
||||
'';
|
||||
|
||||
# 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'
|
||||
'';
|
||||
|
||||
# Health warning — self-gates: prints nothing while nomarchy-doctor
|
||||
# exits 0, so the module only appears when the sheet has a ✖ (the
|
||||
# same self-hide discipline as vpn/nightlight/updates). The tooltip
|
||||
# carries the first failing lines; click opens the full sheet.
|
||||
# Absolute path to the doctor binary: waybar's custom-module env can
|
||||
# miss system PATH, and `command -v … || exit 0` then self-hides forever.
|
||||
doctorStatus = pkgs.writeShellScriptBin "nomarchy-doctor-status" ''
|
||||
out=$(${pkgs.nomarchy-doctor}/bin/nomarchy-doctor 2>/dev/null) && exit 0
|
||||
# Strip ANSI so the tooltip is plain text (and waybar never chokes on
|
||||
# control chars); keep only the ✖ lines.
|
||||
tip=$(printf '%s\n' "$out" \
|
||||
| ${pkgs.gnused}/bin/sed 's/\x1b\[[0-9;]*m//g' \
|
||||
| grep '✖' | head -5 \
|
||||
| ${pkgs.jq}/bin/jq -Rs 'rtrimstr("\n") + "\n(click for the full sheet)"')
|
||||
# (md-alert-circle): alert shape that survives incomplete Nerd Font
|
||||
# cuts better than ; class:bad still paints it @bad.
|
||||
printf '{"text":"","tooltip":%s,"class":"bad"}\n' "$tip"
|
||||
'';
|
||||
|
||||
# Per-theme override probe.
|
||||
assetDir = config.nomarchy.themesDir + "/${t.slug}";
|
||||
styleOverride = assetDir + "/waybar.css";
|
||||
@@ -63,7 +149,12 @@ let
|
||||
(lib.mapAttrsToList (name: value: "@define-color ${name} ${value};") t.colors);
|
||||
|
||||
generatedSettings = {
|
||||
layer = "top";
|
||||
# `bottom`, not `top`: on Hyprland the `top` layer renders above every
|
||||
# window — so a real fullscreen surface (a browser video gone
|
||||
# fullscreen) sits *under* the bar. On `bottom` the fullscreen window
|
||||
# covers the bar, while the exclusive zone still reserves its space in
|
||||
# normal tiling. Keep in sync with the whole-swap jsoncs (parity rule).
|
||||
layer = "bottom";
|
||||
position = "top";
|
||||
height = 34;
|
||||
margin-top = t.ui.gapsOut;
|
||||
@@ -75,11 +166,28 @@ let
|
||||
# home-manager switch restyles the running bar without a restart.
|
||||
reload_style_on_change = true;
|
||||
|
||||
modules-left = [ "hyprland/workspaces" "hyprland/window" ];
|
||||
# Logo + powermenu: whole-swaps already ship these (parity was reverse
|
||||
# — BACKLOG #63). Click targets are existing nomarchy-menu entry points.
|
||||
modules-left = [ "custom/nomarchy" "hyprland/workspaces" "hyprland/window" ];
|
||||
modules-center = [ "clock" ];
|
||||
modules-right = [ "tray" "pulseaudio" "network" "cpu" "memory" "custom/powerprofile" ]
|
||||
modules-right = [ "custom/recording" "idle_inhibitor" "tray" "custom/vpn" "pulseaudio" "custom/powerprofile" "custom/nightlight" ]
|
||||
++ lib.optional showLanguage "hyprland/language"
|
||||
++ [ "battery" "custom/notification" ];
|
||||
++ [ "battery" "custom/doctor" "custom/updates" "custom/notification" "custom/powermenu" ];
|
||||
|
||||
"custom/nomarchy" = {
|
||||
interval = "once";
|
||||
# U+F000 — Nomarchy monogram (literal UTF-8; Nix has no \u escapes).
|
||||
# CSS pins font-family: Nomarchy so Nerd Fonts' glass glyph does not win.
|
||||
format = "";
|
||||
on-click = "nomarchy-menu";
|
||||
tooltip-format = "Nomarchy menu";
|
||||
};
|
||||
|
||||
"custom/powermenu" = {
|
||||
format = ""; # U+F011 power symbol
|
||||
on-click = "nomarchy-menu power";
|
||||
tooltip = false;
|
||||
};
|
||||
|
||||
"hyprland/workspaces" = {
|
||||
format = "{icon}";
|
||||
@@ -93,55 +201,135 @@ let
|
||||
|
||||
clock = {
|
||||
format = "{:%H:%M}";
|
||||
format-alt = "{:%A %d %B %Y}";
|
||||
tooltip-format = "<tt><small>{calendar}</small></tt>";
|
||||
# Left-click → the calendar (nomarchy-calendar → calcurse in a floating
|
||||
# ghostty). Replaces the old format-alt date toggle; the long date now
|
||||
# rides in the tooltip's first line, so nothing is lost on hover.
|
||||
on-click = "nomarchy-calendar";
|
||||
# The zone line stays live under auto-timezone (the tz-watch SIGUSR2
|
||||
# reload keeps it showing the currently *detected* zone).
|
||||
tooltip-format = "{:%A %d %B %Y}\n{:%Z (UTC%z)}\n<tt><small>{calendar}</small></tt>";
|
||||
};
|
||||
|
||||
# Active keyboard layout (per focused device) — only placed in
|
||||
# modules-right when showLanguage (see above).
|
||||
"hyprland/language" = {
|
||||
format = " {short}";
|
||||
format = "<span size='${toString (t.fonts.size + 2)}pt'></span> {short}";
|
||||
tooltip = false;
|
||||
};
|
||||
|
||||
pulseaudio = {
|
||||
format = "{icon} {volume}%";
|
||||
format = "<span size='${toString (t.fonts.size + 2)}pt'>{icon}</span> {volume}%";
|
||||
format-muted = "";
|
||||
format-icons.default = [ "" "" "" ];
|
||||
on-click = "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
|
||||
# Right-click → the full mixer (per-app volumes) in a floating window
|
||||
# (item 35). pwvucontrol ships in the template suite; the Hyprland
|
||||
# windowrule floats it. Keep in sync with the whole-swap jsoncs.
|
||||
on-click-right = "pwvucontrol";
|
||||
};
|
||||
|
||||
network = {
|
||||
format-wifi = " {essid}";
|
||||
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";
|
||||
# Idle inhibitor (caffeine): click → to hold the screen awake —
|
||||
# blocks hypridle's lock / display-off / suspend during video or a
|
||||
# presentation. Waybar holds a Wayland idle-inhibit while activated;
|
||||
# the state resets with the bar (deliberate — caffeine shouldn't
|
||||
# survive a relogin). Summer-night's whole-swap bar had this first
|
||||
# (reverse parity gap, 2026-07-04).
|
||||
idle_inhibitor = {
|
||||
format = "{icon}";
|
||||
format-icons = { activated = ""; deactivated = ""; };
|
||||
tooltip-format-activated = "Screen held awake — click to release";
|
||||
tooltip-format-deactivated = "Keep the screen awake (caffeine)";
|
||||
};
|
||||
|
||||
cpu.format = " {usage}%";
|
||||
memory.format = " {percentage}%";
|
||||
# No network module: nm-applet lives in the tray (the GUI path), so the
|
||||
# bar's wifi/ethernet indicator would just duplicate it.
|
||||
|
||||
battery = {
|
||||
states = { warning = 25; critical = 10; };
|
||||
format = "{icon} {capacity}%";
|
||||
format-charging = " {capacity}%";
|
||||
format = "<span size='${toString (t.fonts.size + 2)}pt'>{icon}</span> {capacity}%";
|
||||
format-charging = "<span size='${toString (t.fonts.size + 2)}pt'></span> {capacity}%";
|
||||
format-icons = [ "" "" "" "" "" ];
|
||||
# Click either the battery or the power-profile icon → the combined
|
||||
# power menu (profile + charge cap). The granular System rows stay.
|
||||
on-click = "nomarchy-menu powermgmt";
|
||||
};
|
||||
|
||||
"custom/powerprofile" = {
|
||||
exec = "nomarchy-powerprofile-status";
|
||||
return-type = "json";
|
||||
interval = 5;
|
||||
on-click = "nomarchy-powerprofile-cycle";
|
||||
# Opens the same combined power menu as the battery icon (item 36b).
|
||||
# nomarchy-powerprofile-cycle stays a standalone bin for downstream
|
||||
# rebinding — it's just no longer the default click.
|
||||
on-click = "nomarchy-menu powermgmt";
|
||||
};
|
||||
|
||||
# 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";
|
||||
};
|
||||
|
||||
# Health check is real work (systemctl/disk/git sweeps), so a long
|
||||
# interval — this is a tripwire, not a monitor.
|
||||
"custom/doctor" = {
|
||||
exec = "nomarchy-doctor-status";
|
||||
return-type = "json";
|
||||
format = "{}";
|
||||
interval = 300;
|
||||
# signal 10: poke after a fix (or from theme-shot) so the tripwire
|
||||
# doesn't wait a full interval after a first-poll miss.
|
||||
signal = 10;
|
||||
on-click = "nomarchy-menu doctor";
|
||||
};
|
||||
|
||||
# Screen recording indicator. Self-gates: visible only while
|
||||
# nomarchy-record runs (status prints nothing otherwise), and the
|
||||
# click IS the stop surface — the menu only starts recordings.
|
||||
# signal 8: the recorder pokes the bar on start/stop so the ⏺
|
||||
# appears/vanishes instantly (the interval is just a safety net).
|
||||
"custom/recording" = {
|
||||
exec = "nomarchy-record status";
|
||||
return-type = "json";
|
||||
interval = 10;
|
||||
signal = 8;
|
||||
on-click = "nomarchy-record stop";
|
||||
};
|
||||
|
||||
# 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
|
||||
# (text/tooltip/class) on every change, so it tracks count and DND with
|
||||
# no polling. Left-click toggles the panel; right-click toggles DND.
|
||||
# The `dnd-*` classes (bell-off glyph) are styled muted below.
|
||||
# Every *suppressed* state — DND or app-inhibited — maps to the bell-off
|
||||
# glyph and the muted color below, so "notifications are off right now"
|
||||
# reads by SHAPE, not color alone (color-blind sweep, item 28): inhibited
|
||||
# used to reuse the normal / bells, distinguishable only by color.
|
||||
"custom/notification" = {
|
||||
exec = "swaync-client -swb";
|
||||
return-type = "json";
|
||||
@@ -152,8 +340,8 @@ let
|
||||
notification = "";
|
||||
dnd-none = "";
|
||||
dnd-notification = "";
|
||||
inhibited-none = "";
|
||||
inhibited-notification = "";
|
||||
inhibited-none = "";
|
||||
inhibited-notification = "";
|
||||
dnd-inhibited-none = "";
|
||||
dnd-inhibited-notification = "";
|
||||
};
|
||||
@@ -183,6 +371,11 @@ let
|
||||
border-radius: ${toString t.ui.rounding}px;
|
||||
}
|
||||
|
||||
/* Dim states use the palette's @muted role: since item 28b it is
|
||||
floor-guaranteed legible on @base in every theme (muted/base >=
|
||||
2.0, gated by tools/check-theme-contrast.py) — the palettes that
|
||||
once made it vanish (gruvbox muted≈base, item 27) were retuned.
|
||||
Secondary-but-not-dim stays alpha(@text, 0.85). */
|
||||
#workspaces button {
|
||||
padding: 0 8px;
|
||||
color: @muted;
|
||||
@@ -200,7 +393,7 @@ let
|
||||
}
|
||||
|
||||
#window {
|
||||
color: @subtext;
|
||||
color: alpha(@text, 0.85);
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
@@ -209,15 +402,63 @@ let
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#tray, #pulseaudio, #network, #cpu, #memory, #custom-powerprofile, #language, #battery, #custom-notification {
|
||||
color: @subtext;
|
||||
#custom-nomarchy {
|
||||
color: @accent;
|
||||
font-family: Nomarchy;
|
||||
font-size: ${toString (t.fonts.size + 4)}pt;
|
||||
padding: 0 10px;
|
||||
}
|
||||
#custom-nomarchy:hover { color: @accentAlt; }
|
||||
|
||||
/* notifications waiting → accent; Do-Not-Disturb → muted bell-off */
|
||||
#tray, #pulseaudio, #custom-powerprofile, #custom-nightlight, #custom-updates, #custom-vpn, #custom-recording, #idle_inhibitor, #language, #battery, #custom-doctor, #custom-notification, #custom-powermenu {
|
||||
color: alpha(@text, 0.85);
|
||||
padding: 0 10px;
|
||||
}
|
||||
#custom-powermenu:hover { color: @bad; }
|
||||
|
||||
/* Group rhythm (item 28c): a wider breath before each functional
|
||||
group of the right cluster — media/stats · toggles · status —
|
||||
on top of the uniform module spacing. A group head can self-hide
|
||||
(e.g. no battery on desktops); grouping then degrades to the
|
||||
uniform spacing, never breaks. */
|
||||
#pulseaudio, #custom-powerprofile, #battery { margin-left: 14px; }
|
||||
|
||||
/* Caffeine engaged → warm tone (the screen is being held awake). */
|
||||
#idle_inhibitor.activated { color: @warn; }
|
||||
|
||||
/* Recording in progress → the alert red; the ⏺ is also the stop button. */
|
||||
#custom-recording.recording { color: @bad; }
|
||||
|
||||
/* 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; }
|
||||
|
||||
/* Doctor tripwire — only rendered when something is ✖, so it is
|
||||
always the alert color. */
|
||||
#custom-doctor { color: @bad; }
|
||||
|
||||
/* Icon-only status modules carry no text, so they don't get the
|
||||
(size+2)pt Pango icon span the icon+text modules use — bump their
|
||||
font-size to match, or these glyphs read smaller than the volume /
|
||||
battery / language icons beside them. */
|
||||
#custom-recording, #custom-updates, #custom-vpn, #custom-nightlight, #custom-doctor, #custom-notification, #custom-powermenu { font-size: ${toString (t.fonts.size + 2)}pt; }
|
||||
|
||||
/* The speedometer + caffeine glyphs render small in their em box —
|
||||
size them up a touch more so they read at a glance. */
|
||||
#custom-powerprofile, #idle_inhibitor { font-size: ${toString (t.fonts.size + 3)}pt; }
|
||||
|
||||
/* notifications waiting → accent; suppressed (DND or app-inhibited) →
|
||||
muted bell-off, so it reads by shape+color, never color alone. */
|
||||
#custom-notification.notification { color: @accent; }
|
||||
#custom-notification.dnd-none,
|
||||
#custom-notification.dnd-notification { color: @muted; }
|
||||
#custom-notification.dnd-notification,
|
||||
#custom-notification.inhibited-none,
|
||||
#custom-notification.inhibited-notification { color: @muted; }
|
||||
|
||||
#pulseaudio.muted { color: @muted; }
|
||||
#battery.warning { color: @warn; }
|
||||
@@ -228,7 +469,14 @@ in
|
||||
{
|
||||
programs.waybar = lib.mkIf config.nomarchy.waybar.enable {
|
||||
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
|
||||
# a plain home.nix assignment. For per-theme identity, prefer the
|
||||
@@ -249,7 +497,8 @@ in
|
||||
};
|
||||
|
||||
# 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 — plus
|
||||
# the supervisor hyprland.nix exec-onces.
|
||||
home.packages = lib.optionals config.nomarchy.waybar.enable
|
||||
[ powerProfileStatus powerProfileCycle ];
|
||||
[ waybarSupervisor powerProfileStatus powerProfileCycle vpnStatus doctorStatus calendarLauncher ];
|
||||
}
|
||||
|
||||
@@ -22,27 +22,48 @@ let
|
||||
'';
|
||||
in
|
||||
{
|
||||
imports = [ ./options.nix ./plymouth.nix ./file-manager.nix ./power.nix ./services.nix ];
|
||||
imports = [ ./options.nix ./plymouth.nix ./greeter.nix ./file-manager.nix ./power.nix ./services.nix ./hardware.nix ./timezone.nix ./oom.nix ];
|
||||
|
||||
config = {
|
||||
# The safe half of distro branding: distroName flows into
|
||||
# /etc/os-release PRETTY_NAME, systemd-boot entry titles and the
|
||||
# ISO boot-menu label. distroId stays "nixos" on purpose — it feeds
|
||||
# DEFAULT_HOSTNAME and upstream isNixos checks (see roadmap).
|
||||
# Distro branding. distroName flows into /etc/os-release PRETTY_NAME,
|
||||
# systemd-boot entry titles and the ISO boot-menu label; distroId is the
|
||||
# machine-readable ID (DEFAULT_HOSTNAME, lsb DISTRIB_ID, CPE name).
|
||||
# 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.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
|
||||
# is mostly seen over SSH or on a bare console). Branded, and doubles
|
||||
# as a cheat sheet for the distro's own helpers.
|
||||
users.motd = lib.mkDefault ''
|
||||
# as a cheat sheet for the distro's own helpers. Fingerprint line only
|
||||
# when fprintd is enabled (no permanent nag on machines without a
|
||||
# reader); doctor is always on PATH via systemPackages.
|
||||
users.motd = lib.mkDefault (''
|
||||
|
||||
${distroName} — a NixOS desktop, themed from one JSON.
|
||||
|
||||
sys-update update inputs + rebuild the system
|
||||
sys-rebuild rebuild the system, no input update
|
||||
home-update apply home/theme changes (no sudo)
|
||||
nomarchy-theme-sync apply <theme> switch the whole palette
|
||||
nomarchy-doctor read-only health check
|
||||
SUPER+? keybindings cheatsheet
|
||||
'';
|
||||
SUPER+M → System › Firmware check LVFS firmware updates (fwupd)
|
||||
'' + lib.optionalString config.nomarchy.hardware.fingerprint.enable ''
|
||||
SUPER+M → System › Fingerprint enroll a finger (fprintd)
|
||||
'');
|
||||
|
||||
# Unfree allowed distro-wide: pragmatic-desktop territory (vendor
|
||||
# GPU/wifi drivers, firmware, fonts, …). The custom nixpkgs-config
|
||||
@@ -69,6 +90,15 @@ in
|
||||
console.earlySetup = 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;
|
||||
|
||||
# Magic SysRq Keys: safety net for Wayland lockups. Alt+SysRq+REISUB allows
|
||||
# safe reboot without data loss when the compositor hangs.
|
||||
boot.kernel.sysctl."kernel.sysrq" = lib.mkDefault 1;
|
||||
|
||||
# ── Wayland session: Hyprland ────────────────────────────────────
|
||||
# Installs the binary, registers the session, wires up
|
||||
# xdg-desktop-portal-hyprland. Configuration is Home Manager's job.
|
||||
@@ -79,22 +109,8 @@ in
|
||||
extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; # file pickers, etc.
|
||||
};
|
||||
|
||||
services.greetd = lib.mkIf cfg.greeter.enable {
|
||||
enable = lib.mkDefault true;
|
||||
settings = {
|
||||
default_session = {
|
||||
# start-hyprland is Hyprland 0.55's watchdog launcher; running
|
||||
# the bare binary makes every session print a warning.
|
||||
command = lib.mkDefault "${pkgs.tuigreet}/bin/tuigreet --time --remember --greeting 'Welcome to ${distroName}' --cmd start-hyprland";
|
||||
user = "greeter";
|
||||
};
|
||||
# Boot straight into the session once; logout → normal greeter.
|
||||
initial_session = lib.mkIf (cfg.greeter.autoLogin != null) {
|
||||
command = "start-hyprland";
|
||||
user = cfg.greeter.autoLogin;
|
||||
};
|
||||
};
|
||||
};
|
||||
# The greetd/tuigreet login screen lives in ./greeter.nix — themed
|
||||
# from the state JSON (console.colors + --theme) at system rebuild.
|
||||
|
||||
# ── Audio: Pipewire ──────────────────────────────────────────────
|
||||
security.rtkit.enable = lib.mkDefault cfg.audio.enable;
|
||||
@@ -113,6 +129,10 @@ in
|
||||
services.dbus.enable = lib.mkDefault true;
|
||||
services.upower.enable = lib.mkDefault true;
|
||||
|
||||
# Hardware security key (FIDO2/U2F/GPG) support. Without pcscd, tokens like
|
||||
# YubiKeys often silently fail in WebAuthn (browsers), SSH, and GPG.
|
||||
services.pcscd.enable = lib.mkDefault true;
|
||||
|
||||
# Firmware updates via LVFS. Ships the daemon + its metadata-refresh
|
||||
# timer only — it never flashes anything on its own; applying an update
|
||||
# is an explicit `fwupdmgr update`. On by default as desktop security
|
||||
@@ -121,7 +141,33 @@ in
|
||||
# `services.fwupd.enable = false`.
|
||||
services.fwupd.enable = lib.mkDefault true;
|
||||
|
||||
# Drive health monitoring (SMART).
|
||||
services.smartd = {
|
||||
enable = lib.mkDefault true;
|
||||
notifications.x11.enable = lib.mkDefault true;
|
||||
notifications.wall.enable = lib.mkDefault true;
|
||||
};
|
||||
# Core security: enable AppArmor to confine desktop apps and services.
|
||||
security.apparmor.enable = true;
|
||||
security.apparmor.killUnconfinedConfinables = false;
|
||||
|
||||
# Core stability: reboot automatically after 10s on a kernel panic
|
||||
# (prevents the system from hanging indefinitely on a black screen).
|
||||
boot.kernelParams = [ "panic=10" "oops=panic" ];
|
||||
|
||||
# Explicitly enable systembus-notify to resolve a mkDefault conflict
|
||||
# between earlyoom (true) and smartd (false).
|
||||
services.systembus-notify.enable = true;
|
||||
|
||||
# Enable I2C and DDC/CI by default for external monitor brightness control.
|
||||
nomarchy.hardware.i2c.ddcci = 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
|
||||
# right for suspend (resumes from RAM, no other gate), but an encrypted
|
||||
@@ -231,6 +277,13 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
# Background filesystem health checks. Scans all BTRFS filesystems to detect
|
||||
# and (if RAID/dup) correct bitrot before it propagates.
|
||||
services.btrfs.autoScrub = lib.mkIf ((config.fileSystems."/".fsType or "") == "btrfs") {
|
||||
enable = lib.mkDefault true;
|
||||
interval = lib.mkDefault "monthly";
|
||||
};
|
||||
|
||||
# ── Fonts ────────────────────────────────────────────────────────
|
||||
# The ten most popular Nerd Fonts ship by default, so any of them
|
||||
# can be named in the theme state's fonts.mono and actually resolve
|
||||
@@ -249,6 +302,10 @@ in
|
||||
# (every weight × variant) — too heavy for the ISO and closures.
|
||||
nerd-fonts.mononoki
|
||||
nerd-fonts.inconsolata
|
||||
# GeistMono (57 MB) — the Boreal theme's mono face; modern and
|
||||
# geometric, unlike the ten defaults. Cheap enough for the ISO
|
||||
# (Iosevka above was rejected at 1.1 GB; this is a compact family).
|
||||
nerd-fonts.geist-mono
|
||||
inter
|
||||
noto-fonts
|
||||
noto-fonts-color-emoji
|
||||
@@ -268,13 +325,16 @@ in
|
||||
# ── Essential packages ───────────────────────────────────────────
|
||||
environment.systemPackages = with pkgs; [
|
||||
nomarchy-theme-sync # provided by overlays.default
|
||||
nomarchy-doctor # read-only health check (System › Doctor)
|
||||
nomarchy-control-center # TUI control center
|
||||
nomarchy-detect-hw # post-install hardware re-probe (HARDWARE.md §8)
|
||||
|
||||
# Friendly wrappers for the two rebuild paths (README §3). Run as
|
||||
# your user: `nix flake update` must NOT run as root (libgit2
|
||||
# refuses the user-owned flake repo) — sudo happens inside, only
|
||||
# for the system switch.
|
||||
(pkgs.writeShellScriptBin "sys-update" ''
|
||||
set -e
|
||||
set -euo pipefail
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
echo "sys-update: run as your normal user (it sudos the rebuild itself)" >&2
|
||||
exit 1
|
||||
@@ -282,15 +342,99 @@ in
|
||||
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
|
||||
echo "sys-update: updating flake inputs in $flake"
|
||||
nix flake update --flake "$flake"
|
||||
before=$(readlink -f /run/current-system)
|
||||
log=$(mktemp)
|
||||
trap 'rm -f "$log"' EXIT
|
||||
set +e
|
||||
if command -v nixos-rebuild-snap >/dev/null 2>&1; then
|
||||
sudo nixos-rebuild-snap "$@" # BTRFS snapshot first
|
||||
sudo nixos-rebuild-snap "$@" 2>&1 | tee "$log"
|
||||
else
|
||||
sudo nixos-rebuild switch --flake "$flake#default" "$@"
|
||||
sudo nixos-rebuild switch --flake "$flake#default" "$@" 2>&1 | tee "$log"
|
||||
fi
|
||||
rc=''${PIPESTATUS[0]}
|
||||
set -e
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo
|
||||
echo "sys-update: rebuild FAILED (exit $rc). Last lines:"
|
||||
tail -n 40 "$log" || true
|
||||
echo
|
||||
echo "Diagnose: nomarchy-doctor"
|
||||
echo "Recovery: docs/RECOVERY.md (boot menu generations / snapper)"
|
||||
exit "$rc"
|
||||
fi
|
||||
# What did that update actually change? Package-level diff of the
|
||||
# old vs new generation (the informative half of "informative +
|
||||
# rock-stable"); never fails the run.
|
||||
after=$(readlink -f /run/current-system)
|
||||
if [ "$before" = "$after" ]; then
|
||||
echo "sys-update: no changes — the system is identical."
|
||||
else
|
||||
echo "sys-update: what changed:"
|
||||
${pkgs.nvd}/bin/nvd diff "$before" "$after" || true
|
||||
fi
|
||||
'')
|
||||
# The no-update twin (hardware-QA request): rebuild the system
|
||||
# against the CURRENT lock — config changes only, no `nix flake
|
||||
# update` — mirroring how home-update never touches the lock.
|
||||
# Same snapshot-first path when available.
|
||||
(pkgs.writeShellScriptBin "sys-rebuild" ''
|
||||
set -euo pipefail
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
echo "sys-rebuild: run as your normal user (it sudos the rebuild itself)" >&2
|
||||
exit 1
|
||||
fi
|
||||
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
|
||||
before=$(readlink -f /run/current-system)
|
||||
log=$(mktemp)
|
||||
trap 'rm -f "$log"' EXIT
|
||||
set +e
|
||||
if command -v nixos-rebuild-snap >/dev/null 2>&1; then
|
||||
sudo nixos-rebuild-snap "$@" 2>&1 | tee "$log"
|
||||
else
|
||||
sudo nixos-rebuild switch --flake "$flake#default" "$@" 2>&1 | tee "$log"
|
||||
fi
|
||||
rc=''${PIPESTATUS[0]}
|
||||
set -e
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo
|
||||
echo "sys-rebuild: rebuild FAILED (exit $rc). Last lines:"
|
||||
tail -n 40 "$log" || true
|
||||
echo
|
||||
echo "Diagnose: nomarchy-doctor"
|
||||
echo "Recovery: docs/RECOVERY.md (boot menu generations / snapper)"
|
||||
exit "$rc"
|
||||
fi
|
||||
# Same what-changed diff as sys-update (the twins stay twins).
|
||||
after=$(readlink -f /run/current-system)
|
||||
if [ "$before" = "$after" ]; then
|
||||
echo "sys-rebuild: no changes — the system is identical."
|
||||
else
|
||||
echo "sys-rebuild: what changed:"
|
||||
${pkgs.nvd}/bin/nvd diff "$before" "$after" || true
|
||||
fi
|
||||
'')
|
||||
(pkgs.writeShellScriptBin "home-update" ''
|
||||
set -euo pipefail
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
echo "home-update: run as your normal user" >&2
|
||||
exit 1
|
||||
fi
|
||||
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
|
||||
log=$(mktemp)
|
||||
trap 'rm -f "$log"' EXIT
|
||||
set +e
|
||||
home-manager switch --flake "$flake" "$@" 2>&1 | tee "$log"
|
||||
rc=''${PIPESTATUS[0]}
|
||||
set -e
|
||||
exec home-manager switch --flake "''${NOMARCHY_PATH:-$HOME/.nomarchy}" "$@"
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo
|
||||
echo "home-update: switch FAILED (exit $rc). Last lines:"
|
||||
tail -n 40 "$log" || true
|
||||
echo
|
||||
echo "Diagnose: nomarchy-doctor"
|
||||
echo "Recovery: home-manager generations (or docs/RECOVERY.md)"
|
||||
exit "$rc"
|
||||
fi
|
||||
'')
|
||||
|
||||
git
|
||||
@@ -304,6 +448,7 @@ in
|
||||
wl-clipboard
|
||||
grim
|
||||
slurp
|
||||
hyprpicker
|
||||
] ++ lib.optional (cfg.snapper.enable && (config.fileSystems."/".fsType or "") == "btrfs")
|
||||
# Snapshot, then rebuild — rollback material for system changes
|
||||
# (theme changes don't need it; HM generations already roll back).
|
||||
@@ -320,8 +465,73 @@ in
|
||||
nixos-rebuild switch --flake /etc/nixos#default "$@"
|
||||
'')
|
||||
# The desktop snapshot manager (browse / diff / restore / rollback over
|
||||
# snapper, elevating via polkit) — what `nomarchy-menu snapshot` launches.
|
||||
++ lib.optional cfg.snapper.enable pkgs.btrfs-assistant;
|
||||
# snapper, elevating via polkit) — the primary `nomarchy-menu snapshot`
|
||||
# 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.
|
||||
boot.loader.systemd-boot.configurationLimit = lib.mkDefault 10;
|
||||
|
||||
@@ -25,6 +25,7 @@ in
|
||||
services.udisks2.enable = lib.mkDefault true; # mount/unmount removable media
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
file-roller # GUI archive manager (backend for thunar-archive-plugin)
|
||||
ffmpegthumbnailer # video thumbnails (Thunar + yazi)
|
||||
libgsf # ODF thumbnails
|
||||
poppler-utils # PDF thumbnails / pdftoppm (yazi PDF preview too)
|
||||
|
||||
70
modules/nixos/greeter.nix
Normal file
70
modules/nixos/greeter.nix
Normal file
@@ -0,0 +1,70 @@
|
||||
# Greeter — greetd/tuigreet, themed from the same theme-state.json that
|
||||
# drives the desktop (nomarchy.system.stateFile; the Plymouth model:
|
||||
# baked at SYSTEM rebuild, so it follows the theme as of the last
|
||||
# sys-update, not the last instant apply).
|
||||
#
|
||||
# tuigreet draws on the virtual console with the 16 ANSI slots, so the
|
||||
# theming is two-part:
|
||||
# 1. console.colors — the VT palette becomes the theme's ansi[] hexes
|
||||
# (which also themes raw ttys and the LUKS passphrase prompt: the
|
||||
# same JSON reaches every pre-session surface).
|
||||
# 2. --theme — tuigreet components on NAMED slots (its parser is
|
||||
# ratatui Color::from_str; names map to the standard indexes, e.g.
|
||||
# blue=4, gray=7, white=15, so the palette above hands them the
|
||||
# theme's colors). ANSI "black" stays dark even in light themes —
|
||||
# the greeter reads terminal-dark there, the same convention every
|
||||
# terminal applies to ANSI colors.
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.nomarchy.system;
|
||||
distroName = config.system.nixos.distroName;
|
||||
|
||||
state =
|
||||
if cfg.stateFile != null
|
||||
then builtins.fromJSON (builtins.readFile cfg.stateFile)
|
||||
else { };
|
||||
# A sparse/hand-rolled state without a proper ansi block just skips the
|
||||
# theming (stock tuigreet grey) — never an eval error.
|
||||
ansi = state.ansi or [ ];
|
||||
themed = builtins.isList ansi && builtins.length ansi == 16;
|
||||
|
||||
tuigreetTheme = lib.concatStringsSep ";" [
|
||||
"container=black" # ansi[0] — the theme's terminal background
|
||||
"border=blue" # ansi[4] — the accent family in every shipped palette
|
||||
"title=cyan"
|
||||
"greet=cyan"
|
||||
"prompt=green"
|
||||
"input=white" # ansi[15] — bright foreground
|
||||
"action=blue"
|
||||
"button=yellow"
|
||||
"time=cyan"
|
||||
"text=gray" # ansi[7] — muted foreground
|
||||
];
|
||||
in
|
||||
{
|
||||
config = {
|
||||
# VT palette from the theme (RRGGBB, no #; lands as vt.default_* kernel
|
||||
# params). mkDefault so a downstream console.colors wins.
|
||||
console.colors = lib.mkIf themed (lib.mkDefault (map (lib.removePrefix "#") ansi));
|
||||
|
||||
services.greetd = lib.mkIf cfg.greeter.enable {
|
||||
enable = lib.mkDefault true;
|
||||
settings = {
|
||||
default_session = {
|
||||
# start-hyprland is Hyprland 0.55's watchdog launcher; running
|
||||
# the bare binary makes every session print a warning.
|
||||
command = lib.mkDefault ("${pkgs.tuigreet}/bin/tuigreet --time --remember --greeting 'Welcome to ${distroName}'"
|
||||
+ lib.optionalString themed " --theme '${tuigreetTheme}'"
|
||||
+ " --cmd start-hyprland");
|
||||
user = "greeter";
|
||||
};
|
||||
# Boot straight into the session once; logout → normal greeter.
|
||||
initial_session = lib.mkIf (cfg.greeter.autoLogin != null) {
|
||||
command = "start-hyprland";
|
||||
user = cfg.greeter.autoLogin;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
281
modules/nixos/hardware.nix
Normal file
281
modules/nixos/hardware.nix
Normal file
@@ -0,0 +1,281 @@
|
||||
# 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;
|
||||
# Fingerprint PAM can follow theme-state.json (menu toggle → next
|
||||
# sys-rebuild), same bridge as autoTimezone (BACKLOG #55). Missing or
|
||||
# invalid JSON fails closed (theme-state-read.nix) instead of a raw stack.
|
||||
hwState =
|
||||
if config.nomarchy.system.stateFile != null
|
||||
then import ../theme-state-read.nix { inherit lib; } config.nomarchy.system.stateFile
|
||||
else { };
|
||||
pamFromState = (hwState.settings or { }).fingerprint.pam or false;
|
||||
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.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = pamFromState;
|
||||
defaultText = lib.literalExpression
|
||||
"(settings.fingerprint.pam from theme-state.json) or false";
|
||||
description = ''
|
||||
Use the fingerprint for login and sudo (PAM). Opt-in — password-only
|
||||
stays the default for the cautious; enroll a finger first. Defaults
|
||||
from theme-state.json `settings.fingerprint.pam` (System › Fingerprint
|
||||
menu) when set; otherwise false.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
i2c = {
|
||||
enable = lib.mkEnableOption ''
|
||||
I2C devices support. Enables access to /dev/i2c-* (useful for RGB
|
||||
controllers, sensors, and DDC/CI monitor control)'';
|
||||
|
||||
ddcci = lib.mkEnableOption ''
|
||||
the ddcci-driver kernel module to expose external monitors as standard
|
||||
backlight devices via DDC/CI. This allows brightness keys and swayosd
|
||||
to natively control external displays'';
|
||||
};
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
];
|
||||
};
|
||||
})
|
||||
|
||||
# ── I2C / DDC/CI ───────────────────────────────────────────────────
|
||||
(lib.mkIf cfg.i2c.enable {
|
||||
hardware.i2c.enable = true;
|
||||
})
|
||||
(lib.mkIf cfg.i2c.ddcci {
|
||||
# The driver needs I2C underneath it
|
||||
hardware.i2c.enable = true;
|
||||
boot.extraModulePackages = [ config.boot.kernelPackages.ddcci-driver ];
|
||||
boot.kernelModules = [ "ddcci_backlight" ];
|
||||
})
|
||||
|
||||
# ── 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
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;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
# Deliberately small: only things a downstream user plausibly disagrees
|
||||
# with get a toggle. Everything else in the system module is set with
|
||||
# lib.mkDefault, so plain NixOS options override it natively.
|
||||
{ lib, ... }:
|
||||
{ config, lib, ... }:
|
||||
|
||||
{
|
||||
options.nomarchy.system = {
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
greeter.autoLogin = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
default = config.nomarchy.settings.greeter.autoLogin or null;
|
||||
example = "ada";
|
||||
description = ''
|
||||
Log this user straight into Hyprland on boot (greetd
|
||||
@@ -33,7 +33,7 @@
|
||||
description = ''
|
||||
theme-state.json for the system-side consumers (currently the
|
||||
Plymouth splash background). lib.mkFlake wires it automatically
|
||||
from your flake; null falls back to the Tokyo Night base color.
|
||||
from your flake; null falls back to the Boreal base color.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -44,7 +44,17 @@
|
||||
option'' // { 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 = config.nomarchy.settings.bluetooth.enable or 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 ''
|
||||
hourly/daily BTRFS timeline snapshots of / via snapper, plus the
|
||||
@@ -92,15 +102,17 @@
|
||||
|
||||
batteryChargeLimit = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.ints.between 50 100);
|
||||
default = null;
|
||||
default = config.nomarchy.settings.power.batteryChargeLimit or null;
|
||||
example = 80;
|
||||
description = ''
|
||||
Stop charging at this percentage to extend battery lifespan,
|
||||
where the hardware exposes a charge threshold
|
||||
(/sys/class/power_supply/BAT*/charge_control_end_threshold).
|
||||
where the hardware exposes charge_control_end_threshold on a
|
||||
system battery (type=Battery under /sys/class/power_supply;
|
||||
name-agnostic — BAT0, CMB0, …).
|
||||
null leaves charging at the firmware default. Backend-independent
|
||||
(a small systemd unit writes the sysfs knob at boot), so it
|
||||
works under PPD too; needs nomarchy.system.power.laptop.
|
||||
(a small systemd unit writes the sysfs knob, re-applied on AC
|
||||
state changes), so it works under PPD too; needs
|
||||
nomarchy.system.power.laptop.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
@@ -12,17 +12,18 @@ let
|
||||
|
||||
state =
|
||||
if cfg.stateFile != null
|
||||
then builtins.fromJSON (builtins.readFile cfg.stateFile)
|
||||
then import ../theme-state-read.nix { inherit lib; } cfg.stateFile
|
||||
else { };
|
||||
colorOf = key: fallback: lib.removePrefix "#" ((state.colors or { }).${key} or fallback);
|
||||
base = colorOf "base" "#1a1b26";
|
||||
# Fallbacks match the distro default theme (Boreal) when stateFile is null.
|
||||
base = colorOf "base" "#21272F";
|
||||
# Splash elements are recolored from the palette so they read on any
|
||||
# base, light or dark: foreground glyphs → text, field/track boxes →
|
||||
# surface (raised from base in both polarities), the progress fill →
|
||||
# accent. The shipped art is a fixed navy that vanished on dark themes.
|
||||
text = colorOf "text" "#a9b1d6";
|
||||
surface = colorOf "surface" "#32344a";
|
||||
accent = colorOf "accent" "#7aa2f7";
|
||||
text = colorOf "text" "#D3DAE0";
|
||||
surface = colorOf "surface" "#303A46";
|
||||
accent = colorOf "accent" "#B79BE8";
|
||||
magick = lib.getExe' pkgs.imagemagick "magick";
|
||||
|
||||
# Plymouth's Window.SetBackgroundTopColor takes three floats in
|
||||
|
||||
@@ -38,10 +38,10 @@ in
|
||||
|
||||
# Battery charge limit via sysfs. PPD can't cap charge at all, and
|
||||
# TLP's own knob only applies under TLP — so a tiny oneshot writes
|
||||
# the threshold directly, independent of the backend. Boot-time only:
|
||||
# some firmwares reset the threshold on unplug; revisit with a udev
|
||||
# hook if that bites. `-` paths that don't exist are skipped, so this
|
||||
# is a clean no-op on hardware without the control.
|
||||
# the threshold directly, independent of the backend. Re-applied on
|
||||
# AC state changes by the udev rule below (some firmwares reset the
|
||||
# threshold when the charger is unplugged). `[ -w ]` skips paths that
|
||||
# don't exist, so this is a clean no-op on hardware without the control.
|
||||
systemd.services.nomarchy-battery-charge-limit = lib.mkIf chargeLimit {
|
||||
description = "Cap battery charging at ${toString cfg.batteryChargeLimit}%";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
@@ -50,11 +50,27 @@ in
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
for thresh in /sys/class/power_supply/BAT*/charge_control_end_threshold; do
|
||||
# Name-agnostic (BAT0, CMB0, …): same type/scope filter as
|
||||
# nomarchy-battery-notify — BACKLOG #60.
|
||||
for d in /sys/class/power_supply/*/; do
|
||||
[ "$(cat "$d/type" 2>/dev/null)" = Battery ] || continue
|
||||
[ "$(cat "$d/scope" 2>/dev/null || echo System)" = Device ] && continue
|
||||
thresh="$d/charge_control_end_threshold"
|
||||
[ -w "$thresh" ] && echo ${toString cfg.batteryChargeLimit} > "$thresh"
|
||||
done
|
||||
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 = {
|
||||
tailscale.enable = lib.mkEnableOption ''
|
||||
Tailscale, the mesh VPN — ships the daemon; authenticate once with
|
||||
`sudo tailscale up`'';
|
||||
Tailscale, the mesh VPN — ships the daemon and makes the login user its
|
||||
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 continuous file sync, running as the login user — add
|
||||
@@ -75,7 +76,8 @@ in
|
||||
|
||||
printing.enable = lib.mkEnableOption ''
|
||||
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'' // { default = config.nomarchy.settings.printing.enable or false; };
|
||||
|
||||
openrgb.enable = lib.mkEnableOption ''
|
||||
the OpenRGB daemon and GUI for controlling RGB lighting on peripherals
|
||||
@@ -124,6 +126,12 @@ in
|
||||
config = lib.mkMerge [
|
||||
(lib.mkIf cfg.tailscale.enable {
|
||||
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 {
|
||||
@@ -246,6 +254,9 @@ in
|
||||
nssmdns4 = 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 {
|
||||
|
||||
88
modules/nixos/timezone.nix
Normal file
88
modules/nixos/timezone.nix
Normal file
@@ -0,0 +1,88 @@
|
||||
# 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 stateFile is null;
|
||||
# a set-but-missing/invalid path fails closed via theme-state-read.nix.
|
||||
state =
|
||||
if cfg.stateFile != null
|
||||
then import ../theme-state-read.nix { inherit lib; } 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;
|
||||
})
|
||||
];
|
||||
}
|
||||
84
modules/theme-state-read.nix
Normal file
84
modules/theme-state-read.nix
Normal file
@@ -0,0 +1,84 @@
|
||||
# Pure theme-state.json loader — fail closed with a short, actionable
|
||||
# message instead of a raw `readFile` / `fromJSON` stack buried in a
|
||||
# consumer. Used by modules/home/theme.nix (required path), the NixOS
|
||||
# stateFile consumers, and lib.mkFlake (early gate).
|
||||
#
|
||||
# Field-level schema checks stay in theme.nix (post-defaults). This file
|
||||
# only gates *existence* and *JSON-shape* so the first failure the user
|
||||
# sees points at the state file, not at nightlight.nix.
|
||||
{ lib }:
|
||||
|
||||
path:
|
||||
|
||||
let
|
||||
pathStr = toString path;
|
||||
|
||||
tip = ''
|
||||
Fix:
|
||||
• Missing file → copy the template next to your flake.nix:
|
||||
templates/downstream/theme-state.json
|
||||
or regenerate from a preset:
|
||||
nomarchy-theme-sync apply boreal
|
||||
• Bad syntax / wrong shape → edit theme-state.json (trailing commas
|
||||
are the usual culprit) or reset with `apply` as above.
|
||||
• Field-level schema (colors, ui, border, …):
|
||||
nomarchy-theme-sync validate
|
||||
Eval-time field checks live in modules/home/theme.nix.'';
|
||||
|
||||
missingMsg = ''
|
||||
|
||||
Nomarchy: theme-state.json is missing:
|
||||
${pathStr}
|
||||
|
||||
This file is required (appearance + menu settings). Add it to your
|
||||
flake checkout so evaluation stays pure.
|
||||
${tip}'';
|
||||
|
||||
emptyMsg = ''
|
||||
|
||||
Nomarchy: theme-state.json is empty:
|
||||
${pathStr}
|
||||
${tip}'';
|
||||
|
||||
notObjectMsg = ''
|
||||
|
||||
Nomarchy: theme-state.json must be a JSON object `{ ... }`:
|
||||
${pathStr}
|
||||
${tip}'';
|
||||
|
||||
# lib.trim / lib.strings.trim — strip leading/trailing whitespace so an
|
||||
# all-whitespace file counts as empty and a BOM-less `{` is recognized.
|
||||
strip = s:
|
||||
let
|
||||
# Prefer lib.trim when present (nixpkgs ≥ 23.11); fall back so a
|
||||
# very old pin still gates missing/non-object.
|
||||
trim =
|
||||
if lib ? trim then lib.trim
|
||||
else if lib.strings ? trim then lib.strings.trim
|
||||
else (x: x);
|
||||
in trim s;
|
||||
|
||||
in
|
||||
if !(builtins.pathExists path) then
|
||||
throw missingMsg
|
||||
else
|
||||
let
|
||||
raw = builtins.readFile path;
|
||||
stripped = strip raw;
|
||||
in
|
||||
if stripped == "" then
|
||||
throw emptyMsg
|
||||
# Reject non-objects before fromJSON where we can (null / array / string
|
||||
# literals). Subtle syntax errors still surface from fromJSON itself —
|
||||
# those messages already include line/column; the path tip above is the
|
||||
# part a raw stack used to bury.
|
||||
else if builtins.match "[[:space:]]*\\{.*" stripped == null then
|
||||
throw notObjectMsg
|
||||
else
|
||||
let
|
||||
value = builtins.fromJSON raw;
|
||||
in
|
||||
if !(builtins.isAttrs value) then
|
||||
throw notObjectMsg
|
||||
else
|
||||
value
|
||||
12
pkgs/nomarchy-battery-notify/default.nix
Normal file
12
pkgs/nomarchy-battery-notify/default.nix
Normal file
@@ -0,0 +1,12 @@
|
||||
# Low-battery notification watcher (nomarchy.batteryNotify). A package
|
||||
# (not an inline script) so the VM check can exercise the crossing logic
|
||||
# on a minimal node against a test_power fake battery — same reasoning
|
||||
# as nomarchy-doctor. libnotify is deliberately NOT a runtimeInput: the
|
||||
# user unit puts it on PATH; the check shims notify-send instead.
|
||||
{ writeShellApplication, coreutils }:
|
||||
|
||||
writeShellApplication {
|
||||
name = "nomarchy-battery-notify";
|
||||
runtimeInputs = [ coreutils ];
|
||||
text = builtins.readFile ./nomarchy-battery-notify.sh;
|
||||
}
|
||||
58
pkgs/nomarchy-battery-notify/nomarchy-battery-notify.sh
Normal file
58
pkgs/nomarchy-battery-notify/nomarchy-battery-notify.sh
Normal file
@@ -0,0 +1,58 @@
|
||||
# Low-battery notifications — the session-side complement to the bar's
|
||||
# battery colors: Waybar paints the module @warn at 25% and @bad at 10%
|
||||
# (waybar.nix battery.states) but nothing *notified*. This watcher polls
|
||||
# the same sysfs state the bar reads and fires exactly one notification
|
||||
# per downward crossing — normal at 25%, critical at 10% (swaync keeps
|
||||
# critical toasts up until dismissed) — re-arming once the charger lands.
|
||||
#
|
||||
# notify-send is resolved from PATH on purpose (not a runtimeInput): the
|
||||
# unit (battery-notify.nix) provides libnotify; the VM check shims it to
|
||||
# capture the calls.
|
||||
#
|
||||
# usage: nomarchy-battery-notify [poll-interval-seconds]
|
||||
|
||||
interval="${1:-30}"
|
||||
warn=25
|
||||
crit=10
|
||||
|
||||
# System batteries only: type Battery, and not scope=Device (how
|
||||
# peripheral batteries — mice, headsets — report). Name-agnostic on
|
||||
# purpose: BAT0, dual-battery BAT0+BAT1, CMB0, test_battery all match.
|
||||
batteries() {
|
||||
local d
|
||||
for d in /sys/class/power_supply/*/; do
|
||||
[ "$(cat "$d/type" 2>/dev/null)" = Battery ] || continue
|
||||
[ "$(cat "$d/scope" 2>/dev/null || echo System)" = Device ] && continue
|
||||
printf '%s\n' "$d"
|
||||
done
|
||||
}
|
||||
|
||||
# Self-gate: no battery, not a laptop — a clean no-op on desktops.
|
||||
[ -n "$(batteries)" ] || exit 0
|
||||
|
||||
state=ok # ok → warn → crit, one notification per downward crossing
|
||||
while :; do
|
||||
sum=0 n=0 discharging=
|
||||
while IFS= read -r d; do
|
||||
cap=$(cat "$d/capacity" 2>/dev/null) || continue
|
||||
sum=$((sum + cap)); n=$((n + 1))
|
||||
[ "$(cat "$d/status" 2>/dev/null)" = Discharging ] && discharging=1
|
||||
done < <(batteries)
|
||||
|
||||
if [ "$n" -gt 0 ] && [ -n "$discharging" ]; then
|
||||
cap=$((sum / n))
|
||||
if [ "$cap" -le "$crit" ] && [ "$state" != crit ]; then
|
||||
notify-send -u critical -a Nomarchy "Battery critical: ${cap}%" \
|
||||
"Plug in now."
|
||||
state=crit
|
||||
elif [ "$cap" -le "$warn" ] && [ "$state" = ok ]; then
|
||||
notify-send -u normal -a Nomarchy "Battery low: ${cap}%" \
|
||||
"Consider plugging in."
|
||||
state=warn
|
||||
fi
|
||||
else
|
||||
# On the charger (or nothing readable): re-arm for the next drain.
|
||||
state=ok
|
||||
fi
|
||||
sleep "$interval"
|
||||
done
|
||||
7
pkgs/nomarchy-control-center/default.nix
Normal file
7
pkgs/nomarchy-control-center/default.nix
Normal file
@@ -0,0 +1,7 @@
|
||||
{ writeShellApplication, coreutils, gawk, jq, gum }:
|
||||
|
||||
writeShellApplication {
|
||||
name = "nomarchy-control-center";
|
||||
runtimeInputs = [ coreutils gawk jq gum ];
|
||||
text = builtins.readFile ./nomarchy-control-center.sh;
|
||||
}
|
||||
249
pkgs/nomarchy-control-center/nomarchy-control-center.sh
Normal file
249
pkgs/nomarchy-control-center/nomarchy-control-center.sh
Normal file
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Nomarchy Control Center — TUI frontend over nomarchy-theme-sync and tools
|
||||
# Uses charmbracelet/gum for rendering.
|
||||
|
||||
trap 'exit 0' SIGINT
|
||||
|
||||
function get_state() {
|
||||
nomarchy-theme-sync get "$1" 2>/dev/null || echo ""
|
||||
}
|
||||
|
||||
function set_state() {
|
||||
nomarchy-theme-sync --quiet set "$1" "$2" --no-switch
|
||||
}
|
||||
|
||||
function first_boot() {
|
||||
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "Welcome to Nomarchy!"
|
||||
|
||||
echo "Pick a Theme Preset:"
|
||||
themes=$(nomarchy-theme-sync list)
|
||||
chosen_theme=$(printf "%s" "$themes" | gum choose)
|
||||
if [ -n "$chosen_theme" ]; then
|
||||
echo "Applying theme $chosen_theme..."
|
||||
nomarchy-theme-sync apply "$chosen_theme"
|
||||
fi
|
||||
|
||||
if gum confirm "Enable auto-commit for settings?"; then
|
||||
set_state "settings.autoCommit" "true"
|
||||
else
|
||||
set_state "settings.autoCommit" "false"
|
||||
fi
|
||||
|
||||
if command -v nomarchy-autotimezone >/dev/null 2>&1; then
|
||||
if gum confirm "Enable automatic timezone detection?"; then
|
||||
set_state "settings.autoTimezone" "true"
|
||||
else
|
||||
set_state "settings.autoTimezone" "false"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Post-install hardware/day-2 tips (#43 Firmware; #73 fingerprint +
|
||||
# doctor). Self-gated on the same CLI presence the System menu uses;
|
||||
# first-boot is once, so no dismiss state file is needed.
|
||||
if command -v fwupdmgr >/dev/null 2>&1; then
|
||||
gum style --foreground 245 \
|
||||
"Tip: SUPER+M → System › Firmware checks LVFS updates (never auto-flashes)."
|
||||
fi
|
||||
if command -v fprintd-list >/dev/null 2>&1; then
|
||||
gum style --foreground 245 \
|
||||
"Tip: SUPER+M → System › Fingerprint to enroll a finger (or fprintd-enroll)."
|
||||
fi
|
||||
if command -v nomarchy-doctor >/dev/null 2>&1; then
|
||||
gum style --foreground 245 \
|
||||
"Tip: run nomarchy-doctor (or SUPER+M → System › Doctor) for a read-only health check."
|
||||
fi
|
||||
|
||||
gum style --foreground 212 "First boot configuration complete!"
|
||||
echo "You can always change these later in the Control Center."
|
||||
read -r -n 1 -s -p "Press any key to exit..."
|
||||
echo
|
||||
}
|
||||
|
||||
function main_menu() {
|
||||
while true; do
|
||||
choice=$(gum choose "Appearance" "System Toggles" "Health (Doctor)" "Rollback" "Exit")
|
||||
case "$choice" in
|
||||
"Appearance") appearance_menu ;;
|
||||
"System Toggles") toggles_menu ;;
|
||||
"Health (Doctor)")
|
||||
if command -v nomarchy-doctor >/dev/null 2>&1; then
|
||||
nomarchy-doctor || true
|
||||
else
|
||||
echo "nomarchy-doctor not found."
|
||||
fi
|
||||
echo
|
||||
read -r -n 1 -s -p "Press any key to return..."
|
||||
echo
|
||||
;;
|
||||
"Rollback")
|
||||
gens=$(home-manager generations 2>/dev/null | head -10)
|
||||
if [ -z "$gens" ]; then
|
||||
echo "No Home Manager generations found."
|
||||
else
|
||||
tmp=$(mktemp)
|
||||
printf "%s\n" "$gens" | awk '{ printf "Desktop gen %s — %s %s%s\n", $5, $1, $2, (NR==1 ? " (current)" : "") }' > "$tmp"
|
||||
sel=$(gum choose < "$tmp" || true)
|
||||
rm -f "$tmp"
|
||||
if [ -n "$sel" ]; then
|
||||
num=${sel#Desktop gen }
|
||||
num=${num%% *}
|
||||
path=$(printf "%s\n" "$gens" | awk -v n="$num" '$5 == n { print $7 }')
|
||||
if [ -x "$path/activate" ]; then
|
||||
echo "Activating Home Manager generation $num..."
|
||||
"$path/activate"
|
||||
echo "Desktop rolled back to generation $num."
|
||||
else
|
||||
echo "Generation $num not found."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo
|
||||
read -r -n 1 -s -p "Press any key to return..."
|
||||
echo
|
||||
;;
|
||||
"Exit"|*) break ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
function appearance_menu() {
|
||||
while true; do
|
||||
choice=$(gum choose "Pick Theme" "Toggle Blur" "Set Gaps" "Back")
|
||||
case "$choice" in
|
||||
"Pick Theme")
|
||||
themes=$(nomarchy-theme-sync list)
|
||||
chosen=$(printf "%s" "$themes" | gum choose)
|
||||
if [ -n "$chosen" ]; then
|
||||
nomarchy-theme-sync apply "$chosen"
|
||||
fi
|
||||
;;
|
||||
"Toggle Blur")
|
||||
blur=$(get_state "ui.blur")
|
||||
if [ "$blur" = "true" ]; then
|
||||
set_state "ui.blur" "false"
|
||||
else
|
||||
set_state "ui.blur" "true"
|
||||
fi
|
||||
echo "Blur setting saved (requires rebuild)."
|
||||
sleep 1
|
||||
;;
|
||||
"Set Gaps")
|
||||
gaps=$(gum input --prompt "Enter gaps in pixels: " --placeholder "$(get_state ui.gapsOut)")
|
||||
if [ -n "$gaps" ]; then
|
||||
set_state "ui.gapsOut" "$gaps"
|
||||
echo "Gaps set to $gaps (requires rebuild)."
|
||||
sleep 1
|
||||
fi
|
||||
;;
|
||||
"Back"|*) break ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
function toggles_menu() {
|
||||
while true; do
|
||||
choice=$(gum choose "Auto-commit" "Auto-timezone" "Night Light" "Updates" "Bluetooth" "Printing" "Terminal" "Keyboard Layout" "Auto-Login" "Back")
|
||||
case "$choice" in
|
||||
"Auto-commit")
|
||||
cur=$(get_state "settings.autoCommit")
|
||||
if [ "$cur" = "true" ]; then
|
||||
set_state "settings.autoCommit" "false"
|
||||
echo "Auto-commit disabled."
|
||||
else
|
||||
set_state "settings.autoCommit" "true"
|
||||
echo "Auto-commit enabled."
|
||||
fi
|
||||
sleep 1
|
||||
;;
|
||||
"Auto-timezone")
|
||||
cur=$(get_state "settings.autoTimezone")
|
||||
if [ "$cur" = "true" ]; then
|
||||
set_state "settings.autoTimezone" "false"
|
||||
echo "Auto-timezone disabled."
|
||||
else
|
||||
set_state "settings.autoTimezone" "true"
|
||||
echo "Auto-timezone enabled."
|
||||
fi
|
||||
sleep 1
|
||||
;;
|
||||
"Night Light")
|
||||
if command -v nomarchy-nightlight >/dev/null 2>&1; then
|
||||
nomarchy-nightlight toggle
|
||||
sleep 1
|
||||
else
|
||||
echo "nomarchy-nightlight not found."
|
||||
sleep 1
|
||||
fi
|
||||
;;
|
||||
"Updates")
|
||||
cur=$(get_state "settings.updates.enable")
|
||||
if [ "$cur" = "true" ]; then
|
||||
set_state "settings.updates.enable" "false"
|
||||
echo "Updates checker disabled (requires rebuild)."
|
||||
else
|
||||
set_state "settings.updates.enable" "true"
|
||||
echo "Updates checker enabled (requires rebuild)."
|
||||
fi
|
||||
sleep 1
|
||||
;;
|
||||
"Bluetooth")
|
||||
cur=$(get_state "settings.bluetooth.enable")
|
||||
if [ "$cur" = "true" ]; then
|
||||
set_state "settings.bluetooth.enable" "false"
|
||||
echo "Bluetooth disabled (requires rebuild)."
|
||||
else
|
||||
set_state "settings.bluetooth.enable" "true"
|
||||
echo "Bluetooth enabled (requires rebuild)."
|
||||
fi
|
||||
sleep 1
|
||||
;;
|
||||
"Printing")
|
||||
cur=$(get_state "settings.printing.enable")
|
||||
if [ "$cur" = "true" ]; then
|
||||
set_state "settings.printing.enable" "false"
|
||||
echo "Printing services disabled (requires rebuild)."
|
||||
else
|
||||
set_state "settings.printing.enable" "true"
|
||||
echo "Printing services enabled (requires rebuild)."
|
||||
fi
|
||||
sleep 1
|
||||
;;
|
||||
"Terminal")
|
||||
term=$(gum choose "ghostty" "kitty" "alacritty" "wezterm" "foot")
|
||||
if [ -n "$term" ]; then
|
||||
set_state "settings.terminal" "\"$term\""
|
||||
echo "Terminal set to $term (requires rebuild)."
|
||||
fi
|
||||
sleep 1
|
||||
;;
|
||||
"Keyboard Layout")
|
||||
layout=$(gum input --prompt "Enter keyboard layout (e.g. us, de, es): " --placeholder "$(get_state settings.keyboard.layout | tr -d '"')")
|
||||
if [ -n "$layout" ]; then
|
||||
set_state "settings.keyboard.layout" "\"$layout\""
|
||||
echo "Keyboard layout set to $layout (requires rebuild)."
|
||||
fi
|
||||
sleep 1
|
||||
;;
|
||||
"Auto-Login")
|
||||
user=$(gum input --prompt "Enter auto-login username (or empty to disable): " --placeholder "$(get_state settings.greeter.autoLogin | tr -d '"')")
|
||||
if [ -n "$user" ]; then
|
||||
set_state "settings.greeter.autoLogin" "\"$user\""
|
||||
echo "Auto-login set to $user (requires rebuild)."
|
||||
else
|
||||
set_state "settings.greeter.autoLogin" "null"
|
||||
echo "Auto-login disabled (requires rebuild)."
|
||||
fi
|
||||
sleep 1
|
||||
;;
|
||||
"Back"|*) break ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
if [ "${1:-}" = "--first-boot" ]; then
|
||||
first_boot
|
||||
else
|
||||
main_menu
|
||||
fi
|
||||
43
pkgs/nomarchy-detect-hw/default.nix
Normal file
43
pkgs/nomarchy-detect-hw/default.nix
Normal file
@@ -0,0 +1,43 @@
|
||||
# Post-install hardware re-probe (BACKLOG #58 / HARDWARE.md §8).
|
||||
# Same pure nomarchy_detect_hw protocol as the installer; prints suggested
|
||||
# hardwareProfile + system.nix snippets. Never rewrites the flake.
|
||||
{ lib
|
||||
, stdenvNoCC
|
||||
, makeWrapper
|
||||
, bash
|
||||
, coreutils
|
||||
, pciutils
|
||||
, usbutils
|
||||
, util-linux
|
||||
}:
|
||||
|
||||
stdenvNoCC.mkDerivation {
|
||||
pname = "nomarchy-detect-hw";
|
||||
version = "0.1.0";
|
||||
|
||||
src = ./.;
|
||||
|
||||
nativeBuildInputs = [ makeWrapper ];
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
share=$out/share/nomarchy-detect-hw
|
||||
install -Dm644 ${../nomarchy-install/hardware-db.sh} "$share/hardware-db.sh"
|
||||
install -Dm755 nomarchy-detect-hw.sh $out/bin/nomarchy-detect-hw
|
||||
patchShebangs $out/bin/nomarchy-detect-hw
|
||||
|
||||
wrapProgram $out/bin/nomarchy-detect-hw \
|
||||
--prefix PATH : ${lib.makeBinPath [ bash coreutils pciutils usbutils util-linux ]} \
|
||||
--set NOMARCHY_DETECT_HW_SHARE "$share"
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Probe hardware and print Nomarchy hardwareProfile / system.nix suggestions";
|
||||
license = lib.licenses.mit;
|
||||
mainProgram = "nomarchy-detect-hw";
|
||||
platforms = lib.platforms.linux;
|
||||
};
|
||||
}
|
||||
134
pkgs/nomarchy-detect-hw/nomarchy-detect-hw.sh
Normal file
134
pkgs/nomarchy-detect-hw/nomarchy-detect-hw.sh
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
# nomarchy-detect-hw — post-install re-probe (BACKLOG #58 / HARDWARE.md §8).
|
||||
#
|
||||
# Runs the same pure nomarchy_detect_hw protocol as the installer and
|
||||
# prints suggested hardwareProfile + system.nix snippets. Never rewrites
|
||||
# the flake (no --apply).
|
||||
#
|
||||
# Usage:
|
||||
# nomarchy-detect-hw # human-readable report + snippets
|
||||
# nomarchy-detect-hw --raw # MODULE / NOMARCHY / DETAIL lines only
|
||||
set -euo pipefail
|
||||
|
||||
SHARE="${NOMARCHY_DETECT_HW_SHARE:?nomarchy-detect-hw: not run via the packaged wrapper}"
|
||||
# shellcheck source=/dev/null
|
||||
source "$SHARE/hardware-db.sh"
|
||||
|
||||
raw=false
|
||||
case "${1:-}" in
|
||||
--raw|-r) raw=true ;;
|
||||
-h|--help)
|
||||
cat <<'EOF'
|
||||
Usage: nomarchy-detect-hw [--raw]
|
||||
|
||||
Probe this machine the same way the live installer does and print
|
||||
suggested flake hardwareProfile and system.nix hardware lines.
|
||||
|
||||
--raw emit only MODULE / NOMARCHY / NOMARCHY-NPU / DETAIL lines
|
||||
(installer protocol). Default is a human report + snippets.
|
||||
|
||||
Does not modify any files. Paste the snippets into ~/.nomarchy after review.
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
"") ;;
|
||||
*)
|
||||
echo "nomarchy-detect-hw: unknown option: $1 (try --help)" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if $raw; then
|
||||
nomarchy_detect_hw
|
||||
exit 0
|
||||
fi
|
||||
|
||||
profiles=()
|
||||
nomarchy_lines=()
|
||||
npu=""
|
||||
details=()
|
||||
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
MODULE\ *) profiles+=("${line#MODULE }") ;;
|
||||
NOMARCHY\ *) nomarchy_lines+=("${line#NOMARCHY }") ;;
|
||||
NOMARCHY-NPU\ *) npu="${line#NOMARCHY-NPU }" ;;
|
||||
DETAIL\ *) details+=("${line#DETAIL }") ;;
|
||||
esac
|
||||
done < <(nomarchy_detect_hw)
|
||||
|
||||
echo "nomarchy-detect-hw — suggestions for this machine"
|
||||
echo "(read-only; paste into ~/.nomarchy after review)"
|
||||
echo
|
||||
|
||||
if ((${#details[@]})); then
|
||||
echo "## Detected"
|
||||
for d in "${details[@]}"; do
|
||||
printf ' · %s\n' "$d"
|
||||
done
|
||||
echo
|
||||
fi
|
||||
|
||||
echo "## flake.nix — hardwareProfile"
|
||||
if ((${#profiles[@]})); then
|
||||
echo " hardwareProfile = ["
|
||||
for p in "${profiles[@]}"; do
|
||||
printf ' "%s"\n' "$p"
|
||||
done
|
||||
echo " ];"
|
||||
else
|
||||
echo " # (no modules detected — leave hardwareProfile unset or null)"
|
||||
fi
|
||||
echo
|
||||
|
||||
echo "## system.nix — nomarchy.hardware / power (safe defaults active;"
|
||||
echo "## heavier opt-ins stay commented, same as the installer)"
|
||||
if ((${#profiles[@]})) && printf '%s\n' "${profiles[@]}" | grep -qx 'common-pc-laptop'; then
|
||||
echo " nomarchy.system.power.laptop = true;"
|
||||
echo " # nomarchy.system.power.batteryChargeLimit = 80;"
|
||||
fi
|
||||
for nm in "${nomarchy_lines[@]:-}"; do
|
||||
case "$nm" in
|
||||
hardware.intel.enable=true)
|
||||
echo " nomarchy.hardware.intel.enable = true; # GuC/HuC (i915)"
|
||||
echo " # nomarchy.hardware.intel.computeRuntime = true; # OpenCL/oneVPL (opt-in)"
|
||||
;;
|
||||
hardware.intel.guc=false)
|
||||
echo " nomarchy.hardware.intel.guc = false; # xe driver → GuC default-on"
|
||||
;;
|
||||
hardware.amd.enable=true)
|
||||
echo " nomarchy.hardware.amd.enable = true; # amd-pstate + VA-API"
|
||||
echo " # nomarchy.hardware.amd.rocm.enable = true; # ROCm (multi-GB, opt-in)"
|
||||
echo ' # nomarchy.hardware.amd.rocm.gfxOverride = ""; # e.g. "11.0.0" for unlisted iGPU'
|
||||
;;
|
||||
hardware.fingerprint.enable=true)
|
||||
echo " nomarchy.hardware.fingerprint.enable = true; # fprintd (enroll: fprintd-enroll)"
|
||||
echo " # nomarchy.hardware.fingerprint.pam = true; # login + sudo (opt-in)"
|
||||
;;
|
||||
hardware.camera.hideIrSensor=true)
|
||||
echo " nomarchy.hardware.camera.hideIrSensor = true; # dual-sensor: hide IR node"
|
||||
;;
|
||||
*)
|
||||
echo " # (unmapped NOMARCHY line: $nm)"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [[ -n "$npu" ]]; then
|
||||
echo " # nomarchy.hardware.npu.enable = true; # $npu NPU (experimental; userspace BYO)"
|
||||
echo " # nomarchy.hardware.latestKernel = true; # if the NPU driver needs a newer kernel"
|
||||
fi
|
||||
if printf '%s\n' "${profiles[@]:-}" | grep -qx 'common-gpu-nvidia'; then
|
||||
echo " # NVIDIA: common-gpu-nvidia is in hardwareProfile (flake.nix)."
|
||||
echo " # Hybrid/PRIME, power, open-module — plain NixOS; see docs/HARDWARE.md §6"
|
||||
echo " # hardware.nvidia.prime = { ... };"
|
||||
echo " # hardware.nvidia.powerManagement.enable = true;"
|
||||
echo " # hardware.nvidia.open = false; # or true on newer cards"
|
||||
fi
|
||||
if ((${#nomarchy_lines[@]} == 0)) && [[ -z "$npu" ]] \
|
||||
&& ! printf '%s\n' "${profiles[@]:-}" | grep -qx 'common-gpu-nvidia' \
|
||||
&& ! printf '%s\n' "${profiles[@]:-}" | grep -qx 'common-pc-laptop'; then
|
||||
echo " # (no nomarchy.hardware lines suggested)"
|
||||
fi
|
||||
echo
|
||||
echo "Apply with: sudo nixos-rebuild switch --flake \${NOMARCHY_PATH:-\$HOME/.nomarchy}#default"
|
||||
echo "Docs: docs/HARDWARE.md §8"
|
||||
10
pkgs/nomarchy-doctor/default.nix
Normal file
10
pkgs/nomarchy-doctor/default.nix
Normal file
@@ -0,0 +1,10 @@
|
||||
# One-shot read-only health check (System › Doctor / `nomarchy-doctor`).
|
||||
# A package (not an inline script) so the VM check can exercise it on a
|
||||
# minimal node without pulling in the whole distro module.
|
||||
{ writeShellApplication, coreutils, gawk, git, gnugrep, jq, systemd }:
|
||||
|
||||
writeShellApplication {
|
||||
name = "nomarchy-doctor";
|
||||
runtimeInputs = [ coreutils gawk git gnugrep jq systemd ];
|
||||
text = builtins.readFile ./nomarchy-doctor.sh;
|
||||
}
|
||||
264
pkgs/nomarchy-doctor/nomarchy-doctor.sh
Normal file
264
pkgs/nomarchy-doctor/nomarchy-doctor.sh
Normal file
@@ -0,0 +1,264 @@
|
||||
# nomarchy-doctor — one-shot, READ-ONLY health check: the things that
|
||||
# actually break user machines, as a pass/fail sheet where every
|
||||
# failure prints the one command that fixes it. It never changes
|
||||
# anything itself. Exit 0 = no failures (warnings allowed), 1 = at
|
||||
# least one ✖.
|
||||
|
||||
flake="${NOMARCHY_PATH:-$HOME/.nomarchy}"
|
||||
fails=0
|
||||
warns=0
|
||||
|
||||
grn=$'\033[32m'; red=$'\033[31m'; ylw=$'\033[33m'; dim=$'\033[2m'; rst=$'\033[0m'
|
||||
ok() { printf ' %b✔%b %s\n' "$grn" "$rst" "$1"; }
|
||||
bad() { printf ' %b✖%b %s\n' "$red" "$rst" "$1"
|
||||
printf ' %bfix: %s%b\n' "$dim" "$2" "$rst"
|
||||
fails=$((fails + 1)); }
|
||||
warn() { printf ' %b●%b %s\n' "$ylw" "$rst" "$1"
|
||||
if [ $# -gt 1 ]; then printf ' %b%s%b\n' "$dim" "$2" "$rst"; fi
|
||||
warns=$((warns + 1)); }
|
||||
skip() { printf ' %b– %s%b\n' "$dim" "$1" "$rst"; }
|
||||
|
||||
printf 'nomarchy-doctor — %s\n\n' "$(date '+%Y-%m-%d %H:%M')"
|
||||
|
||||
# ── systemd units ────────────────────────────────────────────────────
|
||||
mapfile -t sysfailed < <(systemctl --failed --no-legend --plain 2>/dev/null | awk '{print $1}')
|
||||
if [ "${#sysfailed[@]}" -eq 0 ]; then
|
||||
ok "no failed system units"
|
||||
else
|
||||
bad "failed system unit(s): ${sysfailed[*]}" \
|
||||
"journalctl -b -u <unit> (read why; sys-rebuild after fixing)"
|
||||
fi
|
||||
|
||||
# A user bus only exists inside a session (not over bare SSH/root).
|
||||
if [ -n "$(systemctl --user is-system-running 2>/dev/null || true)" ]; then
|
||||
mapfile -t usrfailed < <(systemctl --user --failed --no-legend --plain 2>/dev/null | awk '{print $1}')
|
||||
if [ "${#usrfailed[@]}" -eq 0 ]; then
|
||||
ok "no failed user units"
|
||||
else
|
||||
bad "failed user unit(s): ${usrfailed[*]}" \
|
||||
"journalctl --user -b -u <unit>"
|
||||
fi
|
||||
else
|
||||
skip "user units (no user session bus here)"
|
||||
fi
|
||||
|
||||
# ── disk space ───────────────────────────────────────────────────────
|
||||
# Only real on-disk filesystems; skips the tmpfs/9p/overlay mounts of
|
||||
# live systems and test VMs (where usage numbers mean nothing).
|
||||
seen=""
|
||||
for mp in / /boot /nix; do
|
||||
[ -d "$mp" ] || continue
|
||||
line=$(df -PT "$mp" 2>/dev/null | awk 'NR==2 {gsub("%","",$6); print $1, $2, $6, $5}')
|
||||
[ -n "$line" ] || continue
|
||||
read -r dev fstype use availkb <<<"$line"
|
||||
case "$fstype" in
|
||||
ext2|ext3|ext4|btrfs|xfs|vfat|f2fs|zfs) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
case " $seen " in *" $dev "*) continue ;; esac
|
||||
seen="$seen $dev"
|
||||
avail=$(numfmt --to=iec $((availkb * 1024)))
|
||||
if [ "$use" -ge 90 ]; then
|
||||
bad "$mp is ${use}% full (${avail} free)" \
|
||||
"sudo nix-collect-garbage --delete-older-than 14d (then sys-rebuild to prune old boot entries)"
|
||||
else
|
||||
ok "$mp has space (${use}% used, ${avail} free)"
|
||||
fi
|
||||
done
|
||||
|
||||
# ── the flake + state file ───────────────────────────────────────────
|
||||
if [ -f "$flake/theme-state.json" ]; then
|
||||
if jq empty "$flake/theme-state.json" 2>/dev/null; then
|
||||
ok "theme-state.json parses"
|
||||
else
|
||||
bad "theme-state.json is not valid JSON (rebuilds will fail)" \
|
||||
"nomarchy-theme-sync validate (names the spot; fix it, or re-apply a theme)"
|
||||
fi
|
||||
if git -C "$flake" ls-files --error-unmatch theme-state.json >/dev/null 2>&1; then
|
||||
ok "theme-state.json is git-tracked"
|
||||
else
|
||||
bad "theme-state.json is NOT git-tracked (flake evaluation can't see it)" \
|
||||
"git -C $flake add theme-state.json"
|
||||
fi
|
||||
else
|
||||
skip "theme-state.json (no flake checkout at $flake)"
|
||||
fi
|
||||
|
||||
if [ -d "$flake/.git" ]; then
|
||||
dirty=$(git -C "$flake" status --porcelain 2>/dev/null | wc -l)
|
||||
if [ "$dirty" -gt 0 ]; then
|
||||
warn "$dirty uncommitted change(s) in $flake (normal — theme writes land there)" \
|
||||
"commit when happy: git -C $flake add -A && git -C $flake commit -m settings"
|
||||
else
|
||||
ok "flake checkout is clean"
|
||||
fi
|
||||
behind=$(git -C "$flake" rev-list --count 'HEAD..@{u}' 2>/dev/null || echo 0)
|
||||
if [ "$behind" -gt 0 ]; then
|
||||
warn "flake is $behind commit(s) behind its upstream" "git -C $flake pull, then sys-rebuild + home-update"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── generation age ───────────────────────────────────────────────────
|
||||
# The profile SYMLINK's own mtime is the generation's creation time
|
||||
# (store paths themselves are all epoch-1).
|
||||
link=$(readlink /nix/var/nix/profiles/system 2>/dev/null || true)
|
||||
if [ -n "$link" ] && [ -e "/nix/var/nix/profiles/$link" ]; then
|
||||
ts=$(stat -c %Y "/nix/var/nix/profiles/$link" 2>/dev/null || echo 0)
|
||||
days=$(( ($(date +%s) - ts) / 86400 ))
|
||||
if [ "$days" -gt 30 ]; then
|
||||
warn "last system rebuild was $days days ago" \
|
||||
"sys-update when convenient (security fixes arrive with input bumps)"
|
||||
else
|
||||
ok "system generation is $days day(s) old"
|
||||
fi
|
||||
else
|
||||
skip "system generation age (no system profile)"
|
||||
fi
|
||||
|
||||
# ── snapper timeline (when enabled) ──────────────────────────────────
|
||||
if [ -n "$(systemctl list-unit-files snapper-timeline.timer --no-legend --plain 2>/dev/null)" ]; then
|
||||
if systemctl is-active --quiet snapper-timeline.timer; then
|
||||
ok "snapper timeline snapshots are running"
|
||||
else
|
||||
bad "snapper is enabled but the timeline timer is not active" \
|
||||
"systemctl status snapper-timeline.timer (then journalctl -u snapper-timeline.service)"
|
||||
fi
|
||||
else
|
||||
skip "snapper (not enabled on this machine)"
|
||||
fi
|
||||
|
||||
# ══ hardware ═════════════════════════════════════════════════════════
|
||||
# Every check below self-gates: it skips cleanly when the tool, service,
|
||||
# or device isn't present, so the section shrinks to fit the machine and
|
||||
# never fails just because a feature is absent. Still read-only.
|
||||
|
||||
# ── network (NetworkManager) ─────────────────────────────────────────
|
||||
if command -v nmcli >/dev/null 2>&1; then
|
||||
nmstate=$(nmcli -t -f STATE general status 2>/dev/null || true)
|
||||
case "$nmstate" in
|
||||
connected*) ok "NetworkManager: $nmstate" ;;
|
||||
"") skip "NetworkManager (no state reported)" ;;
|
||||
*) warn "NetworkManager state: $nmstate" \
|
||||
"connect in System › Network (or: nmcli device)" ;;
|
||||
esac
|
||||
else
|
||||
skip "NetworkManager (nmcli not present)"
|
||||
fi
|
||||
|
||||
# ── default audio sink (PipeWire) ────────────────────────────────────
|
||||
# wpctl/pactl talk to the user's PipeWire session — only meaningful with
|
||||
# a user bus (not over bare SSH/root).
|
||||
if [ -n "$(systemctl --user is-system-running 2>/dev/null || true)" ]; then
|
||||
if command -v wpctl >/dev/null 2>&1; then
|
||||
if wpctl inspect @DEFAULT_AUDIO_SINK@ >/dev/null 2>&1; then
|
||||
ok "default audio sink present"
|
||||
else
|
||||
warn "no default PipeWire sink" "pick one in System › Audio (or: wpctl status)"
|
||||
fi
|
||||
elif command -v pactl >/dev/null 2>&1; then
|
||||
defsink=$(pactl get-default-sink 2>/dev/null || true)
|
||||
if [ -n "$defsink" ] && [ "$defsink" != "@DEFAULT_SINK@" ]; then
|
||||
ok "default audio sink: $defsink"
|
||||
else
|
||||
warn "no default audio sink" "pick one in System › Audio"
|
||||
fi
|
||||
else
|
||||
skip "audio sink (no wpctl/pactl)"
|
||||
fi
|
||||
else
|
||||
skip "audio sink (no user session bus here)"
|
||||
fi
|
||||
|
||||
# ── GPU acceleration smoke (only if the probes are installed) ────────
|
||||
if command -v vainfo >/dev/null 2>&1; then
|
||||
if vainfo >/dev/null 2>&1; then
|
||||
ok "VA-API acceleration works (vainfo)"
|
||||
else
|
||||
warn "vainfo present but VA-API probe failed" \
|
||||
"check mkFlake.hardwareProfile / GPU drivers (or run: vainfo)"
|
||||
fi
|
||||
else
|
||||
skip "VA-API smoke (vainfo not installed)"
|
||||
fi
|
||||
if command -v glxinfo >/dev/null 2>&1; then
|
||||
if [ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]; then
|
||||
if glxinfo -B >/dev/null 2>&1; then
|
||||
ok "OpenGL renderer responds (glxinfo)"
|
||||
else
|
||||
warn "glxinfo present but GL probe failed" \
|
||||
"check mkFlake.hardwareProfile / GPU drivers (or run: glxinfo -B)"
|
||||
fi
|
||||
else
|
||||
skip "OpenGL smoke (no display attached)"
|
||||
fi
|
||||
else
|
||||
skip "OpenGL smoke (glxinfo not installed)"
|
||||
fi
|
||||
|
||||
# ── fingerprint (only when fprintd is enabled) ───────────────────────
|
||||
if [ -n "$(systemctl list-unit-files fprintd.service --no-legend --plain 2>/dev/null || true)" ]; then
|
||||
enrolled=""
|
||||
if command -v fprintd-list >/dev/null 2>&1 && [ -n "${USER:-}" ]; then
|
||||
enrolled=$(fprintd-list "$USER" 2>/dev/null || true)
|
||||
fi
|
||||
if [ -n "$enrolled" ] && ! printf '%s' "$enrolled" | grep -qi 'no fingers'; then
|
||||
ok "fprintd enabled, fingerprint(s) enrolled for ${USER:-user}"
|
||||
elif [ -n "$enrolled" ]; then
|
||||
warn "fprintd enabled but no fingerprints enrolled" "enroll one: fprintd-enroll"
|
||||
else
|
||||
ok "fprintd unit installed (fingerprint enabled)"
|
||||
fi
|
||||
else
|
||||
skip "fingerprint (fprintd not enabled)"
|
||||
fi
|
||||
|
||||
# ── firmware updates (fwupd) ─────────────────────────────────────────
|
||||
# fwupd is D-Bus-activated, so an idle daemon is normal, not a fault.
|
||||
# get-updates reads local metadata (no flashing); a hit is a soft warn.
|
||||
if command -v fwupdmgr >/dev/null 2>&1 \
|
||||
&& [ -n "$(systemctl list-unit-files fwupd.service --no-legend --plain 2>/dev/null || true)" ]; then
|
||||
if systemctl is-active --quiet fwupd.service; then
|
||||
ok "fwupd daemon active"
|
||||
else
|
||||
skip "fwupd daemon (idle — D-Bus-activated on demand)"
|
||||
fi
|
||||
if timeout 20 fwupdmgr get-updates >/dev/null 2>&1; then
|
||||
warn "firmware updates are available" "review, then apply: fwupdmgr update"
|
||||
else
|
||||
ok "firmware up to date (no pending updates)"
|
||||
fi
|
||||
else
|
||||
skip "fwupd (not enabled / fwupdmgr absent)"
|
||||
fi
|
||||
|
||||
# ── laptop battery charge threshold (only when a limit is set) ───────
|
||||
# Name-agnostic (BAT0, CMB0, …) — same type/scope filter as notify (#60).
|
||||
bat_seen=0; bat_limited=0
|
||||
for bat in /sys/class/power_supply/*/; do
|
||||
[ "$(cat "$bat/type" 2>/dev/null)" = Battery ] || continue
|
||||
[ "$(cat "$bat/scope" 2>/dev/null || echo System)" = Device ] && continue
|
||||
f="$bat/charge_control_end_threshold"
|
||||
[ -r "$f" ] || continue
|
||||
bat_seen=1
|
||||
lim=$(cat "$f" 2>/dev/null || true)
|
||||
case "$lim" in
|
||||
''|*[!0-9]*) continue ;;
|
||||
esac
|
||||
if [ "$lim" -lt 100 ]; then
|
||||
bat_limited=1
|
||||
ok "$(basename "$bat") charge limit active at ${lim}%"
|
||||
fi
|
||||
done
|
||||
if [ "$bat_seen" -eq 1 ] && [ "$bat_limited" -eq 0 ]; then
|
||||
skip "battery charge limit (none set — charges to 100%)"
|
||||
fi
|
||||
|
||||
# ── verdict ──────────────────────────────────────────────────────────
|
||||
echo
|
||||
if [ "$fails" -eq 0 ]; then
|
||||
printf '%b✔ healthy%b — %d warning(s)\n' "$grn" "$rst" "$warns"
|
||||
exit 0
|
||||
else
|
||||
printf '%b✖ %d problem(s)%b, %d warning(s) — each ✖ above shows its fix\n' "$red" "$fails" "$rst" "$warns"
|
||||
exit 1
|
||||
fi
|
||||
@@ -13,6 +13,7 @@
|
||||
, cryptsetup
|
||||
, lvm2 # dmsetup
|
||||
, pciutils # lspci
|
||||
, usbutils # lsusb (fingerprint-reader VID probe; sysfs is the fallback)
|
||||
, btrfs-progs # inspect-internal map-swapfile (hibernation offset)
|
||||
# Baked metadata — what this installer installs and where it came from.
|
||||
, templateDir # templates/downstream (home.nix, theme-state.json)
|
||||
@@ -50,14 +51,18 @@ stdenvNoCC.mkDerivation {
|
||||
# Empty flake registry: no network lookups for indirect refs.
|
||||
echo '{"flakes":[],"version":2}' > "$share/registry.json"
|
||||
mkdir -p "$share/template"
|
||||
cp ${templateDir}/home.nix ${templateDir}/theme-state.json "$share/template/"
|
||||
# Full downstream template is the SoT; install script copies + patches.
|
||||
cp ${templateDir}/flake.nix ${templateDir}/system.nix \
|
||||
${templateDir}/home.nix ${templateDir}/theme-state.json \
|
||||
"$share/template/"
|
||||
install -Dm644 patch-template.py "$share/patch-template.py"
|
||||
|
||||
# nixos-install / nixos-generate-config / nixos-enter / nix / systemd
|
||||
# tools come from the live system on purpose — they must match it.
|
||||
wrapProgram $out/bin/nomarchy-install \
|
||||
--prefix PATH : ${lib.makeBinPath [
|
||||
bash gum disko whois git python3
|
||||
util-linux gptfdisk parted cryptsetup lvm2 pciutils btrfs-progs
|
||||
util-linux gptfdisk parted cryptsetup lvm2 pciutils usbutils btrfs-progs
|
||||
]} \
|
||||
--set NOMARCHY_INSTALL_SHARE "$share" \
|
||||
--set NOMARCHY_NIXPKGS ${nixpkgsPath} \
|
||||
|
||||
@@ -15,12 +15,14 @@
|
||||
# proved fragile twice before.
|
||||
{ mainDrive
|
||||
, withLuks ? true
|
||||
, swapSize ? "0" # "0" = no swap; otherwise e.g. "16G" (sized for hibernation)
|
||||
, swapSize ? "0" # "0" or "0G" = no swap; otherwise e.g. "16G" (hibernation-sized)
|
||||
, ...
|
||||
}:
|
||||
|
||||
let
|
||||
btrfsMountOptions = [ "compress=zstd" "noatime" ];
|
||||
# Installer historically always appended "G"; accept both "0" and "0G".
|
||||
noSwap = swapSize == "0" || swapSize == "0G";
|
||||
|
||||
rootBtrfs = {
|
||||
type = "btrfs";
|
||||
@@ -32,7 +34,7 @@ let
|
||||
"@log" = { mountpoint = "/var/log"; mountOptions = btrfsMountOptions; };
|
||||
# snapper timeline snapshots (nomarchy.system.snapper.enable)
|
||||
"@snapshots" = { mountpoint = "/.snapshots"; mountOptions = btrfsMountOptions; };
|
||||
} // (if swapSize == "0" then { } else {
|
||||
} // (if noSwap then { } else {
|
||||
# Hibernation-ready swapfile on its own subvolume; disko's
|
||||
# mkswapfile handles the BTRFS NOCOW requirements.
|
||||
"@swap" = {
|
||||
|
||||
@@ -112,6 +112,7 @@ HARDWARE_DB=(
|
||||
# ----------------------------------------------------------------------------
|
||||
nomarchy_detect_hw() {
|
||||
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 "")
|
||||
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}')
|
||||
@@ -126,7 +127,7 @@ nomarchy_detect_hw() {
|
||||
|
||||
# GPU (lspci may list several; report all)
|
||||
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
|
||||
case "$gpu_line" in
|
||||
*"[10de:"*|*"NVIDIA"*) nvidia=1 ;;
|
||||
@@ -140,10 +141,102 @@ nomarchy_detect_hw() {
|
||||
(( intelgpu )) && { echo "MODULE common-gpu-intel"; echo "DETAIL gpu: Intel"; detected=1; }
|
||||
fi
|
||||
|
||||
# Chassis (glob test, not compgen — nixpkgs' non-interactive bash
|
||||
# is built without the completion builtins)
|
||||
local bats=(/sys/class/power_supply/BAT*)
|
||||
if [[ -e "${bats[0]}" ]]; then
|
||||
# ── 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: any system battery (type=Battery, not scope=Device) —
|
||||
# name-agnostic BAT0/CMB0/… (BACKLOG #60). Not a bare BAT* glob.
|
||||
local has_bat=0 d
|
||||
for d in /sys/class/power_supply/*/; do
|
||||
[[ "$(cat "$d/type" 2>/dev/null)" == Battery ]] || continue
|
||||
[[ "$(cat "$d/scope" 2>/dev/null || echo System)" == Device ]] && continue
|
||||
has_bat=1
|
||||
break
|
||||
done
|
||||
if (( has_bat )); then
|
||||
echo "MODULE common-pc-laptop"
|
||||
echo "DETAIL chassis: laptop (battery present)"
|
||||
else
|
||||
|
||||
@@ -18,8 +18,11 @@
|
||||
# [NOMARCHY_TIMEZONE=UTC] [NOMARCHY_LUKS_PASSPHRASE=...] \
|
||||
# [NOMARCHY_LOCALE=en_US.UTF-8] [NOMARCHY_KB_LAYOUT=us] [NOMARCHY_KB_VARIANT=] \
|
||||
# [NOMARCHY_SWAP_GB=N (default: RAM size; 0 = none)] \
|
||||
# [NOMARCHY_LUKS_PASSPHRASE=... | NOMARCHY_NO_LUKS=1] \
|
||||
# [NOMARCHY_HW="auto"|"none"|"mod1 mod2"] [NOMARCHY_FINISH=none|reboot|poweroff]
|
||||
# nomarchy-install
|
||||
# Unattended encryption is fail-closed: set a passphrase, or explicit
|
||||
# NOMARCHY_NO_LUKS=1 — never silently install cleartext.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -54,6 +57,15 @@ if [[ $EUID -ne 0 ]]; then
|
||||
exec sudo --preserve-env "$0" "$@"
|
||||
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."
|
||||
|
||||
[[ -d /sys/firmware/efi ]] \
|
||||
@@ -77,7 +89,9 @@ export NIX_CONFIG="flake-registry = $SHARE/registry.json"
|
||||
# substituter resolves to the TARGET store (i.e. itself), so without
|
||||
# this nothing flows from the ISO and nix bootstraps gcc from source.
|
||||
NIXOS_INSTALL_OPTS=()
|
||||
OFFLINE=false
|
||||
if ! timeout 3 bash -c '</dev/tcp/cache.nixos.org/443' 2>/dev/null; then
|
||||
OFFLINE=true
|
||||
info "No network — substituting from the ISO store only."
|
||||
NIX_CONFIG+=$'\nsubstituters =\nextra-substituters = daemon?trusted=1\nbuilders ='
|
||||
# Must ALSO go through nixos-install as a flag: it passes its own
|
||||
@@ -110,6 +124,15 @@ fi
|
||||
[[ -b "$TARGET_DISK" ]] || fail "$TARGET_DISK is not a block device."
|
||||
info "Target: $TARGET_DISK"
|
||||
|
||||
# Single whole-disk install only (no dual-boot path) — if the chosen disk
|
||||
# already carries a recognizable OS/filesystem signature, call it out
|
||||
# explicitly before the pre-wipe below destroys it.
|
||||
existing_sig="$(lsblk -no FSTYPE,LABEL "$TARGET_DISK" 2>/dev/null || true
|
||||
blkid "$TARGET_DISK"* 2>/dev/null || true)"
|
||||
if grep -qiE 'ntfs|bitlocker|microsoft|crypto_luks' <<< "$existing_sig"; then
|
||||
warn "$TARGET_DISK has existing data (Windows/BitLocker/NTFS or LUKS) — it will be destroyed."
|
||||
fi
|
||||
|
||||
# ─── Encryption ─────────────────────────────────────────────────────────
|
||||
section "Disk encryption"
|
||||
|
||||
@@ -117,7 +140,15 @@ section "Disk encryption"
|
||||
# logs you straight into the desktop (the passphrase already gates access).
|
||||
LUKS_PASSPHRASE=""
|
||||
if [[ "$UNATTENDED" == "1" ]]; then
|
||||
LUKS_PASSPHRASE="${NOMARCHY_LUKS_PASSPHRASE:-}"
|
||||
# Fail-closed: unattended without a passphrase used to install
|
||||
# cleartext (easy CI footgun). Require an explicit opt-out.
|
||||
if [[ "${NOMARCHY_NO_LUKS:-}" == "1" ]]; then
|
||||
LUKS_PASSPHRASE=""
|
||||
elif [[ -n "${NOMARCHY_LUKS_PASSPHRASE:-}" ]]; then
|
||||
LUKS_PASSPHRASE="$NOMARCHY_LUKS_PASSPHRASE"
|
||||
else
|
||||
fail "Unattended install needs NOMARCHY_LUKS_PASSPHRASE or NOMARCHY_NO_LUKS=1"
|
||||
fi
|
||||
elif gum confirm --default=yes "Encrypt the disk with LUKS? (default — also enables passwordless desktop login)"; then
|
||||
while true; do
|
||||
p1=$(gum input --password --placeholder "LUKS passphrase (min 8 chars)")
|
||||
@@ -161,8 +192,8 @@ else
|
||||
warn "Invalid username (lowercase letters, digits, - and _)."
|
||||
done
|
||||
while true; do
|
||||
PASSWORD=$(gum input --password --placeholder "password for $USERNAME")
|
||||
[[ -n "$PASSWORD" ]] || { warn "Empty password."; continue; }
|
||||
PASSWORD=$(gum input --password --placeholder "password for $USERNAME (min 8 chars)")
|
||||
[[ ${#PASSWORD} -ge 8 ]] || { warn "Too short (min 8 chars)."; continue; }
|
||||
p2=$(gum input --password --placeholder "repeat password")
|
||||
[[ "$PASSWORD" == "$p2" ]] && break
|
||||
warn "Passwords don't match."
|
||||
@@ -210,6 +241,8 @@ section "Hardware detection"
|
||||
source "$SHARE/hardware-db.sh"
|
||||
|
||||
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}"
|
||||
if [[ "$hw_mode" == "none" ]]; then
|
||||
info "Hardware profiles skipped."
|
||||
@@ -220,8 +253,10 @@ else
|
||||
if [[ -n "$detection" ]]; then
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
MODULE\ *) HW_PROFILES+=("${line#MODULE }") ;;
|
||||
DETAIL\ *) info "→ ${line#DETAIL }" ;;
|
||||
MODULE\ *) HW_PROFILES+=("${line#MODULE }") ;;
|
||||
NOMARCHY-NPU\ *) NPU_VENDOR="${line#NOMARCHY-NPU }" ;;
|
||||
NOMARCHY\ *) HW_NOMARCHY+=("${line#NOMARCHY }") ;;
|
||||
DETAIL\ *) info "→ ${line#DETAIL }" ;;
|
||||
esac
|
||||
done <<< "$detection"
|
||||
fi
|
||||
@@ -241,6 +276,14 @@ info "Profiles: ${HW_PROFILES[*]:-(none)}"
|
||||
# ─── Review & point of no return ────────────────────────────────────────
|
||||
section "Review"
|
||||
|
||||
# Match the Source line to the same cache.nixos.org probe that sets OFFLINE
|
||||
# (above): offline = ISO store only; online may still hit substituters.
|
||||
if [[ "$OFFLINE" == true ]]; then
|
||||
SOURCE_NET="pinned into the ISO, no network needed"
|
||||
else
|
||||
SOURCE_NET="pinned into the ISO; may use network binary caches"
|
||||
fi
|
||||
|
||||
gum style --border normal --padding "0 2" \
|
||||
"Disk: $TARGET_DISK (WILL BE ERASED)" \
|
||||
"Encryption: $([[ $WITH_LUKS == true ]] && echo "LUKS2 + desktop auto-login" || echo none)" \
|
||||
@@ -250,7 +293,7 @@ gum style --border normal --padding "0 2" \
|
||||
"Timezone: $TIMEZONE" \
|
||||
"Hardware: ${HW_PROFILES[*]:-none}" \
|
||||
"Snapshots: snapper timeline on /" \
|
||||
"Source: nomarchy ${NOMARCHY_REV:0:12}${NOMARCHY_REV:+ }$([[ -z "${NOMARCHY_REV:-}" ]] && echo "(dirty tree) ")— pinned into the ISO, no network needed"
|
||||
"Source: nomarchy ${NOMARCHY_REV:0:12}${NOMARCHY_REV:+ }$([[ -z "${NOMARCHY_REV:-}" ]] && echo "(dirty tree) ")— $SOURCE_NET"
|
||||
|
||||
if [[ "$UNATTENDED" != "1" ]]; then
|
||||
typed=$(gum input --placeholder "type the disk name ($(basename "$TARGET_DISK")) to confirm the wipe")
|
||||
@@ -304,11 +347,19 @@ if [[ $WITH_LUKS == true ]]; then
|
||||
unset LUKS_PASSPHRASE
|
||||
fi
|
||||
|
||||
# disko-config treats exact "0" as no-swap; "${SWAP_GB}G" would pass "0G"
|
||||
# and still create a useless @swap subvolume (layout vs resume disagreed).
|
||||
if [[ "$SWAP_GB" == "0" ]]; then
|
||||
DISKO_SWAP_SIZE="0"
|
||||
else
|
||||
DISKO_SWAP_SIZE="${SWAP_GB}G"
|
||||
fi
|
||||
|
||||
disko_log=$(mktemp --suffix=.disko.log)
|
||||
if ! disko --mode destroy,format,mount --yes-wipe-all-disks \
|
||||
--argstr mainDrive "$TARGET_DISK" \
|
||||
--arg withLuks "$WITH_LUKS" \
|
||||
--argstr swapSize "${SWAP_GB}G" \
|
||||
--argstr swapSize "$DISKO_SWAP_SIZE" \
|
||||
"$SHARE/disko-config.nix" >"$disko_log" 2>&1; then
|
||||
tail -n 30 "$disko_log"
|
||||
fail "disko failed — full log: $disko_log"
|
||||
@@ -317,21 +368,14 @@ rm -f "$LUKS_KEY_PATH" "$disko_log"
|
||||
success "Disk partitioned and mounted at /mnt"
|
||||
|
||||
# Hibernation plumbing: the swapfile's physical offset goes into the
|
||||
# kernel cmdline. Deactivate swap first so nixos-generate-config doesn't
|
||||
# also emit a swapDevices entry (we write our own, with resume wiring).
|
||||
RESUME_CONFIG=""
|
||||
# kernel cmdline (patched into system.nix). Deactivate swap first so
|
||||
# nixos-generate-config doesn't also emit a swapDevices entry.
|
||||
resume_offset=""
|
||||
root_uuid=""
|
||||
if [[ "$SWAP_GB" != "0" ]]; then
|
||||
swapoff -a 2>/dev/null || true
|
||||
resume_offset=$(btrfs inspect-internal map-swapfile -r /mnt/swap/swapfile)
|
||||
root_uuid=$(findmnt -no UUID /mnt)
|
||||
RESUME_CONFIG=$(cat <<NIX
|
||||
|
||||
# Swapfile (hibernation-ready: resume points into it).
|
||||
swapDevices = [{ device = "/swap/swapfile"; }];
|
||||
boot.resumeDevice = "/dev/disk/by-uuid/$root_uuid";
|
||||
boot.kernelParams = [ "resume_offset=$resume_offset" ];
|
||||
NIX
|
||||
)
|
||||
success "Swapfile created (resume offset $resume_offset)"
|
||||
fi
|
||||
|
||||
@@ -345,133 +389,100 @@ nixos-generate-config --root /mnt
|
||||
mv /mnt/etc/nixos/hardware-configuration.nix "$FLAKE_DIR/"
|
||||
rm -rf /mnt/etc/nixos
|
||||
|
||||
cp "$SHARE/template/theme-state.json" "$FLAKE_DIR/"
|
||||
# templates/downstream is the single source of truth (same files as
|
||||
# `nix flake init -t`). Copy, then patch install-time values only.
|
||||
cp "$SHARE/template/flake.nix" \
|
||||
"$SHARE/template/system.nix" \
|
||||
"$SHARE/template/home.nix" \
|
||||
"$SHARE/template/theme-state.json" \
|
||||
"$FLAKE_DIR/"
|
||||
|
||||
# home.nix is generated (not copied from the template) so the chosen
|
||||
# keyboard layout reaches the Hyprland session — standalone HM cannot
|
||||
# read system.nix.
|
||||
cat > "$FLAKE_DIR/home.nix" <<EOF
|
||||
# Your user environment. The Nomarchy desktop (Hyprland, Waybar,
|
||||
# Ghostty, theming engine, Stylix) comes from homeModules.nomarchy;
|
||||
# tune it via the nomarchy.* options, add your own packages and
|
||||
# programs below.
|
||||
{ pkgs, ... }:
|
||||
# Detected hardware → flags for the patcher (safe defaults active).
|
||||
has_intel=false; has_amd=false; has_fp=false
|
||||
intel_guc_off=false; has_camera_ir=false
|
||||
if [[ ${#HW_NOMARCHY[@]} -gt 0 ]]; then
|
||||
for nm in "${HW_NOMARCHY[@]}"; do
|
||||
case "$nm" in
|
||||
hardware.intel.enable=true) has_intel=true ;;
|
||||
hardware.intel.guc=false) intel_guc_off=true ;;
|
||||
hardware.amd.enable=true) has_amd=true ;;
|
||||
hardware.fingerprint.enable=true) has_fp=true ;;
|
||||
hardware.camera.hideIrSensor=true) has_camera_ir=true ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
# NVIDIA is a nixos-hardware MODULE only (no nomarchy.hardware.nvidia.*) —
|
||||
# still emit commented plain-NixOS guidance in system.nix (BACKLOG #59).
|
||||
has_nvidia=false
|
||||
[[ " ${HW_PROFILES[*]:-} " == *" common-gpu-nvidia "* ]] && has_nvidia=true
|
||||
is_laptop=false
|
||||
[[ " ${HW_PROFILES[*]:-} " == *" common-pc-laptop "* ]] && is_laptop=true
|
||||
thermald=false
|
||||
[[ $is_laptop == true ]] && grep -q GenuineIntel /proc/cpuinfo 2>/dev/null && thermald=true
|
||||
|
||||
{
|
||||
# Keyboard for the desktop session; console + LUKS prompt get the
|
||||
# same layout from system.nix (xkb + console.useXkbConfig).
|
||||
nomarchy.keyboard.layout = "$KB_LAYOUT";
|
||||
nomarchy.keyboard.variant = "$KB_VARIANT";
|
||||
|
||||
# Examples:
|
||||
# nomarchy.terminal = "kitty"; # swap the default terminal
|
||||
# nomarchy.waybar.enable = false; # bring your own bar
|
||||
# nomarchy.stylix.enable = false; # opt out of GTK/Qt theming
|
||||
|
||||
home.packages = with pkgs; [
|
||||
# firefox
|
||||
];
|
||||
}
|
||||
EOF
|
||||
|
||||
hw_nix=""
|
||||
# JSON for patch-template.py (stdin). Hardware profiles as a JSON array.
|
||||
hw_json="["
|
||||
first=1
|
||||
for p in "${HW_PROFILES[@]:-}"; do
|
||||
[[ -n "$p" ]] && hw_nix+=" \"$p\""
|
||||
[[ -z "$p" ]] && continue
|
||||
if [[ $first -eq 1 ]]; then first=0; else hw_json+=","; fi
|
||||
hw_json+=$(printf '%s' "$p" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().rstrip("\n")))')
|
||||
done
|
||||
hw_json+="]"
|
||||
|
||||
cat > "$FLAKE_DIR/flake.nix" <<EOF
|
||||
{
|
||||
description = "$HOSTNAME_ — my Nomarchy machine";
|
||||
|
||||
# The only input. nixpkgs, home-manager etc. come pinned through it —
|
||||
# tested together upstream. Generated by nomarchy-install; your machine
|
||||
# lives in system.nix and home.nix, this file is never hand-edited.
|
||||
inputs.nomarchy.url = "${NOMARCHY_FLAKE_URL}";
|
||||
|
||||
outputs = { nomarchy, ... }:
|
||||
nomarchy.lib.mkFlake {
|
||||
src = ./.;
|
||||
username = "$USERNAME";
|
||||
hardwareProfile = [$hw_nix ];
|
||||
};
|
||||
}
|
||||
EOF
|
||||
|
||||
AUTOLOGIN_CONFIG=""
|
||||
if [[ $WITH_LUKS == true ]]; then
|
||||
AUTOLOGIN_CONFIG=$(cat <<NIX
|
||||
|
||||
# The LUKS passphrase already gates this machine — skip the second
|
||||
# password prompt and boot straight into the desktop.
|
||||
nomarchy.system.greeter.autoLogin = "$USERNAME";
|
||||
NIX
|
||||
)
|
||||
resume_json="null"
|
||||
root_uuid_json="null"
|
||||
if [[ -n "$resume_offset" && "$SWAP_GB" != "0" ]]; then
|
||||
resume_json=$(printf '%s' "$resume_offset" | python3 -c 'import json,sys; print(json.dumps(int(sys.stdin.read().strip())))')
|
||||
root_uuid_json=$(printf '%s' "$root_uuid" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().rstrip("\n")))')
|
||||
fi
|
||||
|
||||
# Laptop power: power-profiles-daemon ships by default; mark this a laptop
|
||||
# so battery-only features apply, and enable thermald on Intel. Keyed off
|
||||
# the same battery probe that chose the common-pc-laptop hardware profile.
|
||||
POWER_CONFIG=""
|
||||
if [[ " ${HW_PROFILES[*]:-} " == *" common-pc-laptop "* ]]; then
|
||||
power_lines=" # Laptop power management (power-profiles-daemon + menu/Waybar
|
||||
# switcher). Uncomment to stop charging at 80% to extend battery life.
|
||||
nomarchy.system.power.laptop = true;
|
||||
# nomarchy.system.power.batteryChargeLimit = 80;"
|
||||
if grep -q GenuineIntel /proc/cpuinfo 2>/dev/null; then
|
||||
power_lines+="
|
||||
nomarchy.system.power.thermal.enable = true; # thermald (Intel)"
|
||||
fi
|
||||
POWER_CONFIG=$(cat <<NIX
|
||||
|
||||
$power_lines
|
||||
NIX
|
||||
)
|
||||
fi
|
||||
|
||||
# initialHashedPassword is safe to template: mkpasswd's alphabet is
|
||||
# [a-zA-Z0-9./$] — no Nix string metacharacters.
|
||||
cat > "$FLAKE_DIR/system.nix" <<EOF
|
||||
# Your machine: hostname, users, services. The distro itself comes from
|
||||
# Nomarchy (via flake.nix); override its defaults here with plain NixOS
|
||||
# options, or the nomarchy.system.* toggles.
|
||||
{ pkgs, username, ... }:
|
||||
|
||||
python3 "$SHARE/patch-template.py" "$FLAKE_DIR" <<PYJSON
|
||||
{
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
networking.hostName = "$HOSTNAME_";
|
||||
time.timeZone = "$TIMEZONE";
|
||||
i18n.defaultLocale = "$LOCALE";
|
||||
|
||||
# One keyboard layout everywhere: xkb is the source of truth, and the
|
||||
# distro defaults (console.useXkbConfig + the systemd initrd, set in
|
||||
# the nomarchy module) derive the virtual console and the LUKS
|
||||
# passphrase prompt from it — so only the chosen layout is written here.
|
||||
# The Hyprland session reads the same layout from nomarchy.keyboard.* in
|
||||
# home.nix.
|
||||
services.xserver.xkb.layout = "$KB_LAYOUT";
|
||||
services.xserver.xkb.variant = "$KB_VARIANT";
|
||||
|
||||
# Your login user — \`username\` flows in from flake.nix automatically.
|
||||
users.users.\${username} = {
|
||||
isNormalUser = true;
|
||||
extraGroups = [ "wheel" "networkmanager" "video" "input" ];
|
||||
initialHashedPassword = "$HASHED_PASSWORD";
|
||||
};
|
||||
$AUTOLOGIN_CONFIG$POWER_CONFIG$RESUME_CONFIG
|
||||
# Hourly/daily BTRFS timeline snapshots + nixos-rebuild-snap.
|
||||
nomarchy.system.snapper.enable = true;
|
||||
|
||||
system.stateVersion = "26.05";
|
||||
"hostname": $(printf '%s' "$HOSTNAME_" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().rstrip("\n")))'),
|
||||
"username": $(printf '%s' "$USERNAME" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().rstrip("\n")))'),
|
||||
"timezone": $(printf '%s' "$TIMEZONE" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().rstrip("\n")))'),
|
||||
"locale": $(printf '%s' "$LOCALE" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().rstrip("\n")))'),
|
||||
"keyboardLayout": $(printf '%s' "$KB_LAYOUT" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().rstrip("\n")))'),
|
||||
"keyboardVariant": $(printf '%s' "$KB_VARIANT" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().rstrip("\n")))'),
|
||||
"hashedPassword": $(printf '%s' "$HASHED_PASSWORD" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().rstrip("\n")))'),
|
||||
"autoLogin": $([[ $WITH_LUKS == true ]] && echo true || echo false),
|
||||
"laptop": $is_laptop,
|
||||
"thermald": $thermald,
|
||||
"hardwareProfiles": $hw_json,
|
||||
"hardware": {
|
||||
"intel": $has_intel,
|
||||
"intelGucOff": $intel_guc_off,
|
||||
"amd": $has_amd,
|
||||
"fingerprint": $has_fp,
|
||||
"cameraIr": $has_camera_ir,
|
||||
"nvidia": $has_nvidia,
|
||||
"npu": $(if [[ -n "$NPU_VENDOR" ]]; then printf '%s' "$NPU_VENDOR" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().rstrip("\n")))'; else echo null; fi)
|
||||
},
|
||||
"resumeOffset": $resume_json,
|
||||
"rootUuid": $root_uuid_json
|
||||
}
|
||||
EOF
|
||||
PYJSON
|
||||
|
||||
# The flake.lock: composed offline — nomarchy is path-locked to the very
|
||||
# source the ISO carries (original stays the forge URL, so a later
|
||||
# `nix flake update` on the installed machine re-resolves normally).
|
||||
if ! python3 "$SHARE/compose-lock.py" "$SHARE/flake.lock" "$FLAKE_DIR/flake.lock" \
|
||||
# NOMARCHY_TEST_FORCE_COMPOSE_FAIL=1 — unattended harness only (#54 V2):
|
||||
# pretends compose-lock failed so the offline fail-closed arm is exercised
|
||||
# without poisoning the ISO store.
|
||||
compose_ok=0
|
||||
if [[ "${NOMARCHY_TEST_FORCE_COMPOSE_FAIL:-}" == 1 ]]; then
|
||||
compose_ok=1
|
||||
elif python3 "$SHARE/compose-lock.py" "$SHARE/flake.lock" "$FLAKE_DIR/flake.lock" \
|
||||
"$NOMARCHY_LOCKED_JSON" "$NOMARCHY_ORIGINAL_JSON"; then
|
||||
compose_ok=0
|
||||
else
|
||||
compose_ok=1
|
||||
fi
|
||||
if (( compose_ok != 0 )); then
|
||||
if [[ "$OFFLINE" == true ]]; then
|
||||
fail "Offline lock composition failed and there is no network to fall back to — cannot finish an offline install."
|
||||
fi
|
||||
warn "Offline lock composition failed — resolving over the network."
|
||||
(cd "$FLAKE_DIR" && nix --extra-experimental-features "nix-command flakes" flake lock)
|
||||
fi
|
||||
@@ -485,13 +496,9 @@ fi
|
||||
commit -qm "Initial Nomarchy configuration"
|
||||
)
|
||||
|
||||
# The user must own their flake — libgit2 refuses repositories owned by
|
||||
# someone else, which breaks `home-manager switch` (and theme switching)
|
||||
# outright. The first normal NixOS user is always 1000:users(100); the
|
||||
# account doesn't exist in the target yet, so numeric ids it is.
|
||||
chown -R 1000:100 "$FLAKE_DIR"
|
||||
# The templates come out of the nix store mode 0444 and cp preserves
|
||||
# that — without this the user owns home.nix but can't edit it.
|
||||
# that — without this the user can't edit home.nix after they own it.
|
||||
# Ownership is applied after nixos-install (real uid/gid; see below).
|
||||
chmod -R u+w "$FLAKE_DIR"
|
||||
|
||||
# /etc/nixos on the installed system points at the user-owned flake.
|
||||
@@ -543,6 +550,17 @@ fi
|
||||
nixos-install --no-root-passwd "${NIXOS_INSTALL_OPTS[@]}" --flake "path:$FLAKE_DIR#default"
|
||||
success "System installed (bootloader in place)"
|
||||
|
||||
# The user must own their flake — libgit2 refuses repositories owned by
|
||||
# someone else, which breaks `home-manager switch` (and theme switching)
|
||||
# outright. Resolve real uid/gid from the target after nixos-install
|
||||
# created the account (do not hard-code 1000:100 — first free uid or
|
||||
# primary group can differ).
|
||||
USER_UID=$(nixos-enter --root /mnt -- id -u "$USERNAME") \
|
||||
|| fail "Could not resolve uid for install user '$USERNAME' on target"
|
||||
USER_GID=$(nixos-enter --root /mnt -- id -g "$USERNAME") \
|
||||
|| fail "Could not resolve gid for install user '$USERNAME' on target"
|
||||
chown -R "$USER_UID:$USER_GID" "$FLAKE_DIR"
|
||||
|
||||
# Pre-activate the Home Manager generation so the FIRST boot lands in the
|
||||
# fully themed desktop, not bare Hyprland. Best-effort: a failure here
|
||||
# only costs the user one `home-manager switch` after logging in.
|
||||
@@ -573,6 +591,8 @@ cat > /mnt/root/nomarchy-hm-activate.sh <<EOF
|
||||
set -ex
|
||||
exec > /var/log/nomarchy-hm-preactivate.log 2>&1
|
||||
export PATH=/run/current-system/sw/bin:\$PATH
|
||||
# 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
|
||||
# build (default substituters — the live-side build already proved the
|
||||
# no-network case) is a last-resort fallback.
|
||||
@@ -595,13 +615,39 @@ sleep 2
|
||||
runuser -u "$USERNAME" -- bash -lc \
|
||||
"USER=$USERNAME HOME=/home/$USERNAME NIX_REMOTE=daemon HOME_MANAGER_BACKUP_EXT=bak \$out/activate"
|
||||
EOF
|
||||
if nixos-enter --root /mnt -- bash /root/nomarchy-hm-activate.sh; then
|
||||
# NOMARCHY_TEST_FORCE_HM_FAIL=1 — unattended harness only (#54 V2): take
|
||||
# the failure arm so the durable recovery hint is exercised without a
|
||||
# real activation breakage.
|
||||
hm_activate_ok=0
|
||||
if [[ "${NOMARCHY_TEST_FORCE_HM_FAIL:-}" == 1 ]]; then
|
||||
hm_activate_ok=1
|
||||
warn "NOMARCHY_TEST_FORCE_HM_FAIL=1 — skipping real pre-activate (test harness)"
|
||||
elif nixos-enter --root /mnt -- bash /root/nomarchy-hm-activate.sh; then
|
||||
hm_activate_ok=0
|
||||
else
|
||||
hm_activate_ok=1
|
||||
fi
|
||||
if (( hm_activate_ok == 0 )); then
|
||||
success "Desktop pre-activated — first boot is fully themed"
|
||||
else
|
||||
warn "Desktop pre-activation failed (see /var/log/nomarchy-hm-preactivate.log"
|
||||
warn "on the installed system); after first login run:"
|
||||
warn " home-manager switch --flake ~/.nomarchy -b bak"
|
||||
tail -n 5 /mnt/var/log/nomarchy-hm-preactivate.log 2>/dev/null || true
|
||||
# The live session (and this warning) ends with this install — drop a
|
||||
# durable hint on the TARGET so the fix still surfaces on first login.
|
||||
# Numeric ids from the target account (USER_UID/USER_GID above) — the
|
||||
# name does not exist in the live ISO's passwd.
|
||||
hint_file="/mnt/home/$USERNAME/NOMARCHY-DESKTOP-NOT-THEMED.txt"
|
||||
cat > "$hint_file" <<HINT
|
||||
Desktop pre-activation failed during install — see
|
||||
/var/log/nomarchy-hm-preactivate.log for details. Finish it with:
|
||||
|
||||
home-manager switch --flake ~/.nomarchy -b bak
|
||||
|
||||
(delete this file once done)
|
||||
HINT
|
||||
chown "$USER_UID:$USER_GID" "$hint_file"
|
||||
fi
|
||||
rm -f /mnt/root/nomarchy-hm-activate.sh
|
||||
|
||||
|
||||
265
pkgs/nomarchy-install/patch-template.py
Normal file
265
pkgs/nomarchy-install/patch-template.py
Normal file
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Patch a copied templates/downstream machine flake with install-time values.
|
||||
|
||||
The template is the single source of truth for commented opt-ins and the
|
||||
starter app suite. The installer copies it, then this script only:
|
||||
|
||||
* replaces known placeholders (hostname, username, locale, keyboard, …)
|
||||
* fills the __NOMARCHY_INSTALLER__ region with detected/active config
|
||||
* sets hardwareProfile on flake.nix
|
||||
|
||||
Usage:
|
||||
patch-template.py <flake-dir> # reads a JSON object from stdin
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BEGIN = " # __NOMARCHY_INSTALLER_BEGIN__"
|
||||
END = " # __NOMARCHY_INSTALLER_END__"
|
||||
|
||||
|
||||
def nix_str(s: str) -> str:
|
||||
"""Escape a string for a Nix double-quoted literal."""
|
||||
return (
|
||||
s.replace("\\", "\\\\")
|
||||
.replace('"', '\\"')
|
||||
.replace("${", "\\${")
|
||||
.replace("\n", "\\n")
|
||||
)
|
||||
|
||||
|
||||
def replace_once(text: str, old: str, new: str, label: str) -> str:
|
||||
if old not in text:
|
||||
sys.exit(f"patch-template: missing placeholder for {label}: {old!r}")
|
||||
return text.replace(old, new, 1)
|
||||
|
||||
|
||||
def patch_flake(text: str, v: dict) -> str:
|
||||
text = replace_once(
|
||||
text,
|
||||
'description = "My Nomarchy machine";',
|
||||
f'description = "{nix_str(v["hostname"])} — my Nomarchy machine";',
|
||||
"flake description",
|
||||
)
|
||||
text = replace_once(
|
||||
text,
|
||||
'username = "me"; # <- your login name',
|
||||
f'username = "{nix_str(v["username"])}"; # <- your login name',
|
||||
"flake username",
|
||||
)
|
||||
profiles = v.get("hardwareProfiles") or []
|
||||
if profiles:
|
||||
items = " ".join(f'"{nix_str(p)}"' for p in profiles)
|
||||
hw_line = f" hardwareProfile = [ {items} ];"
|
||||
else:
|
||||
hw_line = " # hardwareProfile = null; # no nixos-hardware profiles selected"
|
||||
# Replace the optional hardwareProfile comment block with the install choice.
|
||||
text, n = re.subn(
|
||||
r"\n # Optional: a nixos-hardware module name for your machine, e\.g\.\n"
|
||||
r" # hardwareProfile = \"framework-13-7040-amd\";\n"
|
||||
r" # Names: https://github.com/NixOS/nixos-hardware\n"
|
||||
r" # \(the future installer fills this in automatically from DMI data\)\n",
|
||||
f"\n{hw_line}\n"
|
||||
f" # Names: https://github.com/NixOS/nixos-hardware\n",
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
if n != 1:
|
||||
sys.exit("patch-template: could not patch hardwareProfile block in flake.nix")
|
||||
return text
|
||||
|
||||
|
||||
def patch_home(text: str, v: dict) -> str:
|
||||
layout = nix_str(v["keyboardLayout"])
|
||||
variant = nix_str(v.get("keyboardVariant") or "")
|
||||
text = replace_once(
|
||||
text,
|
||||
' nomarchy.keyboard.layout = "us";',
|
||||
f' nomarchy.keyboard.layout = "{layout}";',
|
||||
"home keyboard layout",
|
||||
)
|
||||
text = replace_once(
|
||||
text,
|
||||
' nomarchy.keyboard.variant = "";',
|
||||
f' nomarchy.keyboard.variant = "{variant}";',
|
||||
"home keyboard variant",
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
def build_installer_region(v: dict) -> str:
|
||||
lines: list[str] = [
|
||||
BEGIN,
|
||||
" # Written by nomarchy-install from live detection. Safe defaults are",
|
||||
" # active; heavier opt-ins stay in the commented catalog below.",
|
||||
]
|
||||
|
||||
if v.get("autoLogin"):
|
||||
user = nix_str(v["username"])
|
||||
lines += [
|
||||
" # LUKS passphrase already gates this machine — skip the greeter password.",
|
||||
f' nomarchy.system.greeter.autoLogin = "{user}";',
|
||||
]
|
||||
|
||||
if v.get("laptop"):
|
||||
lines += [
|
||||
" # Laptop power (PPD + menu/Waybar). Uncomment to cap charge at 80%.",
|
||||
" nomarchy.system.power.laptop = true;",
|
||||
" # nomarchy.system.power.batteryChargeLimit = 80;",
|
||||
]
|
||||
if v.get("thermald"):
|
||||
lines.append(
|
||||
" nomarchy.system.power.thermal.enable = true; # thermald (Intel)"
|
||||
)
|
||||
|
||||
hw = v.get("hardware") or {}
|
||||
if any(
|
||||
hw.get(k)
|
||||
for k in ("intel", "amd", "fingerprint", "cameraIr", "npu", "nvidia")
|
||||
):
|
||||
lines.append(" # Hardware enablement (auto-detected).")
|
||||
if hw.get("intel"):
|
||||
lines.append(
|
||||
" nomarchy.hardware.intel.enable = true; # GuC/HuC (i915)"
|
||||
)
|
||||
if hw.get("intelGucOff"):
|
||||
lines.append(
|
||||
" nomarchy.hardware.intel.guc = false; # xe driver → GuC default-on"
|
||||
)
|
||||
lines.append(
|
||||
" # nomarchy.hardware.intel.computeRuntime = true; # OpenCL/oneVPL (opt-in)"
|
||||
)
|
||||
if hw.get("amd"):
|
||||
lines += [
|
||||
" nomarchy.hardware.amd.enable = true; # amd-pstate + VA-API",
|
||||
" # nomarchy.hardware.amd.rocm.enable = true; # ROCm (multi-GB, opt-in)",
|
||||
' # nomarchy.hardware.amd.rocm.gfxOverride = ""; # e.g. "11.0.0" for unlisted iGPU',
|
||||
]
|
||||
if hw.get("fingerprint"):
|
||||
lines += [
|
||||
" nomarchy.hardware.fingerprint.enable = true; # fprintd (enroll: fprintd-enroll)",
|
||||
" # nomarchy.hardware.fingerprint.pam = true; # login + sudo (opt-in)",
|
||||
]
|
||||
if hw.get("cameraIr"):
|
||||
lines.append(
|
||||
" nomarchy.hardware.camera.hideIrSensor = true; # dual-sensor: hide IR node"
|
||||
)
|
||||
if hw.get("npu"):
|
||||
vendor = nix_str(hw["npu"])
|
||||
lines += [
|
||||
f" # nomarchy.hardware.npu.enable = true; # {vendor} NPU (experimental; userspace BYO)",
|
||||
" # nomarchy.hardware.latestKernel = true; # if the NPU driver needs a newer kernel",
|
||||
]
|
||||
# NVIDIA: profile is in flake.nix (common-gpu-nvidia). Hybrid/PRIME
|
||||
# knobs are plain NixOS — comment-only guidance, same pattern as ROCm.
|
||||
if hw.get("nvidia"):
|
||||
lines += [
|
||||
" # NVIDIA: common-gpu-nvidia is in hardwareProfile (flake.nix).",
|
||||
" # Hybrid/PRIME, power, open-module — plain NixOS; see docs/HARDWARE.md §6",
|
||||
" # and https://wiki.nixos.org/wiki/Nvidia (bus IDs are machine-specific).",
|
||||
" # hardware.nvidia.prime = { ... }; # offload/sync",
|
||||
" # hardware.nvidia.powerManagement.enable = true; # suspend/resume",
|
||||
" # hardware.nvidia.open = false; # true = open module (newer cards)",
|
||||
]
|
||||
|
||||
if v.get("resumeOffset") is not None:
|
||||
root_uuid = nix_str(v["rootUuid"])
|
||||
offset = v["resumeOffset"]
|
||||
lines += [
|
||||
" # Swapfile (hibernation-ready: resume points into it).",
|
||||
' swapDevices = [{ device = "/swap/swapfile"; }];',
|
||||
f' boot.resumeDevice = "/dev/disk/by-uuid/{root_uuid}";',
|
||||
f' boot.kernelParams = [ "resume_offset={offset}" ];',
|
||||
]
|
||||
|
||||
# Always on for installer layout (BTRFS + @snapshots).
|
||||
lines += [
|
||||
" # Hourly/daily BTRFS timeline snapshots + nixos-rebuild-snap.",
|
||||
" nomarchy.system.snapper.enable = true;",
|
||||
END,
|
||||
]
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def patch_system(text: str, v: dict) -> str:
|
||||
text = replace_once(
|
||||
text,
|
||||
' networking.hostName = "my-nomarchy";',
|
||||
f' networking.hostName = "{nix_str(v["hostname"])}";',
|
||||
"hostName",
|
||||
)
|
||||
text = replace_once(
|
||||
text,
|
||||
' time.timeZone = "UTC";',
|
||||
f' time.timeZone = "{nix_str(v["timezone"])}";',
|
||||
"timeZone",
|
||||
)
|
||||
text = replace_once(
|
||||
text,
|
||||
' i18n.defaultLocale = "en_US.UTF-8";',
|
||||
f' i18n.defaultLocale = "{nix_str(v["locale"])}";',
|
||||
"locale",
|
||||
)
|
||||
text = replace_once(
|
||||
text,
|
||||
' services.xserver.xkb.layout = "us";',
|
||||
f' services.xserver.xkb.layout = "{nix_str(v["keyboardLayout"])}";',
|
||||
"xkb layout",
|
||||
)
|
||||
text = replace_once(
|
||||
text,
|
||||
' services.xserver.xkb.variant = "";',
|
||||
f' services.xserver.xkb.variant = "{nix_str(v.get("keyboardVariant") or "")}";',
|
||||
"xkb variant",
|
||||
)
|
||||
|
||||
# Inject password into the user attrset (template has no password for flake-init).
|
||||
user_block = """ users.users.${username} = {
|
||||
isNormalUser = true;
|
||||
extraGroups = [ "wheel" "networkmanager" "video" "input" ];
|
||||
};"""
|
||||
# HASHED_PASSWORD is sha-512 crypt; alphabet is safe in Nix double quotes.
|
||||
hashed = nix_str(v["hashedPassword"])
|
||||
user_patched = f""" users.users.${{username}} = {{
|
||||
isNormalUser = true;
|
||||
extraGroups = [ "wheel" "networkmanager" "video" "input" ];
|
||||
initialHashedPassword = "{hashed}";
|
||||
}};"""
|
||||
text = replace_once(text, user_block, user_patched, "user password")
|
||||
|
||||
if BEGIN not in text or END not in text:
|
||||
sys.exit("patch-template: system.nix missing __NOMARCHY_INSTALLER__ markers")
|
||||
region = build_installer_region(v)
|
||||
text = re.sub(
|
||||
re.escape(BEGIN) + r".*?" + re.escape(END) + r"\n?",
|
||||
region,
|
||||
text,
|
||||
count=1,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) != 2:
|
||||
sys.exit("usage: patch-template.py <flake-dir>")
|
||||
flake_dir = Path(sys.argv[1])
|
||||
vals = json.load(sys.stdin)
|
||||
|
||||
mapping = {
|
||||
"flake.nix": patch_flake,
|
||||
"home.nix": patch_home,
|
||||
"system.nix": patch_system,
|
||||
}
|
||||
for name, fn in mapping.items():
|
||||
path = flake_dir / name
|
||||
path.write_text(fn(path.read_text(), vals))
|
||||
print(f"patch-template: patched {', '.join(mapping)} in {flake_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -18,6 +18,7 @@ Commands:
|
||||
apply <name|file.json> merge a preset into the state + rebuild
|
||||
set <dotted.path> <value> tweak one key (e.g. `set ui.gapsOut 16`) + rebuild
|
||||
get [dotted.path] print the current state (or one key)
|
||||
validate check the state file against the schema (read-only)
|
||||
wallpaper apply the current wallpaper via swww
|
||||
bg [next|auto] cycle the theme's wallpapers (instant, no rebuild)
|
||||
"""
|
||||
@@ -25,6 +26,7 @@ Commands:
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -90,11 +92,23 @@ def load_state(path: Path = STATE_FILE) -> dict:
|
||||
except FileNotFoundError:
|
||||
die(f"state file not found: {path} (set $NOMARCHY_PATH to your flake checkout)")
|
||||
except json.JSONDecodeError as e:
|
||||
die(f"invalid JSON in {path}: {e}")
|
||||
die(f"theme-state.json has a JSON syntax error at line {e.lineno}, "
|
||||
f"column {e.colno}: {e.msg}\n"
|
||||
f" file: {path}\n"
|
||||
f" fix: correct the syntax by hand (a trailing comma is the "
|
||||
f"usual culprit) — nothing was changed")
|
||||
|
||||
|
||||
def write_state(state: dict) -> None:
|
||||
"""Atomic write: render to a temp file in the same dir, then rename."""
|
||||
"""Atomic write: render to a temp file in the same dir, then rename.
|
||||
Validates first — an invalid state never reaches disk (the old file
|
||||
stays untouched), so a bad `set` can't brick the next rebuild."""
|
||||
errors, warnings = validate_state(state)
|
||||
for w in warnings:
|
||||
log(f"warning: {w}")
|
||||
if errors:
|
||||
die("refusing to write an invalid state (nothing changed):\n ✖ "
|
||||
+ "\n ✖ ".join(errors))
|
||||
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, tmp = tempfile.mkstemp(dir=STATE_FILE.parent, prefix=".theme-state.", suffix=".json")
|
||||
try:
|
||||
@@ -116,6 +130,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:
|
||||
out = dict(base)
|
||||
for k, v in override.items():
|
||||
@@ -126,6 +173,152 @@ def deep_merge(base: dict, override: dict) -> dict:
|
||||
return out
|
||||
|
||||
|
||||
# ─── Schema validation ─────────────────────────────────────────────────────
|
||||
# The friendly-errors contract (with theme.nix, which enforces the same
|
||||
# rules at eval time): a hand-edited state file fails HERE, before it is
|
||||
# written, with the field, the problem, and the fix — never as a Nix
|
||||
# stack trace at rebuild. Hard errors cover the appearance schema the
|
||||
# modules consume; settings.* (menu-writer territory) and unknown keys
|
||||
# only warn, so custom keys and newer/older schemas keep working.
|
||||
|
||||
HEX_RE = re.compile(r"^#[0-9a-fA-F]{6}$")
|
||||
COLOR_ROLES = ("base", "mantle", "surface", "overlay", "text", "subtext",
|
||||
"muted", "accent", "accentAlt", "good", "warn", "bad")
|
||||
TOP_KEYS = {"version", "name", "slug", "mode", "wallpaper", "colors", "ansi",
|
||||
"fonts", "ui", "icons", "border", "settings"}
|
||||
UI_UINTS = ("gapsIn", "gapsOut", "borderSize", "rounding", "iconSize")
|
||||
UI_OPACITIES = ("activeOpacity", "inactiveOpacity", "terminalOpacity")
|
||||
UI_BOOLS = ("blur", "shadow")
|
||||
|
||||
|
||||
def _is_hex(v) -> bool:
|
||||
return isinstance(v, str) and bool(HEX_RE.match(v))
|
||||
|
||||
|
||||
def _is_num(v) -> bool:
|
||||
return isinstance(v, (int, float)) and not isinstance(v, bool)
|
||||
|
||||
|
||||
def validate_state(state) -> tuple:
|
||||
"""Return (errors, warnings) — friendly strings, one problem each."""
|
||||
errors, warnings = [], []
|
||||
|
||||
def err(field, problem, fix):
|
||||
errors.append(f"{field}: {problem}\n fix: {fix}")
|
||||
|
||||
if not isinstance(state, dict):
|
||||
err("top level", "must be a JSON object",
|
||||
"re-apply a preset: nomarchy-theme-sync apply boreal")
|
||||
return errors, warnings
|
||||
|
||||
for k in state:
|
||||
if k not in TOP_KEYS:
|
||||
warnings.append(f"unknown top-level key '{k}' — a typo? (it is ignored)")
|
||||
|
||||
for field, want in (("name", "the theme's display name"),
|
||||
("slug", "the theme's directory name"),
|
||||
("wallpaper", "a filename in the theme's backgrounds/ (or \"\")"),
|
||||
("icons", "an icon theme name (or \"\" for pick-by-mode)")):
|
||||
v = state.get(field)
|
||||
if v is not None and not isinstance(v, str):
|
||||
err(field, f"must be a string ({want}), got {v!r}", 'quote it, e.g. "boreal"')
|
||||
|
||||
mode = state.get("mode")
|
||||
if mode is not None and mode not in ("dark", "light"):
|
||||
err("mode", f'must be "dark" or "light", got {mode!r}', 'set it to "dark" or "light"')
|
||||
|
||||
colors = state.get("colors", {})
|
||||
if not isinstance(colors, dict):
|
||||
err("colors", "must be an object of palette roles",
|
||||
"re-apply a preset to restore the palette")
|
||||
else:
|
||||
for k, v in colors.items():
|
||||
if k not in COLOR_ROLES:
|
||||
warnings.append(f"colors.{k}: not a Nomarchy palette role (it is ignored)")
|
||||
elif not _is_hex(v):
|
||||
err(f"colors.{k}", f'must be "#RRGGBB", got {v!r}',
|
||||
'use a 6-digit hex color like "#7aa2f7"')
|
||||
|
||||
ansi = state.get("ansi")
|
||||
if ansi is not None and not (isinstance(ansi, list) and len(ansi) == 16
|
||||
and all(_is_hex(c) for c in ansi)):
|
||||
err("ansi", 'must be a list of exactly 16 "#RRGGBB" strings',
|
||||
"re-apply a preset to restore the terminal palette")
|
||||
|
||||
fonts = state.get("fonts", {})
|
||||
if isinstance(fonts, dict):
|
||||
for k in ("mono", "ui"):
|
||||
v = fonts.get(k)
|
||||
if v is not None and not isinstance(v, str):
|
||||
err(f"fonts.{k}", f"must be a font family name (string), got {v!r}",
|
||||
'quote it, e.g. "JetBrainsMono Nerd Font"')
|
||||
size = fonts.get("size")
|
||||
if size is not None and not (_is_num(size) and size > 0):
|
||||
err("fonts.size", f"must be a positive number (points), got {size!r}",
|
||||
"use a plain number like 11 (no quotes)")
|
||||
else:
|
||||
err("fonts", "must be an object", 're-add: {"mono": "...", "ui": "...", "size": 11}')
|
||||
|
||||
ui = state.get("ui", {})
|
||||
if isinstance(ui, dict):
|
||||
for k in UI_UINTS:
|
||||
v = ui.get(k)
|
||||
if v is not None and not (isinstance(v, int) and not isinstance(v, bool) and v >= 0):
|
||||
err(f"ui.{k}", f"must be a non-negative whole number (pixels), got {v!r}",
|
||||
"use a plain number like 12 (no quotes)")
|
||||
for k in UI_OPACITIES:
|
||||
v = ui.get(k)
|
||||
if v is not None and not (_is_num(v) and 0 <= v <= 1):
|
||||
err(f"ui.{k}", f"must be a number between 0 and 1, got {v!r}",
|
||||
"use e.g. 0.95 (no quotes)")
|
||||
for k in UI_BOOLS:
|
||||
v = ui.get(k)
|
||||
if v is not None and not isinstance(v, bool):
|
||||
err(f"ui.{k}", f"must be true or false, got {v!r}",
|
||||
"use true or false (no quotes)")
|
||||
else:
|
||||
err("ui", "must be an object", "re-apply a preset to restore the UI block")
|
||||
|
||||
border = state.get("border")
|
||||
if border is not None:
|
||||
if not isinstance(border, dict):
|
||||
err("border", "must be an object", 're-add: {"active": "accent", "inactive": "overlay"}')
|
||||
else:
|
||||
for k in ("active", "inactive"):
|
||||
v = border.get(k)
|
||||
if v is not None and not (isinstance(v, str)
|
||||
and (v in COLOR_ROLES or _is_hex(v))):
|
||||
err(f"border.{k}",
|
||||
f'must be a palette role ({", ".join(COLOR_ROLES)}) or "#RRGGBB", got {v!r}',
|
||||
'use e.g. "accent" or "#7aa2f7"')
|
||||
|
||||
settings = state.get("settings")
|
||||
if settings is not None and not isinstance(settings, dict):
|
||||
err("settings", "must be an object (the menu writes feature flags here)",
|
||||
're-add: "settings": {}')
|
||||
|
||||
slug = state.get("slug")
|
||||
if isinstance(slug, str) and slug and find_preset(slug) is None:
|
||||
warnings.append(f"slug '{slug}' matches no shipped/custom preset — "
|
||||
f"fine for a hand-rolled theme, but per-theme assets won't resolve")
|
||||
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def cmd_validate(_args) -> None:
|
||||
state = load_state() # dies with the friendly syntax error on bad JSON
|
||||
errors, warnings = validate_state(state)
|
||||
for w in warnings:
|
||||
print(f" ● {w}")
|
||||
if errors:
|
||||
print(f"nomarchy-theme-sync: {STATE_FILE} has "
|
||||
f"{len(errors)} problem(s):", file=sys.stderr)
|
||||
for e in errors:
|
||||
print(f" ✖ {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
log(f"{STATE_FILE} is valid ({len(warnings)} warning(s))")
|
||||
|
||||
|
||||
# ─── Font verification ────────────────────────────────────────────────────
|
||||
|
||||
def check_fonts(state: dict) -> None:
|
||||
@@ -165,12 +358,35 @@ def run_switch() -> None:
|
||||
# Persistent (timeout 0): stays up for the whole rebuild — replaced
|
||||
# in place by the success/failure notification below — so a multi-
|
||||
# minute switch never looks like it silently failed.
|
||||
notify("Applying theme — rebuilding the desktop…", persistent=True)
|
||||
notify("Applying changes — rebuilding the desktop…", persistent=True)
|
||||
result = subprocess.run(argv) # stream output to the caller's terminal
|
||||
if result.returncode != 0:
|
||||
notify("Theme rebuild FAILED — see terminal / journal", urgency="critical")
|
||||
die("rebuild failed (state file already updated; fix and re-run)")
|
||||
notify("Theme applied ✓")
|
||||
# BACKLOG #56: point at doctor, not a silent wall of Nix noise.
|
||||
notify(
|
||||
"Rebuild FAILED — scroll up for last lines; run nomarchy-doctor",
|
||||
urgency="critical",
|
||||
)
|
||||
die(
|
||||
"rebuild failed (state already written; fix and re-run). "
|
||||
"Diagnose: nomarchy-doctor. Recovery: docs/RECOVERY.md"
|
||||
)
|
||||
notify("Changes applied ✓")
|
||||
# Waybar runs from Hyprland's exec-once (not a systemd unit HM would
|
||||
# restart on switch), so nudge the running bar onto the freshly rebuilt
|
||||
# config/style. Under the nomarchy-waybar supervisor a plain kill IS a
|
||||
# clean restart with the new files — waybar's in-place SIGUSR2 reload
|
||||
# is what crashed the bar on hardware (double-reload race with
|
||||
# reload_style_on_change while the symlinks flip), so prefer the
|
||||
# restart whenever the supervisor is there to catch it; fall back to
|
||||
# SIGUSR2 for unsupervised/custom bars. No-ops if nothing is running.
|
||||
if shutil.which("pkill"):
|
||||
supervised = subprocess.run(
|
||||
["pgrep", "-f", "nomarchy-waybar"], capture_output=True
|
||||
).returncode == 0
|
||||
if supervised:
|
||||
subprocess.run(["pkill", "-x", "waybar"], check=False)
|
||||
else:
|
||||
subprocess.run(["pkill", "--signal", "SIGUSR2", "-x", "waybar"], check=False)
|
||||
|
||||
|
||||
# ─── Theme assets (wallpapers) ────────────────────────────────────────────
|
||||
@@ -257,10 +473,17 @@ def cmd_apply(args) -> None:
|
||||
die(f"unknown theme '{args.theme}' (try `nomarchy-theme-sync list`)")
|
||||
|
||||
preset = json.loads(preset_path.read_text())
|
||||
# Merge over current state: presets define palette/name/wallpaper,
|
||||
# user tweaks (gaps, fonts) outside the preset survive.
|
||||
state = deep_merge(load_state(), preset)
|
||||
# Merge over current state. Presets carry a full appearance block —
|
||||
# palette, border, fonts and ui — so a switch always replaces the last
|
||||
# theme's look (a theme can ship a bespoke font/opacity/geometry, e.g.
|
||||
# Boreal, without it leaking into the next). settings.* (feature state,
|
||||
# keyboard/monitor memory) live outside any preset and survive. A
|
||||
# per-key `set ui.<k>`/`fonts.<k>` tweak holds until the next apply.
|
||||
old = load_state()
|
||||
state = deep_merge(old, preset)
|
||||
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)}")
|
||||
check_fonts(state)
|
||||
if not args.no_switch:
|
||||
@@ -275,6 +498,7 @@ def cmd_set(args) -> None:
|
||||
except json.JSONDecodeError:
|
||||
value = args.value # bare string ("#7aa2f7", font names, paths)
|
||||
|
||||
was_on = auto_commit_enabled(state)
|
||||
node = state
|
||||
keys = args.path.split(".")
|
||||
for key in keys[:-1]:
|
||||
@@ -284,12 +508,18 @@ def cmd_set(args) -> None:
|
||||
node[keys[-1]] = value
|
||||
|
||||
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}")
|
||||
if args.path.startswith("fonts."):
|
||||
check_fonts(state)
|
||||
if not args.no_switch:
|
||||
run_switch()
|
||||
apply_wallpaper(state)
|
||||
# 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)
|
||||
|
||||
|
||||
def cmd_get(args) -> None:
|
||||
@@ -301,7 +531,10 @@ def cmd_get(args) -> None:
|
||||
node = node[key]
|
||||
except (KeyError, TypeError):
|
||||
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:
|
||||
print(json.dumps(state, indent=2))
|
||||
|
||||
@@ -357,6 +590,8 @@ def main() -> None:
|
||||
p.add_argument("path", nargs="?")
|
||||
p.set_defaults(func=cmd_get)
|
||||
|
||||
sub.add_parser("validate", help="check theme-state.json against the schema (read-only)").set_defaults(func=cmd_validate)
|
||||
|
||||
sub.add_parser("wallpaper", help="apply the current wallpaper via swww").set_defaults(func=cmd_wallpaper)
|
||||
|
||||
p = sub.add_parser("bg", help="wallpaper control: `bg next` cycles, `bg auto` resets")
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# My Nomarchy machine
|
||||
|
||||
This directory is the **single source of truth** for a Nomarchy machine
|
||||
flake. `nix flake init -t …` copies it as-is; **`nomarchy-install` copies
|
||||
the same files and only patches** install-time values (username, hostname,
|
||||
keyboard, detected hardware, password hash, …). Keep commented opt-ins and
|
||||
`home.packages` here — do not invent a second catalog in the installer.
|
||||
|
||||
1. Overwrite the placeholder hardware config with the real one:
|
||||
`nixos-generate-config --show-hardware-config > hardware-configuration.nix`
|
||||
2. Set `flake.nix` up **once** (Nomarchy repo URL, your username, optionally
|
||||
@@ -19,7 +25,7 @@
|
||||
Day-to-day:
|
||||
|
||||
```sh
|
||||
nomarchy-theme-sync list # 21 shipped presets
|
||||
nomarchy-theme-sync list # 24 shipped presets
|
||||
nomarchy-theme-sync apply gruvbox # writes state + home-manager switch
|
||||
nomarchy-theme-sync bg next # cycle wallpapers (instant, no rebuild)
|
||||
```
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# tested together upstream. This file is written once (by you or the
|
||||
# installer) and never hand-edited afterwards; your machine lives in
|
||||
# system.nix and home.nix. v1 is the release branch.
|
||||
# Installer copies this template and patches username + hardwareProfile.
|
||||
inputs.nomarchy.url = "git+https://git.bemagri.xyz/bernardo/nomarchy.git?ref=v1";
|
||||
|
||||
outputs = { nomarchy, ... }:
|
||||
|
||||
@@ -2,37 +2,69 @@
|
||||
# Ghostty, theming engine, Stylix) comes from homeModules.nomarchy;
|
||||
# tune it via the nomarchy.* options, add your own packages and
|
||||
# programs below.
|
||||
#
|
||||
# Source of truth for install and flake-init: nomarchy-install copies this
|
||||
# file and only patches keyboard layout/variant.
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
# Session keyboard — match services.xserver.xkb.* in system.nix (console
|
||||
# + LUKS prompt follow xkb via the distro module). Installer patches these.
|
||||
nomarchy.keyboard.layout = "us";
|
||||
nomarchy.keyboard.variant = "";
|
||||
|
||||
# ── Overrides (the desktop is on by default; tweak or opt out) ──────
|
||||
# nomarchy.terminal = "kitty"; # swap the default terminal
|
||||
# nomarchy.waybar.enable = false; # bring your own bar
|
||||
# nomarchy.stylix.enable = false; # opt out of GTK/Qt theming
|
||||
# nomarchy.keyboard.layout = "de"; # session keyboard (set the matching
|
||||
# # services.xserver.xkb.layout in
|
||||
# # system.nix too; the console + LUKS
|
||||
# # prompt follow it automatically)
|
||||
|
||||
# ── Opt-in features — uncomment and tweak to enable ─────────────────
|
||||
# nomarchy.nightlight = { # scheduled blue-light filter (hyprsunset)
|
||||
# enable = true;
|
||||
# enable = true; # declarative opt-in (or just enable it from
|
||||
# # the menu: System › Night light)
|
||||
# temperature = 4000; # night warmth in K — lower is warmer
|
||||
# sunset = "20: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
|
||||
# { name = "eDP-1"; position = "0x0"; }
|
||||
# { name = "HDMI-A-1"; position = "auto-right"; }
|
||||
# ]; # names from `hyprctl devices`
|
||||
#
|
||||
# nomarchy.launchOrFocus = [ # SUPER+<key> focuses the app's window,
|
||||
# { key = "B"; class = "firefox"; } # or launches it if none is open;
|
||||
# { key = "O"; class = "obsidian"; }# rows appear in the SUPER+? cheatsheet
|
||||
# ]; # (class from `hyprctl clients`)
|
||||
#
|
||||
# nomarchy.displayProfiles = { # named layouts for the SAME outputs —
|
||||
# docked = { # switch: System › Display › Profiles
|
||||
# monitors = [
|
||||
# { name = "eDP-1"; resolution = "disable"; }
|
||||
# { name = "DP-3"; position = "0x0"; }
|
||||
# { name = "DP-4"; position = "auto-right"; }
|
||||
# ];
|
||||
# workspaces = { "1" = "DP-3"; "9" = "DP-4"; }; # optional ws→output pins
|
||||
# };
|
||||
# undocked = [ { name = "eDP-1"; position = "0x0"; } ]; # bare list = monitors only
|
||||
# }; # applied instantly + remembered in the
|
||||
# # flake state (settings.displayProfile);
|
||||
# # the menu's Auto-switch row makes dock/
|
||||
# # undock pick the matching profile
|
||||
#
|
||||
# nomarchy.keyboard.devices = { # a specific keyboard's own layout
|
||||
# "keychron-keychron-k2" = { layout = "de"; };
|
||||
# };
|
||||
#
|
||||
# nomarchy.keyboard.layouts = [ "de" "fr" ]; # rofi-prompt for a layout when a
|
||||
# # new keyboard connects + remember it
|
||||
# nomarchy.keyboard.layouts = [ "de" "fr" ]; # rofi-prompt for a layout when a new
|
||||
# # keyboard connects + remember it in the
|
||||
# # flake state (graduates to .devices above)
|
||||
|
||||
# ── Application suite ───────────────────────────────────────────────
|
||||
# A starter complete-workstation set, installed for your user — yours to
|
||||
@@ -53,6 +85,8 @@
|
||||
inkscape # vector graphics
|
||||
mpv # media player
|
||||
amberol # music player (local library; streaming → spotify below)
|
||||
pwvucontrol # PipeWire volume mixer (right-click the Waybar volume)
|
||||
calcurse # TUI calendar — click the Waybar clock to open it
|
||||
|
||||
# ── More apps — uncomment to add ─────────────────────────────────
|
||||
# Web browsers
|
||||
@@ -74,7 +108,7 @@
|
||||
|
||||
# Office & documents
|
||||
# onlyoffice-desktopeditors # stronger MS-Office fidelity
|
||||
# kdePackages.okular # PDF viewer
|
||||
# kdePackages.okular # heavier PDF viewer (zathura ships by default)
|
||||
# xournalpp # PDF annotation
|
||||
# pdfarranger
|
||||
# calibre # ebook library
|
||||
@@ -105,6 +139,11 @@
|
||||
# kdePackages.kdenlive # video editor (pulls in KDE libs)
|
||||
# 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
|
||||
# vlc
|
||||
# spotify # (unfree)
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
# The distro itself comes from nomarchy.nixosModules.nomarchy — override
|
||||
# any of its defaults here with plain NixOS options (they use mkDefault),
|
||||
# or via the nomarchy.system.* toggles.
|
||||
#
|
||||
# Source of truth for install and flake-init: nomarchy-install copies this
|
||||
# file and only patches placeholders + the __NOMARCHY_INSTALLER__ region.
|
||||
{ pkgs, username, ... }:
|
||||
|
||||
{
|
||||
@@ -12,12 +15,28 @@
|
||||
time.timeZone = "UTC";
|
||||
i18n.defaultLocale = "en_US.UTF-8";
|
||||
|
||||
# One keyboard layout everywhere: xkb is the source of truth; the distro
|
||||
# defaults (console.useXkbConfig + systemd initrd) derive the virtual
|
||||
# console and the LUKS passphrase prompt from it. Match home.nix
|
||||
# nomarchy.keyboard.*.
|
||||
services.xserver.xkb.layout = "us";
|
||||
services.xserver.xkb.variant = "";
|
||||
|
||||
# Your login user — `username` flows in from flake.nix automatically.
|
||||
# The installer adds initialHashedPassword here; flake-init: set a password
|
||||
# (or users.mutableUsers) before first boot if needed.
|
||||
users.users.${username} = {
|
||||
isNormalUser = true;
|
||||
extraGroups = [ "wheel" "networkmanager" "video" "input" ];
|
||||
};
|
||||
|
||||
# __NOMARCHY_INSTALLER_BEGIN__
|
||||
# Installer fills this region (power, hardware, snapper, resume, autoLogin).
|
||||
# Flake-init: leave empty and use the commented catalog below, or enable
|
||||
# snapper when on BTRFS with a /.snapshots subvolume:
|
||||
# nomarchy.system.snapper.enable = true;
|
||||
# __NOMARCHY_INSTALLER_END__
|
||||
|
||||
# ── Overrides (uncomment to change) ─────────────────────────────────
|
||||
# nomarchy.system.greeter.enable = false; # bring your own login manager
|
||||
# environment.systemPackages = [ pkgs.htop ];
|
||||
@@ -31,7 +50,29 @@
|
||||
# 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)
|
||||
# i2c.enable = true; # /dev/i2c-* (RGB, sensors); ddcci is distro-default for external brightness
|
||||
# i2c.ddcci = true; # already mkDefault true — set false to opt out
|
||||
# };
|
||||
#
|
||||
# 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.podman.enable = true; # rootless containers (docker → podman)
|
||||
# nomarchy.services.flatpak.enable = true; # Flatpak + the Flathub remote
|
||||
|
||||
@@ -1,55 +1,61 @@
|
||||
{
|
||||
"version": 1,
|
||||
"name": "Tokyo Night",
|
||||
"slug": "tokyo-night",
|
||||
"name": "Boreal",
|
||||
"slug": "boreal",
|
||||
"mode": "dark",
|
||||
"wallpaper": "",
|
||||
"colors": {
|
||||
"base": "#1a1b26",
|
||||
"mantle": "#161720",
|
||||
"surface": "#32344a",
|
||||
"overlay": "#444b6a",
|
||||
"text": "#a9b1d6",
|
||||
"subtext": "#787c99",
|
||||
"muted": "#444b6a",
|
||||
"accent": "#7aa2f7",
|
||||
"accentAlt": "#ad8ee6",
|
||||
"good": "#9ece6a",
|
||||
"warn": "#e0af68",
|
||||
"bad": "#f7768e"
|
||||
"base": "#21272F",
|
||||
"mantle": "#1A1F26",
|
||||
"surface": "#303A46",
|
||||
"overlay": "#404D5C",
|
||||
"text": "#D3DAE0",
|
||||
"subtext": "#97A3B2",
|
||||
"muted": "#5E6A78",
|
||||
"accent": "#B79BE8",
|
||||
"accentAlt": "#86C7C0",
|
||||
"good": "#A3CF88",
|
||||
"warn": "#E6C384",
|
||||
"bad": "#E2818A"
|
||||
},
|
||||
"border": {
|
||||
"active": "accent",
|
||||
"inactive": "overlay"
|
||||
},
|
||||
"ansi": [
|
||||
"#32344a",
|
||||
"#f7768e",
|
||||
"#9ece6a",
|
||||
"#e0af68",
|
||||
"#7aa2f7",
|
||||
"#ad8ee6",
|
||||
"#449dab",
|
||||
"#787c99",
|
||||
"#444b6a",
|
||||
"#ff7a93",
|
||||
"#b9f27c",
|
||||
"#ff9e64",
|
||||
"#7da6ff",
|
||||
"#bb9af7",
|
||||
"#0db9d7",
|
||||
"#acb0d0"
|
||||
"#2E3742",
|
||||
"#E2818A",
|
||||
"#A3CF88",
|
||||
"#E6C384",
|
||||
"#86A9E0",
|
||||
"#B79BE8",
|
||||
"#86C7C0",
|
||||
"#D3DAE0",
|
||||
"#5E6A78",
|
||||
"#EC98A0",
|
||||
"#B4DB9C",
|
||||
"#EFCF98",
|
||||
"#9DBBEC",
|
||||
"#C6AEEF",
|
||||
"#99D6CF",
|
||||
"#E8EDF2"
|
||||
],
|
||||
"fonts": {
|
||||
"mono": "JetBrainsMono Nerd Font",
|
||||
"mono": "GeistMono Nerd Font",
|
||||
"ui": "Inter",
|
||||
"size": 11
|
||||
},
|
||||
"ui": {
|
||||
"gapsIn": 5,
|
||||
"gapsOut": 12,
|
||||
"borderSize": 2,
|
||||
"rounding": 10,
|
||||
"gapsIn": 6,
|
||||
"gapsOut": 14,
|
||||
"borderSize": 3,
|
||||
"rounding": 12,
|
||||
"iconSize": 44,
|
||||
"activeOpacity": 1.0,
|
||||
"inactiveOpacity": 0.95,
|
||||
"terminalOpacity": 0.96,
|
||||
"inactiveOpacity": 0.93,
|
||||
"terminalOpacity": 0.9,
|
||||
"blur": true,
|
||||
"shadow": true
|
||||
}
|
||||
},
|
||||
"settings": {}
|
||||
}
|
||||
|
||||
@@ -1,59 +1,61 @@
|
||||
{
|
||||
"version": 1,
|
||||
"name": "Tokyo Night",
|
||||
"slug": "tokyo-night",
|
||||
"name": "Boreal",
|
||||
"slug": "boreal",
|
||||
"mode": "dark",
|
||||
"wallpaper": "",
|
||||
"colors": {
|
||||
"base": "#1a1b26",
|
||||
"mantle": "#161720",
|
||||
"surface": "#32344a",
|
||||
"overlay": "#444b6a",
|
||||
"text": "#a9b1d6",
|
||||
"subtext": "#787c99",
|
||||
"muted": "#444b6a",
|
||||
"accent": "#7aa2f7",
|
||||
"accentAlt": "#ad8ee6",
|
||||
"good": "#9ece6a",
|
||||
"warn": "#e0af68",
|
||||
"bad": "#f7768e"
|
||||
"base": "#21272F",
|
||||
"mantle": "#1A1F26",
|
||||
"surface": "#303A46",
|
||||
"overlay": "#404D5C",
|
||||
"text": "#D3DAE0",
|
||||
"subtext": "#97A3B2",
|
||||
"muted": "#5E6A78",
|
||||
"accent": "#B79BE8",
|
||||
"accentAlt": "#86C7C0",
|
||||
"good": "#A3CF88",
|
||||
"warn": "#E6C384",
|
||||
"bad": "#E2818A"
|
||||
},
|
||||
"border": {
|
||||
"active": "accent",
|
||||
"inactive": "overlay"
|
||||
},
|
||||
"ansi": [
|
||||
"#32344a",
|
||||
"#f7768e",
|
||||
"#9ece6a",
|
||||
"#e0af68",
|
||||
"#7aa2f7",
|
||||
"#ad8ee6",
|
||||
"#449dab",
|
||||
"#787c99",
|
||||
"#444b6a",
|
||||
"#ff7a93",
|
||||
"#b9f27c",
|
||||
"#ff9e64",
|
||||
"#7da6ff",
|
||||
"#bb9af7",
|
||||
"#0db9d7",
|
||||
"#acb0d0"
|
||||
"#2E3742",
|
||||
"#E2818A",
|
||||
"#A3CF88",
|
||||
"#E6C384",
|
||||
"#86A9E0",
|
||||
"#B79BE8",
|
||||
"#86C7C0",
|
||||
"#D3DAE0",
|
||||
"#5E6A78",
|
||||
"#EC98A0",
|
||||
"#B4DB9C",
|
||||
"#EFCF98",
|
||||
"#9DBBEC",
|
||||
"#C6AEEF",
|
||||
"#99D6CF",
|
||||
"#E8EDF2"
|
||||
],
|
||||
"fonts": {
|
||||
"mono": "JetBrainsMono Nerd Font",
|
||||
"mono": "GeistMono Nerd Font",
|
||||
"ui": "Inter",
|
||||
"size": 11
|
||||
},
|
||||
"ui": {
|
||||
"gapsIn": 5,
|
||||
"gapsOut": 12,
|
||||
"borderSize": 2,
|
||||
"rounding": 10,
|
||||
"gapsIn": 6,
|
||||
"gapsOut": 14,
|
||||
"borderSize": 3,
|
||||
"rounding": 12,
|
||||
"iconSize": 44,
|
||||
"activeOpacity": 1.0,
|
||||
"inactiveOpacity": 0.95,
|
||||
"terminalOpacity": 0.96,
|
||||
"inactiveOpacity": 0.93,
|
||||
"terminalOpacity": 0.9,
|
||||
"blur": true,
|
||||
"shadow": true
|
||||
}
|
||||
},
|
||||
"settings": {}
|
||||
}
|
||||
|
||||
60
themes/boreal.json
Normal file
60
themes/boreal.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"version": 1,
|
||||
"name": "Boreal",
|
||||
"slug": "boreal",
|
||||
"mode": "dark",
|
||||
"wallpaper": "",
|
||||
"colors": {
|
||||
"base": "#21272F",
|
||||
"mantle": "#1A1F26",
|
||||
"surface": "#303A46",
|
||||
"overlay": "#404D5C",
|
||||
"text": "#D3DAE0",
|
||||
"subtext": "#97A3B2",
|
||||
"muted": "#5E6A78",
|
||||
"accent": "#B79BE8",
|
||||
"accentAlt": "#86C7C0",
|
||||
"good": "#A3CF88",
|
||||
"warn": "#E6C384",
|
||||
"bad": "#E2818A"
|
||||
},
|
||||
"border": {
|
||||
"active": "accent",
|
||||
"inactive": "overlay"
|
||||
},
|
||||
"fonts": {
|
||||
"mono": "GeistMono Nerd Font",
|
||||
"ui": "Inter",
|
||||
"size": 11
|
||||
},
|
||||
"ui": {
|
||||
"gapsIn": 6,
|
||||
"gapsOut": 14,
|
||||
"borderSize": 3,
|
||||
"rounding": 12,
|
||||
"iconSize": 44,
|
||||
"activeOpacity": 1.0,
|
||||
"inactiveOpacity": 0.93,
|
||||
"terminalOpacity": 0.90,
|
||||
"blur": true,
|
||||
"shadow": true
|
||||
},
|
||||
"ansi": [
|
||||
"#2E3742",
|
||||
"#E2818A",
|
||||
"#A3CF88",
|
||||
"#E6C384",
|
||||
"#86A9E0",
|
||||
"#B79BE8",
|
||||
"#86C7C0",
|
||||
"#D3DAE0",
|
||||
"#5E6A78",
|
||||
"#EC98A0",
|
||||
"#B4DB9C",
|
||||
"#EFCF98",
|
||||
"#9DBBEC",
|
||||
"#C6AEEF",
|
||||
"#99D6CF",
|
||||
"#E8EDF2"
|
||||
]
|
||||
}
|
||||
BIN
themes/boreal/backgrounds/aurora-boreal.png
Normal file
BIN
themes/boreal/backgrounds/aurora-boreal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 MiB |
42
themes/boreal/launcher.rasi
Normal file
42
themes/boreal/launcher.rasi
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Boreal — app-launcher override (themes/<slug>/launcher.rasi).
|
||||
* Installed to ~/.local/share/rofi/themes/launcher.rasi and used only by
|
||||
* `rofi -show drun` (the SUPER+Space / SUPER+D launcher). It inherits the
|
||||
* frosted aurora panel from the sibling rofi.rasi (this whole-swap theme,
|
||||
* which HM deploys under its source basename — rofi.rasi — not custom.rasi),
|
||||
* then reshapes the list into a 4×3 grid of large application icons.
|
||||
* Text menus keep the list — they don't use this theme.
|
||||
*/
|
||||
|
||||
@import "rofi.rasi"
|
||||
|
||||
window { width: 780px; }
|
||||
|
||||
listview {
|
||||
columns: 4;
|
||||
lines: 3;
|
||||
fixed-columns: true;
|
||||
spacing: 10px;
|
||||
padding: 6px 0px 0px 0px;
|
||||
/* Fill row-by-row so Down at a column's foot pages to the next screen
|
||||
instead of jumping to the top of the next column (rofi's default
|
||||
Vertical flow). Mirrors the theme-picker grid in rofi.nix. */
|
||||
flow: horizontal;
|
||||
}
|
||||
|
||||
element {
|
||||
orientation: vertical;
|
||||
padding: 16px 8px;
|
||||
spacing: 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
element-icon {
|
||||
size: 56px;
|
||||
horizontal-align: 0.5;
|
||||
}
|
||||
|
||||
element-text {
|
||||
horizontal-align: 0.5;
|
||||
vertical-align: 0.5;
|
||||
}
|
||||
BIN
themes/boreal/preview.png
Normal file
BIN
themes/boreal/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
122
themes/boreal/rofi.rasi
Normal file
122
themes/boreal/rofi.rasi
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Boreal — rofi whole-swap (themes/<slug>/rofi.rasi).
|
||||
* The base theme: a frosted aurora panel used by every menu (and, as a
|
||||
* list, the fallback launcher). The app launcher reshapes this into a
|
||||
* grid via themes/boreal/launcher.rasi, which @imports this file. The
|
||||
* `configuration {}` block is omitted on purpose — it comes from
|
||||
* modules/home/rofi.nix. Element structure mirrors the generated theme
|
||||
* so the theme-grid picker's per-invocation -theme-str still lays out.
|
||||
*/
|
||||
|
||||
* {
|
||||
base: #21272F;
|
||||
glassBg: #21272FE6;
|
||||
surface: #303A46;
|
||||
overlay: #404D5C;
|
||||
text: #D3DAE0;
|
||||
subtext: #97A3B2;
|
||||
muted: #5E6A78;
|
||||
accent: #B79BE8;
|
||||
accentAlt: #86C7C0;
|
||||
accentSoft: #B79BE83B;
|
||||
frostEdge: #86C7C05C;
|
||||
|
||||
font: "GeistMono Nerd Font 11";
|
||||
background-color: transparent;
|
||||
text-color: @text;
|
||||
}
|
||||
|
||||
window {
|
||||
background-color: @glassBg;
|
||||
border: 1px;
|
||||
border-color: @frostEdge;
|
||||
border-radius: 14px;
|
||||
width: 620px;
|
||||
location: center;
|
||||
anchor: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
mainbox {
|
||||
background-color: transparent;
|
||||
children: [ inputbar, message, listview ];
|
||||
spacing: 12px;
|
||||
}
|
||||
|
||||
inputbar {
|
||||
children: [ prompt, entry ];
|
||||
background-color: @surface;
|
||||
text-color: @text;
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
spacing: 10px;
|
||||
}
|
||||
|
||||
prompt { text-color: @accent; }
|
||||
|
||||
entry {
|
||||
text-color: @text;
|
||||
cursor: text;
|
||||
placeholder: "Search…";
|
||||
placeholder-color: @muted;
|
||||
}
|
||||
|
||||
listview {
|
||||
background-color: transparent;
|
||||
columns: 1;
|
||||
lines: 8;
|
||||
dynamic: true;
|
||||
scrollbar: true;
|
||||
spacing: 6px;
|
||||
padding: 4px 0px 0px 0px;
|
||||
}
|
||||
|
||||
element {
|
||||
background-color: transparent;
|
||||
text-color: @text;
|
||||
orientation: horizontal;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
spacing: 12px;
|
||||
}
|
||||
|
||||
element normal.normal,
|
||||
element alternate.normal {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Selected row lifts on a soft aurora-violet wash. */
|
||||
element selected.normal {
|
||||
background-color: @accentSoft;
|
||||
text-color: @text;
|
||||
}
|
||||
|
||||
element-icon {
|
||||
background-color: transparent;
|
||||
size: 30px;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
element-text {
|
||||
background-color: transparent;
|
||||
text-color: inherit;
|
||||
highlight: bold;
|
||||
vertical-align: 0.5;
|
||||
}
|
||||
|
||||
message { padding: 0px; }
|
||||
|
||||
textbox {
|
||||
background-color: @surface;
|
||||
text-color: @text;
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
scrollbar {
|
||||
width: 4px;
|
||||
handle-width: 4px;
|
||||
handle-color: @accent;
|
||||
background-color: @surface;
|
||||
border: 0px;
|
||||
}
|
||||
168
themes/boreal/waybar.css
Normal file
168
themes/boreal/waybar.css
Normal file
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Boreal — Waybar whole-swap (themes/<slug>/waybar.css).
|
||||
*
|
||||
* A theme waybar.css replaces the ENTIRE generated stylesheet and is read
|
||||
* raw (no palette is prepended), so this file MUST define its own colors.
|
||||
* Identity: a floating "aurora frost" bar — three frosted-glass pills
|
||||
* (launcher+clock · workspaces · status) over the wallpaper's blur. The
|
||||
* whole right side is ONE continuous pill: the system tray shares its
|
||||
* glass with the status modules, no separators, no boxes. Companion:
|
||||
* waybar.jsonc.
|
||||
*/
|
||||
|
||||
@define-color base #21272F;
|
||||
@define-color mantle #1A1F26;
|
||||
@define-color surface #303A46;
|
||||
@define-color overlay #404D5C;
|
||||
@define-color text #D3DAE0;
|
||||
@define-color subtext #97A3B2;
|
||||
@define-color muted #5E6A78;
|
||||
@define-color accent #B79BE8;
|
||||
@define-color accentAlt #86C7C0;
|
||||
@define-color good #A3CF88;
|
||||
@define-color warn #E6C384;
|
||||
@define-color bad #E2818A;
|
||||
|
||||
* {
|
||||
font-family: "GeistMono Nerd Font", "Symbols Nerd Font";
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* The bar itself is invisible — the pills float over the wallpaper. */
|
||||
window#waybar {
|
||||
background: transparent;
|
||||
color: @text;
|
||||
}
|
||||
|
||||
/* ── The three frosted-glass pills ────────────────────────────────────
|
||||
Global blur turns the translucent @base into frosted glass; a faint
|
||||
frost-cyan edge lifts each pill off the wallpaper. */
|
||||
.modules-left,
|
||||
.modules-center,
|
||||
.modules-right {
|
||||
background: alpha(@base, 0.72);
|
||||
border: 1px solid alpha(@accentAlt, 0.14);
|
||||
border-radius: 14px;
|
||||
padding: 0 6px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Every module sits ON its pill: transparent, so nothing boxes the tray
|
||||
off from the status icons — the seamless-cluster requirement. */
|
||||
#custom-nomarchy,
|
||||
#clock,
|
||||
#custom-recording,
|
||||
#custom-updates,
|
||||
#custom-doctor,
|
||||
#idle_inhibitor,
|
||||
#custom-nightlight,
|
||||
#language,
|
||||
#custom-vpn,
|
||||
#pulseaudio,
|
||||
#battery,
|
||||
#custom-powerprofile,
|
||||
#tray,
|
||||
#custom-notification,
|
||||
#custom-powermenu {
|
||||
background: transparent;
|
||||
color: alpha(@text, 0.9);
|
||||
padding: 0 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ── Launcher — the aurora-violet Nomarchy mark opens the pill ────────
|
||||
The mark lives at U+F000 in the dedicated "Nomarchy" font; pin the
|
||||
family or the Nerd Font's glass glyph at the same codepoint wins. */
|
||||
#custom-nomarchy {
|
||||
color: @accent;
|
||||
font-family: Nomarchy;
|
||||
font-size: 18px;
|
||||
padding: 0 12px 0 14px;
|
||||
}
|
||||
#custom-nomarchy:hover { color: @accentAlt; }
|
||||
|
||||
/* ── Clock — mono digits align; date recedes to frost-grey ───────────*/
|
||||
#clock {
|
||||
color: @text;
|
||||
font-weight: 600;
|
||||
padding-left: 4px;
|
||||
}
|
||||
#clock.date { color: @subtext; font-weight: 500; }
|
||||
|
||||
/* ── Workspaces — dots that bloom to an aurora pill when active ───────*/
|
||||
#workspaces { padding: 0 2px; }
|
||||
|
||||
#workspaces button {
|
||||
color: @muted;
|
||||
padding: 0 9px;
|
||||
margin: 3px 1px;
|
||||
border-radius: 9px;
|
||||
transition: color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
#workspaces button:hover {
|
||||
color: @text;
|
||||
background: alpha(@overlay, 0.5);
|
||||
}
|
||||
#workspaces button.active {
|
||||
color: @base;
|
||||
background: @accent;
|
||||
}
|
||||
#workspaces button.urgent {
|
||||
color: @base;
|
||||
background: @bad;
|
||||
}
|
||||
|
||||
/* ── Status semantics (neutral until a state fires) ──────────────────*/
|
||||
#custom-recording.recording { color: @bad; }
|
||||
#custom-updates.available { color: @accent; }
|
||||
#custom-doctor { color: @bad; }
|
||||
#idle_inhibitor.activated { color: @warn; }
|
||||
#custom-nightlight.on { color: @warn; }
|
||||
#custom-vpn.on { color: @good; }
|
||||
#pulseaudio.muted { color: @muted; }
|
||||
|
||||
#battery.charging { color: @good; }
|
||||
#battery.warning:not(.charging) { color: @warn; }
|
||||
#battery.critical:not(.charging) { color: @bad; }
|
||||
|
||||
/* Icon-only status modules carry no text, so they don't get the 13pt
|
||||
Pango icon span the icon+text modules (volume/battery/language) use —
|
||||
bump their font-size to match, or these glyphs read noticeably smaller
|
||||
than their neighbours. */
|
||||
#custom-recording,
|
||||
#custom-updates,
|
||||
#custom-doctor,
|
||||
#custom-nightlight,
|
||||
#custom-vpn,
|
||||
#custom-notification { font-size: 17px; }
|
||||
/* Speedometer + caffeine cup render small in their em box — a touch more. */
|
||||
#custom-powerprofile,
|
||||
#idle_inhibitor { font-size: 18px; }
|
||||
|
||||
#custom-notification.notification { color: @accent; }
|
||||
#custom-notification.dnd-none,
|
||||
#custom-notification.dnd-notification,
|
||||
#custom-notification.inhibited-none,
|
||||
#custom-notification.inhibited-notification { color: @muted; }
|
||||
|
||||
/* Tray shares the pill's glass — same transparent background, its own
|
||||
modest padding so nm-applet & friends breathe with the status icons. */
|
||||
#tray { padding: 0 6px; }
|
||||
#tray > .passive { -gtk-icon-effect: dim; }
|
||||
#tray > .needs-attention { -gtk-icon-effect: highlight; }
|
||||
|
||||
/* Power button closes the pill on the right; warms to alert on hover. */
|
||||
#custom-powermenu { color: @subtext; padding: 0 12px 0 8px; }
|
||||
#custom-powermenu:hover { color: @bad; }
|
||||
|
||||
/* ── Tooltips — frosted, frost-cyan edge ─────────────────────────────*/
|
||||
tooltip {
|
||||
background: alpha(@base, 0.96);
|
||||
border: 1px solid alpha(@accentAlt, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
tooltip label { color: @text; }
|
||||
179
themes/boreal/waybar.jsonc
Normal file
179
themes/boreal/waybar.jsonc
Normal file
@@ -0,0 +1,179 @@
|
||||
{
|
||||
"margin-top": 8,
|
||||
"margin-left": 14,
|
||||
"margin-right": 14,
|
||||
"margin-bottom": 0,
|
||||
"height": 38,
|
||||
"layer": "bottom",
|
||||
"position": "top",
|
||||
"spacing": 4,
|
||||
"reload_style_on_change": true,
|
||||
"modules-left": [
|
||||
"custom/nomarchy",
|
||||
"clock",
|
||||
"clock#date"
|
||||
],
|
||||
"modules-center": [
|
||||
"hyprland/workspaces"
|
||||
],
|
||||
"modules-right": [
|
||||
"custom/recording",
|
||||
"custom/updates",
|
||||
"custom/doctor",
|
||||
"idle_inhibitor",
|
||||
"custom/nightlight",
|
||||
"hyprland/language",
|
||||
"custom/vpn",
|
||||
"pulseaudio",
|
||||
"battery",
|
||||
"custom/powerprofile",
|
||||
"tray",
|
||||
"custom/notification",
|
||||
"custom/powermenu"
|
||||
],
|
||||
"custom/nomarchy": {
|
||||
"interval": "once",
|
||||
"format": "",
|
||||
"on-click": "nomarchy-menu",
|
||||
"tooltip-format": "Nomarchy menu"
|
||||
},
|
||||
"clock": {
|
||||
"format": "{:%H:%M}",
|
||||
"on-click": "nomarchy-calendar",
|
||||
"tooltip": true,
|
||||
"tooltip-format": "{:%Z (UTC%z)}\n<tt><small>{calendar}</small></tt>"
|
||||
},
|
||||
"clock#date": {
|
||||
"format": "{:%a %d %b}",
|
||||
"tooltip": false
|
||||
},
|
||||
"hyprland/workspaces": {
|
||||
"disable-scroll": true,
|
||||
"all-outputs": true,
|
||||
"on-click": "activate",
|
||||
"on-scroll-up": "hyprctl dispatch workspace r+1",
|
||||
"on-scroll-down": "hyprctl dispatch workspace r-1"
|
||||
},
|
||||
"custom/recording": {
|
||||
"return-type": "json",
|
||||
"interval": 10,
|
||||
"signal": 8,
|
||||
"exec": "nomarchy-record status",
|
||||
"on-click": "nomarchy-record stop"
|
||||
},
|
||||
"custom/updates": {
|
||||
"return-type": "json",
|
||||
"interval": 1800,
|
||||
"signal": 9,
|
||||
"exec": "nomarchy-updates status",
|
||||
"on-click": "sh -c '$TERMINAL -e nomarchy-updates upgrade'"
|
||||
},
|
||||
"custom/doctor": {
|
||||
"return-type": "json",
|
||||
"format": "{}",
|
||||
"interval": 300,
|
||||
"signal": 10,
|
||||
"exec": "nomarchy-doctor-status",
|
||||
"on-click": "nomarchy-menu doctor"
|
||||
},
|
||||
"idle_inhibitor": {
|
||||
"format": "{icon}",
|
||||
"format-icons": {
|
||||
"activated": " ",
|
||||
"deactivated": " "
|
||||
}
|
||||
},
|
||||
"custom/nightlight": {
|
||||
"return-type": "json",
|
||||
"interval": 3,
|
||||
"exec": "nomarchy-nightlight status",
|
||||
"on-click": "nomarchy-nightlight toggle"
|
||||
},
|
||||
"hyprland/language": {
|
||||
"format": "<span size='13pt'></span> {short}",
|
||||
"tooltip": false
|
||||
},
|
||||
"custom/vpn": {
|
||||
"return-type": "json",
|
||||
"interval": 5,
|
||||
"exec": "nomarchy-vpn-status",
|
||||
"on-click": "nomarchy-vpn"
|
||||
},
|
||||
"pulseaudio": {
|
||||
"scroll-step": 5,
|
||||
"format": "<span size='13pt'>{icon}</span> {volume}%",
|
||||
"format-bluetooth": "<span size='13pt'>{icon}</span> {volume}%",
|
||||
"format-bluetooth-muted": " {icon}",
|
||||
"format-muted": "<span size='13pt'></span> muted",
|
||||
"format-icons": {
|
||||
"headphone": "",
|
||||
"hands-free": "",
|
||||
"headset": "",
|
||||
"phone": "",
|
||||
"portable": "",
|
||||
"car": "",
|
||||
"default": [
|
||||
"",
|
||||
"",
|
||||
""
|
||||
]
|
||||
},
|
||||
"on-click": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle",
|
||||
"on-click-right": "pwvucontrol",
|
||||
"tooltip-format": "{desc} | {volume}%"
|
||||
},
|
||||
"battery": {
|
||||
"interval": 30,
|
||||
"states": {
|
||||
"warning": 25,
|
||||
"critical": 10
|
||||
},
|
||||
"format": "<span size='13pt'>{icon}</span> {capacity}%",
|
||||
"format-charging": "<span size='13pt'></span> {capacity}%",
|
||||
"format-plugged": "<span size='13pt'></span> {capacity}%",
|
||||
"format-icons": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"on-click": "nomarchy-menu powermgmt",
|
||||
"tooltip-format": "Battery status"
|
||||
},
|
||||
"custom/powerprofile": {
|
||||
"return-type": "json",
|
||||
"interval": 5,
|
||||
"exec": "nomarchy-powerprofile-status",
|
||||
"on-click": "nomarchy-menu powermgmt"
|
||||
},
|
||||
"tray": {
|
||||
"icon-size": 15,
|
||||
"spacing": 10
|
||||
},
|
||||
"custom/notification": {
|
||||
"format": "{icon}",
|
||||
"return-type": "json",
|
||||
"exec": "swaync-client -swb",
|
||||
"exec-if": "which swaync-client",
|
||||
"escape": true,
|
||||
"tooltip": true,
|
||||
"on-click": "swaync-client -t -sw",
|
||||
"on-click-right": "swaync-client -d -sw",
|
||||
"format-icons": {
|
||||
"none": "",
|
||||
"notification": "",
|
||||
"dnd-none": "",
|
||||
"dnd-notification": "",
|
||||
"inhibited-none": "",
|
||||
"inhibited-notification": "",
|
||||
"dnd-inhibited-none": "",
|
||||
"dnd-inhibited-notification": ""
|
||||
}
|
||||
},
|
||||
"custom/powermenu": {
|
||||
"format": "",
|
||||
"on-click": "nomarchy-menu power",
|
||||
"tooltip": false
|
||||
}
|
||||
}
|
||||
@@ -6,38 +6,55 @@
|
||||
"wallpaper": "",
|
||||
"colors": {
|
||||
"base": "#eff1f5",
|
||||
"mantle": "#cbcdd0",
|
||||
"surface": "#bcc0cc",
|
||||
"mantle": "#e6e9ef",
|
||||
"surface": "#bfc4d0",
|
||||
"overlay": "#acb0be",
|
||||
"text": "#4c4f69",
|
||||
"subtext": "#5c5f77",
|
||||
"muted": "#acb0be",
|
||||
"muted": "#8c8fa1",
|
||||
"accent": "#1e66f5",
|
||||
"accentAlt": "#ea76cb",
|
||||
"accentAlt": "#8839ef",
|
||||
"good": "#40a02b",
|
||||
"warn": "#df8e1d",
|
||||
"warn": "#d6860a",
|
||||
"bad": "#d20f39"
|
||||
},
|
||||
"border": {
|
||||
"active": "accent",
|
||||
"inactive": "overlay"
|
||||
},
|
||||
"fonts": {
|
||||
"mono": "JetBrainsMono Nerd Font",
|
||||
"ui": "Inter",
|
||||
"size": 11
|
||||
},
|
||||
"ui": {
|
||||
"gapsIn": 5,
|
||||
"gapsOut": 12,
|
||||
"borderSize": 2,
|
||||
"rounding": 10,
|
||||
"iconSize": 36,
|
||||
"activeOpacity": 1.0,
|
||||
"inactiveOpacity": 0.95,
|
||||
"terminalOpacity": 0.96,
|
||||
"blur": true,
|
||||
"shadow": true
|
||||
},
|
||||
"ansi": [
|
||||
"#bcc0cc",
|
||||
"#d20f39",
|
||||
"#40a02b",
|
||||
"#df8e1d",
|
||||
"#1e66f5",
|
||||
"#ea76cb",
|
||||
"#179299",
|
||||
"#5c5f77",
|
||||
"#acb0be",
|
||||
"#d20f39",
|
||||
"#40a02b",
|
||||
"#df8e1d",
|
||||
"#1e66f5",
|
||||
"#ea76cb",
|
||||
"#179299",
|
||||
"#6c6f85"
|
||||
"#acb0be",
|
||||
"#6c6f85",
|
||||
"#d20f39",
|
||||
"#40a02b",
|
||||
"#df8e1d",
|
||||
"#1e66f5",
|
||||
"#ea76cb",
|
||||
"#179299",
|
||||
"#bcc0cc"
|
||||
]
|
||||
}
|
||||
|
||||
BIN
themes/catppuccin-latte/preview.png
Normal file
BIN
themes/catppuccin-latte/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
@@ -22,6 +22,23 @@
|
||||
"active": "accent",
|
||||
"inactive": "overlay"
|
||||
},
|
||||
"fonts": {
|
||||
"mono": "JetBrainsMono Nerd Font",
|
||||
"ui": "Inter",
|
||||
"size": 11
|
||||
},
|
||||
"ui": {
|
||||
"gapsIn": 5,
|
||||
"gapsOut": 12,
|
||||
"borderSize": 2,
|
||||
"rounding": 10,
|
||||
"iconSize": 36,
|
||||
"activeOpacity": 1.0,
|
||||
"inactiveOpacity": 0.95,
|
||||
"terminalOpacity": 0.96,
|
||||
"blur": true,
|
||||
"shadow": true
|
||||
},
|
||||
"ansi": [
|
||||
"#45475a",
|
||||
"#f38ba8",
|
||||
|
||||
@@ -2,82 +2,82 @@
|
||||
theme[main_bg]="#1E1E2E"
|
||||
|
||||
# Main text color
|
||||
theme[main_fg]="#c6d0f5"
|
||||
theme[main_fg]="#cdd6f4"
|
||||
|
||||
# Title color for boxes
|
||||
theme[title]="#c6d0f5"
|
||||
theme[title]="#cdd6f4"
|
||||
|
||||
# Highlight color for keyboard shortcuts
|
||||
theme[hi_fg]="#8caaee"
|
||||
theme[hi_fg]="#89b4fa"
|
||||
|
||||
# Background color of selected item in processes box
|
||||
theme[selected_bg]="#51576d"
|
||||
theme[selected_bg]="#585b70"
|
||||
|
||||
# Foreground color of selected item in processes box
|
||||
theme[selected_fg]="#8caaee"
|
||||
theme[selected_fg]="#89b4fa"
|
||||
|
||||
# Color of inactive/disabled text
|
||||
theme[inactive_fg]="#838ba7"
|
||||
theme[inactive_fg]="#7f849c"
|
||||
|
||||
# Color of text appearing on top of graphs, i.e uptime and current network graph scaling
|
||||
theme[graph_text]="#f2d5cf"
|
||||
theme[graph_text]="#f5e0dc"
|
||||
|
||||
# Background color of the percentage meters
|
||||
theme[meter_bg]="#51576d"
|
||||
theme[meter_bg]="#585b70"
|
||||
|
||||
# Misc colors for processes box including mini cpu graphs, details memory graph and details status text
|
||||
theme[proc_misc]="#f2d5cf"
|
||||
theme[proc_misc]="#f5e0dc"
|
||||
|
||||
# CPU, Memory, Network, Proc box outline colors
|
||||
theme[cpu_box]="#ca9ee6" #Mauve
|
||||
theme[mem_box]="#a6d189" #Green
|
||||
theme[net_box]="#ea999c" #Maroon
|
||||
theme[proc_box]="#8caaee" #Blue
|
||||
theme[cpu_box]="#cba6f7" #Mauve
|
||||
theme[mem_box]="#a6e3a1" #Green
|
||||
theme[net_box]="#eba0ac" #Maroon
|
||||
theme[proc_box]="#89b4fa" #Blue
|
||||
|
||||
# Box divider line and small boxes line color
|
||||
theme[div_line]="#737994"
|
||||
theme[div_line]="#6c7086"
|
||||
|
||||
# Temperature graph color (Green -> Yellow -> Red)
|
||||
theme[temp_start]="#a6d189"
|
||||
theme[temp_mid]="#e5c890"
|
||||
theme[temp_end]="#e78284"
|
||||
theme[temp_start]="#a6e3a1"
|
||||
theme[temp_mid]="#f9e2af"
|
||||
theme[temp_end]="#f38ba8"
|
||||
|
||||
# CPU graph colors (Teal -> Lavender)
|
||||
theme[cpu_start]="#81c8be"
|
||||
theme[cpu_mid]="#85c1dc"
|
||||
theme[cpu_end]="#babbf1"
|
||||
theme[cpu_start]="#94e2d5"
|
||||
theme[cpu_mid]="#74c7ec"
|
||||
theme[cpu_end]="#b4befe"
|
||||
|
||||
# Mem/Disk free meter (Mauve -> Lavender -> Blue)
|
||||
theme[free_start]="#ca9ee6"
|
||||
theme[free_mid]="#babbf1"
|
||||
theme[free_end]="#8caaee"
|
||||
theme[free_start]="#cba6f7"
|
||||
theme[free_mid]="#b4befe"
|
||||
theme[free_end]="#89b4fa"
|
||||
|
||||
# Mem/Disk cached meter (Sapphire -> Lavender)
|
||||
theme[cached_start]="#85c1dc"
|
||||
theme[cached_mid]="#8caaee"
|
||||
theme[cached_end]="#babbf1"
|
||||
theme[cached_start]="#74c7ec"
|
||||
theme[cached_mid]="#89b4fa"
|
||||
theme[cached_end]="#b4befe"
|
||||
|
||||
# Mem/Disk available meter (Peach -> Red)
|
||||
theme[available_start]="#ef9f76"
|
||||
theme[available_mid]="#ea999c"
|
||||
theme[available_end]="#e78284"
|
||||
theme[available_start]="#fab387"
|
||||
theme[available_mid]="#eba0ac"
|
||||
theme[available_end]="#f38ba8"
|
||||
|
||||
# Mem/Disk used meter (Green -> Sky)
|
||||
theme[used_start]="#a6d189"
|
||||
theme[used_mid]="#81c8be"
|
||||
theme[used_end]="#99d1db"
|
||||
theme[used_start]="#a6e3a1"
|
||||
theme[used_mid]="#94e2d5"
|
||||
theme[used_end]="#89dceb"
|
||||
|
||||
# Download graph colors (Peach -> Red)
|
||||
theme[download_start]="#ef9f76"
|
||||
theme[download_mid]="#ea999c"
|
||||
theme[download_end]="#e78284"
|
||||
theme[download_start]="#fab387"
|
||||
theme[download_mid]="#eba0ac"
|
||||
theme[download_end]="#f38ba8"
|
||||
|
||||
# Upload graph colors (Green -> Sky)
|
||||
theme[upload_start]="#a6d189"
|
||||
theme[upload_mid]="#81c8be"
|
||||
theme[upload_end]="#99d1db"
|
||||
theme[upload_start]="#a6e3a1"
|
||||
theme[upload_mid]="#94e2d5"
|
||||
theme[upload_end]="#89dceb"
|
||||
|
||||
# Process box color gradient for threads, mem and cpu usage (Sapphire -> Mauve)
|
||||
theme[process_start]="#85c1dc"
|
||||
theme[process_mid]="#babbf1"
|
||||
theme[process_end]="#ca9ee6"
|
||||
theme[process_start]="#74c7ec"
|
||||
theme[process_mid]="#b4befe"
|
||||
theme[process_end]="#cba6f7"
|
||||
|
||||
BIN
themes/catppuccin/preview.png
Normal file
BIN
themes/catppuccin/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -1,2 +0,0 @@
|
||||
@define-color foreground #cdd6f4;
|
||||
@define-color background #181824;
|
||||
@@ -7,11 +7,11 @@
|
||||
"colors": {
|
||||
"base": "#060B1E",
|
||||
"mantle": "#05091a",
|
||||
"surface": "#060B1E",
|
||||
"surface": "#0e1836",
|
||||
"overlay": "#6d7db6",
|
||||
"text": "#ffcead",
|
||||
"subtext": "#F99957",
|
||||
"muted": "#6d7db6",
|
||||
"muted": "#5568a0",
|
||||
"accent": "#7d82d9",
|
||||
"accentAlt": "#c89dc1",
|
||||
"good": "#92a593",
|
||||
@@ -22,6 +22,23 @@
|
||||
"active": "accent",
|
||||
"inactive": "overlay"
|
||||
},
|
||||
"fonts": {
|
||||
"mono": "JetBrainsMono Nerd Font",
|
||||
"ui": "Inter",
|
||||
"size": 11
|
||||
},
|
||||
"ui": {
|
||||
"gapsIn": 5,
|
||||
"gapsOut": 12,
|
||||
"borderSize": 2,
|
||||
"rounding": 10,
|
||||
"iconSize": 36,
|
||||
"activeOpacity": 1.0,
|
||||
"inactiveOpacity": 0.95,
|
||||
"terminalOpacity": 0.96,
|
||||
"blur": true,
|
||||
"shadow": true
|
||||
},
|
||||
"ansi": [
|
||||
"#060B1E",
|
||||
"#ED5B5A",
|
||||
|
||||
BIN
themes/ethereal/preview.png
Normal file
BIN
themes/ethereal/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
@@ -11,7 +11,7 @@
|
||||
"overlay": "#475258",
|
||||
"text": "#d3c6aa",
|
||||
"subtext": "#d3c6aa",
|
||||
"muted": "#475258",
|
||||
"muted": "#59646a",
|
||||
"accent": "#7fbbb3",
|
||||
"accentAlt": "#d699b6",
|
||||
"good": "#a7c080",
|
||||
@@ -22,6 +22,23 @@
|
||||
"active": "accent",
|
||||
"inactive": "overlay"
|
||||
},
|
||||
"fonts": {
|
||||
"mono": "JetBrainsMono Nerd Font",
|
||||
"ui": "Inter",
|
||||
"size": 11
|
||||
},
|
||||
"ui": {
|
||||
"gapsIn": 5,
|
||||
"gapsOut": 12,
|
||||
"borderSize": 2,
|
||||
"rounding": 10,
|
||||
"iconSize": 36,
|
||||
"activeOpacity": 1.0,
|
||||
"inactiveOpacity": 0.95,
|
||||
"terminalOpacity": 0.96,
|
||||
"blur": true,
|
||||
"shadow": true
|
||||
},
|
||||
"ansi": [
|
||||
"#475258",
|
||||
"#e67e80",
|
||||
|
||||
@@ -22,7 +22,7 @@ theme[selected_bg]="#3d484d"
|
||||
theme[selected_fg]="#dbbc7f"
|
||||
|
||||
# Color of inactive/disabled text
|
||||
theme[inactive_fg]="#2d353b"
|
||||
theme[inactive_fg]="#59646a"
|
||||
|
||||
# Color of text appearing on top of graphs, i.e uptime and current network graph scaling
|
||||
theme[graph_text]="#d3c6aa"
|
||||
|
||||
BIN
themes/everforest/preview.png
Normal file
BIN
themes/everforest/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
60
themes/executive-slate.json
Normal file
60
themes/executive-slate.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"version": 1,
|
||||
"name": "Executive Slate",
|
||||
"slug": "executive-slate",
|
||||
"mode": "dark",
|
||||
"wallpaper": "",
|
||||
"colors": {
|
||||
"base": "#191C24",
|
||||
"mantle": "#111319",
|
||||
"surface": "#2A3040",
|
||||
"overlay": "#3E4759",
|
||||
"text": "#D7DDEA",
|
||||
"subtext": "#9AA5BB",
|
||||
"muted": "#5C6578",
|
||||
"accent": "#5A8AE6",
|
||||
"accentAlt": "#86A9F2",
|
||||
"good": "#6FAE72",
|
||||
"warn": "#DBA85A",
|
||||
"bad": "#D06B71"
|
||||
},
|
||||
"border": {
|
||||
"active": "accent",
|
||||
"inactive": "overlay"
|
||||
},
|
||||
"fonts": {
|
||||
"mono": "JetBrainsMono Nerd Font",
|
||||
"ui": "Inter",
|
||||
"size": 11
|
||||
},
|
||||
"ui": {
|
||||
"gapsIn": 5,
|
||||
"gapsOut": 12,
|
||||
"borderSize": 2,
|
||||
"rounding": 10,
|
||||
"iconSize": 36,
|
||||
"activeOpacity": 1.0,
|
||||
"inactiveOpacity": 0.95,
|
||||
"terminalOpacity": 0.96,
|
||||
"blur": true,
|
||||
"shadow": true
|
||||
},
|
||||
"ansi": [
|
||||
"#2A3040",
|
||||
"#D06B71",
|
||||
"#6FAE72",
|
||||
"#DBA85A",
|
||||
"#5A8AE6",
|
||||
"#8C79D6",
|
||||
"#55A5AE",
|
||||
"#D7DDEA",
|
||||
"#5C6578",
|
||||
"#DE7C82",
|
||||
"#82BE85",
|
||||
"#E7BB6E",
|
||||
"#86A9F2",
|
||||
"#A594E8",
|
||||
"#6FBEC6",
|
||||
"#F0F3F9"
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user