Merge remote-tracking branch 'origin/main' into main
This commit is contained in:
@@ -36,6 +36,8 @@ jobs:
|
|||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
env:
|
env:
|
||||||
NIX_CONFIG: |
|
NIX_CONFIG: |
|
||||||
|
max-jobs = 1
|
||||||
|
cores = 2
|
||||||
experimental-features = nix-command flakes
|
experimental-features = nix-command flakes
|
||||||
sandbox = false
|
sandbox = false
|
||||||
NIX_SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
|
NIX_SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
|
||||||
@@ -68,9 +70,12 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
id: update
|
id: update
|
||||||
|
|
||||||
- name: Gate — nix flake check (eval only)
|
- name: Gate — evaluate every output (eval tier, memory-bounded)
|
||||||
|
# Per-output processes, not one big `nix flake check --no-build`:
|
||||||
|
# the single-process walk peaks ~6 GB RSS and OOMs inside the
|
||||||
|
# runner's 2 GB container cap (details in tools/ci-eval.sh).
|
||||||
if: steps.update.outputs.changed == '1'
|
if: steps.update.outputs.changed == '1'
|
||||||
run: nix flake check --no-build
|
run: bash tools/ci-eval.sh
|
||||||
|
|
||||||
- name: Gate — V1 build (toplevel + home-manager)
|
- name: Gate — V1 build (toplevel + home-manager)
|
||||||
if: steps.update.outputs.changed == '1'
|
if: steps.update.outputs.changed == '1'
|
||||||
|
|||||||
@@ -57,11 +57,15 @@ jobs:
|
|||||||
curl -L "https://releases.nixos.org/nix/nix-${NIX_VERSION}/install" | sh -s -- --no-daemon
|
curl -L "https://releases.nixos.org/nix/nix-${NIX_VERSION}/install" | sh -s -- --no-daemon
|
||||||
echo "$HOME/.nix-profile/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.nix-profile/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: nix flake check (eval only)
|
- name: Evaluate every output (eval tier, memory-bounded)
|
||||||
# Full module-system evaluation of every output — both nixos
|
# Same coverage as `nix flake check --no-build` — both nixos
|
||||||
# configs, the home config, the checks.* derivations (instantiated,
|
# configs, the home config, the checks.* derivations (instantiated,
|
||||||
# not run) and the downstream template through lib.mkFlake.
|
# not run) and the downstream template through lib.mkFlake — but
|
||||||
run: nix flake check --no-build
|
# ONE output per nix process. A single process walking everything
|
||||||
|
# peaks at ~6.0 GB RSS (measured 2026-07-12): it OOM'd the 4 GB
|
||||||
|
# VPS itself, and can never fit this runner's 2 GB container cap.
|
||||||
|
# Per-output processes cap at the largest single output (~1 GB).
|
||||||
|
run: bash tools/ci-eval.sh
|
||||||
|
|
||||||
- name: Python syntax (all tracked scripts)
|
- name: Python syntax (all tracked scripts)
|
||||||
# Every tracked *.py — theme-sync, the installer composers
|
# Every tracked *.py — theme-sync, the installer composers
|
||||||
|
|||||||
@@ -194,7 +194,10 @@ nomarchy-what-changed # plain-language last rebuild (system + desktop; --summar
|
|||||||
`~/.nomarchy` is **your machine flake** (`system.nix` / `home.nix`). Newer
|
`~/.nomarchy` is **your machine flake** (`system.nix` / `home.nix`). Newer
|
||||||
Nomarchy code arrives when `flake.lock` updates the `nomarchy` input
|
Nomarchy code arrives when `flake.lock` updates the `nomarchy` input
|
||||||
(`nomarchy-pull`), not by `git pull` of that directory (many installs have
|
(`nomarchy-pull`), not by `git pull` of that directory (many installs have
|
||||||
no remote there — only local commits from auto-commit).
|
no remote there — only local commits from auto-commit). With auto-commit
|
||||||
|
on (menu: **System › Auto-commit**), all three commands above also sweep
|
||||||
|
any pending hand edits into a commit before they switch, so
|
||||||
|
`git -C ~/.nomarchy log` mirrors your generations.
|
||||||
|
|
||||||
| When | Run |
|
| When | Run |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -242,6 +245,7 @@ two tables below are split along exactly that line.
|
|||||||
| `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.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.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.dockAudio.enable` | `true` | Default audio output follows dock/monitor sinks (HDMI/DP/USB) on hotplug, with a toast; EasyEffects follows along; unplug falls back to built-in |
|
||||||
| `nomarchy.firstBootWelcome.enable` | `true` | One dismissible “you're set” toast on the first session (SUPER+M / SUPER+T / SUPER+? + network pointer); marker is `settings.firstBootShown` in the flake checkout |
|
| `nomarchy.firstBootWelcome.enable` | `true` | One dismissible “you're set” toast on the first session (SUPER+M / SUPER+T / SUPER+? + network pointer); marker is `settings.firstBootShown` in the flake checkout |
|
||||||
| `nomarchy.idle.enable` | `true` | hyprlock + hypridle (idle lock 5 min, display off 10, suspend 15 min — battery-only) |
|
| `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 |
|
||||||
@@ -293,6 +297,7 @@ option, e.g. `services.easyeffects.enable = lib.mkForce false;` — see
|
|||||||
| `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.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.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.fingerprint.pam` | `false` | Opt-in: use the fingerprint for login + sudo (PAM) |
|
||||||
|
| `nomarchy.hardware.fingerprint.parallel` | `true` | With PAM on: password *or* fingerprint at the same prompt (whichever comes first); `false` = stock sequential pam_fprintd. Password alone always stays sufficient |
|
||||||
| `nomarchy.hardware.npu.enable` | `false` | Opt-in/experimental: load the on-die NPU driver (`amdxdna`/`intel_vpu`); userspace runtime is BYO |
|
| `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.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.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) |
|
||||||
|
|||||||
@@ -93,18 +93,11 @@ high-ROI, etc.) live in the journal + ROADMAP — not here.*
|
|||||||
fixture. Control-center / MOTD already mention these; the gap is the
|
fixture. Control-center / MOTD already mention these; the gap is the
|
||||||
silent first *graphical* session for people who never open those.
|
silent first *graphical* session for people who never open those.
|
||||||
|
|
||||||
- **Fingerprint *or* password, in parallel** (2026-07-11, Bernardo) —
|
|
||||||
`fingerprint.pam` gives pam_fprintd's sequential UX: sudo waits on
|
|
||||||
the reader, password only after failure/timeout. Bernardo wants
|
|
||||||
either factor accepted at the same prompt (type the password *or*
|
|
||||||
touch the sensor, whichever comes first). Stock PAM can't express
|
|
||||||
this; needs a parallel-conversation module (e.g. `pam-any`, not in
|
|
||||||
nixpkgs — would be a new package + `fingerprint.pam = "any"` mode).
|
|
||||||
Cost: medium (package + PAM wiring + careful lockout testing on real
|
|
||||||
hardware). Security note: keep sudo fallback sane if fprintd hangs.
|
|
||||||
|
|
||||||
_(#80–#83 + #85–#88 shipped 2026-07-11. Theme A day-2 + neon-glass finish
|
_(#80–#83 + #85–#88 shipped 2026-07-11. Theme A day-2 + neon-glass finish
|
||||||
shipped — VISION ✓. Dock/hibernate V3 → HARDWARE-QUEUE.)_
|
shipped — VISION ✓. Dock/hibernate V3 → HARDWARE-QUEUE. Parallel
|
||||||
|
fingerprint-or-password shipped 2026-07-12 (Bernardo promoted it live;
|
||||||
|
`fingerprint.parallel`, pam-fprint-grosshack) — reader V3 →
|
||||||
|
HARDWARE-QUEUE.)_
|
||||||
|
|
||||||
### v1.0 pointer
|
### v1.0 pointer
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,59 @@ the **T14s** (webcam case).
|
|||||||
— should suspend as usual. Optional: with a docked display-profile
|
— should suspend as usual. Optional: with a docked display-profile
|
||||||
(`eDP-1` disable), confirm clamshell layout still looks right after
|
(`eDP-1` disable), confirm clamshell layout still looks right after
|
||||||
lid close. Pass = no surprise suspend when docked.
|
lid close. Pass = no surprise suspend when docked.
|
||||||
- [ ] **#87 dock audio sink follow** — V2 asserts WirePlumber
|
- [ ] **#87 round 2: dock audio auto-follow (watcher)** — round 1 result
|
||||||
`90-nomarchy-dock-audio` rules (HDMI/USB priority). On a laptop
|
(Newton, 2026-07-12): audio stayed on speakers when docking; the
|
||||||
with HDMI or USB-dock audio: play sound on speakers, plug external
|
running generation had no `90-nomarchy-dock-audio` rules yet, and
|
||||||
audio → default sink should move to HDMI/dock (`wpctl status` shows
|
live testing showed the priority rules alone can never win anyway —
|
||||||
`*` on the external sink); unplug → `*` returns to built-in.
|
WirePlumber's stored default (`default-nodes` state, written by any
|
||||||
Manual `wpctl set-default` still works; Bluetooth may outrank dock
|
past explicit pick) outranks `priority.session`, and this machine
|
||||||
(by design). Pass = auto switch without menu intervention.
|
(like most) has one. Fix shipped: the `nomarchy-dock-audio` user
|
||||||
|
service `pactl set-default-sink`s dock-class sinks on hotplug;
|
||||||
|
EasyEffects follows the default (verified live: EE's output links
|
||||||
|
moved to a fake dock-named null sink mid-playback). Retest after
|
||||||
|
pull+rebuild+home on ≥ this rev: play audio, plug the dock →
|
||||||
|
toast "Audio → <monitor/dock>" and sound moves with EE effects
|
||||||
|
still active; unplug → sound returns to speakers; while docked,
|
||||||
|
picking speakers in the Audio menu sticks until the next replug.
|
||||||
|
If it does NOT switch, capture for diagnosis: `pactl subscribe`
|
||||||
|
output during the plug, `pactl --format=json list sinks` while
|
||||||
|
docked (sink names may not match the dock regexes, or the sink
|
||||||
|
node may pre-exist and only flip route availability — the watcher
|
||||||
|
keys on new-sink events).
|
||||||
|
- [ ] **Docked display/audio menu rows** (2026-07-12) — with a real
|
||||||
|
external monitor: Display menu shows "Laptop screen off ·
|
||||||
|
everything → <ext>" (only with >1 active output), "Screen on ·
|
||||||
|
<name>" after a disable, "Move workspace · → next monitor", "Swap
|
||||||
|
workspaces" (exactly 2 outputs). Laptop-off must move its
|
||||||
|
workspaces to the external; Screen on restores it (preferred/auto —
|
||||||
|
declared scale/pos return at the next relogin/switch). Audio menu
|
||||||
|
shows "Send output → <dock sink>" when one exists and isn't
|
||||||
|
default; picking it moves output + toasts. Pass = rows appear/act
|
||||||
|
as described, and none of them show on a single-display session.
|
||||||
|
Also — **run this one with nothing important open**: with the
|
||||||
|
laptop screen off via the menu, physically unplug the dock. The
|
||||||
|
panel should re-enable within ~2s (`rescue_blackout` in the
|
||||||
|
display watcher). Known risk from the VM harness
|
||||||
|
(tools/monitor-fallback.nix): on Hyprland 0.55.4 the zero-output
|
||||||
|
state itself can crash the compositor faster than any rescue
|
||||||
|
(softGL VM: 4/5 runs; control with the panel active survives
|
||||||
|
always) — a crash on real DRM ends the session to the greeter on
|
||||||
|
the re-lit panel. If that happens on hardware, the "Laptop screen
|
||||||
|
off" row needs a rework (mirror instead of disable, or drop until
|
||||||
|
a Hyprland bump) — file it as a NOW bug with the coredump.
|
||||||
|
- [ ] **Parallel fingerprint-or-password on the real reader** (AMD dev
|
||||||
|
box, 2026-07-12) — with a finger enrolled and
|
||||||
|
`fingerprint.pam = true` (parallel is the default): `sudo -k true`
|
||||||
|
must show ONE prompt ("Enter Password or Place finger…"); typing
|
||||||
|
the password immediately works, touching the sensor instead works,
|
||||||
|
wrong-finger ×3 falls back to password, and password keeps working
|
||||||
|
with fprintd stopped (`systemctl stop fprintd`). Also check
|
||||||
|
hyprlock and the greeter accept both factors and don't wedge on a
|
||||||
|
leftover prompt after a fingerprint win (known cosmetic quirk of
|
||||||
|
the hack — pthread_cancel'd prompt). `fingerprint.parallel = false`
|
||||||
|
must restore the old sequential behavior. VM already asserts the
|
||||||
|
PAM stack shape + password-only lockout safety with no reader
|
||||||
|
(checks.hardware-toggles).
|
||||||
- [ ] **#55 fingerprint enroll on real reader** — with
|
- [ ] **#55 fingerprint enroll on real reader** — with
|
||||||
`nomarchy.hardware.fingerprint.enable` and a physical reader: System ›
|
`nomarchy.hardware.fingerprint.enable` and a physical reader: System ›
|
||||||
Fingerprint › Enroll a finger; List shows it; Verify succeeds; optional
|
Fingerprint › Enroll a finger; List shows it; Verify succeeds; optional
|
||||||
|
|||||||
111
agent/JOURNAL.md
111
agent/JOURNAL.md
@@ -26,6 +26,117 @@ Template:
|
|||||||
- **Pending:** #89 is fully closed.
|
- **Pending:** #89 is fully closed.
|
||||||
- **Next suggestion:** Pick the next item from the BACKLOG.
|
- **Next suggestion:** Pick the next item from the BACKLOG.
|
||||||
|
|
||||||
|
## 2026-07-12 — CI OOM: memory-bounded eval for check.yml/bump.yml
|
||||||
|
- **Task:** Bernardo: check.yml always red since he capped the runner
|
||||||
|
container at 2 GB (uncapped it OOM'd the whole 4 GB VPS); bump.yml
|
||||||
|
status unknown.
|
||||||
|
- **Did:** Measured (GNU time, eval-cache off): one-process `nix flake
|
||||||
|
check --no-build` peaks **6.0 GB** RSS — that's the whole story. New
|
||||||
|
`tools/ci-eval.sh`: same coverage, ONE output per nix process (heap
|
||||||
|
freed between outputs); both workflows now call it. bump.yml also got
|
||||||
|
max-jobs=1/cores=2. Facts: bump.yml WORKS — landed 8fded63 on
|
||||||
|
2026-07-06, on schedule; ROADMAP's "no CI today" was stale → fixed.
|
||||||
|
- **Verified:** V1+ — ci-eval.sh ran green locally end-to-end (3:22);
|
||||||
|
per-output cold peaks measured: worst check 0.99 GB
|
||||||
|
(hardware-toggles), nomarchy toplevel 0.78 GB, HM 0.60 GB, **except
|
||||||
|
nomarchy-live 2.69 GB** (live ISO eval) — fits only with the
|
||||||
|
container's swap allowance (docker default --memory-swap = 2×memory);
|
||||||
|
if Bernardo set swap off, that one output still OOMs → server-side
|
||||||
|
one-liner (--memory=2g --memory-swap=6g) or a skip-hatch decision.
|
||||||
|
- **Pending:** real proof is the next push's Actions run on the VPS —
|
||||||
|
watch nomarchy-live's step; can't be verified from this machine.
|
||||||
|
- **Next suggestion:** confirm green run on the remote, then hardware
|
||||||
|
V3 batch.
|
||||||
|
|
||||||
|
## 2026-07-12 — parallel fingerprint-or-password (interactive, promoted)
|
||||||
|
- **Task:** Bernardo promoted the PROPOSED "fingerprint or password in
|
||||||
|
parallel" item live; wants it default-on for fingerprint-PAM users.
|
||||||
|
- **Did:** Packaged pam-fprint-grosshack v0.3.0 (pkgs/, source-reviewed
|
||||||
|
first: all failure paths → PAM_AUTHINFO_UNAVAIL, typed password only
|
||||||
|
ferried via AUTHTOK to pam_unix try_first_pass — can't lock out
|
||||||
|
password login). New `nomarchy.hardware.fingerprint.parallel`
|
||||||
|
(default true; false = stock sequential). Wiring swaps stock fprintd's
|
||||||
|
rule modulePath (mkForce) so ordering is inherited. README/template
|
||||||
|
rows; BACKLOG PROPOSED entry closed into ROADMAP ✓.
|
||||||
|
- **Verified:** V2 — checks.hardware-toggles extended to three nodes and
|
||||||
|
green: parallel node asserts grosshack line before pam_unix in
|
||||||
|
/etc/pam.d/sudo AND `sudo -S` with the right password succeeds /
|
||||||
|
wrong password fails with NO reader present (the lockout invariant);
|
||||||
|
seqpam node = stock pam_fprintd, no grosshack; nopam = neither.
|
||||||
|
Package builds (meson needed libpam-wrapper + python3). flake check
|
||||||
|
green.
|
||||||
|
- **Pending:** V3 on the AMD dev box (HARDWARE-QUEUE): the actual
|
||||||
|
type-or-touch race, fprintd-stopped fallback, hyprlock/greeter
|
||||||
|
behavior after a fingerprint win (pthread_cancel'd prompt quirk).
|
||||||
|
- **Next suggestion:** dock + fingerprint V3 batch on hardware, then
|
||||||
|
#89 slice 3.
|
||||||
|
|
||||||
|
## 2026-07-12 — undock blackout rescue (interactive follow-up)
|
||||||
|
- **Task:** Bernardo: "if I turn the laptop screen off and undock, does
|
||||||
|
the panel come back?" Answer had to be tested, not recalled.
|
||||||
|
- **Did:** Built tools/monitor-fallback.nix (softGL desktop VM, crib of
|
||||||
|
theme-shot): headless "external" + menu's exact disable + output
|
||||||
|
remove. Verdict: Hyprland 0.55.4 does NOT re-enable → shipped
|
||||||
|
`rescue_blackout` in the display hotplug watcher (immediate + retried
|
||||||
|
keyword re-enable of disabled outputs on zero-active, toast).
|
||||||
|
- **Verified:** V2 for the hazard (VM reproduced the dead state);
|
||||||
|
rescue itself is V1+components — the VM crashes before rescue can act
|
||||||
|
in 4/5 runs (aquamarine ABRT in dispatchIdle on the zero-output
|
||||||
|
state; control with panel active survives 5/5 — discriminator run).
|
||||||
|
Crash predates rescue (run with rescue provably inert also crashed).
|
||||||
|
Also fixed: `grep -c .` exit-1-at-zero disabling the rescue's own
|
||||||
|
trigger. flake check green.
|
||||||
|
- **Pending:** real-dock V3 (HARDWARE-QUEUE, "nothing important open"
|
||||||
|
warning); if real DRM crashes too → rework row (mirror) or wait for
|
||||||
|
Hyprland bump; consider reporting upstream with the coredump.
|
||||||
|
- **Next suggestion:** dock V3 results, then #89 slice 3.
|
||||||
|
|
||||||
|
## 2026-07-12 — dock audio auto-follow + docked menu rows (interactive)
|
||||||
|
- **Task:** Bernardo's dock test (= #87's V3): audio stayed on speakers,
|
||||||
|
had to hand-tweak EasyEffects; wants docked Display/Audio menu options.
|
||||||
|
- **Did:** Root-caused live on Newton: WirePlumber's stored default
|
||||||
|
(default-nodes) outranks the #87 priority rules (control test: new
|
||||||
|
sink, no watcher → default pinned), and the running gen lacked the
|
||||||
|
rules anyway; EE 8 follows the default fine (kcfg default true +
|
||||||
|
observed links move). Shipped `modules/home/dock-audio.nix`
|
||||||
|
(`nomarchy.dockAudio.enable`, on): pactl-subscribe watcher
|
||||||
|
set-default-sinks new dock-class sinks (regexes imported from
|
||||||
|
dock-audio-rules.nix) + toast + startup sweep. Menu: Display gains
|
||||||
|
laptop-off/screen-on/move-workspace/swap rows (live-only keywords);
|
||||||
|
Audio gains self-gated "Send output → <dock sink>" row.
|
||||||
|
- **Verified:** V2-equivalent live on Newton — watcher: fake dock-named
|
||||||
|
null sink → default followed, unplug → fell back, EE links moved
|
||||||
|
mid-playback (pw-link); menu: generated script bash -n, dock-row jq
|
||||||
|
exercised against real pactl JSON, dispatchers probed on live
|
||||||
|
Hyprland. flake check green; option-docs green. Beware: kill the
|
||||||
|
subscribe pipe by PID — pkill -f self-matches the test shell.
|
||||||
|
- **Pending:** real-dock V3 ×2 in HARDWARE-QUEUE (watcher may miss
|
||||||
|
sinks that pre-exist and only flip route availability — capture pactl
|
||||||
|
subscribe + sink JSON if it fails again).
|
||||||
|
- **Next suggestion:** #89 slice 3 (Signal pixmap) or dock V3 feedback.
|
||||||
|
|
||||||
|
## 2026-07-12 — auto-commit sweep before pull/rebuild/home (interactive)
|
||||||
|
- **Task:** Bernardo's request (no BACKLOG item): with autoCommit on,
|
||||||
|
hand edits to `~/.nomarchy` (his had 4) stayed forever-dirty —
|
||||||
|
theme-sync's commit is pathspec-limited to theme-state.json by design.
|
||||||
|
- **Did:** New internal `nomarchy-autocommit` in nomarchy-lifecycle:
|
||||||
|
live-reads `settings.autoCommit`, commits *everything* dirty with
|
||||||
|
message `nomarchy: auto-commit before <label>` + file list in the
|
||||||
|
body; called (`|| true`, never blocks) at the top of nomarchy-pull /
|
||||||
|
-rebuild / -home. Exposed as `passthru.autocommit`; package exported.
|
||||||
|
Docs/comments swept: README, RECOVERY, ROADMAP ✓ entry, rofi toggle
|
||||||
|
text, control-center prompt, theme-sync docstring.
|
||||||
|
- **Verified:** V1+ — `nix flake check --no-build` green; new permanent
|
||||||
|
`checks.lifecycle-autocommit` builds (real binary, sandbox repos:
|
||||||
|
sweep/clean/off/non-repo); end-to-end on a *copy* of the real
|
||||||
|
~/.nomarchy via built nomarchy-rebuild with stubbed sudo/rebuild —
|
||||||
|
the 4 real dirty files landed in one commit, tree clean. No VM boot:
|
||||||
|
no session-visible surface. Real-machine confirmation lands after the
|
||||||
|
next nomarchy-pull picks up this rev.
|
||||||
|
- **Pending:** nothing queued; Bernardo's actual ~/.nomarchy still has
|
||||||
|
its 4 uncommitted files until his next pull/rebuild with the new rev.
|
||||||
|
- **Next suggestion:** back to BACKLOG (#89 slice 3, Signal pixmap).
|
||||||
|
|
||||||
## 2026-07-12 — #89 slice 2: EasyEffects tray icon themed monochrome (b98248f)
|
## 2026-07-12 — #89 slice 2: EasyEffects tray icon themed monochrome (b98248f)
|
||||||
- **Task:** BACKLOG #89 slice 2 — EasyEffects SNI shows Papirus' full-
|
- **Task:** BACKLOG #89 slice 2 — EasyEffects SNI shows Papirus' full-
|
||||||
colour blue equaliser, clashing with the monochrome tray.
|
colour blue equaliser, clashing with the monochrome tray.
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ Elan, …) via `lsusb` or `/sys/bus/usb/.../idVendor`. On hit:
|
|||||||
```nix
|
```nix
|
||||||
nomarchy.hardware.fingerprint.enable = true; # services.fprintd
|
nomarchy.hardware.fingerprint.enable = true; # services.fprintd
|
||||||
# nomarchy.hardware.fingerprint.pam = true; # login + sudo (opt-in)
|
# nomarchy.hardware.fingerprint.pam = true; # login + sudo (opt-in)
|
||||||
|
# nomarchy.hardware.fingerprint.parallel = false; # sequential prompt instead
|
||||||
```
|
```
|
||||||
|
|
||||||
### Enroll (menu or CLI)
|
### Enroll (menu or CLI)
|
||||||
@@ -190,6 +191,21 @@ fprintd-list "$USER"
|
|||||||
PAM stays opt-in on purpose: password-only remains the cautious default
|
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.
|
until a finger is enrolled. Full enroll on a real reader is V3/hardware.
|
||||||
|
|
||||||
|
### Parallel prompt (password *or* finger, whichever first)
|
||||||
|
|
||||||
|
With `fingerprint.pam` on, sudo/login/hyprlock show **one** prompt that
|
||||||
|
accepts either factor — type the password or touch the sensor
|
||||||
|
(2026-07-12; `pam-fprint-grosshack`, an fprintd fork, since stock PAM
|
||||||
|
can't express parallel factors). This is the default;
|
||||||
|
`fingerprint.parallel = false` restores stock pam_fprintd's sequential
|
||||||
|
wait-for-the-reader-then-password. Lockout safety is structural: the
|
||||||
|
module never validates passwords itself — a typed password is handed to
|
||||||
|
the normal `pam_unix` rule, and every failure (no reader, fprintd hung,
|
||||||
|
timeout) falls through to password, so finger auth can only ever *add* a
|
||||||
|
way in. Verify the wiring with
|
||||||
|
`grep pam_fprintd_grosshack /etc/pam.d/sudo`. Known cosmetic quirk: after
|
||||||
|
a fingerprint win a leftover password prompt may linger on the console.
|
||||||
|
|
||||||
### Doctor
|
### Doctor
|
||||||
|
|
||||||
`nomarchy-doctor` reports fprintd unit + enroll status when present.
|
`nomarchy-doctor` reports fprintd unit + enroll status when present.
|
||||||
@@ -438,7 +454,7 @@ Full tables: [README § options](../README.md). Hardware-shaped surface:
|
|||||||
| `mkFlake.hardwareProfile` | nixos-hardware name or list |
|
| `mkFlake.hardwareProfile` | nixos-hardware name or list |
|
||||||
| `nomarchy.hardware.intel.enable` / `.guc` / `.computeRuntime` | Intel gap layer |
|
| `nomarchy.hardware.intel.enable` / `.guc` / `.computeRuntime` | Intel gap layer |
|
||||||
| `nomarchy.hardware.amd.enable` / `.pstate` / `.vaapi` / `.rocm.*` | AMD gap layer |
|
| `nomarchy.hardware.amd.enable` / `.pstate` / `.vaapi` / `.rocm.*` | AMD gap layer |
|
||||||
| `nomarchy.hardware.fingerprint.enable` / `.pam` | fprintd + PAM |
|
| `nomarchy.hardware.fingerprint.enable` / `.pam` / `.parallel` | fprintd + PAM (parallel password-or-finger prompt by default) |
|
||||||
| `nomarchy.hardware.npu.enable` | in-kernel NPU only |
|
| `nomarchy.hardware.npu.enable` | in-kernel NPU only |
|
||||||
| `nomarchy.hardware.latestKernel` | `linuxPackages_latest` |
|
| `nomarchy.hardware.latestKernel` | `linuxPackages_latest` |
|
||||||
| `nomarchy.hardware.camera.hideIrSensor` / `.irMatch` | dual-sensor webcams (v4l2 only; §7) |
|
| `nomarchy.hardware.camera.hideIrSensor` / `.irMatch` | dual-sensor webcams (v4l2 only; §7) |
|
||||||
|
|||||||
@@ -269,6 +269,9 @@ default yet.
|
|||||||
on whenever fprintd is enabled; Nomarchy forces PAM to follow the
|
on whenever fprintd is enabled; Nomarchy forces PAM to follow the
|
||||||
`pam` flag, but only after a **system** rebuild. Verify with
|
`pam` flag, but only after a **system** rebuild. Verify with
|
||||||
`grep pam_fprintd /etc/pam.d/sudo` (should be empty when pam is off).
|
`grep pam_fprintd /etc/pam.d/sudo` (should be empty when pam is off).
|
||||||
|
With pam on, the prompt accepts password *or* finger in parallel by
|
||||||
|
default (`fingerprint.parallel = false` for stock sequential) — see
|
||||||
|
HARDWARE.md §5.
|
||||||
- **Browser profiles:** Nomarchy does not manage Chromium/Firefox state.
|
- **Browser profiles:** Nomarchy does not manage Chromium/Firefox state.
|
||||||
Bookmarks/extensions live under `~/.config/chromium` (or
|
Bookmarks/extensions live under `~/.config/chromium` (or
|
||||||
`~/.config/google-chrome` / ungoogled paths if that was your previous
|
`~/.config/google-chrome` / ungoogled paths if that was your previous
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ boreal` (or any preset). If a switch failed halfway, the state file is written
|
|||||||
`home-manager switch --flake ~/.nomarchy` (or `nomarchy-home`).
|
`home-manager switch --flake ~/.nomarchy` (or `nomarchy-home`).
|
||||||
|
|
||||||
Your flake checkout is a git repo, and with auto-commit enabled every
|
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,
|
apply is a commit — and every `nomarchy-pull`/`-rebuild`/`-home` first
|
||||||
`git revert` the culprit, then `nomarchy-home`.
|
sweeps pending hand edits into one, so history mirrors your generations:
|
||||||
|
`git -C ~/.nomarchy log` to see what changed, `git revert` the culprit,
|
||||||
|
then `nomarchy-home`.
|
||||||
|
|
||||||
## 2. The desktop won't start at all
|
## 2. The desktop won't start at all
|
||||||
|
|
||||||
|
|||||||
@@ -402,6 +402,68 @@ Design/decision records and a running log of shipped work (items marked
|
|||||||
built-in. Rules in `modules/nixos/dock-audio-rules.nix`;
|
built-in. Rules in `modules/nixos/dock-audio-rules.nix`;
|
||||||
`checks.dock-audio` pure + conf-file VM. Real hotplug V3 in
|
`checks.dock-audio` pure + conf-file VM. Real hotplug V3 in
|
||||||
HARDWARE-QUEUE.
|
HARDWARE-QUEUE.
|
||||||
|
✓ **Dock audio round 2 — auto-follow watcher (2026-07-12):** the T14s
|
||||||
|
dock test showed priority rules alone can't deliver "just works":
|
||||||
|
WirePlumber's *stored* default (default-nodes state, written by any
|
||||||
|
past explicit device pick — every real machine has one) outranks
|
||||||
|
`priority.session`, so the default never moves on plug. Fix:
|
||||||
|
`nomarchy-dock-audio` (modules/home/dock-audio.nix,
|
||||||
|
`nomarchy.dockAudio.enable`, default on) — a `pactl subscribe` watcher
|
||||||
|
that `set-default-sink`s new dock-class sinks (regex data imported
|
||||||
|
from dock-audio-rules.nix, single source) + toasts; startup sweep for
|
||||||
|
login-while-docked (only when the default isn't already dock-class, so
|
||||||
|
restarts don't override a manual pick). EasyEffects needs nothing: EE 8
|
||||||
|
`useDefaultOutputDevice` defaults true and verified live — with a
|
||||||
|
stream playing, EE's output links moved to a fake dock-named null sink
|
||||||
|
and back on unload. Unplug = WirePlumber priority fallback to built-in.
|
||||||
|
Docked menu rows shipped alongside: Display gains "Laptop screen off ·
|
||||||
|
everything → <ext>" (live-only `keyword monitor <eDP>,disable` — a
|
||||||
|
persisted disable could black-screen an undocked boot), "Screen on ·
|
||||||
|
<name>", "Move workspace · → next monitor"
|
||||||
|
(`movecurrentworkspacetomonitor +1`), "Swap workspaces"
|
||||||
|
(`swapactiveworkspaces`, exactly-2 gate); Audio gains a self-gated
|
||||||
|
"Send output → <dock sink>" quick row (the watcher's manual override).
|
||||||
|
Real-dock V3 for both in HARDWARE-QUEUE.
|
||||||
|
✓ **Undock blackout rescue (2026-07-12):** Bernardo asked whether the
|
||||||
|
panel comes back if you undock while "Laptop screen off" — VM answer
|
||||||
|
(tools/monitor-fallback.nix, full softGL desktop): **no**, Hyprland
|
||||||
|
0.55.4 leaves zero active outputs and never re-enables a soft-disabled
|
||||||
|
monitor. Fix: `rescue_blackout` in the display hotplug watcher
|
||||||
|
(hyprland.nix) — on monitorremoved with zero active outputs,
|
||||||
|
immediately (and with retries) `keyword monitor <name>,preferred,auto,
|
||||||
|
auto` every disabled output + toast; independent of the profile
|
||||||
|
auto-switch gate (safety invariant, not a preference). Sharp edge
|
||||||
|
found while testing: the zero-output state itself crashes Hyprland in
|
||||||
|
the VM ~4/5 runs (ABRT in aquamarine CBackend::dispatchIdle, ~0.5s —
|
||||||
|
faster than any userland rescue; control run with the panel active
|
||||||
|
survives 5/5, so it's the state, not headless removal; monitor-
|
||||||
|
disconnect crashes are a known upstream class). Posture: rescue ships
|
||||||
|
as defense-in-depth (recovers every surviving case; worst case on real
|
||||||
|
DRM is session-to-greeter on the re-lit panel, not a black brick);
|
||||||
|
V3 decides if the menu row needs a mirror-based rework. Also fixed in
|
||||||
|
the rescue: `grep -c .` exits 1 at count 0 — an `||`-guard on that
|
||||||
|
pipeline silently disabled the rescue exactly when it mattered.
|
||||||
|
✓ **Parallel fingerprint-or-password (2026-07-12, Bernardo promoted
|
||||||
|
from PROPOSED):** `nomarchy.hardware.fingerprint.parallel` (default
|
||||||
|
**true** — the better UX is what opting into fingerprint PAM gets you;
|
||||||
|
`false` restores stock sequential pam_fprintd). Implementation:
|
||||||
|
packaged `pam-fprint-grosshack` v0.3.0 (pkgs/, GitLab pin — the
|
||||||
|
field-standard fprintd fork; stock PAM cannot express parallel
|
||||||
|
factors, linux-pam#301; the cleaner-concept `pam-any` stays the
|
||||||
|
fallback candidate if grosshack fights greetd/hyprlock). Source
|
||||||
|
reviewed before packaging: every failure path returns
|
||||||
|
PAM_AUTHINFO_UNAVAIL (falls through to password) and a typed password
|
||||||
|
is only ferried via PAM_AUTHTOK to the stock
|
||||||
|
`auth sufficient pam_unix.so … try_first_pass` — the hack never
|
||||||
|
validates passwords itself, so password login cannot be locked out by
|
||||||
|
it. Wiring reuses stock fprintd's rule slot (mkForce'd modulePath) so
|
||||||
|
ordering is inherited, not recomputed. checks.hardware-toggles grew to
|
||||||
|
three nodes: parallel default (stack shape + password-works/
|
||||||
|
wrong-password-fails with no reader), parallel=false (stock
|
||||||
|
pam_fprintd), pam=false (neither). Real-reader race → HARDWARE-QUEUE
|
||||||
|
(AMD dev box). Accepted quirks (documented in the package): process-
|
||||||
|
wide SIGUSR1 handler, pthread_cancel'd prompt on fingerprint win,
|
||||||
|
prompt buffer not zeroized.
|
||||||
✓ **neon-glass finish (#88, `VISION § D`):** rewrote whole-swap
|
✓ **neon-glass finish (#88, `VISION § D`):** rewrote whole-swap
|
||||||
`rofi.rasi` to rofi-native `@color` glass panel (cyan rim, soft
|
`rofi.rasi` to rofi-native `@color` glass panel (cyan rim, soft
|
||||||
selection wash — same hex-alpha glass pattern as Boreal); btop title/
|
selection wash — same hex-alpha glass pattern as Boreal); btop title/
|
||||||
@@ -938,10 +1000,21 @@ Design/decision records and a running log of shipped work (items marked
|
|||||||
shrinks the security-patch latency the single-input model otherwise imposes
|
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
|
(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` —
|
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
|
worth fast-laning lock-only/security bumps ahead of feature batches.
|
||||||
has **no CI today** (manual `nix flake update` only). Explicitly *not* a
|
**Shipped since:** `.gitea/workflows/check.yml` (eval tier on every main
|
||||||
binary cache: compile-from-source is a deliberate values call (Gentoo-style),
|
push) and `bump.yml` (Monday 05:17 UTC scheduled lock bump, eval + V1
|
||||||
so this automates the *config/lock* channel, not artifact distribution.
|
build gate, auto-lands on green — first landing 8fded63, 2026-07-06).
|
||||||
|
2026-07-12: both gates moved from one big `nix flake check --no-build`
|
||||||
|
(~6.0 GB peak RSS — OOM'd the 4 GB VPS, then could never fit the
|
||||||
|
runner's 2 GB container cap) to `tools/ci-eval.sh`, one output per nix
|
||||||
|
process. **nomarchy-live is excluded by decision** (Bernardo, same
|
||||||
|
day): the ISO is built/distributed manually, so its guard rides that
|
||||||
|
manual flow — and its eval alone is 2.69 GB, the one output that can't
|
||||||
|
fit the cap (Actions run 315: everything else green, it OOM-killed).
|
||||||
|
With it out, the worst CI output is 0.99 GB — no server change needed. 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):**
|
- ✓ **Opt-in auto-commit of state mutations (in-flake state, Phase 4):**
|
||||||
`settings.autoCommit` (menu: **System › Auto-commit**, self-gated on the
|
`settings.autoCommit` (menu: **System › Auto-commit**, self-gated on the
|
||||||
flake being a git repo) makes every `apply`/`set` also `git commit`
|
flake being a git repo) makes every `apply`/`set` also `git commit`
|
||||||
@@ -960,6 +1033,18 @@ Design/decision records and a running log of shipped work (items marked
|
|||||||
`get` now prints booleans JSON-style (`true`, not Python's `True`) —
|
`get` now prints booleans JSON-style (`true`, not Python's `True`) —
|
||||||
which also un-sticks the System menu's "Auto timezone (on/off)" label,
|
which also un-sticks the System menu's "Auto timezone (on/off)" label,
|
||||||
whose `= true` comparison could never match before.
|
whose `= true` comparison could never match before.
|
||||||
|
- ✓ **Auto-commit sweep before switch (2026-07-12):** the pathspec limit
|
||||||
|
above left hand edits (system.nix/home.nix, lock bumps) forever-dirty
|
||||||
|
even with `settings.autoCommit` on. Now `nomarchy-pull`/`-rebuild`/
|
||||||
|
`-home` run `nomarchy-autocommit` (internal, in nomarchy-lifecycle)
|
||||||
|
first: same live-read flag, commits *everything* dirty with an honest
|
||||||
|
`nomarchy: auto-commit before <pull|rebuild|home switch>` message whose
|
||||||
|
body lists the swept files — so `git log` mirrors the generation list
|
||||||
|
and hand edits still never ride a settings-named commit. Same fallback
|
||||||
|
identity; never fatal (`|| true` at the call sites — a git hiccup can't
|
||||||
|
block a rebuild); before the ff-only pull, it also un-dirties the tree.
|
||||||
|
Guarded by `checks.lifecycle-autocommit` (sandbox-repo round trip of
|
||||||
|
the real binary: sweep, no-op on clean/off/non-repo).
|
||||||
- **"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
|
||||||
|
|||||||
@@ -105,6 +105,11 @@ unattended env it uses is in the script, and the same flow works
|
|||||||
interactively from the live terminal. If the desktop comes up unthemed,
|
interactively from the live terminal. If the desktop comes up unthemed,
|
||||||
read `/var/log/nomarchy-hm-preactivate.log` on the installed system.
|
read `/var/log/nomarchy-hm-preactivate.log` on the installed system.
|
||||||
|
|
||||||
|
Two full-desktop VM harnesses (softGL Hyprland, too heavy for `checks.*`)
|
||||||
|
live next to the scripts: `tools/theme-shot.nix` (themed-desktop
|
||||||
|
screenshots) and `tools/monitor-fallback.nix` (undock blackout rescue —
|
||||||
|
disabled panel must re-enable when the last active output disappears).
|
||||||
|
|
||||||
## 5. VM-specific gotchas
|
## 5. VM-specific gotchas
|
||||||
|
|
||||||
- **No KVM** (e.g. nested without acceleration): everything works but
|
- **No KVM** (e.g. nested without acceleration): everything works but
|
||||||
|
|||||||
86
flake.nix
86
flake.nix
@@ -93,6 +93,7 @@
|
|||||||
# switch can refresh a broken system-package nomarchy-pull.
|
# switch can refresh a broken system-package nomarchy-pull.
|
||||||
# Includes nomarchy-what-changed (generation readability, #82).
|
# Includes nomarchy-what-changed (generation readability, #82).
|
||||||
nomarchy-lifecycle = final.callPackage ./pkgs/nomarchy-lifecycle { };
|
nomarchy-lifecycle = final.callPackage ./pkgs/nomarchy-lifecycle { };
|
||||||
|
pam-fprint-grosshack = final.callPackage ./pkgs/pam-fprint-grosshack { };
|
||||||
};
|
};
|
||||||
|
|
||||||
nixosModules.nomarchy = {
|
nixosModules.nomarchy = {
|
||||||
@@ -125,6 +126,7 @@
|
|||||||
# Overlay tools exported for `nix build .#nomarchy-doctor` etc.
|
# Overlay tools exported for `nix build .#nomarchy-doctor` etc.
|
||||||
# (maintainer/CI convenience — modules install them via the overlay).
|
# (maintainer/CI convenience — modules install them via the overlay).
|
||||||
nomarchy-doctor = pkgs.nomarchy-doctor;
|
nomarchy-doctor = pkgs.nomarchy-doctor;
|
||||||
|
nomarchy-lifecycle = pkgs.nomarchy-lifecycle;
|
||||||
nomarchy-control-center = pkgs.nomarchy-control-center;
|
nomarchy-control-center = pkgs.nomarchy-control-center;
|
||||||
nomarchy-battery-notify = pkgs.nomarchy-battery-notify;
|
nomarchy-battery-notify = pkgs.nomarchy-battery-notify;
|
||||||
nomarchy-first-boot = pkgs.nomarchy-first-boot;
|
nomarchy-first-boot = pkgs.nomarchy-first-boot;
|
||||||
@@ -259,6 +261,55 @@
|
|||||||
touch $out
|
touch $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
# The lifecycle "sweep before switch": with settings.autoCommit
|
||||||
|
# on, nomarchy-pull/-rebuild/-home first commit every dirty file
|
||||||
|
# in the machine flake (theme-sync's own auto_commit stays
|
||||||
|
# pathspec-limited to theme-state.json). Exercises the real
|
||||||
|
# helper binary against sandbox repos: flag on → one commit,
|
||||||
|
# clean tree, fallback identity, file list in the body; clean
|
||||||
|
# tree / flag off / no repo → untouched, never an error.
|
||||||
|
lifecycle-autocommit = pkgs.runCommand "nomarchy-lifecycle-autocommit"
|
||||||
|
{ nativeBuildInputs = [ pkgs.git ]; }
|
||||||
|
''
|
||||||
|
set -eu
|
||||||
|
tool=${pkgs.nomarchy-lifecycle.autocommit}/bin/nomarchy-autocommit
|
||||||
|
export HOME=$PWD # no global git config → fallback identity
|
||||||
|
|
||||||
|
mkdir flake && git -C flake init -q
|
||||||
|
printf '{ "settings": { "autoCommit": true } }' > flake/theme-state.json
|
||||||
|
echo base > flake/system.nix
|
||||||
|
git -C flake add -A
|
||||||
|
git -C flake -c user.name=T -c user.email=t@t commit -qm init
|
||||||
|
|
||||||
|
echo edit >> flake/system.nix
|
||||||
|
echo lock > flake/flake.lock
|
||||||
|
NOMARCHY_PATH=$PWD/flake $tool rebuild
|
||||||
|
[ -z "$(git -C flake status --porcelain)" ] \
|
||||||
|
|| { echo "FAIL: tree still dirty after sweep"; exit 1; }
|
||||||
|
git -C flake log -1 --format=%s \
|
||||||
|
| grep -qx 'nomarchy: auto-commit before rebuild'
|
||||||
|
git -C flake log -1 --format=%ae | grep -qx 'nomarchy@localhost'
|
||||||
|
git -C flake log -1 --format=%b | grep -q 'flake.lock'
|
||||||
|
|
||||||
|
# Clean tree → no new commit
|
||||||
|
NOMARCHY_PATH=$PWD/flake $tool rebuild
|
||||||
|
[ "$(git -C flake rev-list --count HEAD)" = 2 ]
|
||||||
|
|
||||||
|
# Flag off → dirty stays dirty, no commit
|
||||||
|
printf '{ "settings": { "autoCommit": false } }' > flake/theme-state.json
|
||||||
|
echo more >> flake/system.nix
|
||||||
|
NOMARCHY_PATH=$PWD/flake $tool rebuild
|
||||||
|
[ "$(git -C flake rev-list --count HEAD)" = 2 ]
|
||||||
|
[ -n "$(git -C flake status --porcelain)" ]
|
||||||
|
|
||||||
|
# Not a git repo → silent no-op, exit 0
|
||||||
|
mkdir plain
|
||||||
|
printf '{ "settings": { "autoCommit": true } }' > plain/theme-state.json
|
||||||
|
NOMARCHY_PATH=$PWD/plain $tool rebuild
|
||||||
|
|
||||||
|
touch $out
|
||||||
|
'';
|
||||||
|
|
||||||
# Installer hardware DB ↔ nixos-hardware (item 49): every module
|
# Installer hardware DB ↔ nixos-hardware (item 49): every module
|
||||||
# name the DB table and the CPU/GPU/chassis autodetect reference
|
# name the DB table and the CPU/GPU/chassis autodetect reference
|
||||||
# must exist in the PINNED nixos-hardware.nixosModules — a lock
|
# must exist in the PINNED nixos-hardware.nixosModules — a lock
|
||||||
@@ -489,7 +540,11 @@
|
|||||||
# what they SET (kernel cmdline, fprintd, PAM). Real hardware
|
# what they SET (kernel cmdline, fprintd, PAM). Real hardware
|
||||||
# behaviour (firmware/driver/device) needs bare metal and is out of
|
# behaviour (firmware/driver/device) needs bare metal and is out of
|
||||||
# scope here. Imports only hardware.nix, so the VM stays minimal.
|
# scope here. Imports only hardware.nix, so the VM stays minimal.
|
||||||
# Two nodes: pam=true must wire pam_fprintd; pam=false must NOT
|
# Three nodes: pam=true wires the PARALLEL module by default
|
||||||
|
# (pam_fprintd_grosshack in stock fprintd's rule slot, before
|
||||||
|
# pam_unix) and password-only auth must still work with no
|
||||||
|
# reader present — the lockout-safety invariant; parallel=false
|
||||||
|
# wires stock sequential pam_fprintd; pam=false wires neither
|
||||||
# (NixOS defaults fprintAuth from fprintd.enable — we force off).
|
# (NixOS defaults fprintAuth from fprintd.enable — we force off).
|
||||||
hardware-toggles = pkgs.testers.runNixOSTest {
|
hardware-toggles = pkgs.testers.runNixOSTest {
|
||||||
name = "nomarchy-hardware-toggles";
|
name = "nomarchy-hardware-toggles";
|
||||||
@@ -502,6 +557,17 @@
|
|||||||
fingerprint = { enable = true; pam = true; };
|
fingerprint = { enable = true; pam = true; };
|
||||||
npu.enable = true;
|
npu.enable = true;
|
||||||
};
|
};
|
||||||
|
users.users.alice = {
|
||||||
|
isNormalUser = true;
|
||||||
|
password = "t0ps3cret";
|
||||||
|
extraGroups = [ "wheel" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
seqpam = { ... }: {
|
||||||
|
imports = [ ./modules/nixos/hardware.nix ];
|
||||||
|
nomarchy.hardware.fingerprint = {
|
||||||
|
enable = true; pam = true; parallel = false;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
nopam = { ... }: {
|
nopam = { ... }: {
|
||||||
imports = [ ./modules/nixos/hardware.nix ];
|
imports = [ ./modules/nixos/hardware.nix ];
|
||||||
@@ -514,7 +580,23 @@
|
|||||||
assert "amd_pstate=active" in cmdline, cmdline
|
assert "amd_pstate=active" in cmdline, cmdline
|
||||||
assert "i915.enable_guc=3" in cmdline, cmdline
|
assert "i915.enable_guc=3" in cmdline, cmdline
|
||||||
machine.succeed("systemctl cat fprintd.service")
|
machine.succeed("systemctl cat fprintd.service")
|
||||||
machine.succeed("grep -q pam_fprintd /etc/pam.d/sudo")
|
machine.succeed("grep -q pam_fprintd_grosshack /etc/pam.d/sudo")
|
||||||
|
# The parallel rule must be an auth line ordered BEFORE
|
||||||
|
# pam_unix — its typed password is only ferried to pam_unix.
|
||||||
|
machine.succeed(
|
||||||
|
"awk '/^auth/ && /pam_fprintd_grosshack/ {g=NR} "
|
||||||
|
"/^auth/ && /pam_unix/ {u=NR} "
|
||||||
|
"END {exit !(g && u && g < u)}' /etc/pam.d/sudo"
|
||||||
|
)
|
||||||
|
# Lockout safety: this VM has no fingerprint reader, so the
|
||||||
|
# grosshack rule can only fail through — the password alone
|
||||||
|
# must still authenticate sudo (and a wrong one must not).
|
||||||
|
machine.succeed("su - alice -c 'echo t0ps3cret | sudo -S -k true'")
|
||||||
|
machine.fail("su - alice -c 'echo wrongpw | sudo -S -k true'")
|
||||||
|
|
||||||
|
seqpam.wait_for_unit("multi-user.target")
|
||||||
|
seqpam.succeed("grep -q pam_fprintd /etc/pam.d/sudo")
|
||||||
|
seqpam.fail("grep -q pam_fprintd_grosshack /etc/pam.d/sudo")
|
||||||
|
|
||||||
nopam.wait_for_unit("multi-user.target")
|
nopam.wait_for_unit("multi-user.target")
|
||||||
nopam.succeed("systemctl cat fprintd.service")
|
nopam.succeed("systemctl cat fprintd.service")
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
./mime.nix # default applications (mimeapps.list), degrades with the suite
|
./mime.nix # default applications (mimeapps.list), degrades with the suite
|
||||||
./recording.nix # nomarchy-record: screen recording behind Capture + the bar ⏺
|
./recording.nix # nomarchy-record: screen recording behind Capture + the bar ⏺
|
||||||
./battery-notify.nix # low-battery toasts at the bar's 25/10% thresholds
|
./battery-notify.nix # low-battery toasts at the bar's 25/10% thresholds
|
||||||
|
./dock-audio.nix # default sink follows dock/monitor audio on hotplug (#87)
|
||||||
./first-boot.nix # one-shot "you're set" toast on first session (#81)
|
./first-boot.nix # one-shot "you're set" toast on first session (#81)
|
||||||
./satty.nix # satty screenshot annotation tool, themed
|
./satty.nix # satty screenshot annotation tool, themed
|
||||||
];
|
];
|
||||||
|
|||||||
90
modules/home/dock-audio.nix
Normal file
90
modules/home/dock-audio.nix
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Automatic audio-output follow for docks / external monitors (#87 round
|
||||||
|
# 2). The WirePlumber priority rules (modules/nixos/dock-audio-rules.nix)
|
||||||
|
# only decide among sinks when there is NO stored user default — but
|
||||||
|
# WirePlumber persists every explicit pick (menu, pavucontrol, pactl) in
|
||||||
|
# its default-nodes state, and that stored default outranks priority. So
|
||||||
|
# on any machine where a device was ever picked, docking never moves the
|
||||||
|
# default — Bernardo's T14s dock test, 2026-07-12. This watcher makes the
|
||||||
|
# switch explicit: when a dock-class sink appears (same regexes as the
|
||||||
|
# wireplumber rules — imported from that file, single source of truth) it
|
||||||
|
# `pactl set-default-sink`s it and toasts. EasyEffects needs no handling:
|
||||||
|
# EE 8's useDefaultOutputDevice defaults to true, so its output follows
|
||||||
|
# the default device and effects stay in the chain (a user who pinned a
|
||||||
|
# device inside EE keeps their pin — that's an explicit choice). Unplug
|
||||||
|
# needs no handling either: the stored dock sink vanishes and WirePlumber
|
||||||
|
# falls back by priority to the built-in output. A manual pick made while
|
||||||
|
# docked sticks until the next plug event (a plug is treated as intent).
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.nomarchy;
|
||||||
|
rules = import ../nixos/dock-audio-rules.nix;
|
||||||
|
dockSinkRe = lib.concatStringsSep "|"
|
||||||
|
(map (m: lib.removePrefix "~" m."node.name")
|
||||||
|
(lib.concatMap (r: r.matches) rules."monitor.alsa.rules"));
|
||||||
|
|
||||||
|
watcher = pkgs.writeShellScript "nomarchy-dock-audio" ''
|
||||||
|
set -euo pipefail
|
||||||
|
PACTL=${pkgs.pulseaudio}/bin/pactl
|
||||||
|
JQ=${pkgs.jq}/bin/jq
|
||||||
|
TAB=$(printf '\t')
|
||||||
|
|
||||||
|
# Self-gate: no PipeWire pulse socket (nomarchy.audio off, TTY-only
|
||||||
|
# session) means nothing to watch — exit clean, stay dead.
|
||||||
|
rt="''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||||
|
for _ in $(seq 60); do [ -e "$rt/pulse/native" ] && break; sleep 1; done
|
||||||
|
[ -e "$rt/pulse/native" ] || exit 0
|
||||||
|
|
||||||
|
is_dock() {
|
||||||
|
printf '%s\n' "$1" | ${pkgs.gnugrep}/bin/grep -qE '^(${dockSinkRe})$'
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_to() { # $1 = sink node name, $2 = description
|
||||||
|
[ "$($PACTL get-default-sink)" = "$1" ] && return 0
|
||||||
|
$PACTL set-default-sink "$1" 2>/dev/null || return 0
|
||||||
|
${pkgs.libnotify}/bin/notify-send -a Nomarchy "Audio" \
|
||||||
|
"Output → ''${2:-$1}" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Startup sweep (login/relogin while already docked): adopt a dock
|
||||||
|
# sink only when the current default is NOT one — a service restart
|
||||||
|
# must not override a manual in-dock pick.
|
||||||
|
if ! is_dock "$($PACTL get-default-sink)"; then
|
||||||
|
$PACTL --format=json list sinks \
|
||||||
|
| $JQ -r '.[] | "\(.name)\t\(.description)"' \
|
||||||
|
| while IFS="$TAB" read -r name desc; do
|
||||||
|
if is_dock "$name"; then switch_to "$name" "$desc"; break; fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Event loop: `pactl subscribe` prints "Event 'new' on sink #NN".
|
||||||
|
LC_ALL=C $PACTL subscribe | while read -r _ ev _ kind id; do
|
||||||
|
[ "$ev" = "'new'" ] && [ "$kind" = "sink" ] || continue
|
||||||
|
id=''${id#\#}
|
||||||
|
case "$id" in *[!0-9]*|"") continue ;; esac
|
||||||
|
sleep 0.5 # let the node's description/routes settle
|
||||||
|
line=$($PACTL --format=json list sinks | $JQ -r --argjson i "$id" \
|
||||||
|
'.[] | select(.index == $i) | "\(.name)\t\(.description)"')
|
||||||
|
[ -n "$line" ] || continue
|
||||||
|
name=''${line%%"$TAB"*}; desc=''${line#*"$TAB"}
|
||||||
|
if is_dock "$name"; then switch_to "$name" "$desc"; fi
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = lib.mkIf cfg.dockAudio.enable {
|
||||||
|
systemd.user.services.nomarchy-dock-audio = {
|
||||||
|
Unit = {
|
||||||
|
Description = "Move the default audio sink to dock/monitor outputs on hotplug";
|
||||||
|
After = [ "graphical-session.target" ];
|
||||||
|
PartOf = [ "graphical-session.target" ];
|
||||||
|
};
|
||||||
|
Service = {
|
||||||
|
ExecStart = "${watcher}";
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = 2;
|
||||||
|
};
|
||||||
|
Install.WantedBy = [ "graphical-session.target" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -215,7 +215,43 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
|
|||||||
case "$v" in true|True) return 0 ;; *) return 1 ;; esac
|
case "$v" in true|True) return 0 ;; *) return 1 ;; esac
|
||||||
}
|
}
|
||||||
outputs() { hyprctl monitors all -j 2>/dev/null | jq -r '.[].name' | sort; }
|
outputs() { hyprctl monitors all -j 2>/dev/null | jq -r '.[].name' | sort; }
|
||||||
|
|
||||||
|
# Blackout rescue — the session must never end up with ZERO active
|
||||||
|
# outputs. Hyprland does NOT re-enable a soft-disabled monitor when
|
||||||
|
# the last active one disconnects (VM-verified, tools/
|
||||||
|
# monitor-fallback.nix): menu "Laptop screen off" + sudden undock
|
||||||
|
# left a dead session. On every monitorremoved, if nothing is
|
||||||
|
# active, re-enable whatever is disabled (preferred/auto — the
|
||||||
|
# declared rule returns at the next reload/relogin). Independent of
|
||||||
|
# the profile auto-switch and its settings gate: this is a safety
|
||||||
|
# invariant, not a layout preference.
|
||||||
|
active_count() {
|
||||||
|
# NB: `grep -c .` exits 1 when the count is 0 — the case this whole
|
||||||
|
# rescue exists for — so never `||`-guard on the pipeline status.
|
||||||
|
hyprctl monitors -j 2>/dev/null | jq -r '.[].name' | grep -c . || true
|
||||||
|
}
|
||||||
|
rescue_blackout() {
|
||||||
|
# Act IMMEDIATELY and retry: the zero-output state is not just a
|
||||||
|
# black screen, it can kill the compositor outright — in the softGL
|
||||||
|
# VM harness aquamarine ABRT'd (CBackend::dispatchIdle) within
|
||||||
|
# ~0.5s of the last output going while it persisted, so shrinking
|
||||||
|
# the window is the priority; the keyword is idempotent and the
|
||||||
|
# retries cover a first attempt racing the removal teardown.
|
||||||
|
tries=0
|
||||||
|
while [ "$(active_count)" = 0 ] && [ "$tries" -lt 4 ]; do
|
||||||
|
hyprctl monitors all -j 2>/dev/null | jq -r '.[] | select(.disabled) | .name' \
|
||||||
|
| while read -r m; do
|
||||||
|
[ -n "$m" ] || continue
|
||||||
|
hyprctl keyword monitor "$m,preferred,auto,auto" >/dev/null 2>&1
|
||||||
|
done
|
||||||
|
tries=$((tries+1))
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ "$tries" -gt 0 ] && [ "$(active_count)" != 0 ]; then
|
||||||
|
notify-send "Display" "Screen re-enabled — the active output disappeared." 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
check_monitors() {
|
check_monitors() {
|
||||||
cur=$(outputs)
|
cur=$(outputs)
|
||||||
[ "$cur" = "$1" ] && return
|
[ "$cur" = "$1" ] && return
|
||||||
@@ -239,7 +275,12 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
|
|||||||
# Listen to Hyprland's IPC socket for instant hotplug reaction
|
# 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
|
${pkgs.socat}/bin/socat -U - UNIX-CONNECT:$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock | while read -r line; do
|
||||||
case "$line" in
|
case "$line" in
|
||||||
monitoradded*|monitorremoved*)
|
monitorremoved*)
|
||||||
|
rescue_blackout
|
||||||
|
res=$(check_monitors "$prev")
|
||||||
|
[ -n "$res" ] && prev="$res"
|
||||||
|
;;
|
||||||
|
monitoradded*)
|
||||||
res=$(check_monitors "$prev")
|
res=$(check_monitors "$prev")
|
||||||
[ -n "$res" ] && prev="$res"
|
[ -n "$res" ] && prev="$res"
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -364,6 +364,7 @@ in
|
|||||||
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; };
|
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; };
|
||||||
|
dockAudio.enable = lib.mkEnableOption "automatic default-output switch to dock/monitor audio (HDMI/DisplayPort/USB sinks) on hotplug, with a toast; EasyEffects follows along, and unplugging falls back to the built-in output" // { default = true; };
|
||||||
firstBootWelcome.enable = lib.mkEnableOption "one dismissible \"you're set\" toast on the first session (menu/themes/keys + network pointer); marker is settings.firstBootShown in the flake checkout" // { default = true; };
|
firstBootWelcome.enable = lib.mkEnableOption "one dismissible \"you're set\" toast on the first session (menu/themes/keys + network pointer); marker is settings.firstBootShown in the flake checkout" // { 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; };
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ let
|
|||||||
|
|
||||||
px = n: mkLiteral "${toString n}px";
|
px = n: mkLiteral "${toString n}px";
|
||||||
|
|
||||||
|
# Dock-class audio sinks (HDMI/DP/USB) — the same regex data the
|
||||||
|
# WirePlumber priority rules and the dock-audio watcher consume
|
||||||
|
# (modules/nixos/dock-audio-rules.nix is the single source).
|
||||||
|
dockSinkRe = lib.concatStringsSep "|"
|
||||||
|
(map (m: lib.removePrefix "~" m."node.name")
|
||||||
|
(lib.concatMap (r: r.matches)
|
||||||
|
(import ../nixos/dock-audio-rules.nix)."monitor.alsa.rules"));
|
||||||
|
|
||||||
# Per-theme override probe (same convention as waybar.css).
|
# Per-theme override probe (same convention as waybar.css).
|
||||||
rasiOverride = cfg.themesDir + "/${t.slug}/rofi.rasi";
|
rasiOverride = cfg.themesDir + "/${t.slug}/rofi.rasi";
|
||||||
hasRasiOverride = builtins.pathExists rasiOverride;
|
hasRasiOverride = builtins.pathExists rasiOverride;
|
||||||
@@ -498,13 +506,30 @@ ${themeRows}
|
|||||||
if [ ! -e "''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/pulse/native" ]; then
|
if [ ! -e "''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/pulse/native" ]; then
|
||||||
notify-send "Audio" "No PipeWire/PulseAudio socket (nomarchy.audio off?)."; exit 0
|
notify-send "Audio" "No PipeWire/PulseAudio socket (nomarchy.audio off?)."; exit 0
|
||||||
fi
|
fi
|
||||||
|
# Docked quick row: the first dock-class sink (HDMI/DP/USB —
|
||||||
|
# dockSinkRe, single source with the wireplumber rules) that is
|
||||||
|
# not already the default. One pick sends output there; the
|
||||||
|
# dock-audio watcher normally does this on plug, so this row is
|
||||||
|
# the manual override / catch-up. pactl by store path — it is
|
||||||
|
# not on PATH (PipeWire stack, wpctl only).
|
||||||
|
sinks=$(${pkgs.pulseaudio}/bin/pactl --format=json list sinks 2>/dev/null || true)
|
||||||
|
cursink=$(${pkgs.pulseaudio}/bin/pactl get-default-sink 2>/dev/null || true)
|
||||||
|
dockname=$(printf '%s' "$sinks" | jq -r --arg cur "$cursink" --arg re '${dockSinkRe}' \
|
||||||
|
'[.[] | select((.name | test("^(" + $re + ")$")) and .name != $cur)][0].name // empty')
|
||||||
|
dockdesc=
|
||||||
|
[ -n "$dockname" ] && dockdesc=$(printf '%s' "$sinks" \
|
||||||
|
| jq -r --arg n "$dockname" '[.[] | select(.name == $n)][0].description // empty')
|
||||||
choice=$( {
|
choice=$( {
|
||||||
|
[ -n "$dockname" ] && row "Send output → ''${dockdesc:-$dockname}" video-display
|
||||||
row "Output device" audio-volume-high
|
row "Output device" audio-volume-high
|
||||||
row "Input device" audio-input-microphone
|
row "Input device" audio-input-microphone
|
||||||
back
|
back
|
||||||
} | rofi_menu -show-icons -p Audio) || exit 0
|
} | rofi_menu -show-icons -p Audio) || exit 0
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
"$BACK") exec "$0" system ;;
|
"$BACK") exec "$0" system ;;
|
||||||
|
"Send output"*)
|
||||||
|
${pkgs.pulseaudio}/bin/pactl set-default-sink "$dockname" \
|
||||||
|
&& notify-send "Audio" "Output → ''${dockdesc:-$dockname}" ;;
|
||||||
*Output*) exec rofi-pulse-select sink ;;
|
*Output*) exec rofi-pulse-select sink ;;
|
||||||
*Input*) exec rofi-pulse-select source ;;
|
*Input*) exec rofi-pulse-select source ;;
|
||||||
esac ;;
|
esac ;;
|
||||||
@@ -522,6 +547,14 @@ ${themeRows}
|
|||||||
|| { notify-send "Display" "Hyprland is not running."; exit 0; }
|
|| { notify-send "Display" "Hyprland is not running."; exit 0; }
|
||||||
mons=$(hyprctl monitors -j)
|
mons=$(hyprctl monitors -j)
|
||||||
|
|
||||||
|
# Docked context: which outputs are active, which is the built-in
|
||||||
|
# panel, is anything currently soft-disabled (monitors all shows
|
||||||
|
# disabled outputs; plain monitors does not).
|
||||||
|
nActive=$(printf '%s' "$mons" | jq -r '.[].name' | grep -c .)
|
||||||
|
internal=$(printf '%s' "$mons" | jq -r '[.[].name | select(test("^(eDP|LVDS|DSI)"))][0] // empty')
|
||||||
|
external=$(printf '%s' "$mons" | jq -r '[.[].name | select(test("^(eDP|LVDS|DSI)") | not)][0] // empty')
|
||||||
|
offmon=$(printf '%s' "$(hyprctl monitors all -j)" | jq -r '[.[] | select(.disabled)][0].name // empty')
|
||||||
|
|
||||||
# Choose the output (skip the chooser when only one is connected and
|
# Choose the output (skip the chooser when only one is connected and
|
||||||
# there are no profiles — the Profiles row needs the chooser to live in).
|
# there are no profiles — the Profiles row needs the chooser to live in).
|
||||||
# Where the mode picker's Back returns to: the output chooser when it
|
# Where the mode picker's Back returns to: the output chooser when it
|
||||||
@@ -530,14 +563,54 @@ ${themeRows}
|
|||||||
# `display` would just re-open this same mode picker (item 40).
|
# `display` would just re-open this same mode picker (item 40).
|
||||||
modeBack=system
|
modeBack=system
|
||||||
profs=; command -v nomarchy-display-profile >/dev/null 2>&1 && profs=1
|
profs=; command -v nomarchy-display-profile >/dev/null 2>&1 && profs=1
|
||||||
if [ -n "$profs" ] || [ "$(printf '%s' "$mons" | jq -r '.[].name' | grep -c .)" -gt 1 ]; then
|
if [ -n "$profs" ] || [ "$nActive" -gt 1 ] || [ -n "$offmon" ]; then
|
||||||
name=$( {
|
name=$( {
|
||||||
[ -n "$profs" ] && printf 'Profiles ›\n'
|
[ -n "$profs" ] && printf 'Profiles ›\n'
|
||||||
|
# Docked rows — self-gated: never offer to disable the only
|
||||||
|
# active display; the re-enable row appears whenever an
|
||||||
|
# output is soft-off (so an undocked laptop can recover too).
|
||||||
|
[ "$nActive" -gt 1 ] && [ -n "$internal" ] && [ -n "$external" ] \
|
||||||
|
&& printf 'Laptop screen off · everything → %s\n' "$external"
|
||||||
|
[ -n "$offmon" ] && printf 'Screen on · %s\n' "$offmon"
|
||||||
|
[ "$nActive" -gt 1 ] && printf 'Move workspace · → next monitor\n'
|
||||||
|
[ "$nActive" -eq 2 ] && printf 'Swap workspaces · %s ⇄ %s\n' \
|
||||||
|
"$(printf '%s' "$mons" | jq -r '.[0].name')" \
|
||||||
|
"$(printf '%s' "$mons" | jq -r '.[1].name')"
|
||||||
printf '%s' "$mons" | jq -r '.[] | "\(.name) · \(.width)x\(.height)@\(.refreshRate|round)Hz"'
|
printf '%s' "$mons" | jq -r '.[] | "\(.name) · \(.width)x\(.height)@\(.refreshRate|round)Hz"'
|
||||||
printf '%s\n' "$BACK"
|
printf '%s\n' "$BACK"
|
||||||
} | rofi_menu -p Display ) || exit 0
|
} | rofi_menu -p Display ) || exit 0
|
||||||
[ "$name" = "$BACK" ] && exec "$0" system
|
[ "$name" = "$BACK" ] && exec "$0" system
|
||||||
[ "$name" = "Profiles ›" ] && exec "$0" display-profile
|
[ "$name" = "Profiles ›" ] && exec "$0" display-profile
|
||||||
|
case "$name" in
|
||||||
|
"Laptop screen off"*)
|
||||||
|
# Live-only on purpose: Hyprland moves the panel's
|
||||||
|
# workspaces to the remaining output on disable; persisting
|
||||||
|
# a disable could black-screen an undocked boot, so the
|
||||||
|
# declared rule returns at the next relogin/switch.
|
||||||
|
if hyprctl keyword monitor "$internal,disable" >/dev/null 2>&1; then
|
||||||
|
notify-send "Display" "Laptop screen off — workspaces moved to $external (this session; turn it back on from this menu)."
|
||||||
|
else
|
||||||
|
notify-send "Display" "Could not disable $internal."
|
||||||
|
fi
|
||||||
|
exit 0 ;;
|
||||||
|
"Screen on"*)
|
||||||
|
# preferred/auto/auto is good enough live; the declared
|
||||||
|
# scale/position come back at the next relogin/switch.
|
||||||
|
if hyprctl keyword monitor "$offmon,preferred,auto,auto" >/dev/null 2>&1; then
|
||||||
|
notify-send "Display" "$offmon is back on."
|
||||||
|
else
|
||||||
|
notify-send "Display" "Could not enable $offmon."
|
||||||
|
fi
|
||||||
|
exit 0 ;;
|
||||||
|
"Move workspace"*)
|
||||||
|
hyprctl dispatch movecurrentworkspacetomonitor +1 >/dev/null 2>&1
|
||||||
|
exit 0 ;;
|
||||||
|
"Swap workspaces"*)
|
||||||
|
hyprctl dispatch swapactiveworkspaces \
|
||||||
|
"$(printf '%s' "$mons" | jq -r '.[0].name')" \
|
||||||
|
"$(printf '%s' "$mons" | jq -r '.[1].name')" >/dev/null 2>&1
|
||||||
|
exit 0 ;;
|
||||||
|
esac
|
||||||
name=''${name%% ·*} # strip the " · WxH@R" hint
|
name=''${name%% ·*} # strip the " · WxH@R" hint
|
||||||
modeBack=display
|
modeBack=display
|
||||||
else
|
else
|
||||||
@@ -806,7 +879,9 @@ ${themeRows}
|
|||||||
autocommit)
|
autocommit)
|
||||||
# Toggle opt-in auto-commit: every menu/theme mutation also commits
|
# Toggle opt-in auto-commit: every menu/theme mutation also commits
|
||||||
# theme-state.json (that file only) in the downstream flake, so
|
# theme-state.json (that file only) in the downstream flake, so
|
||||||
# settings history is `git log`. Nothing in Nix consumes the flag —
|
# settings history is `git log`; nomarchy-pull/-rebuild/-home
|
||||||
|
# additionally sweep everything else dirty (hand edits, lock bumps)
|
||||||
|
# into a commit first. Nothing in Nix consumes the flag —
|
||||||
# the tool reads the live state on each write — so the toggle is
|
# the tool reads the live state on each write — so the toggle is
|
||||||
# instant, no rebuild. The off-write commits too (the tool fires
|
# instant, no rebuild. The off-write commits too (the tool fires
|
||||||
# when the flag was on before OR after), keeping history consistent.
|
# when the flag was on before OR after), keeping history consistent.
|
||||||
@@ -815,7 +890,7 @@ ${themeRows}
|
|||||||
notify-send "Auto-commit" "Off — menu changes stay uncommitted in your flake."
|
notify-send "Auto-commit" "Off — menu changes stay uncommitted in your flake."
|
||||||
else
|
else
|
||||||
nomarchy-theme-sync --quiet set settings.autoCommit true --no-switch
|
nomarchy-theme-sync --quiet set settings.autoCommit true --no-switch
|
||||||
notify-send "Auto-commit" "On — each change commits theme-state.json in your flake."
|
notify-send "Auto-commit" "On — menu changes commit; rebuilds sweep pending edits in too."
|
||||||
fi
|
fi
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,20 @@ in
|
|||||||
menu) when set; otherwise false.
|
menu) when set; otherwise false.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
parallel = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
With fingerprint PAM on, accept the password OR a fingerprint at the
|
||||||
|
same prompt (type or touch, whichever comes first) instead of stock
|
||||||
|
pam_fprintd's sequential wait-for-the-reader-then-password. Uses the
|
||||||
|
pam-fprint-grosshack module (an fprintd fork — source-reviewed; every
|
||||||
|
failure path falls through to the normal password rule, so password
|
||||||
|
login can never be locked out by it). Set false for the stock
|
||||||
|
sequential behavior.
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
npu.enable = lib.mkEnableOption ''
|
npu.enable = lib.mkEnableOption ''
|
||||||
@@ -205,7 +219,20 @@ in
|
|||||||
"passwd" "chsh" "chfn" "chpasswd"
|
"passwd" "chsh" "chfn" "chpasswd"
|
||||||
"polkit-1" "swaylock"
|
"polkit-1" "swaylock"
|
||||||
"groupadd" "groupdel" "groupmod" "groupmems"
|
"groupadd" "groupdel" "groupmod" "groupmems"
|
||||||
] (_: { fprintAuth = cfg.fingerprint.pam; });
|
] (_: {
|
||||||
|
fprintAuth = cfg.fingerprint.pam;
|
||||||
|
} // lib.optionalAttrs (cfg.fingerprint.pam && cfg.fingerprint.parallel) {
|
||||||
|
# Parallel mode: same rule slot as stock fprintd (so ordering —
|
||||||
|
# sufficient, before pam_unix — is inherited), different module.
|
||||||
|
# grosshack prompts for the password itself while polling the
|
||||||
|
# reader; whichever lands first wins. A typed password makes the
|
||||||
|
# rule FAIL with the token stored, and the stock
|
||||||
|
# `auth sufficient pam_unix.so … try_first_pass` right after it
|
||||||
|
# does the actual validation — password stays sufficient on its
|
||||||
|
# own, so a broken reader/fprintd can never lock login out.
|
||||||
|
rules.auth.fprintd.modulePath = lib.mkForce
|
||||||
|
"${pkgs.pam-fprint-grosshack}/lib/security/pam_fprintd_grosshack.so";
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
# ── Newest kernel for very-new hardware (opt-in escape hatch) ──────
|
# ── Newest kernel for very-new hardware (opt-in escape hatch) ──────
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function first_boot() {
|
|||||||
nomarchy-theme-sync apply "$chosen_theme"
|
nomarchy-theme-sync apply "$chosen_theme"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if gum confirm "Enable auto-commit for settings?"; then
|
if gum confirm "Enable auto-commit for your config? (settings changes + a sweep of pending edits before each rebuild)"; then
|
||||||
set_state "settings.autoCommit" "true"
|
set_state "settings.autoCommit" "true"
|
||||||
else
|
else
|
||||||
set_state "settings.autoCommit" "false"
|
set_state "settings.autoCommit" "false"
|
||||||
|
|||||||
@@ -15,6 +15,41 @@ let
|
|||||||
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
|
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
# Opt-in sweep (settings.autoCommit — the same flag theme-sync honours):
|
||||||
|
# commit EVERYTHING dirty in the machine flake before a pull/rebuild/
|
||||||
|
# home switch, so hand edits to system.nix/home.nix and lock bumps land
|
||||||
|
# in history at the moment they become live — `git log` mirrors the
|
||||||
|
# generation list. Complements nomarchy-theme-sync's auto_commit, which
|
||||||
|
# is pathspec-limited to theme-state.json on menu writes precisely so
|
||||||
|
# half-finished hand edits never ride a settings-named commit; here the
|
||||||
|
# sweep is the point, and the commit body lists what was swept. Never
|
||||||
|
# fatal — callers `|| true` so a git hiccup can't block a rebuild. Not
|
||||||
|
# in the symlinkJoin paths (internal; callers use the store path) but
|
||||||
|
# exposed as passthru.autocommit for checks.lifecycle-autocommit.
|
||||||
|
nomarchy-autocommit = writeShellScriptBin "nomarchy-autocommit" ''
|
||||||
|
${preamble "nomarchy-autocommit"}
|
||||||
|
label="''${1:-rebuild}"
|
||||||
|
[ -d "$flake/.git" ] || exit 0
|
||||||
|
command -v git >/dev/null 2>&1 || exit 0
|
||||||
|
flag=$(${jq}/bin/jq -r '.settings.autoCommit // false' \
|
||||||
|
"$flake/theme-state.json" 2>/dev/null || true)
|
||||||
|
[ "$flag" = "true" ] || exit 0
|
||||||
|
dirty=$(git -C "$flake" status --porcelain || true)
|
||||||
|
[ -n "$dirty" ] || exit 0
|
||||||
|
g=(git -C "$flake")
|
||||||
|
if [ -z "$("''${g[@]}" config user.email 2>/dev/null || true)" ]; then
|
||||||
|
g+=(-c user.name=Nomarchy -c user.email=nomarchy@localhost)
|
||||||
|
fi
|
||||||
|
"''${g[@]}" add -A
|
||||||
|
if "''${g[@]}" commit --quiet -m "nomarchy: auto-commit before $label" \
|
||||||
|
-m "$dirty"; then
|
||||||
|
echo "nomarchy-autocommit: committed pending flake changes before $label:"
|
||||||
|
echo "$dirty" | sed 's/^/ /'
|
||||||
|
else
|
||||||
|
echo "nomarchy-autocommit: commit failed — continuing without it" >&2
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
nomarchy-pull = writeShellScriptBin "nomarchy-pull" ''
|
nomarchy-pull = writeShellScriptBin "nomarchy-pull" ''
|
||||||
${preamble "nomarchy-pull"}
|
${preamble "nomarchy-pull"}
|
||||||
if [ ! -e "$flake/flake.nix" ]; then
|
if [ ! -e "$flake/flake.nix" ]; then
|
||||||
@@ -22,6 +57,9 @@ let
|
|||||||
echo " Set NOMARCHY_PATH or put your machine flake in ~/.nomarchy." >&2
|
echo " Set NOMARCHY_PATH or put your machine flake in ~/.nomarchy." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
# With settings.autoCommit on, sweep pending hand edits into a commit
|
||||||
|
# first — also keeps the ff-only pull below safe on a dirty tree.
|
||||||
|
${nomarchy-autocommit}/bin/nomarchy-autocommit pull || true
|
||||||
# Optional: pull *your* machine config only if this checkout tracks a
|
# Optional: pull *your* machine config only if this checkout tracks a
|
||||||
# remote. Distro updates come from the nomarchy flake *input* below —
|
# remote. Distro updates come from the nomarchy flake *input* below —
|
||||||
# many installs have no upstream on ~/.nomarchy (local auto-commits).
|
# many installs have no upstream on ~/.nomarchy (local auto-commits).
|
||||||
@@ -52,6 +90,9 @@ let
|
|||||||
echo "nomarchy-rebuild: no flake.nix at $flake" >&2
|
echo "nomarchy-rebuild: no flake.nix at $flake" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
# With settings.autoCommit on, sweep pending hand edits (system.nix,
|
||||||
|
# lock bumps, …) into a commit so `git log` mirrors the generations.
|
||||||
|
${nomarchy-autocommit}/bin/nomarchy-autocommit rebuild || true
|
||||||
before=$(readlink -f /run/current-system)
|
before=$(readlink -f /run/current-system)
|
||||||
log=$(mktemp)
|
log=$(mktemp)
|
||||||
trap 'rm -f "$log"' EXIT
|
trap 'rm -f "$log"' EXIT
|
||||||
@@ -94,6 +135,9 @@ let
|
|||||||
echo "nomarchy-home: no flake.nix at $flake" >&2
|
echo "nomarchy-home: no flake.nix at $flake" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
# With settings.autoCommit on, sweep pending hand edits (home.nix, …)
|
||||||
|
# into a commit so `git log` mirrors the generations.
|
||||||
|
${nomarchy-autocommit}/bin/nomarchy-autocommit "home switch" || true
|
||||||
# Snapshot the active HM generation before the switch so we can nvd it.
|
# Snapshot the active HM generation before the switch so we can nvd it.
|
||||||
hm_before=""
|
hm_before=""
|
||||||
for d in \
|
for d in \
|
||||||
@@ -164,6 +208,7 @@ symlinkJoin {
|
|||||||
home-update
|
home-update
|
||||||
nomarchy-what-changed
|
nomarchy-what-changed
|
||||||
];
|
];
|
||||||
|
passthru.autocommit = nomarchy-autocommit;
|
||||||
meta = {
|
meta = {
|
||||||
description = "Nomarchy machine-flake lifecycle: pull, rebuild, home";
|
description = "Nomarchy machine-flake lifecycle: pull, rebuild, home";
|
||||||
mainProgram = "nomarchy-pull";
|
mainProgram = "nomarchy-pull";
|
||||||
|
|||||||
@@ -143,7 +143,10 @@ def auto_commit(message: str) -> None:
|
|||||||
fire this when the flag is on before OR after the write, so the
|
fire this when the flag is on before OR after the write, so the
|
||||||
disable-toggle itself lands in history instead of staying dirty.
|
disable-toggle itself lands in history instead of staying dirty.
|
||||||
`bg` is deliberately excluded (runtime wallpaper churn); the wallpaper
|
`bg` is deliberately excluded (runtime wallpaper churn); the wallpaper
|
||||||
path rides along with the next apply/set commit."""
|
path rides along with the next apply/set commit. The rest of a dirty
|
||||||
|
tree (hand edits, lock bumps) is swept by nomarchy-lifecycle's
|
||||||
|
nomarchy-autocommit right before a pull/rebuild/home switch — same
|
||||||
|
flag, honest message — so it never rides a settings-named commit."""
|
||||||
if not (FLAKE_DIR / ".git").exists() or shutil.which("git") is None:
|
if not (FLAKE_DIR / ".git").exists() or shutil.which("git") is None:
|
||||||
return
|
return
|
||||||
git = ["git", "-C", str(FLAKE_DIR)]
|
git = ["git", "-C", str(FLAKE_DIR)]
|
||||||
|
|||||||
50
pkgs/pam-fprint-grosshack/default.nix
Normal file
50
pkgs/pam-fprint-grosshack/default.nix
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# pam_fprintd_grosshack.so — fprintd's PAM module forked to accept the
|
||||||
|
# password OR a fingerprint at the same prompt (upstream calls itself a
|
||||||
|
# "gross hack"; it is also the field-standard answer to this PAM gap —
|
||||||
|
# stock PAM cannot express parallel factors, linux-pam#301).
|
||||||
|
#
|
||||||
|
# Source reviewed before packaging (2026-07-12, v0.3.0 = fprintd 1.94.2
|
||||||
|
# base + ~60-line delta): every failure path — no reader, no enrolled
|
||||||
|
# prints, fprintd absent, timeout, password typed — returns
|
||||||
|
# PAM_AUTHINFO_UNAVAIL, i.e. the module FAILS and PAM falls through to
|
||||||
|
# the password rule; a typed password is never validated here, only
|
||||||
|
# ferried via PAM_AUTHTOK to pam_unix (try_first_pass). Accepted
|
||||||
|
# quirks: installs a process-wide SIGUSR1 handler in the calling
|
||||||
|
# process, pthread_cancels the prompt thread on fingerprint win, and
|
||||||
|
# does not zeroize the prompt buffer. Consumed by
|
||||||
|
# modules/nixos/hardware.nix (nomarchy.hardware.fingerprint.parallel).
|
||||||
|
{ lib, stdenv, fetchFromGitLab, meson, ninja, pkg-config, gettext, python3
|
||||||
|
, glib, libfprint, polkit, pam, systemd, dbus, libpam-wrapper }:
|
||||||
|
|
||||||
|
stdenv.mkDerivation rec {
|
||||||
|
pname = "pam-fprint-grosshack";
|
||||||
|
version = "0.3.0";
|
||||||
|
|
||||||
|
src = fetchFromGitLab {
|
||||||
|
owner = "mishakmak";
|
||||||
|
repo = "pam-fprint-grosshack";
|
||||||
|
rev = "v${version}";
|
||||||
|
hash = "sha256-obczZbf/oH4xGaVvp3y3ZyDdYhZnxlCWvL0irgEYIi0=";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Only the PAM module is built (the fork disables every other fprintd
|
||||||
|
# subdir), but the top-level meson still resolves the full fprintd
|
||||||
|
# dependency set — hence libfprint/polkit in buildInputs.
|
||||||
|
# pam_wrapper is only exercised by the (disabled) test suite, but the
|
||||||
|
# top-level meson marks it required whenever the pam option is on.
|
||||||
|
nativeBuildInputs = [ meson ninja pkg-config gettext python3 ];
|
||||||
|
buildInputs = [ glib libfprint polkit pam systemd dbus libpam-wrapper ];
|
||||||
|
|
||||||
|
mesonFlags = [
|
||||||
|
"-Dpam_modules_dir=${placeholder "out"}/lib/security"
|
||||||
|
"-Dsystemd=false"
|
||||||
|
"-Dman=false"
|
||||||
|
];
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "PAM module accepting password or fingerprint in parallel (fprintd fork)";
|
||||||
|
homepage = "https://gitlab.com/mishakmak/pam-fprint-grosshack";
|
||||||
|
license = lib.licenses.gpl2Plus;
|
||||||
|
platforms = lib.platforms.linux;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
# 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.dockAudio.enable = false; # keep audio put when docking
|
||||||
#
|
#
|
||||||
# Icon pack: Papirus ships by default (auto Dark/Light by theme mode) and
|
# Icon pack: Papirus ships by default (auto Dark/Light by theme mode) and
|
||||||
# is the ONLY icon pack in your closure. To use another, set the `icons`
|
# is the ONLY icon pack in your closure. To use another, set the `icons`
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
# amd.rocm.gfxOverride = "11.0.0"; # HSA override for an unlisted iGPU
|
# amd.rocm.gfxOverride = "11.0.0"; # HSA override for an unlisted iGPU
|
||||||
# fingerprint.enable = true; # fprintd; installer-set when a reader is detected
|
# fingerprint.enable = true; # fprintd; installer-set when a reader is detected
|
||||||
# fingerprint.pam = true; # use the fingerprint for login + sudo
|
# fingerprint.pam = true; # use the fingerprint for login + sudo
|
||||||
|
# fingerprint.parallel = false; # sequential prompt instead of password-or-finger
|
||||||
# npu.enable = true; # on-die NPU driver (experimental; userspace runtime BYO)
|
# 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)
|
# 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
|
# camera.hideIrSensor = true; # dual-sensor webcam: hide the IR node so apps get the color cam
|
||||||
|
|||||||
47
tools/ci-eval.sh
Executable file
47
tools/ci-eval.sh
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# CI eval tier — same coverage as `nix flake check --no-build`, bounded
|
||||||
|
# memory. One evaluator process walking every output accumulates ~6.0 GB
|
||||||
|
# peak RSS on this repo (measured 2026-07-12, eval-cache off): that
|
||||||
|
# OOM'd the whole 4 GB VPS behind git.bemagri.xyz, and can never fit the
|
||||||
|
# runner's 2 GB container cap. Evaluating ONE output per fresh nix
|
||||||
|
# process caps the peak at the largest single output (~0.93 GB,
|
||||||
|
# downstream-template-system): the heap is freed when each process
|
||||||
|
# exits. Wall time is worse (shared nixpkgs re-eval per process);
|
||||||
|
# irrelevant next to the memory cap. nomarchy-live is skipped by
|
||||||
|
# decision (see the loop below); with it out, the worst output is
|
||||||
|
# hardware-toggles at 0.99 GB. Used by .gitea/workflows/check.yml
|
||||||
|
# and bump.yml; runs anywhere (`bash tools/ci-eval.sh`).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
sys=${1:-x86_64-linux}
|
||||||
|
|
||||||
|
list() {
|
||||||
|
nix eval --raw ".#$1" \
|
||||||
|
--apply 'a: builtins.concatStringsSep "\n" (builtins.attrNames a)'
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluate() {
|
||||||
|
echo "eval: $1"
|
||||||
|
nix eval --raw ".#$1" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
for a in $(list "checks.$sys"); do
|
||||||
|
evaluate "checks.$sys.$a.drvPath"
|
||||||
|
done
|
||||||
|
for c in $(list nixosConfigurations); do
|
||||||
|
# nomarchy-live (the installer ISO) is deliberately NOT CI-gated
|
||||||
|
# (Bernardo, 2026-07-12): the ISO is built and distributed manually,
|
||||||
|
# so its guard lives with that manual flow (local `nix flake check` /
|
||||||
|
# the ISO build itself) — and it is the one output whose EVAL alone
|
||||||
|
# (~2.7 GB) cannot fit the runner's 2 GB cap (Actions run 315: every
|
||||||
|
# other output green, this one OOM-killed).
|
||||||
|
if [ "$c" = "nomarchy-live" ]; then
|
||||||
|
echo "skip: nixosConfigurations.$c (manual ISO flow — see comment)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
evaluate "nixosConfigurations.$c.config.system.build.toplevel.drvPath"
|
||||||
|
done
|
||||||
|
for h in $(list homeConfigurations); do
|
||||||
|
evaluate "homeConfigurations.$h.activationPackage.drvPath"
|
||||||
|
done
|
||||||
|
echo "ci-eval: all outputs evaluated."
|
||||||
110
tools/monitor-fallback.nix
Normal file
110
tools/monitor-fallback.nix
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# Undock blackout-rescue harness — boots the full desktop headlessly
|
||||||
|
# (softGL Hyprland, same recipe as theme-shot.nix) and replays the
|
||||||
|
# dangerous sequence: add a headless "external", soft-disable the
|
||||||
|
# internal panel (the Display menu's "Laptop screen off" command), then
|
||||||
|
# remove the external. Hyprland does NOT re-enable a disabled monitor on
|
||||||
|
# its own (verified 2026-07-12 — the session ends with zero active
|
||||||
|
# outputs), so the display-profile watcher's rescue_blackout
|
||||||
|
# (modules/home/hyprland.nix) must bring the panel back; this asserts it
|
||||||
|
# does. KNOWN FLAKE (2026-07-12, Hyprland 0.55.4): the zero-active-output
|
||||||
|
# state itself crashes Hyprland in this VM ~4/5 runs (ABRT in aquamarine
|
||||||
|
# CBackend::dispatchIdle, faster than any userland rescue) — a control
|
||||||
|
# run removing the external with the internal ACTIVE survives every
|
||||||
|
# time, so it's the zero-output state, not headless removal. A crashed
|
||||||
|
# run is that upstream bug, not a rescue regression; the assertion can
|
||||||
|
# only pass on a surviving run. Maintainer tool, NOT a checks.* gate
|
||||||
|
# (full-desktop VM — same weight call as theme-shot.nix). Run:
|
||||||
|
# nix build --impure -f tools/monitor-fallback.nix --no-link -L
|
||||||
|
let
|
||||||
|
flake = builtins.getFlake ("git+file://" + toString ../.);
|
||||||
|
inherit (flake.inputs) nixpkgs home-manager;
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
overlays = [ flake.overlays.default ];
|
||||||
|
config.allowUnfree = true;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
pkgs.testers.runNixOSTest {
|
||||||
|
name = "monitor-fallback";
|
||||||
|
node.pkgsReadOnly = false;
|
||||||
|
node.specialArgs = { username = "nomarchy"; };
|
||||||
|
nodes.machine = { pkgs, lib, ... }: {
|
||||||
|
imports = [ flake.nixosModules.nomarchy home-manager.nixosModules.home-manager ];
|
||||||
|
virtualisation.memorySize = 4096;
|
||||||
|
virtualisation.cores = 4;
|
||||||
|
hardware.graphics.enable = true;
|
||||||
|
environment.variables.LIBGL_ALWAYS_SOFTWARE = "1";
|
||||||
|
boot.initrd.availableKernelModules = [ "virtio_gpu" ];
|
||||||
|
virtualisation.qemu.options = [ "-vga none" "-device virtio-gpu-pci,xres=1920,yres=1080" ];
|
||||||
|
users.users.nomarchy = {
|
||||||
|
isNormalUser = true;
|
||||||
|
password = "test";
|
||||||
|
extraGroups = [ "wheel" "video" ];
|
||||||
|
};
|
||||||
|
nomarchy.system.stateFile = flake + "/themes/boreal.json";
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /home/nomarchy/.nomarchy 0755 nomarchy users -"
|
||||||
|
"C /home/nomarchy/.nomarchy/theme-state.json 0644 nomarchy users - ${flake + "/themes/boreal.json"}"
|
||||||
|
];
|
||||||
|
services.greetd.settings.initial_session = {
|
||||||
|
command = "start-hyprland";
|
||||||
|
user = "nomarchy";
|
||||||
|
};
|
||||||
|
home-manager.useGlobalPkgs = true;
|
||||||
|
home-manager.users.nomarchy = {
|
||||||
|
imports = [ flake.homeModules.nomarchy ];
|
||||||
|
nomarchy.stateFile = flake + "/themes/boreal.json";
|
||||||
|
nomarchy.idle.enable = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
testScript = ''
|
||||||
|
import json, time
|
||||||
|
|
||||||
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
machine.wait_for_file("/run/user/1000/hypr", 180)
|
||||||
|
machine.sleep(20) # softGL first paint settle
|
||||||
|
|
||||||
|
hy = ("su - nomarchy -c 'XDG_RUNTIME_DIR=/run/user/1000 "
|
||||||
|
"HYPRLAND_INSTANCE_SIGNATURE=$(ls /run/user/1000/hypr | head -1) hyprctl ")
|
||||||
|
|
||||||
|
def mons(all=False):
|
||||||
|
out = machine.succeed(hy + ("-j monitors all'" if all else "-j monitors'"))
|
||||||
|
return json.loads(out)
|
||||||
|
|
||||||
|
initial = mons()
|
||||||
|
assert len(initial) == 1, f"expected 1 initial monitor, got {initial}"
|
||||||
|
internal = initial[0]["name"]
|
||||||
|
print(f"internal panel: {internal}")
|
||||||
|
|
||||||
|
# "Plug the dock": add a headless output as the external monitor.
|
||||||
|
machine.succeed(hy + "output create headless'")
|
||||||
|
for _ in range(20):
|
||||||
|
if len(mons()) == 2: break
|
||||||
|
time.sleep(1)
|
||||||
|
names = [x["name"] for x in mons()]
|
||||||
|
ext = [n for n in names if n != internal][0]
|
||||||
|
print(f"external (headless): {ext}")
|
||||||
|
|
||||||
|
# Menu action: "Laptop screen off" — the exact command the row runs.
|
||||||
|
machine.succeed(hy + f"keyword monitor {internal},disable'")
|
||||||
|
for _ in range(20):
|
||||||
|
if [x["name"] for x in mons()] == [ext]: break
|
||||||
|
time.sleep(1)
|
||||||
|
assert [x["name"] for x in mons()] == [ext], f"disable failed: {mons()}"
|
||||||
|
alln = {x["name"]: x.get("disabled", False) for x in mons(all=True)}
|
||||||
|
assert alln.get(internal) is True, f"{internal} not marked disabled: {alln}"
|
||||||
|
print("laptop panel disabled, only external active — now yank it")
|
||||||
|
|
||||||
|
# Sudden undock: remove the external while the internal is disabled.
|
||||||
|
machine.succeed(hy + f"output remove {ext}'")
|
||||||
|
recovered = False
|
||||||
|
for i in range(30):
|
||||||
|
if [x["name"] for x in mons()] == [internal]:
|
||||||
|
recovered = True
|
||||||
|
print(f"recovered after {i+1}s: {internal} re-enabled")
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
print(f"monitors now: {mons(all=True)}")
|
||||||
|
assert recovered, "internal panel did NOT come back after undock"
|
||||||
|
'';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user