#139 — calcurse/doctor floated full-screen. Two faults stacked, which is
why it looked like the float rules were dead:
1. Hyprland 0.55.4 silently ignores percentage `size` rules. Matrix on
hardware (both orders, kitty memory off): `size 60% 65%` never
applies, `size 1536 936` applies either way, configerrors empty both
times. Rule order was a red herring.
2. Kitty defaults to remember_window_size=yes and replays the last OS
window's size into every float — so after a tiled terminal, a sheet
opens maximized. This is the Ghostty regression: Ghostty had no such
memory, so fault 1 stayed invisible until #95.
Fix: remember_window_size=no (floats must be deterministic) and the sheets
ask for their own size — term-sheet.nix reads the focused monitor and
hands kitty the px, because px in a rule cannot mean "a fraction of *this*
screen" and one pair cannot serve 2560x1440 and the 1366x768 Acer (#131).
Those windows keep float/center and deliberately carry no size rule.
#141 — the updates click did nothing: whole-swaps hand-wrote
`sh -c '$TERMINAL …'` and Waybar has no TERMINAL (home.sessionVariables →
login shells only; the bar is spawned by Hyprland). Fixed at the root:
`nomarchy-updates upgrade-window` opens its own window, so all five call
sites name one env-free command and a theme file stops having an opinion
about terminals. checks.waybar-swap-env guards the class.
Bernardo asked mid-task for the update window to float — it is now the
third caller of the sheet helper (com.nomarchy.updates, 45%x50%).
V3 on the dev box: calendar 1536x936, doctor 1408x1008, updates 1152x720
— exactly the intended fractions, floating and centred; #141 driven under
`env -i` with no TERMINAL, matching Waybar's real environment; the guard
proven by reintroducing the bug. Acer V3 queued.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo: every rebuild while docked re-lights the panel and throws a
workspace onto it; he re-picks Dock mode by hand. Static diagnosis, three
steps, each already proven by our own code:
- dock mode is runtime-only (`keyword monitor <internal>, disable`,
hyprland.nix:205) and absent from state.json — the one display setting
outside the in-flake state;
- the config's only monitor line is the catch-all `,preferred,auto,1`, so
a reload re-enables every output. The undock path already uses `hyprctl
reload` AS the hammer that re-enables a disabled panel (hardware-proven
2026-07-14) — so a rebuild performs an undock by construction;
- reconcile diffs `monitors all`, which lists disabled monitors, so the
set is unchanged across the flip and the watcher never sees it. Blind
spot of round 4/5's "the output set is the truth".
Notes the trap: baking the disable into the config is a zero-output
session at login (the #100/#127 brick). Persist intent, re-assert from
the watcher, gate on an external being present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo's Meet calls lost their microphone because our own dock-audio
reprobe restarted PipeWire/Pulse/WirePlumber on every monitoradded (#100).
Chromium's audio service does not reconnect when the server disappears
under it, so it kept running and enumerated nothing; Zoom, started fresh
into a settled graph, looked fine and made it read like a browser bug.
reprobe now escalates cheapest-first — select → repair a parked card's
profile → restart the graph — so an ordinary plug never touches the graph
and no client loses its connection. wait_for_dock_sink polls for the sink
the plug is about to publish, so "not yet" is not mistaken for "not ever"
and cannot fall through to the restart. The restart stays as rung 3 for
the codec that only publishes its route after a re-probe: the one claim
the original comment made, and the only one it never demonstrated.
Verified on the dev box against the live graph: rung 1 selects the same
sink the restart-first path picked (HiFi__HDMI1__sink) in 2.0s instead of
~3.5s with pipewire/pulse/wireplumber MainPIDs unchanged; rung 3 reachable
and restarts+retries. Diagnosis proof was ordering across four boots —
restart-after-Chromium in both broken sessions, 24s before it in the
healthy one — and Bernardo reproduced on demand with a dock/undock.
Sweep: BACKLOG #138 deleted, ROADMAP gets the design record (incl. the
stale restart-first sentence in the #100 entry), V3 queued on the dev box
(relogin first — the watcher is exec-once), journal entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo's hypothesis (pipewire restarts, Chromium never reconnects) is
right, and the journal names the restarter: dock-audio.nix:153 runs
`systemctl --user restart pipewire pipewire-pulse wireplumber` on every
monitoradded (#100). Chromium's audio service does not reconnect, so it
enumerates nothing — Meet's "no mic or speakers".
Ordering across four boots is decisive, and Bernardo asked for exactly
this check: restart-after-Chromium in both broken sessions (boot -3
14:06:49 → 14:07:52; boot -2 11:10:34 → 11:31:22), restart-BEFORE in
today's working one (08:04:19 vs 08:03:55, a 24s miss). Hibernate is not
the cause, only how a Chromium lives long enough to meet a plug event.
Retiers off [human] — no repro needed, and it predicts a live one:
dock/undock now should break Meet in this session. Fix is ours, not
Chromium's: the graph restart is inherited from "the old working flake",
never shown necessary, and repair_dock_cards already handles the parked-
card case without it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo's 2026-07-16 observations, filed with the diagnosis done on the
machine rather than as symptoms:
- #137 plymouth: unindexed Window.Get* → whole splash placed on head 0's
geometry; off-centre on any second monitor (LUKS prompt included).
- #138 [human] Meet claims no mic/speakers: audio graph + Flatpak sandbox
ruled out live; needs the browser named and the failure in front of us.
- #139 terminal floats full-screen: % size rules silently no-op on
Hyprland 0.55.4 (absolute px applies), and Kitty replays its remembered
window size — the second half is the Ghostty regression from #95.
- #141 waybar updates click dead: whole-swaps use $TERMINAL, which is
absent from Waybar's env; 4 jsonc lines, a parity break not a bug.
- #140 PROPOSED: Ask Claude → web chat instead of the claude-code REPL.
Also gives #127 its "if diagnosis fails" branch: a screensaver while
docked, which is the panel-protection consolation prize, not a new item.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewing 060bf52 at Bernardo's request. The call is right — the failure
mode is a brick (no wake, no VT, hard power-off with the btrfs risk that
carries), so prevent-over-recover beats a lit panel. Three things it leaves
open, now tracked instead of remembered:
#135 — the mitigation is hardcoded policy with no way back. Docked externals
never blank and hyprlock has no timeout, so they stay lit indefinitely:
~60-100W nightly and OLED burn-in from a near-static lock screen. Add
settings.idle.dpmsWhenDocked (default false = today's behaviour), mostly so
the mitigation has a named surface to DELETE once wake is proven, rather
than a LATER note that quietly becomes the product. Ordered after the
hardware test: if the mitigation is withdrawn, the key never exists.
#136 — the gate is clamshell, not "docked". It skips only when an internal
is present-but-not-enabled; lid-open docked still falls through to dpms off.
Commit body, BACKLOG and journal all claim the broader "docked". Either the
scope is deliberate (fix the wording) or the mitigation misses half the
docked cases. Bernardo's call; the hardware test settles it.
Also recorded in #127 the caveat that outranks both: the fix is a cure for
an UNCONFIRMED cause. If DPMS-off is not the trigger, we hold the cost and
still have the brick.
No code change — #127 stays [blocked:hw] pending the hardware run.
Bernardo wants a newer LM Studio and rejected the only mechanism that works
today (a hand-pinned fetchTarball rev+sha256 in home.nix — verified working,
but a second unlocked pin and exactly the Nix expertise the distro exists to
spare its users). He is taking stable 0.4.15-2 for now.
Records why there is no seam (mkFlake takes src/username/hardwareProfile/
system; homeConfigurations uses mkFlake's own pkgs, so a system.nix overlay
cannot reach a home package), the sketch he asked for (unstable.lmstudio,
maybe stable. as an explicit synonym), and the shape that keeps the one-input
promise: carry nixpkgs-unstable upstream and expose it via overlays.default,
so downstream stays single-input and locked.
Costs left open for triage, not decided: two-channel lock churn against the
full-checklist rule, closure duplication (measured: the two lmstudio build
closures share 690 of ~3.7-4k paths), the weakening of "tested together
upstream", whether an unreferenced input should lock at all, and whether the
seam covers system.nix too. Cheaper fallback noted (mkFlake `overlays ? []`).
PROPOSED per BACKLOG rules — awaiting triage, not implemented.
Verifying Bernardo's #107 upgrade path (legacy theme-state.json checkout →
new code) turned up a footgun in state-read.nix's own help text. It said:
• Still on theme-state.json? Rename it to state.json (…)
On a git flake only tracked files exist, so a hand-rename leaves state.json
untracked and therefore invisible — producing exactly the "state file is
missing" error the tip hangs off. Point at the menu write (which stages the
rename via git add --intent-to-add + git rm), name `git add state.json` for
the manual path, and add a bullet for "file is on disk but eval says
missing", which is otherwise a genuinely confusing dead end.
Message verified by rendering the real failure, not by reading the source.
The rename itself is safe and needs no code change: with identical content,
theme-state.json and state.json produce byte-identical derivations (HM
2yqqy8h…, system 9qp9l3g…), so it cannot change what boots. Legacy eval,
the migration write, and re-eval on a dirty/intent-to-add tree all pass.
#133 filed: no check pins the compat shim, so it can rot before we drop it.
856a9d1 restructured the picker and shipped at "V0 menu-back; HM gen builds",
which §2.1 does not allow for a user-visible change. Rendered all eight
screens under the real HM session and viewed them.
IA verified: six-entry root; System → Connectivity/Devices/Recovery/
Preferences; Recovery lists each scope once; Preferences carries the migrated
update-checks/Bluetooth/Printing toggles; Control Center gone from the
dispatch (asserted, not eyeballed); no stderr on any screen. No code change.
Two findings, both filed rather than fixed:
#131 — the VM showed the long Recovery labels ellipsized. That specific
symptom is a guest artifact (they measure 462px/509px in Inter 11 against
~668px of room at 1920), but the same measurement says they DO truncate at
1366, where the picker is only 546px wide — cutting exactly the cost hint
#111 added them to carry. The Acer Aspire M5-481T is 1366x768 and in the QA
fleet. Shortening vs -mesg vs widening is a design call.
#132 — the guest draws icons at ~1/4 of ui.iconSize and clips a 6-row root
behind a scrollbar despite lines=8. Root predates #105, so this is the test
environment, not a regression — but it means menu screenshots are evidence
for content and never for geometry. Documented in docs/TESTING.md §5 so the
next agent doesn't chase it.
e9dd3d1 pinned dialog button labels to base00 for dark palettes. That is
correct for suggested-action, whose background adw-gtk3 builds opaquely as
mix(white,@accent_bg_color,0.9) — but destructive-action is the one class
whose background is derived from currentColor:
background-color: mix(@destructive_color, alpha(currentColor, 0.15), 0.9)
so pinning the label dragged the background down with it. On Boreal the
"Delete" button of a GtkMessageDialog rendered #21272F on #252931 —
1.03:1, invisible — and light palettes hit the same 1.03:1. The most
safety-critical button in a confirmation dialog was the one that vanished.
Pin the destructive background to the same opaque pastel ladder
suggested-action uses. Enabled-only (:not(.flat):not(:disabled)): the
sheet's disabled chrome already renders legibly and must keep it.
Boreal 1.03 -> 6.19:1, summer-day 1.03 -> 2.74:1 (= upstream's
white-on-accent convention; the residual light-mode weakness is #130).
V2: GtkMessageDialog rendered under the real HM session and viewed —
boreal before/after, summer-night (whole-swap), summer-day (light).
Scripted checks stayed green throughout and never saw this; only the
render did. Follow-ups #129 (permanent guard) and #130 filed.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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*).
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Bernardo's call — the rename target is decided, so drop the alternative
rather than leave whoever picks the item re-opening it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four failures from Bernardo's round-4 dock pass on the AMD dev box, plus a
headphone regression, tracked to three causes.
The display watcher ran socat with `-T 1`, which closes Hyprland's IPC
socket after one second of *inactivity*. A dock is normally plugged into an
idle desktop, so the socket was almost always mid-reconnect at that moment
and `monitoradded` was lost outright — and the wallpaper repaint, the
docking profile, and the dock-audio reprobe all hung off that single event.
The reconnect path recovered only the lid inhibitor, so the dock stayed
half-configured until something else poked the watcher.
Make the connection long-lived and take the periodic tick from a 1s `read`
timeout instead, then let the *output set* — not the event — decide: a new
`reconcile` diffs it and is called on both ticks and monitor events, so an
event now only makes the reaction instant rather than necessary. A newly
present external output is as unambiguous a physical plug as the event was,
so it may still override a manual sink choice. Wallpaper paints are retried
over the first few seconds because awww registers an output on its own
schedule, not the compositor's.
Hyprland calls every key-capable evdev node a "keyboard": the lid and power
buttons, a monitor's control channel, and each extra HID collection of a
real keyboard. One dock with one keyboard asked for a layout four times.
udev already draws the distinction properly, so take ID_INPUT_KEYBOARD from
it, map the names into Hyprland's spelling, and ask once per physical
keyboard — applying the answer to every node it brought with it.
Finally, a sink WirePlumber has been told to prefer is stored as the
configured default and outranks every priority rule. On UCM cards the
headphones are a sink of their own rather than a route, so once anything
pinned a sink (this feature on a dock plug, or the user in the Audio menu),
plugging headphones in could no longer take the audio back. The `watch`
loop already subscribed to exactly the right events with an empty handler;
finish it. Unplug needs no undo — the pin goes stale and WirePlumber falls
back by priority on its own, to the dock when docked and the speakers
otherwise.
Verified V2: checks.docking-ux and checks.dock-audio pass with new guards
against each regression; the keyboard filter and both sink selectors run
against the dev box's real devices and sink names. The hotplug paths need
hardware — queued as docking round 5.
Also queues #107, renaming theme.json now that it holds the system state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BACKLOG #99: the live-session seed (nomarchy-seed-flake) did a bare
`cp -r` with no `git init`, so `~/.nomarchy` was not a repo in the live
session and nomarchy-doctor's `git ls-files --error-unmatch
theme-state.json` false-alarmed "theme-state.json is NOT git-tracked"
(installed systems commit at nomarchy-install.sh:654 and were always
fine). The seed now init+add+commits, exactly like the installer.
Verified: V2 — focused serial-log VM run
(/tmp/nomarchy-v2-theme-99/): doctor prints "theme-state.json is
git-tracked" and "flake checkout is clean"; GIT_REPO_OK / TRACKED_OK
markers. A drv-equivalence test proves the seed change is
build-neutral: homeConfigurations.<user>.activationPackage is
byte-identical path-seeded vs git-seeded
(bvl30ggn...-home-manager-generation.drv). nix flake check green.
The same run exposed a separate, pre-existing failure — offline theme
switching in the live session rebuilds from source and dies offline —
filed as BACKLOG #113 (NOT caused by this change: same drv either way).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Session wrap-up: progress notes so the next iteration resumes without
re-deriving — #94 installed-chromium path proven at V1 (live posture is
the gap, #103 covers it), #98 ruled out palette-level causes, #99
root-caused (live seed lacks git init) with the fix drafted but held on
the open live offline theme-switch failure; a serial-log VM run is in
flight. V0 (notes only); two deliberate working-tree drafts documented
in the entries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>