Commit Graph

809 Commits

Author SHA1 Message Date
060bf524af fix(idle): skip DPMS-off when docked (#127 mitigation)
All checks were successful
Check / eval (push) Successful in 5m24s
Blanking the only live output in dock mode led to a brick where wake and
even VT switch failed. Skip hypridle's 10m dpms off when any laptop
internal (eDP/LVDS/DSI) is present but not enabled; lock at 5m remains.
Undocked still blanks. Document LATER revisit for intentional
DPMS-when-docked once wake is proven.

V1: nix flake check --no-build. V3: docked idle stays lit under lock.
2026-07-15 12:51:34 +01:00
e9ab0d8d64 fix(display): docked idle wake + dump without SSH (#127)
All checks were successful
Check / eval (push) Successful in 3m44s
Recover from DPMS/lock blackout without a second machine: undock/enable
always issue dpms on; hypridle on-resume and after_sleep run
nomarchy-display-wake (zero-output undock rescue + auto-dump).
nomarchy-display-dump writes ~/nomarchy-display-dump-*.txt (SUPER+SHIFT+D
or Ctrl+Alt+F3 TTY). HARDWARE-QUEUE #127 no longer requires SSH.

V1: nix flake check --no-build. V3: docked idle on AMD box still open.
2026-07-15 12:37:00 +01:00
52d1581131 feat(power): suspend-then-hibernate 1h on battery (#115)
All checks were successful
Check / eval (push) Successful in 6m16s
Human decision: 1h delay, battery only, Preferences enable/disable.
Wire systemd suspend-then-hibernate + HibernateDelaySec=1h +
HibernateOnACPower=false when settings.power.suspendThenHibernate is on
and boot.resumeDevice is set. Lid: s2h undocked, plain suspend on AC.
nomarchy-suspend for hypridle + Power menu (live state). LUKS unlock
before encrypted hibernate so s2h→disk is not double-password.
state-bridges assert the policy; HARDWARE-QUEUE #115 + suggested order.

V1: nix flake check --no-build. V3: bag-carry on hardware.
2026-07-15 12:13:23 +01:00
e9dd3d14c5 fix(theme): dark adw-gtk3 for dark palettes — GTK dialog buttons (#98)
Some checks failed
Check / eval (push) Has been cancelled
Stylix always used the light adw-gtk3 sheet with recolored tokens; that
sheet hardcodes near-black button labels, which became black-on-dark in
Boreal GTK dialogs. Force adw-gtk3-dark (and prefer-dark) for dark mode,
and pin dialog button label colours in Stylix extraCss.
2026-07-15 12:06:38 +01:00
edc1415521 docs(RECOVERY): fix snapper cross-ref after renumber (§5)
All checks were successful
Check / eval (push) Successful in 4m21s
2026-07-15 12:00:31 +01:00
c9dd7575bf docs: explain automatic generation prune for users (#128)
Some checks failed
Check / eval (push) Has been cancelled
RECOVERY §4 documents the 14-day / keep-≥3-past policy for system and
Home Manager gens, with dry-run and timer commands. README and
REQUIREMENTS point here so install and day-2 hygiene match the product.
2026-07-15 12:00:17 +01:00
e4800d7d8e feat(nixos): prune system+HM gens after 14d, keep ≥3 past (#128)
Some checks failed
Check / eval (push) Has been cancelled
Weekly nomarchy-gen-prune deletes only generations that are both older
than 14 days and beyond the three most recent past gens (current always
kept), for system and Home Manager profiles. Stock nix.gc no longer uses
--delete-older-than. checks.gen-prune covers selection self-test and
unit wiring; doctor points at the new tool when disk is tight.
2026-07-15 11:59:26 +01:00
1f6f21249e docs(agent): #128 — 14d prune for system and Home Manager gens
Some checks failed
Check / eval (push) Has been cancelled
Same floor (keep current + ≥3 past); age window 14 days not 30; apply
independently to system and HM profiles.
2026-07-15 11:55:58 +01:00
9dac8a8b23 docs(agent): file #128 — auto-prune NixOS gens (30d, keep ≥3 past)
Some checks failed
Check / eval (push) Has been cancelled
Policy: remove system generations older than 30 days only when they are
also beyond the three most recent past gens, so rare rebuilds never lose
rollback depth. Notes existing 14d nix.gc has no count floor.
2026-07-15 11:53:50 +01:00
856a9d1a49 feat(menu): Recovery scopes (#111), System IA (#105), drop Control Center (#110)
Some checks failed
Check / eval (push) Has been cancelled
System is Connectivity / Devices / Recovery / Preferences under the same
six-entry root. Recovery lists desktop (HM), system boot, and BTRFS files
once each with cost labels. Control Center is gone: blur/gaps live in Look
& Feel; update checks and Bluetooth/Printing package toggles in Preferences
(with rebuilds). docs/RECOVERY paths updated.
2026-07-15 11:49:57 +01:00
0ba9633728 test: permanent V2 checks for airplane (#104) and fastfetch logo (#122)
Some checks failed
Check / eval (push) Has been cancelled
Move nomarchy-airplane to pkgs/ so checks can import it. checks.airplane
covers the session state machine, menu/keybind surfaces, and whole-swap
waybar parity (mocked nmcli; live radios stay V3). checks.fastfetch-logo
asserts the baked mark is sextant-based with the module flags locked in.
2026-07-15 11:42:18 +01:00
875dae9c58 feat: airplane mode (#104) + crisper fastfetch logo (#122)
Some checks failed
Check / eval (push) Has been cancelled
Airplane: runtime Wi-Fi+Bluetooth kill-switch with prior-state restore,
System menu + SUPER+CTRL+R, Waybar plane glyph (self-hides, whole-swap
parity). Fastfetch: chafa sextants at 24×12 (was block 20×10) for a
finer monogram; V2 kitty screenshots under boreal and summer-night.
2026-07-15 11:33:59 +01:00
d8e1a13d50 refactor(#107): theme-state.json → state.json, theme-sync → state-sync
Some checks failed
Check / eval (push) Has been cancelled
The machine flake's git-tracked settings file is system state, not
"theme" only — rename it to state.json. CLI becomes nomarchy-state-sync
with a nomarchy-theme-sync symlink for scripts and muscle memory.

Eval (mkFlake, doctor, lifecycle) still accepts theme-state.json; the
next write migrates to state.json and removes the legacy file.
Documented in MIGRATION.md; drop the CLI alias after release notes.
2026-07-15 11:26:59 +01:00
013403deb4 feat: close #94 installed chromium; silence Hyprland update-news
Some checks failed
Check / eval (push) Has been cancelled
Re-boot of the offline install disk proves the residual #94 pass:
chromium on PATH, https/html mime → chromium-browser.desktop, and the
browser window actually opens. Kill ecosystem update-news / donation nag
so first boot is not Hyprland's "updated to …!" dialog.
2026-07-15 11:23:07 +01:00
46adcc9989 docs(agent): file #127 — docked idle blackout, no wake, undock no panel
All checks were successful
Check / eval (push) Successful in 3m47s
Bernardo 2026-07-15: docked idle → black screens; external KB/mouse did
not wake; unplugging the external did not restore eDP; Caps Lock still
worked. Captures the "session crash" as a seat brick. HARDWARE-QUEUE
repro + journal steps for the next controlled run.
2026-07-15 11:11:57 +01:00
e08426880d docs(agent): V2 results for doctor-float + offline test-install
All checks were successful
Check / eval (push) Successful in 3m46s
Doctor-float PASS (kitty classed window). Offline test-install PASS:
nomarchy-live ISO name, first-boot themed desktop without autogenerated
banner (pre-activate bake). Disk-filter fixture for fd0 exclusion.
2026-07-15 10:36:20 +01:00
be2d0d200b feat(menu): group keybinds cheatsheet Window/Workspace/Menu/Media (#108)
All checks were successful
Check / eval (push) Successful in 3m39s
Every bind in keybinds.nix carries a group; SUPER+? renders section
headers. checks.keybinds-cheatsheet asserts all groups are valid and
every description appears in the sheet (same source as Hyprland binds).

Verified: V0 flake check; built cheatsheet shows ── Window ── / etc.
2026-07-15 10:04:33 +01:00
e6be5a5770 fix: disk picker fd0 (#112), menu Back guard (#106), offline theme (#113)
Some checks failed
Check / eval (push) Has been cancelled
- #112: list_installable_disks drops fd/loop/sr/zram/<8GiB, largest-first
  so OVMF no longer offers /dev/fd0 as the default wipe target
- #106: tools/check-menu-back.py + checks.menu-back (proved to fail)
- #113: offline theme-switch contract documented (default/pinned only);
  theme-sync run_switch adds offline-oriented hint when no network

Verified: V0 flake check; installer-safety; menu-back; py_compile.
2026-07-15 10:02:19 +01:00
748d4af414 fix(install): bake dconf, seed main, name ISO, display modes (#123–#126)
Some checks failed
Check / eval (push) Has been cancelled
Batch of existing-bug fixes from Acer/XPS install findings:

- #123: HM pre-activate creates /run/user/$UID and prefers
  dbus-run-session so dconf no longer aborts the desktop bake
- #124: main-built ISOs seed ?ref=main; patch-template rewrites
  inputs.nomarchy.url from NOMARCHY_FLAKE_URL (was set but unused)
- #125: image.baseName → nomarchy-live-….iso (not nixos-live-…)
- #126: Display mode list sorted by pixel area; toast shows hyprctl
  size; TESTING notes QEMU fixed-window scaling illusion

Verified: V0 flake check, installer-safety, baseName eval, patch-template
flakeUrl rewrite, mode-sort fixture.
2026-07-15 09:58:53 +01:00
25e498a5ef docs(agent): file #125 ISO still named nixos, #126 display res invert
Some checks failed
Check / eval (push) Has been cancelled
Bernardo: live/install ISO artifact is nixos-live-….iso (image.baseName);
live VM Display picker appeared to invert higher/lower modes — investigate
sort/apply vs QEMU scaling before fixing.
2026-07-15 09:53:14 +01:00
545e40f4d9 feat(terminal): Kitty only — drop Ghostty (#95)
Some checks failed
Check / eval (push) Has been cancelled
Ghostty's OpenGL 4.3 floor broke the Acer (HD 4000 / 4.2). Dual-terminal
support (default Ghostty + install-time Kitty fallback) was more complexity
than it was worth.

- Remove ghostty.nix / nomarchy.ghostty.enable
- Kitty always installed and themed from theme-state
- Default nomarchy.terminal = kitty; SUPER+Return, doctor, calendar,
  what-changed use kitty --class=com.nomarchy.*
- Drop install-time glxinfo probe and mesa-demos on the installer
- Docs/REQUIREMENTS no longer list OpenGL 4.3 as a terminal floor

Verified: V0 flake check; option-docs; installer-safety; template HM has
kitty, no ghostty; menu/calendar emit classed kitty launches.
2026-07-15 09:51:27 +01:00
1e648b6c49 feat(terminal): themed Kitty when OpenGL < 4.3 (#95)
All checks were successful
Check / eval (push) Successful in 3m41s
Ghostty needs OpenGL 4.3; older GPUs (Intel HD 4000) top out at 4.2.
Installer probes glxinfo and writes settings.terminal=kitty into
theme-state when below the floor. modules/home/kitty.nix enables Kitty
with the same palette, mono font, and opacity as Ghostty so the fallback
still looks like Nomarchy.

Default machines stay Ghostty-only (no kitty in the HM closure). Doctor
and calendar still invoke Ghostty by class — V3 on Acer for SUPER+Return.

Verified: V0 flake check; installer-safety; HM with terminal=kitty builds
themed kitty.conf + TERMINAL=kitty; default template has no kitty bin.
2026-07-15 09:45:32 +01:00
a09399702d docs: REQUIREMENTS.md — OpenGL 4.3 floor, disk/RAM planning
All checks were successful
Check / eval (push) Successful in 3m44s
Acer M5-481T fails Ghostty because Ivy Bridge HD 4000 tops out at
OpenGL 4.2 in Mesa; Ghostty needs 4.3 — a generation ceiling, not a
missing driver. Document that plus UEFI/x86_64, RAM, and storage floors
(Nix store generations + BTRFS snapper + hibernate swap≈RAM). Linked
from README, docs map, HARDWARE.md; #95 points here.
2026-07-15 09:40:17 +01:00
2c9df329dc docs(agent): #95 both Ghostty fails are Acer pre/post HM, XPS is fine
Some checks failed
Check / eval (push) Has been cancelled
Bernardo: XPS starts Ghostty. Aspire only fails — first attempt before
home built (OpenGLOutdated), second after home built (quiet GDK_DISABLE
exit). Kitty was the manual working shell used to run both.
2026-07-15 09:36:54 +01:00
70f2d4d54d docs(agent): #95 is two Ghostty failures, not one OpenGL floor
Some checks failed
Check / eval (push) Has been cancelled
Photo A (Acer): OpenGL 4.2 → OpenGLOutdated / SurfaceError.
Photo B (XPS): silent exit after GDK_DISABLE=gles-api,vulkan with no
OpenGL line — different machine, different root cause. A fix for only
one is not done.
2026-07-15 09:34:17 +01:00
fbc93159a6 docs(agent): queue #123–#124 + diagnose #95 from Acer/XPS install photos
Some checks failed
Check / eval (push) Has been cancelled
Real installs on Acer Aspire M5-481T and Dell XPS 9350 (2026-07-14):

- #123: HM pre-activate dies at dconf without XDG_RUNTIME_DIR on both
  machines — partial first boot (weird GTK + missing nm-applet are
  symptoms of the incomplete bake).
- #124: install seeds ?ref=v1 while writing main-era nomarchy.hardware.*,
  and v1 lags hard enough that rebuilds die with "option does not exist".
- #95: Ghostty on the Acer is OpenGL 4.2 < required 4.3 (OpenGLOutdated);
  XPS starts Ghostty fine.

HARDWARE-QUEUE V3 rows; photos stay local (.gitignore WhatsApp Image*).
2026-07-15 09:33:37 +01:00
355d8cb1a4 fix(mime): text/plain falls through to Text Editor on live (#119)
All checks were successful
Check / eval (push) Successful in 4m5s
A singleton default naming code.desktop left the live ISO with no text
handler — GIO skips missing .desktop files and offers nothing. Prefer
vscode when present, then org.gnome.TextEditor.desktop. Live also gains
mpv (video defaults had the same hole; free via the template pin).

checks.live-baseline-apps now requires every Default Applications key
to resolve to a present .desktop on live (HM + system path) and on the
default template install. Proved to fail on a vscode-only text/plain.

Verified: V2 (live-baseline-apps green + prove-to-fail); V0 flake check.
2026-07-15 09:24:48 +01:00
6fbad46bca docs(agent): #122 — sextants DO work; chase the fallback, not one font's charset
All checks were successful
Check / eval (push) Successful in 3m27s
Bernardo asked whether #122 was quick, and then caught the reason my first
answer was wrong: the mono face is per THEME (ghostty.nix:22 reads
t.fonts.mono from the theme JSON), and Boreal — the default theme and the base
state — is GeistMono Nerd Font, not stylix's JetBrains Mono. I had probed the
wrong font, then drawn a confident conclusion from a single charset.

Both conclusions inverted on measurement, so the item now says the opposite of
what it said an hour ago:

  * GeistMono covers no sextants, no braille — and no block elements or box
    drawing either. So the CURRENT logo already renders through fontconfig
    fallback on the default theme, and always has. Fallback is the normal case
    here, not the hazard I painted it as.
  * Fallback supplies sextants fine: fc-list :charset=1fb00 finds 37 families
    on a real Nomarchy box, including the shipped nerd-fonts.caskaydia-cove and
    noto-fonts' Symbols 2; fc-match under GeistMono picks CaskaydiaCove. No
    tofu. Braille: 59 families.

So --symbols sextant is available and is roughly a one-flag edit for 6x the
detail — rendered both variants to confirm the shape genuinely sharpens. The
item is re-scoped accordingly: the edit is minutes, the WORK is verification.
Two things can still make it worse and only a real terminal can say — fallback
metrics (CaskaydiaCove's sextants drawn in GeistMono's cell may misalign or
differ in stroke weight, exactly the seam that looks fine in a VM and wrong on
a laptop), and the mark being diagonal-heavy, since diagonals staircase under
any cell symbol: more resolution buys a finer staircase, not a straight line.
If the mark itself wants straight strokes that is a logo.svg design change
hitting all three surfaces, not a chafa flag — settle that first.

V0: nix flake check --no-build passes. Backlog only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:47:44 +01:00
347f6544aa docs(agent): file #122 — a crisper fastfetch logo, with the trap named up front
All checks were successful
Check / eval (push) Successful in 3m27s
Bernardo 2026-07-14: the fastfetch logo should be crisper, with straight lines.

Filed with the cause rather than the wish. modules/home/fastfetch.nix converts
the vector mark with `chafa --symbols block --size 20x10`, and that symbol class
is the chunkiness: restricted to full/half blocks, a 20x10 grid carries ~20x20
pixels of detail, so every diagonal staircases. chafa 1.18.2 ships sextant (2x3
subcells), quad and wedge — verified present today, so the cheap lever exists.

Named the trap that decides the approach, because trying it first is what
prevents a wasted round: sextants are Unicode 13 Symbols for Legacy Computing,
and if the shipped terminal font lacks them the logo renders as tofu — strictly
worse than chunky. So font coverage in the real Ghostty gets checked before a
symbol class is chosen, and if it is missing, hand-authored box-drawing art is
the honest alternative (and the only way to get genuinely straight lines; any
image→symbol converter approximates).

Also recorded that the three logo surfaces are different renderers — fastfetch,
the ISO splash, the Waybar glyph font — so this item touches one of them, while
a change to the shared logo.svg would hit all three and should be split.

Visual change, so THEME-DESIGN and the honesty rule apply: pass needs
screenshots viewed under two themes, no tofu, accent tracking intact, and the
fastfetch row grid still aligned.

V0: nix flake check --no-build passes. Backlog only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:40:54 +01:00
19ba4e2b12 fix(nixos): smartd self-gates on the hardware — the doctor was right, smartd wasn't
Some checks failed
Check / eval (push) Has been cancelled
BACKLOG #118. Bernardo booted the live ISO and the Waybar health icon was red,
reporting smartd. Everything downstream turned out to be working correctly,
which is the part worth recording: smartd's config is DEVICESCAN, and where no
drive answers SMART it exits 17 ("Unable to monitor any SMART enabled
devices"), systemd marks the unit failed, nomarchy-doctor faithfully reports a
failed system unit, and Waybar paints @bad. The doctor was telling the truth.
smartd was the bug.

Scope was never live-only, which is why this sat in NOW rather than as a live
nit: services.smartd.enable mkDefaults true on every machine and QEMU virtio
exposes no SMART, so every VM install has been booting to a health warning
about a daemon with nothing to do — and every V2 run had been showing it as
noise.

Fixed with the distro's own self-gate convention: an ExecCondition running
`smartctl --scan`, which prints nothing exactly when smartd would find nothing.
A failed condition leaves the unit inactive rather than failed. Deliberately
NOT SuccessExitStatus = 17: that would also swallow exit 17 from a machine that
does have drives, which is the entire reason the daemon ships.

V2. checks.smartd-gate boots the REAL distro module rather than a restatement
of it (its nixpkgs.config needs mkForce to yield to the test's pkgs) and
asserts both halves, because they pull in opposite directions: a gate that
never skips leaves the red icon, and a gate that always skips silently disables
drive-health monitoring on real hardware — the failure nobody notices until a
disk dies quietly. So the no-SMART node must go ActiveState=inactive, unfailed,
and absent from `systemctl --failed` (what the doctor actually reads); and the
gate's logic is driven against a scan that DOES find a device, since QEMU
cannot answer SMART honestly and pretending otherwise would test nothing. The
check was proved to fail by unwiring the condition. flake check, doctor,
hardware-toggles, live-baseline-apps, option-docs and state-bridges pass.

V3 pending: that smartd still RUNS where drives have SMART (dev box, real
NVMe). Queued with an explicit fail-condition — if it skips there, revert the
gate rather than tune it.

Also swept: #120's size table said the duplicate chromium was gone; #121 was
reverted, so it is back and the table says so.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:38:58 +01:00
0727ba6d44 revert(pkgs): leave the duplicate chromium — honestly, and with the numbers
All checks were successful
Check / eval (push) Successful in 3m21s
Bernardo on #121 (shipped in 0b464af): "Let's leave it honestly. Sounds too
much work for a negligible gain." Agreed, and his own reasoning is the
argument: the fix worked, but it substitutes into a buildCommand nixpkgs owns,
which couples us to wrapper internals and has a SILENT no-op failure mode —
so it also needed a permanent check, because nothing about a returning
duplicate breaks a build. That standing maintenance cost buys ~195 MiB of
download and ~19 MiB of disk. Boring beats clever at that exchange rate.

Removed the overlay and checks.chromium-single-closure. The template HM closure
is back to 9.38 GiB with the duplicate present — verified by closure, not by
reading the diff. overlays.default now carries a comment saying the duplicate
is known and deliberate, where the next person will look for it.

The ROADMAP entry stays, downgraded from ✓ to ✗ decided-against: it holds the
measurement, the approach that worked (self-contained -wv copy; repoint the
wrapper's share/* symlinks; .override composes with overrideAttrs), and the
commit to recover it from. The bug is real and worth an upstream patch — the
wrapper should take its desktop entry from the variant it wraps, which helps
every NixOS user with enableWideVine, not only us. Revisit locally if #120's
netinstall ships, since download is the one axis this moves.

What survives is worth more than the fix, and is stamped on #120: closure size
is not disk size and is not image size — measure the artifact. Removing a
687 MiB path moved the ISO by 8 KiB, because mksquashfs dedupes duplicate files
and auto-optimise-store hardlinks them. Without that caveat, #120's "drop the
pin, save 4 GiB" would have evaporated at the squashfs exactly as this did.

V0: nix flake check --no-build passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:32:06 +01:00
0b464af3e5 fix(pkgs): one chromium, not two — and what that is actually worth
All checks were successful
Check / eval (push) Successful in 3m32s
BACKLOG #121. nixpkgs' chromium wrapper with enableWideVine = true (what the
template ships, for DRM) runs the `-wv` copy but links its share/* from the
PLAIN unwrapped build. A symlink is a store reference, so a second 687 MiB
chromium rode along for a directory of .desktop files nothing executes.
overlays.default now points those symlinks at the copy we already run; the
template needs no change, since lib.nix, the repo pkgs and nixosModules all
apply the overlay.

Checked first, because it decided whether the item was possible at all:
whether the `-wv` copy references the plain build. It is `cp -a` of it, so it
plausibly would have — and then no wrapper change could have dropped the
duplicate. It does not. Also verified rather than assumed: `.override
{ enableWideVine = true; }` composes with the overlay's overrideAttrs (the
order usually matters), so the template's existing line picks the fix up
untouched; the wrapper still execs the -wv build with WidevineCdm present; and
share/{applications,icons,man} match stock with the .desktop byte-identical.

I claimed this was "687 MiB off every install". It is not, and measuring the
artifact instead of the closure is what corrected it:

    nominal closure          -687 MiB  (9.38 -> 8.71 GiB)
    ISO image                -8 KiB    of 8.078 GiB
    installed disk           ~-19 MiB
    cache-install download   -195 MiB

Two dedupes that closure arithmetic cannot see. mksquashfs detects duplicate
files, so the ISO had already stored the near-identical blocks once. And
auto-optimise-store (on by default here) hardlinks identical files: the two
paths share inodes — verified by stat, and du counts 639 MiB for the pair
against ~620 for one. The only thing that cannot dedupe is the wire, where the
extra path is its own 195 MiB NAR.

So this is a DOWNLOAD fix — precisely what #120's netinstall cares about — and
close to a no-op for the offline ISO that is the default today. Whether that
justifies coupling an overlay to nixpkgs wrapper internals is Bernardo's call;
it is guarded and no-ops if upstream moves, but it is his to weigh. Worth an
upstream patch regardless: the wrapper should take its desktop entry from the
variant it wraps.

The guard is the load-bearing part, because the failure mode is a SILENT no-op
that no build complains about: checks.chromium-single-closure asserts exactly
one full unwrapped chromium in the template closure, and was proved to fail by
neutering the overlay — it reports "found 2", names both paths, and warns off
the tempting "fix" of dropping enableWideVine, which silently removes DRM.

The rule this establishes, now recorded in ROADMAP and on #120 (whose entire
size table is closure arithmetic): closure size is not disk size and is not
image size. Measure the artifact.

V2: flake check, chromium-single-closure, live-baseline-apps, template-sot,
option-docs, state-bridges all pass. Three ISOs built from one tree for the
numbers above.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:26:24 +01:00
00b1e10fc3 feat(live): the live ISO gets a desktop — browser, office, editor — for +41 MiB
All checks were successful
Check / eval (push) Successful in 3m22s
BACKLOG #103. Bernardo, live ISO 2026-07-14: still no browser, no office —
"that makes a live iso useful". He is right, and it is the thing a user judges
the distro by before installing, and what they boot to rescue a machine that
won't start. It could do neither.

The live HM user now names its own set: chromium, libreoffice-fresh,
gnome-text-editor, amberol, snapshot. Firefox deliberately excluded (Bernardo's
call): chromium already owns the HTTP mime default and ships in the installed
template, so it is the browser that matches what a user gets post-install.

I asked him to choose on a "~+2 GB" premise. The premise was wrong, and
measuring rather than assuming is what corrected it: system.extraDependencies
ALREADY pins the template's HM closure into the ISO for offline installs, so
chromium/libreoffice/amberol were already in the image's store — merely absent
from the live user's profile, so nothing put them on PATH or in the launcher.
The live and template chromium resolve to the SAME store path, verified, so
reusing the template's exact `chromium.override { enableWideVine = true; }`
costs zero; a plain `chromium` would have been a second 2.5 GiB closure. Only
gnome-text-editor and snapshot are new: 9 paths, 133 MiB uncompressed. His
Firefox call still held for the right reason — it was the one item genuinely
unpinned.

V2. Measured ISO delta, both built from the same tree — the item's pass
condition: 8.038 → 8.078 GiB = +41.2 MiB (+0.50%). New
checks.live-baseline-apps asserts each app is on PATH *and* has a .desktop the
launcher can see (a binary without one is invisible, which is the failure that
matters), that HTTPS resolves to a chromium entry actually PRESENT (#94's exact
trap — a mime default naming a package nothing ships), and that firefox has not
crept back, since that is a size decision not a drive-by. The guard was proved
to fail: dropping snapshot makes it name the missing entry. flake check,
live-install-entry, option-docs, template-sot green.

V3 pending: that the apps LAUNCH needs hardware — queued on the Acer M5-481T,
from the launcher only, since that is the claim a file-level check cannot make.

Also filed, from measuring the 8 GiB rather than speculating about it:
  * #120 (NEXT, Bernardo's call) — a netinstall ISO beside the offline one.
    Starts from numbers: the offline pin is only 4.02 of 18.03 GiB uncompressed
    (~22%), so dropping it still leaves a 14 GiB desktop (~6.3 GiB compressed).
    "No pin" is not the lighter ISO; and without a binary cache for Nomarchy's
    own outputs a netinstall trades a download for a from-source install.
  * #121 — the Widevine wrapper drags a SECOND 687 MiB unwrapped chromium in
    for its share/applications alone. Pre-existing, on every installed machine.
  * #119 — text/plain names vscode, which the live ISO never ships.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:01:20 +01:00
b1c4e516c9 docs(agent): #103 confirmed on hardware — record the why, the inventory, and the size trade
All checks were successful
Check / eval (push) Successful in 3m17s
Bernardo, live ISO 2026-07-14: still no browser, no office/utility apps —
"that makes a live iso useful". Not a new item; #103 already covers it and #94
already pointed here. Recorded against #103 instead of duplicating.

Captured the rationale, which outlives the app list: a live ISO is what
someone judges the distro by before installing, and what they boot to rescue a
machine that won't start — read a wiki page, save a document off a dying disk,
check webmail. The live session can do none of that today.

Verified the inventory rather than restating the report: eval of nomarchy-live's
HM packages + systemPackages gives chromium=no firefox=no libreoffice=no
amberol=no snapshot=no text-editor=no. It does carry terminal tooling (yazi,
btop, bat, fd, ripgrep, ghostty) plus zathura and imv, so the gap is precisely
the GUI apps a non-Nix user reaches for.

Flagged the part that makes this more than a package list: the ISO-size delta
is the real trade — both browsers plus libreoffice-fresh is plausibly +2 GB on
an image people download, and squashfs can't be trimmed afterwards. Listed the
options (both browsers / Chromium only / lighter office for the live medium)
and asked for measured deltas per option, noting that nomarchy-live and the
installed profile need not carry the same set — the live medium is the one
with a size ceiling.

V0: nix flake check --no-build passes. Backlog only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:30:38 +01:00
334354a103 docs(agent): file #118 — smartd fails with no SMART device, reddening the health icon
Some checks failed
Check / eval (push) Has been cancelled
Bernardo tested the live ISO and saw a smartd error on the Waybar doctor
module. Filed with the cause rather than the symptom: reproduced headlessly in
a scratch runNixOSTest whose node's only config is services.smartd.enable =
true — the same block default.nix:166 mkDefaults on for every machine.

  smartd: In the system's table of devices NO devices found to scan
  smartd: Unable to monitor any SMART enabled devices. Exiting.
  smartd.service: Main process exited, code=exited, status=17/n/a

Everything downstream is working correctly: systemctl --failed lists
smartd.service, so the doctor's generic failed-unit check reports it and
#custom-doctor goes @bad. The doctor is telling the truth — smartd is what
needs fixing.

Scope is wider than the live ISO, which is why it sits in NOW with the other
live findings rather than as a live-only nit: smartd mkDefaults true on
`nomarchy` too, and QEMU virtio disks expose no SMART, so a plain VM install
boots to a red health icon out of the box. Every V2 QEMU run has been showing
this.

The fix direction is the distro's own self-gate convention: `smartctl --scan`
prints nothing on such a machine, so an ExecCondition leaves the unit inactive
instead of failed and real SMART failures still surface. Recorded explicitly
that SuccessExitStatus = 17 is the wrong fix — it would also swallow smartd
dying on a machine that does have drives, which is the reason the daemon
exists.

V0: nix flake check --no-build passes. Backlog only — no fix attempted here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:27:59 +01:00
0bb75b05a9 docs(agent): #117 is an input to #110, not work — Control Center is being retired
All checks were successful
Check / eval (push) Successful in 3m12s
Bernardo's triage: the Control Center is going away (#110), so making its
toggles rebuild would be building on a condemned surface. #117 stays filed
because the observation outlives the tool — whatever menu inherits those rows
must not reprint "requires rebuild" — but it is marked not-standalone so no
agent picks it up as work. The previous entry's "next suggestion: #117" was
wrong and the correction is appended rather than rewritten.

#110's migration matrix gets what #116 turned up, which changes its shape:
its Bluetooth and Printing rows write settings.{bluetooth,printing}.enable,
and those keys only became live bridges in a9f3a64 — so those two toggles
never worked for any user. There is therefore no behavior to preserve, but
the keys and their bridges are real now and outlive the TUI: dropping the
rows must not drop the keys, or a machine whose theme-state.json already says
bluetooth.enable = false silently flips back on at its next rebuild.
checks.state-bridges covers exactly that and fails loudly if the bridges go
with the tool. Updates (settings.updates.enable) is read in
modules/home/options.nix — the HM side, where nomarchy.settings genuinely
exists — so it needs none of this; verified, not assumed.

V0: nix flake check --no-build passes. Backlog/journal only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:21:41 +01:00
a9f3a642ee fix(nixos): two toggles that reported success and did nothing — wire the state bridges
All checks were successful
Check / eval (push) Successful in 3m16s
BACKLOG #116. `nomarchy.settings` is declared in exactly one place —
modules/home/options.nix:412, the Home Manager side. On NixOS the attribute
does not exist, and `or <fallback>` swallows the missing-attribute error, so
four options that "defaulted from the state" had silently been their fallback
on every machine ever built.

The item said three options, and called them benign. Both halves were wrong,
and re-grepping rather than trusting the account is what found it:

  * There were four. The original enumeration read options.nix instead of
    modules/nixos/ and missed services.nix's printing.enable — the same
    mistake in miniature as the bug it was filing.
  * Two were live user-facing bugs. Control Center is shipped
    (default.nix:337) and reachable from the menu; its Bluetooth and Printing
    toggles wrote settings.{bluetooth,printing}.enable and printed "requires
    rebuild", and the rebuild changed nothing. They had never worked.

The fix is one shape, now uniform: the option declares a STATIC default, and
the implementing module reads the state via theme-state-read.nix (fails closed
on bad JSON, unlike greeter.nix's raw fromJSON — also moved onto the reader
here) and mkDefaults it behind `mkIf (state != null)`. An absent key leaves the
option default as the single source of the fallback; a hand-set system.nix
value still pins it. batteryChargeLimit gets no bridge and loses its dead read:
power.nix's oneshot already reads that key with jq at RUNTIME and prefers it
over the baked value, which is why that menu worked all along.

V2. The bug is proved real before/after on the same flipped state: BEFORE,
bluetooth stays true and printing stays false; AFTER, both flip, and a hand-set
value still outranks the state. Nothing in a build fails when a bridge dies, so
the guards are the point — checks.state-bridges asserts 11 eval cases, and
checks.printing-from-state boots a VM whose only input is the state file and
waits for a running cups.service. The guard was itself proved to fail:
re-breaking the bluetooth bridge makes it throw, naming both assertions. A
check that passes whether or not the property holds is worse than no check
(625b7e3). flake check, option-docs, template-sot, downstream-template-*,
installer-safety, hardware-toggles and battery-charge-limit all pass.

No V3: the mechanism is fully proved headlessly. Design record in ROADMAP §
NixOS-side state bridges (#116); new #117 (PROPOSED) for the control-center
toggles still leaving the rebuild to the user.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:03:12 +01:00
7353568115 docs(agent): #116 undercounted the phantom reads, and two of them are live bugs
All checks were successful
Check / eval (push) Successful in 3m2s
Re-grepped the tree instead of trusting eb38008's own account of it. Two
corrections, both in the direction of worse:

A fourth read: services.nix:80 `printing.enable`. The original enumeration
only looked at options.nix, which is the same mistake in miniature as the
bug it was filing — `or` hides a dead bridge, so the only honest check is a
grep of all of modules/nixos/, which the item's own pass condition already
said and its own list did not do.

"Benign today" was wrong about Bluetooth. The TUI control center is shipped
(default.nix:337) and reachable from the menu's Control Center row, and its
Bluetooth and Printing toggles write settings.bluetooth.enable /
settings.printing.enable and print "disabled (requires rebuild)". Nothing
reads either key, so the fallback wins and the rebuild changes nothing. Two
user-facing toggles that report success and do nothing — that is a live bug,
not a dead key. batteryChargeLimit stays benign for the stated reason: its
menu patches the baked option instead.

nomarchy.settings is declared in exactly one place, modules/home/options.nix:
412 — verified, not assumed. V0: nix flake check --no-build passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:45:00 +01:00
50bfdb99d0 docs(agent): reconstruct the five journal entries a crashed session owed
All checks were successful
Check / eval (push) Successful in 3m3s
A session crashed after shipping the fingerprint/idle/menu batch. The work
itself survived intact: 625b7e3, e2de906, fbbeac6, 9792976 and eb38008 are
committed and pushed, and each did its own sync sweep — BACKLOG #115/#116
filed, HARDWARE-QUEUE carrying the toggle test and the removed fprintd trap,
docs and template updated. Only the journal was owed, so the next session
would have read docking round 7 as the head and missed five iterations of
context, including two live bugs the batch exposed.

Reconstructed from the commit messages rather than from memory, and marked
as such in the file — the commits are the record, this is an index into
them. The entries keep what the messages proved and what they left pending,
notably the lesson from fbbeac6 (the lock field has never been visible; I
assumed it was and reasoned from there) and the one from 625b7e3 (a check
that passes whether or not the property holds is worse than no check).

V0: nix flake check --no-build passes. Docs-only.

Noted, not done: the journal is at 56 entries against its own ~25 rotation
threshold — it was already 51 before this, so the tail is overdue for
JOURNAL-ARCHIVE.md. That is its own commit, not this one's business.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:40:24 +01:00
eb38008ebb feat(menu): one fingerprint switch, an auto-login toggle, and the state bridge they needed
All checks were successful
Check / eval (push) Successful in 3m7s
Bernardo, post-reboot: "Use for login" was the wrong question. Whether the
finger works is one decision, not two, and whether login prompts at all is
a different decision that was never in the menu.

System › Fingerprint is now a single Fingerprint (on/off) switch, leading
the menu with enroll/list/verify/delete as the plumbing behind it. It
writes the one settings.fingerprint.pam key, and modules/home/idle.nix now
defaults idle.fingerprint from that same key — so the lock screen and
login/sudo move together instead of drifting apart the way they did until
e2de906. nomarchy-fingerprint does the two rebuilds this needs (sudo
system for PAM, home switch for hyprlock) and refuses to turn on with no
finger enrolled.

System › Auto-login is new (nomarchy-autologin), and it is what decides
whether anything is asked at boot: auto-login on means no prompt whatever
the fingerprint switch says; off means the greeter asks, for a password or
a finger. Installer-seeded ON for LUKS machines — the passphrase already
gates the disk — and off without it, where the greeter is the only thing
between power-on and the desktop.

Both had to become state-owned to be toggleable at all, which surfaced two
real bugs:

  * nomarchy.system.greeter.autoLogin defaulted from
    `config.nomarchy.settings…` — an attribute that exists ONLY on the Home
    Manager side. On NixOS it is absent and `or null` swallowed the error,
    so the default silently evaluated to null on every machine ever built.
    That is why the installer baked a Nix line: the state path never
    worked. Now read via theme-state-read.nix (the hardware.nix/timezone.nix
    pattern) and mkDefault'd, so the menu owns it and a hand-set line still
    pins it. Two more options read the same phantom bridge — BACKLOG #116.
  * `theme-sync get` printed Python's "None" for a JSON null, so every
    `case … null)` a caller writes would miss. Now prints "null", as the
    comment above it already promised for booleans.

The installer seeds the state instead of emitting the system.nix line,
because that line outranks the state and would strand the toggle.

V1 (V3 pending: HARDWARE-QUEUE). nix flake check --no-build, installer-
safety and option-docs all pass. Proved by eval/build, not assumed: a state
carrying autoLogin yields greetd initial_session {"user":"bernardo"}, the
template state (no autoLogin) yields none, and a hand-set null beats a state
that says otherwise; a state with only fingerprint.pam=true — nothing set by
hand — renders the hyprlock auth.fingerprint block; both new tools pass
bash -n and land in systemPackages (nomarchy-fingerprint only with a
reader); the patcher writes settings.greeter.autoLogin and no system.nix
line; and the get round trip prints null, so the menu reads "Auto-login
(off)" where it would have read "(on)".

The reader itself, the two rebuilds, and the reboot are hardware — queued.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 14:34:56 +01:00
9792976b11 feat(idle): a real lock bind, and a lock screen that speaks in sentences
All checks were successful
Check / eval (push) Successful in 3m7s
Two nits from the post-reboot hardware pass.

Super+Escape never locked anything — there was no lock bind at all, only
the power menu's Lock row and the idle timeout. (Escape is unusable here
anyway; keybinds.nix has said so since the power menu moved to Super+X,
and an unswallowed Escape just prints its sequence into the terminal.)
Lock now lives at Super+Ctrl+L: the one row in that family that is not a
menu module, because bare Super+L collides with common app binds. It
dispatches loginctl lock-session, the same path the power menu's Lock row
takes, so logind marks the session locked and hypridle's lock_cmd remains
the single place that decides what a lock looks like.

The lock prompt read as clipped fragments ("password… or scan finger" /
"scanning finger…") next to a 64px clock. Both messages are sentences
now, and the input field's placeholder is sentence-cased to match — the
two can share the screen, so they cannot read as two different voices.

V1 + V3 pending: nix flake check --no-build passes; the built home
generation carries `bind=$mod CTRL, L, exec, loginctl lock-session`, the
new ready/present messages (built with idle.fingerprint forced on, since
the in-repo config leaves it off), and a "SUPER + CTRL + L  Lock screen"
row the cheatsheet generated on its own from the shared list. The reader
itself only exists on hardware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 14:18:29 +01:00
fbbeac6c79 fix(idle): put the fingerprint hint where it can actually be seen
All checks were successful
Check / eval (push) Successful in 3m0s
e2de906 fixed the invisible reader and left an invisible message. The lock
screen is deliberately just the clock: the input field fades out while empty
(hyprlock's fade_on_empty default) and only appears once you type. So
$FPRINTPROMPT in the field's placeholder rendered faithfully into a widget
nobody sees until they have already given up on the finger and started typing
— which Bernardo reported as "unlock worked, but still no message", the same
symptom as before the fix, for a completely different reason.

I had assumed the field was visible and reasoned from there. It has never been
visible; "it has always been like this" is what cracked it.

Both candidate surfaces were tested on hardware rather than picked: a label
does expand $FPRINTPROMPT (so the line is live — "password… or scan finger",
then "scanning finger…" on touch), and fade_on_empty=false would also work.
Bernardo chose the label: it keeps the clock-only look, where the always-on
field would have made opting into fingerprint mean permanently gaining a
widget the design never had.

subtext-on-base is the palette's secondary-text role and is already held to a
3.0 floor on all 21 themes by checks.theme-contrast — the guard that exists
because two themes once shipped subtext == base and made hint text invisible,
which is the same bug as this commit, one layer down. The placeholder goes
back to its exact previous string, so the non-fingerprint path is unchanged.

Verified V2: nix flake check --no-build; checks.theme-contrast and
checks.option-docs pass. V3: Bernardo confirms the line reads on the real
lock screen — the mechanism itself is already proven on his reader.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:05:50 +01:00
e2de9062d8 feat(idle): unlock by fingerprint, and say so — hyprlock never used the PAM flag
All checks were successful
Check / eval (push) Successful in 2m59s
Bernardo tested the lock screen after turning `fingerprint.pam` on: sudo took
a finger, hyprlock did nothing. The PAM stack was not the problem — grosshack
sits in /etc/pam.d/hyprlock at order 11400 exactly like sudo's. hyprlock just
never asks it: its PAM conversation runs only on submit, so a module that
wants to prompt *while* polling the reader never gets a turn. hyprlock has its
own fprintd-over-D-Bus backend instead, behind `auth:fingerprint:enabled`,
which nothing in the distro ever set. So `fingerprint.pam = true` promised
"login + sudo" and silently skipped the surface you meet most often.

The second half is the one worth having: enabling the backend alone yields a
reader that works while the field still reads "password…". $FPRINTPROMPT is
the ONLY slot hyprlock renders the ready/scanning/failed messages into, and
ours hardcoded the placeholder — proven on hardware, where a test config with
the backend on unlocked by finger and said nothing whatsoever. A feature that
works and cannot be discovered fails VISION's "discoverable without reading
the README" either way, so the placeholder now carries $FPRINTPROMPT and the
messages mirror sudo's "Enter Password or Place finger".

New `nomarchy.idle.fingerprint` (default false), mirroring
`nomarchy.hardware.fingerprint.pam` the way `nomarchy.keyboard.layout` mirrors
`services.xserver.xkb.layout`: hyprlock is configured in standalone Home
Manager, which has no `osConfig` to derive the NixOS side from. Opt-in rather
than implied, because with no reader hyprlock advertises a scan that cannot
happen. Documented in the template (SoT for opt-in comments) and README —
option-docs caught the missing row, which is the check doing its job.

Verified V2: nix flake check --no-build; checks.option-docs passes (failed
first with "undocumented option", as it should). V3 pending: Bernardo applies
it and confirms the field reads "password… or scan finger" and both factors
work — the mechanism itself is already proven on his reader via a test config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:58:53 +01:00
625b7e38a3 docs(hardware): sudo path of parallel fingerprint passes; drop a check that cannot fail
All checks were successful
Check / eval (push) Successful in 3m4s
Bernardo, AMD dev box, gen 422, fingerprint.pam = true (parallel default):
one prompt, typed password works, sensor works, wrong-finger x3 falls back to
password. Three of the item's counts confirmed on the real reader.

The fourth — "password keeps working with fprintd stopped" — is removed as a
trap rather than checked off. fprintd is D-Bus activated, so `systemctl stop`
does not keep it down: PAM asks D-Bus, D-Bus restarts it. Bernardo ran it and
reported that *the fingerprint still worked*, which is precisely the evidence
that the daemon came back and the dead-reader path was never exercised. A
check that passes whether or not the property holds is worse than no check.

The property itself needs no hardware: password is `auth sufficient` at order
11700, independent of the fprintd rule at 11400, so no fprintd failure can
gate it — visible by eval on the rendered pam.d text, and asserted in the VM
with no reader by checks.hardware-toggles. Doing it for real would need
`systemctl mask`, and masking a working machine's auth daemon to re-prove an
evaluated invariant is not a trade worth making.

Still open on this item: hyprlock, the greeter (needs the reboot), and
`fingerprint.parallel = false` restoring sequential behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:48:16 +01:00
e4404e0162 test(docking): prove the keyboard-restore path; the reload clobber is real
All checks were successful
Check / eval (push) Successful in 3m10s
The last unproven half of bec826b, and it needed a keyboard that stays behind
when the dock leaves. The dev box's by-tech hangs off the BenQ's own hub, so
it always walked away with the cable — a Logitech K400 with its receiver in
the laptop is the real case, and Bernardo happened to plug one in.

Both halves measured on hardware (dev box, 2026-07-14):

  - PRIMARY: across a real unplug + the fixed transition, the K400 kept its
    remembered `us`. Panel back, layout intact.
  - CONTROL: a bare `hyprctl reload` with no restore took the same board from
    `us` to the session's `gb`.

The control matters more than the pass. That reload clobbers runtime
`device[<name>]:kb_layout` was an inference from the mechanism when
restore_keyboards was written — reasonable, but unmeasured, and this session
has already shown what unmeasured reasoning about this watcher is worth.
It holds: without that call the undock's escalation would have silently
broken the layout of every keyboard that outlives the cable, trading a black
panel for a keyboard that types the wrong characters at the password prompt.

Verified V2 again after the comment/doc edits: nix flake check --no-build,
checks.docking-ux, shellcheck clean.

Round 8 now asks only for what is still open: relogin (the watcher calls the
transition by baked store path, so exec-once means it runs the old build
until then) and repetition — 5+ unplugs, one with the lid shut.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:35:23 +01:00
bec826baf0 fix(docking): the undock keyword is inert, not raced; escalate to reload
All checks were successful
Check / eval (push) Successful in 3m10s
Round 7 FAILED: 5-6 consecutive unplugs on the dev box, the panel never came
back. Round 6's retry could not have worked, because round 6's diagnosis was
wrong — and so was the "phantom re-add" theory I brought to this session.

With ZERO enabled outputs — panel disabled by the dock, external gone —
Hyprland 0.55.4 accepts `hyprctl keyword monitor eDP-1,preferred,auto,1`,
prints `ok`, exits 0, and never flushes it: the rule waits for a DRM event
that is not coming. It is inert, not raced. Retrying it 25x/poll for 6 polls
bought 30s of black screen and nothing else.

What made this survive two rounds is worth more than the fix: every
`transition=undock result=ok` in the round-6 journal was Bernardo plugging
the cable back in because the screen was black. His hotplug flushed the
queued rule and the next poll took the credit. Success was indistinguishable
from the user working around the failure, so the logs confirmed whichever
story we brought to them. Ten minutes of probing on hardware killed both.

Probed live 2026-07-14 (dev box, cable out): keyword inert across 4s and 5s
in two runs; `dispatch forcerendererreload` inert too; only `hyprctl reload`
escapes — 99ms and 289ms. So: issue the keyword (enough whenever another
output is still enabled, e.g. the menu's Dock mode), and escalate to reload
only when `monitors` proves it inert. After a reload, re-assert the rule so a
config that parks the panel off cannot undo the undock, and restore per-device
keyboard layouts — a reload re-reads the config and drops the runtime
`device[<name>]:kb_layout` an external board depends on. The menu's `enable`
now proves itself against `monitors` too, instead of cheering for an exit code.

Verified V3 (partial): the fixed transition driven through a real unplug —
panel on, workspaces 1-3 home, 1.8s, `keyword=inert escalate=reload` ->
`enable=via-reload` -> `result=ok`. V2: nix flake check --no-build; docking-ux
+ dock-audio; monitor-fallback.nix; shellcheck clean on display-transition.

NOT proven, and queued as HARDWARE-QUEUE round 8: the watcher-driven path
(exec-once + baked store path = relogin required, the round-5 trap) and
repetition. The keyboard-restore path is untested — the dev box's keyboard
hangs off the dock's hub, so it leaves with the cable; round 8 adds a check
with a keyboard in the laptop. The VM harness still cannot reach any of this:
QEMU aborts Hyprland if the last active output is deleted while the DRM output
is disabled, which is the same zero-output degeneracy from the other side.

New BACKLOG #114 (PROPOSED): tuigreet ignores per-device keyboard layouts —
a VT has one keymap, so the greeter cannot honour `device[]:kb_layout`.
Bernardo hit it logging out while docked; not a regression, a design gap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:28:06 +01:00
28be6779a7 docs(hardware): round 6 settles 4 of 5 counts; headphones still untested
All checks were successful
Check / eval (push) Successful in 3m15s
Bernardo's round 6 report plus the keyboard result confirms on hardware:
wallpaper on the external, automatic dock mode, audio reaching the
monitor, and the external keyboard prompting exactly once (was four).

That leaves headphone jack-follow as the only count from rounds 4-6 never
tested at all, so it gets its own entry rather than staying buried in a
list of settled ones. Fold it into the round 7 session — the dock is
already in hand and the two checks don't interfere.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:37:52 +01:00
44ecc9094e fix(docking): retry the undock; a lost keyword left the panel black
Some checks failed
Check / eval (push) Has been cancelled
Round 6 passed on the plug side but unplugging the cable left the laptop
panel black until it was re-docked. The journal named it: across three
undocks, two logged result=enable-timeout and one result=ok — a race, not
a logic error.

Two halves, both needed:

  - `hyprctl keyword monitor eDP-1,preferred,auto,1` is accepted (exit 0)
    while Hyprland is still tearing down the departing external, then
    silently dropped. Re-issue it on every poll for 5s and keep `monitors`
    listing the panel — not hyprctl's exit code — as the only proof.

  - The watcher fired the undock once off the removal event and marked the
    departure handled regardless (`|| true` + awaiting_lid_open=1). Since
    known_outputs had already moved, the set-changed guard never reopened
    and nothing retried, so one lost keyword cost the panel until re-dock.
    The undock is now a queued invariant driven on the 1s tick, bounded at
    6 attempts so a genuinely unenableable panel can't pin the inhibitor.

The VM harness cannot reach this race: QEMU's headless backend aborts
Hyprland if the last active output is deleted while the DRM output is
disabled, so the test must undock before removing DP-1 and never collides
the two. Noted there, and V3 queued as round 7 — unplug 5+ times, since
the old failure only hit ~2 in 3.

Verified: V2 — nix flake check --no-build; monitor-fallback.nix exit 0 in
57s; shellcheck clean. The fix itself is V3 pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:35:12 +01:00
f625c0eaf4 feat(waybar): show the layout indicator only when the layout is ambiguous (#109)
All checks were successful
Check / eval (push) Successful in 3m27s
The indicator answers "which layout am I typing in?", so it is worth bar
space exactly when that question has more than one answer. It was gated on a
per-device override merely *existing*, which is the trap this box walked
into: the junk rows a keyboard's extra HID collections leave behind are all
remembered at the session layout, so they added no second answer and still
raised an indicator that could only ever show one value.

Gate on layouts rather than devices, and read the runtime-remembered
settings.keyboard.devices alongside the declared option — the new-keyboard
watcher writes those without a rebuild and they only graduate into
keyboard.devices later, so a bar reading the option alone was wrong for
exactly as long as the memory was fresh, which is when it matters most.

keyboard.layouts is now excluded on purpose: hyprland.nix keeps it as the
pool the picker offers first and deliberately never merges it into kb_layout,
so listing candidates cannot make the current layout ambiguous. A device
actually using one of them still does.

All four whole-swaps list the module unconditionally, so a single-layout
setup saw on a swapped bar what the generated bar hides — the parity rule was
already broken. Filter a swap through the same answer instead of asking four
hand-written files to re-derive it.

Logic is pure in modules/home/waybar-language.nix, matching monitor-rules /
dock-audio-rules; checks.waybar-language unit-tests both halves against
boreal's real swap, including a fixture-drift guard so the parity assertion
cannot pass vacuously.

Verified V2: new check passes, docking-ux and theme-wholeswap still pass,
`nix flake check` is clean, and the live config's generated bar now resolves
modules-right without hyprland/language on its single-layout default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:10:48 +01:00
ce480f3669 fix(docking): dock automatically; repair cards parked on pro-audio
All checks were successful
Check / eval (push) Successful in 3m9s
Bernardo's retest on the AMD dev box. Two of the four reports were never
running the new code — both watchers are Hyprland `exec-once`, so a
`nomarchy-home` switch leaves the old processes alive until relogin; only
`nomarchy-dock-audio`, a user service, had picked the fix up. The journal
showed the old watcher's `event=monitoradded` next to the new audio tool's
`result=no-available-sink`, which is what gave it away. The other two were
real.

Automatic dock mode did not exist. The watcher auto-*undocked* when the last
external left, but on plug it only ever matched a display profile — and with
no profile saved and displayProfileAuto unset it did nothing at all, while
`Dock mode` was reachable only from the menu. Half the pair being automatic
is what made the dock feel broken: the session came home by itself but never
left. Mirror it, letting a matched profile win as the more specific layout.

The BenQ's audio was unreachable by any means because WirePlumber had the
GPU's DisplayPort card pinned to `pro-audio`, whose raw `pro-output-N` sinks
carry no ports, no routing and no volume — so no sink matched, nothing was
selectable, and the profile is stored per card and so survived forever. The
monitor's own USB card is input-only (every profile has sinks: 0), leaving
DisplayPort as the only path to its speakers. Repair the card when a plug
finds nothing routable, which is both the symptom and the only moment worth
touching profiles: narrow to cards carrying an *available* HDMI/DP port —
the monitor asking for audio over the cable — so the internal analog card is
never touched and a considered pro-audio setup elsewhere survives.

Verified V2: checks.docking-ux and checks.dock-audio pass with guards for
both; the repair filter picks exactly the GPU card → HiFi against the dev
box's live graph, leaving the BenQ USB, internal analog and Bluetooth cards
alone. Hotplug itself still needs hardware — HARDWARE-QUEUE round 6, which
now leads with the relogin requirement that invalidated round 5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 10:56:52 +01:00