Filed as bemagri against davatorium/rofi with the 2.0.0 line numbers
(view.c:861 → listview.c:622-631 → :646-651) read from the exact source
nixpkgs builds, not the next branch. No duplicate: the nearest, #1064,
is the opposite request (keep the selection after EXECUTING an entry —
closed, milestone 1.7.4), which is also why the report scopes itself to
the filter-changed path and sends no patch: that behaviour is deliberate
for reloads and the scoping call is the maintainer's.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#143 — Bernardo: type `ca`, move onto ChatGPT, backspace once, and the
highlight sits on GHex while Slack is row 1. Upstream rofi 2.0.0, not our
config: listview_set_num_elements() re-asserts the OLD index —
`listview_set_selected(lv, lv->selected)` — and set_selected only clamps
(`MIN(selected, req_elements - 1)`). So the ROW NUMBER survives a
refilter, never the entry, and nothing checks it still matches the query.
Typing more hides it; backspacing exposes it. No option controls it
(auto-select is a different feature). Correction to the report: it is the
listview widget, so every rofi surface does it — drun is just where the
list is long and (sort = true) reorders each keystroke.
#144 — "I lost all my thunderbird accounts and emails" was a missing
profiles.ini. The profile was untouched: 30 GB, 64 account prefs, 19 GB
ImapMail, last used 2026-06-26; TB found no profile list and made an
empty one. Fixed live in ~8 lines (backup kept). MIGRATION.md already
warns about this exact class for Chromium extensions but never names
Mozilla's profiles.ini — which deserves its own bullet because it is the
inverse: no data lost, looks catastrophic, and the tempting fix (restore
a snapshot over a good 30 GB profile) is the actual risk. Firefox here
has the same fingerprint (hm-bak from Jun 2025 names a different profile
than today's ini) — left alone; repointing it is Bernardo's call.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo saw Recovery ellipsize on a 2560x1440 external, which falsified
the item's model (40% + Inter 11 → "only below 1920"). Real cause: text
menus render through themes/<slug>/rofi.rasi, where boreal + neon-glass
pinned width: 620px (fixed — a wide panel buys nothing) while seven used
40%, and the font is whatever that file says — mostly monospace, far
wider than the Inter that was measured. The modelled combination ships in
no theme.
The insight: a menu must fit its longest label, which is a count of
characters in the THEME's font — so the window must scale with the font,
not the screen. 40% gives a 1366 panel 546px and a 2560 one 1024px for
the same row, and a 14pt mono theme needs ~25% more room than an 11pt one
on both; a percentage cannot see either fact.
Rofi has the right unit (`ch` = width of one digit in the current font)
and `calc( a min b )` to cap it, so every text menu — generated and all
nine whole-swaps — is now `width: calc( 84ch min 65% )`. 84ch fits the
longest row we ship (69 chars) plus icon and padding; the cap is
Bernardo's condition (never sprawl on a low-res panel) and it is
measured, not assumed.
Verified on hardware, not by arithmetic: 84ch = 756px in GeistMono 11 and
924px in JetBrainsMono 14; `calc( 84ch min 300px )` → 300px, so the clamp
really clamps. Screenshots at both ends — the real Recovery menu at 756px
(29% of 2560) with every label complete, and the Acer worst case
reproduced pixel-exactly (888px = 65% of 1366, JetBrainsMono 14) also
complete. Grid views override width per-invocation and are untouched.
checks.rofi-text-width guards the class, proven by pinning boreal back to
620px and watching it fail by name. Acer V3 queued for its own fontconfig.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo saw the Recovery labels ellipsize on a 2560x1440 external, which
falsifies both halves of this item's model (width: 40% + Inter 11 →
"only below ~1920, confirm on the Acer"):
- text menus render through themes/<slug>/rofi.rasi, and boreal (his) +
neon-glass pin `width: 620px` — fixed, so a wide panel buys nothing;
the other seven use 40%. launcher.rasi's 780px is a red herring (it
themes -show drun only).
- the font is whatever that file sets — boreal GeistMono 11, summer/
kanagawa JetBrainsMono 14, nord/lumon/retro-82 13 — mostly MONOSPACE,
much wider than the Inter the item measured. Only the generated theme
follows fonts.ui.
- so the modelled 40% + Inter 11 is shipped by no theme, and truncation
is most theme x monitor pairs, not an edge case.
Measured for boreal (9.33px/char off kitty at the same font/size, 518px
room): 3 of 4 rows cut, "What changed" fits — "some of the submenus",
exactly as reported.
Names the decision instead of taking it: fitting the narrowest combo we
ship (40% @ 1366 + JBMono 14) is ~39 chars, which cannot hold scope AND
cost — so "just shorten the labels" would gut #111 for everyone on a big
screen. Options: (a) width in `ch` so the window scales with the font
(fixes the class, 9 theme files), (b) cost to a rofi -mesg line, (c)
shorten and lose the hint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#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.
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.
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.
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.
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>
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>
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>