Compare commits
267 Commits
v1
...
53f75f6e1b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53f75f6e1b | ||
|
|
656ebc9735 | ||
|
|
4ebd6770ec | ||
|
|
4c656b1e73 | ||
|
|
5ef66c242e | ||
|
|
dafa83e922 | ||
|
|
c0fc16e25c | ||
|
|
37615c85a4 | ||
|
|
2fa5231215 | ||
|
|
808990592d | ||
|
|
49e0061dbb | ||
|
|
860c70466f | ||
|
|
c44616aeb0 | ||
|
|
429de59b52 | ||
|
|
79d73cd623 | ||
|
|
e841251399 | ||
|
|
cf97f5605d | ||
|
|
dc33694a56 | ||
|
|
7eb9b8ad4f | ||
|
|
aff50e6070 | ||
|
|
ba8963a385 | ||
|
|
2d0cb48d5c | ||
|
|
8c048a2692 | ||
|
|
858362cfcc | ||
|
|
2055842c33 | ||
|
|
59975a6a59 | ||
| 5c5e1f797c | |||
| 6356f9b408 | |||
| f0f568f8d5 | |||
| ac3b75b018 | |||
| b37a50c779 | |||
| d5ee59fcb1 | |||
| 8002757ca3 | |||
| c41c8abaa7 | |||
| 45c584db26 | |||
| 44264428af | |||
| 84c145467e | |||
| 0834038072 | |||
| 869ca6b16c | |||
| 1e1d568947 | |||
| d7427ae5da | |||
| e2ede3d813 | |||
| 3d5cb21302 | |||
| 910f357f08 | |||
| 3bc8a46927 | |||
| 79630314aa | |||
| b5263bcef8 | |||
| d547393e7b | |||
|
|
eb951569cf | ||
|
|
099d214529 | ||
|
|
2fff3ad628 | ||
|
|
3eeba0611d | ||
|
|
a95cf49ddf | ||
|
|
3760e39a54 | ||
|
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
102
.gitea/workflows/check.yml
Normal file
102
.gitea/workflows/check.yml
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# 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 — FUTURE #20, not NEXT) ─────────────────────
|
||||||
|
# Eval-only CI is the standing decision: Gitea act_runner stays docker
|
||||||
|
# compose (no /dev/kvm). Full checks.* VMs need a *separate* host with
|
||||||
|
# nix + KVM (label nix-kvm); see agent/BACKLOG.md FUTURE #20. Do NOT
|
||||||
|
# uncomment until that label is online — Gitea queues forever otherwise.
|
||||||
|
#
|
||||||
|
# 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
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,7 +1,3 @@
|
|||||||
# Study material from the previous iteration — not part of the flake.
|
|
||||||
# Delete the directory once everything worth porting has been ported.
|
|
||||||
old_distro/
|
|
||||||
|
|
||||||
# Nix build artifacts
|
# Nix build artifacts
|
||||||
result
|
result
|
||||||
result-*
|
result-*
|
||||||
|
|||||||
65
AGENTS.md
Normal file
65
AGENTS.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
This file is the entry point for **any** AI coding agent, whatever the
|
||||||
|
vendor or harness. Everything agents need lives in vendor-neutral,
|
||||||
|
git-tracked markdown (`agent/`, `docs/`); harness-specific config is a
|
||||||
|
thin *adapter* (see bottom) and never holds shared content.
|
||||||
|
|
||||||
|
## Where things live (read this first)
|
||||||
|
|
||||||
|
| Path | Role |
|
||||||
|
|------|------|
|
||||||
|
| **`agent/`** | Agent instructions + loop state — **only executable queue is `BACKLOG.md`** |
|
||||||
|
| **`agent/README.md`** | Map of the agent files |
|
||||||
|
| **`docs/VISION.md`** | Product themes toward **v1.0** (not a queue) |
|
||||||
|
| **`docs/ROADMAP.md`** | Design history + shipped log |
|
||||||
|
| **`docs/README.md`** | Full documentation map |
|
||||||
|
| **`.claude/`** | Claude Code adapter only (permissions, subagent defs) — 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
|
||||||
|
- **Before your first change, read `agent/VERIFICATION.md`** and follow
|
||||||
|
it — every change, however small, climbs its V0–V3 ladder. For theme
|
||||||
|
or visual work, also read `agent/THEME-DESIGN.md`; before any VM test,
|
||||||
|
`docs/TESTING.md`.
|
||||||
|
- **Honesty rule** (`agent/VERIFICATION.md`, `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`).
|
||||||
|
- **Sync sweep on completion** (`agent/LOOP.md` §5): when a task ships,
|
||||||
|
update or delete every cross-reference it made stale — BACKLOG
|
||||||
|
pitches/pointers, HARDWARE-QUEUE entries, docs — in the same commit.
|
||||||
|
No stale items, no unsynced information.
|
||||||
|
- **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 & escalation
|
||||||
|
|
||||||
|
Push mechanical work down to cheaper models; keep judgment on the
|
||||||
|
strongest model available. Capability tiers, standing roles (scout /
|
||||||
|
runner), and per-harness model mappings: **`agent/DELEGATION.md`**.
|
||||||
|
Work above your tier? Return it — don't burn tokens.
|
||||||
|
|
||||||
|
## Harness adapters
|
||||||
|
|
||||||
|
One directory per harness, holding only what that harness *requires* in
|
||||||
|
its own format (permissions, subagent/skill registration). Adapters point
|
||||||
|
into `agent/`; they never carry policy, queue items, or vision text.
|
||||||
|
|
||||||
|
| Harness | Adapter |
|
||||||
|
|---------|---------|
|
||||||
|
| Claude Code | `.claude/` — `CLAUDE.md` is a symlink to this file |
|
||||||
|
| others | add a sibling dir + a row here; keep it a thin shim |
|
||||||
217
README.md
217
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)
|
├── flake.nix # inputs + the downstream API (exports below)
|
||||||
├── lib.nix # nomarchy.lib.mkFlake — one-call downstream wrapper
|
├── lib.nix # nomarchy.lib.mkFlake — one-call downstream wrapper
|
||||||
├── theme-state.json # ★ THE single source of truth (git-tracked!)
|
├── 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.json # palette (required, works alone)
|
||||||
│ └── nord/ # assets (optional, fixed filenames)
|
│ └── nord/ # assets (optional, fixed filenames)
|
||||||
│ ├── backgrounds/ # wallpapers (auto-picked, SUPER+SHIFT+T cycles)
|
│ ├── 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
|
│ ├── keybinds.nix # single source: Hyprland binds + SUPER+? sheet
|
||||||
│ ├── swaync.nix # notifications, same JSON
|
│ ├── swaync.nix # notifications, same JSON
|
||||||
│ ├── idle.nix # hyprlock + hypridle, 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
|
│ ├── yazi.nix # flagship TUI file manager + plugins
|
||||||
│ ├── osd.nix # swayosd volume/brightness OSD
|
│ ├── osd.nix # swayosd volume/brightness OSD
|
||||||
│ ├── shell.nix # zsh + starship + bat/eza/zoxide
|
│ ├── 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)
|
│ └── live.nix # bootable live ISO (try the distro, no install)
|
||||||
├── pkgs/
|
├── pkgs/
|
||||||
│ ├── nomarchy-theme-sync/ # state writer + rebuild dispatcher (Python)
|
│ ├── nomarchy-theme-sync/ # state writer + rebuild dispatcher (Python)
|
||||||
│ └── nomarchy-install/ # live-ISO installer (gum + disko + mkFlake)
|
│ ├── nomarchy-install/ # live-ISO installer (gum + disko + mkFlake)
|
||||||
├── templates/downstream/ # `nix flake init -t` starter for users
|
│ ├── nomarchy-doctor/ # read-only health sheet, one command per failure
|
||||||
├── docs/TESTING.md # how to verify changes (incl. AI-agent rules)
|
│ ├── nomarchy-control-center/ # TUI settings front-end over nomarchy-theme-sync (gum)
|
||||||
├── docs/OVERRIDES.md # how downstream users override defaults
|
│ └── nomarchy-battery-notify/ # low-battery toast watcher backing battery-notify.nix
|
||||||
├── docs/ROADMAP.md # forward-looking plans + shipped-fixes log
|
├── 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 instructions + loop state — map: agent/README.md
|
||||||
|
│ # BACKLOG (executable queue), LOOP, VERIFICATION, …
|
||||||
|
├── AGENTS.md # agent entry, any vendor (CLAUDE.md symlinks here)
|
||||||
|
├── .claude/ # Claude Code adapter: permissions + subagent defs
|
||||||
└── tools/ # maintainer-only
|
└── tools/ # maintainer-only
|
||||||
├── import-palettes.py # converts old-distro themes → JSON + assets
|
├── import-palettes.py # converts old-distro themes → JSON + assets
|
||||||
├── test-live-iso.sh # build the ISO + boot it in QEMU
|
├── test-live-iso.sh # build the ISO + boot it in QEMU
|
||||||
@@ -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**
|
installing to disk: pick a disk, LUKS2 full-disk encryption **by default**
|
||||||
(in exchange the desktop logs in passwordless — the passphrase already
|
(in exchange the desktop logs in passwordless — the passphrase already
|
||||||
gates the machine), user + hostname + timezone, hardware autodetection
|
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` —
|
then disko partitions (GPT + ESP + BTRFS subvolumes incl. `@snapshots` —
|
||||||
snapper timeline snapshots are on) and `nixos-install` runs — **without a
|
snapper timeline snapshots are on) and `nixos-install` runs — **without a
|
||||||
network** when the ISO was built from a clean tree (the target's
|
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"
|
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
|
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 —
|
`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:
|
later by the installer — and never hand-edited; it's a single call:
|
||||||
@@ -161,20 +182,35 @@ home-manager switch --flake .#me # desktop: every theme change, no
|
|||||||
Day-to-day you'll use the shipped shortcuts instead:
|
Day-to-day you'll use the shipped shortcuts instead:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sys-update # nix flake update + system rebuild (BTRFS snapshot first when available)
|
nomarchy-pull # nix flake update — refresh inputs (nomarchy, nixpkgs, …);
|
||||||
home-update # home-manager switch (no flake update, no sudo)
|
# optional git pull only if ~/.nomarchy tracks a remote
|
||||||
|
nomarchy-rebuild # system rebuild against the CURRENT lock (sudo inside;
|
||||||
|
# BTRFS snapshot first when snapper is on; nvd diff after)
|
||||||
|
nomarchy-home # home-manager switch — desktop only, no sudo, no lock bump
|
||||||
```
|
```
|
||||||
|
|
||||||
**Order matters when pulling distro updates.** `home-update` does *not*
|
`~/.nomarchy` is **your machine flake** (`system.nix` / `home.nix`). Newer
|
||||||
touch the lock — it rebuilds the desktop against the **current**
|
Nomarchy code arrives when `flake.lock` updates the `nomarchy` input
|
||||||
`flake.lock`. A new Nomarchy revision (new keybinds, theming, modules)
|
(`nomarchy-pull`), not by `git pull` of that directory (many installs have
|
||||||
arrives only when the lock is updated, which `sys-update` does
|
no remote there — only local commits from auto-commit).
|
||||||
(`nix flake update`). So to pull an update that affects the desktop layer:
|
|
||||||
run `sys-update` **first** (updates the lock + rebuilds the system), **then**
|
| When | Run |
|
||||||
`home-update` (re-applies the desktop against the new lock). Doing them in
|
|---|---|
|
||||||
the other order rebuilds the desktop against the *old* inputs and silently
|
| Pull newer Nomarchy / nixpkgs / inputs | `nomarchy-pull` |
|
||||||
skips the new home-side changes. After a home-side keybind/config change,
|
| You changed `system.nix` (or after pull) | `nomarchy-rebuild` |
|
||||||
also `hyprctl reload` (or relogin) so the running session re-reads it.
|
| You changed `home.nix` / theme / desktop | `nomarchy-home` |
|
||||||
|
| Full upgrade (inputs + both layers) | `nomarchy-pull && nomarchy-rebuild && nomarchy-home` |
|
||||||
|
|
||||||
|
**Order matters for distro updates.** `nomarchy-home` rebuilds against the
|
||||||
|
**current** `flake.lock` — it never updates inputs. A new Nomarchy revision
|
||||||
|
lands only when the lock is updated (`nomarchy-pull`). So for an upstream
|
||||||
|
desktop change: **pull → rebuild → home**. Home before pull rebuilds against
|
||||||
|
the old inputs and silently skips new home-side changes. After a home-side
|
||||||
|
keybind/config change, also `hyprctl reload` (or relogin) so the session
|
||||||
|
re-reads it.
|
||||||
|
|
||||||
|
Legacy aliases still work: `sys-update` → pull+rebuild, `sys-rebuild` →
|
||||||
|
`nomarchy-rebuild`, `home-update` → `nomarchy-home`.
|
||||||
|
|
||||||
Override anything via the `nomarchy.*` surface or plain NixOS/HM options:
|
Override anything via the `nomarchy.*` surface or plain NixOS/HM options:
|
||||||
appearance (gaps/colors/fonts) changes through `nomarchy-theme-sync`,
|
appearance (gaps/colors/fonts) changes through `nomarchy-theme-sync`,
|
||||||
@@ -182,6 +218,13 @@ behaviour (input/misc/monitor/chrome) is `mkDefault` so a plain `home.nix`
|
|||||||
assignment wins, and bind/exec-once lists concatenate. Full guide with
|
assignment wins, and bind/exec-once lists concatenate. Full guide with
|
||||||
examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
||||||
|
|
||||||
|
**Where each option goes.** `nomarchy.system.*`, `nomarchy.hardware.*`, and
|
||||||
|
`nomarchy.services.*` are NixOS options — set them in `system.nix`. Everything
|
||||||
|
else under `nomarchy.*` is a Home Manager option — set it in `home.nix`. The
|
||||||
|
two tables below are split along exactly that line.
|
||||||
|
|
||||||
|
**`home.nix`** (Home Manager — the desktop):
|
||||||
|
|
||||||
| Option | Default | Purpose |
|
| Option | Default | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `nomarchy.stateFile` | — (required) | Path to your theme-state.json |
|
| `nomarchy.stateFile` | — (required) | Path to your theme-state.json |
|
||||||
@@ -190,12 +233,14 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
|||||||
| `nomarchy.keyboard.variant` | `""` | XKB variant for the session |
|
| `nomarchy.keyboard.variant` | `""` | XKB variant for the session |
|
||||||
| `nomarchy.keyboard.devices` | `{}` | Per-device layout overrides (Hyprland `device` blocks keyed by `hyprctl devices` name) — e.g. an external keyboard with its own layout/variant |
|
| `nomarchy.keyboard.devices` | `{}` | Per-device layout overrides (Hyprland `device` blocks keyed by `hyprctl devices` name) — e.g. an external keyboard with its own layout/variant |
|
||||||
| `nomarchy.keyboard.layouts` | `[]` | Extra candidate layouts; when set, a watcher prompts (rofi) for a layout on a newly-connected keyboard and remembers it per-device |
|
| `nomarchy.keyboard.layouts` | `[]` | Extra candidate layouts; when set, a watcher prompts (rofi) for a layout on a newly-connected keyboard and remembers it per-device |
|
||||||
| `nomarchy.nightlight.enable` | `false` | Scheduled blue-light filter (hyprsunset) — warm at night (`.temperature`, default 4000K) between `.sunset`/`.sunrise`, no shift by day |
|
| `nomarchy.nightlight.enable` | `false` | Scheduled blue-light filter (hyprsunset) — warm at night (`.temperature`, default 4000K) between `.sunset`/`.sunrise`, no shift by day; set BOTH `.latitude`/`.longitude` for **geo mode** (wlsunset computes sunrise/sunset from your location; the fixed times are ignored); off by default — enable it from the menu (System › Night light; the first enable rebuilds), then toggle on/off instantly from the menu or the Waybar moon indicator (writes `settings.nightlight.on` in your flake, no rebuild) and it survives reboot, so it stays reproducible |
|
||||||
|
| `nomarchy.updates.enable` | `false` | Passive update awareness: a background check (`.interval`, default daily) that flags when flake inputs (nixpkgs, the Nomarchy input, …) are behind upstream — and, with Flatpak on, when apps have updates (`.flatpak`) — via a Waybar indicator + notification. Never changes anything; click the indicator to run the upgrade flow |
|
||||||
| `nomarchy.hyprland.enable` | `true` | Nomarchy's Hyprland config |
|
| `nomarchy.hyprland.enable` | `true` | Nomarchy's Hyprland config |
|
||||||
| `nomarchy.waybar.enable` | `true` | Nomarchy's Waybar |
|
| `nomarchy.waybar.enable` | `true` | Nomarchy's Waybar |
|
||||||
| `nomarchy.rofi.enable` | `true` | Themed rofi launcher + `nomarchy-menu` dispatcher |
|
| `nomarchy.rofi.enable` | `true` | Themed rofi launcher + `nomarchy-menu` dispatcher |
|
||||||
| `nomarchy.swaync.enable` | `true` | swaync notifications, themed |
|
| `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.yazi.enable` | `true` | yazi TUI file manager, themed + curated plugins |
|
||||||
| `nomarchy.osd.enable` | `true` | swayosd on-screen display for volume/brightness/mute |
|
| `nomarchy.osd.enable` | `true` | swayosd on-screen display for volume/brightness/mute |
|
||||||
| `nomarchy.shell.enable` | `true` | zsh + starship prompt + bat/eza/zoxide (zsh is the default login shell) |
|
| `nomarchy.shell.enable` | `true` | zsh + starship prompt + bat/eza/zoxide (zsh is the default login shell) |
|
||||||
@@ -205,21 +250,52 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
|||||||
| `nomarchy.fastfetch.enable` | `true` | fastfetch fronted by the themed Nomarchy logo |
|
| `nomarchy.fastfetch.enable` | `true` | fastfetch fronted by the themed Nomarchy logo |
|
||||||
| `nomarchy.keys.enable` | `true` | gpg-agent fronting SSH + pinentry-qt |
|
| `nomarchy.keys.enable` | `true` | gpg-agent fronting SSH + pinentry-qt |
|
||||||
| `nomarchy.displays.enable` | `true` | nwg-displays interactive monitor arranger (helper for `nomarchy.monitors`) |
|
| `nomarchy.displays.enable` | `true` | nwg-displays interactive monitor arranger (helper for `nomarchy.monitors`) |
|
||||||
|
| `nomarchy.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.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.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.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.fileManager.enable` | `true` | Thunar GUI + gvfs/tumbler/udisks2 (the "open folder" handler) |
|
||||||
| `nomarchy.system.greeter.enable` | `true` | greetd/tuigreet |
|
| `nomarchy.system.greeter.enable` | `true` | greetd/tuigreet |
|
||||||
| `nomarchy.system.greeter.autoLogin` | `null` | Auto-login this user into Hyprland (installer sets it on LUKS machines) |
|
| `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.audio.enable` | `true` | Pipewire stack |
|
||||||
| `nomarchy.system.bluetooth.enable` | `true` | Bluetooth + blueman |
|
| `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.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.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.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.laptop` | `false` | Marks a laptop, gating battery-only features; the installer sets it when a battery is present |
|
||||||
| `nomarchy.system.power.thermal.enable` | `false` | thermald (Intel-only); the installer enables it on a GenuineIntel CPU |
|
| `nomarchy.system.power.thermal.enable` | `false` | thermald (Intel-only); the installer enables it on a GenuineIntel CPU |
|
||||||
| `nomarchy.system.power.batteryChargeLimit` | `null` | Stop charging at this % (e.g. `80`) where the hardware supports it; needs `power.laptop` |
|
| `nomarchy.system.power.batteryChargeLimit` | `null` | Stop charging at this % (e.g. `80`) where the hardware supports it; needs `power.laptop` |
|
||||||
| `nomarchy.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.syncthing.enable` | `false` | Opt-in: Syncthing file sync as the login user (GUI at `127.0.0.1:8384`) |
|
||||||
| `nomarchy.services.podman.enable` | `false` | Opt-in: rootless Podman (`docker` aliased to it) |
|
| `nomarchy.services.podman.enable` | `false` | Opt-in: rootless Podman (`docker` aliased to it) |
|
||||||
| `nomarchy.services.flatpak.enable` | `false` | Opt-in: Flatpak + the Flathub remote |
|
| `nomarchy.services.flatpak.enable` | `false` | Opt-in: Flatpak + the Flathub remote |
|
||||||
@@ -235,14 +311,33 @@ 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.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.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.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
|
Beyond the `nomarchy.*` surface, the system layer turns on the usual
|
||||||
desktop services with `lib.mkDefault` (override natively). One worth
|
desktop services with `lib.mkDefault` (override natively) and enforces
|
||||||
calling out: **`services.fwupd.enable`** is on by default for firmware
|
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,
|
updates via LVFS — it only refreshes metadata, never flashes on its own,
|
||||||
so run `fwupdmgr update` to apply. Disable with `services.fwupd.enable =
|
so run `fwupdmgr update` to apply; disable with `services.fwupd.enable =
|
||||||
false;` on machines without real firmware (VMs/headless).
|
false;` on machines without real firmware (VMs/headless). 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;`.
|
||||||
|
**`zramSwap`** is also on by default (zstd, 50% of RAM, priority 100):
|
||||||
|
day-to-day memory pressure pages into compressed RAM instead of the
|
||||||
|
disk, keeping the disk swapfile free for **hibernation** — the installer
|
||||||
|
creates a RAM-sized encrypted swapfile and wires `resume`, so
|
||||||
|
Power › Hibernate works out of the box (swap size 0 at install = no
|
||||||
|
swap). Opt out with `zramSwap.enable = false;`; to add hibernation to a
|
||||||
|
machine installed before it existed, follow
|
||||||
|
**[docs/MIGRATION.md](docs/MIGRATION.md)** § Enabling hibernation.
|
||||||
|
|
||||||
## 4. How theming works
|
## 4. How theming works
|
||||||
|
|
||||||
@@ -271,6 +366,31 @@ imperative; nothing in Nix consumes the path): applied at session start and
|
|||||||
after every switch via a tiny activation hook, cycled instantly with
|
after every switch via a tiny activation hook, cycled instantly with
|
||||||
`bg next`.
|
`bg next`.
|
||||||
|
|
||||||
|
### Config the menu writes (not just themes)
|
||||||
|
|
||||||
|
The in-flake-state model isn't only for appearance. **Feature toggles you flip
|
||||||
|
from the menu are written into a `settings.*` section of the *same* state file**
|
||||||
|
— git-tracked, reproducible, never stashed in `~/.local/state`. The menu is just
|
||||||
|
an ergonomic writer for your flake, so version-controlling your downstream
|
||||||
|
reproduces the machine, settings and all. Where a toggle can take effect without
|
||||||
|
a rebuild it does: the menu writes the key (`--no-switch`) and flips the running
|
||||||
|
service, which reads the *live* flake state at session start, so the choice is
|
||||||
|
both instant and survives reboot.
|
||||||
|
|
||||||
|
**Night light** is the first to use this — enable it from the menu (System ›
|
||||||
|
Night light; the first enable rebuilds to install hyprsunset), then on/off is
|
||||||
|
instant. Internally it's two keys: `settings.nightlight.installed` (sticky —
|
||||||
|
gates the unit, the first enable rebuilds) and `settings.nightlight.on` (the
|
||||||
|
instant on/off). Expect more `nomarchy.*` toggles to migrate to this pattern.
|
||||||
|
|
||||||
|
**Auto-commit (opt-in):** System › Auto-commit makes every `apply`/`set`
|
||||||
|
mutation also `git commit` theme-state.json in your flake — *only* that
|
||||||
|
file, so unrelated dirty work is never swept up — turning your settings
|
||||||
|
history into `git log`. Off by default; the toggle is instant (nothing in
|
||||||
|
Nix consumes the flag), the off-write is itself committed so history stays
|
||||||
|
consistent, and wallpaper cycling (`bg next`) is deliberately excluded —
|
||||||
|
the current wallpaper rides along with the next real commit.
|
||||||
|
|
||||||
### Per-theme app assets (`themes/<slug>/`)
|
### Per-theme app assets (`themes/<slug>/`)
|
||||||
|
|
||||||
Recoloring covers 95% of theming; the rest is one optional assets directory
|
Recoloring covers 95% of theming; the rest is one optional assets directory
|
||||||
@@ -284,29 +404,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) |
|
| `waybar.jsonc` | whole-swap for the bar *layout* (must be plain JSON) |
|
||||||
| `rofi.rasi` | **whole-swap**: replaces the generated launcher/menu theme entirely |
|
| `rofi.rasi` | **whole-swap**: replaces the generated launcher/menu theme entirely |
|
||||||
|
|
||||||
Six ported themes ship a `waybar.css` identity (catppuccin, lumon, nord,
|
Four themes ship a `waybar.css` identity (summer-day, summer-night,
|
||||||
retro-82, summer-day, summer-night). Custom user themes can live in
|
executive-slate, boreal). Custom user themes can live in
|
||||||
`$NOMARCHY_PATH/themes/` (preset lookup) and `nomarchy.themesDir` (eval-time
|
`$NOMARCHY_PATH/themes/` (preset lookup) and `nomarchy.themesDir` (eval-time
|
||||||
asset probe).
|
asset probe).
|
||||||
|
|
||||||
## 5. Day-to-day
|
## 5. Day-to-day
|
||||||
|
|
||||||
```sh
|
```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 apply kanagawa # whole desktop, one generation (~a switch)
|
||||||
nomarchy-theme-sync set ui.gapsOut 16 # tweak one knob (also 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 next # cycle wallpapers — instant, no rebuild
|
||||||
nomarchy-theme-sync bg auto # back to the theme's default wallpaper
|
nomarchy-theme-sync bg auto # back to the theme's default wallpaper
|
||||||
nomarchy-theme-sync get colors.accent
|
nomarchy-theme-sync get colors.accent
|
||||||
sys-update # update inputs + rebuild the system (snapshots first)
|
nomarchy-pull # flake input update (no rebuild)
|
||||||
home-update # rebuild just the desktop layer
|
nomarchy-rebuild # rebuild the system, current lock
|
||||||
|
nomarchy-home # rebuild just the desktop layer
|
||||||
|
nomarchy-doctor # read-only health sheet (also: menu › System › Doctor)
|
||||||
```
|
```
|
||||||
|
|
||||||
Keybinds: `SUPER+Return` terminal · `SUPER+D` launcher · `SUPER+T` theme
|
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+Space` launcher · `SUPER+T` theme
|
||||||
picker · `SUPER+SHIFT+T` next wallpaper · `SUPER+X` power menu ·
|
picker · `SUPER+SHIFT+T` next wallpaper · `SUPER+X` power menu ·
|
||||||
`SUPER+E` file manager (yazi) · `SUPER+N` notifications · `SUPER+CTRL+V`
|
`SUPER+E` file manager (yazi) · `SUPER+N` notifications · `SUPER+CTRL+V`
|
||||||
clipboard history · `SUPER+Q`
|
clipboard history · `SUPER+SHIFT+C` color picker (hyprpicker) · `SUPER+Q`
|
||||||
close · `SUPER+1..9` workspaces · `Print` region screenshot.
|
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
|
Shell aliases (zsh, gated on `nomarchy.shell.enable`) — `alias` lists them
|
||||||
all; the curated set:
|
all; the curated set:
|
||||||
@@ -324,9 +450,9 @@ gd gds # diff · diff --staged
|
|||||||
gl glg # log graph (last 20 · all branches)
|
gl glg # log graph (last 20 · all branches)
|
||||||
gp gpl gf # push · pull · fetch --all --prune
|
gp gpl gf # push · pull · fetch --all --prune
|
||||||
|
|
||||||
# nix (system/home rebuilds keep their full sys-update / home-update names)
|
# nix (lifecycle: nomarchy-pull / nomarchy-rebuild / nomarchy-home — full names)
|
||||||
ns nr # nix shell · nix run (e.g. ns nixpkgs#ripgrep)
|
ns nr # nix shell · nix run (e.g. ns nixpkgs#ripgrep)
|
||||||
nfu nfc # nix flake update · check
|
nfu nfc # nix flake update · check (prefer nomarchy-pull for day-to-day)
|
||||||
nsearch ngc # nix search nixpkgs · nix-collect-garbage -d
|
nsearch ngc # nix search nixpkgs · nix-collect-garbage -d
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
@@ -341,7 +467,10 @@ reload # exec zsh (reload the shell)
|
|||||||
- **New themed value:** add the key to `theme-state.json` and consume it in
|
- **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.
|
the Nix modules. One place — there is no second renderer to keep in sync.
|
||||||
- **Importing more old-distro palettes:**
|
- **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
|
- **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
|
needs the user to set `nomarchy.*` options (e.g. night light, per-device
|
||||||
keyboard layouts, monitor layout, power management), ship a **commented**
|
keyboard layouts, monitor layout, power management), ship a **commented**
|
||||||
@@ -357,6 +486,12 @@ reload # exec zsh (reload the shell)
|
|||||||
|
|
||||||
## Roadmap & known issues
|
## Roadmap & known issues
|
||||||
|
|
||||||
See **[docs/ROADMAP.md](docs/ROADMAP.md)** — forward-looking plans plus the
|
| Doc | Role |
|
||||||
log of shipped fixes. Kept out of the README so this stays a focused entry
|
|-----|------|
|
||||||
point.
|
| **[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.
|
||||||
|
|||||||
150
agent/BACKLOG.md
Normal file
150
agent/BACKLOG.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
### 76. Hibernation default — all agent work shipped `[blocked:hw]`
|
||||||
|
Hibernation + zram on by default (product intent, Bernardo 2026-07-10).
|
||||||
|
**All V0/V1/V2 slices done** (2026-07-10); only the V3 check remains — on a
|
||||||
|
machine **actually running Nomarchy** (the dev box runs Bernardo's own NixOS,
|
||||||
|
not Nomarchy, so it can't close it). Steps in HARDWARE-QUEUE. Design record in
|
||||||
|
**ROADMAP § Hibernation + zram by default**. Shipped:
|
||||||
|
- zram default (`modules/nixos/oom.nix`, zstd/50%/prio 100) — `checks.zram-swap`.
|
||||||
|
- Installer/template already default new installs to a RAM-sized encrypted
|
||||||
|
`@swap` swapfile + `resume_offset` wiring; `swapSize=0` = no-swap opt-out.
|
||||||
|
- `docs/MIGRATION.md` enable-hibernation runbook (existing machines).
|
||||||
|
- `rofi.nix` Power → Hibernate: kept unconditional, **notifies on failure**.
|
||||||
|
- `checks.hibernate` VM test (V2 PASS) — hibernate→resume→crash round-trip
|
||||||
|
(swap partition); zram (prio 100) doesn't swallow the image, disk carries it.
|
||||||
|
- `checks.hibernate-swapfile` VM test (V2 PASS) — the installer's btrfs `@swap`
|
||||||
|
**swapfile** setup: NOCOW `mkswapfile`, file-type swapon, a valid
|
||||||
|
`map-swapfile -r` offset the kernel accepts (`/sys/power/resume_offset`),
|
||||||
|
zram above it. (Bounded proof of the swapfile+offset the partition test
|
||||||
|
can't reach; a full runtime-swapfile hibernate isn't headless-reachable —
|
||||||
|
systemd `CanHibernate=no` for undeclared swap.)
|
||||||
|
|
||||||
|
**V3 remaining (narrowed):** the literal encrypted-swapfile power-cycle —
|
||||||
|
`Hibernate → power off → unlock LUKS → resume session` on the LUKS+@swap
|
||||||
|
layout — plus the no-swap notify in a real session. QEMU can't feed a
|
||||||
|
runtime-computed `resume_offset` into the boot cmdline, and the dev box runs
|
||||||
|
exactly this layout (`/proc/cmdline` verified) but isn't Nomarchy. Steps in
|
||||||
|
HARDWARE-QUEUE. Close #76 on PASS.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
- **MIPI/IPU software-ISP camera** support (no-UVC machines).
|
||||||
|
- **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).
|
||||||
|
|
||||||
|
## FUTURE (decided deferred — not the agent queue head)
|
||||||
|
|
||||||
|
Work we **intend** someday but explicitly **not** NEXT. Agents do not
|
||||||
|
pick these unless Bernardo promotes one into NEXT/NOW.
|
||||||
|
|
||||||
|
### 20. KVM runner → VM suite in CI `[human]`
|
||||||
|
**Status (2026-07-10):** keep **eval-only** CI on the current Gitea
|
||||||
|
stack (act_runner in docker-compose on the 4c/4 GB IONOS VPS). Nested
|
||||||
|
KVM + RAM headroom on that host are a poor fit next to Gitea; full
|
||||||
|
`checks.*` VMs stay local / promotion-time until a **separate**
|
||||||
|
KVM-capable machine exists.
|
||||||
|
|
||||||
|
**When ready:** register a second runner (host-mode nix + `/dev/kvm`,
|
||||||
|
label `nix-kvm` — not the existing docker eval runner), then uncomment
|
||||||
|
the `vm-checks` job in `.gitea/workflows/check.yml` (`runs-on: nix-kvm`,
|
||||||
|
`nix flake check` + toplevel/HM builds). Do not enable the job until
|
||||||
|
that label is online (Gitea queues forever otherwise).
|
||||||
|
|
||||||
|
### Formatter — adopt later `[human]`
|
||||||
|
**Intent:** add a Nix formatter (likely `nixfmt-rfc-style`) in a dedicated
|
||||||
|
pass: reformat the tree once, document in CONVENTIONS, optional CI
|
||||||
|
check. **Not** the queue head — no drive-by reformats until that pass.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- **NVIDIA first-class options** — **deferred past v1** (Bernardo
|
||||||
|
2026-07-10). Keep #59 commented install guidance; no
|
||||||
|
`nomarchy.hardware.nvidia.*` until a hybrid maintainer + queue.
|
||||||
|
|
||||||
|
### v1.0 pointer
|
||||||
|
|
||||||
|
See **VISION**. Open PROPOSED: NVIDIA wrappers deferred past v1; IR
|
||||||
|
portal (b)/(c) need T14s hardware (HARDWARE-QUEUE § T14s). Standing
|
||||||
|
calls: browser default = Chromium; power backend = PPD.
|
||||||
|
|
||||||
|
|
||||||
|
## Decisions `[human]`
|
||||||
|
|
||||||
|
Open calls only Bernardo can make; agents add options/evidence but never
|
||||||
|
decide. **Resolved** entries stay for history; agents treat them as closed.
|
||||||
|
|
||||||
|
### Resolved (2026-07-10)
|
||||||
|
|
||||||
|
- **Docs site vs Markdown-in-repo** — **markdown in-repo for now**
|
||||||
|
(`docs/`, README). A rendered docs site is FUTURE if wanted.
|
||||||
|
- **Default browser** — **ship Chromium** in
|
||||||
|
`templates/downstream/home.nix`; mime → `chromium-browser.desktop`.
|
||||||
|
Opt out: delete the line / override mime.
|
||||||
|
- **Default power backend** — **keep PPD** (`nomarchy.system.power.backend`
|
||||||
|
default). TLP remains the one-line opt-in. Rationale: stability + live
|
||||||
|
profile API for menu/Waybar; Omarchy’s TLP experiment reverted.
|
||||||
|
|
||||||
|
### Resolved (2026-07-10, more)
|
||||||
|
|
||||||
|
- **Formatter adoption** — **yes, but not now.** Tracked as FUTURE
|
||||||
|
(below). Nix-source style only (`nixfmt-rfc-style` or similar); one
|
||||||
|
bulk reformat + CI/check when promoted. Until then: hand-aligned
|
||||||
|
style per CONVENTIONS.
|
||||||
|
|
||||||
|
- **Hibernation** — **want by default** (product intent). Needs a
|
||||||
|
disk-backed swap (file or partition) sized for resume; not zram alone.
|
||||||
|
Implementation is a future/NEXT item when designed (installer +
|
||||||
|
resume device + encrypted-root story).
|
||||||
|
|
||||||
|
### Resolved (2026-07-10, #76 design)
|
||||||
|
|
||||||
|
- **Swap sizing** — **exactly RAM** (installer default, unchanged). Hibernate
|
||||||
|
image ≤ RAM; zram takes day-to-day paging. **`swapSize=0`** stays no-swap.
|
||||||
|
- **Migration** — **docs runbook** (`docs/MIGRATION.md`), not a tool.
|
||||||
|
- **No-swap Hibernate** — keep the menu row; **notify on failure**.
|
||||||
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.
|
||||||
112
agent/DELEGATION.md
Normal file
112
agent/DELEGATION.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# Delegation — capability tiers, roles, token economy
|
||||||
|
|
||||||
|
How to spend model capacity in this repo, for **any** agent harness.
|
||||||
|
Tasks are matched to capability *tiers*, not vendor model names; each
|
||||||
|
harness maps tiers to its own models (table at the bottom). GOALS says
|
||||||
|
what to build, CONVENTIONS how to write it — this says who does which
|
||||||
|
part.
|
||||||
|
|
||||||
|
## The dividing line: evidence vs judgment
|
||||||
|
|
||||||
|
Cheap models gather evidence; they never make verification claims or
|
||||||
|
design decisions. The following always stay on the **frontier** tier and
|
||||||
|
are never delegated downward:
|
||||||
|
|
||||||
|
- deciding what to test and the regression scope (VERIFICATION.md)
|
||||||
|
- interpreting an ambiguous or flaky failure
|
||||||
|
- viewing screenshots and judging visual quality — aesthetic judgment is
|
||||||
|
exactly what small models do badly, and it's load-bearing here
|
||||||
|
- writing non-trivial Nix (module structure, overlays, cross-cutting
|
||||||
|
refactors)
|
||||||
|
- the final diff review and report
|
||||||
|
|
||||||
|
Product calls ("finish vs quarantine", promoting PROPOSED items) sit
|
||||||
|
above even that: they belong to the human.
|
||||||
|
|
||||||
|
## Tiers
|
||||||
|
|
||||||
|
| Tier | Best for | Delegate to it? |
|
||||||
|
|------|----------|-----------------|
|
||||||
|
| **light** | bulk mechanical: search, summarize, audit sweeps, running the harness | freely — never for judgment |
|
||||||
|
| **standard** | well-specified scoped edits, routine research | when the spec is already written |
|
||||||
|
| **frontier** | design, novel code, ambiguous failures, taste | this is the loop's own tier |
|
||||||
|
|
||||||
|
Reasoning effort: default moderate; go maximum only for the hardest
|
||||||
|
calls. A frontier parent may spawn a frontier child for one hard call.
|
||||||
|
Work above your tier gets returned, not attempted.
|
||||||
|
|
||||||
|
## Standing roles
|
||||||
|
|
||||||
|
Two mechanical roles exist for any harness that supports subagents
|
||||||
|
(Claude Code implementations: `.claude/agents/`; other harnesses
|
||||||
|
implement the same contracts in their own format):
|
||||||
|
|
||||||
|
- **scout** (light, read-only): locate where things are defined, map
|
||||||
|
which files touch a subsystem, scan build logs / serial output for
|
||||||
|
error lines, docs-vs-code drift sweeps. Reports facts with paths and
|
||||||
|
line numbers, quotes the minimum snippet, says "not found" plainly —
|
||||||
|
never guesses, never recommends.
|
||||||
|
- **runner** (light, executes): builds, VM boots, screenshot capture,
|
||||||
|
the scripted `tools/` checks. Headless and unattended, every wait
|
||||||
|
bounded by a timeout. Returns commands, exit codes, wall time, and
|
||||||
|
artifact paths — never marks anything passed or verified; the caller
|
||||||
|
makes the verification claim.
|
||||||
|
|
||||||
|
Use them for pure information-gathering or pure execution instead of
|
||||||
|
pulling bulk (log files, wide scans) into the main context. When a
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Economy rules
|
||||||
|
|
||||||
|
- **Only delegate when writing the spec is cheaper than doing the
|
||||||
|
work** — a spawned agent starts cold and must re-derive context. A
|
||||||
|
one-file read is cheaper done directly.
|
||||||
|
- **Brief every child cold; point at the spec, don't restate it.** "The
|
||||||
|
spec is in `agent/BACKLOG.md` #NN — implement it" plus only the
|
||||||
|
*constraints* (scope files, branch, no-VM, no-push).
|
||||||
|
- **Match the tier to the task, not the prestige.** An item whose spec
|
||||||
|
is already written (exact files, exact fixes) is standard-tier work;
|
||||||
|
reserve frontier children for genuine multi-step reasoning. This is
|
||||||
|
the single biggest saving.
|
||||||
|
- The strong model writes the spec, reviews the result, and owns the
|
||||||
|
commit.
|
||||||
|
|
||||||
|
## Fanning out parallel work (V0/V1, no VM)
|
||||||
|
|
||||||
|
When several NEXT items are independent and don't need the VM, spread
|
||||||
|
them across worktree-isolated subagents in parallel:
|
||||||
|
|
||||||
|
- **Disjoint file lanes.** Partition items so no two agents touch the
|
||||||
|
same file (map the touched files first). 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.** Each agent works in an isolated
|
||||||
|
worktree, commits to its own branch, and **never pushes or touches
|
||||||
|
`main`/`v1`**. You review each diff, cherry-pick onto `main`, and do
|
||||||
|
the bookkeeping — a single landing agent can't race itself. Clean up
|
||||||
|
worktrees and branches after landing.
|
||||||
|
- **Lean on scriptable checks as primary evidence.** Where a
|
||||||
|
deterministic `tools/` check or `checks.*` guard already proves the
|
||||||
|
property, that near-free run *is* the V0/V1 evidence.
|
||||||
|
- **Batch V2 at the end, once.** Delegated visual/behavioural items come
|
||||||
|
back "V2 pending"; collect the landed changes and do **one** VM pass
|
||||||
|
covering all of them — the VM render + screenshot review is the most
|
||||||
|
expensive step in the loop; amortise it.
|
||||||
|
- **Re-verify on `main`, but leanly.** After landing, confirm the
|
||||||
|
agent's V0/V1 on the merged tree with a targeted build, not a full
|
||||||
|
re-run. Trust-but-spot-check scales; blind re-running doesn't.
|
||||||
|
|
||||||
|
The judgment list above still holds: *you* review every diff before it
|
||||||
|
lands.
|
||||||
|
|
||||||
|
## Per-harness model mapping
|
||||||
|
|
||||||
|
| Tier | Claude Code |
|
||||||
|
|------|-------------|
|
||||||
|
| light | `haiku` |
|
||||||
|
| standard | `sonnet` |
|
||||||
|
| frontier | the session's top model (`opus` and up) |
|
||||||
|
|
||||||
|
Other harnesses: add a column when one is actually used on this repo.
|
||||||
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.
|
||||||
270
agent/HARDWARE-QUEUE.md
Normal file
270
agent/HARDWARE-QUEUE.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# 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. Checked-off entries are **pruned**
|
||||||
|
on the next sync sweep (LOOP.md §5) — outcomes live in the journal/ROADMAP;
|
||||||
|
git history is the archive. Machines: the **AMD dev box** (Ryzen AI
|
||||||
|
laptop: AMD + fingerprint + NPU), the **Latitude 5310/5410** (Intel QA),
|
||||||
|
the **T14s** (webcam case).
|
||||||
|
|
||||||
|
## Any machine (dev box is fine)
|
||||||
|
|
||||||
|
- [ ] **#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) → `nomarchy-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 (charge-limit re-apply passed on `BAT0` — Latitude
|
||||||
|
2026-07-10).
|
||||||
|
- [ ] **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.
|
||||||
|
- [ ] **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.
|
||||||
|
- [ ] **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 was a real bug → BACKLOG item 26 (fix shipped; SUPER+?
|
||||||
|
cheatsheet PASS on Latitude 2026-07-10).
|
||||||
|
- [ ] **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.
|
||||||
|
- [ ] **Keyboard hotplug picker (re-verify after in-flake graduation)** —
|
||||||
|
plug an external keyboard post-login, pick a layout in rofi, confirm
|
||||||
|
it applies per-device only, persists in `settings.keyboard.devices`,
|
||||||
|
and graduates into a `device{}` block on the next rebuild.
|
||||||
|
- [ ] **Snapshots restore + rollback exercise** (residual — the
|
||||||
|
GUI/polkit half passed on hardware 2026-07-04, a47aa3a): in
|
||||||
|
`sudo nomarchy-snapshots`, restore a single file (`undochange`)
|
||||||
|
and walk a root-config rollback up to (or through) the
|
||||||
|
typed-`yes` gate.
|
||||||
|
- [ ] **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
|
||||||
|
`nomarchy-home`: 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; a link → **Chromium**
|
||||||
|
(`chromium-browser.desktop` — template default as of 2026-07-10).
|
||||||
|
- [ ] **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).
|
||||||
|
Exit-node rows now show "hostname — Country City" (2026-07-11,
|
||||||
|
header-offset parsing): confirm Mullvad-style nodes carry their
|
||||||
|
location, a locationless self-hosted node stays a bare hostname,
|
||||||
|
and picking either still sets the node (`tailscale status` shows it).
|
||||||
|
- [ ] **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.
|
||||||
|
- [ ] **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.
|
||||||
|
- [ ] **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.)
|
||||||
|
|
||||||
|
- [ ] **Audio opens in Amberol** (iteration #60, item 37) — after
|
||||||
|
`nomarchy-home`: 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.)
|
||||||
|
- [ ] **rofi menu polish** (iteration #58, item 39 + #56/#57 34/40) —
|
||||||
|
after `nomarchy-home`: (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).
|
||||||
|
- [ ] **Screenshot annotation (satty)** (iteration #73) — after `nomarchy-home`:
|
||||||
|
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.
|
||||||
|
- [ ] **#76 hibernate→resume on a real Nomarchy install** — the V2 VM test
|
||||||
|
proves the resume mechanism + zram coexistence, but QEMU can't feed a
|
||||||
|
boot-time `resume_offset` for a btrfs *swapfile* nor drive the LUKS
|
||||||
|
initrd unlock, so this triple needs hardware. **Requires a machine
|
||||||
|
actually running Nomarchy** (installer default swap = RAM), NOT the dev
|
||||||
|
box — that box runs Bernardo's own NixOS (TuringMachine, 25.11); its
|
||||||
|
layout resumes fine (verified 2026-07-10) but that only corroborates
|
||||||
|
the *layout*, not Nomarchy's oom.nix/patch-template/rofi wiring.
|
||||||
|
Candidate: a fresh Nomarchy install on a Latitude, or the dev box
|
||||||
|
*after* migrating to Nomarchy (docs/MIGRATION.md).
|
||||||
|
1. Confirm the wiring on the Nomarchy box: `swapon --show` lists
|
||||||
|
`/swap/swapfile` (Type file) **and** `/dev/zram0` at priority 100;
|
||||||
|
`cat /proc/cmdline` shows `resume_offset=…` + `resume=` the btrfs UUID.
|
||||||
|
2. Open apps, note an unsaved in-RAM state. `nomarchy-menu → Power →
|
||||||
|
Hibernate` (or `systemctl hibernate`). Machine writes image, **powers
|
||||||
|
off**.
|
||||||
|
3. Power on: unlock LUKS **once** in the themed prompt; land back in the
|
||||||
|
**same session** — state restored, not a fresh login.
|
||||||
|
4. **Pass** = session restored after full power-off + single unlock.
|
||||||
|
Fail (fresh boot / stuck initrd) → NOW bug in BACKLOG (capture
|
||||||
|
`journalctl -b -1 | grep -i -E 'hibernat|resume|swap'`).
|
||||||
|
- [ ] **#76 no-swap Hibernate notify** — on a **Nomarchy** machine installed
|
||||||
|
with `swap = 0` (or temporarily `sudo swapoff -a` on one): `nomarchy-menu
|
||||||
|
→ Power → Hibernate` must surface a desktop notification ("Couldn't
|
||||||
|
hibernate — likely no swap is configured. See docs/MIGRATION.md →
|
||||||
|
Enabling hibernation."), **not** a silent no-op. (`swapon` after, if you
|
||||||
|
swapoff'd for the test.)
|
||||||
|
- [ ] **Night-light geo mode (2026-07-11)** — in `home.nix` set
|
||||||
|
`nomarchy.nightlight = { enable = true; latitude = "<lat>";
|
||||||
|
longitude = "<long>"; }` (your real coordinates), `nomarchy-home`,
|
||||||
|
relogin. Then: `systemctl --user status wlsunset` is running (and
|
||||||
|
`hyprsunset` is NOT); after local sunset the screen visibly warms
|
||||||
|
(or test by setting coordinates where it's currently night); the
|
||||||
|
Waybar moon + menu toggle still flip it instantly and an *off*
|
||||||
|
survives relogin (the ExecCondition gate on the swapped unit —
|
||||||
|
the eval check proves the wiring, not the runtime gate). **Pass** =
|
||||||
|
warm shift at the location's night + toggle/persistence intact.
|
||||||
|
|
||||||
|
## 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 5310 / 5410 only
|
||||||
|
- [ ] **v1 QA batch on-hardware pass** (583708d batch was QEMU-verified) —
|
||||||
|
general smoke before the next `main → v1` promotion (broader than
|
||||||
|
the 2026-07-10 session; include theme/ISO/greeter leftovers).
|
||||||
|
|
||||||
|
## 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).
|
||||||
1884
agent/JOURNAL-ARCHIVE.md
Normal file
1884
agent/JOURNAL-ARCHIVE.md
Normal file
File diff suppressed because it is too large
Load Diff
546
agent/JOURNAL.md
Normal file
546
agent/JOURNAL.md
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
# Journal — append-only iteration log
|
||||||
|
|
||||||
|
One entry per iteration, **newest first**. This is the loop's short-term
|
||||||
|
memory: the next session reads the last 3–5 entries to orient. Keep an
|
||||||
|
entry under ~15 lines; durable lessons go to MEMORY.md, not here. When
|
||||||
|
this file grows past ~25 entries, rotate the tail to
|
||||||
|
[JOURNAL-ARCHIVE.md](JOURNAL-ARCHIVE.md) (history, never edited).
|
||||||
|
|
||||||
|
Template:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## YYYY-MM-DD — <task title> (<commit sha, or "no commit">)
|
||||||
|
- **Task:** which BACKLOG item (or QA sweep / bootstrap / escalation)
|
||||||
|
- **Did:** what changed, in 2–4 lines
|
||||||
|
- **Verified:** tier reached (V0/V1/V2) + the actual commands/checks run
|
||||||
|
- **Pending:** V3 queued? follow-ups filed? anything [stuck]?
|
||||||
|
- **Next suggestion:** what the following iteration should probably take
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-07-11 — Quick wins: exit-node locations + geo tooltip
|
||||||
|
- **Task:** BACKLOG LATER "VPN exit-node richer display" + a geo-mode
|
||||||
|
tooltip nit from the previous iteration (human asked for quick wins).
|
||||||
|
- **Did:** Tailscale Exit node picker rows now "hostname — Country City",
|
||||||
|
sliced by the header's fixed-width column offsets (COUNTRY/CITY are
|
||||||
|
multi-word; never field-split), locationless nodes stay bare, pick strips
|
||||||
|
back to hostname at the first space. Nightlight Waybar tooltip says
|
||||||
|
"follows your location" in geo mode. Queue's VPN entry updated.
|
||||||
|
- **Verified:** V1 — awk proven against a fabricated 3-case fixture
|
||||||
|
(Mullvad 1-word + multi-word city + bare node); flake check exit 0;
|
||||||
|
downstream-template-home builds (writeShellScriptBin syntax-gates both
|
||||||
|
scripts). Real tailscale output is V3 (existing VPN queue entry).
|
||||||
|
- **Pending:** V3 VPN menu live paths (extended, not new).
|
||||||
|
- **Next suggestion:** wallpapers artifact split (LATER, decided).
|
||||||
|
|
||||||
|
## 2026-07-11 — Night-light geo mode (lat/long → wlsunset)
|
||||||
|
- **Task:** BACKLOG LATER "Night-light geo mode" (human-picked).
|
||||||
|
- **Did:** `nomarchy.nightlight.latitude`/`.longitude` (both set = geo mode):
|
||||||
|
`services.wlsunset` (location-computed sunrise/sunset, `.temperature` →
|
||||||
|
night temp) replaces hyprsunset; the toggle script's `unit=` and the
|
||||||
|
live-state ExecCondition follow via one `unit` let-binding. Template
|
||||||
|
example lines, README row, ROADMAP ✓. New `checks.nightlight-geo`:
|
||||||
|
pure-eval asserts on the rendered units (coords + temp in ExecStart,
|
||||||
|
ExecCondition present, hyprsunset off). Declarative-only (no menu writer —
|
||||||
|
coords are machine config like keyboard layout).
|
||||||
|
- **Verified:** V1 — `nix flake check --no-build` exit 0 (evals the new
|
||||||
|
asserts + option-docs); built `downstream-template-home` (default
|
||||||
|
hyprsunset path unregressed) + the check drv. Caught en route: HM renders
|
||||||
|
`ExecStart` as a list (`toString` needed in eval asserts).
|
||||||
|
- **Pending:** V3 queued (HARDWARE-QUEUE § Any machine): visible warm shift
|
||||||
|
at location night + toggle/persistence on the swapped unit.
|
||||||
|
- **Next suggestion:** wallpapers artifact split (LATER, decided).
|
||||||
|
|
||||||
|
## 2026-07-11 — Sync-sweep rule + grooming + zram user docs (human-requested)
|
||||||
|
- **Task:** Bernardo: groom stale items and codify the check so they don't
|
||||||
|
recur; close the zram user-doc gap.
|
||||||
|
- **Did:** LOOP.md §5 gains step 4 **Sync sweep** (grep item/feature across
|
||||||
|
agent/+docs/ on ship; prune recorded `[x]` queue entries) + an AGENTS.md
|
||||||
|
rule line. Applied it: BACKLOG PROPOSED pruned (auto-theme pitch was
|
||||||
|
stale — shipped as #79; shipped/promoted placeholders removed; v1.0
|
||||||
|
pointer rewritten; FUTURE promoted-stub dropped). HARDWARE-QUEUE pruned
|
||||||
|
29 checked `[x]` entries (363→~250 lines), dangling refs reworded,
|
||||||
|
prune rule in header. README §3 now documents the zram default +
|
||||||
|
hibernation out-of-the-box + MIGRATION pointer.
|
||||||
|
- **Verified:** V0 (docs/meta only) — grep sweep for dangling refs clean.
|
||||||
|
- **Pending:** night-light geo mode next (same session).
|
||||||
|
- **Next suggestion:** wallpapers artifact split (LATER, decided).
|
||||||
|
|
||||||
|
## 2026-07-11 — Vendor-neutral agent docs restructure (human-requested)
|
||||||
|
- **Task:** Bernardo: de-vendor the agent instructions (multi-model repo),
|
||||||
|
clean stale files, better docs/agent/.claude structure.
|
||||||
|
- **Did:** New root `AGENTS.md` entry (CLAUDE.md → symlink). Skill bodies
|
||||||
|
moved to `agent/`: VERIFICATION.md (enforcement, ex-nomarchy skill),
|
||||||
|
DELEGATION.md (tiers light/standard/frontier + scout/runner contracts +
|
||||||
|
fan-out; consolidates CLAUDE.md table + LOOP §economy + skill §6.5),
|
||||||
|
THEME-DESIGN.md (ex-theme skill, now with frontmatter’d shim). `.claude/`
|
||||||
|
is a thin adapter (shims route into `agent/`). Maps updated (agent/,
|
||||||
|
docs/, README tree). Stale: `.gitignore` old_distro entry dropped; local
|
||||||
|
result*/__pycache__ removed. JOURNAL rotated (29 kept, 120 →
|
||||||
|
JOURNAL-ARCHIVE.md).
|
||||||
|
- **Verified:** V0 — `nix flake check --no-build` exit 0 with the symlink
|
||||||
|
tracked; vendor-name grep clean outside DELEGATION's mapping table.
|
||||||
|
- **Pending:** nothing; other harnesses add their own adapter row when used.
|
||||||
|
- **Next suggestion:** #76/#79 remain [blocked:hw]; otherwise groom PROPOSED.
|
||||||
|
|
||||||
|
## 2026-07-11 — #76 checks.hibernate-swapfile (VM-verify the swapfile+offset)
|
||||||
|
- **Task:** Bernardo — push #76's swapfile-resume residual into a VM (like
|
||||||
|
the auto-theme sunset trick).
|
||||||
|
- **Did:** Tried a full runtime-swapfile hibernate→resume test; it hit a
|
||||||
|
bounded, definitive **`CanHibernate=no`** — systemd won't hibernate to a
|
||||||
|
swap that isn't declared in the boot config, and a runtime-computed
|
||||||
|
`resume_offset` can't reach the boot cmdline (the genuine chicken-and-egg).
|
||||||
|
First cut also had an **unbounded `wait_for_shutdown`** that hung → killed
|
||||||
|
it, bounded the wait in a thread (180s) + a `CanHibernate` pre-check, which
|
||||||
|
is what surfaced the clean verdict. Pivoted `checks.hibernate-swapfile` to
|
||||||
|
what a VM *can* prove deterministically and the partition test can't: the
|
||||||
|
installer swapfile **build + offset** — NOCOW `mkswapfile`, file-type
|
||||||
|
swapon, valid `map-swapfile -r` offset accepted by `/sys/power/resume_offset`,
|
||||||
|
zram above it. (Device fix: `/dev/vdb` once `useNixStoreImage` was dropped.)
|
||||||
|
- **Verified:** **V2 PASS** (fresh build, exit 0). BACKLOG/ROADMAP updated;
|
||||||
|
#76 residual narrowed to the literal encrypted power-cycle (dev box runs it).
|
||||||
|
- **Lesson:** the sunset-trick spirit applies but not always the full cycle —
|
||||||
|
when systemd/kernel gates the behaviour (CanHibernate), verify the
|
||||||
|
*construction* deterministically and leave only the physical cycle to HW.
|
||||||
|
Always bound `wait_for_shutdown` (no timeout param → thread + join).
|
||||||
|
- **Next suggestion:** #76/#79 both `[blocked:hw]`; QA / PROPOSED otherwise.
|
||||||
|
|
||||||
|
## 2026-07-11 — Code-review fixes (parallel review of the session batch)
|
||||||
|
- **Task:** Bernardo asked for parallel work; spawned a fresh-eyes opus
|
||||||
|
review of `2055842..HEAD` (static). It found the batch sound — 2 real LOW
|
||||||
|
edge bugs, 1 false positive, 1 cosmetic.
|
||||||
|
- **Did:** (1) `cmd_auto` — inverted sunrise/sunset (sunrise > sunset) went
|
||||||
|
perpetually day; replaced the `not between` shortcut with a real
|
||||||
|
wrap-around `mins >= sunrise or mins < sunset`, so a cross-midnight day
|
||||||
|
window switches both ways. (2) `rofi.nix` autotheme HH:MM guard tightened
|
||||||
|
`[0-2][0-9]` → `[01][0-9]|2[0-3]` so 24–29 are rejected. Dismissed the
|
||||||
|
`breeze` finding (papirus DOES ship breeze/breeze-dark — verified) and the
|
||||||
|
cosmetic doctor-RAM one.
|
||||||
|
- **Verified:** py_compile; boundary truth-table (normal + wrap-around);
|
||||||
|
HH:MM case table (23:00/19:59/20:00 accept, 24/25/30:00 reject); `auto
|
||||||
|
--which` normal-case unchanged; menu bash -n; flake check green. Normal
|
||||||
|
case unchanged so `checks.auto-theme` still holds.
|
||||||
|
- **Pending:** hibernate-swapfile VM test still running (uncommitted flake.nix).
|
||||||
|
- **Next suggestion:** land hibernate-swapfile once its VM test returns.
|
||||||
|
|
||||||
|
## 2026-07-10 — #79 V2: checks.auto-theme (simulated sunset) — was mis-framed V3
|
||||||
|
- **Task:** Bernardo challenged the "V3 real-session" framing — a simulated
|
||||||
|
sunset IS VM-testable. He's right; I'd conflated "needs a real desktop"
|
||||||
|
with "needs the generic home-manager switch."
|
||||||
|
- **Did:** Added `checks.auto-theme` runNixOSTest: installs nomarchy-theme-sync,
|
||||||
|
points `NOMARCHY_DEFAULT_THEMES` at presets, stubs the switch via
|
||||||
|
`NOMARCHY_REBUILD` (a marker), configures the pair+schedule, then **walks
|
||||||
|
the VM clock** 19:55→20:05→20:06→next-day 07:30 asserting slug goes
|
||||||
|
summer-day → night (1 rebuild) → night (idempotent, still 1) → day (2). The
|
||||||
|
self-gating I relied on: presets via env, wallpaper/notify no-op headless,
|
||||||
|
run_switch honors NOMARCHY_REBUILD + waybar nudge is check=False.
|
||||||
|
- **Verified:** **V2 PASS** — `nix build .#checks…auto-theme` fresh, exit 0.
|
||||||
|
Dropped #79's `[blocked:hw]`; it's done (V2). Updated ROADMAP.
|
||||||
|
- **Lesson:** don't default "timer/session feature" to V3 — stub only the
|
||||||
|
generic step (the real HM switch, covered elsewhere) and simulate the rest
|
||||||
|
(clock) headlessly. Only the literal timer-fires-on-OnCalendar is on-HW,
|
||||||
|
and its schedule is systemd-analyze-validated.
|
||||||
|
- **Next suggestion:** NEXT head #76 `[blocked:hw]`; else QA / PROPOSED.
|
||||||
|
|
||||||
|
## 2026-07-10 — #79 slice 3: Look & Feel › Auto theme menu (#79 complete)
|
||||||
|
- **Task:** BACKLOG #79 slice 3 — the menu; completes the feature.
|
||||||
|
- **Did:** `rofi.nix` — "Auto theme (on/off)" row in Look & Feel + an
|
||||||
|
`autotheme)` submenu: toggle, pick day/night (from
|
||||||
|
`nomarchy-theme-sync list`), set sunrise/sunset (HH:MM input, validated).
|
||||||
|
Enable writes the flag + defaults the summer pair `--no-switch`, then
|
||||||
|
`exec auto --force` — one rebuild installs the timer AND applies the right
|
||||||
|
theme. Disable is instant `--no-switch` (`auto` self-gates; the lingering
|
||||||
|
timer no-ops). Day/night/times are live `--no-switch`.
|
||||||
|
- **Verified:** **V1** — HM generation builds (`writeShellScriptBin` bash -n
|
||||||
|
on nomarchy-menu passes) + bash -n on the built menu. **Logic test**
|
||||||
|
(stubbed rofi/theme-sync/notify): selecting "Auto theme (off)" runs exactly
|
||||||
|
`set day summer-day`, `set night summer-night`, `set enable true`
|
||||||
|
(all `--no-switch`), then `auto --force` — the intended one-rebuild path.
|
||||||
|
Disable path is static-clear (set false `--no-switch`, no `auto`); a
|
||||||
|
stateful-stub dynamic test would loop (stub doesn't reflect the flip). V0
|
||||||
|
flake check green. **#79 collapsed to `[blocked:hw]`** (all 3 slices done;
|
||||||
|
only live-session switch remains, needs a Nomarchy machine).
|
||||||
|
- **Pending:** V3 real-session (timer switches day↔night across sunset).
|
||||||
|
- **Next suggestion:** NEXT head #76/#79 both `[blocked:hw]`; QA/PROPOSED.
|
||||||
|
|
||||||
|
## 2026-07-10 — #79 slice 2: auto-theme systemd timer
|
||||||
|
- **Task:** BACKLOG #79 slice 2 — the timer/hook.
|
||||||
|
- **Did:** `modules/home/autotheme.nix` (imported in home default.nix):
|
||||||
|
`systemd.user.{service,timer}.nomarchy-auto-theme`, install gated on
|
||||||
|
`config.nomarchy.settings.autoTheme.enable or false` (state-driven, like
|
||||||
|
updates.nix gates on its enable). Service runs `nomarchy-theme-sync auto`
|
||||||
|
with a PATH for `home-manager`; timer `OnStartupSec=1min` +
|
||||||
|
`OnCalendar=*:0/15` + `Persistent`. Chose periodic over exact
|
||||||
|
sunrise/sunset timers — robust to suspend/DST, and `auto` is idempotent so
|
||||||
|
ticks are cheap (rebuild only at the transition).
|
||||||
|
- **Verified:** **V1** — real-config eval (state temp-enabled, restored):
|
||||||
|
`settings.autoTheme.enable`→true, timer `OnCalendar`=`*:0/15`, service
|
||||||
|
ExecStart=`… nomarchy-theme-sync auto`; disabled→no units (isolation eval
|
||||||
|
condition=false). `systemd-analyze calendar '*:0/15'` valid. **V0** flake
|
||||||
|
check green. (Gotcha: a new .nix file is invisible to the flake until
|
||||||
|
`git add` — flake eval only sees tracked files.)
|
||||||
|
- **Pending:** live timer→`home-manager switch` on a real Nomarchy session
|
||||||
|
(needs a Nomarchy machine, like #76). Slice 3 (menu) next.
|
||||||
|
- **Next suggestion:** #79 slice 3 — Look & Feel › Auto theme menu.
|
||||||
|
|
||||||
|
## 2026-07-10 — #79 slice 1: auto time-of-day theme CLI primitive
|
||||||
|
- **Task:** Split BACKLOG #79 (`VISION § D`, `[big]`) into 3 slices (CLI /
|
||||||
|
timer / menu); took slice 1.
|
||||||
|
- **Did:** `nomarchy-theme-sync auto` — reads
|
||||||
|
`settings.autoTheme.{enable,day,night,sunrise,sunset}`, computes day/night
|
||||||
|
by the local clock, and applies the matching preset **only if different**
|
||||||
|
(idempotent → the slice-2 timer won't rebuild needlessly). `--which`
|
||||||
|
prints the decision without switching; `--force`/`--no-switch` too.
|
||||||
|
Factored `cmd_apply`'s core into `apply_named` so `auto` reuses the exact
|
||||||
|
same one engine (GOALS: no second pipeline).
|
||||||
|
- **Verified:** **V1** package builds; functional tests on the built bin
|
||||||
|
against scratch state: wide-day→day slug, 00:00–00:01 window→night,
|
||||||
|
disabled→no-op, missing day/night→die, `auto --no-switch` writes state,
|
||||||
|
re-run→"already on" no-op, plain `apply` still writes (refactor intact).
|
||||||
|
**V0** py_compile + flake check + `checks.theme-sync-validate` green.
|
||||||
|
- **Pending:** slices 2 (systemd sunrise/sunset timers + login apply, V2)
|
||||||
|
and 3 (Look & Feel menu) remain under #79.
|
||||||
|
- **Next suggestion:** #79 slice 2 — the timer/hook (V2 runNixOSTest).
|
||||||
|
|
||||||
|
## 2026-07-10 — #78 Omarchy migrant doc (docs/OMARCHY.md)
|
||||||
|
- **Task:** BACKLOG #78 (promoted from PROPOSED) — Omarchy→Nomarchy
|
||||||
|
one-pager (`VISION § F`).
|
||||||
|
- **Did:** New `docs/OMARCHY.md`: leads with the config-model shift
|
||||||
|
(imperative dotfiles → declarative flake + menu, rollback via
|
||||||
|
generations), then install (ISO/`nomarchy-install` or MIGRATION.md),
|
||||||
|
a familiar-bindings table, theming (`SUPER+T`/`nomarchy-theme-sync`),
|
||||||
|
packages (home.packages / `nix shell`, no AUR), update+rollback
|
||||||
|
(`nomarchy-pull`/`-rebuild`/`-home`, boot-menu generations,
|
||||||
|
`nomarchy-doctor`). Added to `docs/README.md` map + VISION § F ✓.
|
||||||
|
- **Verified:** V0 (docs). Grounded Nomarchy side in code: pulled real
|
||||||
|
binds from `keybinds.nix`; confirmed all 7 cited commands, the `nsearch`
|
||||||
|
alias, and the named slugs (gruvbox/rose-pine/nord) exist. Kept
|
||||||
|
Omarchy-side claims to known facts (Arch, dotfiles, pacman/AUR).
|
||||||
|
- **Pending:** none.
|
||||||
|
- **Next suggestion:** NEXT head #76 `[blocked:hw]`; last open PROPOSED is
|
||||||
|
auto time-of-day theme pair (`§D`, `[big]` — needs splitting).
|
||||||
|
|
||||||
|
## 2026-07-10 — #77 doctor hibernate/sleep section (V2 PASS)
|
||||||
|
- **Task:** BACKLOG #77 (promoted from PROPOSED by Bernardo) — read-only
|
||||||
|
hibernate section in nomarchy-doctor (`VISION § C`, #76 follow-on).
|
||||||
|
- **Did:** Added a hibernate/sleep row group to `nomarchy-doctor.sh`: resume
|
||||||
|
device on cmdline (+ `resume_offset` for a swapfile), disk swap ≥ RAM,
|
||||||
|
zram active, previous-boot suspend/hibernate error. Self-gates (`skip` on
|
||||||
|
swap=0); only ok/warn/skip → never fails the sheet. Added `hibernate` to
|
||||||
|
the `checks.doctor` probe list.
|
||||||
|
- **Verified:** **V2 PASS** — `checks.doctor` VM test green (fresh build,
|
||||||
|
exit 0; asserts the hibernate row renders + healthy run still 0). **V1** —
|
||||||
|
ran on this box's real LUKS+@swap layout: all three `ok` rows render
|
||||||
|
(resume set, swap 34G ≥ RAM, zram 16G) and the sheet reaches its verdict.
|
||||||
|
**V0** bash -n + flake check.
|
||||||
|
- **Gotcha (fixed + MEMORY):** the doctor runs `set -euo pipefail`; my first
|
||||||
|
cut aborted before the verdict because a no-match `grep` inside `$(…)` and
|
||||||
|
a `cond && action` both trip set -e. Fixed with `|| true` / `|| echo 0` and
|
||||||
|
an `if`. First VM run caught it (exit 1, no verdict) — the test earned its keep.
|
||||||
|
- **Pending:** none — fully agent-verifiable (read-only checks, no V3).
|
||||||
|
- **Next suggestion:** NEXT head is #76 `[blocked:hw]`; remaining PROPOSED
|
||||||
|
(Omarchy doc §F, auto time-of-day theme §D) await triage.
|
||||||
|
|
||||||
|
## 2026-07-10 — QA sweep (clean) + VISION-gap pass → 3 PROPOSED
|
||||||
|
- **Task:** Bernardo — QA/drift sweep, then a VISION-gap pass with proposals.
|
||||||
|
- **Did (sweep):** Fanned out two subagents (Bernardo OK'd spawning for
|
||||||
|
efficiency): nomarchy-runner ran the deterministic checks + `bash -n`/
|
||||||
|
`py_compile` (all PASS; the 4 "needs-args" scripts pass via their flake
|
||||||
|
derivations); nomarchy-scout hunted fuzzy drift (dead code, stale refs,
|
||||||
|
TODOs, changed-area contradictions, count/version) — **all clean**.
|
||||||
|
Spot-checked scout's count claim myself: 24 theme JSONs / 24 previews /
|
||||||
|
README "24 presets" — consistent. Eval sweep green. **No fixes needed.**
|
||||||
|
- **Did (VISION):** Verified candidate gaps against the tree before
|
||||||
|
proposing (killed two false gaps — nomarchy-rebuild/home already tail logs
|
||||||
|
+ point at doctor on failure = §A shipped; `nvd diff` = §B covered; System
|
||||||
|
› Firmware menu already exists). Filed 3 **PROPOSED** (verified open):
|
||||||
|
doctor hibernate/sleep section (§C, top Day-2 pick, #76 follow-on),
|
||||||
|
Omarchy migrant one-pager (§F), auto time-of-day theme pair (§D, `[big]`).
|
||||||
|
- **Verified:** V0 (agent-notes/docs only). Sweep evidence above.
|
||||||
|
- **Pending:** none. Proposals await Bernardo's triage into a tier.
|
||||||
|
- **Next suggestion:** if a PROPOSED is promoted, the doctor hibernate
|
||||||
|
section is the cleanest next build; else NEXT #76 stays `[blocked:hw]`.
|
||||||
|
|
||||||
|
## 2026-07-10 — Icon overrides: user-facing docs (OVERRIDES.md)
|
||||||
|
- **Task:** Bernardo asked whether the session's features need user docs.
|
||||||
|
- **Did:** Hibernation already covered (README swapfile line, MIGRATION.md
|
||||||
|
runbook, discoverable Power-menu row) — no gap. Icons had one: the only
|
||||||
|
doc was the template comment. Added an "Icon pack" subsection to
|
||||||
|
`docs/OVERRIDES.md` § Appearance — `nomarchy-theme-sync set icons "<name>"`,
|
||||||
|
the known-packs table, the ~1 GiB/opt-in rationale, and that the choice is
|
||||||
|
sticky across `apply` (presets carry no icon field; verified `deep_merge`).
|
||||||
|
- **Verified:** command path confirmed (`cmd_set` writes any dotted key;
|
||||||
|
`icons` is a schema key). **Built all 4 packs and listed real theme dirs**
|
||||||
|
to fix the examples — caught Qogir using capital `Qogir-Dark`/`Light`
|
||||||
|
(lowercase would silently fall back to hicolor). Docs-only (V0).
|
||||||
|
- **Pending:** none.
|
||||||
|
- **Next suggestion:** NEXT #76 `[blocked:hw]`; LATER/QA-sweep next.
|
||||||
|
|
||||||
|
## 2026-07-10 — Icon themes: opt-in cross-pack overrides (no default bloat)
|
||||||
|
- **Task:** LATER "per-theme icon overrides / more icon packs". Recon found
|
||||||
|
papirus is already ~1 GiB and ships only Papirus/Dark/Light (no free
|
||||||
|
variety); real variety needs extra packs at ~hundreds of MB each. Bernardo
|
||||||
|
chose the **opt-in, no-default-bloat** shape (over shipping packs).
|
||||||
|
- **Did:** `theme.nix` resolves `nomarchy.theme.iconThemePackage` from a
|
||||||
|
small `iconPacks` registry (papirus·tela·qogir·reversal·numix, by name
|
||||||
|
prefix). Default themes name `Papirus-*` → resolves to the *plain* papirus
|
||||||
|
derivation (no symlinkJoin, identical closure); only a theme naming another
|
||||||
|
pack's set symlinkJoins that one pack in. `stylix.nix` uses it
|
||||||
|
(mkDefault). Opt-in note in `templates/downstream/home.nix`; ROADMAP §
|
||||||
|
Icon themes updated (incl. the deliberate no-default-packs decision).
|
||||||
|
- **Verified:** **V1** — HM activationPackage builds (exit 0). Eval proofs:
|
||||||
|
default `iconThemePackage` = plain `papirus-icon-theme-*` (zero delta);
|
||||||
|
with `icons="Tela-dark"` → `nomarchy-icon-themes` symlinkJoin (papirus+tela)
|
||||||
|
and name flows, **without building tela**. All 5 pack attrs exist in the
|
||||||
|
pinned nixpkgs. `template-sot` + `option-docs` + `flake check` green.
|
||||||
|
- **Pending:** none. No default visual change (identical papirus), so no
|
||||||
|
screenshot needed; the opt-in render is the downstream user's to see.
|
||||||
|
- **Next suggestion:** NEXT still #76 `[blocked:hw]`; LATER/QA-sweep next.
|
||||||
|
|
||||||
|
## 2026-07-10 — #76 slice 3: hibernate VM test (V2 PASS) → #76 agent-complete
|
||||||
|
- **Task:** BACKLOG #76 slice 3 — the mandatory V2 hibernate test + V3 queue.
|
||||||
|
- **Did:** Added `checks.hibernate` (flake.nix), modeled on nixpkgs'
|
||||||
|
hibernate.nix but wired to Nomarchy's #76 risk: imports oom.nix (zram
|
||||||
|
default-on), disk swap at lower priority + explicit `boot.resumeDevice`,
|
||||||
|
systemd-initrd. Test drives hibernate→power-off→resume→crash: ramfs marker
|
||||||
|
survives resume, a crash-boot loses it (real resume, not a survivor), and
|
||||||
|
asserts zram@100 + disk-swap<100 so the image lands on disk not volatile
|
||||||
|
zram. Queued the V3 laptop steps (real LUKS+@swap+offset resume + no-swap
|
||||||
|
notify) in HARDWARE-QUEUE. Collapsed #76 to a `[blocked:hw]` stub; design
|
||||||
|
record → ROADMAP § "Hibernation + zram by default".
|
||||||
|
- **Verified:** **V2 PASS** — `nix build .#checks.x86_64-linux.hibernate`
|
||||||
|
built the driver fresh and exited 0 (all assertions held). `nix flake
|
||||||
|
check --no-build` green.
|
||||||
|
- **Pending:** only V3 (dev box / laptop) — #76's last check; close on PASS.
|
||||||
|
- **Next suggestion:** #76 is now blocked:hw; NEXT head falls to LATER/
|
||||||
|
PROPOSED triage or a QA sweep — pick per LOOP.md orientation next session.
|
||||||
|
|
||||||
|
## 2026-07-10 — #76 slice 2: Hibernate notify-on-failure
|
||||||
|
- **Task:** BACKLOG #76 remaining slice 2 — per the settled "keep row, notify
|
||||||
|
on failure" call.
|
||||||
|
- **Did:** `modules/home/rofi.nix` power menu Hibernate arm now runs
|
||||||
|
`systemctl hibernate 2>/dev/null || notify-send "Hibernate" "…see
|
||||||
|
docs/MIGRATION.md"`. On success the call blocks until resume, so the notify
|
||||||
|
only fires on a genuine failure (e.g. swap=0 no-swap machine). Row stays
|
||||||
|
unconditional (no hiding).
|
||||||
|
- **Verified:** **V1** — `nix build …homeConfigurations.nomarchy.activationPackage`
|
||||||
|
green (rebuilds `nomarchy-menu.drv`; `writeShellScriptBin` runs bash -n at
|
||||||
|
build). Control-flow proof on the arm extracted from the built script:
|
||||||
|
failing `systemctl` → notify fires; succeeding → no notify. `nix flake
|
||||||
|
check --no-build` green.
|
||||||
|
- **Pending:** slice 3 = the V2 hibernate→resume VM test + V3 laptop
|
||||||
|
(Hibernate→power off→resume on the LUKS+@swap machine; plus confirm the
|
||||||
|
no-swap notify surfaces in a real session). Both remain open on #76.
|
||||||
|
- **Next suggestion:** slice 3 — add a `checks.*` hibernate VM test (as far
|
||||||
|
as QEMU allows) and queue the V3 laptop steps in HARDWARE-QUEUE.
|
||||||
|
|
||||||
|
## 2026-07-10 — #76 slice 1: MIGRATION.md hibernation runbook
|
||||||
|
- **Task:** BACKLOG #76 remaining slice 1 — docs runbook for enabling
|
||||||
|
hibernation on an existing machine (per the settled "docs not tool" call).
|
||||||
|
- **Did:** Added § "Enabling hibernation on an existing machine (no reinstall)"
|
||||||
|
to `docs/MIGRATION.md`: create `@swap` subvol (subvolid=5) + `mkswapfile`
|
||||||
|
(NOCOW), read `map-swapfile -r` offset, wire `fileSystems."/swap"` +
|
||||||
|
`swapDevices` + `resumeDevice` + `resume_offset` into system.nix. Flagged
|
||||||
|
the mount as required on the hand-edit path (installer gets it from
|
||||||
|
disko-gen hardware-config). Notes zram-priority reservation, no-LUKS
|
||||||
|
variant, `swap=0` opt-out. Worked example = this dev machine.
|
||||||
|
- **Verified:** **V0** — read-only commands run live against this machine's
|
||||||
|
real LUKS(`cryptroot`)+btrfs(`@`)+`/swap/swapfile` layout (RAM 31G, fsuuid,
|
||||||
|
`findmnt SOURCE`, `mkswapfile` flags, zram@100 vs swapfile@-1 confirms the
|
||||||
|
design intent). `nix flake check --no-build` green.
|
||||||
|
- **Pending:** slice 2 — Hibernate row notify-on-failure (V1/V2); then the
|
||||||
|
V2 hibernate VM test + V3 laptop.
|
||||||
|
- **Next suggestion:** slice 2 (menu notify-on-failure).
|
||||||
|
|
||||||
|
## 2026-07-10 — #76 hibernation design calls (no commit yet → this bookkeeping)
|
||||||
|
- **Task:** BACKLOG #76 design slice — Bernardo made the open calls.
|
||||||
|
- **Did:** Recon showed the hibernation half is **already built for new
|
||||||
|
installs** (installer defaults swap=RAM hibernation-ready `@swap` swapfile;
|
||||||
|
`patch-template.py` writes swapDevices/resumeDevice/resume_offset; Power
|
||||||
|
menu Hibernate row + LUKS/hyprlock handling exist). Settled 3 calls:
|
||||||
|
(1) swap sizing = **exactly RAM** (no change); (2) migration = **docs
|
||||||
|
runbook in MIGRATION.md**, not a tool; (3) no-swap Hibernate = **keep row,
|
||||||
|
notify on failure**. Folded into BACKLOG #76 (design ✓, re-sliced remaining:
|
||||||
|
MIGRATION.md runbook + notify-on-failure menu) and Decisions.
|
||||||
|
- **Verified:** **V0** — `nix flake check --no-build` green (docs-only).
|
||||||
|
- **Pending:** two remaining slices below; V2 hibernate VM test + V3 laptop.
|
||||||
|
- **Next suggestion:** slice 1 — `docs/MIGRATION.md` hibernation-enable runbook
|
||||||
|
(V0), then slice 2 — Hibernate notify-on-failure (V1/V2).
|
||||||
|
|
||||||
|
## 2026-07-10 — #76 zram slice: compressed-RAM swap default
|
||||||
|
- **Task:** BACKLOG #76 (`[big]`) — split; took the low-risk zram-default slice.
|
||||||
|
- **Did:** `zramSwap` on by default in `modules/nixos/oom.nix` (zstd, 50% RAM,
|
||||||
|
priority 100) as the memory-pressure layer before earlyoom, following the
|
||||||
|
earlyoom mkDefault precedent (no toggle). High priority keeps day-to-day
|
||||||
|
paging in zram so a disk swapfile stays reserved for #76's hibernate image.
|
||||||
|
Added `checks.zram-swap` runNixOSTest (imports oom.nix). Split #76 in
|
||||||
|
BACKLOG → remainder is the hibernation half (swapfile/resume/installer).
|
||||||
|
- **Verified:** **V2** — `nix build .#checks…zram-swap` PASS: /dev/zram0 active
|
||||||
|
in /proc/swaps, `[zstd]` comp_algorithm, priority 100. **V1** full distro
|
||||||
|
toplevel builds. **V0** flake check --no-build green.
|
||||||
|
- **Pending:** hibernation half of #76 still open (needs hibernate→resume VM
|
||||||
|
test + V3 laptop). No V3 for zram (under-load tuning is secondary).
|
||||||
|
- **Next suggestion:** #76 hibernation half — design default swapSize + the
|
||||||
|
resume/installer wiring; it's the boot-critical, V2-hibernate + V3 part.
|
||||||
|
|
||||||
|
## 2026-07-10 — #41 hardware-confirmed: OpenFile portal floats (V3 close)
|
||||||
|
- **Task:** BACKLOG #41 — final on-hardware confirmation (Bernardo reported PASS).
|
||||||
|
- **Did:** #41 already out of NEXT + portal rule shipped
|
||||||
|
(`hyprland.nix` xdg-desktop-portal-gtk float/center). Bernardo verified the
|
||||||
|
GTK OpenFile file-picker floats on real hardware; dropped the "OpenFile not
|
||||||
|
re-probed / optional residual" caveats in HARDWARE-QUEUE — #41 fully closed.
|
||||||
|
- **Verified:** **V3** (Bernardo, Latitude). No code change. Bookkeeping only;
|
||||||
|
QA-sweep guards run green this session: `nix flake check --no-build`,
|
||||||
|
option-docs/template-sot/theme-sync-validate builds, contrast 24×7.
|
||||||
|
- **Pending:** none for #41. NEXT head is #76 (hibernation + zram, `[big]`).
|
||||||
|
- **Next suggestion:** split #76 — take the low-risk **zram-default** slice
|
||||||
|
(V2-testable headlessly) before the boot-critical hibernate/resume work.
|
||||||
|
|
||||||
|
## 2026-07-10 — Close #41 GTK portal float rule
|
||||||
|
- **Task:** BACKLOG #41 residual (GTK file-chooser portal class).
|
||||||
|
- **Did:** Added float+center for `xdg-desktop-portal-gtk` /
|
||||||
|
`.xdg-desktop-portal-gtk-wrapped` (desktop + libexec name from package).
|
||||||
|
Polkit already PASS on Latitude. Removed #41 from NEXT; only #20 remains.
|
||||||
|
- **Verified:** **V0** windowrule-syntax check path unchanged; class from
|
||||||
|
package `xdg-desktop-portal-gtk.desktop` / community configs. SoftGL
|
||||||
|
still cannot materialize portal dialogs.
|
||||||
|
- **Pending:** optional hardware spot-check OpenFile floats (HARDWARE-QUEUE).
|
||||||
|
- **Next suggestion:** #20 is `[human]` only — or LATER/PROPOSED triage.
|
||||||
|
|
||||||
|
## 2026-07-10 — Product calls: charge live, identity, btop, NVIDIA skip
|
||||||
|
- **Task:** Bernardo decisions on remaining PROPOSED.
|
||||||
|
- **Did:** (1) Instant charge-limit: udev GROUP=users on threshold,
|
||||||
|
menu/powermgmt live sysfs + state, oneshot reads live theme-state +
|
||||||
|
AC re-apply. (2) Look & Feel already had Theme/wallpaper/night light;
|
||||||
|
added Reset wallpaper (auto). (3) NVIDIA first-class deferred past v1
|
||||||
|
(HARDWARE.md). (4) Identity retunes: white L-steps, lumon subtext,
|
||||||
|
hackerman surface+UI warn/bad, matte-black subtext, miasma bad.
|
||||||
|
(5) Hand `btop.theme` for 9 identity/whole-swap slugs from palette map.
|
||||||
|
- **Verified:** **V0** contrast 24×7; **V1** battery-charge-limit VM
|
||||||
|
(udev restart). **V3** live charge write on real laptop (HARDWARE-QUEUE).
|
||||||
|
- **Next suggestion:** only #20/#41 blocked, or Decisions.
|
||||||
|
|
||||||
|
## 2026-07-10 — Summer pair polish + status CSS + preview recapture
|
||||||
|
- **Task:** finish agent-able PROPOSED theme leftovers.
|
||||||
|
- **Did:** summer-day/night — JSON hierarchy (night subtext/muted/overlay
|
||||||
|
ladder; day overlay≠muted); waybar CSS tokens track JSON roles; font
|
||||||
|
13px both; day battery 25/10; VPN pill + full status states
|
||||||
|
(.recording/.available/.on/.activated/battery warn). Recaptured
|
||||||
|
executive-slate + neon-glass `preview.png` via theme-shot desktop
|
||||||
|
(480×270). Closed those PROPOSED lines.
|
||||||
|
- **Verified:** **V0** contrast 24×7 + wholeswap; **V2** theme-shot
|
||||||
|
summer-day/night/executive-slate/neon-glass — viewed bar crops +
|
||||||
|
previews. Identity taste retunes stay PROPOSED.
|
||||||
|
- **Next suggestion:** only blocked NEXT (#20/#41) or product Decisions.
|
||||||
|
|
||||||
|
## 2026-07-10 — #74 matrix flags + #75 V2 install (default arm)
|
||||||
|
- **Task:** NEXT #74 unattended matrix · #75 SoT V2 install after ISO.
|
||||||
|
- **Did:** `NOMARCHY_TEST_SWAP_GB` / `NOMARCHY_TEST_NO_LUKS` drive
|
||||||
|
`tools/test-install.sh` go.sh + first-boot LUKS skip; nixpkgs OVMF
|
||||||
|
fallback. Full offline install V2 (SWAP=2 LUKS) after live ISO rebuild
|
||||||
|
with starter packages — first-boot themed Boreal, no red banner.
|
||||||
|
Closed #74/#75. Other matrix arms scripted (not full-ISO re-run).
|
||||||
|
- **Verified:** **V2** — install powered off ~460s; first-boot.png
|
||||||
|
viewed (`/tmp/nomarchy-vm-75/first-boot.png`): themed wallpaper +
|
||||||
|
waybar (logo, clock, modules), doctor glyph, no autogen banner.
|
||||||
|
- **Next suggestion:** NEXT only #20 [human] + #41 [blocked:hw].
|
||||||
|
|
||||||
|
## 2026-07-10 — Loop batch land #64–#73 (parallel worktrees)
|
||||||
|
- **Task:** agent-actionable NEXT #64–#73 via worktree agents; land on main.
|
||||||
|
- **Did:** #64 MIGRATION snapshots · #65 real-uid chown · #66 theme-state
|
||||||
|
fail-closed reader · #67/#68 theme fidelity+ANSI · #69/#70 audit+import
|
||||||
|
tools · #71 IR portal docs · #72 template-sot check · #73 MOTD/first-boot
|
||||||
|
fingerprint+doctor tips. Agents did not push; parent cherry-picked
|
||||||
|
(stripped agent BACKLOG races). Closed #64–#73.
|
||||||
|
- **Verified:** **V0/V1** — theme-contrast 24×7 pass; template-sot green;
|
||||||
|
bash -n install; flake check --no-build green.
|
||||||
|
- **Next suggestion:** #74 unattended matrix + #75 V2 install (KVM).
|
||||||
|
|
||||||
|
## 2026-07-10 — Promote agent-actionable PROPOSED → NEXT #64–#75
|
||||||
|
- **Task:** Bernardo: bump non-hw/non-human PROPOSED categories 1+2.
|
||||||
|
- **Did:** **#64** MIGRATION snapshots · **#65** installer chown ·
|
||||||
|
**#66** mkFlake theme-state errors · **#67** theme fidelity nits ·
|
||||||
|
**#68** #52 residual ANSI · **#69** audit identity exemptions ·
|
||||||
|
**#70** import-pipeline hierarchy · **#71** IR portal docs (a) ·
|
||||||
|
**#72** template SoT CI · **#73** hardware hints residual · **#74**
|
||||||
|
unattended install matrix · **#75** SoT V2 install. Breadcrumbs in
|
||||||
|
PROPOSED; actionable items sit above #20/#41 in NEXT.
|
||||||
|
- **Verified:** V0 (backlog only).
|
||||||
|
- **Next suggestion:** queue head **#64** (docs) or batch #67/#68 themes.
|
||||||
|
|
||||||
|
## 2026-07-10 — PROPOSED cleanup (stale exam residue)
|
||||||
|
- **Task:** Bernardo: clean stale PROPOSED items after A/B/C + #52 ship.
|
||||||
|
- **Did:** Rewrote PROPOSED to open work only — dropped #52 shipped
|
||||||
|
high-ROI bullets, neon-glass quarantine, empty/shipped installer
|
||||||
|
audit sections, template seed drift (keys already present), soft
|
||||||
|
gaps fixed by #50, and “no action” kanagawa/boreal notes. Kept
|
||||||
|
product day-2, installer follow-ups, theme polish residuals, tooling.
|
||||||
|
- **Verified:** V0 (backlog hygiene); spot-checked template keys,
|
||||||
|
latte/mocha #52 fixes, usbutils, package exports.
|
||||||
|
- **Next suggestion:** triage the lean PROPOSED list or HARDWARE-QUEUE.
|
||||||
|
|
||||||
|
## 2026-07-10 — Pending closeout: #14, #57 V2, #41 slice
|
||||||
|
- **Task:** pending V2/CI/float leftovers after A/B/C batch.
|
||||||
|
- **Did:** **#14 closed** — first scheduled lock bump landed
|
||||||
|
`8fded63` (2026-07-06 Mon 05:20 UTC) as `nomarchy-bump` /
|
||||||
|
`chore(lock): scheduled upstream bump` (Actions token push works).
|
||||||
|
**#57 V2** — `checks.live-install-entry` asserts live HM gen has
|
||||||
|
`nomarchy-install.desktop` (Name/Terminal/Exec) + installer bin +
|
||||||
|
Tools menu gate in `nomarchy-menu`. **#41 slice** — float/center rules
|
||||||
|
for hyprpolkitagent + pinentry-qt from package app-id strings;
|
||||||
|
`tools/capture-float-classes.nix` harness (softGL empty clients);
|
||||||
|
residual GTK portal stays NEXT `#41 [blocked:hw]`. HARDWARE-QUEUE
|
||||||
|
float confirm steps added.
|
||||||
|
- **Verified:** **V2** live-install-entry green; windowrule-syntax green.
|
||||||
|
SoftGL class capture inconclusive (empty clients). **V3** float confirm
|
||||||
|
+ fingerprint/#60 still HARDWARE-QUEUE. **#20** still `[human]`.
|
||||||
|
- **Next suggestion:** Bernardo: #20 KVM runner, or HARDWARE-QUEUE pass.
|
||||||
|
|
||||||
|
## 2026-07-10 — #55 fingerprint menu + #56 human rebuild errors
|
||||||
|
- **Task:** NEXT #55 (System › Fingerprint) + #56 (rebuild failure UX).
|
||||||
|
- **Did:** #55 — System row self-gated on fprintd-list; enroll/list/verify/
|
||||||
|
delete-all terminal flows; Use for login writes
|
||||||
|
`settings.fingerprint.pam` (hardware.nix default from stateFile);
|
||||||
|
HARDWARE.md §5 + HARDWARE-QUEUE V3 enroll. #56 — sys-update /
|
||||||
|
sys-rebuild / home-update tee last 40 lines + nomarchy-doctor pointer;
|
||||||
|
theme-sync run_switch notify/die text. Closed #55/#56.
|
||||||
|
- **Verified:** **V1** — HM template rebuild (menu + theme-sync);
|
||||||
|
option-docs green; failure-footer shell smoke. **V3 pending:** real
|
||||||
|
fingerprint enroll (queue). **V2 pending:** forced bad sys-rebuild in
|
||||||
|
VM (footer text pure/logic covered).
|
||||||
|
- **Next suggestion:** queue was A–C; remaining NEXT is blocked items
|
||||||
|
(#14/#20/#41) or PROPOSED triage.
|
||||||
|
|
||||||
|
## 2026-07-10 — #58 nomarchy-detect-hw CLI
|
||||||
|
- **Task:** NEXT #58 — post-install re-probe package (HARDWARE.md §8).
|
||||||
|
- **Did:** `pkgs/nomarchy-detect-hw` wrapping installer’s hardware-db;
|
||||||
|
human report + snippets + `--raw` protocol; overlay + systemPackages;
|
||||||
|
HARDWARE.md §8 updated. Closed #58.
|
||||||
|
- **Verified:** **V1** — package builds; ran on this AMD laptop: correct
|
||||||
|
MODULE/NOMARCHY lines (amd cpu/gpu, laptop, ssd, fingerprint, IR cam,
|
||||||
|
NPU) and printable system.nix snippets.
|
||||||
|
- **Next suggestion:** #55 fingerprint menu or #56 human rebuild errors.
|
||||||
|
|
||||||
|
## 2026-07-10 — #57 live Install Nomarchy affordance
|
||||||
|
- **Task:** NEXT #57 — durable live-ISO install surface (not only toast).
|
||||||
|
- **Did:** `xdg.desktopEntries.nomarchy-install` (Terminal=true) on live
|
||||||
|
HM; Tools › Install Nomarchy self-gated on `command -v nomarchy-install`;
|
||||||
|
welcome toast wording updated. Closed #57.
|
||||||
|
- **Verified:** **V1** — live config eval exposes entry name/exec;
|
||||||
|
`downstream-template-home` rebuilds with menu gate. **V2 pending:**
|
||||||
|
ISO rebuild + smoke that the .desktop appears in the session.
|
||||||
|
- **Next suggestion:** #58 `nomarchy-detect-hw` CLI.
|
||||||
|
|
||||||
|
## 2026-07-10 — #63 generated Waybar logo + powermenu
|
||||||
|
- **Task:** NEXT #63 — reverse parity: generated bar lacked whole-swap
|
||||||
|
logo → menu and power icon → power menu.
|
||||||
|
- **Did:** `custom/nomarchy` (left, Nomarchy.ttf U+F000) + `custom/powermenu`
|
||||||
|
(right, U+F011 → `nomarchy-menu power`) in generated settings/style.
|
||||||
|
Fixed Nix `\uf000` pitfall (no `\u` escapes — use literal UTF-8).
|
||||||
|
Whole-swaps already had both; no JSON/CSS edits. Closed #63.
|
||||||
|
- **Verified:** **V2** — `THEME=tokyo-night` theme-shot: left monogram N,
|
||||||
|
right power glyph, accent styling; config modules-left/right correct.
|
||||||
|
Crop paths under `/tmp/theme-shot-tn2-*.png`.
|
||||||
|
- **Next suggestion:** product path #55 fingerprint menu (or #57 live install).
|
||||||
|
|
||||||
161
agent/LOOP.md
Normal file
161
agent/LOOP.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# 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
|
||||||
|
harness- and vendor-agnostic; the same iteration works under any of:
|
||||||
|
|
||||||
|
- **An interactive self-paced loop** in any agent harness (e.g. `/loop`
|
||||||
|
in Claude Code) — the agent iterates until stopped.
|
||||||
|
- **Headless** (a one-shot CLI invocation, 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) |
|
||||||
|
|
||||||
|
Instructions live next to the state: `VERIFICATION.md` (enforcement),
|
||||||
|
`DELEGATION.md` (tiers/roles/economy), `GOALS.md`, `THEME-DESIGN.md`.
|
||||||
|
|
||||||
|
## Model & token economy
|
||||||
|
|
||||||
|
Spend expensive tokens on judgment, not mechanics. Tiers, roles, and the
|
||||||
|
full delegation rules are in **`DELEGATION.md`**; the loop-specific
|
||||||
|
habits:
|
||||||
|
|
||||||
|
- **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
|
||||||
|
frontier-tier model running the loop.
|
||||||
|
- **Delegate mechanical subtasks down.** 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
|
||||||
|
light/standard-tier subagent per `DELEGATION.md`. The strong model
|
||||||
|
writes the spec, reviews the result, and owns the commit.
|
||||||
|
- **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`.
|
||||||
|
4. **Sync sweep.** Grep the item's number and feature name across
|
||||||
|
`agent/` and `docs/` and update or delete every cross-reference the
|
||||||
|
ship made stale: the PROPOSED pitch that spawned it (and BACKLOG's
|
||||||
|
v1.0 pointer), HARDWARE-QUEUE entries it supersedes, ROADMAP/README/
|
||||||
|
docs mentions of the old behavior. While there, prune checked-off
|
||||||
|
`[x]` HARDWARE-QUEUE entries whose outcome is already recorded
|
||||||
|
(journal/ROADMAP — git history is the archive). A shipped item must
|
||||||
|
leave no stale pointer behind; the sweep rides in the same commit.
|
||||||
|
|
||||||
|
### 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.
|
||||||
170
agent/MEMORY.md
Normal file
170
agent/MEMORY.md
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# 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.
|
||||||
|
- **Don't default a "timer/session" feature to V3 — most of it is VM-testable.**
|
||||||
|
A scheduled/session behaviour usually decomposes into a *generic* step
|
||||||
|
already covered elsewhere (e.g. `home-manager switch`, exercised by every
|
||||||
|
theme apply) and a *specific* decision (which theme, when). Stub the generic
|
||||||
|
step (`NOMARCHY_REBUILD=<marker>` for theme-sync) and **simulate time by
|
||||||
|
moving the VM clock** (`date -s`, `timedatectl set-ntp false`, `time.timeZone
|
||||||
|
= "UTC"`), then assert the decision + state change headlessly. `checks.auto-theme`
|
||||||
|
does exactly this for #79's sunset/sunrise. Only the literal
|
||||||
|
timer-fires-on-`OnCalendar` is truly on-hardware, and `systemd-analyze
|
||||||
|
calendar` validates that schedule. (I first mis-framed #79 as V3 — it's V2.)
|
||||||
|
- **`writeShellScriptBin` scripts run `set -euo pipefail`** (nomarchy-doctor,
|
||||||
|
the menu, lifecycle CLIs). So a **no-match `grep` inside `$(…)`** (grep exits
|
||||||
|
1 → command-sub fails → abort) and a **standalone `cond && action`** (false
|
||||||
|
cond → abort) both kill the script mid-run — the tell is output that stops
|
||||||
|
before the final/verdict line with no error. Guard: `… | grep … || true`
|
||||||
|
inside `$()`, `cmd 2>/dev/null || echo 0` for captures, and `if` instead of
|
||||||
|
`&& action`. (#77 doctor hibernate section; caught by the checks.doctor VM
|
||||||
|
test on first run.)
|
||||||
|
- 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).
|
||||||
|
- **Hibernation reference (Latitude / Newton, BACKLOG #76):** LUKS whole
|
||||||
|
root BTRFS; `@swap` → `/swap`; file `/swap/swapfile`;
|
||||||
|
`boot.resumeDevice` = LUKS root UUID; `resume_offset` from
|
||||||
|
`btrfs inspect-internal map-swapfile -r`. Swap is encrypted with root
|
||||||
|
(not a cleartext partition). No zram on that box yet — zram is additive
|
||||||
|
for live pressure only. Installer already creates this when swapSize>0.
|
||||||
|
- CI (`.gitea/workflows/check.yml`) is **eval-tier only** (standing
|
||||||
|
decision 2026-07-10): act_runner docker-compose on the Gitea VPS; no
|
||||||
|
KVM there. Full VM suite is BACKLOG **FUTURE #20**, not NEXT — needs a
|
||||||
|
separate nix+/dev/kvm runner. Container gotchas are 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).
|
||||||
|
- 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: `nomarchy-pull` (lock) then
|
||||||
|
`nomarchy-rebuild` then `nomarchy-home`, or desktop changes are silently
|
||||||
|
skipped against the old
|
||||||
|
lock (README § 3).
|
||||||
|
- Hyprland 0.55 renames `stayfocused` → `stay_focused` (and similar
|
||||||
|
underscore effects); `stayfocused 1` is `invalid field type` at parse
|
||||||
|
(§ polkit workspace rules, 2026-07-10 hardware).
|
||||||
|
- 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).
|
||||||
|
- Agent instructions live vendor-neutrally in `agent/` (VERIFICATION,
|
||||||
|
DELEGATION, THEME-DESIGN; entry AGENTS.md) — `.claude/` is a thin
|
||||||
|
adapter (permissions + subagent defs only; skills were removed
|
||||||
|
2026-07-11). 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).
|
||||||
54
agent/README.md
Normal file
54
agent/README.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Agent instructions + loop state
|
||||||
|
|
||||||
|
Everything an AI agent needs to work on Nomarchy, vendor-neutral and
|
||||||
|
git-tracked. Protocol: **[LOOP.md](LOOP.md)**. Entry point for every
|
||||||
|
harness: repo-root **[AGENTS.md](../AGENTS.md)**.
|
||||||
|
|
||||||
|
## Instructions (how to work)
|
||||||
|
|
||||||
|
| File | Who writes | Role |
|
||||||
|
|------|------------|------|
|
||||||
|
| [LOOP.md](LOOP.md) | Human | One-iteration protocol (orient → pick → work → verify → commit → record) + the V0–V3 ladder |
|
||||||
|
| [VERIFICATION.md](VERIFICATION.md) | Human (agents propose) | Enforcement: preflight, honesty rules, visual protocol, hardware-blocked checks, reporting |
|
||||||
|
| [DELEGATION.md](DELEGATION.md) | Human (agents propose) | Capability tiers, scout/runner roles, token economy, parallel fan-out |
|
||||||
|
| [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 |
|
||||||
|
| [THEME-DESIGN.md](THEME-DESIGN.md) | Human (agents propose) | Theme/visual design instructions |
|
||||||
|
|
||||||
|
## State (what's happening)
|
||||||
|
|
||||||
|
| File | Who writes | Role |
|
||||||
|
|------|------------|------|
|
||||||
|
| [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; older → [JOURNAL-ARCHIVE.md](JOURNAL-ARCHIVE.md)) |
|
||||||
|
| [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 |
|
||||||
|
|
||||||
|
## Harness adapters (vendor-specific, thin)
|
||||||
|
|
||||||
|
Shared content never lives in an adapter — adapters only register/route
|
||||||
|
into the files above, in whatever format their harness requires.
|
||||||
|
|
||||||
|
| Path | Harness | Role |
|
||||||
|
|------|---------|------|
|
||||||
|
| [../AGENTS.md](../AGENTS.md) | any | Entry point (`CLAUDE.md` is a symlink to it) |
|
||||||
|
| [../.claude/settings.json](../.claude/settings.json) | Claude Code | Tool permissions |
|
||||||
|
| [../.claude/agents/](../.claude/agents/) | Claude Code | `nomarchy-scout` / `nomarchy-runner` role defs (contracts in [DELEGATION.md](DELEGATION.md)) |
|
||||||
|
|
||||||
|
Do **not** put backlog items, vision text, or policy under an adapter
|
||||||
|
directory — 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.
|
||||||
49
agent/THEME-DESIGN.md
Normal file
49
agent/THEME-DESIGN.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Theme design — instructions for visual work
|
||||||
|
|
||||||
|
For creating, updating, refining, or troubleshooting Nomarchy themes and
|
||||||
|
visual design. Act as an experienced UI/UX designer and Linux ricing
|
||||||
|
expert for Wayland environments: themes here must be cohesive,
|
||||||
|
intentional, and visually striking — aesthetics are a load-bearing
|
||||||
|
feature of this distro (`agent/GOALS.md` pillar 4).
|
||||||
|
|
||||||
|
## Context & architecture
|
||||||
|
|
||||||
|
All theme data lives in `themes/` at the repo root — one JSON palette per
|
||||||
|
theme plus per-app assets (whole-swap `waybar.jsonc`/CSS, btop themes,
|
||||||
|
wallpapers). Before generating any new configuration, read the existing
|
||||||
|
files there: learn how current themes are structured, how the syntax is
|
||||||
|
formatted for each application, and how they integrate into the NixOS/HM
|
||||||
|
modules. Always match the established pattern — there is no second
|
||||||
|
theming pipeline (`agent/GOALS.md` non-goals), so a new visual surface
|
||||||
|
consumes the palette from the state JSON, never a side file.
|
||||||
|
|
||||||
|
## Design responsibilities
|
||||||
|
|
||||||
|
1. **Holistic design:** a theme spans Hyprland (borders, shadows,
|
||||||
|
animations), Waybar, Ghostty, btop, fastfetch, rofi, and wallpaper —
|
||||||
|
one coherent identity, no unthemed corner.
|
||||||
|
2. **Color theory:** create or adapt advanced palettes. Draw inspiration
|
||||||
|
from established aesthetics (Everforest, Nord, Gruvbox) or r/unixporn
|
||||||
|
trends, but innovate. Ensure harmony between background, foreground,
|
||||||
|
accents, and warning/error colors.
|
||||||
|
3. **Typography & iconography:** pair UI and monospace fonts and icon
|
||||||
|
themes to the specific vibe of the palette.
|
||||||
|
4. **Accessibility:** high contrast for text readability; status
|
||||||
|
information is never color-only (see `agent/MEMORY.md` design
|
||||||
|
invariants — glyph/shape carries state, and identity themes are
|
||||||
|
deliberately not traffic-lights).
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. Read `themes/` to understand the current structure.
|
||||||
|
2. State the "vibe", the primary palette (hex codes), and typography
|
||||||
|
choices of the proposed design before implementing.
|
||||||
|
3. Implement by generating or updating files within the `themes/`
|
||||||
|
structure, following `agent/CONVENTIONS.md` (Waybar parity rule:
|
||||||
|
whole-swap `waybar.jsonc` files stay in sync with the generated
|
||||||
|
config).
|
||||||
|
4. Verify per `agent/VERIFICATION.md` §3 — scripted checks first
|
||||||
|
(`tools/check-theme-contrast.py`, `tools/audit-theme-design.py`), then
|
||||||
|
headless before/after screenshots you actually view, under at least
|
||||||
|
two themes. New themes are imported via `tools/import-palettes.py` and
|
||||||
|
round-tripped through the theme switcher.
|
||||||
178
agent/VERIFICATION.md
Normal file
178
agent/VERIFICATION.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# Verification — the enforcement rules
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
This document is an **enforcer**, not the workflow itself: the ladder and
|
||||||
|
iteration protocol live in `agent/LOOP.md`, VM instructions and gotchas in
|
||||||
|
`docs/TESTING.md`. It applies to **every** change to this repo — features,
|
||||||
|
fixes, theming, module changes, lock bumps, docs, backlog grooming.
|
||||||
|
"Small" or "obvious" changes are exactly where verification gets skipped,
|
||||||
|
so they trigger it too. If this file and those docs ever disagree, 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" (§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`. Four 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 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 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
|
||||||
|
(`agent/GOALS.md` is the full statement):
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
437
docs/HARDWARE.md
Normal file
437
docs/HARDWARE.md
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
# 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; first-class wrappers deferred for v1):**
|
||||||
|
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). No `nomarchy.hardware.nvidia.*`
|
||||||
|
stack for v1 — hybrid PRIME remains wiki/plain NixOS until a maintainer
|
||||||
|
commits to hardware testing.
|
||||||
|
|
||||||
|
## 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 / CC — live sysfs + state; boot oneshot re-applies | No rebuild for live; rebuild bakes Nix option |
|
||||||
|
| 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 |
|
||||||
415
docs/MIGRATION.md
Normal file
415
docs/MIGRATION.md
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
# 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`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enabling hibernation on an existing machine (no reinstall)
|
||||||
|
|
||||||
|
New Nomarchy installs are hibernation-ready out of the box — the installer
|
||||||
|
defaults the swapfile to **= RAM** on its own `@swap` subvolume and wires the
|
||||||
|
resume offset. This section is for machines that have **no hibernate swap**:
|
||||||
|
one installed with `swap = 0`, one migrated here whose reused
|
||||||
|
`hardware-configuration.nix` carries no swapfile, or an older install
|
||||||
|
predating the default. If `swapon --show` already lists `/swap/swapfile`,
|
||||||
|
you have nothing to do.
|
||||||
|
|
||||||
|
Hibernation writes RAM to disk, and zram (compressed *RAM*) can't hold that
|
||||||
|
image across a power-off — so you need a real disk swap ≥ the RAM you use.
|
||||||
|
Nomarchy puts it on an `@swap` subvolume *inside* the encrypted volume, so
|
||||||
|
the image is encrypted at rest and the initrd LUKS unlock gates resume.
|
||||||
|
|
||||||
|
> **Reversible:** this is one subvolume plus four config lines. Remove them
|
||||||
|
> and rebuild — or just boot the previous generation — to undo. `/home` is
|
||||||
|
> never touched.
|
||||||
|
|
||||||
|
Worked example below is **this machine**: LUKS mapper `cryptroot`, btrfs `@`.
|
||||||
|
Substitute your own device/UUID/offset where shown.
|
||||||
|
|
||||||
|
### 1. Size and locate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Swap = RAM, rounded up to whole GiB (matches the installer default).
|
||||||
|
ram_gb=$(awk '/MemTotal/ {print int(($2 + 1048575) / 1048576)}' /proc/meminfo)
|
||||||
|
|
||||||
|
# The decrypted BTRFS device backing / (strip the [subvol] suffix) and its
|
||||||
|
# filesystem UUID — the same UUID resolves to the mapper once initrd unlocks.
|
||||||
|
dev=$(findmnt -no SOURCE / | sed 's/\[.*//') # e.g. /dev/mapper/cryptroot
|
||||||
|
fsuuid=$(findmnt -no UUID /) # e.g. d8e2b02d-…
|
||||||
|
echo "swap=${ram_gb}G dev=$dev uuid=$fsuuid"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create the `@swap` subvolume + swapfile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create the subvolume at the BTRFS top level (subvolid=5), beside @, @home…
|
||||||
|
sudo mkdir -p /mnt/btrfs-top
|
||||||
|
sudo mount -o subvolid=5 "$dev" /mnt/btrfs-top
|
||||||
|
sudo btrfs subvolume create /mnt/btrfs-top/@swap
|
||||||
|
sudo umount /mnt/btrfs-top && sudo rmdir /mnt/btrfs-top
|
||||||
|
|
||||||
|
# Mount it and create the swapfile. `mkswapfile` applies the BTRFS NOCOW
|
||||||
|
# requirements automatically (a copy-on-write swapfile would corrupt).
|
||||||
|
sudo mkdir -p /swap
|
||||||
|
sudo mount -o subvol=@swap,noatime "$dev" /swap
|
||||||
|
sudo btrfs filesystem mkswapfile --size "${ram_gb}g" --uuid clear /swap/swapfile
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Read the resume offset
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo btrfs inspect-internal map-swapfile -r /swap/swapfile # prints the offset
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Wire it into `system.nix`
|
||||||
|
|
||||||
|
Add these to your machine's `system.nix`, substituting your `$fsuuid` and the
|
||||||
|
offset from step 3. The `fileSystems."/swap"` mount is **required** on this
|
||||||
|
path — a fresh install inherits it from disko-generated
|
||||||
|
`hardware-configuration.nix`, but a hand edit must declare it so `/swap` is
|
||||||
|
mounted before swap activates:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# Hibernation: encrypted swapfile on the @swap subvolume.
|
||||||
|
fileSystems."/swap" = {
|
||||||
|
device = "/dev/disk/by-uuid/<fsuuid>";
|
||||||
|
fsType = "btrfs";
|
||||||
|
options = [ "subvol=@swap" "noatime" ];
|
||||||
|
};
|
||||||
|
swapDevices = [{ device = "/swap/swapfile"; }];
|
||||||
|
boot.resumeDevice = "/dev/disk/by-uuid/<fsuuid>";
|
||||||
|
boot.kernelParams = [ "resume_offset=<offset>" ];
|
||||||
|
```
|
||||||
|
|
||||||
|
zram stays on by default (Nomarchy sets `zramSwap` at priority 100 in
|
||||||
|
`modules/nixos/oom.nix`); this disk swapfile sits lower, so day-to-day
|
||||||
|
paging stays in compressed RAM and the file is reserved for the hibernate
|
||||||
|
image — exactly the intent.
|
||||||
|
|
||||||
|
### 5. Rebuild and test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nixos-rebuild switch --flake ~/.nomarchy#default
|
||||||
|
systemctl hibernate # or nomarchy-menu → Power → Hibernate
|
||||||
|
```
|
||||||
|
|
||||||
|
The machine powers off; on the next boot you unlock LUKS once and land back
|
||||||
|
in your session. If you get a *fresh* boot instead, the usual cause is a
|
||||||
|
wrong `resume_offset` or a swapfile smaller than in-use RAM — re-read the
|
||||||
|
offset (step 3) and confirm `swap ≥ RAM`.
|
||||||
|
|
||||||
|
**No LUKS?** Same steps; `dev`/`$fsuuid` point at the plain BTRFS partition
|
||||||
|
and the image is unencrypted at rest. **`swap = 0` opt-out** stays valid — if
|
||||||
|
you don't want hibernation, skip all of this; the Power-menu Hibernate row
|
||||||
|
just reports that no swap is configured.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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) |
|
||||||
125
docs/OMARCHY.md
Normal file
125
docs/OMARCHY.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# Coming from Omarchy
|
||||||
|
|
||||||
|
Nomarchy and [Omarchy](https://omarchy.org) are cousins: the same
|
||||||
|
Hyprland + Waybar desktop feel, the same opinionated "beautiful out of the
|
||||||
|
box," one-command theming, and a keyboard-driven menu. If you liked
|
||||||
|
Omarchy, you'll be at home here in minutes.
|
||||||
|
|
||||||
|
The one real difference is the **foundation**:
|
||||||
|
|
||||||
|
| | Omarchy | Nomarchy |
|
||||||
|
|------------|---------|----------|
|
||||||
|
| Base | Arch Linux | NixOS |
|
||||||
|
| Config | **imperative** dotfiles you edit in `~/.config`, live | **declarative** — built from a flake; changed via the menu or your `home.nix`/`system.nix` |
|
||||||
|
| Packages | `pacman` / `yay` / AUR, anytime | `nixpkgs` — added to your flake (no AUR) |
|
||||||
|
| Recovery | reinstall / restore dotfiles | **every rebuild is a bootable generation** — roll back from the boot menu |
|
||||||
|
|
||||||
|
This page maps the day-to-day deltas. Nothing about the *desktop* has to
|
||||||
|
be relearned — only *how you change it*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The one mental shift: declarative, not dotfiles
|
||||||
|
|
||||||
|
In Omarchy you open a file under `~/.config`, save it, and the change is
|
||||||
|
live. In Nomarchy the whole desktop is **built from your flake**, so you
|
||||||
|
change it one of two ways:
|
||||||
|
|
||||||
|
1. **The menu** (`SUPER+M`) — the everyday path. It writes the change into
|
||||||
|
your flake's state and rebuilds for you. You never hand-edit a config
|
||||||
|
for theming, night light, wallpaper, keyboard layout, power, etc.
|
||||||
|
2. **Your flake** — for anything the menu doesn't cover, edit
|
||||||
|
`home.nix` / `system.nix` (a handful of `nomarchy.*` options and plain
|
||||||
|
Home-Manager/NixOS), then run a rebuild.
|
||||||
|
|
||||||
|
The payoff for the extra indirection: **a bad change never bricks you.**
|
||||||
|
Every `nomarchy-rebuild` is a new NixOS generation; if one misbehaves,
|
||||||
|
reboot and pick the previous entry from the boot menu — your files
|
||||||
|
(`/home`) are never touched. See [RECOVERY.md](RECOVERY.md).
|
||||||
|
|
||||||
|
> Editing `~/.config/hypr/*` by hand won't stick — Home-Manager owns those
|
||||||
|
> files and rewrites them on the next rebuild. Change the source (menu or
|
||||||
|
> flake) instead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Omarchy** | Run the bootstrap on a fresh Arch install. |
|
||||||
|
| **Nomarchy** | Boot the ISO and run **`nomarchy-install`** (single-disk UEFI, optional LUKS, hibernation-ready swap). |
|
||||||
|
| **Already on NixOS?** | Convert in place, no reformat — [MIGRATION.md](MIGRATION.md). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Keybindings (they'll feel familiar)
|
||||||
|
|
||||||
|
The full, always-current list is one keypress away: **`SUPER+?`** opens the
|
||||||
|
live cheatsheet — you never have to memorize this table.
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| `SUPER+Return` | Terminal |
|
||||||
|
| `SUPER+Space` | App launcher |
|
||||||
|
| `SUPER+M` | Main menu (theme, network, audio, power, tools…) |
|
||||||
|
| `SUPER+T` | Theme picker |
|
||||||
|
| `SUPER+SHIFT+T` | Next wallpaper |
|
||||||
|
| `SUPER+E` | File manager (yazi) |
|
||||||
|
| `SUPER+X` | Power menu (lock / suspend / hibernate / reboot …) |
|
||||||
|
| `SUPER+?` | Keybindings cheatsheet |
|
||||||
|
| `Print` | Screenshot region → clipboard |
|
||||||
|
| `SUPER+CTRL+<key>` | Jump straight to a menu module (clipboard `V`, calc `C`, emoji `E`, capture `S`, …) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Theming
|
||||||
|
|
||||||
|
Both distros reskin everything from one place. In Nomarchy:
|
||||||
|
|
||||||
|
- **`SUPER+T`** (or `nomarchy-menu theme`) — pick from 24 presets with live
|
||||||
|
previews; it rebuilds and applies to Hyprland, Waybar, the terminal,
|
||||||
|
btop and GTK/Qt at once.
|
||||||
|
- Or from a shell: `nomarchy-theme-sync apply <slug>` (e.g. `gruvbox`,
|
||||||
|
`rose-pine`, `nord`).
|
||||||
|
- Tweak gaps / rounding / fonts / opacity / icon pack without leaving the
|
||||||
|
theme system — see [OVERRIDES.md](OVERRIDES.md). One git-tracked JSON
|
||||||
|
(`theme-state.json`) is the single source of truth.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installing software
|
||||||
|
|
||||||
|
There is no AUR. Two paths:
|
||||||
|
|
||||||
|
- **Keep it:** add the package to `home.packages` in your flake's
|
||||||
|
`home.nix`, then `nomarchy-home`. It's now declared, reproducible, and
|
||||||
|
part of your rollback history. A curated default app set already ships —
|
||||||
|
you extend the list.
|
||||||
|
- **Just this once:** `nix shell nixpkgs#<pkg>` drops you into a shell with
|
||||||
|
it on `PATH`, gone when you exit — the equivalent of a throwaway install.
|
||||||
|
|
||||||
|
Search with `nsearch <name>` (`nix search nixpkgs`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Update & roll back
|
||||||
|
|
||||||
|
| | Omarchy | Nomarchy |
|
||||||
|
|---|---|---|
|
||||||
|
| **Update** | update command | `nomarchy-pull` (bump inputs), then `nomarchy-rebuild` (system) + `nomarchy-home` (desktop) |
|
||||||
|
| **Roll back** | restore dotfiles | reboot → pick the previous **generation** in the boot menu (or a snapper snapshot) |
|
||||||
|
| **Health check** | — | `nomarchy-doctor` (System › Doctor) — read-only pass/fail sheet, each ✖ shows its fix |
|
||||||
|
|
||||||
|
On a failed rebuild, `nomarchy-rebuild` / `nomarchy-home` print the last
|
||||||
|
log lines and point you at `nomarchy-doctor` and [RECOVERY.md](RECOVERY.md)
|
||||||
|
— you're never left guessing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's exactly the same
|
||||||
|
|
||||||
|
Hyprland tiling and feel, Waybar, the rofi-driven menu, the blur/rounding
|
||||||
|
aesthetic, keyboard-first everything, and strong opinionated defaults. The
|
||||||
|
desktop is the part you already know — this page is only about the NixOS
|
||||||
|
plumbing underneath.
|
||||||
@@ -21,6 +21,61 @@ nomarchy-theme-sync set fonts.mono "FiraCode Nerd Font"
|
|||||||
nomarchy-theme-sync apply gruvbox # whole palette
|
nomarchy-theme-sync apply gruvbox # whole palette
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Icon pack
|
||||||
|
|
||||||
|
Icons follow the active theme's light/dark mode using **Papirus** — the only
|
||||||
|
icon pack shipped by default (Papirus alone is ~1 GiB, so extra packs are
|
||||||
|
opt-in rather than a cost every install pays). To switch to another pack:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nomarchy-theme-sync set icons "Tela-dark" # or "" to return to Papirus-by-mode
|
||||||
|
```
|
||||||
|
|
||||||
|
Only the pack you name is pulled into your system (the first switch downloads
|
||||||
|
it). Known packs and an example theme name from each:
|
||||||
|
|
||||||
|
| Set `icons` to… | Pack |
|
||||||
|
|---|---|
|
||||||
|
| `Papirus`, `Papirus-Dark`, `Papirus-Light` | Papirus (default; `""` auto-picks Dark/Light by mode) |
|
||||||
|
| `Tela`, `Tela-dark`, `Tela-<color>[-dark]` | Tela (colors: blue, green, red, purple, nord, dracula, …) |
|
||||||
|
| `Qogir`, `Qogir-Dark`, `Qogir-Light` | Qogir (note the capital D/L) |
|
||||||
|
| `Reversal`, `Reversal-dark` | Reversal |
|
||||||
|
| `Numix-Circle`, `Numix-Circle-Light` | Numix Circle |
|
||||||
|
|
||||||
|
The choice is a **sticky global override** — it survives `apply <palette>`
|
||||||
|
switches (presets don't carry an icon field). To add a pack that isn't listed,
|
||||||
|
append a row to `iconPacks` in `modules/home/theme.nix`. Set `icons` to `""`
|
||||||
|
any time to drop back to Papirus with automatic Dark/Light.
|
||||||
|
|
||||||
|
### Auto theme (day/night)
|
||||||
|
|
||||||
|
Switch automatically between a light **day** theme and a dark **night** theme
|
||||||
|
on a schedule — the same one engine as a manual `apply`, no second pipeline.
|
||||||
|
Turn it on from **Look & Feel › Auto theme** (`SUPER+M`): toggle it, pick the
|
||||||
|
day and night themes, and set the sunrise/sunset times. The same from a shell:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nomarchy-theme-sync set settings.autoTheme.day summer-day --no-switch
|
||||||
|
nomarchy-theme-sync set settings.autoTheme.night summer-night --no-switch
|
||||||
|
nomarchy-theme-sync set settings.autoTheme.sunset 20:00 --no-switch
|
||||||
|
nomarchy-theme-sync set settings.autoTheme.enable true --no-switch
|
||||||
|
nomarchy-theme-sync auto --force # one rebuild: installs the timer + applies now
|
||||||
|
```
|
||||||
|
|
||||||
|
State lives in `settings.autoTheme`:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `enable` | on / off |
|
||||||
|
| `day` / `night` | theme slugs — e.g. `summer-day` / `summer-night` (see `nomarchy-theme-sync list`) |
|
||||||
|
| `sunrise` / `sunset` | switch times, `"HH:MM"` (24-hour) |
|
||||||
|
|
||||||
|
A timer re-checks every ~15 minutes: the day theme applies around sunrise, the
|
||||||
|
night theme around sunset — but it only rebuilds when the active theme
|
||||||
|
actually needs to change, so most checks do nothing. Enabling rebuilds once
|
||||||
|
(to install the timer); disabling is instant. Preview the current decision
|
||||||
|
without switching with `nomarchy-theme-sync auto --which`.
|
||||||
|
|
||||||
These values are deliberately kept at normal priority in the modules, so they
|
These values are deliberately kept at normal priority in the modules, so they
|
||||||
stay owned by the theme system. If you *insist* on pinning one in `home.nix`
|
stay owned by the theme system. If you *insist* on pinning one in `home.nix`
|
||||||
regardless of the active theme, use `lib.mkForce` (see §4) — but then the CLI
|
regardless of the active theme, use `lib.mkForce` (see §4) — but then the CLI
|
||||||
@@ -39,7 +94,7 @@ in your `home.nix` wins — no `mkForce` needed:
|
|||||||
input.follow_mouse = 0; # was 1
|
input.follow_mouse = 0; # was 1
|
||||||
input.touchpad.natural_scroll = false;
|
input.touchpad.natural_scroll = false;
|
||||||
misc.disable_splash_rendering = 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;
|
animations.enabled = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,6 +105,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
|
### Adding vs. overriding lists
|
||||||
|
|
||||||
`bind`, `bindel`, `bindl`, `bindm` and `exec-once` are lists kept at normal
|
`bind`, `bindel`, `bindl`, `bindm` and `exec-once` are lists kept at normal
|
||||||
@@ -61,7 +121,7 @@ autostarts run *alongside* the defaults:
|
|||||||
wayland.windowManager.hyprland.settings = {
|
wayland.windowManager.hyprland.settings = {
|
||||||
bind = [
|
bind = [
|
||||||
"$mod, B, exec, firefox"
|
"$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" ];
|
exec-once = [ "nm-applet --indicator" ];
|
||||||
};
|
};
|
||||||
@@ -140,6 +200,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 gaps / colors / rounding / fonts | `nomarchy-theme-sync set …` or `apply` |
|
||||||
| Change input / misc / monitor / animations / terminal chrome | plain assignment in `home.nix` |
|
| 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) |
|
| Add keybinds / autostarts | add to the `bind` / `exec-once` list (concatenates) |
|
||||||
| Replace all keybinds | `bind = lib.mkForce [ … ]` |
|
| Replace all keybinds | `bind = lib.mkForce [ … ]` |
|
||||||
| Hardcode an appearance value against the theme | `lib.mkForce` in `home.nix` |
|
| Hardcode an appearance value against the theme | `lib.mkForce` in `home.nix` |
|
||||||
|
|||||||
41
docs/README.md
Normal file
41
docs/README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# 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 |
|
||||||
|
| [OMARCHY.md](OMARCHY.md) | Users | Coming from Omarchy — bindings/theme/install/config map |
|
||||||
|
|
||||||
|
## Related (not under `docs/`)
|
||||||
|
|
||||||
|
| Path | Role |
|
||||||
|
|------|------|
|
||||||
|
| [../AGENTS.md](../AGENTS.md) | Agent entry point, any vendor/harness (`CLAUDE.md` symlinks to it) |
|
||||||
|
| [../agent/README.md](../agent/README.md) | Agent instructions + **executable** loop state: BACKLOG, LOOP, VERIFICATION, … |
|
||||||
|
| [../.claude/](../.claude/) | Claude Code adapter 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 `nomarchy-home`).
|
||||||
|
|
||||||
|
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 `nomarchy-home`.
|
||||||
|
|
||||||
|
## 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 `nomarchy-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.
|
||||||
727
docs/ROADMAP.md
727
docs/ROADMAP.md
@@ -1,9 +1,20 @@
|
|||||||
# Roadmap & changelog
|
# Roadmap & changelog
|
||||||
|
|
||||||
Forward-looking plans, plus a running log of shipped fixes (the
|
Design/decision records and a running log of shipped work (items marked
|
||||||
"Known issues & follow-ups" section). Split out of the README so that
|
✓). Split out of the README so that stays a focused entry point.
|
||||||
stays a focused entry point — what Nomarchy is, how to install it, and
|
|
||||||
how to override it. Items marked ✓ are shipped.
|
> **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
|
## Roadmap
|
||||||
- **Menu system** (apps launcher + theme switching + system actions), built
|
- **Menu system** (apps launcher + theme switching + system actions), built
|
||||||
@@ -16,7 +27,7 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
`theme` (SUPER+T) · `clipboard` (cliphist, SUPER+CTRL+V) · `calc`
|
`theme` (SUPER+T) · `clipboard` (cliphist, SUPER+CTRL+V) · `calc`
|
||||||
(rofi-calc, live, SUPER+CTRL+C) · `files` (fd → xdg-open,
|
(rofi-calc, live, SUPER+CTRL+C) · `files` (fd → xdg-open,
|
||||||
SUPER+CTRL+F) · `emoji` (rofi-emoji, SUPER+CTRL+E) · `web` (Google).
|
SUPER+CTRL+F) · `emoji` (rofi-emoji, SUPER+CTRL+E) · `web` (Google).
|
||||||
SUPER+D is `rofi -show drun`.
|
SUPER+Space is `rofi -show drun`.
|
||||||
- ✓ shipped modules: `network` (nmtui in `$TERMINAL`) · `bluetooth`
|
- ✓ shipped modules: `network` (nmtui in `$TERMINAL`) · `bluetooth`
|
||||||
(blueman-manager) · `capture` (grim/slurp submenu: region/full →
|
(blueman-manager) · `capture` (grim/slurp submenu: region/full →
|
||||||
clipboard/file, saved to `~/Pictures/Screenshots`) · `keybinds` (the
|
clipboard/file, saved to `~/Pictures/Screenshots`) · `keybinds` (the
|
||||||
@@ -28,9 +39,9 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
source** for both the Hyprland binds and the SUPER+? rofi list, so they
|
source** for both the Hyprland binds and the SUPER+? rofi list, so they
|
||||||
can't drift; `nomarchy-menu keybinds` renders the padded two-column
|
can't drift; `nomarchy-menu keybinds` renders the padded two-column
|
||||||
sheet (generated/mouse binds carried in its `extra` rows)
|
sheet (generated/mouse binds carried in its `extra` rows)
|
||||||
- ✓ shipped binds: `SUPER+Space` → `rofi -show drun` (quick launch) ·
|
- ✓ shipped binds: `SUPER+Space` → `rofi -show drun` (app launcher) ·
|
||||||
`SUPER+M` → `nomarchy-menu` (main menu) · `SUPER+?` → the cheatsheet;
|
`SUPER+M` → `nomarchy-menu` (main menu) · `SUPER+?` → the cheatsheet
|
||||||
`SUPER+D` stays `-show drun`
|
(SUPER+D dropped — Space is the only launcher bind)
|
||||||
- launcher icons: ✓ `show-icons` on, drawing from the theme's icon set
|
- launcher icons: ✓ `show-icons` on, drawing from the theme's icon set
|
||||||
(Papirus, via the icon-themes work below)
|
(Papirus, via the icon-themes work below)
|
||||||
- decision record: resolves the old Walker/Lua question — no GTK4
|
- decision record: resolves the old Walker/Lua question — no GTK4
|
||||||
@@ -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
|
one-off — any new feature that earns a menu entry must be placed in the
|
||||||
right submenu (don't let the root creep back to a flat list), with its
|
right submenu (don't let the root creep back to a flat list), with its
|
||||||
direct `SUPER+CTRL+<mnemonic>` bind and self-gating as applicable.
|
direct `SUPER+CTRL+<mnemonic>` bind and self-gating as applicable.
|
||||||
|
- ✓ **Back everywhere:** every list menu now ends with a `↩ Back` entry that
|
||||||
|
returns one level up (power/theme/power-profile/clipboard/files/capture,
|
||||||
|
not just Tools/System), so you never have to Esc out and reopen. A `back`
|
||||||
|
helper + a shared `BACK` label keep it uniform; matched exactly so it can't
|
||||||
|
collide with clipboard/filename content. Esc still quits instantly.
|
||||||
- ✓ **Menu modules from rofi plugins:** the old `calc` flow committed the
|
- ✓ **Menu modules from rofi plugins:** the old `calc` flow committed the
|
||||||
expression blind (result only in the *next* menu's `-mesg`) and `qalc -t`
|
expression blind (result only in the *next* menu's `-mesg`) and `qalc -t`
|
||||||
misparsed common phrasings (`15% of 200` → `rem(15, 1 B)`, the natural-
|
misparsed common phrasings (`15% of 200` → `rem(15, 1 B)`, the natural-
|
||||||
@@ -67,25 +83,136 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
launcher, and yazi (SUPER+E) already covers real browsing.
|
launcher, and yazi (SUPER+E) already covers real browsing.
|
||||||
- **More menu modules from rofi tools:** the script-based counterparts
|
- **More menu modules from rofi tools:** the script-based counterparts
|
||||||
(run via `rofi -dmenu`, like the hand-rolled modules), each a deliberate
|
(run via `rofi -dmenu`, like the hand-rolled modules), each a deliberate
|
||||||
replacement of an existing flow: **rofi-network-manager** (a keyboard
|
replacement of an existing flow.
|
||||||
wifi/VPN picker vs today's `nmtui`-in-terminal `network`), **rofi-rbw /
|
- ✓ **Network** (`networkmanager_dmenu`): a native rofi wifi/VPN picker
|
||||||
rofi-pass** (a secrets module — Bitwarden via rbw, or `pass` — pairs with
|
replacing the old `nmtui`-in-terminal `network` flow. Configured (xdg
|
||||||
`keys.nix`), and **rofi-pulse-select** (an audio sink/source switcher).
|
`networkmanager-dmenu/config.ini`) to drive rofi with `rofi_highlight`
|
||||||
Decide per-module whether it earns replacing the current path.
|
for connected/available rows; editing a connection drops to nmtui in the
|
||||||
|
terminal. Inherits the generated theme like every other module.
|
||||||
|
- ✓ **Audio** (`rofi-pulse-select`): a PipeWire/pulse sink/source switcher
|
||||||
|
under System → Audio (Output/Input), self-gated on the pulse socket.
|
||||||
|
- Deferred: **rofi-rbw / rofi-pass** secrets module — no Bitwarden/`pass`
|
||||||
|
setup here today (secrets are gpg/ssh + gnome-keyring, see `keys.nix`),
|
||||||
|
so it'd mean adopting a new secret manager. Revisit if that changes.
|
||||||
|
- Also: menu search is now **case-insensitive fuzzy** (`matching = fuzzy`,
|
||||||
|
`sorting-method = fzf`) across every module + the launcher.
|
||||||
|
- ✓ **Printer setup menu module:** a `nomarchy-menu printers` entry in the
|
||||||
|
**System** submenu opens **system-config-printer** (the CUPS admin GUI —
|
||||||
|
discovery, drivers/PPDs, options, test page), mirroring the bluetooth/blueman
|
||||||
|
pattern: the printing service ships the package (`environment.systemPackages`)
|
||||||
|
and the menu execs it, self-gated on the `system-config-printer` binary so
|
||||||
|
the entry appears only when `nomarchy.services.printing` is on. No direct
|
||||||
|
`SUPER+CTRL` bind — printer setup is a rare one-off, not a frequent utility
|
||||||
|
(left out deliberately; easy to add). Chose the GUI over a rofi-native
|
||||||
|
`lpadmin` flow (driver/PPD picking is impractical in rofi) and the CUPS web
|
||||||
|
UI (an unthemed browser page). Validated: the generated menu script's
|
||||||
|
`bash -n` build check passes, and an eval confirms printing-on puts
|
||||||
|
system-config-printer in `systemPackages`. Pending an on-machine check.
|
||||||
|
- ✓ **VPN setup & management menu:** a dedicated **System → VPN** flow
|
||||||
|
(`nomarchy-vpn`, `modules/home/rofi.nix`) that goes past the Network module
|
||||||
|
(`networkmanager_dmenu` only connects/disconnects *existing* VPNs) to a guided
|
||||||
|
setup + management surface across the three common kinds:
|
||||||
|
- **WireGuard:** import a `.conf` into NetworkManager (`nmcli connection import
|
||||||
|
type wireguard file …` — NM handles wg tunnels natively, no plugin) and
|
||||||
|
toggle it up/down.
|
||||||
|
- **OpenVPN:** import an `.ovpn` (`nmcli connection import type openvpn file …`);
|
||||||
|
the `networkmanager-openvpn` plugin ships system-side
|
||||||
|
(`networking.networkmanager.plugins`, mkDefault) so the openvpn type is
|
||||||
|
available — import type is chosen by file extension.
|
||||||
|
- **Tailscale:** status (read-only) + `up`/`down` + **exit-node** selection. It
|
||||||
|
lives outside NetworkManager, so the menu drives the `tailscale` CLI
|
||||||
|
directly, **self-gated** on the CLI being present (= `nomarchy.services.tailscale`,
|
||||||
|
which makes the login user the **operator** via `extraSetFlags`). So
|
||||||
|
up/down/exit-node run **inline without sudo**, falling back to a sudo terminal
|
||||||
|
only if the operator grant is absent; the first interactive login uses a
|
||||||
|
terminal (the auth URL is visible).
|
||||||
|
Shape: a `nomarchy-menu vpn` rofi submenu under **System** — NM
|
||||||
|
VPN/WireGuard connections shown ● active / ○ inactive and toggled on select
|
||||||
|
(networkmanager-group users need no sudo), Import via the Files/`fd` picker
|
||||||
|
(`*.conf`/`*.ovpn`), the Tailscale block when present — ending in `↩ Back`. A
|
||||||
|
self-gating Waybar **`custom/vpn`** shield (`nomarchy-vpn-status`: shown only
|
||||||
|
while a NM tunnel or Tailscale is up; `@good` tone; click opens the submenu),
|
||||||
|
wired into the generated bar **and the summer whole-swaps**. Secrets stay in
|
||||||
|
the connection manager's own store (NetworkManager / Tailscale) — no new secret
|
||||||
|
manager (cf. the deferred rofi-rbw/pass note above). **Decided: import-first**
|
||||||
|
— a from-scratch WireGuard keypair/peer editor is too much rofi surface, so
|
||||||
|
creation is deferred to `nm-connection-editor`. Eval + build green; **pending
|
||||||
|
an on-machine check** (the nmcli import/up-down + Tailscale paths need a live
|
||||||
|
session with real configs). Remaining (optional): richer exit-node display
|
||||||
|
(country/city).
|
||||||
- **Theme parity with legacy:** summer-day/night now carry their legacy
|
- **Theme parity with legacy:** summer-day/night now carry their legacy
|
||||||
bar layouts as `waybar.jsonc` whole-swaps (adapted: dead legacy script
|
bar layouts as `waybar.jsonc` whole-swaps (adapted: dead legacy script
|
||||||
modules dropped, Nerd-Fonts-v2 codepoints remapped to FontAwesome/v3,
|
modules dropped, Nerd-Fonts-v2 codepoints remapped to FontAwesome/v3,
|
||||||
logo button opens nomarchy-menu); the other four identity themes are
|
logo button opens nomarchy-menu); the other four identity themes are
|
||||||
palette recolors and already match. Remaining: a visual pass over all
|
palette recolors and already match. Remaining: a visual pass over all
|
||||||
six on the live ISO
|
six on the live ISO
|
||||||
- **Per-theme rofi identity:** the `themes/<slug>/rofi.rasi` whole-swap
|
- ✓ **Per-theme rofi identity:** the `themes/<slug>/rofi.rasi` whole-swap
|
||||||
ships, and summer-day/night carry their legacy designs (inverted window,
|
ships, and all six identity themes now carry a designed `.rasi`. summer-day/
|
||||||
green inputbar, yellow bottom-border). Remaining: author `.rasi`
|
night keep their legacy ports (inverted window, green inputbar, yellow
|
||||||
identities for the other four ported themes if/when they want one (the
|
bottom-border); the other four were authored from each theme's character
|
||||||
generated palette theme is the default and looks fine)
|
(no legacy layout to port — their waybar whole-swaps are palette-only):
|
||||||
- **Faster switches:** move `backgrounds/` out of the flake source (the 86 MB
|
**nord** a soft rounded "frost panel" (frost border, brighter-frost
|
||||||
re-copy on every state write is the main eval tax), then pre-built theme
|
selection, aurora-purple prompt); **retro-82** a sharp CRT terminal (square
|
||||||
variants if still needed
|
corners, amber-on-navy, a teal scanline underline, mono); **lumon** a
|
||||||
|
clinical cyan "bezel" (thick cyan frame, a *framed*-not-filled readout
|
||||||
|
inputbar, mono); **kanagawa** ink-and-paper (warm washi-paper frame, a
|
||||||
|
deeper ink-well inputbar, crystal-blue wave reserved for the selection).
|
||||||
|
Each is self-contained (it replaces the generated theme) and keeps the
|
||||||
|
element structure the theme-grid picker's per-invocation `-theme-str`
|
||||||
|
layers onto; all four parse clean under `rofi -dump-theme`. The generated
|
||||||
|
palette theme stays the default for the other 15 presets. Remaining: a
|
||||||
|
visual pass over the four on hardware (the parse check confirms syntax, not
|
||||||
|
aesthetics).
|
||||||
|
- ✓ **Visual theme picker (preview thumbnails):** `nomarchy-menu theme` is now
|
||||||
|
a rofi **icon grid of real desktop previews** instead of a plain-text list —
|
||||||
|
each theme a screenshot of its themed desktop (waybar + floating terminal),
|
||||||
|
pretty name beneath, **grouped dark-first then light** in one scrollable grid
|
||||||
|
(the previews make the mode obvious, so no light/dark submenu split), the
|
||||||
|
active theme marked `✓`, ending in `↩ Back`. The grid, the Name→slug map and
|
||||||
|
the active mark are all **generated at eval time** from the preset JSONs in
|
||||||
|
`rofi.nix` (`builtins.readDir` + `fromJSON`); "active" is just `t.slug`, since
|
||||||
|
every switch rebuilds the menu. Grid layout via a per-invocation `-theme-str`
|
||||||
|
(`listview { columns: 3; flow: horizontal; }` so Down scrolls row-by-row, +
|
||||||
|
vertical `element` cards, name centred below). **Sizing gotcha (learned the
|
||||||
|
hard way):** rofi's `element-icon` `size` is a **single value → a square
|
||||||
|
cell** (a two-value `WxH` is silently collapsed), and the icon is *contained*
|
||||||
|
in that square. So a 16:9 preview letterboxes (theme-coloured bands top/
|
||||||
|
bottom), and a cell can't be shorter-than-square without the bands returning.
|
||||||
|
The fix: a build-time imagemagick step **centre-crops each preview to a
|
||||||
|
square** so it fills the cell edge-to-edge; the window width is derived so a
|
||||||
|
column is exactly the icon side (no slack margins). One knob, `themeGridIconW`
|
||||||
|
(240px), drives both icon and window. **Workflow:** Bernardo captures previews
|
||||||
|
on real hardware and commits them as `themes/<slug>/preview.png`, **already
|
||||||
|
downscaled to 480×270** (~2.4 MB total for all 21, vs ~28 MB full-res) — the
|
||||||
|
source stays 16:9 and untouched; only the *displayed* thumb is squared at
|
||||||
|
build, so the crop is reversible. Graceful fallback when a theme has no
|
||||||
|
`preview.png`: a plain-name row, so it degrades cleanly.
|
||||||
|
(A headless VM-render route — `runNixOSTest` + software-GL Hyprland
|
||||||
|
(`LIBGL_ALWAYS_SOFTWARE` on virtio-gpu) + `machine.screenshot()` QMP
|
||||||
|
framebuffer dump — was prototyped 2026-06-19 and **works** (themed waybar
|
||||||
|
rendered + captured); real-hardware capture was chosen for fidelity +
|
||||||
|
simplicity, with the VM route as a documented fallback if hand-capturing all
|
||||||
|
themes gets tedious.)
|
||||||
|
- **Faster switches:** move `backgrounds/` out of the flake source. Diagnosis
|
||||||
|
(confirmed): `themes/` is 86 MB and `backgrounds/` is **all** of it — the
|
||||||
|
palette JSONs + per-theme overrides are only ~208 KB, and the wallpapers are
|
||||||
|
**never read at Nix eval** (only the Python `swww` path uses them). But
|
||||||
|
`theme-state.json` is git-tracked, so every `apply` rewrites it → the flake
|
||||||
|
tree changes → Nix re-copies the whole 86 MB source before `home-manager
|
||||||
|
switch` can evaluate. You pay an 86 MB copy to change a 1 KB file.
|
||||||
|
**Decided approach (deferred — don't want the extra moving part yet):**
|
||||||
|
Option 1, a **separate pinned wallpapers artifact** — a `Nomarchy-wallpapers`
|
||||||
|
repo or release tarball, pulled once via a flake input / `fetchurl` (pinned
|
||||||
|
by hash → content-addressed, never re-copied on a state write), with
|
||||||
|
`nomarchy-theme-sync` reading wallpapers from that stable store path (the
|
||||||
|
`NOMARCHY_DEFAULT_THEMES` env hook already anticipates external theme
|
||||||
|
assets). Keeps eval **pure**; the live ISO still bakes them in (fetched at
|
||||||
|
build). A state write then re-copies only ~208 KB. Rejected alternative:
|
||||||
|
moving `theme-state.json` out to `~/.config` + `--impure` eval (one repo, no
|
||||||
|
second artifact, but trades away the in-tree-pinned-state reproducibility).
|
||||||
|
Follow-on if `home-manager switch` itself is still the bottleneck after the
|
||||||
|
copy is gone: **pre-built theme variants** (build each theme's generation
|
||||||
|
ahead of time so a switch just activates a cached one).
|
||||||
- Greeter (tuigreet/SDDM) theming from the same JSON (Plymouth ships since
|
- Greeter (tuigreet/SDDM) theming from the same JSON (Plymouth ships since
|
||||||
v1: `nomarchy.system.plymouth.*`, background tinted from the state file)
|
v1: `nomarchy.system.plymouth.*`, background tinted from the state file)
|
||||||
- Installer round 2: multi-disk BTRFS RAID, impermanence, BIOS/legacy
|
- Installer round 2: multi-disk BTRFS RAID, impermanence, BIOS/legacy
|
||||||
@@ -99,10 +226,28 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
✓ `isoImage.splashImage` — the vendored vector logo
|
✓ `isoImage.splashImage` — the vendored vector logo
|
||||||
(`modules/nixos/branding/logo.svg`, from legacy) recolored to the palette
|
(`modules/nixos/branding/logo.svg`, from legacy) recolored to the palette
|
||||||
accent on the theme base, built at ISO-build time (`hosts/live.nix`).
|
accent on the theme base, built at ISO-build time (`hosts/live.nix`).
|
||||||
Remaining: `isoImage.grubTheme` so UEFI boot matches the isolinux splash
|
✓ **`isoImage.grubTheme` (UEFI boot matches BIOS):** `hosts/live.nix` now
|
||||||
(needs a full grub theme dir), and the `distroId` question (it changes
|
builds a `nomarchyGrubTheme` dir whose background is the *same* composed
|
||||||
`DEFAULT_HOSTNAME` and upstream `isNixos` checks — needs a test pass;
|
splash image as the isolinux splash (accent logo on base), with a
|
||||||
nixos-* CLI names stay regardless)
|
palette-coloured boot menu in the lower third (clear of the centred logo)
|
||||||
|
and an accent timeout bar. Derived from `nixos-grub2-theme` only to reuse
|
||||||
|
its bundled DejaVu `.pf2` (grub `loadfont`s every `.pf2` in the dir); the
|
||||||
|
stock NixOS `logo.png` is dropped since ours is in the background. Built +
|
||||||
|
structure-verified (theme.txt palette colours, 1920×1080 background, font
|
||||||
|
present). Remaining: a UEFI ISO-boot render check on hardware (the file
|
||||||
|
wiring is confirmed; the visual is not CI-testable, same as the splash).
|
||||||
|
✓ **`distroId = "nomarchy"`:** os-release is now honest — `ID=nomarchy`,
|
||||||
|
`ID_LIKE=nixos` (the standard derivative-distro lineage marker, cf.
|
||||||
|
Ubuntu→debian), `DEFAULT_HOSTNAME=nomarchy`, lsb `DISTRIB_ID`/`CPE_NAME`
|
||||||
|
follow. **Verified safe:** `switch-to-configuration` builds its "is this
|
||||||
|
NixOS?" guard from the *configured* distroId (and `/etc/NIXOS` remains as
|
||||||
|
the fallback), so rebuilds keep working — a new `checks.distro-id`
|
||||||
|
VM-test boots such a system and runs `switch-to-configuration dry-activate`
|
||||||
|
green; nixos-* CLI tools are package names, untouched. The one side effect
|
||||||
|
(isNixos→false blanks the upstream nixos.org URLs) is handled by
|
||||||
|
`extraOSReleaseArgs` restoring `HOME_URL`/`DOCUMENTATION_URL`/`SUPPORT_URL`/
|
||||||
|
`BUG_REPORT_URL` to the project. os-release output eval-verified from the
|
||||||
|
real downstream config.
|
||||||
- ✓ **fastfetch branding:** `modules/home/fastfetch.nix`
|
- ✓ **fastfetch branding:** `modules/home/fastfetch.nix`
|
||||||
(`nomarchy.fastfetch.enable`) — the vendored vector logo, recolored to
|
(`nomarchy.fastfetch.enable`) — the vendored vector logo, recolored to
|
||||||
the palette accent and rendered to compact block-art via chafa at build
|
the palette accent and rendered to compact block-art via chafa at build
|
||||||
@@ -119,9 +264,9 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
`gl`/`glg`, `gp`/`gpl`, `gf`), and a nix block (`ns`, `nr`, `nfu`, `nfc`,
|
`gl`/`glg`, `gp`/`gpl`, `gf`), and a nix block (`ns`, `nr`, `nfu`, `nfc`,
|
||||||
`nsearch`, `ngc`), plus `path` and `reload`. Scope decision: short where it
|
`nsearch`, `ngc`), plus `path` and `reload`. Scope decision: short where it
|
||||||
helps but **never shadow a real binary** (same rule as rg/fd — the git block
|
helps but **never shadow a real binary** (same rule as rg/fd — the git block
|
||||||
deliberately avoids `gs`/ghostscript); system/home rebuilds keep their full
|
deliberately avoids `gs`/ghostscript); lifecycle is `nomarchy-pull` /
|
||||||
`sys-update`/`home-update` names rather than getting cryptic aliases. The set
|
`nomarchy-rebuild` / `nomarchy-home` (full names on PATH, not aliases). The
|
||||||
is documented in README §5 (Day-to-day), and `alias` lists them in-shell.
|
set is documented in README §5 (Day-to-day), and `alias` lists shell shortcuts.
|
||||||
- **Theme-switch feedback:** ✓ the "rebuilding…" notification is now
|
- **Theme-switch feedback:** ✓ the "rebuilding…" notification is now
|
||||||
persistent (timeout 0) and replaced in place by "applied ✓" / failure
|
persistent (timeout 0) and replaced in place by "applied ✓" / failure
|
||||||
via a synchronous tag, so a multi-minute switch never reads as a failed
|
via a synchronous tag, so a multi-minute switch never reads as a failed
|
||||||
@@ -130,8 +275,36 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
- **Icon themes:** ✓ ships `papirus-icon-theme`; the resolved name lives on
|
- **Icon themes:** ✓ ships `papirus-icon-theme`; the resolved name lives on
|
||||||
`nomarchy.theme.iconTheme` (the JSON's optional `icons` field, else
|
`nomarchy.theme.iconTheme` (the JSON's optional `icons` field, else
|
||||||
Papirus-Dark/Light by `mode`) and feeds both Stylix's `gtk.iconTheme`
|
Papirus-Dark/Light by `mode`) and feeds both Stylix's `gtk.iconTheme`
|
||||||
(Thunar/GTK apps) and rofi's `show-icons`. Remaining (optional): per-theme
|
(Thunar/GTK apps) and rofi's `show-icons`.
|
||||||
`icons` overrides for the presets, or shipping more icon packs.
|
✓ **Opt-in cross-pack overrides (no default bloat):** `theme.nix` now
|
||||||
|
resolves `nomarchy.theme.iconThemePackage` from a small pack registry
|
||||||
|
(`iconPacks`: Papirus·Tela·Qogir·Reversal·Numix). Default themes name
|
||||||
|
`Papirus-*`, so the resolved package is the *plain* `papirus-icon-theme`
|
||||||
|
derivation — byte-identical closure, zero added MB. Only when a theme's
|
||||||
|
`icons` names a set from another pack is that pack (and only it)
|
||||||
|
symlinkJoined alongside papirus. Adding a pack = one `iconPacks` row.
|
||||||
|
**Deliberately NOT shipping more packs by default** (Bernardo 2026-07-10):
|
||||||
|
papirus alone is already ~1 GiB and each extra pack adds ~hundreds of MB,
|
||||||
|
so variety is opt-in per install, not a default closure cost.
|
||||||
|
- ✓ **Auto time-of-day theme (#79, `VISION § D`):** an automatic light/dark
|
||||||
|
theme switch through the **same** engine as a manual apply — no second
|
||||||
|
pipeline. `settings.autoTheme = {enable, day, night, sunrise, sunset}`
|
||||||
|
drives `nomarchy-theme-sync auto`, which picks day vs night by the local
|
||||||
|
clock and applies the preset **only if it differs** (idempotent).
|
||||||
|
`modules/home/autotheme.nix` installs a user timer
|
||||||
|
(`OnStartupSec` + `OnCalendar=*:0/15` + `Persistent`, gated on the state
|
||||||
|
flag) that ticks `auto`; Look & Feel › Auto theme toggles it and picks the
|
||||||
|
pair/times. `cmd_apply`'s core was factored into `apply_named` so `auto`
|
||||||
|
reuses it verbatim. Periodic (not exact sunrise/sunset timers) for
|
||||||
|
robustness to suspend/DST; the idempotent `auto` keeps ticks cheap. Enable
|
||||||
|
rebuilds once (installs the timer + `auto --force`); disable is instant
|
||||||
|
(the flag flips and `auto` self-gates). **V2:** `checks.auto-theme` moves
|
||||||
|
the VM clock across a configured sunset/sunrise and asserts the theme flips
|
||||||
|
day→night→day with the right rebuild count (idempotent between) — the
|
||||||
|
`home-manager switch` itself is stubbed via `NOMARCHY_REBUILD` (that
|
||||||
|
generic apply path is exercised by every manual theme change). The only
|
||||||
|
on-hardware residual is the live user timer firing on its `OnCalendar`
|
||||||
|
schedule (the schedule is `systemd-analyze`-validated).
|
||||||
- **Nicer shell out of the box:** ✓ zsh is the default login shell, with a
|
- **Nicer shell out of the box:** ✓ zsh is the default login shell, with a
|
||||||
starship prompt themed from the JSON, autosuggestions + syntax
|
starship prompt themed from the JSON, autosuggestions + syntax
|
||||||
highlighting, and modern-CLI ergonomics — `cat`→bat (theme "ansi", so it
|
highlighting, and modern-CLI ergonomics — `cat`→bat (theme "ansi", so it
|
||||||
@@ -142,7 +315,8 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
`libreoffice-fresh`, `vscode`, `gimp`, `inkscape` — ships *active* in the
|
`libreoffice-fresh`, `vscode`, `gimp`, `inkscape` — ships *active* in the
|
||||||
downstream `templates/downstream/home.nix` `home.packages`, with the heavier
|
downstream `templates/downstream/home.nix` `home.packages`, with the heavier
|
||||||
opt-ins (`texliveFull` — multi-GB; `texliveMedium` is lighter — plus
|
opt-ins (`texliveFull` — multi-GB; `texliveMedium` is lighter — plus
|
||||||
browser/email) commented just below. **Decision: no `nomarchy.apps.*` option
|
browser/email) commented just below — **Chromium is active by default**
|
||||||
|
(2026-07-10); other browsers stay commented. **Decision: no `nomarchy.apps.*` option
|
||||||
surface.** Unlike `nomarchy.services.*` (real config behind each toggle —
|
surface.** Unlike `nomarchy.services.*` (real config behind each toggle —
|
||||||
systemd units, subuid ranges, the Flathub oneshot), a bare package install
|
systemd units, subuid ranges, the Flathub oneshot), a bare package install
|
||||||
has nothing behind the toggle: `mkIf x { environment.systemPackages = [p]; }`
|
has nothing behind the toggle: `mkIf x { environment.systemPackages = [p]; }`
|
||||||
@@ -159,6 +333,36 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
logo/lock/bullet → text, entry/progress-box → surface, progress-bar →
|
logo/lock/bullet → text, entry/progress-box → surface, progress-bar →
|
||||||
accent. Reads on light and dark; follows the theme as of the last system
|
accent. Reads on light and dark; follows the theme as of the last system
|
||||||
rebuild (like the background tint).
|
rebuild (like the background tint).
|
||||||
|
- ✓ **Hibernation + zram by default (BACKLOG #76):** two memory layers ship
|
||||||
|
on by default. `modules/nixos/oom.nix` enables `zramSwap` (zstd, 50% RAM,
|
||||||
|
**priority 100**) as the compressed-RAM layer *before* earlyoom; the
|
||||||
|
installer defaults the disk swap to a **RAM-sized** hibernation-ready
|
||||||
|
swapfile on an `@swap` btrfs subvolume *inside* the encrypted volume, with
|
||||||
|
`patch-template.py` wiring `swapDevices` + `boot.resumeDevice` +
|
||||||
|
`resume_offset` (from `btrfs inspect-internal map-swapfile -r`). The
|
||||||
|
priority split is the point: day-to-day paging stays in zram, so the disk
|
||||||
|
swapfile is reserved for the hibernate image. `swapSize=0` stays a clean
|
||||||
|
no-swap opt-out; the Power-menu Hibernate row is kept unconditional and
|
||||||
|
**notifies on failure** (no-swap machines) rather than silently no-op'ing.
|
||||||
|
Existing machines get an *enable-hibernation* runbook in `docs/MIGRATION.md`
|
||||||
|
(no tool). Guarded by three VM checks — `checks.zram-swap` (device/algo/
|
||||||
|
priority), `checks.hibernate` (a full hibernate→resume→crash cycle on a swap
|
||||||
|
*partition* that proves the resume mechanism + that zram at priority 100
|
||||||
|
doesn't swallow the image), and `checks.hibernate-swapfile` (the installer's
|
||||||
|
btrfs `@swap` *swapfile* build — NOCOW `mkswapfile`, file-type swapon, a
|
||||||
|
valid `map-swapfile -r` offset the kernel accepts, zram above it). A full
|
||||||
|
hibernate from a *runtime* swapfile isn't headless-reachable (systemd
|
||||||
|
`CanHibernate=no` for undeclared swap; and a runtime-computed `resume_offset`
|
||||||
|
can't be fed into the boot cmdline), so the literal encrypted-swapfile
|
||||||
|
power-cycle + LUKS-initrd unlock stays a V3 laptop check.
|
||||||
|
✓ **Doctor hibernate/sleep section (#77, `VISION § C`):** `nomarchy-doctor`
|
||||||
|
gained a read-only hibernate row group — resume device on the cmdline
|
||||||
|
(+ `resume_offset` for a swapfile), disk swap ≥ RAM, zram active, and any
|
||||||
|
suspend/hibernate error in the previous boot's kernel log. Self-gates
|
||||||
|
(`skip` on `swap=0`); only `ok`/`warn`/`skip`, never fails the sheet. The
|
||||||
|
`checks.doctor` VM test asserts the `hibernate` row renders. (Gotcha
|
||||||
|
recorded: the script runs `set -euo pipefail`, so a no-match `grep` inside
|
||||||
|
`$(…)` or a `cond && action` aborts it — use `|| true` and `if`.)
|
||||||
- ✓ **Hibernate double-unlock:** on resume from hibernate the LUKS
|
- ✓ **Hibernate double-unlock:** on resume from hibernate the LUKS
|
||||||
passphrase already gates the machine, but locking hyprlock before sleep
|
passphrase already gates the machine, but locking hyprlock before sleep
|
||||||
meant a second password. Fixed by *not locking* before an encrypted
|
meant a second password. Fixed by *not locking* before an encrypted
|
||||||
@@ -176,8 +380,17 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
comes from the agent's zsh integration; cache TTLs (30 min / 2 h) govern
|
comes from the agent's zsh integration; cache TTLs (30 min / 2 h) govern
|
||||||
re-prompting. gnome-keyring stays the Secret Service (modern versions run
|
re-prompting. gnome-keyring stays the Secret Service (modern versions run
|
||||||
no SSH agent, so no socket contention); screen lock doesn't flush the
|
no SSH agent, so no socket contention); screen lock doesn't flush the
|
||||||
cache. Remaining (optional): a session-level `SSH_AUTH_SOCK` export so GUI
|
cache. ✓ **session-level `SSH_AUTH_SOCK`:** besides the zsh integration,
|
||||||
clients launched outside a shell also see the agent.
|
a `home.sessionVariables` export now covers GUI clients launched outside a
|
||||||
|
shell (rofi launcher, autostarted apps) that never inherit the interactive
|
||||||
|
shell's copy — resolved with `gpgconf --list-dirs agent-ssh-socket` at
|
||||||
|
session-init (the same lookup the shell integration uses, so the two can't
|
||||||
|
drift), reaching GUI apps via the login shell that starts Hyprland the same
|
||||||
|
way `NIXOS_OZONE_WL` does. Verified by building the home generation and
|
||||||
|
inspecting the rendered `hm-session-vars.sh` — it carries
|
||||||
|
`export SSH_AUTH_SOCK="$(…/gpgconf --list-dirs agent-ssh-socket)"` with the
|
||||||
|
command substitution intact (unescaped, resolved at session-init). Pending
|
||||||
|
an on-machine check that a GUI git/ssh client picks up the agent.
|
||||||
- **Sanitize & organize the repo:** a housekeeping pass for consistency
|
- **Sanitize & organize the repo:** a housekeeping pass for consistency
|
||||||
and clarity.
|
and clarity.
|
||||||
- ✓ pruned now-redundant config: the installer no longer writes
|
- ✓ pruned now-redundant config: the installer no longer writes
|
||||||
@@ -204,18 +417,15 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
hand-formatting, so `nixfmt-rfc-style --check` flags ~33 files. Adopting a
|
hand-formatting, so `nixfmt-rfc-style --check` flags ~33 files. Adopting a
|
||||||
formatter would be a one-time repo-wide reformat that flattens that
|
formatter would be a one-time repo-wide reformat that flattens that
|
||||||
alignment — a maintainer call, not a silent cleanup, so left untouched.
|
alignment — a maintainer call, not a silent cleanup, so left untouched.
|
||||||
- **Full docs review & restructure:** a dedicated pass over all the prose,
|
- ✓ **Full docs review & restructure** (completed 2026-07-04 in four
|
||||||
not just the code-adjacent cleanup the repo-sanitize item covers. The
|
slices): the roadmap/backlog split (this file + agent/BACKLOG.md);
|
||||||
README has grown to ~540 lines with a ~200-line roadmap inline — the
|
option tables reconciled with the live surface and guarded permanently
|
||||||
biggest single move is likely **splitting the roadmap out** (e.g.
|
by `checks.option-docs`; drift pass over OVERRIDES/TESTING/template
|
||||||
`ROADMAP.md` / `docs/`) so the README stays a focused "what it is / how
|
README (claims held; two gaps fixed); the install/first-run story now
|
||||||
to install / how to override" entry point. Also: reconcile every option
|
hands off README §3 → template README explicitly; and a new
|
||||||
table against the live `nomarchy.*` surface (snapper and others are
|
**docs/RECOVERY.md** runbook (generations → journals → older boot
|
||||||
missing); a pass over `docs/OVERRIDES.md` + `docs/TESTING.md` and the
|
generation → snapper → nixos-enter). Still open, deliberately: the
|
||||||
`templates/downstream/README.md` for drift; check the install/first-run
|
docs-site-vs-Markdown call (BACKLOG § Decisions).
|
||||||
story reads cleanly end to end; and decide whether anything wants a real
|
|
||||||
docs site vs. staying Markdown-in-repo. Pairs with the first-boot welcome
|
|
||||||
and control-center items (shared "how do I…" surface).
|
|
||||||
- **Laptop power / battery management:** a real power story behind a
|
- **Laptop power / battery management:** a real power story behind a
|
||||||
`nomarchy.system.power.*` surface (`modules/nixos/power.nix`), replacing the
|
`nomarchy.system.power.*` surface (`modules/nixos/power.nix`), replacing the
|
||||||
old "only `services.upower` for Waybar reporting" baseline:
|
old "only `services.upower` for Waybar reporting" baseline:
|
||||||
@@ -231,7 +441,21 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
turns it on for a `GenuineIntel` CPU.
|
turns it on for a `GenuineIntel` CPU.
|
||||||
- ✓ **longevity:** `power.batteryChargeLimit` (e.g. 80) — a backend-
|
- ✓ **longevity:** `power.batteryChargeLimit` (e.g. 80) — a backend-
|
||||||
independent sysfs oneshot writes `charge_control_end_threshold`. Off by
|
independent sysfs oneshot writes `charge_control_end_threshold`. Off by
|
||||||
default; the installer scaffolds it commented-out on laptops.
|
default; the installer scaffolds it commented-out on laptops. ✓ the
|
||||||
|
threshold is now **re-applied on AC state changes** (a `services.udev`
|
||||||
|
rule on `SUBSYSTEM=="power_supply", ATTR{type}=="Mains"` restarts the
|
||||||
|
oneshot via `systemctl --no-block`), closing the firmware-resets-on-
|
||||||
|
unplug gap — the match is by adapter *type*, not kernel name
|
||||||
|
(AC/AC0/ADP1/ACAD vary), and `restart` (not `try-restart`) re-applies
|
||||||
|
even if the boot run was inactive. Eval-verified both ways (rule present
|
||||||
|
with charge limit on / absent off), and **VM-verified the trigger**
|
||||||
|
(`checks.battery-charge-limit`: the `test_power` module fakes a Mains
|
||||||
|
adapter, toggling `ac_online` emits a real `power_supply` uevent, and the
|
||||||
|
udev rule restarts the oneshot — confirmed by a changed `InvocationID`).
|
||||||
|
The sysfs *write* itself needs a real `charge_control_end_threshold`, so
|
||||||
|
a final on-hardware check (a `sudo nixos-rebuild` + a physical unplug)
|
||||||
|
remains — the dev box has both an `AC` Mains adapter and a `BAT0`
|
||||||
|
`charge_control_end_threshold`, so it can exercise it.
|
||||||
- ✓ **installer:** writes `power.laptop = true` (battery probe) and
|
- ✓ **installer:** writes `power.laptop = true` (battery probe) and
|
||||||
`power.thermal.enable` (Intel) into the generated `system.nix`.
|
`power.thermal.enable` (Intel) into the generated `system.nix`.
|
||||||
- ✓ **idle cohesion:** `modules/home/idle.nix` now suspends only on
|
- ✓ **idle cohesion:** `modules/home/idle.nix` now suspends only on
|
||||||
@@ -241,8 +465,6 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
logind's lid handling is left at its defaults (suspend on lid close,
|
logind's lid handling is left at its defaults (suspend on lid close,
|
||||||
ignore when docked) — an explicit "I'm done" that coheres with the
|
ignore when docked) — an explicit "I'm done" that coheres with the
|
||||||
idle behaviour.
|
idle behaviour.
|
||||||
- Remaining: a boot-only→event-driven charge-limit re-apply (udev) if a
|
|
||||||
firmware resets the threshold on unplug.
|
|
||||||
- ✓ **Waybar parity:** the `custom/powerprofile` indicator now shows in the
|
- ✓ **Waybar parity:** the `custom/powerprofile` indicator now shows in the
|
||||||
summer-day/night whole-swap themes too. `powerProfileStatus`/`Cycle` are
|
summer-day/night whole-swap themes too. `powerProfileStatus`/`Cycle` are
|
||||||
named `writeShellScriptBin`s on PATH (`waybar.nix` home.packages), so the
|
named `writeShellScriptBin`s on PATH (`waybar.nix` home.packages), so the
|
||||||
@@ -263,6 +485,31 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
Complements nixos-hardware (model quirks) rather than replacing it; keep
|
Complements nixos-hardware (model quirks) rather than replacing it; keep
|
||||||
the detection in the installer's `hardware-db` so an installed machine
|
the detection in the installer's `hardware-db` so an installed machine
|
||||||
bakes the right defaults, all overridable through `nomarchy.hardware.*`.
|
bakes the right defaults, all overridable through `nomarchy.hardware.*`.
|
||||||
|
- ✓ **Mechanism + broad seed shipped** (`modules/nixos/hardware.nix`): a
|
||||||
|
vendor-keyed `nomarchy.hardware.*` surface (`intel`, `amd`, `fingerprint`,
|
||||||
|
`npu`), `hardware-db.sh` extended to detect Intel/AMD, a fingerprint reader
|
||||||
|
(libfprint USB vendor IDs) and an NPU (Intel VPU / AMD XDNA PCI IDs), and
|
||||||
|
the installer bakes the matching toggles into `system.nix` — safe defaults
|
||||||
|
active (GuC/HuC, amd-pstate, AMD VA-API env, fprintd), heavy/experimental
|
||||||
|
opt-ins commented (Intel compute-runtime, ROCm, fingerprint PAM, NPU
|
||||||
|
driver). Audited against the nixos-hardware commons so it only fills the
|
||||||
|
gap above them — microcode / the media-driver VA-API stack / weekly fstrim
|
||||||
|
already come from `common-cpu-*` / `common-gpu-*` / `common-pc-ssd`.
|
||||||
|
Exercised live on the dev machine's detection (an AMD Ryzen-AI laptop: AMD
|
||||||
|
+ fingerprint + NPU all detected) and a toggles-on build (`amd_pstate=active`
|
||||||
|
+ `i915.enable_guc=3` in kernel-params, `fprintd.service` present).
|
||||||
|
**Remaining: on-hardware verification of the runtime bits (AMD/NPU/Intel
|
||||||
|
GPU-compute) — none are testable in CI or on the Intel Latitudes for the
|
||||||
|
AMD paths.** SSD TRIM is intentionally left to `common-pc-ssd`.
|
||||||
|
- ✓ **Hardening (driver-gen + kernel awareness):** `intel.guc` emits the
|
||||||
|
*i915* param, so the installer turns it off when the GPU is on the newer
|
||||||
|
`xe` driver (Lunar Lake / Battlemage / Panther Lake — GuC is default-on
|
||||||
|
there); the NPU detector matches the PCI *accelerator class* (not just a
|
||||||
|
device-ID list) so new gens are caught without a code change; and a
|
||||||
|
`nomarchy.hardware.latestKernel` escape hatch (+ a build-time warning when
|
||||||
|
`npu.enable` predates the shipped kernel) covers very-new hardware whose
|
||||||
|
drivers only just landed. A config-assertion VM test guards the wiring
|
||||||
|
(`checks.hardware-toggles`: kernel cmdline + fprintd + PAM, booted in a VM).
|
||||||
Worked example — ThinkPad T14s (Ryzen 7 PRO 7840U + Radeon 780M):
|
Worked example — ThinkPad T14s (Ryzen 7 PRO 7840U + Radeon 780M):
|
||||||
- **GPU compute — ROCm:** the 780M is an RDNA3 iGPU (gfx1103) ROCm doesn't
|
- **GPU compute — ROCm:** the 780M is an RDNA3 iGPU (gfx1103) ROCm doesn't
|
||||||
officially list, so it needs `HSA_OVERRIDE_GFX_VERSION=11.0.0`. Multi-GB
|
officially list, so it needs `HSA_OVERRIDE_GFX_VERSION=11.0.0`. Multi-GB
|
||||||
@@ -287,6 +534,96 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
distro-wide regardless).
|
distro-wide regardless).
|
||||||
Sub-items here can graduate into their own roadmap entries as they're
|
Sub-items here can graduate into their own roadmap entries as they're
|
||||||
scoped; the unifying work is the detection + `nomarchy.hardware.*` surface.
|
scoped; the unifying work is the detection + `nomarchy.hardware.*` surface.
|
||||||
|
- **Webcam support & tuning:** improve out-of-the-box webcam behaviour.
|
||||||
|
Motivating case — on the ThinkPad T14s AMD Gen 4 the camera shows a dark,
|
||||||
|
low-quality image. **Diagnosed on hardware (2026-06-26)**, and it's *not* what
|
||||||
|
the first cut of this item guessed (UVC default-controls tuning vs a MIPI/
|
||||||
|
libcamera gap):
|
||||||
|
- The camera is a **USB UVC** module (Chicony `04f2:b7c0`, `uvcvideo`) and is
|
||||||
|
**dual-sensor** — `video0` Color (MJPG, up to 2592×1944 / 1080p) + `video2`
|
||||||
|
**IR** (8-bit `GREY` only, the face-unlock sensor). The AMD IPU `[1022:1502]`
|
||||||
|
is present on PCI but **unused** (the camera enumerates over USB, not the
|
||||||
|
MIPI/ISP path), so the libcamera-software-ISP branch is moot on this machine.
|
||||||
|
- The **raw color capture is fine**: at factory defaults with auto-exposure,
|
||||||
|
`/dev/video0` measures luma ≈130/255 *from the first frame* (no AE ramp, not
|
||||||
|
dark); forcing manual exposure made it *worse*. So there is **no v4l2 control
|
||||||
|
default to bake** — the speculated `v4l2-ctl`-tuning / udev-oneshot fix is the
|
||||||
|
wrong tree.
|
||||||
|
- **Real cause is the consumption path.** PipeWire/WirePlumber exposes the
|
||||||
|
device through **both** backends at once — two `[v4l2]` sources (node 144 =
|
||||||
|
`/dev/video0` Color, node 146 = `/dev/video2` **IR**) **plus** two
|
||||||
|
`[libcamera]` nodes (Color + IR) — and the IR sensor is presented as an
|
||||||
|
**indistinguishable** "Integrated Camera". So an app's camera picker shows up
|
||||||
|
to *four* identical entries, and choosing the IR one yields a black/dark
|
||||||
|
monochrome frame; the libcamera path can also negotiate the low-res `YUYV`
|
||||||
|
640×480 mode ("bad quality"). The default source is the color node, so apps
|
||||||
|
that don't let you choose are fine — the breakage is selecting (or an app
|
||||||
|
auto-selecting) the wrong node.
|
||||||
|
- **Fix — validated live on hardware (2026-06-26).** Two WirePlumber 0.5
|
||||||
|
drop-ins collapse the four entries to one clean color camera, confirmed via
|
||||||
|
`wpctl status` (before: 2 v4l2 + 2 libcamera incl. both IR nodes → after:
|
||||||
|
**1** v4l2 source = `/dev/video0` color, **0** libcamera):
|
||||||
|
1. **Hide the IR node** — `monitor.v4l2.rules` matching the IR sensor →
|
||||||
|
`node.disabled = true`. (Tested by card name `~.*Integrated I`; the
|
||||||
|
**shipping** match should key on the more robust, vendor-neutral heuristic
|
||||||
|
of a **`GREY`-only / no-color-format** node, since other vendors' IR cards
|
||||||
|
are named differently.)
|
||||||
|
2. **Drop the duplicate backend** — `wireplumber.profiles.main.monitor.libcamera
|
||||||
|
= disabled`, since a plain UVC cam is fully covered by v4l2 (also kills the
|
||||||
|
libcamera low-res-`YUYV` path).
|
||||||
|
**Drawbacks / design constraints for the module:**
|
||||||
|
- Rule 2 (libcamera off) is only safe **when a UVC camera exists** — on a
|
||||||
|
MIPI/IPU-only machine (no UVC fallback) it would kill the camera entirely,
|
||||||
|
so it **must be conditional on detection**, not blanket. Rule 1 (IR-hide)
|
||||||
|
is broadly safe. Exactly the `nomarchy.hardware.*`-gated targeting the
|
||||||
|
parent item calls for.
|
||||||
|
- **Face-unlock is *not* broken:** `node.disabled` only hides the PipeWire
|
||||||
|
node; the kernel `/dev/video2` stays openable, so Howdy (which reads the IR
|
||||||
|
device directly, bypassing PipeWire) still works.
|
||||||
|
A true MIPI/IPU software-ISP camera with no UVC fallback stays a separate
|
||||||
|
future item.
|
||||||
|
- **Shipped (2026-06-27):** `nomarchy.hardware.camera.hideIrSensor` (+ an
|
||||||
|
overridable `irMatch` regex) in `modules/nixos/hardware.nix` emits rule 1 via
|
||||||
|
`services.pipewire.wireplumber.extraConfig`; the installer's `hardware-db.sh`
|
||||||
|
auto-detects a paired RGB+IR webcam (from `/sys/class/video4linux/*/name`)
|
||||||
|
and bakes the toggle into `system.nix`, with a commented example in the
|
||||||
|
downstream template. **Decision: v4l2 IR-hide only — libcamera is left
|
||||||
|
untouched** so an external camera you plug in is never affected. Surgical
|
||||||
|
internal-only libcamera scoping proved impossible: the distinguishing device
|
||||||
|
props (`api.libcamera.location`, `device.product.name`) bind *after* the
|
||||||
|
monitor rule runs, and the only early-matchable prop (`device.api`) is
|
||||||
|
all-or-nothing — so a broad libcamera-off was the only option and was rejected
|
||||||
|
as the blunt instrument it is (external USB cams are UVC and keep working via
|
||||||
|
v4l2 regardless). Verified: installer detection fires on the T14s; the
|
||||||
|
generated drop-in's serialized content is valid WP-0.5 config; and the exact
|
||||||
|
shipped `irMatch` was re-confirmed live (1 V4L2 source = the colour
|
||||||
|
`/dev/video0`, libcamera untouched, IR node disabled in the WirePlumber log).
|
||||||
|
Remaining: an on-Nomarchy end-to-end check (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*
|
- **Opt-in services & integrations:** the counterpart to the opt-*out*
|
||||||
application suite above — heavier or more personal integrations shipped
|
application suite above — heavier or more personal integrations shipped
|
||||||
**off by default**, each a `nomarchy.services.<name>.enable` toggle a
|
**off by default**, each a `nomarchy.services.<name>.enable` toggle a
|
||||||
@@ -344,19 +681,80 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
(no kanshi — it fights Hyprland's own output management). `nwg-displays`
|
(no kanshi — it fights Hyprland's own output management). `nwg-displays`
|
||||||
ships behind `nomarchy.displays.enable` as an interactive arranger (a
|
ships behind `nomarchy.displays.enable` as an interactive arranger (a
|
||||||
helper to find values; the declarative config stays the source of truth —
|
helper to find values; the declarative config stays the source of truth —
|
||||||
its output file isn't sourced). Remaining: true docked/undocked **profile
|
its output file isn't sourced). The System ▸ **Display** menu picks an
|
||||||
switching** of the *same* outputs (Hyprland's per-output rules cover the
|
output's resolution from its advertised modes: applied live via `hyprctl
|
||||||
common "external connects → arrange" case, not multi-layout toggles), and
|
keyword monitor` (current position + scale kept) and persisted to the
|
||||||
optionally workspace-to-monitor binding.
|
in-flake state (`settings.monitors.<name>`, no rebuild), overlaid onto
|
||||||
|
`nomarchy.monitors` by name so the next rebuild bakes it in — the monitor
|
||||||
|
twin of the keyboard-layout graduation. Remaining: true docked/undocked
|
||||||
|
**profile switching** of the *same* outputs (Hyprland's per-output rules
|
||||||
|
cover the common "external connects → arrange" case, not multi-layout
|
||||||
|
toggles), and optionally workspace-to-monitor binding.
|
||||||
- ✓ **Night light / blue-light filter:** `nomarchy.nightlight` (opt-in) —
|
- ✓ **Night light / blue-light filter:** `nomarchy.nightlight` (opt-in) —
|
||||||
a scheduled colour-temperature shift via `hyprsunset` (Hyprland-native),
|
a scheduled colour-temperature shift via `hyprsunset` (Hyprland-native),
|
||||||
warm at night, identity (no shift) by day. `modules/home/nightlight.nix`
|
warm at night, identity (no shift) by day. `modules/home/nightlight.nix`
|
||||||
drives the HM `services.hyprsunset` with two time-based profiles
|
drives the HM `services.hyprsunset` with two time-based profiles
|
||||||
(`sunrise` → identity, `sunset` → `temperature`), so hyprsunset handles the
|
(`sunrise` → identity, `sunset` → `temperature`), so hyprsunset handles the
|
||||||
schedule and the on-login state. Needs an on-hardware check that hyprsunset
|
schedule and the on-login state. Active-profile-at-session-start was
|
||||||
applies the active profile at session start. Remaining (optional): a menu +
|
confirmed on hardware (2026-06-18). ✓ **Menu + Waybar toggle (opt-in,
|
||||||
Waybar toggle to force it on/off, and geo (lat/long) auto sunset/sunrise
|
instant, in-flake state):** two git-tracked keys in the state file, both
|
||||||
(would mean wlsunset, which schedules by location).
|
menu-written (exposed via the new `nomarchy.settings` option) —
|
||||||
|
`settings.nightlight.installed` (**sticky**: gates the hyprsunset unit;
|
||||||
|
`nomarchy.nightlight.enable` `mkDefault`-reads it) and
|
||||||
|
`settings.nightlight.on` (runtime on/off). **Off by default.** The *first*
|
||||||
|
enable from the menu writes `installed` and rebuilds to create the unit (the
|
||||||
|
one accepted rebuild); **every toggle after is instant** —
|
||||||
|
`nomarchy-theme-sync set settings.nightlight.on … --no-switch` (atomic +
|
||||||
|
`git add -N`, no rebuild) plus a `systemctl` start/stop. Splitting the sticky
|
||||||
|
flag from the on/off is what avoids a **decay** bug: an instant-off writes
|
||||||
|
only `on`, so an unrelated later rebuild (e.g. `sys-update`) never drops the
|
||||||
|
unit and "on" stays instant. Persistence across logout/reboot comes from an
|
||||||
|
`ExecCondition` on the unit (`nomarchy-nightlight should-start`) that reads the
|
||||||
|
**live** working-tree on/off at start time — *not* the eval-frozen store copy
|
||||||
|
— so an off survives a reboot with no rebuild; a later rebuild bakes the same
|
||||||
|
value. No more `~/.local/state` marker, no marker `ConditionPathExists`. The
|
||||||
|
`nomarchy-menu` System-submenu entry is always shown (current on/off) so the
|
||||||
|
feature can be enabled from the menu; the self-gating Waybar
|
||||||
|
`custom/nightlight` indicator shows the moon while running and hides otherwise.
|
||||||
|
This is the first cut of a broader principle — **any user-settable config
|
||||||
|
gets a menu writer that lands it in the downstream flake; no state lives
|
||||||
|
outside the checkout** (Phase 1 night-light + Phase 2 the per-device
|
||||||
|
keyboard-layout memory below are done — both now in the git-tracked state
|
||||||
|
file; next target: opt-in auto-commit of the flake on each mutation, owned
|
||||||
|
files only). The on-machine check PASSed 2026-07-04 (enable/disable on
|
||||||
|
hardware). **Geo mode shipped 2026-07-11:** set BOTH
|
||||||
|
`nomarchy.nightlight.latitude`/`.longitude` and wlsunset replaces
|
||||||
|
hyprsunset — sunrise/sunset computed from the location daily, the fixed
|
||||||
|
times ignored; same toggle script, moon indicator, and live-state
|
||||||
|
`ExecCondition` gate (the backing unit name is a single let-binding).
|
||||||
|
Declarative-only on purpose (coords are hardware-independent machine
|
||||||
|
config, like the keyboard layout — no menu writer). Guarded by
|
||||||
|
`checks.nightlight-geo` (pure eval over the rendered units); the visible
|
||||||
|
warm shift is hardware-tier, queued.
|
||||||
|
- ✓ **Automatic timezone (location-following clock):** `nomarchy.system.autoTimezone`
|
||||||
|
(opt-in, off by default) — geoclue + `services.automatic-timezoned` drive
|
||||||
|
`/etc/localtime` from your location, so travelling to another zone updates the
|
||||||
|
Waybar clock on its own. **Menu-driven, in-flake state** (the night-light /
|
||||||
|
keyboard philosophy): the flag is `settings.autoTimezone` in theme-state.json,
|
||||||
|
git-tracked; the System-menu entry (`nomarchy-menu autotimezone` →
|
||||||
|
`nomarchy-autotimezone`) writes it and rebuilds. **Not instant like
|
||||||
|
night-light** — it's a *system* service (not a user unit you can start/stop),
|
||||||
|
so the toggle drives a `sudo nixos-rebuild` (bakes the service + the
|
||||||
|
`time.timeZone` override) plus a `home-manager switch` (the Waybar-refresh
|
||||||
|
watcher), both off the one flag. **time.timeZone handling:** a runtime zone
|
||||||
|
needs `/etc/localtime` writable; automatic-timezoned sets `time.timeZone = null`
|
||||||
|
itself, but the installer's static value would collide (a hard eval error), so
|
||||||
|
the module forces it null (`mkForce`) over the installer's value when enabled,
|
||||||
|
and reverts to it when disabled. **Live clock refresh:** Waybar's clock module
|
||||||
|
captures the zone at construction, so `modules/home/timezone.nix` runs a tiny
|
||||||
|
user service that watches timedate1's change signal and reloads Waybar
|
||||||
|
(SIGUSR2) on a real zone change (also catches a manual `timedatectl
|
||||||
|
set-timezone`). Eval-verified both ways (on: geoclue+daemon on, tz forced null
|
||||||
|
over a static installer value, watcher present; off: static tz intact, no
|
||||||
|
service/watcher). **Pending an on-hardware check** — geoclue detection and the
|
||||||
|
SIGUSR2 clock refresh aren't testable in CI (if SIGUSR2 proves insufficient,
|
||||||
|
fall back to restarting waybar). Remaining (optional): a Waybar tooltip line
|
||||||
|
showing the detected zone.
|
||||||
- **Keyboard layouts (per-device + switching):**
|
- **Keyboard layouts (per-device + switching):**
|
||||||
- ✓ **Per-device declarative layout:** `nomarchy.keyboard.devices`
|
- ✓ **Per-device declarative layout:** `nomarchy.keyboard.devices`
|
||||||
(`{ "<hyprctl-device-name>" = { layout; variant; }; }`) generates Hyprland
|
(`{ "<hyprctl-device-name>" = { layout; variant; }; }`) generates Hyprland
|
||||||
@@ -372,20 +770,46 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
watch` daemon runs (exec-once): it polls `hyprctl devices`, and when a
|
watch` daemon runs (exec-once): it polls `hyprctl devices`, and when a
|
||||||
keyboard connects *after* login that isn't in `keyboard.devices` and
|
keyboard connects *after* login that isn't in `keyboard.devices` and
|
||||||
hasn't been chosen before, it pops a rofi layout picker, applies the
|
hasn't been chosen before, it pops a rofi layout picker, applies the
|
||||||
choice with `hyprctl switchxkblayout` (an index into the candidate set,
|
choice as a **per-device** `hyprctl keyword device[<name>]:kb_layout` (the
|
||||||
which `input.kb_layout` carries), and remembers it per-device in
|
runtime twin of the declarative `device{}` blocks — so it isolates that
|
||||||
`~/.local/state/nomarchy/keyboard-layouts` — re-applied silently on later
|
one keyboard and never touches the built-in board), and remembers it. The
|
||||||
reconnects. The boot-time set (incl. the built-in keyboard) is never
|
boot-time set (incl. the built-in keyboard) is never prompted. The
|
||||||
prompted. The runtime-remember complement to the declarative
|
runtime-remember complement to the declarative `keyboard.devices`.
|
||||||
`keyboard.devices`; a stateful runtime piece by design. **Pending an
|
**Picker verified on hardware (2026-06-18)**; the first cut applied the
|
||||||
on-hardware test** (hotplug isn't verifiable in CI). Remaining (bonus):
|
choice with `switchxkblayout` (a global layout-index flip) and merged the
|
||||||
offer to write a choice into `keyboard.devices` so it graduates to the
|
candidate pool into `input.kb_layout`, so a selection leaked onto the
|
||||||
reproducible config.
|
laptop keyboard and stuck after unplug — fixed by the per-device keyword +
|
||||||
- Remaining: a multi-layout cycle bind (`hyprctl switchxkblayout` / xkb
|
keeping the pool out of the session layout.
|
||||||
`grp:` options) for switching layouts on one keyboard; add the
|
- ✓ **In-flake state + graduation (2026-06-23):** the remembered picks moved
|
||||||
`hyprland/language` module to the summer whole-swap themes for parity
|
out of `~/.local/state/nomarchy/keyboard-layouts` into the git-tracked
|
||||||
(the gating doesn't translate to their static JSON). Keep the system
|
state file (`settings.keyboard.devices`, a device-name → layout map) —
|
||||||
(console/initrd) and session layouts in sync (the LUKS-keymap work).
|
Phase 2 of the in-flake-state principle, the same path night-light took.
|
||||||
|
The watcher reads the **live** working tree (a pick is honoured at once)
|
||||||
|
and writes instantly with `--no-switch` (no rebuild), merging the whole map
|
||||||
|
back at the dot-free parent path so a device name containing a dot can't
|
||||||
|
corrupt the dotted set-path. Each remembered device **graduates** into
|
||||||
|
`nomarchy.keyboard.devices` on the next rebuild (a generated Hyprland
|
||||||
|
`device{}` block, `mkDefault` so a hand-written entry still wins), after
|
||||||
|
which the watcher sees it as declared and steps back. Eval-verified
|
||||||
|
(graduation + precedence) and the writer round-trips; **still needs the
|
||||||
|
on-hardware hotplug re-verify** (the picker path isn't testable in CI).
|
||||||
|
- ✓ **Multi-layout cycle bind + summer parity (2026-07-04):**
|
||||||
|
`SUPER+SHIFT+K` → `hyprctl switchxkblayout current next`, rendered
|
||||||
|
only when the session layout has a comma (same gate as the Waybar
|
||||||
|
language indicator) — data lives in `keybinds.nix` as a separate
|
||||||
|
`multiLayoutBinds` list, so hyprland.nix and the cheatsheet consume
|
||||||
|
one source and gate identically (verified both ways: absent on a
|
||||||
|
single layout; bind + cheatsheet row render under `us,de` via
|
||||||
|
extendModules). `current` targets the focused keyboard, so a
|
||||||
|
per-device-overridden board (one layout) is a no-op, never a leak.
|
||||||
|
`hyprland/language` (` {short}`) added to both summer
|
||||||
|
`waybar.jsonc` whole-swaps + `#language` in their CSS — static JSON
|
||||||
|
can't eval-gate, so on summer themes the module shows even with one
|
||||||
|
layout (a deliberate parity-over-minimalism call; it's small).
|
||||||
|
Noticed en route: summer-night carries an `idle_inhibitor` the
|
||||||
|
generated bar lacks — folded into the idle-inhibit backlog item.
|
||||||
|
- Remaining: keep the system (console/initrd) and session layouts in
|
||||||
|
sync (the LUKS-keymap work).
|
||||||
- ✓ **Do-Not-Disturb:** swaync DND toggle wired into the menu
|
- ✓ **Do-Not-Disturb:** swaync DND toggle wired into the menu
|
||||||
(`nomarchy-menu dnd`, in the picker, SUPER+CTRL+D) and a Waybar bell
|
(`nomarchy-menu dnd`, in the picker, SUPER+CTRL+D) and a Waybar bell
|
||||||
indicator (`custom/notification` via `swaync-client -swb`: shows the
|
indicator (`custom/notification` via `swaync-client -swb`: shows the
|
||||||
@@ -404,21 +828,92 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
change). **The backend is VM-verified** (a full BTRFS+LUKS install: both
|
change). **The backend is VM-verified** (a full BTRFS+LUKS install: both
|
||||||
`root`+`home` configs, a `/home` timeline snapshot taken, `/home/.snapshots`
|
`root`+`home` configs, a `/home` timeline snapshot taken, `/home/.snapshots`
|
||||||
a real subvolume, the oneshot `Result=success`).
|
a real subvolume, the oneshot `Result=success`).
|
||||||
- ⚠ **Known bug — btrfs-assistant 2.2 segfaults on launch** in nixpkgs
|
- ✓ **Resolved — the "btrfs-assistant 2.2 segfault" was unprivileged-only**
|
||||||
26.05: it crashes inside `libbtrfsutil.so.1.4.0` (a library ABI mismatch,
|
(re-diagnosed 2026-07-04): gdb puts the crash in `btrfs_util_subvolume_
|
||||||
confirmed in the VM regardless of GL/Qt platform — so it's not a VM/GL
|
iterator_next()` — libbtrfsutil's *unprivileged* subvolume-iteration path
|
||||||
artifact and will crash on hardware too). The menu wiring is correct and
|
in btrfs-progs 6.17.1 (upstream-fixed after 6.17.1, kdave/btrfs-progs
|
||||||
the app is installed, but the GUI doesn't open. The snapshot *backend*
|
`886571653` "re-enable tree search v2 ioctl"; symbol versions were checked
|
||||||
(the actual snapshots) is unaffected. Fix path: a nixpkgs override aligning
|
and match, so not an ABI/link issue). **As root it runs fine** — VM-proven
|
||||||
its libbtrfsutil, or wait for an upstream bump; meanwhile the menu entry
|
(root `--version` exits 0, unprivileged exits 139) — and the pkexec
|
||||||
is a no-op when the binary crashes. Fallback if it lingers: a rofi-based
|
launcher runs it as root, so the GUI was never actually broken *when
|
||||||
snapshot menu (no btrfs-assistant dependency).
|
launched right*. What WAS missing distro-wide: **no polkit authentication
|
||||||
- Remaining (optional): a keyboard-driven rofi browse for quick glances;
|
agent**, so every pkexec prompt in the session failed silently — that's
|
||||||
boot-from-snapshot needs a systemd-boot equivalent of grub-btrfs.
|
the root cause of "the GUI doesn't open". Fixes: `hyprpolkitagent`
|
||||||
- **Update awareness:** updates are manual today (`sys-update`/`home-update`);
|
(Hyprland's Qt agent, Stylix-themed) now ships via exec-once in
|
||||||
add a Waybar indicator / notification when flake inputs are stale or a new
|
`hyprland.nix`; the menu prefers `btrfs-assistant-launcher` again with
|
||||||
nixpkgs rev is available (optionally with a pending-change count). Augments,
|
`nomarchy-snapshots` as fallback; `checks.snapshot-gui` guards the root
|
||||||
never replaces, the explicit rebuild flow.
|
path on a real btrfs volume (and the GUI event loop offscreen) against
|
||||||
|
lock-bump regressions. No btrfs-progs patch: our flows are all root-side,
|
||||||
|
so the unprivileged fix rides in with a future lock bump. Original
|
||||||
|
diagnosis kept below for the record. ✓ **Fallback shipped:**
|
||||||
|
the menu now launches `nomarchy-snapshots` (system-side, gated on
|
||||||
|
`nomarchy.system.snapper`) instead — a keyboard-driven snapper
|
||||||
|
browser/restore with no btrfs-assistant dependency. snapper is root-only
|
||||||
|
here (no `ALLOW_USERS`) and rofi can't run as root under Wayland, so it
|
||||||
|
runs in a terminal via `sudo` (one password prompt) and uses `fzf` to pick:
|
||||||
|
browse/diff (read-only), restore files (`undochange`), or roll back (root
|
||||||
|
config only) — each behind a typed-`yes` confirmation, which is *safer*
|
||||||
|
than the fat-fingerable one-click rofi rollback this entry originally
|
||||||
|
avoided. btrfs-assistant stays installed for when nixpkgs fixes it.
|
||||||
|
**Pending an on-machine check of the restore/rollback paths.**
|
||||||
|
- Remaining (optional): boot-from-snapshot needs a systemd-boot equivalent
|
||||||
|
of grub-btrfs.
|
||||||
|
- ✓ **Update awareness:** `nomarchy.updates.enable` (opt-in, `modules/home/
|
||||||
|
updates.nix`) — a `nomarchy-updates` checker on a systemd user timer
|
||||||
|
(`.interval`, default daily) compares each **direct** branch-tracking flake
|
||||||
|
input (nixpkgs, the Nomarchy input, home-manager, stylix … via `git
|
||||||
|
ls-remote` vs the locked rev; offline → skipped, no false alarm) and, when
|
||||||
|
the `flatpak` CLI is present (`.flatpak`), counts Flatpak updates. A
|
||||||
|
self-gating Waybar `custom/updates` indicator (hidden until something's
|
||||||
|
available; accent ` N`, signal-refreshed) + a notification that fires only
|
||||||
|
when the count *grows* (so a daily timer never nags). Click → the upgrade
|
||||||
|
flow in a terminal (`sys-update` / `flatpak update`, each confirmed). Purely
|
||||||
|
passive — it never changes anything, augmenting the explicit rebuild flow.
|
||||||
|
Indicator is in the generated bar and both summer whole-swaps. **Pending an
|
||||||
|
on-machine check** (the ls-remote/notify/timer path needs a live session).
|
||||||
|
- **Automated upstream lock bumps (maintainer CI):** the upstream twin of the
|
||||||
|
user-side checker above — automate advancing Nomarchy's *own* `flake.lock`,
|
||||||
|
which is the entire delivery channel (a downstream takes a single input,
|
||||||
|
`?ref=v1`, so nixpkgs/home-manager/stylix/nixos-hardware reach it
|
||||||
|
*transitively pinned* through that lock; users can't bump them independently).
|
||||||
|
**Tiered by what CI can't do:** the hardware-QA gate (the Latitude 5410 + the
|
||||||
|
AMD dev box — AMD/NPU/Intel-GPU-compute runtime bits aren't testable in CI or
|
||||||
|
on the Intel Latitudes for the AMD paths) can't be automated, so a bot drives
|
||||||
|
`main` and a human still promotes `main → v1`. A scheduled job (Forgejo
|
||||||
|
Actions, or Renovate's Nix manager opening a reviewable lock-diff PR) runs
|
||||||
|
`nix flake update` → `nix flake check` + the existing `runNixOSTest` suite +
|
||||||
|
builds `system.build.toplevel`, and on green lands on `main` — which nobody
|
||||||
|
tracks, so a bad bump costs a debug session, not a user outage; `v1` stays
|
||||||
|
the manual on-hardware gate (it only ever fast-forwards, never force-pushed,
|
||||||
|
or a user's locked rev can vanish from history). **Cadence weekly**
|
||||||
|
(release-26.05 doesn't move fast; nightly is just churn), and `nix flake
|
||||||
|
update` stays *within* the pinned release branches — no surprise major bump
|
||||||
|
(a 26.11 jump is a deliberate `v2`, hand-edited in flake.nix). Side benefit:
|
||||||
|
shrinks the security-patch latency the single-input model otherwise imposes
|
||||||
|
(users are gated on the maintainer for nixpkgs CVEs), since a fix is usually
|
||||||
|
already eval/build/VM-tested and one hardware promotion away from `v1` —
|
||||||
|
worth fast-laning lock-only/security bumps ahead of feature batches. The repo
|
||||||
|
has **no CI today** (manual `nix flake update` only). Explicitly *not* a
|
||||||
|
binary cache: compile-from-source is a deliberate values call (Gentoo-style),
|
||||||
|
so this automates the *config/lock* channel, not artifact distribution.
|
||||||
|
- ✓ **Opt-in auto-commit of state mutations (in-flake state, Phase 4):**
|
||||||
|
`settings.autoCommit` (menu: **System › Auto-commit**, self-gated on the
|
||||||
|
flake being a git repo) makes every `apply`/`set` also `git commit`
|
||||||
|
theme-state.json in the downstream flake. Design decisions: the flag is
|
||||||
|
**live-read by the tool** on each write — nothing in Nix consumes it, so
|
||||||
|
the toggle is instant, no rebuild, no option-surface addition; the commit
|
||||||
|
is **pathspec-limited** (`git commit -- theme-state.json`) so unrelated
|
||||||
|
dirty files are never swept up; it fires when the flag is on before *or*
|
||||||
|
after the write, so the disable-toggle itself lands in history instead of
|
||||||
|
staying forever-dirty; a same-value `set` no-ops (diff against HEAD); a
|
||||||
|
missing git identity falls back to `Nomarchy <nomarchy@localhost>`;
|
||||||
|
`bg next` is deliberately excluded (runtime wallpaper churn — the path
|
||||||
|
rides along with the next real commit). Verified: V1 (HM generation
|
||||||
|
builds, generated menu `bash -n` green) + a 7-assertion sandbox-repo
|
||||||
|
round-trip incl. the apply path; on-machine check queued. Rider fix:
|
||||||
|
`get` now prints booleans JSON-style (`true`, not Python's `True`) —
|
||||||
|
which also un-sticks the System menu's "Auto timezone (on/off)" label,
|
||||||
|
whose `= true` comparison could never match before.
|
||||||
- **"nomarchy" control center:** a single TUI/GUI front-end over the common
|
- **"nomarchy" control center:** a single TUI/GUI front-end over the common
|
||||||
toggles — theme, power profile, opt-in services, display, DND — built on
|
toggles — theme, power profile, opt-in services, display, DND — built on
|
||||||
the same `nomarchy-theme-sync` / `nomarchy.*` surface the menu already
|
the same `nomarchy-theme-sync` / `nomarchy.*` surface the menu already
|
||||||
@@ -426,6 +921,57 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
your theme / essentials" flow (ties into the branding work).
|
your theme / essentials" flow (ties into the branding work).
|
||||||
|
|
||||||
## Known issues & follow-ups
|
## 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'
|
- ✓ **Yazi TOML parse error on startup:** yazi 26.x made fetchers'
|
||||||
`group` field required, so `[[plugin.prepend_fetchers]]` failed to parse
|
`group` field required, so `[[plugin.prepend_fetchers]]` failed to parse
|
||||||
and yazi fell back to presets. Fixed by adding `group = "git"` to both
|
and yazi fell back to presets. Fixed by adding `group = "git"` to both
|
||||||
@@ -471,3 +1017,14 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
`color-scheme` reads `1` on a dark theme and flips to `2` after switching
|
`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
|
to a light one (catppuccin-latte) — so xdg-desktop-portal-gtk does relay
|
||||||
the dconf value GTK4/Qt apps read.
|
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
|
```sh
|
||||||
nix flake check --no-build # full module-system evaluation, no builds
|
nix flake check --no-build # full module-system evaluation, no builds
|
||||||
nix-instantiate --parse <file> # syntax-only, works even on macOS
|
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
|
`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),
|
template through `lib.mkFlake` (including a real nixos-hardware profile),
|
||||||
so template/wrapper drift fails fast too.
|
so template/wrapper drift fails fast too.
|
||||||
|
|
||||||
|
## 1b. CI (automatic on push)
|
||||||
|
|
||||||
|
Every push to `main`/`v1` runs `.gitea/workflows/check.yml`: the §1
|
||||||
|
cheap checks (flake eval, Python + shell syntax) on the Gitea instance.
|
||||||
|
That's the **eval tier only** — the runner is a docker container without
|
||||||
|
KVM, so the `checks.*` VM suite and real builds stay local (this file)
|
||||||
|
until a KVM-capable runner is registered; the workflow carries a
|
||||||
|
commented `vm-checks` job ready for that day. A green CI run is *not* "it
|
||||||
|
renders" (the honesty rule below still applies) — it means "nobody broke
|
||||||
|
evaluation".
|
||||||
|
|
||||||
## 2. Build and boot the live ISO
|
## 2. Build and boot the live ISO
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -53,13 +65,13 @@ Work through these in order; each one exercises a different layer.
|
|||||||
|
|
||||||
| # | Check | Verifies |
|
| # | 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 | ✓ shipped #79 (2026-07-10) — `settings.autoTheme` + timer; still one 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 | ✓ shipped `docs/OMARCHY.md` (#78, 2026-07-10) |
|
||||||
|
| **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": {
|
"locked": {
|
||||||
"lastModified": 1781063120,
|
"lastModified": 1783221248,
|
||||||
"narHash": "sha256-1UIF/mDJluwJQjmmcZ2j1L2+mjYsefe82QCLj0TYSOg=",
|
"narHash": "sha256-ESQnuNHEDChsB4IxoLRhscVahqkDWkTb+qdIz8euYt4=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "baa46aeb6d02e0ba13de67cd35e3d57aedfacf01",
|
"rev": "af2beae5f0fae0a4310cc0e6aef2572f56090353",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -166,11 +166,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1781020964,
|
"lastModified": 1783370751,
|
||||||
"narHash": "sha256-fS7xTi2j2iso5Hj7RNZLv/acDlCT+fgMVkVk40A7Uco=",
|
"narHash": "sha256-E+3MIMvKuo9k+K+qLQ9YXzsBzkgHuyVLnsEbN2DFfuc=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixos-hardware",
|
"repo": "nixos-hardware",
|
||||||
"rev": "32c2cd9e46286c4eced3dc6b613c659126bf3cca",
|
"rev": "662bd6e312d2c8b212e32cb377abaee190749320",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -182,11 +182,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1780902259,
|
"lastModified": 1783148766,
|
||||||
"narHash": "sha256-q8yYEC5f1mFlQO9RGna4LTc9QrcvWunX6FYp83munkQ=",
|
"narHash": "sha256-uslt2pqShTIXDdAHRHv2QkYLsVdY8Oqwz0EA48/RSM8=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "bd0ff2d3eac24699c3664d5966b9ef36f388e2ca",
|
"rev": "a50de1b7d8a586adc18d2395c19de7d6058e6030",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -249,11 +249,11 @@
|
|||||||
"tinted-zed": "tinted-zed"
|
"tinted-zed": "tinted-zed"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1780702455,
|
"lastModified": 1783359251,
|
||||||
"narHash": "sha256-+srjPGNy67nKytYwdlepycL51IG6S34sS4MKRZXK8G0=",
|
"narHash": "sha256-HUiCnEVlJ4n+qJlZojiz/zv+P0cqM5zsg1dxpz2J7Mg=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "stylix",
|
"repo": "stylix",
|
||||||
"rev": "54fa19702f4f2c7f6a981a92850678933588af9a",
|
"rev": "e602ad042f00409f33c8ad2829cd8d59ba345c7e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
854
flake.nix
854
flake.nix
@@ -83,6 +83,13 @@
|
|||||||
# works on machines that don't check out this repo.
|
# works on machines that don't check out this repo.
|
||||||
themesDir = ./themes;
|
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 { };
|
||||||
|
# pull / rebuild / home (+ legacy aliases) — also on HM so a home
|
||||||
|
# switch can refresh a broken system-package nomarchy-pull.
|
||||||
|
nomarchy-lifecycle = final.callPackage ./pkgs/nomarchy-lifecycle { };
|
||||||
};
|
};
|
||||||
|
|
||||||
nixosModules.nomarchy = {
|
nixosModules.nomarchy = {
|
||||||
@@ -112,6 +119,12 @@
|
|||||||
packages.${system} = {
|
packages.${system} = {
|
||||||
nomarchy-theme-sync = pkgs.nomarchy-theme-sync;
|
nomarchy-theme-sync = pkgs.nomarchy-theme-sync;
|
||||||
nomarchy-install = nomarchyInstall;
|
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;
|
default = pkgs.nomarchy-theme-sync;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -134,6 +147,847 @@
|
|||||||
downstream.nixosConfigurations.default.config.system.build.toplevel;
|
downstream.nixosConfigurations.default.config.system.build.toplevel;
|
||||||
downstream-template-home =
|
downstream-template-home =
|
||||||
downstream.homeConfigurations.me.activationPackage;
|
downstream.homeConfigurations.me.activationPackage;
|
||||||
|
|
||||||
|
# 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", "hibernate"]:
|
||||||
|
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";
|
||||||
|
|
||||||
|
# Night-light geo mode: with BOTH lat/long set the backing unit
|
||||||
|
# flips hyprsunset → wlsunset (location-computed sunrise/sunset)
|
||||||
|
# and the live-state ExecCondition gate survives the swap (the
|
||||||
|
# toggle script's unit= comes from the same let-binding, so the
|
||||||
|
# gate landing on wlsunset covers the selection logic). Pure
|
||||||
|
# eval over the rendered HM units — nothing is built. The
|
||||||
|
# visible warm shift itself stays hardware-tier (HARDWARE-QUEUE),
|
||||||
|
# exactly like the original night-light full cycle.
|
||||||
|
nightlight-geo =
|
||||||
|
let
|
||||||
|
inherit (nixpkgs) lib;
|
||||||
|
hm = home-manager.lib.homeManagerConfiguration {
|
||||||
|
inherit pkgs;
|
||||||
|
modules = [
|
||||||
|
self.homeModules.nomarchy
|
||||||
|
{
|
||||||
|
nomarchy.stateFile = ./templates/downstream/theme-state.json;
|
||||||
|
nomarchy.nightlight = {
|
||||||
|
enable = true;
|
||||||
|
latitude = "52.52";
|
||||||
|
longitude = "13.40";
|
||||||
|
};
|
||||||
|
home = { username = "geo"; homeDirectory = "/home/geo"; };
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
units = hm.config.systemd.user.services;
|
||||||
|
exec = toString units.wlsunset.Service.ExecStart;
|
||||||
|
cond = toString units.wlsunset.Service.ExecCondition;
|
||||||
|
expect = c: msg: lib.assertMsg c "nightlight-geo: ${msg}";
|
||||||
|
ok =
|
||||||
|
expect (lib.hasInfix "52.52" exec && lib.hasInfix "13.40" exec)
|
||||||
|
"wlsunset ExecStart lacks the configured coordinates: ${exec}"
|
||||||
|
&& expect (lib.hasInfix "4000" exec)
|
||||||
|
"wlsunset ExecStart lacks the night temperature: ${exec}"
|
||||||
|
&& expect (lib.hasInfix "should-start" cond)
|
||||||
|
"wlsunset lost the live-state ExecCondition gate"
|
||||||
|
&& expect (!(hm.config.services.hyprsunset.enable or false))
|
||||||
|
"geo mode must not also run hyprsunset";
|
||||||
|
in
|
||||||
|
assert ok; pkgs.runCommand "nomarchy-nightlight-geo" { } "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")
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# zram compressed-RAM swap (oom.nix, BACKLOG #76 slice): enabled
|
||||||
|
# by default as the memory-pressure layer *before* earlyoom. Prove
|
||||||
|
# the device comes up as an active swap, uses zstd, and carries the
|
||||||
|
# high priority that keeps day-to-day paging off any disk swapfile
|
||||||
|
# (which #76 reserves for the hibernate image). Imports oom.nix
|
||||||
|
# directly so the check tracks the module, not a hand-copy.
|
||||||
|
zram-swap = pkgs.testers.runNixOSTest {
|
||||||
|
name = "nomarchy-zram-swap";
|
||||||
|
nodes.machine = { ... }: {
|
||||||
|
imports = [ ./modules/nixos/oom.nix ];
|
||||||
|
virtualisation.memorySize = 1024;
|
||||||
|
};
|
||||||
|
testScript = ''
|
||||||
|
# zram-generator brings the device up as swap shortly after boot.
|
||||||
|
machine.wait_until_succeeds("grep -q /dev/zram0 /proc/swaps", timeout=60)
|
||||||
|
# Configured compression algorithm is the selected one ([zstd]).
|
||||||
|
machine.succeed("grep -q '\\[zstd\\]' /sys/block/zram0/comp_algorithm")
|
||||||
|
# High priority (100) so the kernel fills zram before any disk
|
||||||
|
# swap — the ordering #76's hibernate image depends on.
|
||||||
|
machine.succeed(
|
||||||
|
"awk '$1==\"/dev/zram0\" && $5==100 {ok=1} END{exit !ok}' /proc/swaps"
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Hibernate → resume (BACKLOG #76): the load-bearing risk the
|
||||||
|
# hibernation default introduces is coexistence with the zram
|
||||||
|
# swap that oom.nix now enables by default. Prove, in the VM
|
||||||
|
# harness, that (a) a machine hibernates and resumes for real —
|
||||||
|
# a volatile ramfs marker survives resume but NOT a fresh boot —
|
||||||
|
# and (b) zram (priority 100) does not swallow the hibernate
|
||||||
|
# image: the disk swap (lower priority, explicit resumeDevice)
|
||||||
|
# carries it. Modeled on nixpkgs' hibernate.nix (partition swap +
|
||||||
|
# systemd-initrd resume). What QEMU genuinely can't do is feed a
|
||||||
|
# boot-time `resume_offset` for a btrfs *swapfile* (the offset
|
||||||
|
# only exists after the file is allocated on first boot) or drive
|
||||||
|
# a LUKS-initrd unlock end-to-end — so the encrypted @swap
|
||||||
|
# swapfile + offset path stays a V3 laptop check (HARDWARE-QUEUE),
|
||||||
|
# which already runs that exact layout. This test imports oom.nix
|
||||||
|
# so it tracks the real zram default, not a hand-copy.
|
||||||
|
hibernate = pkgs.testers.runNixOSTest {
|
||||||
|
name = "nomarchy-hibernate";
|
||||||
|
nodes.machine = { config, lib, pkgs, ... }: {
|
||||||
|
imports = [ ./modules/nixos/oom.nix ]; # zram default-on (#76)
|
||||||
|
virtualisation.memorySize = 2048;
|
||||||
|
# Store + EFI must persist across the hibernate/resume/crash
|
||||||
|
# boots; a disk swap ≥ 2×RAM holds the image.
|
||||||
|
virtualisation.useNixStoreImage = true;
|
||||||
|
virtualisation.useEFIBoot = true;
|
||||||
|
virtualisation.emptyDiskImages = [ (2 * config.virtualisation.memorySize) ];
|
||||||
|
boot.initrd.systemd.enable = true; # systemd-initrd, like Nomarchy
|
||||||
|
# Disk swap at a LOWER priority than zram (100); explicit
|
||||||
|
# resumeDevice — exactly how the installer wires hibernation —
|
||||||
|
# so the image goes to disk regardless of zram's priority.
|
||||||
|
swapDevices = lib.mkOverride 0 [
|
||||||
|
{ device = "/dev/vdc"; options = [ "x-systemd.makefs" ]; priority = 1; }
|
||||||
|
];
|
||||||
|
boot.resumeDevice = "/dev/vdc";
|
||||||
|
# Let the driver's shell reconnect after resume.
|
||||||
|
systemd.services.backdoor.conflicts = [ "sleep.target" ];
|
||||||
|
powerManagement.resumeCommands = "systemctl --no-block restart backdoor.service";
|
||||||
|
};
|
||||||
|
testScript = ''
|
||||||
|
machine.wait_for_unit("default.target")
|
||||||
|
|
||||||
|
# zram (oom.nix) up at priority 100; disk swap present and lower
|
||||||
|
# — so the hibernate image lands on disk, not volatile zram.
|
||||||
|
machine.wait_until_succeeds("grep -q /dev/zram0 /proc/swaps", timeout=60)
|
||||||
|
machine.succeed(
|
||||||
|
"awk '$1==\"/dev/zram0\"&&$5==100{z=1} "
|
||||||
|
"$1==\"/dev/vdc\"&&$5<100{d=1} END{exit !(z&&d)}' /proc/swaps"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Marker in volatile RAM: survives a resume, never a fresh boot.
|
||||||
|
machine.succeed(
|
||||||
|
"mkdir /run/test",
|
||||||
|
"mount -t ramfs -o size=1m ramfs /run/test",
|
||||||
|
"echo not persisted to disk > /run/test/suspended",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Hibernate, wait for real power-off, then resume.
|
||||||
|
machine.execute("systemctl hibernate >&2 &", check_return=False)
|
||||||
|
machine.wait_for_shutdown()
|
||||||
|
machine.start()
|
||||||
|
machine.succeed("grep 'not persisted to disk' /run/test/suspended")
|
||||||
|
|
||||||
|
# Prove it was a genuine resume: a crash-boot must lose the marker.
|
||||||
|
machine.crash()
|
||||||
|
machine.wait_for_unit("default.target")
|
||||||
|
machine.fail("grep 'not persisted to disk' /run/test/suspended")
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# The installer's BTRFS @swap SWAPFILE setup (#76) — the parts the
|
||||||
|
# partition hibernate test (checks.hibernate) can't reach. A full
|
||||||
|
# hibernate→resume from a *runtime* swapfile isn't reachable
|
||||||
|
# headlessly: systemd reports CanHibernate=no unless the swap is
|
||||||
|
# declared in the boot config, and feeding a runtime-computed
|
||||||
|
# resume_offset into the boot cmdline is a genuine chicken-and-egg.
|
||||||
|
# So the power-cycle stays a real-hardware check (the dev box runs
|
||||||
|
# exactly this layout — /proc/cmdline verified). What IS verified
|
||||||
|
# here, bounded and deterministic, is that the swapfile is built
|
||||||
|
# correctly and its resume offset is valid: NOCOW `mkswapfile`, a
|
||||||
|
# good `map-swapfile -r` offset the kernel accepts, swapon as a file,
|
||||||
|
# and zram (prio 100) above it so the image lands on disk, not zram.
|
||||||
|
hibernate-swapfile = pkgs.testers.runNixOSTest {
|
||||||
|
name = "nomarchy-hibernate-swapfile";
|
||||||
|
nodes.machine = { config, lib, pkgs, ... }: {
|
||||||
|
imports = [ ./modules/nixos/oom.nix ]; # zram default-on (#76)
|
||||||
|
virtualisation.memorySize = 2048;
|
||||||
|
# A dedicated disk for the btrfs @swap subvolume.
|
||||||
|
virtualisation.emptyDiskImages = [ (4 * config.virtualisation.memorySize) ];
|
||||||
|
environment.systemPackages = [ pkgs.btrfs-progs ];
|
||||||
|
};
|
||||||
|
testScript = ''
|
||||||
|
machine.wait_for_unit("default.target")
|
||||||
|
machine.wait_until_succeeds("grep -q /dev/zram0 /proc/swaps", timeout=60)
|
||||||
|
|
||||||
|
# Reproduce the installer layout: btrfs, @swap subvolume, then a
|
||||||
|
# NOCOW swapfile via `btrfs filesystem mkswapfile`.
|
||||||
|
# The lone empty data disk (root is vda; no separate store disk
|
||||||
|
# here since useNixStoreImage is off).
|
||||||
|
machine.succeed("mkfs.btrfs -f /dev/vdb")
|
||||||
|
machine.succeed(
|
||||||
|
"mkdir -p /mnt-btrfs && mount /dev/vdb /mnt-btrfs "
|
||||||
|
"&& btrfs subvolume create /mnt-btrfs/@swap && umount /mnt-btrfs"
|
||||||
|
)
|
||||||
|
machine.succeed("mkdir -p /swap && mount -o subvol=@swap /dev/vdb /swap")
|
||||||
|
machine.succeed("btrfs filesystem mkswapfile --size 3g --uuid clear /swap/swapfile")
|
||||||
|
# NOCOW is mandatory — a copy-on-write swapfile corrupts on btrfs.
|
||||||
|
machine.succeed("lsattr /swap/swapfile | grep -q C")
|
||||||
|
machine.succeed("swapon /swap/swapfile")
|
||||||
|
machine.succeed(
|
||||||
|
"awk '$1==\"/swap/swapfile\" && $2==\"file\"{f=1} END{exit !f}' /proc/swaps"
|
||||||
|
)
|
||||||
|
|
||||||
|
# The Nomarchy-specific offset math (patch-template bakes this
|
||||||
|
# into resume_offset): a valid physical offset the kernel accepts.
|
||||||
|
offset = machine.succeed(
|
||||||
|
"btrfs inspect-internal map-swapfile -r /swap/swapfile"
|
||||||
|
).strip()
|
||||||
|
assert offset.isdigit() and int(offset) > 0, f"bad resume offset: {offset!r}"
|
||||||
|
machine.succeed(f"echo {offset} > /sys/power/resume_offset")
|
||||||
|
|
||||||
|
# zram (prio 100) must sit above the disk swapfile, so a hibernate
|
||||||
|
# image would land on disk, not volatile zram.
|
||||||
|
machine.succeed(
|
||||||
|
"awk '$1==\"/dev/zram0\"&&$5==100{z=1} "
|
||||||
|
"$1==\"/swap/swapfile\"&&$5<100{d=1} END{exit !(z&&d)}' /proc/swaps"
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Auto time-of-day theme (#79, VISION § D): simulate crossing
|
||||||
|
# sunset and sunrise by moving the VM clock, and assert
|
||||||
|
# `nomarchy-theme-sync auto` switches the active theme accordingly
|
||||||
|
# — day stays day (no needless rebuild), sunset flips to night,
|
||||||
|
# a later tick is idempotent, sunrise flips back. The actual
|
||||||
|
# `home-manager switch` is stubbed via NOMARCHY_REBUILD (that
|
||||||
|
# generic apply path is exercised by every manual theme change);
|
||||||
|
# this isolates the auto decision + apply + rebuild-trigger.
|
||||||
|
auto-theme = pkgs.testers.runNixOSTest {
|
||||||
|
name = "nomarchy-auto-theme";
|
||||||
|
nodes.machine = { pkgs, ... }: {
|
||||||
|
environment.systemPackages = [ pkgs.nomarchy-theme-sync ];
|
||||||
|
time.timeZone = "UTC"; # naive HH:MM == the clock we set
|
||||||
|
};
|
||||||
|
testScript = ''
|
||||||
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
machine.succeed("timedatectl set-ntp false || true")
|
||||||
|
|
||||||
|
# Writable flake state (start on the day theme) + a rebuild stub
|
||||||
|
# that records each invocation instead of running home-manager.
|
||||||
|
machine.succeed("mkdir -p /root/.nomarchy")
|
||||||
|
machine.succeed(
|
||||||
|
"echo '{\"slug\":\"summer-day\"}' > /root/.nomarchy/theme-state.json"
|
||||||
|
)
|
||||||
|
machine.succeed(
|
||||||
|
"printf '#!/bin/sh\\necho switch >> /root/rebuilds\\n' > /root/rebuild"
|
||||||
|
" && chmod +x /root/rebuild"
|
||||||
|
)
|
||||||
|
|
||||||
|
env = ("NOMARCHY_PATH=/root/.nomarchy "
|
||||||
|
"NOMARCHY_DEFAULT_THEMES=${./themes} "
|
||||||
|
"NOMARCHY_REBUILD=/root/rebuild ")
|
||||||
|
|
||||||
|
# Pair + schedule (setup writes only, no rebuild).
|
||||||
|
for kv in ["settings.autoTheme.enable true",
|
||||||
|
"settings.autoTheme.day summer-day",
|
||||||
|
"settings.autoTheme.night summer-night",
|
||||||
|
"settings.autoTheme.sunrise 07:00",
|
||||||
|
"settings.autoTheme.sunset 20:00"]:
|
||||||
|
machine.succeed(env + "nomarchy-theme-sync --quiet set " + kv + " --no-switch")
|
||||||
|
|
||||||
|
def slug():
|
||||||
|
return machine.succeed(env + "nomarchy-theme-sync get slug").strip()
|
||||||
|
def rebuilds():
|
||||||
|
return int(machine.succeed("wc -l < /root/rebuilds 2>/dev/null || echo 0").strip())
|
||||||
|
|
||||||
|
# Before sunset: already the day theme → no switch, no rebuild.
|
||||||
|
machine.succeed("date -s '2026-01-02 19:55:00'")
|
||||||
|
machine.succeed(env + "nomarchy-theme-sync auto")
|
||||||
|
assert slug() == "summer-day", f"pre-sunset should stay day, got {slug()}"
|
||||||
|
assert rebuilds() == 0, f"no needless rebuild, got {rebuilds()}"
|
||||||
|
|
||||||
|
# Cross sunset → night theme, exactly one rebuild triggered.
|
||||||
|
machine.succeed("date -s '2026-01-02 20:05:00'")
|
||||||
|
machine.succeed(env + "nomarchy-theme-sync auto")
|
||||||
|
assert slug() == "summer-night", f"post-sunset should be night, got {slug()}"
|
||||||
|
assert rebuilds() == 1, f"sunset should trigger one rebuild, got {rebuilds()}"
|
||||||
|
|
||||||
|
# Another tick still after sunset → idempotent, no extra rebuild.
|
||||||
|
machine.succeed("date -s '2026-01-02 20:06:00'")
|
||||||
|
machine.succeed(env + "nomarchy-theme-sync auto")
|
||||||
|
assert slug() == "summer-night"
|
||||||
|
assert rebuilds() == 1, f"idempotent tick must not rebuild, got {rebuilds()}"
|
||||||
|
|
||||||
|
# Cross sunrise the next morning → back to day, second rebuild.
|
||||||
|
machine.succeed("date -s '2026-01-03 07:30:00'")
|
||||||
|
machine.succeed(env + "nomarchy-theme-sync auto")
|
||||||
|
assert slug() == "summer-day", f"post-sunrise should be day, got {slug()}"
|
||||||
|
assert rebuilds() == 2, f"sunrise should trigger a rebuild, got {rebuilds()}"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Snapshot GUI canary: btrfs-assistant crashes in libbtrfsutil's
|
||||||
|
# UNPRIVILEGED subvolume iteration (btrfs-progs 6.17.1, fixed
|
||||||
|
# upstream after 6.17.1) but runs fine as root — which is how the
|
||||||
|
# menu launches it (pkexec launcher). Guard the root path on a
|
||||||
|
# real btrfs volume so a lock bump that breaks it fails here, not
|
||||||
|
# on a user's machine. The GUI event loop is exercised offscreen
|
||||||
|
# (survives a 5s timeout = no startup crash past the Btrfs scan).
|
||||||
|
snapshot-gui = pkgs.testers.runNixOSTest {
|
||||||
|
name = "nomarchy-snapshot-gui";
|
||||||
|
nodes.machine = { pkgs, ... }: {
|
||||||
|
environment.systemPackages = [ pkgs.btrfs-assistant pkgs.btrfs-progs ];
|
||||||
|
virtualisation.emptyDiskImages = [ 512 ];
|
||||||
|
};
|
||||||
|
testScript = ''
|
||||||
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
machine.succeed(
|
||||||
|
"mkfs.btrfs /dev/vdb && mkdir -p /mnt && mount /dev/vdb /mnt "
|
||||||
|
"&& btrfs subvolume create /mnt/sub1"
|
||||||
|
)
|
||||||
|
# Root (the launcher's effective context): must work.
|
||||||
|
machine.succeed("QT_QPA_PLATFORM=offscreen btrfs-assistant-bin --version")
|
||||||
|
# The GUI proper survives startup (timeout kill = alive → 124).
|
||||||
|
machine.succeed(
|
||||||
|
"set +e; timeout 5 env QT_QPA_PLATFORM=offscreen btrfs-assistant-bin; "
|
||||||
|
"[ $? -eq 124 ]"
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# The distroId question (roadmap "Distro branding"): set
|
||||||
|
# distroId = "nomarchy" so /etc/os-release is honest (ID=nomarchy,
|
||||||
|
# ID_LIKE=nixos). The risk is switch-to-configuration's "is this
|
||||||
|
# NixOS?" guard — but it builds the check from the *configured*
|
||||||
|
# distroId (and /etc/NIXOS still exists), so a rebuild must still
|
||||||
|
# work. This boots such a system and runs `switch-to-configuration
|
||||||
|
# dry-activate` to prove the guard passes. Inline (not the full
|
||||||
|
# module) so the VM stays minimal — the real wiring in default.nix
|
||||||
|
# is covered by the downstream-template-system build.
|
||||||
|
distro-id = pkgs.testers.runNixOSTest {
|
||||||
|
name = "nomarchy-distro-id";
|
||||||
|
nodes.machine = { ... }: {
|
||||||
|
system.nixos.distroName = "Nomarchy";
|
||||||
|
system.nixos.distroId = "nomarchy";
|
||||||
|
# Test base omits switch-to-configuration by default; we need it
|
||||||
|
# to exercise the "is this NixOS?" guard.
|
||||||
|
system.switch.enable = true;
|
||||||
|
};
|
||||||
|
testScript = { nodes, ... }: ''
|
||||||
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
|
||||||
|
# os-release reflects the rebrand, with the nixos lineage marker.
|
||||||
|
machine.succeed("grep -q '^ID=nomarchy$' /etc/os-release")
|
||||||
|
machine.succeed("grep -q '^ID_LIKE=nixos$' /etc/os-release")
|
||||||
|
machine.succeed("grep -q '^NAME=Nomarchy$' /etc/os-release")
|
||||||
|
|
||||||
|
# The rebuild path must still recognise this as a NixOS install
|
||||||
|
# (the guard is built from the configured distroId, not "nixos").
|
||||||
|
machine.succeed(
|
||||||
|
"${nodes.machine.system.build.toplevel}/bin/switch-to-configuration dry-activate"
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# ─── Reference host ────────────────────────────────────────────
|
# ─── Reference host ────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,29 +1,89 @@
|
|||||||
# Live ISO host — boot the full Nomarchy desktop from a USB stick or QEMU
|
# 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
|
# without touching the disk. The live session bundles nomarchy-install
|
||||||
# exists to test the distro end-to-end on real hardware.
|
# (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, ... }:
|
{ lib, pkgs, username, nomarchySrc, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
# ISO boot splash: the Nomarchy monogram recolored to the palette accent,
|
# ISO boot branding: the Nomarchy monogram recolored to the palette accent,
|
||||||
# centred on the theme base. Built from the vendored vector logo and the
|
# centred on the theme base. Built from the vendored vector logo and the
|
||||||
# live theme-state.json (tokyo-night by default). Shows on the isolinux
|
# live theme-state.json (Boreal by default). The same composed image
|
||||||
# (BIOS) boot menu; UEFI/GRUB still uses the stock theme (see roadmap).
|
# backs both the isolinux (BIOS) splash and the GRUB (UEFI) theme below, so
|
||||||
|
# the two boot paths match.
|
||||||
state = builtins.fromJSON (builtins.readFile ../theme-state.json);
|
state = builtins.fromJSON (builtins.readFile ../theme-state.json);
|
||||||
isoColor = key: fallback: lib.removePrefix "#" ((state.colors or { }).${key} or fallback);
|
isoColor = key: fallback: lib.removePrefix "#" ((state.colors or { }).${key} or fallback);
|
||||||
|
accent = isoColor "accent" "B79BE8";
|
||||||
|
base = isoColor "base" "21272F";
|
||||||
|
subtext = isoColor "subtext" "97A3B2";
|
||||||
|
|
||||||
isoSplash = pkgs.runCommand "nomarchy-iso-splash.png"
|
isoSplash = pkgs.runCommand "nomarchy-iso-splash.png"
|
||||||
{ nativeBuildInputs = [ pkgs.imagemagick pkgs.librsvg ]; } ''
|
{ nativeBuildInputs = [ pkgs.imagemagick pkgs.librsvg ]; } ''
|
||||||
rsvg-convert -h 320 ${../modules/nixos/branding/logo.svg} > logo.png
|
rsvg-convert -h 320 ${../modules/nixos/branding/logo.svg} > logo.png
|
||||||
magick logo.png -fill "#${isoColor "accent" "7aa2f7"}" -colorize 100 logo-c.png
|
magick logo.png -fill "#${accent}" -colorize 100 logo-c.png
|
||||||
magick -size 1920x1080 xc:"#${isoColor "base" "1a1b26"}" \
|
magick -size 1920x1080 xc:"#${base}" \
|
||||||
logo-c.png -gravity center -composite $out
|
logo-c.png -gravity center -composite $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
# GRUB (UEFI) theme matched to the BIOS splash: the same accent-logo-on-base
|
||||||
|
# image as the background, plus a palette-coloured boot menu in the lower
|
||||||
|
# third (clear of the centred logo). Derived from nixos-grub2-theme only to
|
||||||
|
# reuse its bundled DejaVu .pf2 font — we overwrite the background and
|
||||||
|
# theme.txt and drop the stock NixOS logo (ours is in the background). grub
|
||||||
|
# loads every .pf2 in the dir, so "DejaVu Regular" resolves.
|
||||||
|
grubThemeTxt = pkgs.writeText "nomarchy-grub-theme.txt" ''
|
||||||
|
title-text: ""
|
||||||
|
desktop-image: "background.png"
|
||||||
|
desktop-color: "#${base}"
|
||||||
|
|
||||||
|
message-font: "DejaVu Regular"
|
||||||
|
message-color: "#${subtext}"
|
||||||
|
terminal-font: "DejaVu Regular"
|
||||||
|
|
||||||
|
+ boot_menu {
|
||||||
|
left = 50%-300
|
||||||
|
width = 600
|
||||||
|
top = 64%
|
||||||
|
height = 26%
|
||||||
|
item_font = "DejaVu Regular"
|
||||||
|
item_color = "#${subtext}"
|
||||||
|
item_height = 36
|
||||||
|
item_spacing = 6
|
||||||
|
selected_item_font = "DejaVu Regular"
|
||||||
|
selected_item_color = "#${accent}"
|
||||||
|
scrollbar = false
|
||||||
|
}
|
||||||
|
|
||||||
|
+ progress_bar {
|
||||||
|
id = "__timeout__"
|
||||||
|
left = 50%-300
|
||||||
|
top = 92%
|
||||||
|
width = 600
|
||||||
|
height = 16
|
||||||
|
show_text = true
|
||||||
|
text = "@TIMEOUT_NOTIFICATION_MIDDLE@"
|
||||||
|
font = "DejaVu Regular"
|
||||||
|
text_color = "#${subtext}"
|
||||||
|
border_color = "#${accent}"
|
||||||
|
bg_color = "#${base}"
|
||||||
|
fg_color = "#${accent}"
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
|
||||||
|
nomarchyGrubTheme = pkgs.runCommand "nomarchy-grub-theme" { } ''
|
||||||
|
cp -r ${pkgs.nixos-grub2-theme} $out
|
||||||
|
chmod -R u+w $out
|
||||||
|
cp ${isoSplash} $out/background.png
|
||||||
|
cp ${grubThemeTxt} $out/theme.txt
|
||||||
|
rm -f $out/logo.png
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
networking.hostName = "nomarchy-live";
|
networking.hostName = "nomarchy-live";
|
||||||
|
|
||||||
isoImage.volumeID = lib.mkForce "NOMARCHY_LIVE";
|
isoImage.volumeID = lib.mkForce "NOMARCHY_LIVE";
|
||||||
isoImage.edition = lib.mkForce "live";
|
isoImage.edition = lib.mkForce "live";
|
||||||
isoImage.splashImage = isoSplash;
|
isoImage.splashImage = isoSplash; # isolinux / BIOS
|
||||||
|
isoImage.grubTheme = nomarchyGrubTheme; # GRUB / UEFI
|
||||||
|
|
||||||
# The minimal-CD profile slims the image for a CONSOLE installer; this
|
# The minimal-CD profile slims the image for a CONSOLE installer; this
|
||||||
# ISO is the desktop, so re-enable what it strips. Above all
|
# ISO is the desktop, so re-enable what it strips. Above all
|
||||||
@@ -112,6 +172,20 @@ in
|
|||||||
# regression). Installed systems keep idle management.
|
# regression). Installed systems keep idle management.
|
||||||
nomarchy.idle.enable = false;
|
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 = {
|
wayland.windowManager.hyprland.settings = {
|
||||||
# QEMU (and some panels) report a tiny "preferred" mode; ask for
|
# QEMU (and some panels) report a tiny "preferred" mode; ask for
|
||||||
# the highest resolution instead.
|
# the highest resolution instead.
|
||||||
@@ -119,7 +193,7 @@ in
|
|||||||
# Welcome toast once the session is up (concatenated onto the
|
# Welcome toast once the session is up (concatenated onto the
|
||||||
# base exec-once list).
|
# base exec-once list).
|
||||||
exec-once = [
|
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.
|
# in flake.nix remain the escape hatch for power users.
|
||||||
{ nixpkgs, home-manager, nixos-hardware, nomarchy }:
|
{ 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 =
|
mkFlake =
|
||||||
{ src # the downstream flake directory (./.)
|
{ src # the downstream flake directory (./.)
|
||||||
, username # login name; also the homeConfigurations attr
|
, username # login name; also the homeConfigurations attr
|
||||||
@@ -13,8 +22,6 @@
|
|||||||
, system ? "x86_64-linux"
|
, system ? "x86_64-linux"
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
inherit (nixpkgs) lib;
|
|
||||||
|
|
||||||
# One profile or several (the installer's autodetection emits a few
|
# One profile or several (the installer's autodetection emits a few
|
||||||
# common-* modules alongside the model-specific one).
|
# common-* modules alongside the model-specific one).
|
||||||
profileNames =
|
profileNames =
|
||||||
@@ -51,8 +58,17 @@
|
|||||||
# and the standalone HM desktop see the same package set.
|
# and the standalone HM desktop see the same package set.
|
||||||
config.allowUnfree = true;
|
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
|
in
|
||||||
{
|
builtins.seq _themeState {
|
||||||
# System layer — rebuilt rarely:
|
# System layer — rebuilt rarely:
|
||||||
# sudo nixos-rebuild switch --flake .#default
|
# sudo nixos-rebuild switch --flake .#default
|
||||||
nixosConfigurations.default = nixpkgs.lib.nixosSystem {
|
nixosConfigurations.default = nixpkgs.lib.nixosSystem {
|
||||||
@@ -67,7 +83,7 @@
|
|||||||
{ environment.systemPackages = [ home-manager.packages.${system}.home-manager ]; }
|
{ environment.systemPackages = [ home-manager.packages.${system}.home-manager ]; }
|
||||||
# System-side theme consumers (Plymouth splash background)
|
# System-side theme consumers (Plymouth splash background)
|
||||||
# read the same JSON the desktop does.
|
# read the same JSON the desktop does.
|
||||||
{ nomarchy.system.stateFile = src + "/theme-state.json"; }
|
{ nomarchy.system.stateFile = statePath; }
|
||||||
]
|
]
|
||||||
++ hardwareModules
|
++ hardwareModules
|
||||||
++ [
|
++ [
|
||||||
@@ -87,7 +103,7 @@
|
|||||||
{
|
{
|
||||||
# Written by nomarchy-theme-sync; reading it is pure — the
|
# Written by nomarchy-theme-sync; reading it is pure — the
|
||||||
# file is part of the downstream flake's source.
|
# file is part of the downstream flake's source.
|
||||||
nomarchy.stateFile = src + "/theme-state.json";
|
nomarchy.stateFile = statePath;
|
||||||
home = {
|
home = {
|
||||||
inherit username;
|
inherit username;
|
||||||
homeDirectory = "/home/${username}";
|
homeDirectory = "/home/${username}";
|
||||||
|
|||||||
36
modules/home/autotheme.nix
Normal file
36
modules/home/autotheme.nix
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Auto time-of-day theme switch (BACKLOG #79, VISION § D). A user timer
|
||||||
|
# periodically runs `nomarchy-theme-sync auto`, which reads
|
||||||
|
# settings.autoTheme = { enable, day, night, sunrise, sunset } from the
|
||||||
|
# state file and applies the day or night preset for the current clock —
|
||||||
|
# through the SAME one engine as a manual `apply` (no second pipeline).
|
||||||
|
#
|
||||||
|
# The command self-gates (no-op when disabled) and is idempotent (it only
|
||||||
|
# rebuilds when the active theme actually needs to change), so the timer
|
||||||
|
# can tick freely. Install is gated on the state flag (like nomarchy.updates
|
||||||
|
# gates on its enable), so a machine not using the feature carries no timer;
|
||||||
|
# enabling it from the menu (slice 3) writes the flag + rebuilds, which is
|
||||||
|
# what brings the timer into being.
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
lib.mkIf (config.nomarchy.settings.autoTheme.enable or false) {
|
||||||
|
systemd.user.services.nomarchy-auto-theme = {
|
||||||
|
Unit.Description = "Apply the day/night theme for the current time";
|
||||||
|
Service = {
|
||||||
|
Type = "oneshot";
|
||||||
|
# The rebuild (`home-manager switch`) and its tools resolve from the
|
||||||
|
# system + per-user profiles — same PATH shape nomarchy-updates uses.
|
||||||
|
Environment = "PATH=/run/current-system/sw/bin:/etc/profiles/per-user/${config.home.username}/bin";
|
||||||
|
ExecStart = "${pkgs.nomarchy-theme-sync}/bin/nomarchy-theme-sync auto";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.user.timers.nomarchy-auto-theme = {
|
||||||
|
Unit.Description = "Periodic day/night theme check";
|
||||||
|
Timer = {
|
||||||
|
OnStartupSec = "1min"; # settle on the right theme shortly after login
|
||||||
|
OnCalendar = "*:0/15"; # re-check every 15 min; auto no-ops if unchanged
|
||||||
|
Persistent = true; # catch a transition missed while powered off
|
||||||
|
};
|
||||||
|
Install.WantedBy = [ "timers.target" ];
|
||||||
|
};
|
||||||
|
}
|
||||||
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.
|
# Nomarchy — Home Manager entry point.
|
||||||
# Consume this via homeModules.nomarchy (flake.nix), which also pulls in
|
# Consume this via homeModules.nomarchy (flake.nix), which also pulls in
|
||||||
# the stylix home module that stylix.nix configures.
|
# the stylix home module that stylix.nix configures.
|
||||||
{ config, pkgs, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
@@ -18,30 +18,62 @@
|
|||||||
./yazi.nix # flagship TUI file manager, themed + plugins
|
./yazi.nix # flagship TUI file manager, themed + plugins
|
||||||
./osd.nix # swayosd volume/brightness OSD, themed
|
./osd.nix # swayosd volume/brightness OSD, themed
|
||||||
./nightlight.nix # scheduled blue-light filter (hyprsunset), opt-in
|
./nightlight.nix # scheduled blue-light filter (hyprsunset), opt-in
|
||||||
|
./autotheme.nix # auto day/night theme switch (settings.autoTheme), opt-in
|
||||||
|
./timezone.nix # keep the Waybar clock in step with auto-timezone changes
|
||||||
|
./updates.nix # passive update-awareness indicator + notification, opt-in
|
||||||
./shell.nix # zsh + starship + bat/eza/zoxide, themed
|
./shell.nix # zsh + starship + bat/eza/zoxide, themed
|
||||||
./keys.nix # gpg-agent (fronts SSH) + pinentry-qt
|
./keys.nix # gpg-agent (fronts SSH) + pinentry-qt
|
||||||
./fastfetch.nix # system info with the themed Nomarchy logo
|
./fastfetch.nix # system info with the themed Nomarchy logo
|
||||||
|
./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
|
# Clipboard history (wl-paste watcher); browsed via the SUPER+CTRL+V
|
||||||
# menu module.
|
# menu module.
|
||||||
services.cliphist.enable = true;
|
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
|
# Wifi from the bar: nm-applet lives in waybar's tray (SNI flag via
|
||||||
# preferStatusNotifierItems — without it there is no tray icon).
|
# preferStatusNotifierItems — without it there is no tray icon).
|
||||||
services.network-manager-applet.enable = true;
|
services.network-manager-applet.enable = true;
|
||||||
xsession.preferStatusNotifierItems = 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; [
|
home.packages = with pkgs; [
|
||||||
awww # wallpaper daemon with animated transitions (the swww fork)
|
awww # wallpaper daemon with animated transitions (the swww fork)
|
||||||
libnotify
|
libnotify
|
||||||
|
hyprpicker
|
||||||
|
# Lifecycle CLIs (pull/rebuild/home). HM profile usually precedes
|
||||||
|
# /run/current-system on PATH, so nomarchy-home can replace a broken
|
||||||
|
# system-generation nomarchy-pull without a full sys rebuild.
|
||||||
|
nomarchy-lifecycle
|
||||||
];
|
];
|
||||||
|
|
||||||
home.sessionVariables = {
|
home.sessionVariables = {
|
||||||
TERMINAL = config.nomarchy.terminal;
|
TERMINAL = config.nomarchy.terminal;
|
||||||
NIXOS_OZONE_WL = "1"; # Electron/Chromium native Wayland
|
NIXOS_OZONE_WL = "1"; # Electron/Chromium native Wayland
|
||||||
|
# Same gate as modules/nixos: CLI nix-shell/shell/run for unfree pkgs
|
||||||
|
# (system nixpkgs.config alone does not cover those entry points).
|
||||||
|
NIXPKGS_ALLOW_UNFREE = "1";
|
||||||
|
|
||||||
# Where the Nomarchy flake (and therefore theme-state.json) lives on
|
# Where the Nomarchy flake (and therefore theme-state.json) lives on
|
||||||
# disk. nomarchy-theme-sync writes its state here; rebuilds read from
|
# disk. nomarchy-theme-sync writes its state here; rebuilds read from
|
||||||
@@ -49,5 +81,12 @@
|
|||||||
NOMARCHY_PATH = "$HOME/.nomarchy";
|
NOMARCHY_PATH = "$HOME/.nomarchy";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Classic nix-shell / nix-env read this; pairs with NIXPKGS_ALLOW_UNFREE
|
||||||
|
# above so "allow unfree" is the default desktop experience, not a
|
||||||
|
# per-command export the user has to remember.
|
||||||
|
xdg.configFile."nixpkgs/config.nix".text = ''
|
||||||
|
{ allowUnfree = true; }
|
||||||
|
'';
|
||||||
|
|
||||||
programs.home-manager.enable = true;
|
programs.home-manager.enable = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,16 @@ let
|
|||||||
c = t.colors;
|
c = t.colors;
|
||||||
in
|
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;
|
enable = true;
|
||||||
enableBashIntegration = true;
|
enableBashIntegration = true;
|
||||||
|
|
||||||
settings = {
|
settings = lib.mkIf config.nomarchy.ghostty.enable {
|
||||||
# ── Typography (from theme-state.json) ────────────────────────
|
# ── Typography (from theme-state.json) ────────────────────────
|
||||||
font-family = t.fonts.mono;
|
font-family = t.fonts.mono;
|
||||||
font-size = t.fonts.size;
|
font-size = t.fonts.size;
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ let
|
|||||||
c = t.colors;
|
c = t.colors;
|
||||||
inherit (config.nomarchy.lib) rgb rgba;
|
inherit (config.nomarchy.lib) rgb rgba;
|
||||||
|
|
||||||
|
# nomarchy-theme-sync — the in-flake state writer the keyboard watcher uses
|
||||||
|
# to remember per-device layouts (same path night-light's toggle takes).
|
||||||
|
sync = lib.getExe config.nomarchy.package;
|
||||||
|
|
||||||
# swayosd's `--input-volume mute-toggle` draws the OSD but never flips the
|
# swayosd's `--input-volume mute-toggle` draws the OSD but never flips the
|
||||||
# source mute (verified on hardware: wpctl toggles the state, swayosd's
|
# source mute (verified on hardware: wpctl toggles the state, swayosd's
|
||||||
# input-mute path is a no-op — unlike its working output-mute path). Do the
|
# input-mute path is a no-op — unlike its working output-mute path). Do the
|
||||||
@@ -27,6 +31,34 @@ let
|
|||||||
${pkgs.swayosd}/bin/swayosd-client --custom-icon "$icon" --custom-progress "$2"
|
${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.
|
# SUPER+1..9 / SUPER+SHIFT+1..9 workspace binds, generated.
|
||||||
workspaceBinds = builtins.concatLists (builtins.genList
|
workspaceBinds = builtins.concatLists (builtins.genList
|
||||||
(i:
|
(i:
|
||||||
@@ -42,23 +74,169 @@ let
|
|||||||
keybinds = import ./keybinds.nix;
|
keybinds = import ./keybinds.nix;
|
||||||
mkBind = b: "${b.mods}, ${b.key}, ${b.action}";
|
mkBind = b: "${b.mods}, ${b.key}, ${b.action}";
|
||||||
|
|
||||||
# A nomarchy.monitors entry -> a Hyprland `monitor` rule. Unset optional
|
# Monitor-rule composition (rendering + the display-profile/resolution
|
||||||
# fields are omitted; `resolution = "disable"` collapses to the short form.
|
# overlays) lives in ./monitor-rules.nix — pure, so
|
||||||
monitorRule = m:
|
# checks.display-profiles unit-tests the overlay semantics directly.
|
||||||
if m.resolution == "disable" then "${m.name}, disable"
|
# The layering story (profiles replace whole-by-name, menu resolution
|
||||||
else lib.concatStringsSep ", " (
|
# picks apply field-level, disable-guard) is documented there.
|
||||||
[ m.name m.resolution (toString m.position) (toString m.scale) ]
|
monitorLib = import ./monitor-rules.nix { inherit lib; };
|
||||||
++ lib.optionals (m.transform != null) [ "transform" (toString m.transform) ]
|
monitorRule = monitorLib.rule;
|
||||||
++ lib.optionals (m.mirror != null) [ "mirror" m.mirror ]
|
|
||||||
++ lib.optionals (m.bitdepth != null) [ "bitdepth" (toString m.bitdepth) ]
|
|
||||||
++ lib.optionals (m.vrr != null) [ "vrr" (toString m.vrr) ]
|
|
||||||
++ lib.optional (m.extra != "") m.extra
|
|
||||||
);
|
|
||||||
|
|
||||||
# Keyboard layout candidates: the primary nomarchy.keyboard.layout (split on
|
# The night-light pattern: nomarchy-display-profile applies rules live
|
||||||
# commas) plus any nomarchy.keyboard.layouts. Drives input.kb_layout so all
|
# via hyprctl and writes settings.displayProfile with --no-switch;
|
||||||
# of them are switchable, and feeds the watcher's picker.
|
# resolve bakes the same choice here at the next rebuild.
|
||||||
kbLayouts = lib.unique (
|
# 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)
|
(lib.splitString "," config.nomarchy.keyboard.layout)
|
||||||
++ config.nomarchy.keyboard.layouts
|
++ config.nomarchy.keyboard.layouts
|
||||||
);
|
);
|
||||||
@@ -68,21 +246,41 @@ let
|
|||||||
# keyboard.devices, not already remembered), ask for a layout and persist it
|
# keyboard.devices, not already remembered), ask for a layout and persist it
|
||||||
# per-device, re-applying silently on later reconnects. Stateful runtime
|
# per-device, re-applying silently on later reconnects. Stateful runtime
|
||||||
# piece — the complement to the declarative keyboard.devices. Reliable
|
# piece — the complement to the declarative keyboard.devices. Reliable
|
||||||
# primitives only: poll hyprctl devices, apply via switchxkblayout (an index
|
# primitives only: poll hyprctl devices, apply with a per-device
|
||||||
# into kbLayouts). NOTE: needs an on-hardware test (hotplug isn't verifiable
|
# `device[<name>]:kb_layout` keyword — the runtime twin of the declarative
|
||||||
# in CI).
|
# device blocks, so it isolates that one keyboard and never disturbs the
|
||||||
|
# built-in board (switchxkblayout flipped the *shared* layout, which leaked
|
||||||
|
# onto the laptop and stuck after unplug).
|
||||||
|
#
|
||||||
|
# The remembered choices live in the git-tracked in-flake state
|
||||||
|
# (settings.keyboard.devices), NOT ~/.local/state — read live from the
|
||||||
|
# working tree (so a pick this session is honoured at once) and written
|
||||||
|
# instantly with `--no-switch` (no rebuild). A later rebuild graduates them
|
||||||
|
# into nomarchy.keyboard.devices (generated device{} blocks), after which the
|
||||||
|
# watcher treats them as declared and steps back. NOTE: needs an on-hardware
|
||||||
|
# test (hotplug isn't verifiable in CI).
|
||||||
keyboardWatch = pkgs.writeShellScriptBin "nomarchy-keyboard-watch" ''
|
keyboardWatch = pkgs.writeShellScriptBin "nomarchy-keyboard-watch" ''
|
||||||
set -u
|
set -u
|
||||||
layouts="${lib.concatStringsSep " " kbLayouts}"
|
layouts="${lib.concatStringsSep " " pickerLayouts}"
|
||||||
declared="${lib.concatStringsSep " " (builtins.attrNames config.nomarchy.keyboard.devices)}"
|
declared="${lib.concatStringsSep " " (builtins.attrNames config.nomarchy.keyboard.devices)}"
|
||||||
state="''${XDG_STATE_HOME:-$HOME/.local/state}/nomarchy/keyboard-layouts"
|
sync=${sync}
|
||||||
mkdir -p "$(dirname "$state")"; [ -f "$state" ] || : > "$state"
|
|
||||||
|
|
||||||
layout_index() { i=0; for l in $layouts; do [ "$l" = "$1" ] && { printf %s "$i"; return; }; i=$((i + 1)); done; printf %s -1; }
|
apply() { hyprctl keyword "device[$1]:kb_layout" "$2" >/dev/null 2>&1; }
|
||||||
apply() { idx=$(layout_index "$2"); [ "$idx" -ge 0 ] && hyprctl switchxkblayout "$1" "$idx" >/dev/null 2>&1; }
|
|
||||||
keyboards() { hyprctl devices -j | jq -r '.keyboards[].name'; }
|
keyboards() { hyprctl devices -j | jq -r '.keyboards[].name'; }
|
||||||
is_declared() { case " $declared " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
|
is_declared() { case " $declared " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
|
||||||
saved_for() { while IFS='=' read -r k v; do [ "$k" = "$1" ] && { printf '%s' "$v"; return; }; done < "$state"; }
|
|
||||||
|
# The remembered map (device-name -> layout) from the LIVE in-flake state;
|
||||||
|
# an absent key (sparse state) reads as an empty object.
|
||||||
|
saved_map() { "$sync" get settings.keyboard.devices 2>/dev/null || echo '{}'; }
|
||||||
|
saved_for() { saved_map | jq -r --arg k "$1" '.[$k] // empty'; }
|
||||||
|
|
||||||
|
# Persist a pick INSTANTLY: merge it into the map and write the whole object
|
||||||
|
# back at the dot-free parent path, so a device name containing a dot can't
|
||||||
|
# corrupt the dotted set-path. --no-switch = write only, no rebuild.
|
||||||
|
remember() {
|
||||||
|
map=$(saved_map | jq -c --arg k "$1" --arg v "$2" '. + {($k): $v}') || return
|
||||||
|
"$sync" --quiet set settings.keyboard.devices "$map" --no-switch
|
||||||
|
}
|
||||||
|
|
||||||
# Startup: re-apply remembered layouts, never prompt — so the built-in
|
# Startup: re-apply remembered layouts, never prompt — so the built-in
|
||||||
# keyboard just stays on the session default.
|
# keyboard just stays on the session default.
|
||||||
@@ -106,7 +304,7 @@ let
|
|||||||
apply "$kb" "$s"
|
apply "$kb" "$s"
|
||||||
else
|
else
|
||||||
choice=$(printf '%s\n' $layouts | rofi -dmenu -p "Layout · $kb")
|
choice=$(printf '%s\n' $layouts | rofi -dmenu -p "Layout · $kb")
|
||||||
[ -n "$choice" ] && { printf '%s=%s\n' "$kb" "$choice" >> "$state"; apply "$kb" "$choice"; }
|
[ -n "$choice" ] && { remember "$kb" "$choice"; apply "$kb" "$choice"; }
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
prev="$cur"
|
prev="$cur"
|
||||||
@@ -135,7 +333,11 @@ in
|
|||||||
# and Hyprland applies them on hotplug. mkDefault so a downstream
|
# and Hyprland applies them on hotplug. mkDefault so a downstream
|
||||||
# `monitor = [...]` replaces the lot (the live ISO uses mkForce too).
|
# `monitor = [...]` replaces the lot (the live ISO uses mkForce too).
|
||||||
monitor = lib.mkDefault
|
monitor = lib.mkDefault
|
||||||
([ ",preferred,auto,1" ] ++ map monitorRule config.nomarchy.monitors);
|
([ ",preferred,auto,1" ] ++ map monitorRule resolvedMonitors);
|
||||||
|
|
||||||
|
# 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
|
# exec-once is a list at normal priority, so downstream additions
|
||||||
# CONCATENATE (your autostart runs alongside ours) rather than
|
# CONCATENATE (your autostart runs alongside ours) rather than
|
||||||
@@ -146,7 +348,21 @@ in
|
|||||||
# Paint the wallpaper as soon as the session is up (waits for
|
# Paint the wallpaper as soon as the session is up (waits for
|
||||||
# the daemon internally).
|
# the daemon internally).
|
||||||
"nomarchy-theme-sync wallpaper"
|
"nomarchy-theme-sync wallpaper"
|
||||||
] ++ lib.optional kbAutoSwitch "${keyboardWatch}/bin/nomarchy-keyboard-watch";
|
# Polkit authentication agent — without one, EVERY pkexec/polkit
|
||||||
|
# prompt in the session fails silently (btrfs-assistant-launcher
|
||||||
|
# was the discovery case). hyprpolkitagent is Hyprland's own Qt
|
||||||
|
# agent, so the prompt is Stylix-themed like every other Qt
|
||||||
|
# surface. Its binary lives in libexec (not bin), hence the path.
|
||||||
|
"${pkgs.hyprpolkitagent}/libexec/hyprpolkitagent"
|
||||||
|
# Waybar is launched here rather than as a systemd user service: bound
|
||||||
|
# to graphical-session.target the unit raced Hyprland's IPC on relogin
|
||||||
|
# and landed in `failed`; exec-once only fires once the compositor is
|
||||||
|
# up. 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 ──────────────────────────────────────────
|
# ── Theme-driven look ──────────────────────────────────────────
|
||||||
# These flow from theme-state.json and stay at NORMAL priority:
|
# These flow from theme-state.json and stay at NORMAL priority:
|
||||||
@@ -201,8 +417,10 @@ in
|
|||||||
|
|
||||||
input = {
|
input = {
|
||||||
# kb_layout/variant come from nomarchy.keyboard.* — set that
|
# kb_layout/variant come from nomarchy.keyboard.* — set that
|
||||||
# option (the installer writes it; it also drives tty/LUKS).
|
# option (the installer writes it; it also drives tty/LUKS). Only the
|
||||||
kb_layout = lib.concatStringsSep "," kbLayouts;
|
# session layout(s) land here; the keyboard.layouts pool is applied
|
||||||
|
# per-device to external boards, never onto the session keyboard.
|
||||||
|
kb_layout = config.nomarchy.keyboard.layout;
|
||||||
kb_variant = config.nomarchy.keyboard.variant;
|
kb_variant = config.nomarchy.keyboard.variant;
|
||||||
follow_mouse = lib.mkDefault 1;
|
follow_mouse = lib.mkDefault 1;
|
||||||
touchpad.natural_scroll = lib.mkDefault true;
|
touchpad.natural_scroll = lib.mkDefault true;
|
||||||
@@ -231,13 +449,79 @@ in
|
|||||||
disable_hyprland_logo = lib.mkDefault true;
|
disable_hyprland_logo = lib.mkDefault true;
|
||||||
disable_splash_rendering = lib.mkDefault true;
|
disable_splash_rendering = lib.mkDefault true;
|
||||||
force_default_wallpaper = lib.mkDefault 0;
|
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 float set (item 41 closed): only dialogs we ship or can
|
||||||
|
# name from package strings / desktop files. Classes: mixer,
|
||||||
|
# blueman, system-config-printer, calendar, hyprpolkitagent,
|
||||||
|
# pinentry-qt, and xdg-desktop-portal-gtk (desktop file + libexec
|
||||||
|
# name; .…-wrapped for the Nix wrapper). SoftGL could not materialize
|
||||||
|
# portal windows; polkit float confirmed on Latitude 2026-07-10.
|
||||||
|
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).
|
||||||
|
# workspace current: agent was started on ws1 at login; without this
|
||||||
|
# the dialog opens on that workspace and the current one looks dead
|
||||||
|
# (hardware: Snapshots menu from ws≠1). stay_focused (0.55 name;
|
||||||
|
# was stayfocused) so it grabs attention when it lands.
|
||||||
|
"float 1, match:class (?i)^(hyprpolkitagent|\\.hyprpolkitagent-wrapped)$"
|
||||||
|
"center 1, match:class (?i)^(hyprpolkitagent|\\.hyprpolkitagent-wrapped)$"
|
||||||
|
"workspace current, match:class (?i)^(hyprpolkitagent|\\.hyprpolkitagent-wrapped)$"
|
||||||
|
"stay_focused 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)$"
|
||||||
|
"workspace current, match:class (?i)^(pinentry(-qt)?|org\\.gnupg\\.pinentry-qt)$"
|
||||||
|
|
||||||
|
# GTK file-chooser portal (xdg-desktop-portal-gtk) — class is the
|
||||||
|
# binary/desktop name; Nix wrap = .xdg-desktop-portal-gtk-wrapped.
|
||||||
|
"float 1, match:class (?i)^(\\.?xdg-desktop-portal-gtk(-wrapped)?)$"
|
||||||
|
"center 1, match:class (?i)^(\\.?xdg-desktop-portal-gtk(-wrapped)?)$"
|
||||||
|
];
|
||||||
|
|
||||||
# Rendered from ./keybinds.nix (the cheatsheet reads the same list),
|
# Rendered from ./keybinds.nix (the cheatsheet reads the same list),
|
||||||
# plus the generated per-workspace binds. Like exec-once above this
|
# plus the generated per-workspace binds. Like exec-once above this
|
||||||
# stays a normal-priority list, so a downstream `bind = [...]`
|
# stays a normal-priority list, so a downstream `bind = [...]`
|
||||||
# concatenates rather than replaces.
|
# concatenates rather than replaces. The layout-cycle bind only
|
||||||
bind = map mkBind keybinds.binds ++ workspaceBinds;
|
# exists when there is something to cycle (same comma condition
|
||||||
|
# the Waybar language indicator gates on).
|
||||||
|
bind = map mkBind (keybinds.binds
|
||||||
|
++ lib.optionals (lib.hasInfix "," config.nomarchy.keyboard.layout)
|
||||||
|
keybinds.multiLayoutBinds)
|
||||||
|
++ workspaceBinds
|
||||||
|
++ lofBinds;
|
||||||
|
|
||||||
# Media keys via swayosd-client: it performs the action AND shows
|
# Media keys via swayosd-client: it performs the action AND shows
|
||||||
# the on-screen display (the nomarchy.osd module). e = repeat,
|
# the on-screen display (the nomarchy.osd module). e = repeat,
|
||||||
@@ -266,11 +550,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
|
# nwg-displays: interactive monitor arranger (applies live via hyprctl and
|
||||||
# writes a config file). A helper to find values for nomarchy.monitors —
|
# writes a config file). A helper to find values for nomarchy.monitors —
|
||||||
# the declarative config stays the source of truth (we don't source its
|
# the declarative config stays the source of truth (we don't source its
|
||||||
# output). Gated on its toggle; pointless without the Hyprland session.
|
# output). Gated on its toggle; pointless without the Hyprland session.
|
||||||
home.packages = lib.optionals
|
home.packages =
|
||||||
(config.nomarchy.hyprland.enable && config.nomarchy.displays.enable)
|
lib.optionals (config.nomarchy.hyprland.enable && config.nomarchy.displays.enable)
|
||||||
[ pkgs.nwg-displays ];
|
[ pkgs.nwg-displays ]
|
||||||
|
# The new-keyboard watcher on PATH (when enabled) so it's discoverable and
|
||||||
|
# runnable by name for debugging — Hyprland still exec-once's it by store path.
|
||||||
|
++ lib.optional (config.nomarchy.hyprland.enable && kbAutoSwitch) keyboardWatch
|
||||||
|
# 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,7 @@
|
|||||||
{
|
{
|
||||||
binds = [
|
binds = [
|
||||||
{ mods = "$mod"; key = "Return"; action = "exec, $terminal"; desc = "Open terminal"; }
|
{ 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 = "Space"; action = "exec, rofi -show drun -theme launcher"; desc = "App launcher"; }
|
||||||
{ mods = "$mod"; key = "D"; action = "exec, rofi -show drun"; desc = "App launcher"; }
|
|
||||||
{ mods = "$mod"; key = "M"; action = "exec, nomarchy-menu"; desc = "Main menu"; }
|
{ mods = "$mod"; key = "M"; action = "exec, nomarchy-menu"; desc = "Main menu"; }
|
||||||
{ mods = "$mod"; key = "E"; action = "exec, $terminal -e yazi"; desc = "File manager (yazi)"; }
|
{ mods = "$mod"; key = "E"; action = "exec, $terminal -e yazi"; desc = "File manager (yazi)"; }
|
||||||
{ mods = "$mod"; key = "Q"; action = "killactive"; desc = "Close window"; }
|
{ mods = "$mod"; key = "Q"; action = "killactive"; desc = "Close window"; }
|
||||||
@@ -33,8 +32,13 @@
|
|||||||
{ mods = "$mod"; key = "X"; action = "exec, nomarchy-menu power"; desc = "Power menu"; }
|
{ mods = "$mod"; key = "X"; action = "exec, nomarchy-menu power"; desc = "Power menu"; }
|
||||||
|
|
||||||
{ mods = "$mod"; key = "N"; action = "exec, swaync-client -t"; desc = "Notification centre"; }
|
{ mods = "$mod"; key = "N"; action = "exec, swaync-client -t"; desc = "Notification centre"; }
|
||||||
# SUPER+? (the "question" keysym already implies Shift on most layouts).
|
# SUPER+? — ? is Shift+/. SHIFT stays in the modmask (you hold it), but
|
||||||
{ mods = "$mod"; key = "question"; action = "exec, nomarchy-menu keybinds"; desc = "Keybindings cheatsheet"; }
|
# 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
|
# Menu functions — SUPER+CTRL+<mnemonic> jumps straight to a
|
||||||
# nomarchy-menu module (all also reachable from the SUPER+M picker).
|
# nomarchy-menu module (all also reachable from the SUPER+M picker).
|
||||||
@@ -43,20 +47,43 @@
|
|||||||
{ mods = "$mod CTRL"; key = "W"; action = "exec, nomarchy-menu web"; desc = "Web search"; }
|
{ 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 = "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 = "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 = "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 = "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 = "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 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
|
# Focus — SUPER + arrow keys.
|
||||||
{ mods = "$mod"; key = "H"; action = "movefocus, l"; desc = "Focus left"; }
|
{ mods = "$mod"; key = "left"; action = "movefocus, l"; desc = "Focus left"; }
|
||||||
{ mods = "$mod"; key = "L"; action = "movefocus, r"; desc = "Focus right"; }
|
{ mods = "$mod"; key = "right"; action = "movefocus, r"; desc = "Focus right"; }
|
||||||
{ mods = "$mod"; key = "K"; action = "movefocus, u"; desc = "Focus up"; }
|
{ mods = "$mod"; key = "up"; action = "movefocus, u"; desc = "Focus up"; }
|
||||||
{ mods = "$mod"; key = "J"; action = "movefocus, d"; desc = "Focus down"; }
|
{ 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 = ""; key = "Print"; action = "exec, grim -g \"$(slurp)\" - | wl-copy"; desc = "Screenshot region → clipboard"; }
|
{ 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 && notify-send Screenshot \"Region copied to clipboard.\""; 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 = [
|
extra = [
|
||||||
@@ -64,5 +91,6 @@
|
|||||||
{ keys = "SUPER + SHIFT + 1-9"; desc = "Move window to workspace 1-9"; }
|
{ keys = "SUPER + SHIFT + 1-9"; desc = "Move window to workspace 1-9"; }
|
||||||
{ keys = "SUPER + drag"; desc = "Move (LMB) / resize (RMB) window"; }
|
{ keys = "SUPER + drag"; desc = "Move (LMB) / resize (RMB) window"; }
|
||||||
{ keys = "Volume / Brightness"; desc = "Hardware keys, shown via the OSD"; }
|
{ 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
|
# GNOME session to lean on) and themed by Stylix's Qt config, so the
|
||||||
# passphrase dialog tracks the palette.
|
# passphrase dialog tracks the palette.
|
||||||
#
|
#
|
||||||
# SSH_AUTH_SOCK is exported by the agent's shell integration (zsh, on via
|
# SSH_AUTH_SOCK reaches both terminal and GUI clients: the agent's zsh
|
||||||
# home.shell.enableZshIntegration in shell.nix). Terminal git/ssh is the
|
# integration sets it in interactive shells (home.shell.enableZshIntegration
|
||||||
# supported path; a GUI client launched outside a shell won't inherit the
|
# in shell.nix), and a session-level home.sessionVariables export (below)
|
||||||
# socket — revisit with a session-level export if that's ever wanted.
|
# covers GUI clients launched outside a shell — e.g. from the rofi launcher —
|
||||||
|
# which never inherit the interactive shell's copy. Both resolve the same
|
||||||
|
# socket via gpgconf, so they can't drift.
|
||||||
#
|
#
|
||||||
# gnome-keyring (system side) stays the Secret Service for application
|
# gnome-keyring (system side) stays the Secret Service for application
|
||||||
# secrets; modern gnome-keyring no longer runs an SSH agent, so there is no
|
# secrets; modern gnome-keyring no longer runs an SSH agent, so there is no
|
||||||
@@ -38,5 +40,16 @@ in
|
|||||||
defaultCacheTtlSsh = 1800;
|
defaultCacheTtlSsh = 1800;
|
||||||
maxCacheTtlSsh = 7200;
|
maxCacheTtlSsh = 7200;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Session-level SSH_AUTH_SOCK so GUI clients launched outside a shell
|
||||||
|
# (rofi launcher, autostarted apps) reach the agent — the shell
|
||||||
|
# integration only covers interactive shells. Resolved with gpgconf at
|
||||||
|
# session-init time (the same lookup the shell integration uses), so it
|
||||||
|
# tracks the agent's real socket rather than a hardcoded path; valid
|
||||||
|
# before first use since the socket is systemd-activated. Reaches GUI
|
||||||
|
# apps the way NIXOS_OZONE_WL does — sourced into the login shell that
|
||||||
|
# starts Hyprland, so every spawned client inherits it.
|
||||||
|
home.sessionVariables.SSH_AUTH_SOCK =
|
||||||
|
"$(${pkgs.gnupg}/bin/gpgconf --list-dirs agent-ssh-socket)";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
61
modules/home/mime.nix
Normal file
61
modules/home/mime.nix
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# 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";
|
||||||
|
|
||||||
|
# Template ships chromium (chromium-browser.desktop). Entry is
|
||||||
|
# skipped by GIO if the package is deleted; install another browser
|
||||||
|
# and/or override these keys to retarget.
|
||||||
|
"text/html" = "chromium-browser.desktop";
|
||||||
|
"x-scheme-handler/http" = "chromium-browser.desktop";
|
||||||
|
"x-scheme-handler/https" = "chromium-browser.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,84 @@
|
|||||||
# Night light — a scheduled blue-light filter via hyprsunset (Hyprland's own
|
# Night light — a scheduled blue-light filter via hyprsunset (Hyprland's own
|
||||||
# gamma/temperature tool). Warm at night, identity (no shift) by day;
|
# gamma/temperature tool). Warm at night, identity (no shift) by day; the
|
||||||
# hyprsunset's time-based `profile` entries handle the schedule and pick the
|
# schedule (temperature/sunrise/sunset) is tuned via nomarchy.nightlight.* in
|
||||||
# right state on session start. Opt-in via nomarchy.nightlight.enable.
|
# home.nix and baked into the unit's time-based `profile`.
|
||||||
#
|
#
|
||||||
# The hyprsunset HM service module is provided by home-manager; this only
|
# Geo mode: when BOTH nomarchy.nightlight.latitude and .longitude are set,
|
||||||
# configures it. Override anything with plain services.hyprsunset.* options.
|
# wlsunset replaces hyprsunset — it computes sunrise/sunset from the
|
||||||
{ config, lib, ... }:
|
# location itself (the fixed .sunrise/.sunset times are ignored). Same
|
||||||
|
# toggle script, same Waybar moon, same live-state ExecCondition gate;
|
||||||
|
# only the unit underneath changes.
|
||||||
|
#
|
||||||
|
# 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
|
let
|
||||||
cfg = config.nomarchy.nightlight;
|
cfg = config.nomarchy.nightlight;
|
||||||
|
s = config.nomarchy.settings.nightlight;
|
||||||
|
sync = lib.getExe config.nomarchy.package;
|
||||||
|
# Geo mode flips the backing unit; everything user-facing stays the same.
|
||||||
|
geo = cfg.latitude != null && cfg.longitude != null;
|
||||||
|
unit = if geo then "wlsunset.service" else "hyprsunset.service";
|
||||||
|
# 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=${unit}
|
||||||
|
# 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 — ${if geo then "follows your location" else "warm on schedule"} (click to disable)","class":"on"}\n'
|
||||||
|
exit 0 ;;
|
||||||
|
on)
|
||||||
|
if installed; then write_on true; start; else install_feature; start; fi ;;
|
||||||
|
off) write_on false; stop ;;
|
||||||
|
toggle)
|
||||||
|
if systemctl --user is-active --quiet "$unit"; then
|
||||||
|
write_on false; stop # on -> off (instant)
|
||||||
|
elif installed; then
|
||||||
|
write_on true; start # installed, off -> on (instant)
|
||||||
|
else
|
||||||
|
install_feature; start # first enable (rebuilds)
|
||||||
|
fi ;;
|
||||||
|
*) echo "usage: nomarchy-nightlight [toggle|status|on|off|should-start]" >&2; exit 64 ;;
|
||||||
|
esac
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.enable {
|
config = {
|
||||||
services.hyprsunset = {
|
# Unit presence tracks the sticky `installed` flag the menu writes (first
|
||||||
|
# enable rebuilds). mkDefault so a hand-set nomarchy.nightlight.enable in
|
||||||
|
# home.nix also works as a declarative opt-in.
|
||||||
|
nomarchy.nightlight.enable = lib.mkDefault s.installed;
|
||||||
|
|
||||||
|
services.hyprsunset = lib.mkIf (cfg.enable && !geo) {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings.profile = [
|
settings.profile = [
|
||||||
# Daytime: identity = no colour change.
|
# Daytime: identity = no colour change.
|
||||||
@@ -21,5 +87,22 @@ in
|
|||||||
{ time = cfg.sunset; temperature = cfg.temperature; }
|
{ time = cfg.sunset; temperature = cfg.temperature; }
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Geo mode: wlsunset owns the schedule — it recomputes sunrise/sunset
|
||||||
|
# from the coordinates daily (no fixed profile to bake).
|
||||||
|
services.wlsunset = lib.mkIf (cfg.enable && geo) {
|
||||||
|
enable = true;
|
||||||
|
latitude = cfg.latitude;
|
||||||
|
longitude = cfg.longitude;
|
||||||
|
temperature.night = 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.${lib.removeSuffix ".service" unit}.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
|
# User-level `nomarchy.*` options — the full surface downstream users
|
||||||
# configure in their home.nix. Kept small on purpose.
|
# configure in their home.nix. Kept small on purpose.
|
||||||
{ lib, pkgs, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
# One output's layout — turned into a Hyprland `monitor` rule in
|
# 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
|
in
|
||||||
{
|
{
|
||||||
options.nomarchy = {
|
options.nomarchy = {
|
||||||
@@ -73,13 +100,13 @@ in
|
|||||||
# ── Preferences ────────────────────────────────────────────────
|
# ── Preferences ────────────────────────────────────────────────
|
||||||
terminal = lib.mkOption {
|
terminal = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "ghostty";
|
default = config.nomarchy.settings.terminal or "ghostty";
|
||||||
description = "Terminal emulator command, used by keybinds and $TERMINAL.";
|
description = "Terminal emulator command, used by keybinds and $TERMINAL.";
|
||||||
};
|
};
|
||||||
|
|
||||||
keyboard.layout = lib.mkOption {
|
keyboard.layout = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "us";
|
default = config.nomarchy.settings.keyboard.layout or "us";
|
||||||
example = "de";
|
example = "de";
|
||||||
description = ''
|
description = ''
|
||||||
XKB layout for the Hyprland session. The console (and the LUKS
|
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
|
picker. When non-empty, a small watcher runs in the session: when a
|
||||||
keyboard connects that isn't covered by nomarchy.keyboard.devices and
|
keyboard connects that isn't covered by nomarchy.keyboard.devices and
|
||||||
hasn't been chosen before, it pops a rofi picker (these layouts plus
|
hasn't been chosen before, it pops a rofi picker (these layouts plus
|
||||||
the primary nomarchy.keyboard.layout), applies the choice, and
|
the primary nomarchy.keyboard.layout), applies the choice to that
|
||||||
remembers it per-device (~/.local/state) — re-applying automatically
|
keyboard only (a per-device kb_layout, so the built-in board is left
|
||||||
on later reconnects. The runtime-remember complement to the
|
alone), and remembers it in the git-tracked in-flake state
|
||||||
declarative keyboard.devices; a stateful runtime piece by design.
|
(settings.keyboard.devices, not ~/.local/state) — re-applied
|
||||||
|
automatically on later reconnects and across reboots. Each remembered
|
||||||
|
choice graduates into nomarchy.keyboard.devices on the next rebuild
|
||||||
|
(a generated device block). The runtime-remember complement to the
|
||||||
|
declarative keyboard.devices.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,7 +166,9 @@ in
|
|||||||
nomarchy.keyboard.layout for that keyboard only — e.g. an external
|
nomarchy.keyboard.layout for that keyboard only — e.g. an external
|
||||||
keyboard that's physically a different layout than the laptop's
|
keyboard that's physically a different layout than the laptop's
|
||||||
built-in one. Hyprland applies it automatically whenever that
|
built-in one. Hyprland applies it automatically whenever that
|
||||||
keyboard is connected.
|
keyboard is connected. The interactive watcher
|
||||||
|
(nomarchy.keyboard.layouts) also writes its remembered picks here on
|
||||||
|
the next rebuild, so a runtime choice graduates into reproducible config.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -159,8 +192,9 @@ in
|
|||||||
|
|
||||||
nightlight = {
|
nightlight = {
|
||||||
enable = lib.mkEnableOption ''
|
enable = lib.mkEnableOption ''
|
||||||
a scheduled blue-light filter (hyprsunset): warm at night, no shift
|
a scheduled blue-light filter (hyprsunset; wlsunset in geo mode):
|
||||||
by day. Opt-in; tune the temperature + sunrise/sunset below'';
|
warm at night, no shift by day. Opt-in; tune the temperature +
|
||||||
|
sunrise/sunset (or latitude/longitude) below'';
|
||||||
|
|
||||||
temperature = lib.mkOption {
|
temperature = lib.mkOption {
|
||||||
type = lib.types.int;
|
type = lib.types.int;
|
||||||
@@ -173,14 +207,57 @@ in
|
|||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "07:00";
|
default = "07:00";
|
||||||
example = "06:30";
|
example = "06:30";
|
||||||
description = "Time (HH:MM) the filter turns OFF — daytime, no colour shift.";
|
description = "Time (HH:MM) the filter turns OFF — daytime, no colour shift. Ignored in geo mode.";
|
||||||
};
|
};
|
||||||
|
|
||||||
sunset = lib.mkOption {
|
sunset = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "20:00";
|
default = "20:00";
|
||||||
example = "21:00";
|
example = "21:00";
|
||||||
description = "Time (HH:MM) the filter turns ON — warm.";
|
description = "Time (HH:MM) the filter turns ON — warm. Ignored in geo mode.";
|
||||||
|
};
|
||||||
|
|
||||||
|
latitude = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "52.52";
|
||||||
|
description = ''
|
||||||
|
Geo mode: set BOTH latitude and longitude and sunrise/sunset are
|
||||||
|
computed from your location every day (wlsunset replaces
|
||||||
|
hyprsunset; the fixed .sunrise/.sunset times are ignored).'';
|
||||||
|
};
|
||||||
|
|
||||||
|
longitude = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "13.40";
|
||||||
|
description = "Geo mode longitude — set together with latitude.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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 nomarchy-pull /
|
||||||
|
nomarchy-rebuild / nomarchy-home / 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.
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -205,11 +282,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 ──────────────────────────────────────────
|
# ── Component toggles ──────────────────────────────────────────
|
||||||
hyprland.enable = lib.mkEnableOption "Nomarchy's Hyprland configuration" // { default = true; };
|
hyprland.enable = lib.mkEnableOption "Nomarchy's Hyprland configuration" // { default = true; };
|
||||||
waybar.enable = lib.mkEnableOption "Nomarchy's Waybar 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; };
|
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; };
|
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; };
|
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; };
|
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; };
|
osd.enable = lib.mkEnableOption "swayosd on-screen display for volume/brightness/mute" // { default = true; };
|
||||||
@@ -220,6 +374,8 @@ in
|
|||||||
btop.enable = lib.mkEnableOption "btop with the per-theme nomarchy theme" // { default = true; };
|
btop.enable = lib.mkEnableOption "btop with the per-theme nomarchy theme" // { default = true; };
|
||||||
stylix.enable = lib.mkEnableOption "Stylix theming for the long tail of apps (GTK, Qt, cursors)" // { default = true; };
|
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; };
|
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) ───────────────────────────────────────
|
# ── Computed (read-only) ───────────────────────────────────────
|
||||||
theme = lib.mkOption {
|
theme = lib.mkOption {
|
||||||
@@ -228,6 +384,17 @@ in
|
|||||||
description = "The parsed theme state (stateFile merged over defaults).";
|
description = "The parsed theme state (stateFile merged over defaults).";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
settings = lib.mkOption {
|
||||||
|
type = lib.types.attrs;
|
||||||
|
readOnly = true;
|
||||||
|
description = ''
|
||||||
|
Parsed feature settings — the `settings` section of the state file,
|
||||||
|
what the menu/Waybar toggles write (e.g. settings.nightlight.enable).
|
||||||
|
Feature options mkDefault-read from here, so a menu toggle lands in the
|
||||||
|
in-flake state (git-tracked, reproducible) rather than ~/.local/state.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
lib = lib.mkOption {
|
lib = lib.mkOption {
|
||||||
type = lib.types.attrs;
|
type = lib.types.attrs;
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
|
|||||||
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"
|
||||||
|
]
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -69,11 +69,11 @@ in
|
|||||||
gpl = "git pull";
|
gpl = "git pull";
|
||||||
gf = "git fetch --all --prune";
|
gf = "git fetch --all --prune";
|
||||||
|
|
||||||
# Nix — the flake/store verbs (system/home rebuilds already have the
|
# Nix — store/flake verbs. Lifecycle is nomarchy-pull /
|
||||||
# sys-update / home-update commands, so they're not re-aliased here).
|
# nomarchy-rebuild / nomarchy-home (full names on PATH, not aliases).
|
||||||
ns = "nix shell"; # ns nixpkgs#ripgrep
|
ns = "nix shell"; # ns nixpkgs#ripgrep
|
||||||
nr = "nix run"; # nr nixpkgs#cowsay
|
nr = "nix run"; # nr nixpkgs#cowsay
|
||||||
nfu = "nix flake update";
|
nfu = "nix flake update"; # prefer nomarchy-pull day-to-day
|
||||||
nfc = "nix flake check";
|
nfc = "nix flake check";
|
||||||
nsearch = "nix search nixpkgs";
|
nsearch = "nix search nixpkgs";
|
||||||
ngc = "nix-collect-garbage -d"; # user generations; sudo for system roots
|
ngc = "nix-collect-garbage -d"; # user generations; sudo for system roots
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ in
|
|||||||
targets = {
|
targets = {
|
||||||
gtk.enable = true;
|
gtk.enable = true;
|
||||||
qt.enable = true;
|
qt.enable = true;
|
||||||
|
# No-op unless programs.zathura is on (viewers.nix enables it).
|
||||||
|
zathura.enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
cursor = {
|
cursor = {
|
||||||
@@ -58,10 +60,12 @@ in
|
|||||||
# GTK/file-manager/rofi icon theme, resolved from the JSON in
|
# GTK/file-manager/rofi icon theme, resolved from the JSON in
|
||||||
# theme.nix (per-theme `icons`, else Papirus-Dark/Light by mode).
|
# theme.nix (per-theme `icons`, else Papirus-Dark/Light by mode).
|
||||||
# Stylix sets gtk.iconTheme from this; both dark/light point at the
|
# Stylix sets gtk.iconTheme from this; both dark/light point at the
|
||||||
# already-mode-resolved name.
|
# already-mode-resolved name. The package is papirus by default and
|
||||||
|
# only unions in another pack when a theme's `icons` names one
|
||||||
|
# (theme.nix — opt-in, no default closure bloat).
|
||||||
icons = {
|
icons = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = lib.mkDefault pkgs.papirus-icon-theme;
|
package = lib.mkDefault t.iconThemePackage;
|
||||||
dark = t.iconTheme;
|
dark = t.iconTheme;
|
||||||
light = t.iconTheme;
|
light = t.iconTheme;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,11 +29,14 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
style = ''
|
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 base ${c.base};
|
||||||
@define-color surface ${c.surface};
|
|
||||||
@define-color text ${c.text};
|
@define-color text ${c.text};
|
||||||
@define-color subtext ${c.subtext};
|
|
||||||
@define-color accent ${c.accent};
|
@define-color accent ${c.accent};
|
||||||
@define-color bad ${c.bad};
|
@define-color bad ${c.bad};
|
||||||
|
|
||||||
@@ -42,6 +45,8 @@ in
|
|||||||
border: ${toString t.ui.borderSize}px solid alpha(@accent, 0.4);
|
border: ${toString t.ui.borderSize}px solid alpha(@accent, 0.4);
|
||||||
border-radius: ${r}px;
|
border-radius: ${r}px;
|
||||||
color: @text;
|
color: @text;
|
||||||
|
font-family: "${t.fonts.ui}", "${t.fonts.mono}";
|
||||||
|
font-size: ${toString t.fonts.size}pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-content .summary {
|
.notification-content .summary {
|
||||||
@@ -50,7 +55,7 @@ in
|
|||||||
}
|
}
|
||||||
|
|
||||||
.notification-content .body {
|
.notification-content .body {
|
||||||
color: @subtext;
|
color: @text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification.critical {
|
.notification.critical {
|
||||||
@@ -62,11 +67,13 @@ in
|
|||||||
border: ${toString t.ui.borderSize}px solid alpha(@accent, 0.4);
|
border: ${toString t.ui.borderSize}px solid alpha(@accent, 0.4);
|
||||||
border-radius: ${r}px;
|
border-radius: ${r}px;
|
||||||
color: @text;
|
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:focus,
|
||||||
.control-center .notification-row:hover {
|
.control-center .notification-row:hover {
|
||||||
background: alpha(@surface, 0.6);
|
background: alpha(@text, 0.1);
|
||||||
border-radius: ${r}px;
|
border-radius: ${r}px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +83,7 @@ in
|
|||||||
}
|
}
|
||||||
|
|
||||||
.widget-title > button {
|
.widget-title > button {
|
||||||
background: @surface;
|
background: alpha(@text, 0.1);
|
||||||
color: @text;
|
color: @text;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: ${r}px;
|
border-radius: ${r}px;
|
||||||
|
|||||||
@@ -18,14 +18,18 @@
|
|||||||
let
|
let
|
||||||
cfg = config.nomarchy;
|
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
|
# Defaults guarantee evaluation succeeds on a sparse or older state
|
||||||
# file (e.g. one written before a schema field was added). The shipped
|
# 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
|
# relative to this module, so it resolves inside Nomarchy's own flake
|
||||||
# source even when consumed downstream.
|
# source even when consumed downstream.
|
||||||
preset = builtins.fromJSON (builtins.readFile ../../themes/tokyo-night.json);
|
preset = builtins.fromJSON (builtins.readFile ../../themes/boreal.json);
|
||||||
|
|
||||||
defaults = preset // {
|
defaults = preset // {
|
||||||
fonts = {
|
fonts = {
|
||||||
@@ -54,16 +58,103 @@ let
|
|||||||
# tone for kanagawa/summer-*). Each preset declares its own so a theme
|
# tone for kanagawa/summer-*). Each preset declares its own so a theme
|
||||||
# switch always replaces it (deep_merge would otherwise leave it stuck).
|
# switch always replaces it (deep_merge would otherwise leave it stuck).
|
||||||
border = { active = "accent"; inactive = "overlay"; };
|
border = { active = "accent"; inactive = "overlay"; };
|
||||||
|
|
||||||
|
# Non-appearance feature settings the menu/watchers write into this same
|
||||||
|
# in-flake state. nomarchy.nightlight: `installed` (sticky — gates the unit,
|
||||||
|
# so the first enable rebuilds) and `on` (instant runtime on/off).
|
||||||
|
# settings.keyboard.devices: per-device layouts the new-keyboard watcher
|
||||||
|
# remembers (device-name -> XKB layout), git-tracked instead of
|
||||||
|
# ~/.local/state; they graduate into nomarchy.keyboard.devices on the next
|
||||||
|
# rebuild. settings.monitors: per-output resolutions the Display menu
|
||||||
|
# remembers (output-name -> "WxH@R"), overlaid onto nomarchy.monitors by
|
||||||
|
# name in hyprland.nix — the monitor twin of the keyboard graduation.
|
||||||
|
# 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;
|
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
|
# 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.
|
# already a literal hex. Unknown roles are rejected by the field checks
|
||||||
resolveColor = v: if lib.hasPrefix "#" v then v else parsed.colors.${v} or v;
|
# 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 = {
|
border = {
|
||||||
active = resolveColor parsed.border.active;
|
active = resolveColor checked.border.active;
|
||||||
inactive = resolveColor parsed.border.inactive;
|
inactive = resolveColor checked.border.inactive;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Resolve the icon theme once and expose it on nomarchy.theme so both
|
# Resolve the icon theme once and expose it on nomarchy.theme so both
|
||||||
@@ -73,10 +164,48 @@ let
|
|||||||
if parsed.icons != "" then parsed.icons
|
if parsed.icons != "" then parsed.icons
|
||||||
else if parsed.mode == "light" then "Papirus-Light"
|
else if parsed.mode == "light" then "Papirus-Light"
|
||||||
else "Papirus-Dark";
|
else "Papirus-Dark";
|
||||||
|
|
||||||
|
# Icon-pack resolution — opt-in, no default bloat. Papirus is the shipped
|
||||||
|
# default and the ONLY icon pack in the closure unless a theme's `icons`
|
||||||
|
# names a set from another pack; then that pack (and only it) is
|
||||||
|
# union-joined alongside papirus. Because the default themes name
|
||||||
|
# `Papirus-*`, the resolved package below is byte-identical to
|
||||||
|
# papirus-icon-theme (no symlinkJoin, no added MB). To offer another set:
|
||||||
|
# add one row to `iconPacks` and set `icons = "<Name>"` in a theme JSON
|
||||||
|
# (see templates/downstream/home.nix). Referencing `pkgs.*` here is
|
||||||
|
# eval-only — a pack enters the closure solely when it is the matched one.
|
||||||
|
papirusPkg = pkgs.papirus-icon-theme;
|
||||||
|
iconPacks = [
|
||||||
|
# pkg + the theme-name prefixes it provides (packs ship many named
|
||||||
|
# variants under one prefix, e.g. Tela / Tela-dark / Tela-blue).
|
||||||
|
{ pkg = papirusPkg; prefixes = [ "Papirus" "breeze" ]; }
|
||||||
|
{ pkg = pkgs.tela-icon-theme; prefixes = [ "Tela" ]; }
|
||||||
|
{ pkg = pkgs.qogir-icon-theme; prefixes = [ "Qogir" ]; }
|
||||||
|
{ pkg = pkgs.reversal-icon-theme; prefixes = [ "Reversal" ]; }
|
||||||
|
{ pkg = pkgs.numix-icon-theme-circle; prefixes = [ "Numix" ]; }
|
||||||
|
];
|
||||||
|
# The pack whose prefix matches the resolved name (null = unknown name →
|
||||||
|
# GTK falls back gracefully; papirus is still present).
|
||||||
|
matchedPack =
|
||||||
|
let m = lib.findFirst
|
||||||
|
(p: lib.any (pre: lib.hasPrefix pre iconTheme) p.prefixes) null iconPacks;
|
||||||
|
in if m == null then null else m.pkg;
|
||||||
|
iconThemePackage =
|
||||||
|
if matchedPack == null || matchedPack == papirusPkg
|
||||||
|
then papirusPkg # single pack → identical store path, zero closure delta
|
||||||
|
else pkgs.symlinkJoin {
|
||||||
|
name = "nomarchy-icon-themes";
|
||||||
|
paths = [ papirusPkg matchedPack ];
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = {
|
config = {
|
||||||
nomarchy.theme = parsed // { inherit iconTheme border; };
|
nomarchy.theme = checked // { inherit iconTheme border iconThemePackage; };
|
||||||
|
|
||||||
|
# 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 = {
|
nomarchy.lib = {
|
||||||
# "#7aa2f7" -> "rgb(7aa2f7)" (Hyprland color syntax)
|
# "#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" ];
|
||||||
|
};
|
||||||
|
}
|
||||||
145
modules/home/updates.nix
Normal file
145
modules/home/updates.nix
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# 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 nomarchy-pull /
|
||||||
|
# nomarchy-rebuild / nomarchy-home / 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: nomarchy-pull && nomarchy-rebuild && nomarchy-home"
|
||||||
|
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) — nomarchy-pull && nomarchy-rebuild && nomarchy-home"
|
||||||
|
[ "$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 nomarchy-pull >/dev/null 2>&1; then
|
||||||
|
read -rp "Run nomarchy-pull && nomarchy-rebuild && nomarchy-home? [y/N] " a
|
||||||
|
if [ "$a" = y ]; then
|
||||||
|
nomarchy-pull
|
||||||
|
nomarchy-rebuild
|
||||||
|
command -v nomarchy-home >/dev/null 2>&1 && nomarchy-home
|
||||||
|
fi
|
||||||
|
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
|
# Named writeShellScriptBins (put on PATH via home.packages below) rather
|
||||||
# than bare writeShellScript store paths, so the whole-swap themes' static
|
# 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.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" ''
|
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
|
command -v powerprofilesctl >/dev/null 2>&1 || exit 0
|
||||||
prof=$(powerprofilesctl get 2>/dev/null) || exit 0
|
prof=$(powerprofilesctl get 2>/dev/null) || exit 0
|
||||||
case "$prof" in
|
case "$prof" in
|
||||||
@@ -51,6 +89,54 @@ let
|
|||||||
[ -n "$next" ] && powerprofilesctl set "$next"
|
[ -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 is installed with the bar (home.packages below); the gate is
|
||||||
|
# only for someone who force-removed it. 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.
|
# Per-theme override probe.
|
||||||
assetDir = config.nomarchy.themesDir + "/${t.slug}";
|
assetDir = config.nomarchy.themesDir + "/${t.slug}";
|
||||||
styleOverride = assetDir + "/waybar.css";
|
styleOverride = assetDir + "/waybar.css";
|
||||||
@@ -63,7 +149,12 @@ let
|
|||||||
(lib.mapAttrsToList (name: value: "@define-color ${name} ${value};") t.colors);
|
(lib.mapAttrsToList (name: value: "@define-color ${name} ${value};") t.colors);
|
||||||
|
|
||||||
generatedSettings = {
|
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";
|
position = "top";
|
||||||
height = 34;
|
height = 34;
|
||||||
margin-top = t.ui.gapsOut;
|
margin-top = t.ui.gapsOut;
|
||||||
@@ -75,11 +166,28 @@ let
|
|||||||
# home-manager switch restyles the running bar without a restart.
|
# home-manager switch restyles the running bar without a restart.
|
||||||
reload_style_on_change = true;
|
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-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"
|
++ 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" = {
|
"hyprland/workspaces" = {
|
||||||
format = "{icon}";
|
format = "{icon}";
|
||||||
@@ -92,56 +200,147 @@ let
|
|||||||
};
|
};
|
||||||
|
|
||||||
clock = {
|
clock = {
|
||||||
format = "{:%H:%M}";
|
# Single module: time + short date (whole-swaps used to split these
|
||||||
format-alt = "{:%A %d %B %Y}";
|
# into clock + clock#date — one click surface for the calendar).
|
||||||
tooltip-format = "<tt><small>{calendar}</small></tt>";
|
format = "{:%H:%M · %a %d %b}";
|
||||||
|
# Left-click → the calendar (nomarchy-calendar → calcurse in a floating
|
||||||
|
# ghostty). Tooltip is plain date/zone only — embedding {calendar}
|
||||||
|
# produced an empty popup on hardware (2026-07-10); month view is
|
||||||
|
# one click away.
|
||||||
|
on-click = "nomarchy-calendar";
|
||||||
|
tooltip = true;
|
||||||
|
# One chrono block only: waybar/fmt empties the tooltip when two
|
||||||
|
# bare `{:%…}` specs are concatenated (bar format works because it
|
||||||
|
# is a single block). Newline + zone live under auto-timezone
|
||||||
|
# (tz-watch SIGUSR2 reload).
|
||||||
|
tooltip-format = "{:%A, %d %B %Y\n%Z (UTC%z)}";
|
||||||
};
|
};
|
||||||
|
|
||||||
# Active keyboard layout (per focused device) — only placed in
|
# Active keyboard layout (per focused device) — only placed in
|
||||||
# modules-right when showLanguage (see above).
|
# modules-right when showLanguage (see above).
|
||||||
"hyprland/language" = {
|
"hyprland/language" = {
|
||||||
format = " {short}";
|
# Two spaces after the glyph: the keyboard icon's right bearing
|
||||||
|
# otherwise kisses the layout code (esp. mono Nerd faces).
|
||||||
|
format = "<span size='${toString (t.fonts.size + 2)}pt'> </span>{short}";
|
||||||
tooltip = false;
|
tooltip = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
pulseaudio = {
|
pulseaudio = {
|
||||||
format = "{icon} {volume}%";
|
# Two trailing spaces on every icon level: high-volume glyphs are the
|
||||||
|
# widest and still kissed the % with a single pad (hardware report).
|
||||||
|
format = "<span size='${toString (t.fonts.size + 2)}pt'>{icon}</span>{volume}%";
|
||||||
format-muted = "";
|
format-muted = "";
|
||||||
format-icons.default = [ "" "" "" ];
|
format-icons.default = [ " " " " " " ];
|
||||||
on-click = "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
|
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 = {
|
# Idle inhibitor (caffeine): click → to hold the screen awake —
|
||||||
format-wifi = " {essid}";
|
# blocks hypridle's lock / display-off / suspend during video or a
|
||||||
format-ethernet = "";
|
# presentation. Waybar holds a Wayland idle-inhibit while activated;
|
||||||
format-disconnected = "";
|
# the state resets with the bar (deliberate — caffeine shouldn't
|
||||||
tooltip-format = "{ipaddr} via {gwaddr}";
|
# survive a relogin). Summer-night's whole-swap bar had this first
|
||||||
# nm-applet sits in the tray for the GUI path; this is the
|
# (reverse parity gap, 2026-07-04).
|
||||||
# keyboard-friendly one.
|
idle_inhibitor = {
|
||||||
on-click = "${config.nomarchy.terminal} -e nmtui";
|
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}%";
|
# No network module: nm-applet lives in the tray (the GUI path), so the
|
||||||
memory.format = " {percentage}%";
|
# bar's wifi/ethernet indicator would just duplicate it.
|
||||||
|
|
||||||
battery = {
|
battery = {
|
||||||
states = { warning = 25; critical = 10; };
|
states = { warning = 25; critical = 10; };
|
||||||
format = "{icon} {capacity}%";
|
# Same double-pad as pulseaudio (single space still tight on some glyphs).
|
||||||
format-charging = " {capacity}%";
|
format = "<span size='${toString (t.fonts.size + 2)}pt'>{icon}</span>{capacity}%";
|
||||||
format-icons = [ "" "" "" "" "" ];
|
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" = {
|
"custom/powerprofile" = {
|
||||||
exec = "nomarchy-powerprofile-status";
|
exec = "nomarchy-powerprofile-status";
|
||||||
return-type = "json";
|
return-type = "json";
|
||||||
interval = 5;
|
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
|
# swaync notification bell + Do-Not-Disturb state. `-swb` streams JSON
|
||||||
# (text/tooltip/class) on every change, so it tracks count and DND with
|
# (text/tooltip/class) on every change, so it tracks count and DND with
|
||||||
# no polling. Left-click toggles the panel; right-click toggles DND.
|
# no polling. Left-click toggles the panel; right-click toggles DND.
|
||||||
# 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" = {
|
"custom/notification" = {
|
||||||
exec = "swaync-client -swb";
|
exec = "swaync-client -swb";
|
||||||
return-type = "json";
|
return-type = "json";
|
||||||
@@ -152,8 +351,8 @@ let
|
|||||||
notification = "";
|
notification = "";
|
||||||
dnd-none = "";
|
dnd-none = "";
|
||||||
dnd-notification = "";
|
dnd-notification = "";
|
||||||
inhibited-none = "";
|
inhibited-none = "";
|
||||||
inhibited-notification = "";
|
inhibited-notification = "";
|
||||||
dnd-inhibited-none = "";
|
dnd-inhibited-none = "";
|
||||||
dnd-inhibited-notification = "";
|
dnd-inhibited-notification = "";
|
||||||
};
|
};
|
||||||
@@ -183,6 +382,11 @@ let
|
|||||||
border-radius: ${toString t.ui.rounding}px;
|
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 {
|
#workspaces button {
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
color: @muted;
|
color: @muted;
|
||||||
@@ -200,7 +404,7 @@ let
|
|||||||
}
|
}
|
||||||
|
|
||||||
#window {
|
#window {
|
||||||
color: @subtext;
|
color: alpha(@text, 0.85);
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,26 +413,90 @@ let
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tray, #pulseaudio, #network, #cpu, #memory, #custom-powerprofile, #language, #battery, #custom-notification {
|
#custom-nomarchy {
|
||||||
color: @subtext;
|
color: @accent;
|
||||||
|
font-family: Nomarchy;
|
||||||
|
font-size: ${toString (t.fonts.size + 4)}pt;
|
||||||
padding: 0 10px;
|
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.notification { color: @accent; }
|
||||||
#custom-notification.dnd-none,
|
#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; }
|
#pulseaudio.muted { color: @muted; }
|
||||||
#battery.warning { color: @warn; }
|
#battery.warning { color: @warn; }
|
||||||
#battery.critical { color: @bad; }
|
#battery.critical { color: @bad; }
|
||||||
#battery.charging { color: @good; }
|
#battery.charging { color: @good; }
|
||||||
|
|
||||||
|
/* Clock (and other) hover tooltips — explicit colors so an empty
|
||||||
|
looking box is never just “text same as background”. */
|
||||||
|
tooltip {
|
||||||
|
background: @surface;
|
||||||
|
border: ${toString t.ui.borderSize}px solid alpha(@accent, 0.4);
|
||||||
|
border-radius: ${toString t.ui.rounding}px;
|
||||||
|
}
|
||||||
|
tooltip label { color: @text; }
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
programs.waybar = lib.mkIf config.nomarchy.waybar.enable {
|
programs.waybar = lib.mkIf config.nomarchy.waybar.enable {
|
||||||
enable = true;
|
enable = true;
|
||||||
systemd.enable = true; # started/stopped with graphical-session.target
|
# Launched from Hyprland's exec-once (hyprland.nix), NOT a systemd user
|
||||||
|
# service. Bound to graphical-session.target the unit raced Hyprland's IPC
|
||||||
|
# on a warm relogin — it started before the socket was up, exited, landed
|
||||||
|
# in `failed`, and was never retried, so the bar vanished. exec-once only
|
||||||
|
# fires once Hyprland is up, dodging the race; theme switches reload the
|
||||||
|
# running bar via SIGUSR2 (nomarchy-theme-sync). No uwsm here to manage the
|
||||||
|
# session target, so we don't depend on its lifecycle.
|
||||||
|
systemd.enable = false;
|
||||||
|
|
||||||
# mkDefault so downstream can replace the whole bar config/style with
|
# mkDefault so downstream can replace the whole bar config/style with
|
||||||
# a plain home.nix assignment. For per-theme identity, prefer the
|
# a plain home.nix assignment. For per-theme identity, prefer the
|
||||||
@@ -248,8 +516,12 @@ in
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
# The power-profile helpers on PATH, so both the generated bar and the
|
# Bar helpers on PATH (whole-swap jsoncs exec them by bare name) +
|
||||||
# whole-swap themes' static waybar.jsonc can exec them by name.
|
# feature deps of bar clicks: calcurse (clock), pwvucontrol (volume
|
||||||
|
# right-click) — not opt-in template packages.
|
||||||
home.packages = lib.optionals config.nomarchy.waybar.enable
|
home.packages = lib.optionals config.nomarchy.waybar.enable
|
||||||
[ powerProfileStatus powerProfileCycle ];
|
[ waybarSupervisor powerProfileStatus powerProfileCycle vpnStatus doctorStatus calendarLauncher
|
||||||
|
pkgs.calcurse
|
||||||
|
pkgs.pwvucontrol
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,33 +22,58 @@ let
|
|||||||
'';
|
'';
|
||||||
in
|
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 = {
|
config = {
|
||||||
# The safe half of distro branding: distroName flows into
|
# Distro branding. distroName flows into /etc/os-release PRETTY_NAME,
|
||||||
# /etc/os-release PRETTY_NAME, systemd-boot entry titles and the
|
# systemd-boot entry titles and the ISO boot-menu label; distroId is the
|
||||||
# ISO boot-menu label. distroId stays "nixos" on purpose — it feeds
|
# machine-readable ID (DEFAULT_HOSTNAME, lsb DISTRIB_ID, CPE name).
|
||||||
# DEFAULT_HOSTNAME and upstream isNixos checks (see roadmap).
|
# distroId = "nomarchy" makes os-release honest — ID=nomarchy with
|
||||||
|
# ID_LIKE=nixos, the standard derivative-distro lineage marker (cf.
|
||||||
|
# Ubuntu→debian) — and is safe: switch-to-configuration builds its
|
||||||
|
# "is this NixOS?" guard from the *configured* distroId (and /etc/NIXOS
|
||||||
|
# still exists as the fallback), so rebuilds keep working; the nixos-*
|
||||||
|
# CLI tools are package names, untouched. The one side effect is that
|
||||||
|
# isNixos goes false and blanks the upstream nixos.org URLs, so we
|
||||||
|
# restore them pointing at the project instead.
|
||||||
system.nixos.distroName = lib.mkDefault "Nomarchy";
|
system.nixos.distroName = lib.mkDefault "Nomarchy";
|
||||||
|
system.nixos.distroId = lib.mkDefault "nomarchy";
|
||||||
|
system.nixos.extraOSReleaseArgs = lib.mkDefault {
|
||||||
|
HOME_URL = "https://git.bemagri.xyz/bernardo/Nomarchy";
|
||||||
|
DOCUMENTATION_URL = "https://git.bemagri.xyz/bernardo/Nomarchy";
|
||||||
|
SUPPORT_URL = "https://git.bemagri.xyz/bernardo/Nomarchy";
|
||||||
|
BUG_REPORT_URL = "https://git.bemagri.xyz/bernardo/Nomarchy/issues";
|
||||||
|
};
|
||||||
|
|
||||||
# MOTD on TTY/SSH login (the desktop auto-logs into Hyprland, so this
|
# MOTD on TTY/SSH login (the desktop auto-logs into Hyprland, so this
|
||||||
# is mostly seen over SSH or on a bare console). Branded, and doubles
|
# is mostly seen over SSH or on a bare console). Branded, and doubles
|
||||||
# as a cheat sheet for the distro's own helpers.
|
# as a cheat sheet for the distro's own helpers. Fingerprint line only
|
||||||
users.motd = lib.mkDefault ''
|
# 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.
|
${distroName} — a NixOS desktop, themed from one JSON.
|
||||||
|
|
||||||
sys-update update inputs + rebuild the system
|
nomarchy-pull update flake inputs (nomarchy, nixpkgs, …)
|
||||||
home-update apply home/theme changes (no sudo)
|
nomarchy-rebuild rebuild the system (current lock)
|
||||||
|
nomarchy-home rebuild the desktop / Home Manager
|
||||||
nomarchy-theme-sync apply <theme> switch the whole palette
|
nomarchy-theme-sync apply <theme> switch the whole palette
|
||||||
|
nomarchy-doctor read-only health check
|
||||||
SUPER+? keybindings cheatsheet
|
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
|
# Unfree allowed distro-wide: pragmatic-desktop territory (vendor
|
||||||
# GPU/wifi drivers, firmware, fonts, …). The custom nixpkgs-config
|
# GPU/wifi drivers, firmware, fonts, …). The custom nixpkgs-config
|
||||||
# type can't carry a nested mkDefault; disagree with
|
# type can't carry a nested mkDefault; disagree with
|
||||||
# `nixpkgs.config = lib.mkForce { allowUnfree = false; }`.
|
# `nixpkgs.config = lib.mkForce { allowUnfree = false; }`.
|
||||||
|
# `NIXPKGS_ALLOW_UNFREE` covers CLI `nix-shell` / `nix shell` /
|
||||||
|
# `nix run` (those ignore the system `nixpkgs.config`); pure flake
|
||||||
|
# eval still needs `--impure` when the env var is the only gate.
|
||||||
nixpkgs.config.allowUnfree = true;
|
nixpkgs.config.allowUnfree = true;
|
||||||
|
environment.variables.NIXPKGS_ALLOW_UNFREE = "1";
|
||||||
|
|
||||||
# ── One keyboard layout everywhere, incl. the LUKS prompt ────────
|
# ── One keyboard layout everywhere, incl. the LUKS prompt ────────
|
||||||
# services.xserver.xkb.layout is the single source of truth for the
|
# services.xserver.xkb.layout is the single source of truth for the
|
||||||
@@ -69,6 +94,15 @@ in
|
|||||||
console.earlySetup = lib.mkDefault true;
|
console.earlySetup = lib.mkDefault true;
|
||||||
boot.initrd.systemd.enable = lib.mkDefault true;
|
boot.initrd.systemd.enable = lib.mkDefault true;
|
||||||
|
|
||||||
|
# Nomarchy roots are BTRFS, not ZFS, so adopt the 26.11 default early and
|
||||||
|
# silence the eval warning the old `true` default emits. mkDefault, so a
|
||||||
|
# genuine ZFS-root downstream can still force it back on.
|
||||||
|
boot.zfs.forceImportRoot = lib.mkDefault false;
|
||||||
|
|
||||||
|
# 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 ────────────────────────────────────
|
# ── Wayland session: Hyprland ────────────────────────────────────
|
||||||
# Installs the binary, registers the session, wires up
|
# Installs the binary, registers the session, wires up
|
||||||
# xdg-desktop-portal-hyprland. Configuration is Home Manager's job.
|
# xdg-desktop-portal-hyprland. Configuration is Home Manager's job.
|
||||||
@@ -79,22 +113,8 @@ in
|
|||||||
extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; # file pickers, etc.
|
extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; # file pickers, etc.
|
||||||
};
|
};
|
||||||
|
|
||||||
services.greetd = lib.mkIf cfg.greeter.enable {
|
# The greetd/tuigreet login screen lives in ./greeter.nix — themed
|
||||||
enable = lib.mkDefault true;
|
# from the state JSON (console.colors + --theme) at system rebuild.
|
||||||
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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# ── Audio: Pipewire ──────────────────────────────────────────────
|
# ── Audio: Pipewire ──────────────────────────────────────────────
|
||||||
security.rtkit.enable = lib.mkDefault cfg.audio.enable;
|
security.rtkit.enable = lib.mkDefault cfg.audio.enable;
|
||||||
@@ -113,6 +133,10 @@ in
|
|||||||
services.dbus.enable = lib.mkDefault true;
|
services.dbus.enable = lib.mkDefault true;
|
||||||
services.upower.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
|
# Firmware updates via LVFS. Ships the daemon + its metadata-refresh
|
||||||
# timer only — it never flashes anything on its own; applying an update
|
# timer only — it never flashes anything on its own; applying an update
|
||||||
# is an explicit `fwupdmgr update`. On by default as desktop security
|
# is an explicit `fwupdmgr update`. On by default as desktop security
|
||||||
@@ -121,7 +145,33 @@ in
|
|||||||
# `services.fwupd.enable = false`.
|
# `services.fwupd.enable = false`.
|
||||||
services.fwupd.enable = lib.mkDefault true;
|
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;
|
networking.networkmanager.enable = lib.mkDefault true;
|
||||||
|
# OpenVPN support for the VPN menu's import/connect flow (nomarchy-vpn).
|
||||||
|
# WireGuard needs no plugin (NetworkManager imports wg .conf natively);
|
||||||
|
# this adds the openvpn type so `nmcli connection import type openvpn` works.
|
||||||
|
# mkDefault so a downstream can drop it to slim the closure.
|
||||||
|
networking.networkmanager.plugins = lib.mkDefault [ pkgs.networkmanager-openvpn ];
|
||||||
|
|
||||||
# No double-unlock on hibernate. Locking the session before sleep is
|
# No double-unlock on hibernate. Locking the session before sleep is
|
||||||
# right for suspend (resumes from RAM, no other gate), but an encrypted
|
# right for suspend (resumes from RAM, no other gate), but an encrypted
|
||||||
@@ -231,6 +281,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 ────────────────────────────────────────────────────────
|
# ── Fonts ────────────────────────────────────────────────────────
|
||||||
# The ten most popular Nerd Fonts ship by default, so any of them
|
# 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
|
# can be named in the theme state's fonts.mono and actually resolve
|
||||||
@@ -249,6 +306,10 @@ in
|
|||||||
# (every weight × variant) — too heavy for the ISO and closures.
|
# (every weight × variant) — too heavy for the ISO and closures.
|
||||||
nerd-fonts.mononoki
|
nerd-fonts.mononoki
|
||||||
nerd-fonts.inconsolata
|
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
|
inter
|
||||||
noto-fonts
|
noto-fonts
|
||||||
noto-fonts-color-emoji
|
noto-fonts-color-emoji
|
||||||
@@ -268,30 +329,15 @@ in
|
|||||||
# ── Essential packages ───────────────────────────────────────────
|
# ── Essential packages ───────────────────────────────────────────
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
nomarchy-theme-sync # provided by overlays.default
|
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
|
# Day-to-day lifecycle (README §3): nomarchy-pull / -rebuild / -home
|
||||||
# your user: `nix flake update` must NOT run as root (libgit2
|
# (+ legacy sys-update / sys-rebuild / home-update). Defined once in
|
||||||
# refuses the user-owned flake repo) — sudo happens inside, only
|
# pkgs/nomarchy-lifecycle; also installed via HM so a home switch can
|
||||||
# for the system switch.
|
# refresh a stale system-package pull script.
|
||||||
(pkgs.writeShellScriptBin "sys-update" ''
|
pkgs.nomarchy-lifecycle
|
||||||
set -e
|
|
||||||
if [ "$(id -u)" -eq 0 ]; then
|
|
||||||
echo "sys-update: run as your normal user (it sudos the rebuild itself)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
|
|
||||||
echo "sys-update: updating flake inputs in $flake"
|
|
||||||
nix flake update --flake "$flake"
|
|
||||||
if command -v nixos-rebuild-snap >/dev/null 2>&1; then
|
|
||||||
sudo nixos-rebuild-snap "$@" # BTRFS snapshot first
|
|
||||||
else
|
|
||||||
sudo nixos-rebuild switch --flake "$flake#default" "$@"
|
|
||||||
fi
|
|
||||||
'')
|
|
||||||
(pkgs.writeShellScriptBin "home-update" ''
|
|
||||||
set -e
|
|
||||||
exec home-manager switch --flake "''${NOMARCHY_PATH:-$HOME/.nomarchy}" "$@"
|
|
||||||
'')
|
|
||||||
|
|
||||||
git
|
git
|
||||||
vim
|
vim
|
||||||
@@ -304,6 +350,7 @@ in
|
|||||||
wl-clipboard
|
wl-clipboard
|
||||||
grim
|
grim
|
||||||
slurp
|
slurp
|
||||||
|
hyprpicker
|
||||||
] ++ lib.optional (cfg.snapper.enable && (config.fileSystems."/".fsType or "") == "btrfs")
|
] ++ lib.optional (cfg.snapper.enable && (config.fileSystems."/".fsType or "") == "btrfs")
|
||||||
# Snapshot, then rebuild — rollback material for system changes
|
# Snapshot, then rebuild — rollback material for system changes
|
||||||
# (theme changes don't need it; HM generations already roll back).
|
# (theme changes don't need it; HM generations already roll back).
|
||||||
@@ -312,16 +359,84 @@ in
|
|||||||
echo "This script must be run as root (use sudo)" >&2
|
echo "This script must be run as root (use sudo)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
# Prefer the path nomarchy-rebuild passes through sudo; fall back
|
||||||
|
# for hand invocations.
|
||||||
|
flake="''${NOMARCHY_PATH:-/etc/nixos}"
|
||||||
echo "Creating pre-rebuild snapshot..."
|
echo "Creating pre-rebuild snapshot..."
|
||||||
${pkgs.snapper}/bin/snapper -c root create \
|
${pkgs.snapper}/bin/snapper -c root create \
|
||||||
-d "Pre-rebuild $(date +'%Y-%m-%d %H:%M:%S')" \
|
-d "Pre-rebuild $(date +'%Y-%m-%d %H:%M:%S')" \
|
||||||
--cleanup-algorithm number
|
--cleanup-algorithm number
|
||||||
echo "Rebuilding..."
|
echo "Rebuilding $flake#default ..."
|
||||||
nixos-rebuild switch --flake /etc/nixos#default "$@"
|
nixos-rebuild switch --flake "$flake#default" "$@"
|
||||||
'')
|
'')
|
||||||
# The desktop snapshot manager (browse / diff / restore / rollback over
|
# The desktop snapshot manager (browse / diff / restore / rollback over
|
||||||
# snapper, elevating via polkit) — what `nomarchy-menu snapshot` launches.
|
# snapper, elevating via polkit) — the primary `nomarchy-menu snapshot`
|
||||||
++ lib.optional cfg.snapper.enable pkgs.btrfs-assistant;
|
# target. The "2.2 segfault" is unprivileged-only (libbtrfsutil
|
||||||
|
# unprivileged subvolume iteration, btrfs-progs 6.17.1, fixed upstream
|
||||||
|
# after); the pkexec launcher runs it as root, where it works —
|
||||||
|
# VM-proven, guarded by checks.snapshot-gui.
|
||||||
|
++ lib.optional cfg.snapper.enable pkgs.btrfs-assistant
|
||||||
|
# Keyboard-driven snapper browser/restore — the menu's fallback when the
|
||||||
|
# GUI is absent, and handy over SSH. Runs as root (snapper is root-only
|
||||||
|
# here; the menu opens it in a terminal via sudo, one password prompt),
|
||||||
|
# fzf to pick, with browse/diff (read-only) and typed-`yes` confirmation
|
||||||
|
# before any write.
|
||||||
|
++ lib.optional cfg.snapper.enable (pkgs.writeShellApplication {
|
||||||
|
name = "nomarchy-snapshots";
|
||||||
|
runtimeInputs = with pkgs; [ snapper fzf gawk gnugrep less coreutils systemd ];
|
||||||
|
text = ''
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo "nomarchy-snapshots must run as root (snapper needs it) — use sudo." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t configs < <(snapper list-configs | awk 'NR>2 {print $1}' | grep .)
|
||||||
|
if [ "''${#configs[@]}" -eq 0 ]; then
|
||||||
|
echo "No snapper configs found." >&2; exit 1
|
||||||
|
elif [ "''${#configs[@]}" -eq 1 ]; then
|
||||||
|
config="''${configs[0]}"
|
||||||
|
else
|
||||||
|
config=$(printf '%s\n' "''${configs[@]}" | fzf --prompt="snapper config> ") || exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
snap=$(snapper -c "$config" list \
|
||||||
|
| fzf --header-lines=2 --prompt="[$config] pick a snapshot (Esc quits)> ") || exit 0
|
||||||
|
num=$(awk '{print $1}' <<<"$snap")
|
||||||
|
case "$num" in ""|*[!0-9]*) continue ;; esac
|
||||||
|
|
||||||
|
action=$(printf '%s\n' \
|
||||||
|
"Browse changes since #$num (read-only)" \
|
||||||
|
"Restore changed files to #$num (undochange)" \
|
||||||
|
"Roll the system back to #$num (reboot)" \
|
||||||
|
"↩ Back to the snapshot list" \
|
||||||
|
| fzf --prompt="snapshot #$num> ") || exit 0
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
Browse*)
|
||||||
|
snapper -c "$config" status "$num..0" | less -R || true ;;
|
||||||
|
Restore*)
|
||||||
|
read -rp "Revert files in '$config' to snapshot #$num? Type yes to confirm: " ans || continue
|
||||||
|
if [ "$ans" = yes ]; then
|
||||||
|
snapper -c "$config" undochange "$num..0"
|
||||||
|
echo "Files restored. Press enter."; read -r _ || true
|
||||||
|
fi ;;
|
||||||
|
Roll*)
|
||||||
|
if [ "$config" != root ]; then
|
||||||
|
echo "Rollback applies to the 'root' config only; use Restore for '$config'. Press enter."
|
||||||
|
read -r _ || true
|
||||||
|
else
|
||||||
|
read -rp "Roll the SYSTEM back to #$num and REBOOT now? Type yes to confirm: " ans || continue
|
||||||
|
if [ "$ans" = yes ]; then
|
||||||
|
snapper -c root rollback "$num"
|
||||||
|
echo "Rolled back — rebooting…"; systemctl reboot
|
||||||
|
fi
|
||||||
|
fi ;;
|
||||||
|
*) continue ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
});
|
||||||
|
|
||||||
# Don't let boot entries fill the ESP over the years.
|
# Don't let boot entries fill the ESP over the years.
|
||||||
boot.loader.systemd-boot.configurationLimit = lib.mkDefault 10;
|
boot.loader.systemd-boot.configurationLimit = lib.mkDefault 10;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ in
|
|||||||
services.udisks2.enable = lib.mkDefault true; # mount/unmount removable media
|
services.udisks2.enable = lib.mkDefault true; # mount/unmount removable media
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
|
file-roller # GUI archive manager (backend for thunar-archive-plugin)
|
||||||
ffmpegthumbnailer # video thumbnails (Thunar + yazi)
|
ffmpegthumbnailer # video thumbnails (Thunar + yazi)
|
||||||
libgsf # ODF thumbnails
|
libgsf # ODF thumbnails
|
||||||
poppler-utils # PDF thumbnails / pdftoppm (yazi PDF preview too)
|
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.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
61
modules/nixos/oom.nix
Normal file
61
modules/nixos/oom.nix
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# zram sits one layer earlier: a compressed RAM swap that absorbs
|
||||||
|
# day-to-day memory pressure (a browser's idle tabs, a big eval's cold
|
||||||
|
# pages) before it ever reaches the earlyoom threshold — more headroom
|
||||||
|
# on the same RAM, no disk. High swap priority so the kernel fills zram
|
||||||
|
# first; any real disk swapfile stays reserved for the hibernate image
|
||||||
|
# (BACKLOG #76's other half — hibernation can't resume from volatile
|
||||||
|
# zram, so day-to-day paging must not consume the disk swap). zstd @ 50%
|
||||||
|
# RAM are the nixpkgs defaults, kept explicit for reproducibility.
|
||||||
|
{ lib, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
zramSwap = {
|
||||||
|
enable = lib.mkDefault true;
|
||||||
|
algorithm = lib.mkDefault "zstd";
|
||||||
|
memoryPercent = lib.mkDefault 50;
|
||||||
|
# Beats any disk swapfile's auto-assigned (negative) priority, so
|
||||||
|
# day-to-day paging lands in zram and the disk swap is left for
|
||||||
|
# hibernation. See the header note and BACKLOG #76.
|
||||||
|
priority = lib.mkDefault 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
# Deliberately small: only things a downstream user plausibly disagrees
|
||||||
# with get a toggle. Everything else in the system module is set with
|
# with get a toggle. Everything else in the system module is set with
|
||||||
# lib.mkDefault, so plain NixOS options override it natively.
|
# lib.mkDefault, so plain NixOS options override it natively.
|
||||||
{ lib, ... }:
|
{ config, lib, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
options.nomarchy.system = {
|
options.nomarchy.system = {
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
greeter.autoLogin = lib.mkOption {
|
greeter.autoLogin = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = null;
|
default = config.nomarchy.settings.greeter.autoLogin or null;
|
||||||
example = "ada";
|
example = "ada";
|
||||||
description = ''
|
description = ''
|
||||||
Log this user straight into Hyprland on boot (greetd
|
Log this user straight into Hyprland on boot (greetd
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
description = ''
|
description = ''
|
||||||
theme-state.json for the system-side consumers (currently the
|
theme-state.json for the system-side consumers (currently the
|
||||||
Plymouth splash background). lib.mkFlake wires it automatically
|
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; };
|
option'' // { default = true; };
|
||||||
|
|
||||||
audio.enable = lib.mkEnableOption "the Pipewire audio stack" // { default = true; };
|
audio.enable = lib.mkEnableOption "the Pipewire audio stack" // { default = true; };
|
||||||
bluetooth.enable = lib.mkEnableOption "Bluetooth support with blueman" // { default = true; };
|
bluetooth.enable = lib.mkEnableOption "Bluetooth support with blueman" // { default = 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 ''
|
snapper.enable = lib.mkEnableOption ''
|
||||||
hourly/daily BTRFS timeline snapshots of / via snapper, plus the
|
hourly/daily BTRFS timeline snapshots of / via snapper, plus the
|
||||||
@@ -92,15 +102,22 @@
|
|||||||
|
|
||||||
batteryChargeLimit = lib.mkOption {
|
batteryChargeLimit = lib.mkOption {
|
||||||
type = lib.types.nullOr (lib.types.ints.between 50 100);
|
type = lib.types.nullOr (lib.types.ints.between 50 100);
|
||||||
default = null;
|
default = config.nomarchy.settings.power.batteryChargeLimit or null;
|
||||||
|
# Dell Adaptive charge mode ignores the end threshold unless we
|
||||||
|
# also select Custom (power.nix oneshot); see Latitude 5310 QA.
|
||||||
example = 80;
|
example = 80;
|
||||||
description = ''
|
description = ''
|
||||||
Stop charging at this percentage to extend battery lifespan,
|
Stop charging at this percentage to extend battery lifespan,
|
||||||
where the hardware exposes a charge threshold
|
where the hardware exposes charge_control_end_threshold on a
|
||||||
(/sys/class/power_supply/BAT*/charge_control_end_threshold).
|
system battery (type=Battery under /sys/class/power_supply;
|
||||||
null leaves charging at the firmware default. Backend-independent
|
name-agnostic — BAT0, CMB0, …).
|
||||||
(a small systemd unit writes the sysfs knob at boot), so it
|
null leaves charging at the firmware default (menu writes 100).
|
||||||
works under PPD too; needs nomarchy.system.power.laptop.
|
Backend-independent: the menu applies live via sysfs (udev
|
||||||
|
GROUP=users on the threshold node) and persists settings in
|
||||||
|
theme-state; a oneshot re-applies on boot and AC replug. On
|
||||||
|
Dell (and similar) the oneshot also selects charge type Custom
|
||||||
|
— Adaptive ignores the threshold while still reporting it.
|
||||||
|
Needs nomarchy.system.power.laptop.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,17 +12,18 @@ let
|
|||||||
|
|
||||||
state =
|
state =
|
||||||
if cfg.stateFile != null
|
if cfg.stateFile != null
|
||||||
then builtins.fromJSON (builtins.readFile cfg.stateFile)
|
then import ../theme-state-read.nix { inherit lib; } cfg.stateFile
|
||||||
else { };
|
else { };
|
||||||
colorOf = key: fallback: lib.removePrefix "#" ((state.colors or { }).${key} or fallback);
|
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
|
# Splash elements are recolored from the palette so they read on any
|
||||||
# base, light or dark: foreground glyphs → text, field/track boxes →
|
# base, light or dark: foreground glyphs → text, field/track boxes →
|
||||||
# surface (raised from base in both polarities), the progress fill →
|
# surface (raised from base in both polarities), the progress fill →
|
||||||
# accent. The shipped art is a fixed navy that vanished on dark themes.
|
# accent. The shipped art is a fixed navy that vanished on dark themes.
|
||||||
text = colorOf "text" "#a9b1d6";
|
text = colorOf "text" "#D3DAE0";
|
||||||
surface = colorOf "surface" "#32344a";
|
surface = colorOf "surface" "#303A46";
|
||||||
accent = colorOf "accent" "#7aa2f7";
|
accent = colorOf "accent" "#B79BE8";
|
||||||
magick = lib.getExe' pkgs.imagemagick "magick";
|
magick = lib.getExe' pkgs.imagemagick "magick";
|
||||||
|
|
||||||
# Plymouth's Window.SetBackgroundTopColor takes three floats in
|
# Plymouth's Window.SetBackgroundTopColor takes three floats in
|
||||||
|
|||||||
@@ -5,13 +5,12 @@
|
|||||||
# gate on powerprofilesctl being present, so there's no system→home wiring
|
# gate on powerprofilesctl being present, so there's no system→home wiring
|
||||||
# to keep in sync (the same way the Waybar battery widget auto-hides on
|
# to keep in sync (the same way the Waybar battery widget auto-hides on
|
||||||
# desktops). See the roadmap in README.md.
|
# desktops). See the roadmap in README.md.
|
||||||
{ config, lib, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.nomarchy.system.power;
|
cfg = config.nomarchy.system.power;
|
||||||
ppd = cfg.backend == "ppd";
|
ppd = cfg.backend == "ppd";
|
||||||
tlp = cfg.backend == "tlp";
|
tlp = cfg.backend == "tlp";
|
||||||
chargeLimit = cfg.laptop && cfg.batteryChargeLimit != null;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
@@ -38,23 +37,138 @@ in
|
|||||||
|
|
||||||
# Battery charge limit via sysfs. PPD can't cap charge at all, and
|
# Battery charge limit via sysfs. PPD can't cap charge at all, and
|
||||||
# TLP's own knob only applies under TLP — so a tiny oneshot writes
|
# TLP's own knob only applies under TLP — so a tiny oneshot writes
|
||||||
# the threshold directly, independent of the backend. Boot-time only:
|
# the threshold directly, independent of the backend. Re-applied on
|
||||||
# some firmwares reset the threshold on unplug; revisit with a udev
|
# AC state changes by the udev rule below (some firmwares reset the
|
||||||
# hook if that bites. `-` paths that don't exist are skipped, so this
|
# threshold when the charger is unplugged).
|
||||||
# is a clean no-op on hardware without the control.
|
#
|
||||||
systemd.services.nomarchy-battery-charge-limit = lib.mkIf chargeLimit {
|
# Instant menu path (user decision 2026-07-10): the threshold node is
|
||||||
description = "Cap battery charging at ${toString cfg.batteryChargeLimit}%";
|
# group-writable for `users` so nomarchy-menu can echo live without
|
||||||
|
# rebuild; this oneshot still owns boot + AC-replug re-apply. Prefer
|
||||||
|
# live theme-state.json under /home/*/.nomarchy so a menu change
|
||||||
|
# survives reboot before the next sys-rebuild bakes the Nix option.
|
||||||
|
systemd.services.nomarchy-battery-charge-limit = lib.mkIf cfg.laptop {
|
||||||
|
description = "Apply battery charge end threshold from state or config";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
path = [ pkgs.jq pkgs.coreutils ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
RemainAfterExit = true;
|
RemainAfterExit = true;
|
||||||
};
|
};
|
||||||
script = ''
|
script = ''
|
||||||
for thresh in /sys/class/power_supply/BAT*/charge_control_end_threshold; do
|
set -euo pipefail
|
||||||
[ -w "$thresh" ] && echo ${toString cfg.batteryChargeLimit} > "$thresh"
|
# Baked generation default (empty = full charge / no cap).
|
||||||
|
limit=${if cfg.batteryChargeLimit != null then toString cfg.batteryChargeLimit else ""}
|
||||||
|
# Prefer the newest live state write (menu path, no rebuild yet).
|
||||||
|
for st in /home/*/.nomarchy/theme-state.json; do
|
||||||
|
[ -r "$st" ] || continue
|
||||||
|
v=$(jq -r '.settings.power.batteryChargeLimit // empty' "$st" 2>/dev/null || true)
|
||||||
|
case "$v" in
|
||||||
|
""|null|Null) ;;
|
||||||
|
*[!0-9]*) ;;
|
||||||
|
*) limit=$v ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ -n "$limit" ] || limit=100
|
||||||
|
# Name-agnostic system batteries (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"
|
||||||
|
[ -e "$thresh" ] || continue
|
||||||
|
# Keep nodes group-writable for the menu's live path (udev
|
||||||
|
# sets this on add; re-assert after firmware recreates attrs).
|
||||||
|
for node in charge_control_end_threshold charge_control_start_threshold charge_types charge_type; do
|
||||||
|
[ -e "$d$node" ] || continue
|
||||||
|
chgrp users "$d$node" 2>/dev/null || true
|
||||||
|
chmod 0664 "$d$node" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
# Dell (and some others): charge_control_* thresholds are only
|
||||||
|
# honoured in Custom mode. Adaptive/Standard ignore end_threshold
|
||||||
|
# while still reporting the written value — battery keeps charging
|
||||||
|
# past the cap (Latitude 5310: end=80, type=[Adaptive], capacity 96%+).
|
||||||
|
# Off (100): restore Adaptive if listed, else leave type alone.
|
||||||
|
ctypes="$d/charge_types"
|
||||||
|
ctype="$d/charge_type"
|
||||||
|
if [ -e "$ctypes" ] || [ -e "$ctype" ]; then
|
||||||
|
listed=$(cat "$ctypes" 2>/dev/null || cat "$ctype" 2>/dev/null || true)
|
||||||
|
write_type() {
|
||||||
|
local t="$1"
|
||||||
|
[ -n "$t" ] || return 0
|
||||||
|
if [ -w "$ctypes" ]; then echo "$t" > "$ctypes" 2>/dev/null || true
|
||||||
|
elif [ -w "$ctype" ]; then echo "$t" > "$ctype" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
if [ "$limit" -lt 100 ] 2>/dev/null; then
|
||||||
|
# listed looks like: "Trickle Fast Standard [Adaptive] Custom"
|
||||||
|
case "$listed" in *Custom*) write_type Custom ;; esac
|
||||||
|
else
|
||||||
|
case "$listed" in
|
||||||
|
*Adaptive*) write_type Adaptive ;;
|
||||||
|
*Standard*) write_type Standard ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Start threshold: must be < end. Keep a ~10% hysteresis band when
|
||||||
|
# the node exists (firmware may reject start >= end).
|
||||||
|
start_node="$d/charge_control_start_threshold"
|
||||||
|
if [ -w "$start_node" ] && [ "$limit" -lt 100 ] 2>/dev/null; then
|
||||||
|
start=$(( limit > 15 ? limit - 10 : 0 ))
|
||||||
|
echo "$start" > "$start_node" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
# Some firmwares (Dell) reset the threshold on unplug *after*
|
||||||
|
# the udev event — write once, wait, write again (type first).
|
||||||
|
echo "$limit" > "$thresh" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
if [ -e "$ctypes" ] || [ -e "$ctype" ]; then
|
||||||
|
listed=$(cat "$ctypes" 2>/dev/null || cat "$ctype" 2>/dev/null || true)
|
||||||
|
if [ "$limit" -lt 100 ] 2>/dev/null; then
|
||||||
|
case "$listed" in
|
||||||
|
*Custom*)
|
||||||
|
if [ -w "$ctypes" ]; then echo Custom > "$ctypes" 2>/dev/null || true
|
||||||
|
elif [ -w "$ctype" ]; then echo Custom > "$ctype" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "$limit" > "$thresh" 2>/dev/null || true
|
||||||
done
|
done
|
||||||
exit 0
|
exit 0
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Writable threshold for the logged-in user (menu live apply) + AC
|
||||||
|
# re-apply of the oneshot. Laptop only; no-op when the attr is absent.
|
||||||
|
# Immediate + delayed restart: Dell/Latitude firmware often stomps the
|
||||||
|
# threshold to 100 right after unplug; a single immediate oneshot
|
||||||
|
# loses the race (hardware: unplug→100, plug→80). systemd-run hands
|
||||||
|
# the delayed restart off so udev's short RUN budget is not held.
|
||||||
|
services.udev.extraRules = lib.mkIf cfg.laptop (
|
||||||
|
let
|
||||||
|
systemctl = "${config.systemd.package}/bin/systemctl";
|
||||||
|
systemdRun = "${config.systemd.package}/bin/systemd-run";
|
||||||
|
in ''
|
||||||
|
SUBSYSTEM=="power_supply", ATTR{type}=="Battery", TEST=="charge_control_end_threshold", GROUP="users", MODE="0664"
|
||||||
|
SUBSYSTEM=="power_supply", ATTR{type}=="Battery", TEST=="charge_types", GROUP="users", MODE="0664"
|
||||||
|
SUBSYSTEM=="power_supply", ATTR{type}=="Battery", TEST=="charge_type", GROUP="users", MODE="0664"
|
||||||
|
SUBSYSTEM=="power_supply", ATTR{type}=="Battery", TEST=="charge_control_start_threshold", GROUP="users", MODE="0664"
|
||||||
|
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="${systemctl} --no-block restart nomarchy-battery-charge-limit.service"
|
||||||
|
SUBSYSTEM=="power_supply", ATTR{type}=="Mains", RUN+="${systemdRun} --no-block --collect --on-active=2s --timer-property=AccuracySec=200ms ${systemctl} --no-block restart nomarchy-battery-charge-limit.service"
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
# Unprivileged restart of the oneshot so the menu can re-apply as
|
||||||
|
# root when the threshold node is not (yet) user-writable.
|
||||||
|
security.polkit.extraConfig = lib.mkIf cfg.laptop ''
|
||||||
|
polkit.addRule(function(action, subject) {
|
||||||
|
if (action.id == "org.freedesktop.systemd1.manage-units" &&
|
||||||
|
subject.isInGroup("users")) {
|
||||||
|
var unit = action.lookup("unit");
|
||||||
|
if (unit == "nomarchy-battery-charge-limit.service") {
|
||||||
|
return polkit.Result.YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ in
|
|||||||
{
|
{
|
||||||
options.nomarchy.services = {
|
options.nomarchy.services = {
|
||||||
tailscale.enable = lib.mkEnableOption ''
|
tailscale.enable = lib.mkEnableOption ''
|
||||||
Tailscale, the mesh VPN — ships the daemon; authenticate once with
|
Tailscale, the mesh VPN — ships the daemon and makes the login user its
|
||||||
`sudo tailscale up`'';
|
operator, so `tailscale up/down/set` and the System → VPN menu work
|
||||||
|
without sudo. Authenticate once (the menu's Connect, or `tailscale up`)'';
|
||||||
|
|
||||||
syncthing.enable = lib.mkEnableOption ''
|
syncthing.enable = lib.mkEnableOption ''
|
||||||
Syncthing continuous file sync, running as the login user — add
|
Syncthing continuous file sync, running as the login user — add
|
||||||
@@ -75,7 +76,8 @@ in
|
|||||||
|
|
||||||
printing.enable = lib.mkEnableOption ''
|
printing.enable = lib.mkEnableOption ''
|
||||||
CUPS printing with Avahi/mDNS, so network printers are auto-discovered
|
CUPS printing with Avahi/mDNS, so network printers are auto-discovered
|
||||||
(add vendor drivers via `services.printing.drivers`)'';
|
(add vendor drivers via `services.printing.drivers`); the menu's
|
||||||
|
System ▸ Printers entry opens the system-config-printer GUI'' // { default = config.nomarchy.settings.printing.enable or false; };
|
||||||
|
|
||||||
openrgb.enable = lib.mkEnableOption ''
|
openrgb.enable = lib.mkEnableOption ''
|
||||||
the OpenRGB daemon and GUI for controlling RGB lighting on peripherals
|
the OpenRGB daemon and GUI for controlling RGB lighting on peripherals
|
||||||
@@ -124,6 +126,12 @@ in
|
|||||||
config = lib.mkMerge [
|
config = lib.mkMerge [
|
||||||
(lib.mkIf cfg.tailscale.enable {
|
(lib.mkIf cfg.tailscale.enable {
|
||||||
services.tailscale.enable = true;
|
services.tailscale.enable = true;
|
||||||
|
# Let the login user drive tailscale (up/down/set — and so the VPN menu's
|
||||||
|
# Tailscale controls) without sudo. It's already in wheel, so the operator
|
||||||
|
# grant is no real new privilege, just skips the password prompt. The
|
||||||
|
# module's tailscaled-set unit applies this after the daemon starts.
|
||||||
|
# mkDefault so a downstream can drop or replace it (e.g. extraSetFlags = []).
|
||||||
|
services.tailscale.extraSetFlags = lib.mkDefault [ "--operator=${args.username}" ];
|
||||||
})
|
})
|
||||||
|
|
||||||
(lib.mkIf cfg.syncthing.enable {
|
(lib.mkIf cfg.syncthing.enable {
|
||||||
@@ -246,6 +254,9 @@ in
|
|||||||
nssmdns4 = true;
|
nssmdns4 = true;
|
||||||
openFirewall = true;
|
openFirewall = true;
|
||||||
};
|
};
|
||||||
|
# The CUPS admin GUI — the menu's System ▸ Printers entry execs it
|
||||||
|
# (self-gated on this binary), so it ships with the printing service.
|
||||||
|
environment.systemPackages = [ pkgs.system-config-printer ];
|
||||||
})
|
})
|
||||||
|
|
||||||
(lib.mkIf cfg.openrgb.enable {
|
(lib.mkIf cfg.openrgb.enable {
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
330
pkgs/nomarchy-doctor/nomarchy-doctor.sh
Normal file
330
pkgs/nomarchy-doctor/nomarchy-doctor.sh
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
# 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).
|
||||||
|
# Dell: Adaptive mode ignores end_threshold while still reporting the
|
||||||
|
# written value — warn if type is not Custom when a limit is active.
|
||||||
|
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
|
||||||
|
ctype=$(cat "$bat/charge_types" 2>/dev/null || cat "$bat/charge_type" 2>/dev/null || true)
|
||||||
|
active=$(printf '%s' "$ctype" | sed -n 's/.*\[\([^]]*\)\].*/\1/p')
|
||||||
|
name=$(basename "$bat")
|
||||||
|
if [ -n "$active" ] && [ "$active" != Custom ]; then
|
||||||
|
warn "$name charge limit ${lim}% but type is $active (not Custom)" \
|
||||||
|
"thresholds only apply in Custom — run: systemctl restart nomarchy-battery-charge-limit"
|
||||||
|
else
|
||||||
|
ok "$name charge limit active at ${lim}%${active:+ (type $active)}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$bat_seen" -eq 1 ] && [ "$bat_limited" -eq 0 ]; then
|
||||||
|
skip "battery charge limit (none set — charges to 100%)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── hibernate / sleep (BACKLOG #76) ──────────────────────────────────
|
||||||
|
# Read-only: is there a working hibernate path? zram is RAM-only and can't
|
||||||
|
# hold a resume image, so hibernation needs a disk swap (partition or file)
|
||||||
|
# PLUS resume= wiring; a swapfile additionally needs resume_offset. A
|
||||||
|
# swap=0 machine opts out cleanly and this section skips. Never fails the
|
||||||
|
# sheet — an absent or under-sized swap is advisory, not a running fault.
|
||||||
|
# (|| echo 0 keeps set -euo pipefail happy if a proc file is unreadable.)
|
||||||
|
disk_swap_kb=$(awk 'NR>1 && $1 !~ /zram/ {s+=$3} END{print s+0}' /proc/swaps 2>/dev/null || echo 0)
|
||||||
|
zram_kb=$(awk 'NR>1 && $1 ~ /zram/ {s+=$3} END{print s+0}' /proc/swaps 2>/dev/null || echo 0)
|
||||||
|
ram_kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
if [ "${disk_swap_kb:-0}" -gt 0 ]; then
|
||||||
|
if grep -q 'resume=' /proc/cmdline 2>/dev/null; then
|
||||||
|
ok "hibernate: resume device set on the kernel cmdline"
|
||||||
|
# A swapfile also needs resume_offset to locate the image within it.
|
||||||
|
if awk 'NR>1 && $1 !~ /zram/ && $2=="file"{f=1} END{exit !f}' /proc/swaps 2>/dev/null \
|
||||||
|
&& ! grep -q 'resume_offset=' /proc/cmdline 2>/dev/null; then
|
||||||
|
warn "swapfile in use but no resume_offset= on the cmdline — resume will fail" \
|
||||||
|
"add boot.kernelParams resume_offset (docs/MIGRATION.md → Enabling hibernation)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "disk swap present but no resume= on the kernel cmdline — hibernate won't resume" \
|
||||||
|
"set boot.resumeDevice (docs/MIGRATION.md → Enabling hibernation)"
|
||||||
|
fi
|
||||||
|
if [ "${ram_kb:-0}" -gt 0 ] && [ "$disk_swap_kb" -lt "$ram_kb" ]; then
|
||||||
|
warn "disk swap ($(numfmt --to=iec $((disk_swap_kb * 1024)))) is smaller than RAM ($(numfmt --to=iec $((ram_kb * 1024)))) — a full hibernate image may not fit" \
|
||||||
|
"size swap ≥ RAM (docs/MIGRATION.md → Enabling hibernation)"
|
||||||
|
else
|
||||||
|
ok "hibernate: disk swap ≥ RAM ($(numfmt --to=iec $((disk_swap_kb * 1024))))"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
skip "hibernate (no disk swap — swap=0 opt-out; zram alone can't resume)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# zram compressed-RAM swap — the memory-pressure layer oom.nix ships on
|
||||||
|
# by default (#76). Its absence on a Nomarchy box means something is off.
|
||||||
|
if [ "${zram_kb:-0}" -gt 0 ]; then
|
||||||
|
ok "zram compressed-RAM swap active ($(numfmt --to=iec $((zram_kb * 1024))))"
|
||||||
|
else
|
||||||
|
warn "zram swap not active (the default memory-pressure layer)" \
|
||||||
|
"expected on by default — check modules/nixos/oom.nix, then sys-rebuild"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Best-effort: a suspend/hibernate failure recorded in the previous boot's
|
||||||
|
# kernel log. Needs journal read access; silently no-ops without it. The
|
||||||
|
# `|| true` keeps a no-match grep / missing -1 boot from tripping set -e.
|
||||||
|
if command -v journalctl >/dev/null 2>&1; then
|
||||||
|
pmerr=$(journalctl -b -1 -k --no-pager 2>/dev/null \
|
||||||
|
| grep -iE 'PM: .*(hibernat|suspend).*(fail|error)|Failed to (hibernate|suspend)' \
|
||||||
|
| tail -n1 || true)
|
||||||
|
if [ -n "$pmerr" ]; then
|
||||||
|
warn "a suspend/hibernate error is in the previous boot's log" \
|
||||||
|
"review: journalctl -b -1 -k -g 'hibernat|suspend'"
|
||||||
|
fi
|
||||||
|
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
|
, cryptsetup
|
||||||
, lvm2 # dmsetup
|
, lvm2 # dmsetup
|
||||||
, pciutils # lspci
|
, pciutils # lspci
|
||||||
|
, usbutils # lsusb (fingerprint-reader VID probe; sysfs is the fallback)
|
||||||
, btrfs-progs # inspect-internal map-swapfile (hibernation offset)
|
, btrfs-progs # inspect-internal map-swapfile (hibernation offset)
|
||||||
# Baked metadata — what this installer installs and where it came from.
|
# Baked metadata — what this installer installs and where it came from.
|
||||||
, templateDir # templates/downstream (home.nix, theme-state.json)
|
, templateDir # templates/downstream (home.nix, theme-state.json)
|
||||||
@@ -50,14 +51,18 @@ stdenvNoCC.mkDerivation {
|
|||||||
# Empty flake registry: no network lookups for indirect refs.
|
# Empty flake registry: no network lookups for indirect refs.
|
||||||
echo '{"flakes":[],"version":2}' > "$share/registry.json"
|
echo '{"flakes":[],"version":2}' > "$share/registry.json"
|
||||||
mkdir -p "$share/template"
|
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
|
# nixos-install / nixos-generate-config / nixos-enter / nix / systemd
|
||||||
# tools come from the live system on purpose — they must match it.
|
# tools come from the live system on purpose — they must match it.
|
||||||
wrapProgram $out/bin/nomarchy-install \
|
wrapProgram $out/bin/nomarchy-install \
|
||||||
--prefix PATH : ${lib.makeBinPath [
|
--prefix PATH : ${lib.makeBinPath [
|
||||||
bash gum disko whois git python3
|
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_INSTALL_SHARE "$share" \
|
||||||
--set NOMARCHY_NIXPKGS ${nixpkgsPath} \
|
--set NOMARCHY_NIXPKGS ${nixpkgsPath} \
|
||||||
|
|||||||
@@ -15,12 +15,14 @@
|
|||||||
# proved fragile twice before.
|
# proved fragile twice before.
|
||||||
{ mainDrive
|
{ mainDrive
|
||||||
, withLuks ? true
|
, 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
|
let
|
||||||
btrfsMountOptions = [ "compress=zstd" "noatime" ];
|
btrfsMountOptions = [ "compress=zstd" "noatime" ];
|
||||||
|
# Installer historically always appended "G"; accept both "0" and "0G".
|
||||||
|
noSwap = swapSize == "0" || swapSize == "0G";
|
||||||
|
|
||||||
rootBtrfs = {
|
rootBtrfs = {
|
||||||
type = "btrfs";
|
type = "btrfs";
|
||||||
@@ -32,7 +34,7 @@ let
|
|||||||
"@log" = { mountpoint = "/var/log"; mountOptions = btrfsMountOptions; };
|
"@log" = { mountpoint = "/var/log"; mountOptions = btrfsMountOptions; };
|
||||||
# snapper timeline snapshots (nomarchy.system.snapper.enable)
|
# snapper timeline snapshots (nomarchy.system.snapper.enable)
|
||||||
"@snapshots" = { mountpoint = "/.snapshots"; mountOptions = btrfsMountOptions; };
|
"@snapshots" = { mountpoint = "/.snapshots"; mountOptions = btrfsMountOptions; };
|
||||||
} // (if swapSize == "0" then { } else {
|
} // (if noSwap then { } else {
|
||||||
# Hibernation-ready swapfile on its own subvolume; disko's
|
# Hibernation-ready swapfile on its own subvolume; disko's
|
||||||
# mkswapfile handles the BTRFS NOCOW requirements.
|
# mkswapfile handles the BTRFS NOCOW requirements.
|
||||||
"@swap" = {
|
"@swap" = {
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ HARDWARE_DB=(
|
|||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
nomarchy_detect_hw() {
|
nomarchy_detect_hw() {
|
||||||
local sys_vendor product_name cpu_vendor
|
local sys_vendor product_name cpu_vendor
|
||||||
|
local nvidia=0 amdgpu=0 intelgpu=0
|
||||||
sys_vendor=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || echo "")
|
sys_vendor=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || echo "")
|
||||||
product_name=$(cat /sys/class/dmi/id/product_name 2>/dev/null || echo "")
|
product_name=$(cat /sys/class/dmi/id/product_name 2>/dev/null || echo "")
|
||||||
cpu_vendor=$(lscpu 2>/dev/null | awk -F: '/Vendor ID/{gsub(/ /,"",$2); print $2; exit}')
|
cpu_vendor=$(lscpu 2>/dev/null | awk -F: '/Vendor ID/{gsub(/ /,"",$2); print $2; exit}')
|
||||||
@@ -126,7 +127,7 @@ nomarchy_detect_hw() {
|
|||||||
|
|
||||||
# GPU (lspci may list several; report all)
|
# GPU (lspci may list several; report all)
|
||||||
if command -v lspci >/dev/null 2>&1; then
|
if command -v lspci >/dev/null 2>&1; then
|
||||||
local gpu_line nvidia=0 amdgpu=0 intelgpu=0
|
local gpu_line
|
||||||
while IFS= read -r gpu_line; do
|
while IFS= read -r gpu_line; do
|
||||||
case "$gpu_line" in
|
case "$gpu_line" in
|
||||||
*"[10de:"*|*"NVIDIA"*) nvidia=1 ;;
|
*"[10de:"*|*"NVIDIA"*) nvidia=1 ;;
|
||||||
@@ -140,10 +141,102 @@ nomarchy_detect_hw() {
|
|||||||
(( intelgpu )) && { echo "MODULE common-gpu-intel"; echo "DETAIL gpu: Intel"; detected=1; }
|
(( intelgpu )) && { echo "MODULE common-gpu-intel"; echo "DETAIL gpu: Intel"; detected=1; }
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Chassis (glob test, not compgen — nixpkgs' non-interactive bash
|
# ── nomarchy.hardware.* enablement — the gap ABOVE the nixos-hardware
|
||||||
# is built without the completion builtins)
|
# commons (GuC/HuC, amd-pstate, the AMD VA-API env, GPU-compute runtimes,
|
||||||
local bats=(/sys/class/power_supply/BAT*)
|
# fprintd, the NPU driver). Emitted as NOMARCHY lines the installer turns
|
||||||
if [[ -e "${bats[0]}" ]]; then
|
# 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 "MODULE common-pc-laptop"
|
||||||
echo "DETAIL chassis: laptop (battery present)"
|
echo "DETAIL chassis: laptop (battery present)"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -18,8 +18,11 @@
|
|||||||
# [NOMARCHY_TIMEZONE=UTC] [NOMARCHY_LUKS_PASSPHRASE=...] \
|
# [NOMARCHY_TIMEZONE=UTC] [NOMARCHY_LUKS_PASSPHRASE=...] \
|
||||||
# [NOMARCHY_LOCALE=en_US.UTF-8] [NOMARCHY_KB_LAYOUT=us] [NOMARCHY_KB_VARIANT=] \
|
# [NOMARCHY_LOCALE=en_US.UTF-8] [NOMARCHY_KB_LAYOUT=us] [NOMARCHY_KB_VARIANT=] \
|
||||||
# [NOMARCHY_SWAP_GB=N (default: RAM size; 0 = none)] \
|
# [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_HW="auto"|"none"|"mod1 mod2"] [NOMARCHY_FINISH=none|reboot|poweroff]
|
||||||
# nomarchy-install
|
# nomarchy-install
|
||||||
|
# Unattended encryption is fail-closed: set a passphrase, or explicit
|
||||||
|
# NOMARCHY_NO_LUKS=1 — never silently install cleartext.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -54,6 +57,15 @@ if [[ $EUID -ne 0 ]]; then
|
|||||||
exec sudo --preserve-env "$0" "$@"
|
exec sudo --preserve-env "$0" "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# `sudo --preserve-env` (needed to carry the NOMARCHY_* vars) also drags in
|
||||||
|
# the live session user's HOME=/home/nomarchy. Root-run `nix` calls below
|
||||||
|
# would then scribble an eval cache + .nix-defexpr into /home/nomarchy —
|
||||||
|
# and the in-chroot one lands on the TARGET disk as a stray, orphaned
|
||||||
|
# /home/nomarchy (no such user on the installed system). Pin root's own
|
||||||
|
# HOME so every root nix invocation stays in /root; the user activation
|
||||||
|
# sets HOME=/home/$USERNAME explicitly and is unaffected.
|
||||||
|
export HOME=/root
|
||||||
|
|
||||||
header "Nomarchy installer" "NixOS, themed and ready to go."
|
header "Nomarchy installer" "NixOS, themed and ready to go."
|
||||||
|
|
||||||
[[ -d /sys/firmware/efi ]] \
|
[[ -d /sys/firmware/efi ]] \
|
||||||
@@ -77,7 +89,9 @@ export NIX_CONFIG="flake-registry = $SHARE/registry.json"
|
|||||||
# substituter resolves to the TARGET store (i.e. itself), so without
|
# substituter resolves to the TARGET store (i.e. itself), so without
|
||||||
# this nothing flows from the ISO and nix bootstraps gcc from source.
|
# this nothing flows from the ISO and nix bootstraps gcc from source.
|
||||||
NIXOS_INSTALL_OPTS=()
|
NIXOS_INSTALL_OPTS=()
|
||||||
|
OFFLINE=false
|
||||||
if ! timeout 3 bash -c '</dev/tcp/cache.nixos.org/443' 2>/dev/null; then
|
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."
|
info "No network — substituting from the ISO store only."
|
||||||
NIX_CONFIG+=$'\nsubstituters =\nextra-substituters = daemon?trusted=1\nbuilders ='
|
NIX_CONFIG+=$'\nsubstituters =\nextra-substituters = daemon?trusted=1\nbuilders ='
|
||||||
# Must ALSO go through nixos-install as a flag: it passes its own
|
# 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."
|
[[ -b "$TARGET_DISK" ]] || fail "$TARGET_DISK is not a block device."
|
||||||
info "Target: $TARGET_DISK"
|
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 ─────────────────────────────────────────────────────────
|
# ─── Encryption ─────────────────────────────────────────────────────────
|
||||||
section "Disk encryption"
|
section "Disk encryption"
|
||||||
|
|
||||||
@@ -117,7 +140,15 @@ section "Disk encryption"
|
|||||||
# logs you straight into the desktop (the passphrase already gates access).
|
# logs you straight into the desktop (the passphrase already gates access).
|
||||||
LUKS_PASSPHRASE=""
|
LUKS_PASSPHRASE=""
|
||||||
if [[ "$UNATTENDED" == "1" ]]; then
|
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
|
elif gum confirm --default=yes "Encrypt the disk with LUKS? (default — also enables passwordless desktop login)"; then
|
||||||
while true; do
|
while true; do
|
||||||
p1=$(gum input --password --placeholder "LUKS passphrase (min 8 chars)")
|
p1=$(gum input --password --placeholder "LUKS passphrase (min 8 chars)")
|
||||||
@@ -161,8 +192,8 @@ else
|
|||||||
warn "Invalid username (lowercase letters, digits, - and _)."
|
warn "Invalid username (lowercase letters, digits, - and _)."
|
||||||
done
|
done
|
||||||
while true; do
|
while true; do
|
||||||
PASSWORD=$(gum input --password --placeholder "password for $USERNAME")
|
PASSWORD=$(gum input --password --placeholder "password for $USERNAME (min 8 chars)")
|
||||||
[[ -n "$PASSWORD" ]] || { warn "Empty password."; continue; }
|
[[ ${#PASSWORD} -ge 8 ]] || { warn "Too short (min 8 chars)."; continue; }
|
||||||
p2=$(gum input --password --placeholder "repeat password")
|
p2=$(gum input --password --placeholder "repeat password")
|
||||||
[[ "$PASSWORD" == "$p2" ]] && break
|
[[ "$PASSWORD" == "$p2" ]] && break
|
||||||
warn "Passwords don't match."
|
warn "Passwords don't match."
|
||||||
@@ -210,6 +241,8 @@ section "Hardware detection"
|
|||||||
source "$SHARE/hardware-db.sh"
|
source "$SHARE/hardware-db.sh"
|
||||||
|
|
||||||
HW_PROFILES=()
|
HW_PROFILES=()
|
||||||
|
HW_NOMARCHY=() # NOMARCHY hardware.* assignments from detection
|
||||||
|
NPU_VENDOR="" # "intel" | "amd" if an NPU was detected (commented opt-in)
|
||||||
hw_mode="${NOMARCHY_HW:-auto}"
|
hw_mode="${NOMARCHY_HW:-auto}"
|
||||||
if [[ "$hw_mode" == "none" ]]; then
|
if [[ "$hw_mode" == "none" ]]; then
|
||||||
info "Hardware profiles skipped."
|
info "Hardware profiles skipped."
|
||||||
@@ -220,8 +253,10 @@ else
|
|||||||
if [[ -n "$detection" ]]; then
|
if [[ -n "$detection" ]]; then
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
case "$line" in
|
case "$line" in
|
||||||
MODULE\ *) HW_PROFILES+=("${line#MODULE }") ;;
|
MODULE\ *) HW_PROFILES+=("${line#MODULE }") ;;
|
||||||
DETAIL\ *) info "→ ${line#DETAIL }" ;;
|
NOMARCHY-NPU\ *) NPU_VENDOR="${line#NOMARCHY-NPU }" ;;
|
||||||
|
NOMARCHY\ *) HW_NOMARCHY+=("${line#NOMARCHY }") ;;
|
||||||
|
DETAIL\ *) info "→ ${line#DETAIL }" ;;
|
||||||
esac
|
esac
|
||||||
done <<< "$detection"
|
done <<< "$detection"
|
||||||
fi
|
fi
|
||||||
@@ -241,6 +276,14 @@ info "Profiles: ${HW_PROFILES[*]:-(none)}"
|
|||||||
# ─── Review & point of no return ────────────────────────────────────────
|
# ─── Review & point of no return ────────────────────────────────────────
|
||||||
section "Review"
|
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" \
|
gum style --border normal --padding "0 2" \
|
||||||
"Disk: $TARGET_DISK (WILL BE ERASED)" \
|
"Disk: $TARGET_DISK (WILL BE ERASED)" \
|
||||||
"Encryption: $([[ $WITH_LUKS == true ]] && echo "LUKS2 + desktop auto-login" || echo none)" \
|
"Encryption: $([[ $WITH_LUKS == true ]] && echo "LUKS2 + desktop auto-login" || echo none)" \
|
||||||
@@ -250,7 +293,7 @@ gum style --border normal --padding "0 2" \
|
|||||||
"Timezone: $TIMEZONE" \
|
"Timezone: $TIMEZONE" \
|
||||||
"Hardware: ${HW_PROFILES[*]:-none}" \
|
"Hardware: ${HW_PROFILES[*]:-none}" \
|
||||||
"Snapshots: snapper timeline on /" \
|
"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
|
if [[ "$UNATTENDED" != "1" ]]; then
|
||||||
typed=$(gum input --placeholder "type the disk name ($(basename "$TARGET_DISK")) to confirm the wipe")
|
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
|
unset LUKS_PASSPHRASE
|
||||||
fi
|
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)
|
disko_log=$(mktemp --suffix=.disko.log)
|
||||||
if ! disko --mode destroy,format,mount --yes-wipe-all-disks \
|
if ! disko --mode destroy,format,mount --yes-wipe-all-disks \
|
||||||
--argstr mainDrive "$TARGET_DISK" \
|
--argstr mainDrive "$TARGET_DISK" \
|
||||||
--arg withLuks "$WITH_LUKS" \
|
--arg withLuks "$WITH_LUKS" \
|
||||||
--argstr swapSize "${SWAP_GB}G" \
|
--argstr swapSize "$DISKO_SWAP_SIZE" \
|
||||||
"$SHARE/disko-config.nix" >"$disko_log" 2>&1; then
|
"$SHARE/disko-config.nix" >"$disko_log" 2>&1; then
|
||||||
tail -n 30 "$disko_log"
|
tail -n 30 "$disko_log"
|
||||||
fail "disko failed — full log: $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"
|
success "Disk partitioned and mounted at /mnt"
|
||||||
|
|
||||||
# Hibernation plumbing: the swapfile's physical offset goes into the
|
# Hibernation plumbing: the swapfile's physical offset goes into the
|
||||||
# kernel cmdline. Deactivate swap first so nixos-generate-config doesn't
|
# kernel cmdline (patched into system.nix). Deactivate swap first so
|
||||||
# also emit a swapDevices entry (we write our own, with resume wiring).
|
# nixos-generate-config doesn't also emit a swapDevices entry.
|
||||||
RESUME_CONFIG=""
|
resume_offset=""
|
||||||
|
root_uuid=""
|
||||||
if [[ "$SWAP_GB" != "0" ]]; then
|
if [[ "$SWAP_GB" != "0" ]]; then
|
||||||
swapoff -a 2>/dev/null || true
|
swapoff -a 2>/dev/null || true
|
||||||
resume_offset=$(btrfs inspect-internal map-swapfile -r /mnt/swap/swapfile)
|
resume_offset=$(btrfs inspect-internal map-swapfile -r /mnt/swap/swapfile)
|
||||||
root_uuid=$(findmnt -no UUID /mnt)
|
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)"
|
success "Swapfile created (resume offset $resume_offset)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -345,133 +389,100 @@ nixos-generate-config --root /mnt
|
|||||||
mv /mnt/etc/nixos/hardware-configuration.nix "$FLAKE_DIR/"
|
mv /mnt/etc/nixos/hardware-configuration.nix "$FLAKE_DIR/"
|
||||||
rm -rf /mnt/etc/nixos
|
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
|
# Detected hardware → flags for the patcher (safe defaults active).
|
||||||
# keyboard layout reaches the Hyprland session — standalone HM cannot
|
has_intel=false; has_amd=false; has_fp=false
|
||||||
# read system.nix.
|
intel_guc_off=false; has_camera_ir=false
|
||||||
cat > "$FLAKE_DIR/home.nix" <<EOF
|
if [[ ${#HW_NOMARCHY[@]} -gt 0 ]]; then
|
||||||
# Your user environment. The Nomarchy desktop (Hyprland, Waybar,
|
for nm in "${HW_NOMARCHY[@]}"; do
|
||||||
# Ghostty, theming engine, Stylix) comes from homeModules.nomarchy;
|
case "$nm" in
|
||||||
# tune it via the nomarchy.* options, add your own packages and
|
hardware.intel.enable=true) has_intel=true ;;
|
||||||
# programs below.
|
hardware.intel.guc=false) intel_guc_off=true ;;
|
||||||
{ pkgs, ... }:
|
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
|
||||||
|
|
||||||
{
|
# JSON for patch-template.py (stdin). Hardware profiles as a JSON array.
|
||||||
# Keyboard for the desktop session; console + LUKS prompt get the
|
hw_json="["
|
||||||
# same layout from system.nix (xkb + console.useXkbConfig).
|
first=1
|
||||||
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=""
|
|
||||||
for p in "${HW_PROFILES[@]:-}"; do
|
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
|
done
|
||||||
|
hw_json+="]"
|
||||||
|
|
||||||
cat > "$FLAKE_DIR/flake.nix" <<EOF
|
resume_json="null"
|
||||||
{
|
root_uuid_json="null"
|
||||||
description = "$HOSTNAME_ — my Nomarchy machine";
|
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())))')
|
||||||
# The only input. nixpkgs, home-manager etc. come pinned through it —
|
root_uuid_json=$(printf '%s' "$root_uuid" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().rstrip("\n")))')
|
||||||
# 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
|
|
||||||
)
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Laptop power: power-profiles-daemon ships by default; mark this a laptop
|
python3 "$SHARE/patch-template.py" "$FLAKE_DIR" <<PYJSON
|
||||||
# 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, ... }:
|
|
||||||
|
|
||||||
{
|
{
|
||||||
boot.loader.systemd-boot.enable = true;
|
"hostname": $(printf '%s' "$HOSTNAME_" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().rstrip("\n")))'),
|
||||||
boot.loader.efi.canTouchEfiVariables = true;
|
"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")))'),
|
||||||
networking.hostName = "$HOSTNAME_";
|
"locale": $(printf '%s' "$LOCALE" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().rstrip("\n")))'),
|
||||||
time.timeZone = "$TIMEZONE";
|
"keyboardLayout": $(printf '%s' "$KB_LAYOUT" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().rstrip("\n")))'),
|
||||||
i18n.defaultLocale = "$LOCALE";
|
"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")))'),
|
||||||
# One keyboard layout everywhere: xkb is the source of truth, and the
|
"autoLogin": $([[ $WITH_LUKS == true ]] && echo true || echo false),
|
||||||
# distro defaults (console.useXkbConfig + the systemd initrd, set in
|
"laptop": $is_laptop,
|
||||||
# the nomarchy module) derive the virtual console and the LUKS
|
"thermald": $thermald,
|
||||||
# passphrase prompt from it — so only the chosen layout is written here.
|
"hardwareProfiles": $hw_json,
|
||||||
# The Hyprland session reads the same layout from nomarchy.keyboard.* in
|
"hardware": {
|
||||||
# home.nix.
|
"intel": $has_intel,
|
||||||
services.xserver.xkb.layout = "$KB_LAYOUT";
|
"intelGucOff": $intel_guc_off,
|
||||||
services.xserver.xkb.variant = "$KB_VARIANT";
|
"amd": $has_amd,
|
||||||
|
"fingerprint": $has_fp,
|
||||||
# Your login user — \`username\` flows in from flake.nix automatically.
|
"cameraIr": $has_camera_ir,
|
||||||
users.users.\${username} = {
|
"nvidia": $has_nvidia,
|
||||||
isNormalUser = true;
|
"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)
|
||||||
extraGroups = [ "wheel" "networkmanager" "video" "input" ];
|
},
|
||||||
initialHashedPassword = "$HASHED_PASSWORD";
|
"resumeOffset": $resume_json,
|
||||||
};
|
"rootUuid": $root_uuid_json
|
||||||
$AUTOLOGIN_CONFIG$POWER_CONFIG$RESUME_CONFIG
|
|
||||||
# Hourly/daily BTRFS timeline snapshots + nixos-rebuild-snap.
|
|
||||||
nomarchy.system.snapper.enable = true;
|
|
||||||
|
|
||||||
system.stateVersion = "26.05";
|
|
||||||
}
|
}
|
||||||
EOF
|
PYJSON
|
||||||
|
|
||||||
# The flake.lock: composed offline — nomarchy is path-locked to the very
|
# The flake.lock: composed offline — nomarchy is path-locked to the very
|
||||||
# source the ISO carries (original stays the forge URL, so a later
|
# source the ISO carries (original stays the forge URL, so a later
|
||||||
# `nix flake update` on the installed machine re-resolves normally).
|
# `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
|
"$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."
|
warn "Offline lock composition failed — resolving over the network."
|
||||||
(cd "$FLAKE_DIR" && nix --extra-experimental-features "nix-command flakes" flake lock)
|
(cd "$FLAKE_DIR" && nix --extra-experimental-features "nix-command flakes" flake lock)
|
||||||
fi
|
fi
|
||||||
@@ -485,13 +496,9 @@ fi
|
|||||||
commit -qm "Initial Nomarchy configuration"
|
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
|
# 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"
|
chmod -R u+w "$FLAKE_DIR"
|
||||||
|
|
||||||
# /etc/nixos on the installed system points at the user-owned flake.
|
# /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"
|
nixos-install --no-root-passwd "${NIXOS_INSTALL_OPTS[@]}" --flake "path:$FLAKE_DIR#default"
|
||||||
success "System installed (bootloader in place)"
|
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
|
# Pre-activate the Home Manager generation so the FIRST boot lands in the
|
||||||
# fully themed desktop, not bare Hyprland. Best-effort: a failure here
|
# fully themed desktop, not bare Hyprland. Best-effort: a failure here
|
||||||
# only costs the user one `home-manager switch` after logging in.
|
# 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
|
set -ex
|
||||||
exec > /var/log/nomarchy-hm-preactivate.log 2>&1
|
exec > /var/log/nomarchy-hm-preactivate.log 2>&1
|
||||||
export PATH=/run/current-system/sw/bin:\$PATH
|
export PATH=/run/current-system/sw/bin:\$PATH
|
||||||
|
# Keep root's nix state in /root, not a stray /home/nomarchy on the target.
|
||||||
|
export HOME=/root
|
||||||
# Normally pre-built in the live env and copied over; the in-chroot
|
# Normally pre-built in the live env and copied over; the in-chroot
|
||||||
# build (default substituters — the live-side build already proved the
|
# build (default substituters — the live-side build already proved the
|
||||||
# no-network case) is a last-resort fallback.
|
# no-network case) is a last-resort fallback.
|
||||||
@@ -595,13 +615,39 @@ sleep 2
|
|||||||
runuser -u "$USERNAME" -- bash -lc \
|
runuser -u "$USERNAME" -- bash -lc \
|
||||||
"USER=$USERNAME HOME=/home/$USERNAME NIX_REMOTE=daemon HOME_MANAGER_BACKUP_EXT=bak \$out/activate"
|
"USER=$USERNAME HOME=/home/$USERNAME NIX_REMOTE=daemon HOME_MANAGER_BACKUP_EXT=bak \$out/activate"
|
||||||
EOF
|
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"
|
success "Desktop pre-activated — first boot is fully themed"
|
||||||
else
|
else
|
||||||
warn "Desktop pre-activation failed (see /var/log/nomarchy-hm-preactivate.log"
|
warn "Desktop pre-activation failed (see /var/log/nomarchy-hm-preactivate.log"
|
||||||
warn "on the installed system); after first login run:"
|
warn "on the installed system); after first login run:"
|
||||||
warn " home-manager switch --flake ~/.nomarchy -b bak"
|
warn " home-manager switch --flake ~/.nomarchy -b bak"
|
||||||
tail -n 5 /mnt/var/log/nomarchy-hm-preactivate.log 2>/dev/null || true
|
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
|
fi
|
||||||
rm -f /mnt/root/nomarchy-hm-activate.sh
|
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()
|
||||||
133
pkgs/nomarchy-lifecycle/default.nix
Normal file
133
pkgs/nomarchy-lifecycle/default.nix
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Day-to-day lifecycle CLIs for a Nomarchy machine flake (~/.nomarchy).
|
||||||
|
# Installed on both NixOS (systemPackages) and Home Manager so a home
|
||||||
|
# switch can refresh them without waiting for a full system rebuild —
|
||||||
|
# otherwise a broken nomarchy-pull can never update itself.
|
||||||
|
{ lib, writeShellScriptBin, nvd, jq, symlinkJoin }:
|
||||||
|
|
||||||
|
let
|
||||||
|
# Shared preamble: refuse root, resolve flake path.
|
||||||
|
preamble = name: ''
|
||||||
|
set -euo pipefail
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
|
echo "${name}: run as your normal user" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
|
||||||
|
'';
|
||||||
|
|
||||||
|
nomarchy-pull = writeShellScriptBin "nomarchy-pull" ''
|
||||||
|
${preamble "nomarchy-pull"}
|
||||||
|
if [ ! -e "$flake/flake.nix" ]; then
|
||||||
|
echo "nomarchy-pull: no flake.nix at $flake" >&2
|
||||||
|
echo " Set NOMARCHY_PATH or put your machine flake in ~/.nomarchy." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Optional: pull *your* machine config only if this checkout tracks a
|
||||||
|
# remote. Distro updates come from the nomarchy flake *input* below —
|
||||||
|
# many installs have no upstream on ~/.nomarchy (local auto-commits).
|
||||||
|
if [ -d "$flake/.git" ] \
|
||||||
|
&& git -C "$flake" rev-parse --abbrev-ref '@{u}' >/dev/null 2>&1; then
|
||||||
|
echo "nomarchy-pull: git pull --ff-only (machine flake tracks upstream)"
|
||||||
|
git -C "$flake" pull --ff-only
|
||||||
|
elif [ -d "$flake/.git" ]; then
|
||||||
|
echo "nomarchy-pull: machine flake has no upstream branch — skipping git pull"
|
||||||
|
echo " (normal for a local-only ~/.nomarchy; distro updates use flake inputs)"
|
||||||
|
fi
|
||||||
|
echo "nomarchy-pull: nix flake update in $flake"
|
||||||
|
nix flake update --flake "$flake"
|
||||||
|
nom=$(nix flake metadata "$flake" --json 2>/dev/null \
|
||||||
|
| ${jq}/bin/jq -r '
|
||||||
|
.locks.nodes.nomarchy.locked
|
||||||
|
| if . == null then empty
|
||||||
|
elif .rev then "nomarchy @ \(.rev[0:12])"
|
||||||
|
else empty end
|
||||||
|
' 2>/dev/null || true)
|
||||||
|
[ -n "''${nom:-}" ] && echo "nomarchy-pull: $nom"
|
||||||
|
echo "nomarchy-pull: done — next: nomarchy-rebuild then nomarchy-home"
|
||||||
|
'';
|
||||||
|
|
||||||
|
nomarchy-rebuild = writeShellScriptBin "nomarchy-rebuild" ''
|
||||||
|
${preamble "nomarchy-rebuild"}
|
||||||
|
if [ ! -e "$flake/flake.nix" ]; then
|
||||||
|
echo "nomarchy-rebuild: no flake.nix at $flake" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
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 env NOMARCHY_PATH="$flake" 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 "nomarchy-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
|
||||||
|
after=$(readlink -f /run/current-system)
|
||||||
|
if [ "$before" = "$after" ]; then
|
||||||
|
echo "nomarchy-rebuild: no changes — the system is identical."
|
||||||
|
else
|
||||||
|
echo "nomarchy-rebuild: what changed:"
|
||||||
|
${nvd}/bin/nvd diff "$before" "$after" || true
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
nomarchy-home = writeShellScriptBin "nomarchy-home" ''
|
||||||
|
${preamble "nomarchy-home"}
|
||||||
|
if [ ! -e "$flake/flake.nix" ]; then
|
||||||
|
echo "nomarchy-home: no flake.nix at $flake" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log=$(mktemp)
|
||||||
|
trap 'rm -f "$log"' EXIT
|
||||||
|
set +e
|
||||||
|
home-manager switch --flake "$flake" "$@" 2>&1 | tee "$log"
|
||||||
|
rc=''${PIPESTATUS[0]}
|
||||||
|
set -e
|
||||||
|
if [ "$rc" -ne 0 ]; then
|
||||||
|
echo
|
||||||
|
echo "nomarchy-home: 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
|
||||||
|
echo "nomarchy-home: desktop applied."
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Legacy aliases
|
||||||
|
sys-update = writeShellScriptBin "sys-update" ''
|
||||||
|
nomarchy-pull && nomarchy-rebuild "$@"
|
||||||
|
'';
|
||||||
|
sys-rebuild = writeShellScriptBin "sys-rebuild" ''
|
||||||
|
exec nomarchy-rebuild "$@"
|
||||||
|
'';
|
||||||
|
home-update = writeShellScriptBin "home-update" ''
|
||||||
|
exec nomarchy-home "$@"
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
symlinkJoin {
|
||||||
|
name = "nomarchy-lifecycle";
|
||||||
|
paths = [
|
||||||
|
nomarchy-pull
|
||||||
|
nomarchy-rebuild
|
||||||
|
nomarchy-home
|
||||||
|
sys-update
|
||||||
|
sys-rebuild
|
||||||
|
home-update
|
||||||
|
];
|
||||||
|
meta = {
|
||||||
|
description = "Nomarchy machine-flake lifecycle: pull, rebuild, home";
|
||||||
|
mainProgram = "nomarchy-pull";
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@ Commands:
|
|||||||
apply <name|file.json> merge a preset into the state + rebuild
|
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
|
set <dotted.path> <value> tweak one key (e.g. `set ui.gapsOut 16`) + rebuild
|
||||||
get [dotted.path] print the current state (or one key)
|
get [dotted.path] print the current state (or one key)
|
||||||
|
auto [--which] apply the day/night theme for the current time
|
||||||
|
validate check the state file against the schema (read-only)
|
||||||
wallpaper apply the current wallpaper via swww
|
wallpaper apply the current wallpaper via swww
|
||||||
bg [next|auto] cycle the theme's wallpapers (instant, no rebuild)
|
bg [next|auto] cycle the theme's wallpapers (instant, no rebuild)
|
||||||
"""
|
"""
|
||||||
@@ -25,6 +27,7 @@ Commands:
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -90,11 +93,23 @@ def load_state(path: Path = STATE_FILE) -> dict:
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
die(f"state file not found: {path} (set $NOMARCHY_PATH to your flake checkout)")
|
die(f"state file not found: {path} (set $NOMARCHY_PATH to your flake checkout)")
|
||||||
except json.JSONDecodeError as e:
|
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:
|
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)
|
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
fd, tmp = tempfile.mkstemp(dir=STATE_FILE.parent, prefix=".theme-state.", suffix=".json")
|
fd, tmp = tempfile.mkstemp(dir=STATE_FILE.parent, prefix=".theme-state.", suffix=".json")
|
||||||
try:
|
try:
|
||||||
@@ -116,6 +131,39 @@ def write_state(state: dict) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def auto_commit_enabled(state: dict) -> bool:
|
||||||
|
return bool((state.get("settings") or {}).get("autoCommit"))
|
||||||
|
|
||||||
|
|
||||||
|
def auto_commit(message: str) -> None:
|
||||||
|
"""Opt-in (settings.autoCommit): commit theme-state.json — and nothing
|
||||||
|
else — after a mutation, so settings history is `git log`. The pathspec
|
||||||
|
keeps unrelated dirty files out of the commit; a missing git identity
|
||||||
|
falls back to a Nomarchy one so a fresh machine never errors. Callers
|
||||||
|
fire this when the flag is on before OR after the write, so the
|
||||||
|
disable-toggle itself lands in history instead of staying dirty.
|
||||||
|
`bg` is deliberately excluded (runtime wallpaper churn); the wallpaper
|
||||||
|
path rides along with the next apply/set commit."""
|
||||||
|
if not (FLAKE_DIR / ".git").exists() or shutil.which("git") is None:
|
||||||
|
return
|
||||||
|
git = ["git", "-C", str(FLAKE_DIR)]
|
||||||
|
# No-op when the file already matches HEAD (a `set` to the same value).
|
||||||
|
# On a repo with no commits yet this diff errors — then just commit.
|
||||||
|
if subprocess.run(git + ["diff", "--quiet", "HEAD", "--", "theme-state.json"],
|
||||||
|
capture_output=True).returncode == 0:
|
||||||
|
return
|
||||||
|
if not subprocess.run(git + ["config", "user.email"],
|
||||||
|
capture_output=True, text=True).stdout.strip():
|
||||||
|
git += ["-c", "user.name=Nomarchy", "-c", "user.email=nomarchy@localhost"]
|
||||||
|
result = subprocess.run(
|
||||||
|
git + ["commit", "--quiet", "-m", message, "--", "theme-state.json"],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
log(f"auto-committed: {message}")
|
||||||
|
else:
|
||||||
|
log(f"auto-commit skipped: {(result.stderr or result.stdout).strip()}")
|
||||||
|
|
||||||
|
|
||||||
def deep_merge(base: dict, override: dict) -> dict:
|
def deep_merge(base: dict, override: dict) -> dict:
|
||||||
out = dict(base)
|
out = dict(base)
|
||||||
for k, v in override.items():
|
for k, v in override.items():
|
||||||
@@ -126,6 +174,152 @@ def deep_merge(base: dict, override: dict) -> dict:
|
|||||||
return out
|
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 ────────────────────────────────────────────────────
|
# ─── Font verification ────────────────────────────────────────────────────
|
||||||
|
|
||||||
def check_fonts(state: dict) -> None:
|
def check_fonts(state: dict) -> None:
|
||||||
@@ -165,12 +359,35 @@ def run_switch() -> None:
|
|||||||
# Persistent (timeout 0): stays up for the whole rebuild — replaced
|
# Persistent (timeout 0): stays up for the whole rebuild — replaced
|
||||||
# in place by the success/failure notification below — so a multi-
|
# in place by the success/failure notification below — so a multi-
|
||||||
# minute switch never looks like it silently failed.
|
# minute switch never looks like it silently failed.
|
||||||
notify("Applying theme — rebuilding the desktop…", persistent=True)
|
notify("Applying changes — rebuilding the desktop…", persistent=True)
|
||||||
result = subprocess.run(argv) # stream output to the caller's terminal
|
result = subprocess.run(argv) # stream output to the caller's terminal
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
notify("Theme rebuild FAILED — see terminal / journal", urgency="critical")
|
# BACKLOG #56: point at doctor, not a silent wall of Nix noise.
|
||||||
die("rebuild failed (state file already updated; fix and re-run)")
|
notify(
|
||||||
notify("Theme applied ✓")
|
"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) ────────────────────────────────────────────
|
# ─── Theme assets (wallpapers) ────────────────────────────────────────────
|
||||||
@@ -250,24 +467,85 @@ def cmd_list(_args) -> None:
|
|||||||
print("\n".join(names))
|
print("\n".join(names))
|
||||||
|
|
||||||
|
|
||||||
def cmd_apply(args) -> None:
|
def apply_named(theme: str, no_switch: bool = False) -> None:
|
||||||
candidate = Path(args.theme).expanduser()
|
"""Merge a preset (name or JSON file) into the state and rebuild. Shared
|
||||||
preset_path = candidate if candidate.is_file() else find_preset(args.theme)
|
by `apply` and `auto` so the day/night switch is the *same* one engine."""
|
||||||
|
candidate = Path(theme).expanduser()
|
||||||
|
preset_path = candidate if candidate.is_file() else find_preset(theme)
|
||||||
if preset_path is None:
|
if preset_path is None:
|
||||||
die(f"unknown theme '{args.theme}' (try `nomarchy-theme-sync list`)")
|
die(f"unknown theme '{theme}' (try `nomarchy-theme-sync list`)")
|
||||||
|
|
||||||
preset = json.loads(preset_path.read_text())
|
preset = json.loads(preset_path.read_text())
|
||||||
# Merge over current state: presets define palette/name/wallpaper,
|
# Merge over current state. Presets carry a full appearance block —
|
||||||
# user tweaks (gaps, fonts) outside the preset survive.
|
# palette, border, fonts and ui — so a switch always replaces the last
|
||||||
state = deep_merge(load_state(), preset)
|
# 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)
|
write_state(state)
|
||||||
log(f"theme: {state.get('name', args.theme)}")
|
if auto_commit_enabled(old) or auto_commit_enabled(state):
|
||||||
|
auto_commit(f"nomarchy: apply theme {state.get('name', theme)}")
|
||||||
|
log(f"theme: {state.get('name', theme)}")
|
||||||
check_fonts(state)
|
check_fonts(state)
|
||||||
if not args.no_switch:
|
if not no_switch:
|
||||||
run_switch()
|
run_switch()
|
||||||
apply_wallpaper(state)
|
apply_wallpaper(state)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_apply(args) -> None:
|
||||||
|
apply_named(args.theme, no_switch=args.no_switch)
|
||||||
|
|
||||||
|
|
||||||
|
def _hhmm_to_min(s: str, default: str) -> int:
|
||||||
|
"""'HH:MM' → minutes since midnight; falls back to `default` on garbage."""
|
||||||
|
for candidate in (s, default):
|
||||||
|
try:
|
||||||
|
h, m = str(candidate).split(":")
|
||||||
|
return int(h) * 60 + int(m)
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
continue
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_auto(args) -> None:
|
||||||
|
"""Apply the day or night theme for the current local time, per
|
||||||
|
settings.autoTheme = {enable, day, night, sunrise, sunset}. Slice 1 of
|
||||||
|
the auto time-of-day pair (BACKLOG #79): the timer (slice 2) and menu
|
||||||
|
(slice 3) drive this; it can also be run by hand or from a user cron."""
|
||||||
|
state = load_state()
|
||||||
|
at = (state.get("settings") or {}).get("autoTheme") or {}
|
||||||
|
if not at.get("enable"):
|
||||||
|
if not args.which:
|
||||||
|
log("auto-theme: off (enable via settings.autoTheme.enable)")
|
||||||
|
return
|
||||||
|
day, night = at.get("day"), at.get("night")
|
||||||
|
if not day or not night:
|
||||||
|
die("auto-theme needs settings.autoTheme.day and .night (theme slugs)")
|
||||||
|
|
||||||
|
sunrise = _hhmm_to_min(at.get("sunrise", "07:00"), "07:00")
|
||||||
|
sunset = _hhmm_to_min(at.get("sunset", "20:00"), "20:00")
|
||||||
|
now = time.localtime()
|
||||||
|
mins = now.tm_hour * 60 + now.tm_min
|
||||||
|
# Daytime is the span between sunrise and sunset. Also handle a
|
||||||
|
# wrap-around pair (sunrise after sunset → the day window crosses
|
||||||
|
# midnight), so an inverted schedule still switches both ways.
|
||||||
|
if sunrise <= sunset:
|
||||||
|
daytime = sunrise <= mins < sunset
|
||||||
|
else:
|
||||||
|
daytime = mins >= sunrise or mins < sunset
|
||||||
|
target = day if daytime else night
|
||||||
|
|
||||||
|
if args.which:
|
||||||
|
print(target)
|
||||||
|
return
|
||||||
|
if state.get("slug") == target and not args.force:
|
||||||
|
log(f"auto-theme: already on {target} ({'day' if daytime else 'night'})")
|
||||||
|
return
|
||||||
|
apply_named(target, no_switch=args.no_switch)
|
||||||
|
|
||||||
|
|
||||||
def cmd_set(args) -> None:
|
def cmd_set(args) -> None:
|
||||||
state = load_state()
|
state = load_state()
|
||||||
try:
|
try:
|
||||||
@@ -275,6 +553,7 @@ def cmd_set(args) -> None:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
value = args.value # bare string ("#7aa2f7", font names, paths)
|
value = args.value # bare string ("#7aa2f7", font names, paths)
|
||||||
|
|
||||||
|
was_on = auto_commit_enabled(state)
|
||||||
node = state
|
node = state
|
||||||
keys = args.path.split(".")
|
keys = args.path.split(".")
|
||||||
for key in keys[:-1]:
|
for key in keys[:-1]:
|
||||||
@@ -284,12 +563,18 @@ def cmd_set(args) -> None:
|
|||||||
node[keys[-1]] = value
|
node[keys[-1]] = value
|
||||||
|
|
||||||
write_state(state)
|
write_state(state)
|
||||||
|
if was_on or auto_commit_enabled(state):
|
||||||
|
auto_commit(f"nomarchy: set {args.path} = {json.dumps(value)}")
|
||||||
log(f"set {args.path} = {value!r}")
|
log(f"set {args.path} = {value!r}")
|
||||||
if args.path.startswith("fonts."):
|
if args.path.startswith("fonts."):
|
||||||
check_fonts(state)
|
check_fonts(state)
|
||||||
if not args.no_switch:
|
if not args.no_switch:
|
||||||
run_switch()
|
run_switch()
|
||||||
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:
|
def cmd_get(args) -> None:
|
||||||
@@ -301,7 +586,10 @@ def cmd_get(args) -> None:
|
|||||||
node = node[key]
|
node = node[key]
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
die(f"no such key: {args.path}")
|
die(f"no such key: {args.path}")
|
||||||
print(json.dumps(node, indent=2) if isinstance(node, (dict, list)) else node)
|
# Booleans print JSON-style (true/false, not Python's True/False) so
|
||||||
|
# shell consumers can compare against the same literal they `set`.
|
||||||
|
print(json.dumps(node, indent=2)
|
||||||
|
if isinstance(node, (dict, list, bool)) else node)
|
||||||
else:
|
else:
|
||||||
print(json.dumps(state, indent=2))
|
print(json.dumps(state, indent=2))
|
||||||
|
|
||||||
@@ -357,6 +645,14 @@ def main() -> None:
|
|||||||
p.add_argument("path", nargs="?")
|
p.add_argument("path", nargs="?")
|
||||||
p.set_defaults(func=cmd_get)
|
p.set_defaults(func=cmd_get)
|
||||||
|
|
||||||
|
p = sub.add_parser("auto", help="apply the day/night theme for the current time (settings.autoTheme)")
|
||||||
|
p.add_argument("--which", action="store_true", help="print the theme that WOULD apply, don't switch")
|
||||||
|
p.add_argument("--force", action="store_true", help="apply even if already on the target theme")
|
||||||
|
p.add_argument("--no-switch", action="store_true", help="write state only, skip the rebuild")
|
||||||
|
p.set_defaults(func=cmd_auto)
|
||||||
|
|
||||||
|
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)
|
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")
|
p = sub.add_parser("bg", help="wallpaper control: `bg next` cycles, `bg auto` resets")
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# My Nomarchy machine
|
# 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:
|
1. Overwrite the placeholder hardware config with the real one:
|
||||||
`nixos-generate-config --show-hardware-config > hardware-configuration.nix`
|
`nixos-generate-config --show-hardware-config > hardware-configuration.nix`
|
||||||
2. Set `flake.nix` up **once** (Nomarchy repo URL, your username, optionally
|
2. Set `flake.nix` up **once** (Nomarchy repo URL, your username, optionally
|
||||||
@@ -19,7 +25,7 @@
|
|||||||
Day-to-day:
|
Day-to-day:
|
||||||
|
|
||||||
```sh
|
```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 apply gruvbox # writes state + home-manager switch
|
||||||
nomarchy-theme-sync bg next # cycle wallpapers (instant, no rebuild)
|
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
|
# tested together upstream. This file is written once (by you or the
|
||||||
# installer) and never hand-edited afterwards; your machine lives in
|
# installer) and never hand-edited afterwards; your machine lives in
|
||||||
# system.nix and home.nix. v1 is the release branch.
|
# 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";
|
inputs.nomarchy.url = "git+https://git.bemagri.xyz/bernardo/nomarchy.git?ref=v1";
|
||||||
|
|
||||||
outputs = { nomarchy, ... }:
|
outputs = { nomarchy, ... }:
|
||||||
|
|||||||
@@ -2,24 +2,45 @@
|
|||||||
# Ghostty, theming engine, Stylix) comes from homeModules.nomarchy;
|
# Ghostty, theming engine, Stylix) comes from homeModules.nomarchy;
|
||||||
# tune it via the nomarchy.* options, add your own packages and
|
# tune it via the nomarchy.* options, add your own packages and
|
||||||
# programs below.
|
# programs below.
|
||||||
|
#
|
||||||
|
# Source of truth for install and flake-init: nomarchy-install copies this
|
||||||
|
# file and only patches keyboard layout/variant.
|
||||||
{ pkgs, ... }:
|
{ 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) ──────
|
# ── Overrides (the desktop is on by default; tweak or opt out) ──────
|
||||||
# nomarchy.terminal = "kitty"; # swap the default terminal
|
# nomarchy.terminal = "kitty"; # swap the default terminal
|
||||||
# nomarchy.waybar.enable = false; # bring your own bar
|
# nomarchy.waybar.enable = false; # bring your own bar
|
||||||
# nomarchy.stylix.enable = false; # opt out of GTK/Qt theming
|
# nomarchy.stylix.enable = false; # opt out of GTK/Qt theming
|
||||||
# nomarchy.keyboard.layout = "de"; # session keyboard (set the matching
|
#
|
||||||
# # services.xserver.xkb.layout in
|
# Icon pack: Papirus ships by default (auto Dark/Light by theme mode) and
|
||||||
# # system.nix too; the console + LUKS
|
# is the ONLY icon pack in your closure. To use another, set the `icons`
|
||||||
# # prompt follow it automatically)
|
# field in your theme-state.json to a theme name from a known pack — only
|
||||||
|
# that pack is then added (no bloat otherwise). Known packs: Papirus,
|
||||||
|
# Tela, Qogir, Reversal, Numix (see modules/home/theme.nix `iconPacks`).
|
||||||
|
# e.g. theme-state.json: "icons": "Tela-dark"
|
||||||
|
|
||||||
# ── Opt-in features — uncomment and tweak to enable ─────────────────
|
# ── Opt-in features — uncomment and tweak to enable ─────────────────
|
||||||
# nomarchy.nightlight = { # scheduled blue-light filter (hyprsunset)
|
# nomarchy.nightlight = { # scheduled blue-light filter (hyprsunset)
|
||||||
# enable = true;
|
# enable = true; # declarative opt-in (or just enable it from
|
||||||
|
# # the menu: System › Night light)
|
||||||
# temperature = 4000; # night warmth in K — lower is warmer
|
# temperature = 4000; # night warmth in K — lower is warmer
|
||||||
# sunset = "20:00";
|
# sunset = "20:00";
|
||||||
# sunrise = "07:00";
|
# sunrise = "07:00";
|
||||||
|
# latitude = "52.52"; # geo mode: set BOTH lat/long and sunrise/
|
||||||
|
# longitude = "13.40"; # sunset follow your location (wlsunset);
|
||||||
|
# # the fixed times above are then ignored
|
||||||
|
# };
|
||||||
|
#
|
||||||
|
# nomarchy.updates = { # passive update-awareness indicator + notification
|
||||||
|
# enable = true;
|
||||||
|
# interval = "daily"; # how often to check (systemd OnCalendar)
|
||||||
|
# flatpak = true; # also count Flatpak updates when flatpak is on
|
||||||
# };
|
# };
|
||||||
#
|
#
|
||||||
# nomarchy.monitors = [ # declarative per-output layout, hotplug-applied
|
# nomarchy.monitors = [ # declarative per-output layout, hotplug-applied
|
||||||
@@ -27,12 +48,33 @@
|
|||||||
# { name = "HDMI-A-1"; position = "auto-right"; }
|
# { name = "HDMI-A-1"; position = "auto-right"; }
|
||||||
# ]; # names from `hyprctl devices`
|
# ]; # 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
|
# nomarchy.keyboard.devices = { # a specific keyboard's own layout
|
||||||
# "keychron-keychron-k2" = { layout = "de"; };
|
# "keychron-keychron-k2" = { layout = "de"; };
|
||||||
# };
|
# };
|
||||||
#
|
#
|
||||||
# nomarchy.keyboard.layouts = [ "de" "fr" ]; # rofi-prompt for a layout when a
|
# nomarchy.keyboard.layouts = [ "de" "fr" ]; # rofi-prompt for a layout when a new
|
||||||
# # new keyboard connects + remember it
|
# # keyboard connects + remember it in the
|
||||||
|
# # flake state (graduates to .devices above)
|
||||||
|
|
||||||
# ── Application suite ───────────────────────────────────────────────
|
# ── Application suite ───────────────────────────────────────────────
|
||||||
# A starter complete-workstation set, installed for your user — yours to
|
# A starter complete-workstation set, installed for your user — yours to
|
||||||
@@ -49,15 +91,16 @@
|
|||||||
home.packages = with pkgs; [
|
home.packages = with pkgs; [
|
||||||
libreoffice-fresh # office suite (documents, sheets, slides)
|
libreoffice-fresh # office suite (documents, sheets, slides)
|
||||||
vscode # code editor (unfree; allowUnfree is on)
|
vscode # code editor (unfree; allowUnfree is on)
|
||||||
|
chromium # web browser (mime defaults → chromium-browser.desktop)
|
||||||
gimp # raster image editor
|
gimp # raster image editor
|
||||||
inkscape # vector graphics
|
inkscape # vector graphics
|
||||||
mpv # media player
|
mpv # media player
|
||||||
amberol # music player (local library; streaming → spotify below)
|
amberol # music player (local library; streaming → spotify below)
|
||||||
|
# pwvucontrol + calcurse ship with the Waybar module (volume right-click / clock)
|
||||||
|
|
||||||
# ── More apps — uncomment to add ─────────────────────────────────
|
# ── More apps — uncomment to add ─────────────────────────────────
|
||||||
# Web browsers
|
# Web browsers (chromium ships above — swap/delete freely)
|
||||||
# firefox
|
# firefox
|
||||||
# chromium
|
|
||||||
# ungoogled-chromium
|
# ungoogled-chromium
|
||||||
# brave # (unfree)
|
# brave # (unfree)
|
||||||
# google-chrome # (unfree)
|
# google-chrome # (unfree)
|
||||||
@@ -74,7 +117,7 @@
|
|||||||
|
|
||||||
# Office & documents
|
# Office & documents
|
||||||
# onlyoffice-desktopeditors # stronger MS-Office fidelity
|
# onlyoffice-desktopeditors # stronger MS-Office fidelity
|
||||||
# kdePackages.okular # PDF viewer
|
# kdePackages.okular # heavier PDF viewer (zathura ships by default)
|
||||||
# xournalpp # PDF annotation
|
# xournalpp # PDF annotation
|
||||||
# pdfarranger
|
# pdfarranger
|
||||||
# calibre # ebook library
|
# calibre # ebook library
|
||||||
@@ -105,6 +148,11 @@
|
|||||||
# kdePackages.kdenlive # video editor (pulls in KDE libs)
|
# kdePackages.kdenlive # video editor (pulls in KDE libs)
|
||||||
# handbrake # transcoder
|
# handbrake # transcoder
|
||||||
|
|
||||||
|
# Webcam tuning (rarely needed — the distro's IR-hide covers the
|
||||||
|
# common dual-sensor problem; these are for genuine tuning cases)
|
||||||
|
# cameractrls # GUI for exposure/focus/zoom, saves per-camera presets
|
||||||
|
# v4l-utils # v4l2-ctl CLI: list formats, poke controls directly
|
||||||
|
|
||||||
# Media
|
# Media
|
||||||
# vlc
|
# vlc
|
||||||
# spotify # (unfree)
|
# spotify # (unfree)
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
# The distro itself comes from nomarchy.nixosModules.nomarchy — override
|
# The distro itself comes from nomarchy.nixosModules.nomarchy — override
|
||||||
# any of its defaults here with plain NixOS options (they use mkDefault),
|
# any of its defaults here with plain NixOS options (they use mkDefault),
|
||||||
# or via the nomarchy.system.* toggles.
|
# 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, ... }:
|
{ pkgs, username, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -12,12 +15,28 @@
|
|||||||
time.timeZone = "UTC";
|
time.timeZone = "UTC";
|
||||||
i18n.defaultLocale = "en_US.UTF-8";
|
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.
|
# 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} = {
|
users.users.${username} = {
|
||||||
isNormalUser = true;
|
isNormalUser = true;
|
||||||
extraGroups = [ "wheel" "networkmanager" "video" "input" ];
|
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) ─────────────────────────────────
|
# ── Overrides (uncomment to change) ─────────────────────────────────
|
||||||
# nomarchy.system.greeter.enable = false; # bring your own login manager
|
# nomarchy.system.greeter.enable = false; # bring your own login manager
|
||||||
# environment.systemPackages = [ pkgs.htop ];
|
# environment.systemPackages = [ pkgs.htop ];
|
||||||
@@ -31,7 +50,29 @@
|
|||||||
# thermal.enable = true; # thermald (Intel CPUs)
|
# thermal.enable = true; # thermald (Intel CPUs)
|
||||||
# };
|
# };
|
||||||
#
|
#
|
||||||
# nomarchy.services.tailscale.enable = true; # mesh VPN — then `sudo tailscale up`
|
# nomarchy.system.autoTimezone.enable = true; # clock follows your location
|
||||||
|
# # (geoclue); travelling updates the
|
||||||
|
# # time on its own. Unsets time.timeZone
|
||||||
|
# # above. Easier toggled from System menu.
|
||||||
|
#
|
||||||
|
# nomarchy.hardware = { # enablement above nixos-hardware (installer-detected)
|
||||||
|
# intel.enable = true; # GuC/HuC firmware; installer sets this on Intel
|
||||||
|
# intel.computeRuntime = true; # OpenCL/oneVPL GPU compute (opt-in)
|
||||||
|
# amd.enable = true; # amd-pstate + radeonsi VA-API; installer-set on AMD
|
||||||
|
# amd.rocm.enable = true; # ROCm GPU compute (multi-GB, opt-in)
|
||||||
|
# amd.rocm.gfxOverride = "11.0.0"; # HSA override for an unlisted iGPU
|
||||||
|
# fingerprint.enable = true; # fprintd; installer-set when a reader is detected
|
||||||
|
# fingerprint.pam = true; # use the fingerprint for login + sudo
|
||||||
|
# npu.enable = true; # on-die NPU driver (experimental; userspace runtime BYO)
|
||||||
|
# latestKernel = true; # newest kernel for very-new hardware (drivers not yet in the default)
|
||||||
|
# camera.hideIrSensor = true; # dual-sensor webcam: hide the IR node so apps get the color cam
|
||||||
|
# # (installer-set when a paired RGB+IR webcam is detected)
|
||||||
|
# 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.syncthing.enable = true; # file sync — GUI at http://127.0.0.1:8384
|
||||||
# nomarchy.services.podman.enable = true; # rootless containers (docker → podman)
|
# nomarchy.services.podman.enable = true; # rootless containers (docker → podman)
|
||||||
# nomarchy.services.flatpak.enable = true; # Flatpak + the Flathub remote
|
# nomarchy.services.flatpak.enable = true; # Flatpak + the Flathub remote
|
||||||
|
|||||||
@@ -1,55 +1,61 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"name": "Tokyo Night",
|
"name": "Boreal",
|
||||||
"slug": "tokyo-night",
|
"slug": "boreal",
|
||||||
"mode": "dark",
|
"mode": "dark",
|
||||||
"wallpaper": "",
|
"wallpaper": "",
|
||||||
"colors": {
|
"colors": {
|
||||||
"base": "#1a1b26",
|
"base": "#21272F",
|
||||||
"mantle": "#161720",
|
"mantle": "#1A1F26",
|
||||||
"surface": "#32344a",
|
"surface": "#303A46",
|
||||||
"overlay": "#444b6a",
|
"overlay": "#404D5C",
|
||||||
"text": "#a9b1d6",
|
"text": "#D3DAE0",
|
||||||
"subtext": "#787c99",
|
"subtext": "#97A3B2",
|
||||||
"muted": "#444b6a",
|
"muted": "#5E6A78",
|
||||||
"accent": "#7aa2f7",
|
"accent": "#B79BE8",
|
||||||
"accentAlt": "#ad8ee6",
|
"accentAlt": "#86C7C0",
|
||||||
"good": "#9ece6a",
|
"good": "#A3CF88",
|
||||||
"warn": "#e0af68",
|
"warn": "#E6C384",
|
||||||
"bad": "#f7768e"
|
"bad": "#E2818A"
|
||||||
|
},
|
||||||
|
"border": {
|
||||||
|
"active": "accent",
|
||||||
|
"inactive": "overlay"
|
||||||
},
|
},
|
||||||
"ansi": [
|
"ansi": [
|
||||||
"#32344a",
|
"#2E3742",
|
||||||
"#f7768e",
|
"#E2818A",
|
||||||
"#9ece6a",
|
"#A3CF88",
|
||||||
"#e0af68",
|
"#E6C384",
|
||||||
"#7aa2f7",
|
"#86A9E0",
|
||||||
"#ad8ee6",
|
"#B79BE8",
|
||||||
"#449dab",
|
"#86C7C0",
|
||||||
"#787c99",
|
"#D3DAE0",
|
||||||
"#444b6a",
|
"#5E6A78",
|
||||||
"#ff7a93",
|
"#EC98A0",
|
||||||
"#b9f27c",
|
"#B4DB9C",
|
||||||
"#ff9e64",
|
"#EFCF98",
|
||||||
"#7da6ff",
|
"#9DBBEC",
|
||||||
"#bb9af7",
|
"#C6AEEF",
|
||||||
"#0db9d7",
|
"#99D6CF",
|
||||||
"#acb0d0"
|
"#E8EDF2"
|
||||||
],
|
],
|
||||||
"fonts": {
|
"fonts": {
|
||||||
"mono": "JetBrainsMono Nerd Font",
|
"mono": "GeistMono Nerd Font",
|
||||||
"ui": "Inter",
|
"ui": "Inter",
|
||||||
"size": 11
|
"size": 11
|
||||||
},
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
"gapsIn": 5,
|
"gapsIn": 6,
|
||||||
"gapsOut": 12,
|
"gapsOut": 14,
|
||||||
"borderSize": 2,
|
"borderSize": 3,
|
||||||
"rounding": 10,
|
"rounding": 12,
|
||||||
|
"iconSize": 44,
|
||||||
"activeOpacity": 1.0,
|
"activeOpacity": 1.0,
|
||||||
"inactiveOpacity": 0.95,
|
"inactiveOpacity": 0.93,
|
||||||
"terminalOpacity": 0.96,
|
"terminalOpacity": 0.9,
|
||||||
"blur": true,
|
"blur": true,
|
||||||
"shadow": true
|
"shadow": true
|
||||||
}
|
},
|
||||||
|
"settings": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,61 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"name": "Tokyo Night",
|
"name": "Boreal",
|
||||||
"slug": "tokyo-night",
|
"slug": "boreal",
|
||||||
"mode": "dark",
|
"mode": "dark",
|
||||||
"wallpaper": "",
|
"wallpaper": "",
|
||||||
"colors": {
|
"colors": {
|
||||||
"base": "#1a1b26",
|
"base": "#21272F",
|
||||||
"mantle": "#161720",
|
"mantle": "#1A1F26",
|
||||||
"surface": "#32344a",
|
"surface": "#303A46",
|
||||||
"overlay": "#444b6a",
|
"overlay": "#404D5C",
|
||||||
"text": "#a9b1d6",
|
"text": "#D3DAE0",
|
||||||
"subtext": "#787c99",
|
"subtext": "#97A3B2",
|
||||||
"muted": "#444b6a",
|
"muted": "#5E6A78",
|
||||||
"accent": "#7aa2f7",
|
"accent": "#B79BE8",
|
||||||
"accentAlt": "#ad8ee6",
|
"accentAlt": "#86C7C0",
|
||||||
"good": "#9ece6a",
|
"good": "#A3CF88",
|
||||||
"warn": "#e0af68",
|
"warn": "#E6C384",
|
||||||
"bad": "#f7768e"
|
"bad": "#E2818A"
|
||||||
},
|
},
|
||||||
"border": {
|
"border": {
|
||||||
"active": "accent",
|
"active": "accent",
|
||||||
"inactive": "overlay"
|
"inactive": "overlay"
|
||||||
},
|
},
|
||||||
"ansi": [
|
"ansi": [
|
||||||
"#32344a",
|
"#2E3742",
|
||||||
"#f7768e",
|
"#E2818A",
|
||||||
"#9ece6a",
|
"#A3CF88",
|
||||||
"#e0af68",
|
"#E6C384",
|
||||||
"#7aa2f7",
|
"#86A9E0",
|
||||||
"#ad8ee6",
|
"#B79BE8",
|
||||||
"#449dab",
|
"#86C7C0",
|
||||||
"#787c99",
|
"#D3DAE0",
|
||||||
"#444b6a",
|
"#5E6A78",
|
||||||
"#ff7a93",
|
"#EC98A0",
|
||||||
"#b9f27c",
|
"#B4DB9C",
|
||||||
"#ff9e64",
|
"#EFCF98",
|
||||||
"#7da6ff",
|
"#9DBBEC",
|
||||||
"#bb9af7",
|
"#C6AEEF",
|
||||||
"#0db9d7",
|
"#99D6CF",
|
||||||
"#acb0d0"
|
"#E8EDF2"
|
||||||
],
|
],
|
||||||
"fonts": {
|
"fonts": {
|
||||||
"mono": "JetBrainsMono Nerd Font",
|
"mono": "GeistMono Nerd Font",
|
||||||
"ui": "Inter",
|
"ui": "Inter",
|
||||||
"size": 11
|
"size": 11
|
||||||
},
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
"gapsIn": 5,
|
"gapsIn": 6,
|
||||||
"gapsOut": 12,
|
"gapsOut": 14,
|
||||||
"borderSize": 2,
|
"borderSize": 3,
|
||||||
"rounding": 10,
|
"rounding": 12,
|
||||||
|
"iconSize": 44,
|
||||||
"activeOpacity": 1.0,
|
"activeOpacity": 1.0,
|
||||||
"inactiveOpacity": 0.95,
|
"inactiveOpacity": 0.93,
|
||||||
"terminalOpacity": 0.96,
|
"terminalOpacity": 0.9,
|
||||||
"blur": true,
|
"blur": true,
|
||||||
"shadow": 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 |
25
themes/boreal/btop.theme
Normal file
25
themes/boreal/btop.theme
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
theme[main_bg]="#21272F"
|
||||||
|
theme[main_fg]="#D3DAE0"
|
||||||
|
theme[title]="#D3DAE0"
|
||||||
|
theme[hi_fg]="#B79BE8"
|
||||||
|
theme[selected_bg]="#404D5C"
|
||||||
|
theme[selected_fg]="#D3DAE0"
|
||||||
|
theme[inactive_fg]="#5E6A78"
|
||||||
|
theme[graph_text]="#97A3B2"
|
||||||
|
theme[meter_bg]="#303A46"
|
||||||
|
theme[proc_misc]="#86C7C0"
|
||||||
|
theme[cpu_box]="#B79BE8"
|
||||||
|
theme[mem_box]="#A3CF88"
|
||||||
|
theme[net_box]="#86C7C0"
|
||||||
|
theme[proc_box]="#303A46"
|
||||||
|
theme[div_line]="#404D5C"
|
||||||
|
theme[temp_start]="#A3CF88"
|
||||||
|
theme[temp_mid]="#E6C384"
|
||||||
|
theme[temp_end]="#E2818A"
|
||||||
|
theme[cpu_start]="#A3CF88"
|
||||||
|
theme[cpu_mid]="#E6C384"
|
||||||
|
theme[cpu_end]="#E2818A"
|
||||||
|
theme[free_start]="#A3CF88"
|
||||||
|
theme[used_start]="#E6C384"
|
||||||
|
theme[download_start]="#B79BE8"
|
||||||
|
theme[upload_start]="#86C7C0"
|
||||||
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;
|
||||||
|
}
|
||||||
169
themes/boreal/waybar.css
Normal file
169
themes/boreal/waybar.css
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
* {
|
||||||
|
/* Mono Nerd face for status glyphs (tight right bearings on the
|
||||||
|
proportional Symbols face make icon+percentage collide). */
|
||||||
|
font-family: "GeistMono Nerd Font", "Symbols Nerd Font Mono";
|
||||||
|
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 — unified time · date ─────────────────────────────────────*/
|
||||||
|
#clock {
|
||||||
|
color: @text;
|
||||||
|
font-weight: 600;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 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; }
|
||||||
174
themes/boreal/waybar.jsonc
Normal file
174
themes/boreal/waybar.jsonc
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
],
|
||||||
|
"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 · %a %d %b}",
|
||||||
|
"on-click": "nomarchy-calendar",
|
||||||
|
"tooltip": true,
|
||||||
|
"tooltip-format": "{:%A, %d %B %Y\n%Z (UTC%z)}"
|
||||||
|
},
|
||||||
|
"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": "",
|
"wallpaper": "",
|
||||||
"colors": {
|
"colors": {
|
||||||
"base": "#eff1f5",
|
"base": "#eff1f5",
|
||||||
"mantle": "#cbcdd0",
|
"mantle": "#e6e9ef",
|
||||||
"surface": "#bcc0cc",
|
"surface": "#bfc4d0",
|
||||||
"overlay": "#acb0be",
|
"overlay": "#acb0be",
|
||||||
"text": "#4c4f69",
|
"text": "#4c4f69",
|
||||||
"subtext": "#5c5f77",
|
"subtext": "#5c5f77",
|
||||||
"muted": "#acb0be",
|
"muted": "#8c8fa1",
|
||||||
"accent": "#1e66f5",
|
"accent": "#1e66f5",
|
||||||
"accentAlt": "#ea76cb",
|
"accentAlt": "#8839ef",
|
||||||
"good": "#40a02b",
|
"good": "#40a02b",
|
||||||
"warn": "#df8e1d",
|
"warn": "#d6860a",
|
||||||
"bad": "#d20f39"
|
"bad": "#d20f39"
|
||||||
},
|
},
|
||||||
"border": {
|
"border": {
|
||||||
"active": "accent",
|
"active": "accent",
|
||||||
"inactive": "overlay"
|
"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": [
|
"ansi": [
|
||||||
"#bcc0cc",
|
|
||||||
"#d20f39",
|
|
||||||
"#40a02b",
|
|
||||||
"#df8e1d",
|
|
||||||
"#1e66f5",
|
|
||||||
"#ea76cb",
|
|
||||||
"#179299",
|
|
||||||
"#5c5f77",
|
"#5c5f77",
|
||||||
"#acb0be",
|
|
||||||
"#d20f39",
|
"#d20f39",
|
||||||
"#40a02b",
|
"#40a02b",
|
||||||
"#df8e1d",
|
"#df8e1d",
|
||||||
"#1e66f5",
|
"#1e66f5",
|
||||||
"#ea76cb",
|
"#ea76cb",
|
||||||
"#179299",
|
"#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",
|
"active": "accent",
|
||||||
"inactive": "overlay"
|
"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": [
|
"ansi": [
|
||||||
"#45475a",
|
"#45475a",
|
||||||
"#f38ba8",
|
"#f38ba8",
|
||||||
|
|||||||
@@ -2,82 +2,82 @@
|
|||||||
theme[main_bg]="#1E1E2E"
|
theme[main_bg]="#1E1E2E"
|
||||||
|
|
||||||
# Main text color
|
# Main text color
|
||||||
theme[main_fg]="#c6d0f5"
|
theme[main_fg]="#cdd6f4"
|
||||||
|
|
||||||
# Title color for boxes
|
# Title color for boxes
|
||||||
theme[title]="#c6d0f5"
|
theme[title]="#cdd6f4"
|
||||||
|
|
||||||
# Highlight color for keyboard shortcuts
|
# Highlight color for keyboard shortcuts
|
||||||
theme[hi_fg]="#8caaee"
|
theme[hi_fg]="#89b4fa"
|
||||||
|
|
||||||
# Background color of selected item in processes box
|
# Background color of selected item in processes box
|
||||||
theme[selected_bg]="#51576d"
|
theme[selected_bg]="#585b70"
|
||||||
|
|
||||||
# Foreground color of selected item in processes box
|
# Foreground color of selected item in processes box
|
||||||
theme[selected_fg]="#8caaee"
|
theme[selected_fg]="#89b4fa"
|
||||||
|
|
||||||
# Color of inactive/disabled text
|
# 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
|
# 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
|
# 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
|
# 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
|
# CPU, Memory, Network, Proc box outline colors
|
||||||
theme[cpu_box]="#ca9ee6" #Mauve
|
theme[cpu_box]="#cba6f7" #Mauve
|
||||||
theme[mem_box]="#a6d189" #Green
|
theme[mem_box]="#a6e3a1" #Green
|
||||||
theme[net_box]="#ea999c" #Maroon
|
theme[net_box]="#eba0ac" #Maroon
|
||||||
theme[proc_box]="#8caaee" #Blue
|
theme[proc_box]="#89b4fa" #Blue
|
||||||
|
|
||||||
# Box divider line and small boxes line color
|
# Box divider line and small boxes line color
|
||||||
theme[div_line]="#737994"
|
theme[div_line]="#6c7086"
|
||||||
|
|
||||||
# Temperature graph color (Green -> Yellow -> Red)
|
# Temperature graph color (Green -> Yellow -> Red)
|
||||||
theme[temp_start]="#a6d189"
|
theme[temp_start]="#a6e3a1"
|
||||||
theme[temp_mid]="#e5c890"
|
theme[temp_mid]="#f9e2af"
|
||||||
theme[temp_end]="#e78284"
|
theme[temp_end]="#f38ba8"
|
||||||
|
|
||||||
# CPU graph colors (Teal -> Lavender)
|
# CPU graph colors (Teal -> Lavender)
|
||||||
theme[cpu_start]="#81c8be"
|
theme[cpu_start]="#94e2d5"
|
||||||
theme[cpu_mid]="#85c1dc"
|
theme[cpu_mid]="#74c7ec"
|
||||||
theme[cpu_end]="#babbf1"
|
theme[cpu_end]="#b4befe"
|
||||||
|
|
||||||
# Mem/Disk free meter (Mauve -> Lavender -> Blue)
|
# Mem/Disk free meter (Mauve -> Lavender -> Blue)
|
||||||
theme[free_start]="#ca9ee6"
|
theme[free_start]="#cba6f7"
|
||||||
theme[free_mid]="#babbf1"
|
theme[free_mid]="#b4befe"
|
||||||
theme[free_end]="#8caaee"
|
theme[free_end]="#89b4fa"
|
||||||
|
|
||||||
# Mem/Disk cached meter (Sapphire -> Lavender)
|
# Mem/Disk cached meter (Sapphire -> Lavender)
|
||||||
theme[cached_start]="#85c1dc"
|
theme[cached_start]="#74c7ec"
|
||||||
theme[cached_mid]="#8caaee"
|
theme[cached_mid]="#89b4fa"
|
||||||
theme[cached_end]="#babbf1"
|
theme[cached_end]="#b4befe"
|
||||||
|
|
||||||
# Mem/Disk available meter (Peach -> Red)
|
# Mem/Disk available meter (Peach -> Red)
|
||||||
theme[available_start]="#ef9f76"
|
theme[available_start]="#fab387"
|
||||||
theme[available_mid]="#ea999c"
|
theme[available_mid]="#eba0ac"
|
||||||
theme[available_end]="#e78284"
|
theme[available_end]="#f38ba8"
|
||||||
|
|
||||||
# Mem/Disk used meter (Green -> Sky)
|
# Mem/Disk used meter (Green -> Sky)
|
||||||
theme[used_start]="#a6d189"
|
theme[used_start]="#a6e3a1"
|
||||||
theme[used_mid]="#81c8be"
|
theme[used_mid]="#94e2d5"
|
||||||
theme[used_end]="#99d1db"
|
theme[used_end]="#89dceb"
|
||||||
|
|
||||||
# Download graph colors (Peach -> Red)
|
# Download graph colors (Peach -> Red)
|
||||||
theme[download_start]="#ef9f76"
|
theme[download_start]="#fab387"
|
||||||
theme[download_mid]="#ea999c"
|
theme[download_mid]="#eba0ac"
|
||||||
theme[download_end]="#e78284"
|
theme[download_end]="#f38ba8"
|
||||||
|
|
||||||
# Upload graph colors (Green -> Sky)
|
# Upload graph colors (Green -> Sky)
|
||||||
theme[upload_start]="#a6d189"
|
theme[upload_start]="#a6e3a1"
|
||||||
theme[upload_mid]="#81c8be"
|
theme[upload_mid]="#94e2d5"
|
||||||
theme[upload_end]="#99d1db"
|
theme[upload_end]="#89dceb"
|
||||||
|
|
||||||
# Process box color gradient for threads, mem and cpu usage (Sapphire -> Mauve)
|
# Process box color gradient for threads, mem and cpu usage (Sapphire -> Mauve)
|
||||||
theme[process_start]="#85c1dc"
|
theme[process_start]="#74c7ec"
|
||||||
theme[process_mid]="#babbf1"
|
theme[process_mid]="#b4befe"
|
||||||
theme[process_end]="#ca9ee6"
|
theme[process_end]="#cba6f7"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user