Commit Graph
100 Commits
Author SHA1 Message Date
David Heinemeier Hansson 9ea9f804cd Missed the glyph 2026-08-11 12:16:25 +02:00
David Heinemeier HanssonandClaude Opus 5 f97ba7375e Wait for a connection before prompting to update
A ping at hyprland.start answers for a machine that has not finished coming up.
Ethernet is still negotiating DHCP, so a working desktop was told to set up
Wi-Fi and offered an update it could already have run. Ask NetworkManager
instead: -s returns once it has tried every connection it could auto-activate,
which is the first moment the answer means anything, and -x then takes that
answer as it stands rather than waiting out a timeout that a laptop with
nothing to connect to would spend in silence.

The update prompt now waits for a connection rather than being phrased around
not having one. There is nothing to update against until a link lands, and one
usually does land later on the machines that started without it, so the prompt
follows the connection whenever it arrives.

That wait runs detached. It outlasts first run by design, and the keybindings
menu is on screen behind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:56:30 +02:00
David Heinemeier HanssonandClaude Opus 5 23d85a992b Open the keybindings menu on first login
The welcome toast spent three lines telling you about a cheatsheet that takes
one keystroke to read, and the only way to act on it was to click the toast,
which opened that cheatsheet. Open it directly instead.

Dismissing the menu exits non-zero, since no selection was made, so the step
tolerates that rather than failing first run and retrying the whole sequence
next login.

It also goes last now. The menu blocks until it is answered, and the Wi-Fi and
update toasts are the only other things left to show, so sending those first
leaves them waiting underneath rather than behind an open menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:56:03 +02:00
David Heinemeier Hansson 383addc8c4 Rearrange keybindings presentation 2026-08-11 11:35:41 +02:00
1e7bb66556 Recover a session lock stranded by a dead shell (#6692)
* Detect a compositor session lock through one helper

omarchy-restart-shell decided whether the session was locked by looking for
"LOCK" anywhere in the hyprctl monitors payload. That works, but not for the
reason the code reads like: Hyprland reports no lock state of its own, and the
string comes from solitaryBlockedBy, the list of reasons a monitor cannot hand
a client the whole screen. An active ext-session-lock is one of those reasons.

A substring match over the whole payload also answers yes to a workspace or a
monitor description that merely spells LOCK, and locking a desktop nobody asked
to lock is the worst way to be wrong. Match the reason list itself, and put it
behind a helper now that a second caller needs the same answer.

That second caller needs a third answer too, because the reason list is not
always readable. Hyprland stops at the first reason on a monitor with no
workspace yet — one just coming back — and returns before it ever looks at the
lock, so a missing LOCK there means nothing was asked rather than nothing was
found. Neither that nor an unreachable compositor is an unlocked session, and
locks strand precisely while outputs are coming and going, so both exit 2.
Callers that only branch on success are unaffected.

The test fixture claimed the string came from a workspace name, so it was
encoding the wrong model of the compositor. It now returns what Hyprland
actually returns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Retake a session lock stranded by a dead shell

ext-session-lock keeps the session locked when its client goes away — that is
the point of the protocol, so a crashing lock screen cannot expose the desktop.
The cost is that a shell which dies while locked leaves the compositor locked
with nothing left to authenticate against: Hyprland's failsafe, which takes a
TTY or another machine to clear.

Nothing carried the lock across a restart. Quickshell relaunches itself after a
crash and omarchy-restart-shell can be run by hand, but both bring back a shell
holding no lock, so the failsafe stayed up. A fresh shell never holds a lock, so
a session already locked as the lock service starts can only be that orphan:
take it back and let the user type their way out.

Asking once is not enough. These deaths happen while outputs are going away,
and the replacement shell comes up inside that same window, where there is
nothing to read a lock off. So the question is asked until the answer means
something: on a short timer while the session settles, and again when a screen
comes back, since a display asleep for hours outlasts any timer worth running
and returns through a state the compositor cannot answer for either. Once an
answer does arrive the search ends, so the timer stops and later screen changes
cost nothing.

Three ways this could lock a desktop nobody asked to lock, all closed. A lock
this shell took itself is not an orphan, including one taken while the question
was in flight — omarchy-restart-shell re-locks a fresh shell, and the answer
cannot tell whose lock it found. Recovery runs once and clears the flag, so
nothing lingers to fire after an unlock. And PAM landing late reopens the
question rather than answering it: clearing the failsafe from a TTY is the
documented way out, so a yes from before there was anything to do about it may
be stale by the time it can be acted on.

The check has to live here rather than in the launcher. Quickshell's crash
handler re-execs in place, keeping the same pid, so a supervising process never
sees the restarts that recovery matters most for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Relaunch the shell when it dies without a signal

Quickshell restarts itself after a crash, but only from its signal handlers:
SIGSEGV, SIGABRT, SIGFPE, SIGILL, SIGBUS, SIGTRAP. Qt does not always leave
that way. When the Wayland connection fails, QWaylandDisplay::checkWaylandError
calls _exit() directly, which raises no signal at all — so the crash handler
never runs, no report lands in ~/.cache/quickshell/crashes, and the desktop is
left with no bar and no explanation.

That is how #6684 ends: the lock path meets a screen with no valid Wayland
output, declines to create a lock surface for it, and the connection dies with
EINVAL. Supervise the launcher so those deaths come back.

A clean exit is deliberate — omarchy-restart-shell stops the shell over IPC and
starts its own replacement — and a signal to the supervisor means the session is
going away, so neither relaunches. Neither does a shell that outlived its
compositor, though that takes more than one unanswered query to conclude: the
shell dies while outputs are being reconfigured, which is also when a busy
compositor can miss one without being gone. A shell that cannot stay up gives
up after five tries in a minute rather than spinning.

Signals need care now that a launcher stands between the session and the shell.
Bash defers a trap until a foreground command returns, so the shell runs as a
job and the supervisor waits on it. Stopping the launcher used to stop the shell
with it, back when this script exec'd Quickshell, so the signal is passed on
rather than leaving a desktop nobody is watching. One arriving during the
backoff sleep only reaches the trap afterwards, so the flag is read again at the
top of the loop: a shutdown racing a crash would otherwise get one more
Quickshell on its way out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:29:35 +02:00
3d1914a8cd Let bar put place a widget on a bar it does not recognize (#6687)
* Place a bar widget on a bar without the widget it names

'omarchy bar put X --after Y' refused outright when Y was not on the bar, so
migration 1786279107 failed for every user whose clock is their own clone of
omarchy.clock rather than the built-in, and took the rest of the migration
chain down with it. put is the verb a migration or an install reaches for
precisely because it cannot know what the bar it places into looks like, so it
now falls back to the widget's usual spot instead of failing. 'plugin enable',
which someone types, still says when it cannot find the target.

A clone also answers as a placement target now, whether it is the widget the
placement named or the anchor the fallback lands against: cloning the clock
leaves a bar carrying your id where omarchy.clock used to be, and a caller
naming the source means the clone that took its place, the way resolveEnabledId
already routes calls to it. So the widget sits next to that clock rather than
at the end of the section.

Fixes #6678

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Keep asking a shell that is still starting

An 'omarchy update' landing while the shell restarts failed migration
1786279107 twice over. Quickshell answers a call made before it finishes
loading with "Not ready to accept queries yet." on stdout and exits 0, so a
caller polling with a ping read a starting shell as up and then took that
sentence for the answer to its real call; report it as unreachable, which every
caller already knows how to handle, and omarchy-restart-shell stops cutting its
readiness loop short on it too.

Reading the plugin manifests is a subprocess behind that, so IPC starts
answering before the registry knows the widget it is being asked to place, and
put refused it as unknown. Say which of the two it is and let put keep asking.

Only a shell that was never there is nothing to fail over. One that never
finishes starting, one that stops responding, one too old to know the call at
all: each has to fail, since omarchy-migrate records a migration that returns 0
as done, and the widget is then never placed and never asked for again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fall back for the shell an update has not restarted yet

omarchy-update runs its migrations before omarchy-update-restart, so the shell
answering migration 1786279107 on the update that carries this fix is still the
one that shipped without it, and it refuses the placement exactly as before.
The users this is for would have watched one more update go wrong. put owns the
fallback it documents, so let the command carry it: asked again without the
neighbour the shell says it cannot find, that shell places the widget.

A restarted shell never answers this way — it falls back itself, and knows to
look for a clone of the widget the placement named, which the command cannot.

Having answered once is now remembered across both asks. A shell that speaks
and is then gone has stopped mid-request, and reading that as a machine that
never had one would leave the migration recorded as done.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Wait for a shell that has not appeared yet

A shell being spawned has no socket to answer on, and nothing tells the command
a launch is under way, so a put landing in that window read the silence as a
machine without a shell and carried on — leaving the migration recorded as done
with nothing placed. Give one three seconds to turn up first. A machine that
genuinely has no shell still carries on, three seconds later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Leave a clone of the widget being put where it is

A clone is the widget it was cloned from wearing its owner's name, so a bar
carrying one already has what put is being asked to place. put only saw the
literal id, and enabling a first-party source whose clone is active is how you
switch back to the built-in — so a migration placing omarchy.keyboard-layout
would have handed a user's own copy back for the shipped one, and called it
done. Targeting learned to read a clone as its source; presence had not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Trim the comments on the bar put path

Roughly a line of comment per line of code, most of it restating what the code
and the assertion messages already say.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 11:07:14 +02:00
David Heinemeier HanssonandClaude Opus 5 0d45f0979b Note that fully hidden groups stay out of GROUP_DESCRIPTIONS
Following the rule as written is what put apply back in the top-level group
listing right after every command in it was hidden: the table drives that
listing on its own, with no regard for whether anything in the group is
visible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 14:17:25 -07:00
David Heinemeier HanssonandClaude Opus 5 536fcd5c6c Move install-time plumbing out of the setup namespace
setup is where a user goes to configure something: direct boot, security
keys, hibernation. These three are not that. omarchy-apply-system is the
ISO's entry point in the target chroot, omarchy-apply-hardware is what it
calls for device quirks, and omarchy-apply-lock is called by
install/config/lockscreen-pam.sh.

apply is the verb they already used to describe themselves, and it carries
the contract: declared state under install/ converged onto the machine,
idempotent, safe to repeat.

The group gets no GROUP_DESCRIPTIONS entry on purpose. That table drives the
top-level group list on its own, so an entry would put apply back in front of
users even with every command in it hidden, the way provision already stays
out. A test covers it.

The ISO installs the runtime from the mirror it ships with, so it moves to
the new names in lockstep and no compatibility route is needed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 14:14:57 -07:00
David Heinemeier HanssonandClaude Opus 5 477284f002 Hide install-time setup plumbing from the command listing
The setup group is described as interactive setup wizards, but these three
are not that. omarchy-setup-system is the ISO's entry point in the target
chroot, omarchy-setup-hardware is what it calls for device quirks, and
omarchy-setup-lock is called by install/config/lockscreen-pam.sh. None of
them is something to browse to and run.

Hiding only affects listings, so the ISO and the install leaves keep routing
through the CLI exactly as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:58:16 -07:00
David Heinemeier HanssonandClaude Opus 5 8bc854069b List Herdr right below Tmux in the keybindings menu
The menu ranks the launcher bindings by hand, and Herdr had no rule, so it
fell into the unranked middle far from the terminal it sits beside in the
bindings. Rank it right after Tmux and shift the rest down, and give
"Show Herdr key bindings" the same treatment next to its Tmux counterpart
at the bottom.

The cache key is bumped so machines holding records from the old order
rebuild them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 22:46:00 +02:00
David Heinemeier HanssonandClaude Opus 5 5649740f51 Build the menu IPC payload with jq instead of perl
Forking perl to produce {"menu":"<route>"} cost more than the IPC call it
fed. jq is already a runtime dependency and emits identical JSON.

Opening the menu drops from ~42ms to ~28ms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 13:33:07 -07:00
e4604fbcfb Generate image picker thumbnails with libvips in parallel (#6686)
Thumbnails were generated one ImageMagick process at a time. Queue the
missing ones and drain them across every core with vipsthumbnail, which
decodes and encodes faster and lets the per-process startup overlap.

Cold cache for a 92 wallpaper directory drops from 14.2s to 1.3s, and
the bundled theme previews from 2.0s to 0.26s.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 22:20:00 +02:00
c53190be07 Remember Bluetooth on/off through the rfkill soft block (#6682)
* Turn Bluetooth off with an rfkill soft block

BlueZ never persists an adapter's Powered property, so turning Bluetooth off in
the panel lasted only until the next boot. Omarchy's answer was AutoEnable=false,
which persists nothing either — it just means "never power the adapter on", so
Bluetooth came up off every boot whatever the user had chosen.

The soft block already does the job. systemd-rfkill saves every switch under
/var/lib/systemd/rfkill and restores it early on the next boot; that is the
entire purpose of the unit. Blocking also covers every controller at once, where
bluetoothctl only ever addresses the default one.

So the block becomes the state and BlueZ follows it: with AutoEnable back at its
stock default, lifting the block is enough for bluetoothd to power the adapter up
on its own. Powered still tracks the block, so the panel switch and icon read it
exactly as before. Everything that turns Bluetooth on or off goes through
omarchy-bluetooth-power, because bluetoothctl power on fails while a block is set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Carry installed machines over to the rfkill block

Existing installs have AutoEnable=false, so their adapter is down at every boot
and Powered is the only record of what the user actually wants. Read it before
anything changes, hand it to the block, then put AutoEnable back to its default
so bluetoothd can act on that block. Only the exact line Omarchy wrote is
reverted, so a hand-edited opt-out survives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Ask the power helper for a direction, not a toggle

The helper runs detached and the switch only moves once BlueZ catches up, so a
second click inside that window re-read the pre-click state and undid the first.
The panel already knows which way it wants to go, so let it say.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Read every controller and bound the power-up wait

The block hits every Bluetooth radio at once, but the state was read from a bare
bluetoothctl show, which reports the default controller only. A powered dongle
sitting behind a powered-down internal controller read as off and got blocked
along with it. Enumerate the controllers and take any powered one as on, exposed
as is-on so callers do not each reinvent the read.

The wait counted probes rather than time, so a wedged D-Bus turned a two-second
bound into roughly fifty across a full power-up. One deadline around the whole
wait holds it near nine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Change the radio through sudo in the migration

/dev/rfkill is only writable unelevated from an active graphical seat, so an
update run over SSH failed here with EACCES. Migrations run under bash -e, so
that aborted before the config revert and the marker, and aborted again on every
retry. The privilege guidance already calls for sudo on machine-wide work run
from a visible terminal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 20:27:40 +02:00
David Heinemeier HanssonandClaude Opus 5 199bd01f94 Reach every window the region picker can highlight
Warping to a target window's center selects the wrong window when a smaller
one covers that center: slurp keeps highlighting the coverer, so Tab could
never leave it. Navigation now warps to the most central point that resolves
back to the target, and skips windows that hovering could not reach either.

Unbinding the picker's transient keys by name also took a same-key binding
out of the user's own config with it; the bind handles are now kept and
removed individually.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 10:17:08 -07:00
David Heinemeier Hansson 03b825f59a Show Herdr keybindings with SUPER + CTRL + K
Mirrors the Tmux keybindings menu on SUPER + ALT + K. Herdr has no CLI
that dumps resolved bindings, so the action list and its defaults come
from `herdr --default-config`, where every action appears as a commented
assignment, and the user config overrides what it sets.

Prose in that default config can read like an assignment, as in
`# type = "popup" opens a session-modal terminal`, so a line counts only
when its value is a bare string or an array of them. Both TOML quote
characters open a string, and with no config file at all the listing
shows Herdr's own defaults rather than the Omarchy seed config Herdr
never loaded.
2026-08-10 18:44:19 +02:00
David Heinemeier Hansson 507059e548 Open Herdr with SUPER + CTRL + RETURN
Mirrors the Tmux binding on SUPER + ALT + RETURN. Herdr reattaches to the
persistent session on its own, so the launcher needs no attach-or-create
dance.
2026-08-10 18:44:19 +02:00
David Heinemeier HanssonandClaude Opus 5 932efbd58b Center the first-boot setup screen and fix its greeter animation
The setup progress screen stacked from row one while the greeter that precedes
it was already centered, so first boot changed shape between its two frames.
Center it on the same block height the ISO install dashboard uses — logo, blank,
title, blank, bar, blank, tip — and repaint it if the VT resizes, which is the
same virtio-gpu KMS handoff the greeter already watches for.

The greeter's ttfx call never passed --xterm-colors, so it hit exactly the
failure the comment above it describes: ttfx resolves even indexed stops to
truecolor, and the console reduces 256-colour codes well but 24-bit ones badly.
The gradient was being crushed rather than rendering as the indexed palette it
was written for, and the settle colour drifted off the green logo drawn beneath
it.

--canvas-width was cols-1 to stay off the autowrap column, which held for tte.
ttfx centres its text two columns right of plain centering, so cols-1 puts the
animated logo a column off the static one and it jumps when the effect starts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 09:31:44 -07:00
4ab51df2b0 Require a factory snapshot to reset a computer (#6680)
Machines without @factory fell back to a degraded reset that kept the
current system and only wiped user state. Turn them away with an
explanation instead, and drop the degraded staging path.

The first-boot worker still honors a wipe-degraded marker so a reset
staged by an older version finishes its scrub rather than handing the
machine over with the seller's accounts intact.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 17:41:05 +02:00
David Heinemeier Hansson 0b670cd13e Title herdr's terminal window like tmux did
tmux set the outer terminal title from `set-titles-string '#h:#W'`, which
is what Hyprland reads for the group bar label. herdr had no equivalent,
so grouped terminals kept whatever the shell or ssh last set - most
visibly wrong when connected to a remote machine.

herdr now renders `ui.window_title` on the server, so mirror the tmux
line. herdr's own default matches, but setting it here keeps the group
bar correct regardless of what upstream picks as its default.
2026-08-10 17:19:11 +02:00
David Heinemeier HanssonandClaude Opus 5 9f13bfc157 Center the timezone picker in first-boot setup
The timezone prompt falls back to `gum filter` when tzupdate can't guess a
zone, which is the common case on a first boot with no network yet. That was
the one widget in the form without a GUM_*_PADDING export, so it drew flush
left while every other step sat centered under the logo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 07:46:06 -07:00
David Heinemeier HanssonandClaude Opus 5 2d44ad59be End deferred first boot on the finished progress bar
Clearing it for a "Starting Omarchy..." card only flashed another screen
before SDDM took over. The cursor now stays hidden through the handoff and
comes back only on the failure path, where the retry prompt needs it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 07:38:47 -07:00
David Heinemeier HanssonandClaude Opus 5 6d7826d635 Give non-login shells the system locale
/etc/profile.d/locale.sh only runs for login shells, so bash started by
SSH or herdr's remote bridge ran in the C locale, where printf emits
\u/\U escapes literally instead of the character.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 05:58:56 -07:00
David Heinemeier HanssonandClaude Opus 5 354c2f0060 Wait for the terminal resize before the first screensaver effect
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 13:59:20 +02:00
David Heinemeier Hansson 4cc14933a4 Use sudo for terminal update inhibition 2026-08-10 03:45:49 -07:00
David Heinemeier HanssonandClaude Opus 5 dc1224c03a Stop the sleep lock budget assertion from flaking
A 1500ms budget plus the one 100ms poll interval the trailing sleep can
overshoot is exactly 1600ms, which was the bound — but the test measures a
whole process around that, so startup pushed real runs to 1602ms. Carry
another interval. Two intervals of overshoot, the regression this guards,
still trips it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 03:31:17 -07:00
David Heinemeier HanssonandClaude Opus 5 f228c4d390 Stop checking for commands the package set guarantees
gum and ttfx ship in omarchy-base.packages and setfont comes with kbd, so
none of them needs a presence check — and every setfont call already
tolerates a failure anyway. btrfs-progs is in omarchy-other.packages and a
reset genuinely cannot proceed without it, so that one keeps its guard and
moves to the helper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 03:31:17 -07:00
David Heinemeier HanssonandClaude Opus 5 4564c24a0e Cover the shared setup form's cancel contract
The form's 0/1/130 statuses gate both the ISO configurator and first-boot
setup, and nothing tested them. Stubs gum with scripted per-screen answers
and drives each prompt bare under `set -euo pipefail` — the shape that makes
the status capture load-bearing, since a cancelled prompt is a failing
assignment. A RETURN trap marks that the prompt returned its status rather
than the shell dying inside it; both exit identically otherwise, so that
marker is what catches a regression to a plain `status=$?`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 03:21:08 -07:00
4e31b61af0 Swap terminaltexteffects for ttfx (#6670)
* Swap terminaltexteffects for ttfx

ttfx is a Rust port of terminaltexteffects that renders byte-identical
frames as a single dependency-free binary. Same option names, defaults,
and exit codes, so every invocation here is unchanged apart from the
command name.

The screensaver runs at --frame-rate 120 with --random-effect. On a
fullscreen canvas Python cannot hold that for the heavier effects
(beams: 14.1 ms/frame against an 8.3 ms budget, so ~71fps); ttfx renders
the same effect at 564fps. Startup drops from ~107 ms to ~1 ms, and the
base image no longer needs Python for the screensaver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Need a migration

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 11:59:39 +02:00
7644b56d4e Extract the setup form shared with the ISO installer (#6669)
The keyboard layout list, the account and hostname validation rules, and the
gum prompts that ask for them all existed twice: once in the ISO
configurator's user step, once in first-boot owner setup. Nothing kept the
copies honest, and they had already drifted — a layout removed on one side
moved English (US) onto a page boundary on the other, burying the default at
the bottom of a screen of layouts.

install/provisioning/setup-form.sh is now the only copy. The PKGBUILD's
existing `cp -a install` ships it to /usr/share/omarchy/install/provisioning/,
and the ISO build vendors that very file out of the runtime package it
bundles, so an install and the first boot that finishes it cannot offer
different layouts or accept different usernames.

Cancel handling is unified along the way, which is what made the prompts
shareable at all. Every prompt reports 0 (answered), 1 (Esc — unwind to the
start of the form), or 130 (Ctrl+C — a side channel each caller defines).
Previously Esc and Ctrl+C were indistinguishable here: both re-asked the same
field, so there was no way back to an earlier answer. Ctrl+C now offers a
confirmed reboot instead. It cannot be a SIGINT trap — gum reads Ctrl+C as a
byte in raw mode, so the shell never receives the signal — so it hangs off the
exit status.

Status capture is written as `x=$(gum ...) && status=0 || status=$?` because
this script runs under `set -e`, where a cancelled prompt is a failing
assignment that would kill setup before the status could be read.

English (US) also leads the layout list now, ahead of the other English
variants. gum choose paginates in --height-sized pages and jumps to the page
holding --selected, so an alphabetical default landed wherever the list length
happened to put it.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 11:19:23 +02:00
David Heinemeier HanssonandGitHub c7c897cb4f Follow herdr's new tab bar status config (#6674) 2026-08-10 10:28:39 +02:00
0a8359072c Show install-style progress through first-boot account setup (#6631)
* Add OEM first-boot setup and factory reset

An OEM-mode ISO install (or omarchy-reset-computer) leaves the machine in OEM
state: fully installed, no user, /var/lib/omarchy/oem/pending armed. On the
next boot omarchy-oem-setup.service runs the configurator's user form on tty1,
creates the user with the groups system setup recorded, finalizes it offline
from the stashed Node tarball, re-keys LUKS from the throwaway install
passphrase to the user's password, and hands off to SDDM.

omarchy-reset-computer returns a machine to that state: it swaps the running
root for a fresh clone of the @factory snapshot the ISO takes at install time,
scrubs machine identity and prior users, and stages omarchy-factory-wipe to
drop the old root and recreate @home/@log on the next boot. Machines installed
before @factory existed get a degraded reset (current system kept, users and
state wiped) with that caveat surfaced in the confirmation.

omarchy-setup-system/-hardware gain --oem to run without an install user; the
group-granting install scripts now record their groups in
/var/lib/omarchy/oem/groups and only call usermod when the user exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Harden OEM setup: correct cryptsetup key-file usage, retry on failure

cryptsetup reads --test-passphrase/--key-file inputs byte-for-byte, so feed
passphrases through process substitution consistently instead of positional
args or stdin (which has different newline semantics). Run each first-boot
setup attempt as its own process so a failure offers a retry instead of
stranding the machine at a user-less login screen — bash ignores errexit
inside `while !` conditions, a child process does not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Always grant wheel sudo in OEM first-boot setup

Detecting an existing %wheel grant by grepping sudoers is error-prone:
omarchy ships narrow '%wheel ALL=(ALL) NOPASSWD: <command>' rules (e.g.
asdcontrol) that match the naive pattern, which left the OEM-created user
matching sudoers entries but unable to run anything. Write the drop-in
unconditionally — a duplicate of an existing full grant is harmless.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix LUKS re-key device resolution and OEM state readability

archinstall's encrypted installs put cryptdevice=PARTUUID=... on the kernel
cmdline, not UUID=, so the first-boot re-key never found its device and
silently skipped — leaving the throwaway auto-unlock keyfile in place, i.e.
the disk effectively unencrypted. Parse every cryptdevice= source spec form
and make any re-key failure abort the attempt loudly: a retry prompt beats a
machine that quietly boots without a passphrase forever.

The OEM state directory also has to be world-readable (its one secret,
luks-key, stays 0600): user finalization reads the stashed Node tarball as
the new user, and the 0700 directory forced it onto the network fallback.

Step markers now land in /var/log/omarchy-oem-setup.log for debuggability.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Purge stale machine-id boot entries when resetting or re-keying

limine-entry-tool keys its limine.conf OS entries by machine-id. A factory
reset gives the machine a fresh identity, so the previous system's entry
survived every rebuild, sorted first, and made Limine stop at a Blake2b
hash-mismatch warning once the UKI was rebuilt. Start limine.conf over from
the shipped template (and drop foreign machine-id history directories on the
ESP) before any post-reset rebuild: in the staged chroot rebuild, in the
first-boot LUKS re-key, and — for unencrypted resets, where nothing else
rebuilds — in a dedicated first-boot refresh when foreign entries are found.

The staged rebuild also verifies every UKI hash referenced by limine.conf
against the file on the ESP before the subvolume swap, and the running
system's limine-snapper-sync is runtime-masked during staging so it cannot
rewrite the config behind the rebuild.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Harden reset and first-boot setup failure paths

Review findings from codex and Copilot:

- Generate throwaway passphrases without a trailing head stage: under
  pipefail, SIGPIPE from the infinite tr failed the substitution and errexit
  aborted every encrypted reset before it could stage anything.
- Stage the fallible parts of a degraded reset (LUKS re-key, boot rebuild)
  before arming the wipe, so a staging failure leaves the machine untouched
  instead of scheduling a wipe for a reset that never finished.
- Gate first-boot setup on the factory wipe having succeeded
  (ConditionPathExists=!wipe-pending plus an in-script guard): creating the
  new user on a half-wiped system would hand their data to the wipe retry.
- Abort the wipe (keeping its retry marker) when deleting the old root or
  recreating @home/@log fails, and abort resets that cannot remove a prior
  account — a surviving account keeps its password and wheel membership.
- Resume a partially-created account on setup retry instead of rejecting the
  username the failed attempt just created.
- Only purge machine-id directories the old limine.conf actually referenced;
  a shared ESP may hold other installations' boot artifacts.
- Recreate the hibernation swapfile (nested subvolume, so never captured by
  the factory snapshot) inside the factory root before its UKI rebuild, so a
  reset machine keeps disk-backed swap and a valid resume offset.
- Source base-test.sh in the OEM groups test per test conventions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Recreate the hibernation swapfile even when resume drop-ins survive

omarchy-hibernation-setup short-circuits as 'already set up' when the resume
mkinitcpio drop-in exists — which it always does in a factory root, while the
swapfile itself never survives the snapshot (nested subvolume). Drop the
marker when the swapfile is gone so setup reconfigures from scratch, and
verify the swapfile actually exists before proceeding with the reset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Second review pass: encrypted-config coverage, factory-baseline sanitization, recoverable rekey

Codex xhigh round 2:

- Detect the LUKS backing device by walking the root's device tree, not only
  the cmdline cryptdevice=; reset/first-boot now re-key roots reached via
  rd.luks/crypttab too, instead of silently leaving the seller's slots valid.
- Sanitize the retained @factory baseline (accounts, /etc/shadow, machine
  identity) during a full reset: the new wheel user could otherwise mount it
  to recover the seller's data, and a second reset would restore the account.
- Re-key the disk recoverably: rebuild the no-auto-unlock UKI before killing
  the throwaway slot or destroying the staged key, and restore the keyfile if
  that rebuild fails, so a retry with a different password can never leave the
  disk locked to the first attempt's password.
- Roll back a degraded reset's live-root auto-unlock material if its boot
  rebuild fails, instead of leaving it for a later rebuild to embed.
- Treat a missing current-machine limine entry as stale so a retry after a
  failed rebuild repairs the config instead of clearing OEM state over it.
- Erase fingerprint enrollments (/var/lib/fprint) in degraded wipes.
- Remove the resume-offset drop-in too when recreating the factory swapfile,
  so the rebuilt UKI gets a correct offset.
- Pin first-boot retries to the account the first attempt created.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Expose factory reset in the Setup menu

Add a 'Reset Computer' entry under Setup (Omarchy's Settings menu, where OS
factory resets conventionally live), guarded to btrfs roots and launched in a
floating terminal. omarchy-reset-computer now self-elevates via sudo so the
menu entry needs no sudo prefix, forwarding the caller's gum theme env as
env arguments so styling survives an env_reset sudoers. The typed 'reset'
confirmation and the sudo password prompt remain as the guards against
accidental triggering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Defer keyboard selection to first boot for OEM installs

The OEM first-boot setup now runs a keyboard step before the user form,
mirroring the ISO configurator: it loads the chosen layout on the live VT so
the password (and the LUKS re-key that follows) are typed under it, and
persists it with systemd-firstboot so the installed system gets both the
console KEYMAP and the XKB layout Hyprland reads — exactly what a normal
install writes. Layouts localectl doesn't know keep the default, same as the
installer.

This lets the OEM operator set nothing user-specific: the machine's owner
picks their keyboard alongside their account at first boot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Rename factory-reset commands to omarchy-system-factory-reset[-finish]

omarchy-reset-computer      -> omarchy-system-factory-reset
omarchy-factory-wipe        -> omarchy-system-factory-reset-finish
(and its systemd unit, log path, and temp mount to match)

Pure rename: every reference — the Setup menu action, the first-boot finish
service the reset stages and enables, the oem-setup ordering/gating, comments,
and the menu test — moves together, with no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Rename OEM vocabulary to provisioning (runtime)

Commands unify under the provisioning family:
  omarchy-oem-setup      → omarchy-provision-owner
  omarchy-finalize-user  → omarchy-provision-user
  omarchy-first-run      → omarchy-provision-first-run

And the deferred-provisioning state/vocabulary replaces 'OEM':
  /var/lib/omarchy/oem/          → /var/lib/omarchy/provisioning/
  /etc/omarchy/oem.key           → /etc/omarchy/provisioning.key
  install/oem/                   → install/provisioning/
  OMARCHY_SETUP_CONTEXT=oem-firstboot → provision-owner
  omarchy-setup-system/-hardware --oem → --defer-provisioning

All callers (provision-first-run→provision-user, autostart, factory-reset
staging the provisioning units, the group-recording scripts) and comments
move together.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Drop remaining OEM mentions from the provisioning groups test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Show install-style progress through first-boot account setup

The owner provisioning flow dropped to plain status lines after the
confirm form, went dark for the minutes finalize-user takes, and ended
with a two-second spinner. Give it the same treatment as the ISO
install dashboard: the logo header with a live progress bar and
rotating tips while the account is created, finalized, re-keyed, and
the boot entries refreshed, then the same tte celebration — ending in
a Start Omarchy Now button that hands off to SDDM, since first boot
continues into the session rather than rebooting.

The bar is the dashboard's engine in miniature: monotonic per-mille
position from an asymptotic time floor per phase, with finalize-user's
run_logged scripts in the log as the work signal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Match the first-boot console font to the ISO installer

The ISO installer runs on a low-resolution pre-KMS console, where the
default 8x16 console font looks large. By the time omarchy-provision-owner
runs, the installed system has reached full KMS resolution, where that
same 8x16 font is physically tiny — so first-boot owner provisioning
looked a lot smaller than the installer it continues.

Scale the console font up on high-resolution framebuffers so the two
read at the same size: latarcyrheb-sun32 (16x32) at >=1600px tall,
sun12x22 at >=1150px, and the default left untouched below that (already
installer-sized). Both fonts ship with kbd, so there's no new dependency,
and it's a no-op off a real VT.

Co-Authored-By: Claude <noreply@anthropic.com>

* Make first-boot font resolution-adaptive; widen the encrypting band

Two fixes to the first-boot provisioning screen surfaced on a real
high-resolution display.

Font: the earlier fixed thresholds could pick a 16px-wide font on a
~1280px-wide console — 80 columns, one short of the 81-column logo — so
the logo wrapped and the layout looked misaligned. Replace the thresholds
with a resolution-adaptive choice: pick the kbd font whose row count is
closest to the ISO installer's ~48-row feel, but never one wide enough to
drop below 90 columns, so the logo can never wrap. Low-resolution
consoles land on the default font (a no-op), matching the installer.

Progress: the LUKS re-key (a full UKI rebuild, the slowest step of an
encrypted first boot) had only an 8%-wide band, so the bar looked stuck
near the top while it worked. Make the bands adaptive — when a re-key is
pending, finalize yields most of its room to a wide, steadily-moving
re-key band; unencrypted installs keep finalize as the dominant step.

Co-Authored-By: Claude <noreply@anthropic.com>

* Add a Welcome greeter before first-boot setup

Deferred-provisioning first boot dropped the new owner straight onto the
keyboard picker. Open with a greeter instead — the same frame the setup
ends on: static logo, a centered "Welcome to Omarchy", and a single
"Start setup" button. No logo animation; it's the starting line, not the
celebration. Shown once in main(), before the keyboard step, so retries
don't repeat it.

Co-Authored-By: Claude <noreply@anthropic.com>

* Center the greeter and show the Omarchy tagline

Vertically center the greeter block (logo, tagline, button) on the
console like the boot logo, and replace "Welcome to Omarchy" with the
tagline "Beautiful, Modern & Opinionated Linux by DHH".

Co-Authored-By: Claude <noreply@anthropic.com>

* Animate the greeter logo with a skippable ColorShift

Run a looping tte ColorShift over the centered logo and replace the
button with a "Press Return to Start Setup" hint. The effect reads from
/dev/null so it never swallows the Return a foreground read waits on, and
--reuse-canvas is anchored one row below the logo so it repaints exactly
the rows drawn above. Return skips ahead into setup at any time.

Co-Authored-By: Claude <noreply@anthropic.com>

* Theme the greeter ColorShift to the Omarchy palette

Sweep the logo ColorShift through the Tokyo Night accent colors
(green, cyan, blue, purple, pink, orange) instead of tte's default
rainbow, settling toward the Omarchy green.

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix greeter ColorShift: green base, no flash, clean handoff

- Use indexed ANSI colors (green 2 + cyan 6 accent), since the framebuffer
  console can't render tte's truecolor and crushed it to a muddy lavender.
  The result is a green-based shift with a cyan accent, settling on green.
- Run one long tte invocation (--cycles 1000) instead of restarting every
  couple cycles, removing the flash at each loop.
- Restore the tty with `stty sane` after killing the effect (tte leaves it
  raw/no-echo, which silently killed the following gum prompts) and clear
  the screen so the leftover frame doesn't linger under the keyboard step.

Co-Authored-By: Claude <noreply@anthropic.com>

* Kill the greeter animation cleanly so the form starts fresh

The ColorShift ran inside a `while` subshell that was backgrounded and
killed by its subshell PID — which orphaned the tte child, leaving it
painting the logo over the keyboard step (the screen never cleared and
the form was garbled). Run tte directly so the tracked PID is tte's own,
and killing it actually stops the animation before the screen is cleared.

Co-Authored-By: Claude <noreply@anthropic.com>

* Don't let the killed animation abort provisioning under set -e

wait on the tte PID reports its kill signal (143). Under set -euo
pipefail that nonzero status aborted greeter_screen — and the whole
service — right after Return, dropping first boot straight to the login
screen instead of the keyboard step. Tolerate it with `|| true`.

Co-Authored-By: Claude <noreply@anthropic.com>

* Wait for the terminal size to settle before drawing the greeter

A terminal emulator — or sudo's pty — reports a stale 24x80 for a few
hundred milliseconds after the process starts, before its real winsize is
set. The greeter measured immediately and fell into the 80x24 fallback,
rendering small in the top-left instead of centered (the real first-boot
console, already settled, was unaffected). Wait for stty to report a
stable size at least as wide as the logo before measuring.

Co-Authored-By: Claude <noreply@anthropic.com>

* Fall back to the default font when the greeter console is too narrow

scale_console_font sizes the console font from /sys/class/graphics/fb0,
but virtio-gpu (notably virtio-vga-gl) can report a resolution that the
console then settles below — leaving a font wide enough that an 81-column
logo no longer fits (e.g. sun32 at a settled 1280px is 80 columns). The
logo wrapped and hugged the top-left, with the animation tiling over it.

After the greeter measures the settled size, if it is still narrower than
the logo, drop to the default (narrowest) console font and re-measure so
the logo always fits and centers. Verified by forcing an 80-column
console: the fallback recovers it to 160 columns, centered, and
provisioning completes.

Co-Authored-By: Claude <noreply@anthropic.com>

* Redraw the greeter on console resize instead of measuring once

The first-boot greeter measured the console once, painted the logo and
launched the tte animation, then blocked on Return. On a fresh
deferred-provisioning boot under virtio-gpu the VT comes up in a
transitional ~80x25 mode and only widens to the real resolution a second
or more later, once KMS takes over (or, on virtio-vga-gl, the SDL window's
size lands). The old settle-wait accepted that transient (two matching
100ms samples was enough), so the greeter committed to an 80-column
geometry. When the VT then resized, Linux scrolled the stranded frame into
the top-left and reset the DEC saved cursor tte paints from with
--reuse-canvas — the small, top-left, tiled/garbled logo. The earlier
"fall back to default8x16 when cols<81" fix couldn't help: if the console
genuinely offers 80 columns, re-applying the narrowest font still leaves
80.

Treat a resize as a redraw trigger instead. greeter_screen now:

- waits for a real quiet period (console signature = VT size + framebuffer
  identity/size held steady ~1.5s) before the first paint;
- sizes the font empirically — apply each candidate, read the columns
  fbcon actually returns, keep the one nearest ~48 rows that still clears
  the 81-column logo — rather than trusting fb0/virtual_size, which under
  virgl can report a size the text console never reaches;
- paints from a nested _greeter_draw and, while waiting for Return, watches
  SIGWINCH and the console signature; on any change it kills tte, settles,
  re-fits the font, and repaints — so a resize arriving five seconds in
  looks the same as one that never happened;
- draws each logo row at an explicit column and falls back to a centered
  text-only greeter (no logo, no animation) when the console is narrower
  than the logo, so a still-transient mode never wraps it into mush;
- runs tte at canvas-width cols-1 to stay off the autowrap column.

Verified the resize path in a tmux pane (a resizable pty): narrow first
paint falls back to centered text, and each later resize repaints a
correctly centered logo, with a clean exit on Return.

Co-Authored-By: Claude <noreply@anthropic.com>

* Ask the owner's timezone at first boot and hand off straight to Omarchy

Three changes to the deferred first-boot setup:

- Timezone: deferred provisioning skips the installer's user step entirely
  (it defers user creation to first boot), but the first-boot form only
  re-asked username/password/name/email — the timezone picker was never
  carried over, so every deferred machine silently kept archinstall's UTC
  default and the owner was never asked. Add the timezone step to the
  first-boot user form (geo-guessed default via tzupdate, same as the ISO
  installer's user step), show it on the confirmation screen, and apply it
  during provisioning with timedatectl.

- Rename the progress-screen title from "Setting up your account" to
  "Setting up your machine": first boot provisions the whole machine, not
  just an account.

- Drop the timed "Installed Omarchy in Xm Ys" celebration screen and the
  "Start Omarchy Now" button from first boot. That send-off belongs to a
  direct install; here the oneshot service just hands straight off to SDDM
  (Before=display-manager.service), which autologins on encrypted installs.
  A brief "Starting Omarchy..." covers the handoff.

Co-Authored-By: Claude <noreply@anthropic.com>

* Autologin only the first deferred boot on unencrypted installs

The owner authenticates in the first-boot form, so setup hands straight into
the desktop instead of asking again at SDDM. Encrypted installs keep autologin
permanently (the LUKS prompt is the auth boundary); unencrypted installs
autologin just this once and a self-removing service deletes the drop-in after
the desktop is up, so later boots use the normal SDDM login and the disk isn't
left permanently open.

Co-Authored-By: Claude <noreply@anthropic.com>

* Ask the owner's hostname at first boot too

Deferred provisioning no longer bakes a hostname into the install, so add the
hostname to the first-boot user form (same letters/digits/dashes validation as
the ISO installer's user step, defaulting to "omarchy" on empty), show it on
the confirmation screen, and apply it with hostnamectl during provisioning —
alongside the timezone step.

Co-Authored-By: Claude <noreply@anthropic.com>

* Let the greeter logo appear from the animation, not a static pre-render

The greeter drew the logo statically in green and then started the tte
ColorShift over the same rows, so the logo visibly flashed as tte's first
frame repainted it. ColorShift shows the full logo on frame one (it's a color
effect, not a type-in), so let tte paint the logo itself: skip the static
render when tte is available (keep it only as the no-tte fallback). The logo
now simply appears, already animating, with no flash.

Co-Authored-By: Claude <noreply@anthropic.com>

* Drop the greeter color animation; show the logo in solid green

The greeter "flash" was tte's ColorShift crushing on the framebuffer console:
tte emits 24-bit truecolor, the console only has 16 flat colors, and
consecutive frames' greens snap to different buckets (green/bright-green/cyan/
gray) — a frame-to-frame judder. A smooth color fade is impossible on the
console (no blending), so the animation can't be made clean there. Show the
logo in solid green (indexed color 2, which renders stably) with no animation.
The resize-redraw resilience stays; there's just no tte to tear down now.

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix first-boot review findings: autologin-once ordering, offline timezone

Review of the deferred first-boot flow surfaced two real, user-facing bugs:

- Autologin fired twice on unencrypted installs. The one-shot cleanup unit was
  ordered After=display-manager and enabled via a graphical.target.wants symlink
  + daemon-reload during the first boot — but graphical.target's job for that
  boot is already computed, so systemd never pulled the new unit in. It only ran
  on the *second* boot (which also autologged in) and removed the drop-in there.
  Order it Before=display-manager instead (and drop the sleep fudge): this boot
  autologins, the next boot's fresh transaction runs the cleanup before SDDM
  reads its config, so that boot shows the normal login. Exactly one autologin,
  deterministic, no race.

- Offline first boot aborted setup. `geo_guessed_timezone=$(tzupdate -p)` is a
  plain assignment under `set -e`, and tzupdate exits non-zero with no network —
  the common case for a fresh machine — killing the attempt before the offline
  gum-filter fallback. Guard it with `|| geo_guessed_timezone=""`.

Also: configure_timezone no longer symlinks /etc/localtime to a nonexistent
zone file on a bad zone (guards on the zoneinfo file existing); scale_console_font's
stty-size fallback emits a trailing newline so `read` can't trip `set -e`; and
drop two stty-sane calls that only existed to recover from tte's raw mode (tte
is gone).

Co-Authored-By: Claude <noreply@anthropic.com>

* Seed placeholder hostname/timezone in deferred install, overwrite at first boot

Simpler than the empty-string approach: instead of writing hostname=""/timezone=""
and having the orchestrator fall back, the deferred install now seeds neutral
valid defaults (hostname=omarchy, timezone=UTC) and the first-boot wizard
overwrites both. Identical end state, one decision in one place, and no reliance
on archinstall accepting empty config values. Runtime comments updated; the
configure_* functions are unchanged (they still overwrite whatever was seeded).

Co-Authored-By: Claude <noreply@anthropic.com>

* Bring back the greeter ColorShift animation

Reverts the "drop the color animation" change — the framebuffer's truecolor
crush gives the ColorShift a quick flash, but that's acceptable; removing the
effect entirely was overreach. Restores the static green logo + looping tte
ColorShift overlay (green base, cyan accent, settling on green), keeping the
resize-redraw resilience and every other fix intact.

Co-Authored-By: Claude <noreply@anthropic.com>

* Hide the omarchy-provision-* commands from the listing

These run from services and install hooks (first-boot provisioning, per-user
finalize, first-login) — never something a user invokes directly. Mark them
omarchy:hidden=true and drop the group/examples that implied a user-facing
route. The binaries still work when called directly by their callers.

Co-Authored-By: Claude <noreply@anthropic.com>

* Hide the factory-reset first-boot worker from the listing

omarchy-system-factory-reset-finish runs from a first-boot service after a
reset — never invoked by hand — so hide it like the provision workers.
omarchy-system-factory-reset itself stays visible (Setup > Reset Computer).

Co-Authored-By: Claude <noreply@anthropic.com>

* Address final-review findings: LUKS fail-open, set -e aborts, drop-to-console

Codex xhigh final pass surfaced several real issues; the contained ones:

- LUKS re-key could fail open (security). If the user's slot couldn't be
  identified, or luksDump/luksKillSlot failed, the code shredded the staged key
  anyway while the throwaway/seller slots stayed valid — leaving the disk
  unlockable by the install passphrase forever. Now it's all-or-nothing: require
  the user's slot, enumerate and kill every other slot, and only retire the
  staged key once all kills succeed; otherwise fail loudly and keep it for retry.

- Greeter animation cleanup: `kill "$anim"` was unguarded, so if tte had already
  exited (crash or resize race) it returned non-zero and `set -e` aborted setup,
  dropping to SDDM with no user. Guard it with `|| true`.

- reset_limine_config: `old_ids=$(grep ... | ...)` on an entry-less limine.conf
  (left by a failed rebuild) exits 1 under pipefail and aborted the retry. Guard
  with `|| true` so a rebuild failure stays recoverable.

- "Drop to console" on a failed setup exited to nothing usable (no user, root
  locked, tty1 getty conflicted). Exec a root shell on tty1 instead; SDDM starts
  when it exits.

Co-Authored-By: Claude <noreply@anthropic.com>

* Drop Bosnian and Khmer from the keyboard picker

Neither has a console keymap (loadkeys ba / loadkeys khmer both fail), so
picking them showed the choice as confirmed but silently left the system on the
default US layout. Remove them from the first-boot picker rather than offer a
layout that can't apply. The orchestrator keeps its defensive unsupported-keymap
handling for install-time robustness.

Co-Authored-By: Claude <noreply@anthropic.com>

* Finish the omarchy-first-run rename in the docs

Two doc references to omarchy-first-run were missed when the script was renamed
to omarchy-provision-first-run; update them to match.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 22:01:35 +02:00
6fa4f78ee1 Add deferred first-boot provisioning and factory reset (#6621)
* Add OEM first-boot setup and factory reset

An OEM-mode ISO install (or omarchy-reset-computer) leaves the machine in OEM
state: fully installed, no user, /var/lib/omarchy/oem/pending armed. On the
next boot omarchy-oem-setup.service runs the configurator's user form on tty1,
creates the user with the groups system setup recorded, finalizes it offline
from the stashed Node tarball, re-keys LUKS from the throwaway install
passphrase to the user's password, and hands off to SDDM.

omarchy-reset-computer returns a machine to that state: it swaps the running
root for a fresh clone of the @factory snapshot the ISO takes at install time,
scrubs machine identity and prior users, and stages omarchy-factory-wipe to
drop the old root and recreate @home/@log on the next boot. Machines installed
before @factory existed get a degraded reset (current system kept, users and
state wiped) with that caveat surfaced in the confirmation.

omarchy-setup-system/-hardware gain --oem to run without an install user; the
group-granting install scripts now record their groups in
/var/lib/omarchy/oem/groups and only call usermod when the user exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Harden OEM setup: correct cryptsetup key-file usage, retry on failure

cryptsetup reads --test-passphrase/--key-file inputs byte-for-byte, so feed
passphrases through process substitution consistently instead of positional
args or stdin (which has different newline semantics). Run each first-boot
setup attempt as its own process so a failure offers a retry instead of
stranding the machine at a user-less login screen — bash ignores errexit
inside `while !` conditions, a child process does not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Always grant wheel sudo in OEM first-boot setup

Detecting an existing %wheel grant by grepping sudoers is error-prone:
omarchy ships narrow '%wheel ALL=(ALL) NOPASSWD: <command>' rules (e.g.
asdcontrol) that match the naive pattern, which left the OEM-created user
matching sudoers entries but unable to run anything. Write the drop-in
unconditionally — a duplicate of an existing full grant is harmless.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix LUKS re-key device resolution and OEM state readability

archinstall's encrypted installs put cryptdevice=PARTUUID=... on the kernel
cmdline, not UUID=, so the first-boot re-key never found its device and
silently skipped — leaving the throwaway auto-unlock keyfile in place, i.e.
the disk effectively unencrypted. Parse every cryptdevice= source spec form
and make any re-key failure abort the attempt loudly: a retry prompt beats a
machine that quietly boots without a passphrase forever.

The OEM state directory also has to be world-readable (its one secret,
luks-key, stays 0600): user finalization reads the stashed Node tarball as
the new user, and the 0700 directory forced it onto the network fallback.

Step markers now land in /var/log/omarchy-oem-setup.log for debuggability.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Purge stale machine-id boot entries when resetting or re-keying

limine-entry-tool keys its limine.conf OS entries by machine-id. A factory
reset gives the machine a fresh identity, so the previous system's entry
survived every rebuild, sorted first, and made Limine stop at a Blake2b
hash-mismatch warning once the UKI was rebuilt. Start limine.conf over from
the shipped template (and drop foreign machine-id history directories on the
ESP) before any post-reset rebuild: in the staged chroot rebuild, in the
first-boot LUKS re-key, and — for unencrypted resets, where nothing else
rebuilds — in a dedicated first-boot refresh when foreign entries are found.

The staged rebuild also verifies every UKI hash referenced by limine.conf
against the file on the ESP before the subvolume swap, and the running
system's limine-snapper-sync is runtime-masked during staging so it cannot
rewrite the config behind the rebuild.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Harden reset and first-boot setup failure paths

Review findings from codex and Copilot:

- Generate throwaway passphrases without a trailing head stage: under
  pipefail, SIGPIPE from the infinite tr failed the substitution and errexit
  aborted every encrypted reset before it could stage anything.
- Stage the fallible parts of a degraded reset (LUKS re-key, boot rebuild)
  before arming the wipe, so a staging failure leaves the machine untouched
  instead of scheduling a wipe for a reset that never finished.
- Gate first-boot setup on the factory wipe having succeeded
  (ConditionPathExists=!wipe-pending plus an in-script guard): creating the
  new user on a half-wiped system would hand their data to the wipe retry.
- Abort the wipe (keeping its retry marker) when deleting the old root or
  recreating @home/@log fails, and abort resets that cannot remove a prior
  account — a surviving account keeps its password and wheel membership.
- Resume a partially-created account on setup retry instead of rejecting the
  username the failed attempt just created.
- Only purge machine-id directories the old limine.conf actually referenced;
  a shared ESP may hold other installations' boot artifacts.
- Recreate the hibernation swapfile (nested subvolume, so never captured by
  the factory snapshot) inside the factory root before its UKI rebuild, so a
  reset machine keeps disk-backed swap and a valid resume offset.
- Source base-test.sh in the OEM groups test per test conventions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Recreate the hibernation swapfile even when resume drop-ins survive

omarchy-hibernation-setup short-circuits as 'already set up' when the resume
mkinitcpio drop-in exists — which it always does in a factory root, while the
swapfile itself never survives the snapshot (nested subvolume). Drop the
marker when the swapfile is gone so setup reconfigures from scratch, and
verify the swapfile actually exists before proceeding with the reset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Second review pass: encrypted-config coverage, factory-baseline sanitization, recoverable rekey

Codex xhigh round 2:

- Detect the LUKS backing device by walking the root's device tree, not only
  the cmdline cryptdevice=; reset/first-boot now re-key roots reached via
  rd.luks/crypttab too, instead of silently leaving the seller's slots valid.
- Sanitize the retained @factory baseline (accounts, /etc/shadow, machine
  identity) during a full reset: the new wheel user could otherwise mount it
  to recover the seller's data, and a second reset would restore the account.
- Re-key the disk recoverably: rebuild the no-auto-unlock UKI before killing
  the throwaway slot or destroying the staged key, and restore the keyfile if
  that rebuild fails, so a retry with a different password can never leave the
  disk locked to the first attempt's password.
- Roll back a degraded reset's live-root auto-unlock material if its boot
  rebuild fails, instead of leaving it for a later rebuild to embed.
- Treat a missing current-machine limine entry as stale so a retry after a
  failed rebuild repairs the config instead of clearing OEM state over it.
- Erase fingerprint enrollments (/var/lib/fprint) in degraded wipes.
- Remove the resume-offset drop-in too when recreating the factory swapfile,
  so the rebuilt UKI gets a correct offset.
- Pin first-boot retries to the account the first attempt created.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Expose factory reset in the Setup menu

Add a 'Reset Computer' entry under Setup (Omarchy's Settings menu, where OS
factory resets conventionally live), guarded to btrfs roots and launched in a
floating terminal. omarchy-reset-computer now self-elevates via sudo so the
menu entry needs no sudo prefix, forwarding the caller's gum theme env as
env arguments so styling survives an env_reset sudoers. The typed 'reset'
confirmation and the sudo password prompt remain as the guards against
accidental triggering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Defer keyboard selection to first boot for OEM installs

The OEM first-boot setup now runs a keyboard step before the user form,
mirroring the ISO configurator: it loads the chosen layout on the live VT so
the password (and the LUKS re-key that follows) are typed under it, and
persists it with systemd-firstboot so the installed system gets both the
console KEYMAP and the XKB layout Hyprland reads — exactly what a normal
install writes. Layouts localectl doesn't know keep the default, same as the
installer.

This lets the OEM operator set nothing user-specific: the machine's owner
picks their keyboard alongside their account at first boot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Rename factory-reset commands to omarchy-system-factory-reset[-finish]

omarchy-reset-computer      -> omarchy-system-factory-reset
omarchy-factory-wipe        -> omarchy-system-factory-reset-finish
(and its systemd unit, log path, and temp mount to match)

Pure rename: every reference — the Setup menu action, the first-boot finish
service the reset stages and enables, the oem-setup ordering/gating, comments,
and the menu test — moves together, with no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Rename OEM vocabulary to provisioning (runtime)

Commands unify under the provisioning family:
  omarchy-oem-setup      → omarchy-provision-owner
  omarchy-finalize-user  → omarchy-provision-user
  omarchy-first-run      → omarchy-provision-first-run

And the deferred-provisioning state/vocabulary replaces 'OEM':
  /var/lib/omarchy/oem/          → /var/lib/omarchy/provisioning/
  /etc/omarchy/oem.key           → /etc/omarchy/provisioning.key
  install/oem/                   → install/provisioning/
  OMARCHY_SETUP_CONTEXT=oem-firstboot → provision-owner
  omarchy-setup-system/-hardware --oem → --defer-provisioning

All callers (provision-first-run→provision-user, autostart, factory-reset
staging the provisioning units, the group-recording scripts) and comments
move together.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Drop remaining OEM mentions from the provisioning groups test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Finish the omarchy-first-run rename in the docs

Two doc references to omarchy-first-run were missed when the script was renamed
to omarchy-provision-first-run; update them to match.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 21:54:21 +02:00
e1d0c4e0a8 Ship the keyboard layout widget on the bar and make clicking it work (#6659)
* Hide the keyboard layout widget on a single-layout install

There is nothing to read or switch when only one layout is configured, so the
label is noise on the bar most people have. Hide it until the keyboard reports
more than one, and keep showing it on a Hyprland that doesn't report the list
at all rather than hiding the widget everywhere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Put the keyboard layout widget on the bar by default

The widget hides itself unless the active keyboard has more than one layout,
so shipping it costs a single-layout machine nothing and saves everyone else
from finding it in the plugin list. Sit it just right of the clock, and add it
to existing bars the way the agents widget was added, leaving a curated bar
and a disabled widget alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Cycle the layout with the hyprctl command that exists

switchxkblayout is a hyprctl command, not a dispatcher, so sending it over the
dispatch socket only produced a Lua syntax error and clicking the widget did
nothing. Run it instead, against the keyboard the label was read from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Add an idempotent bar add command

Nothing put a widget on the bar without going through the running shell:
plugin enable and bar move both forward to it over IPC, which a migration
cannot rely on. Add writes the config file the way position and transparent
already do, and leaves a widget that is already on the bar where the user put
it, so callers can ask for it repeatedly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Put the keyboard layout widget on bars through the bar CLI

The hand-written jq was a normalizer, a presence check and a splice for what
is now one command that carries all three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Keep bar add from writing a bar the shell was not reading

The shell takes a user shell.json only when it parses, says version 1, and
carries a bar layout, and does not deep-merge; anything else leaves the
shipped defaults on screen. Reading and writing the user file regardless
turned a config holding nothing but an idle timeout into a bar holding
nothing but the new widget, and made an unparsable one abort the migration
chain on every update. Work against whichever layout is actually in effect,
seeding the defaults before placing a widget they do not already carry.

A malformed hand-installed manifest fails the whole plugin catalog, which was
enough to refuse a first-party widget, so treat an unreadable catalog as no
answer rather than a no. Leave a widget listed in disabledPlugins off the bar
instead of writing a layout entry the registry refuses to load, and re-check
presence inside the mutation so two adds cannot both miss it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Read a widget's default bar section in one place

cmd_defaults spelled out the same "defaultSection, or center when it is
missing or not a section" rule that the add path already asks for by name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Rename bar add to bar put

'omarchy plugin add' installs a plugin and 'omarchy bar add' placed one that
was already installed, which is too much meaning for one verb.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Place a newly added bar widget with bar put

plugin add reached the bar through plugin enable, which forwards to the
running shell, so it first had to poll until the shell noticed the clone and
then failed outright when no shell was there to ask. Putting a widget on the
bar is a config edit, so do that directly and leave plugin enable to the
plugins that need registering rather than placing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Put bar widgets through the shell instead of the config file

Placing a widget existed twice: once in PluginRegistry, which the shell uses
and owns the config it holds in memory, and once as jq against shell.json.
The second was there so migrations could run without a shell, which they do
not need to: the Quattro upgrade hands over the shipped shell.json before it
runs any, and every other path runs inside a session with a shell up. Ask the
shell, and say so and carry on when there is none to ask.

putBarWidget enables only what is not already on the bar, which is what a
caller that cannot know whether it ran before needs, and is the one thing the
existing enable path would not do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 19:38:46 +02:00
6ddc39520d Clean up the terminal and reconnect when SSH connections drop (#6661)
* Clean up the terminal and reconnect when SSH connections drop

A remote tmux, herdr, or editor arms terminal modes over the SSH pipe
(mouse tracking, focus reporting, the alternate screen) that only it can
disarm. When the connection dies instead of exiting cleanly, those modes
stay armed on the local terminal, and every mouse move floods the prompt
with escape-sequence junk.

Wrap ssh in a shell function that disarms those modes after every exit,
and automatically reconnects when an established interactive session
drops. Remote commands, configured RemoteCommands, and redirected stdin
never reconnect, so their side effects cannot replay, and the retry loop
runs in a subshell so Ctrl-C cancels both the in-flight attempt and the
loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Detect dead SSH connections within a minute

Without keepalives, ssh does not notice a dead peer until TCP gives up,
which can take hours of sitting on a hung terminal with remote-armed
terminal modes stuck on. Ship a client keepalive default so drops are
detected in about 45 seconds, letting the shell's ssh wrapper clean up
and reconnect. ~/.ssh/config is read first and wins, so per-host
overrides still apply.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fail closed when ssh -G cannot resolve the effective config

An unresolvable configuration could hide a RemoteCommand, so treat it
as non-interactive rather than reconnectable. Also strengthen the
tests from Copilot review: assert the complete disarm sequence, and
verify on a real interactive pty that Ctrl-C during a retry attempt
kills the reconnect loop itself, not just the in-flight attempt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Tolerate the explicit RemoteCommand none when probing ssh -G

The literal "none" is how ssh_config cancels a configured
RemoteCommand, and some OpenSSH versions emit it even when unset, which
would have silently disabled reconnecting entirely. Treat it as no
remote command while still failing closed on real ones and unresolvable
configs, and make the fake ssh -G emit the "none" form so the behavior
tests cover it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 19:15:21 +02:00
David Heinemeier Hansson f511cf8642 Hide herdr's outer pane frame like tmux 2026-08-09 14:32:15 +02:00
David Heinemeier Hansson ba7925afd8 Carry tmux's tab moves, zoom flag, and hostname into herdr 2026-08-09 14:25:04 +02:00
David Heinemeier HanssonandClaude Opus 5 e4a8e01409 Tag foot's other app-id as a terminal
The class is matched in full, so the bare "foot" alternative never covered
a window carrying org.codeberg.dnkl.foot. That left universal copy sending
CTRL + C into a terminal, the same way it did for the Omarchy TUIs.

default/hypr/apps/system.lua already treats that app-id as a foot window.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 05:11:43 -07:00
dd61d4a75b Ship herdr alongside tmux (#6406)
* Ship herdr with a config that mirrors our tmux setup

Installs herdr through the mise shim, ships the matching config as an
Omarchy default, and adds the usual refresh/restart pair. The keybindings
map tmux sessions to workspaces, windows to tabs, and keep both the prefix
and direct bindings from config/tmux/tmux.conf.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Add herdr versions of the tmux dev layout functions

hdl, hds, hdlm, and hsl drive herdr through its socket API instead of
tmux. hsl tiles into a real grid since herdr has no select-layout tiled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Namespace the herdr layout helpers so they stay out of the shell

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Create hdlm's tabs in its own workspace instead of the focused one

herdr tab create follows the focused workspace without --workspace, so
switching workspaces while hdlm loops scatters the new tabs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Lay hsl's grid out in visual order

Splitting the first column repeatedly inserted each new column between it
and the previous one, so uneven counts put the spare row in a middle
column instead of the last.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Report herdr config reload failures instead of swallowing them

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Hide herdr's pane scrollbars to match tmux

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Escape queued herdr layout commands

* Install herdr from the omarchy-herdr package instead of mise

* Use native herdr resize keybindings for tmux-style pane resizing

* Rename the omarchy-herdr package to herdr

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 13:57:36 +02:00
David Heinemeier HanssonandClaude Opus 5 19572a13c7 Skip compositor tests without recording a failure
Two problems made a missing Wayland compositor look like broken tests.

The cleanup traps ended on a bare conditional, so when a test skipped
before creating its TMPDIR the trap's last command returned 1 and, under
set -e, that overrode the explicit exit 0.

Six tests that launch quickshell had no compositor guard at all, so they
ran anyway and failed on the Qt platform plugin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 04:29:26 -07:00
David Heinemeier HanssonandClaude Opus 5 8be0ca9d43 Expect Tailscale on the right in the bar defaults test
99293aa gave the Tailscale widget a right defaultSection, but the test
still asserted it landed in center after the weather widget, and that
dropbox was the first widget after the tray.

Anchor both assertions on the tray so they track the placement contract
in omarchy-bar rather than hardcoded indexes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 04:22:01 -07:00
e4d85bd037 Keep concurrent usage collectors off one shared temp file (#6654)
Two Claude collectors running at once both wrote the cache through a temp
path derived from the target, so the second replace found the file already
moved away and crashed the update with a FileNotFoundError.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 13:19:46 +02:00
f8c235a5e4 Read /boot as root when looking for the Omarchy UKI (#6653)
* Find the Omarchy UKI as root when setting up direct boot

/boot is mounted with dmask=0077 on encrypted installs, so the
unprivileged find returned nothing and direct boot always reported that
no UKI was present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Check for the legacy UKI as root when refreshing limine

The unprivileged file tests were always false on encrypted installs, so
the stale <machine-id>_linux.efi was never cleaned up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 13:17:28 +02:00
1ded25fd45 Make a dead lock client diagnosable and recoverable (#6630)
* Persist the Omarchy shell log across sessions

Quickshell only logs to its instance runtime dir on tmpfs, so when the
shell dies the idle/lock event trail is gone after a reboot (#6628).
Launch the shell through omarchy-launch-shell, which pipes stdout/stderr
into the journal under the omarchy-shell tag — bounded, timestamped, and
persistent — and surface that log in omarchy-debug-idle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Recover a locked session whose lock client died

When the shell dies while the session is locked, Hyprland's failsafe
keeps the session locked with no lock client left, and
omarchy-restart-shell refused to run in exactly that state, leaving
reboot as the only way back in (#6628). Gate the refusal on the lock
service actually holding (or acquiring) the lock rather than on the
session's LOCK state — a dead shell and a crash-handler relaunch that
holds no lock both fail that check — then restart the shell, re-acquire
the session lock, and wait for it to report secure, the same
secure-poll omarchy-system-sleep-lock uses, so the user can
authenticate out of the failsafe. Enable Hyprland's
allow_session_lock_restore so the compositor accepts the replacement
lock client.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 12:59:32 +02:00
David Heinemeier Hansson 3aa7c1ee13 Rename menu 2026-08-09 12:48:52 +02:00
David Heinemeier Hansson 4d29cbbb66 Use shared speed test dial scale 2026-08-09 12:32:53 +02:00
5a58f79876 Keep clicking a notification working after a shell restart (#6636)
* Keep clicking a notification working after a shell restart

Notification actions lived only in the sending process: `-a` appended
`-A default=default`, so notify-send blocked on a D-Bus ActionInvoked signal and
the caller ran the command when it arrived. Nothing about that reached disk, so a
restored popup had no action to run and its sender stayed blocked forever.

Replace `-a` with `--exec <command>`, carried as an `omarchy-exec` hint into the
snapshot's `exec` role. It travels through the popup files and history, and the
shell runs it on click, so restored toasts behave exactly like live ones and the
sender exits immediately.

That drops the scaffolding whose only job was keeping a blocked sender alive: the
first-run invitations lose their `--show` re-entry and two transient units each,
omarchy-migrate-notify loses its transient service, and the screenshot,
recording, download, and taildrop toasts lose their wrapper subshells.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Keep a failed toast from failing the work it announces

Moving these sends out of their backgrounded subshells put a fallible command
on the foreground path, where the `&` used to swallow its exit status. A
notification outage — including the shell restart this branch targets — now
propagates:

- taildrop's receiver dies under `set -e` mid-delivery
- omarchy-capture-screenshot reports failure for a screenshot it already saved
- a completed download exits before scheduling its thumbnail cleanup, leaking
  the mktemp file

Announcing is best-effort in all three: the work is already done by the time
the toast goes out.

Also drop the first-run sleep that spaced out the welcome and Wi-Fi toasts.
It compensated for the background notify-send processes this branch removes;
each send now returns only once the server has taken the toast, so sending in
order is enough to stack them newest-on-top.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Stop tying the preview cleanup to the toast's expiry

The shell loads a notification thumbnail into memory when the toast appears and
never re-reads the file, so the preview only has to outlive that load. Deriving
the cleanup delay from the expiry was false precision, and it turned -t into a
variable for no reason: -t is already the helper's expiry setting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 12:30:47 +02:00
David Heinemeier HanssonandClaude Opus 5 99293aa05a Place the Tailscale bar widget on the right
Without defaultSection, both 'omarchy bar defaults' and 'omarchy plugin
add' fall back to center, so upgrades landed Tailscale next to the clock
instead of alongside the other status widgets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 01:02:52 +02:00
b925431025 Give SSH commands the user-level tool paths (#6632)
* Give SSH commands the user-level tool paths

ssh host cmd runs neither a login nor an interactive shell, so on Arch it
gets the bare sshd PATH and can't find mise-managed tools like the agent
CLIs herdr scans for. Set PATH in the PAM environment (per-user via
@{HOME}), append the user-level dirs in env-bootstrap so login shells and
the uwsm session get them too, and source env-bootstrap before bashrc's
interactive guard for bash variants that read it non-interactively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Don't let an empty PATH turn into a cwd entry

Appending with a bare "$PATH:" prefix leaves a leading colon when PATH
is unset, which shells treat as the current directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 14:58:29 +02:00
David Heinemeier HanssonandClaude Opus 5 ab367b9477 Stop mise wrappers from waiting out the release cooldown
The wrappers install and upgrade their tool on first run, so the cooldown
held new versions back for days. Refresh the stubs on existing installs
too, since the generator change alone only reaches fresh installs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 04:01:02 -07:00
David Heinemeier HanssonandClaude Opus 5 76117fb8ba Take mise tool updates without waiting out the release cooldown
mise withholds releases younger than its cooldown, so omarchy update left
tools behind for days. The mup alias already bypassed it by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 03:12:08 -07:00
David Heinemeier Hansson 366b789be1 Revert "Nicer with an extra CR here"
This reverts commit a3aa3c9e82.
2026-08-08 02:35:44 -07:00
David Heinemeier Hansson a3aa3c9e82 Nicer with an extra CR here 2026-08-08 02:35:13 -07:00
David Heinemeier Hansson 48c17bb644 mise already gives the CR needed 2026-08-08 02:32:05 -07:00
David Heinemeier Hansson 412f6acb74 Don't need the extra CR 2026-08-08 02:31:23 -07:00
David Heinemeier Hansson a85880385f Make this note of where they're usable 2026-08-08 02:30:48 -07:00
David Heinemeier HanssonandClaude Opus 5 0f1e0ced36 Remove the omarchy-update-perform compatibility wrapper
Nothing calls it anymore; new code calls omarchy-update directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 02:30:48 -07:00
76dc120ccb Fit the About window to its rendered content (#6627)
* Fit the About window to its rendered content

The About fastfetch layout needs more columns than the shared 875x600
float provides, and the terminal's grid often only reaches its final
size after fastfetch has printed, leaving the output wrapped over the
logo or clipped at the initial 80-column grid (#6465).

Give About a dedicated org.omarchy.about app id so launch-or-focus can
actually match it, render through a loop that repaints on window size
or branding changes, and measure the rendered content to fit the window
around it with even padding, whatever the terminal font or About logo.

Fixes #6465

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Spacing

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 11:05:05 +02:00
David Heinemeier Hansson 47fa3ce3a5 Match style from other updates 2026-08-08 01:44:56 -07:00
318bf2c43a Keep the Quattro upgrade from aborting silently into an unsafe state (#6617)
The script is fetched from the branch but calls into the installed
/usr/share/omarchy tree, which can lag it. A packaged build without
bin/omarchy-done aborted apply_user_transition under set -e two thirds of
the way through: NetworkManager was already enabled, iwd was not yet
disabled, and nothing was printed, so the run read as finished.

The completion markers are now written directly instead of through
omarchy-done, and the two remaining unguarded packaged commands warn
rather than abort. Retiring iwd moves up next to the NetworkManager
enable it depends on, so no failure in between can leave both enabled.
An aborted run now says so instead of returning to the prompt on a green
progress line.

Fixes #6575

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 10:16:41 +02:00
007d6fcd7d Use the gmux backlight instead of the Touch Bar on T2 Macs (#6597)
* Match display backlight candidates against real globs

[[ ]] does not do pathname expansion, so amdgpu_bl* and acpi_video* only
ever tested for files with a literal asterisk in the name. Every machine
without intel_backlight silently fell through to the alphabetical first
entry, which picks acpi_video0 over amdgpu_bl0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Use the gmux backlight instead of the Touch Bar on T2 Macs

/sys/class/backlight on a T2 Mac holds appletb_backlight and
gmux_backlight. Neither was a candidate, so the alphabetical fallback
picked the Touch Bar and brightness keys dimmed it instead of the
display. Add gmux_backlight and never fall back to the Touch Bar, which
is not a display panel on any Mac.

gmux ranks above the GPU backlights because apple-gmux only registers
its device when the kernel has already selected it for the machine, and
on dual-GPU Macs the GPU's own PWM stops driving the panel as soon as
that GPU suspends.

Fixes #6558

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 23:59:50 +02:00
0b24b844df Scope BROWSER to interactive shells so xdg-settings can change the default browser (#6616)
Exporting BROWSER=omarchy-launch-browser into the whole uwsm session made
xdg-settings refuse "set default-web-browser", which broke every browser's
own "Set as default" button. The export only exists for terminal programs
(like gh) to open URLs detached from the terminal process tree, so move it
to default/bash/envs where interactive shells still pick it up.

Fixes #6590

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 23:58:51 +02:00
f76d058a6d Force a database refresh before installing keyrings during the Quattro upgrade (#6615)
The upgrade repoints the mirrorlist and the [omarchy] server, then ran
pacman -Sy. A plain -Sy keeps the legacy database whenever the new server's
copy isn't newer, so the checksums stay stale and every re-download of a
rebuilt package aborts as corrupted.

Fixes #6576

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 23:52:24 +02:00
77cf58ccfe Add Fireworks balance usage panel (#6488)
* Add a Fireworks balance collector and teach the agents panel prepaid ledgers

The omarchy-agent-usage-fireworks collector reads serverless token usage
from the Fireworks billing API, grouped by day and model for the last 30
days, and reshapes it into the shared record contract. Fireworks does not
expose its prepaid ledger through the documented API, so the record carries
an estimated balance instead of rate limits: credits configured in
~/.config/omarchy/agents/fireworks.json minus rated account costs since the
funding date. Credentials come from FIREWORKS_API_KEY/FIREWORKS_ACCOUNT_ID,
the auth.ini that firectl set-api-key writes, or — last, so an explicit
login wins — the key opencode stores for its fireworks-ai provider.

The panel gains two generic capabilities any agent record can use: a
balance object draws a BALANCE section — remaining credit, a fuel-gauge
meter that drains toward empty and lights the bar alarm below 10%, and
funded-versus-spent detail — and hasPromptStats: false keeps prompt and
session counts out of today's tooltip for agents whose billing API only
ever reports tokens, on this machine and through synced snapshots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Feed Claude and Codex usage from pi, omp, and opencode sessions

A subscription burned entirely through another coding agent leaves no
native Claude Code transcripts and no Codex session files, so the panel
showed nothing for it. pi and omp write compatible JSONL sessions, and
opencode records per-message provider, model, and token usage in its
message database; the claude and codex collectors now scan all three —
filtered to Anthropic and OpenAI providers respectively — and merge those
numbers into their local stats. Fireworks stays out on purpose: its billing
API already sees that traffic server-side, and a local scan would count the
same tokens twice.

The collector tests pin XDG_DATA_HOME so a developer's real opencode
history cannot leak into fixture runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 23:49:43 +02:00
b85ae70ebd Stop pipefail from turning grep -q SIGPIPE exits into false negatives (#6614)
* Stop pipefail from turning grep -q SIGPIPE exits into false negatives

grep -q exits at the first match, and when the producer is still writing
it dies with SIGPIPE. Under pipefail that 141 becomes the pipeline's
status, so hardware checks like lspci | grep -q read as "not found" on
exactly the machines they target. The T2 defaults migration hit this and
silently skipped real T2 Macs (#6608).

Redirect grep to /dev/null instead of -q wherever a pipeline feeds grep
in a pipefail context, so grep reads all input and the producer never
gets killed. The install-time T2 checks aren't run under pipefail today
but are switched too, since they're the same detection line the issue
calls out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Re-run the T2 defaults migration its broken hardware check skipped

The SIGPIPE bug marked 1785944594 as applied without doing anything on
affected T2 Macs. The original migration is idempotent, so a fresh
migration can just source it now that the guard is fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address Copilot review: fix OCR grep pipeline and prove the T2 repair

screen_contains piped tesseract into grep -Fqi under the acceptance
suite's pipefail, the same SIGPIPE false negative the rest of the branch
fixes. The T2 test's lspci stub now keeps writing past the pipe buffer
after the match so every scenario exercises the SIGPIPE case, and a new
case runs the rerun migration against fixtures a bitten install would
have.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 23:43:49 +02:00
667d2d2f31 Open panel hotkeys on the focused monitor (#6613)
A bar surface is built per monitor, so panel routing had several live copies
of the same widget to choose from and took whichever registered its slot
first. Pick the one on the monitor Hyprland has focused instead, preferring
an already-open copy so hide and toggle still reach the visible panel.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 23:32:18 +02:00
2521b11fdd Add a disk speed test under a new Trigger > Tests menu (#6607)
* Extract the speed test gauge cluster into a shared SpeedTestOverlay

The dial cluster -- scrim, ignition sweep, self-ranging dials, run-again
button -- moves from the network speed test panel into qs.Ui with the
labels, unit, title, scale stops, and layer namespace as parameters, so
other measurements can wear the same cluster. The network panel keeps
its process handling and becomes a thin dressing of the overlay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add a disk speed test and move speed tests under Trigger > Tests

omarchy-disk-speedtest streams live write and read MB/s once a second by
sampling the backing block device's kernel I/O counters while dd workers
generate the traffic, the same way the network test samples the
interface counters. The stress data is an incompressible urandom chunk
staged in RAM, written with fdatasync per pass and fadvise drop-behind:
O_DIRECT silently falls back to the page cache on btrfs, and zeros never
reach a compressed filesystem at all. Scratch files are created
exclusively per invocation and removed even when a dismissal interrupts
the run mid-phase.

The omarchy.disk-speedtest panel dresses the shared SpeedTestOverlay
with write and read dials in MB/s, titled with the hardware model of
the disk under test.

The menu grows a Trigger > Tests submenu holding the new Disk Speed
Test and the Network Speed Test, which moves there from Setup > Network.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Make the disk speed test reproducible, direct, and read-first

Successive runs could swing 40% because the settled figure was just the
last one-second sample of a single buffered dd stream, taken while btrfs
copy-on-write churned the extent allocator on every rewrite pass and the
fadvise cache-eviction dance stayed advisory.

The test files are now marked NOCOW, which is what makes O_DIRECT truly
direct on btrfs -- with checksums on it silently falls back to the page
cache -- and lets every rewrite land in place. Four parallel workers per
phase give the device a queue depth it can stretch out on, and the
figure the dial settles on is the steady-state average over the whole
phase with the first warm-up second excluded, not whatever rate the
final second happened to catch. Together this tightens successive runs
from +/-40% to a few percent of each other, at the device's actual
spec throughput.

The read phase now runs first, staged against freshly written files,
with the read dial on the left. Workers also only loop while the main
script lives, so a dismissal that loses the kill race can no longer
leave an orphan hammering the disk forever, and any worker dying before
the deadline fails the run instead of passing off partial figures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Drop the menu aliases from the speed test entries

Aliases are reserved for established alternate names users already
type, kept for compatibility -- not something new entries pick up by
default. Note that in the menu definition header and AGENTS.md so the
next entry doesn't repeat it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Group dial readouts with thousands separators

A gen5 disk reads five digits; 11,450 scans, 11450 doesn't. Uses the
locale's grouping separator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Trim redundant overlay props and unused imports from the speed test panels

The network panel restated the overlay's default unit and scale stops,
and both panels carried imports and an omarchyPath property nothing
uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Publish the specific speed test error whichever handler fires last

Process exit and stderr stream-finished have no guaranteed order, so a
failure that beat the collector showed the generic message forever even
when the command emitted an actionable one; the collector now replaces
it once the text lands. Also stop clearing the error on every stdout
line: only a new run should do that, or buffered output delivered after
a failed exit erases the failure message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Arm the disk speed test cleanup before any scratch file exists

A preflight failure -- tmpfs target, missing device statistics, not
enough free space -- exited between mktemp and the trap, leaking the
scratch files. Cleanup also now unlinks before stopping the workers and
sweeps once more after, so even a cleanup cut short by an impatient
SIGKILL leaves no names behind and a final worker pass cannot recreate
one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 23:04:42 +02:00
96bbe53634 Fix panel delegate segfault and the network panel's open stall (#6605)
* fix(network): drop the redundant rescan on the bar click

Opening from the bar ran open() and then a bare refresh(). open() already
triggers onOpenedChanged -> refresh(true), which defers the PHY scan by
disabling the scanner and re-enabling it from scanRestart. The bare
refresh() that followed defaults scanWifi to false, so it took the other
branch and set wifiDevice.scannerEnabled synchronously on the click frame,
undoing the deferral and stalling the open on NetworkManager's access-point
flood. It also double-started the DNS and band probes.

Co-Authored-By: shrijit <shrijitsrivastav@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(network): keep wifi rows QObject-free to prevent a delegate crash

wifiRow() embedded the WifiNetwork QObject in the row it returns, and those
rows are list-model data, so every delegate held a live QObject wrapper in a
var property. When NetworkManager churns the list -- a scan's access-point
flood, an AP disappearing -- the object can be destroyed while a delegate is
still incubating, and quickshell segfaults in QObjectWrapper::wrap_slowPath
on the dangling wrapper.

Project primitives only and resolve the backend object at action time via
the existing networkForSsid(). Both failNetworkAction() and
checkActionCompletion() already no-op on a null network, so a row whose
network has since vanished is handled the same way it was before.

Co-Authored-By: shrijit <shrijitsrivastav@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(bluetooth): keep device rows QObject-free to prevent a delegate crash

Same crash class as the wifi rows: scrollRows embedded the BlueZ Device
QObject in list-model data, so every delegate held a live wrapper in a var
property. Discovery churn -- a scan timeout dropping a device, an unpair --
can destroy the object while a delegate is still incubating, and quickshell
segfaults on the dangling wrapper.

Project primitives for both the scroll rows and the connected rows, and
resolve the backend object by address in deviceFor() for the click actions.
The keyboard flow already went through deviceAt(), which reads the live
device arrays directly rather than model data, so it is untouched.

Co-Authored-By: shrijit <shrijitsrivastav@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(network): guard row disconnects against a vanished network

Row activation resolved the WifiNetwork with networkForSsid() and passed the
result straight to disconnect(), which falls back to connectedWifiNetwork
when handed null. A row is a primitive snapshot, so scan churn can remove its
backing object while the row is still on screen -- activating it then tore
down whatever happened to be connected at that moment rather than doing
nothing.

Route both row paths through disconnectRow(), which resolves first and only
acts when the row still maps to a live network. disconnect() keeps its
fallback for callers that mean "drop the current connection".

Also covers the bar-click open path, which had no regression: the suite
already asserts against Panel.qml source, so assert the closed branch calls
open() alone and never a second refresh().

Co-Authored-By: shrijit <shrijitsrivastav@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: shrijit <shrijitsrivastav@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 17:32:36 +02:00
David Heinemeier HanssonandClaude Fable 5 d2b090f8fc Isolate XDG dirs in shell tests that launch quickshell with a fake HOME
Faking HOME alone was never enough: the shell QML and the agent usage
updater read XDG_STATE_HOME and XDG_CACHE_HOME directly, so a test
quickshell inherited the session's real paths. The bar widget contract
test instantiated the agents widget, whose refresh ran the real
collectors against the empty fake HOME and wrote hollow "Waiting for
auth" records into the developer's real usage data files — hiding the
agents widget from their bar — while littering the real cache with
per-tmpdir scan files.

Point XDG_CONFIG_HOME, XDG_CACHE_HOME, and XDG_STATE_HOME under the fake
home in every test that boots quickshell with one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 17:13:39 +02:00
David Heinemeier HanssonandClaude Fable 5 66b10156e4 Keep the agent mark fallback from stranding on a missing -light twin
The panel resolves marks by walking assets/<id>-light.svg then
assets/<id>.svg, advancing on Image.Error. But the candidates binding is
re-evaluated whenever the provider objects are rebuilt, and a fresh array
identity with identical content reset the walk to the -light candidate.
Re-pointing source at a URL whose load already failed emits no
statusChanged, so the walker never advanced again and agents that ship a
single mark — Claude — fell back to the generic bar glyph on light
surfaces.

Key the reset on the candidate URLs instead of the array identity, and
defer the error advance one tick so stepping source from inside its own
status change doesn't trip the binding-loop detector.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 17:13:31 +02:00
bb8d2f2cb3 Split agent usage into data files and rename the plugin to omarchy.agents (#6603)
* Add agent usage collectors that write display-ready data files

One omarchy-agent-usage-scan-<agent> collector per AI coding agent prints a
complete display-ready usage record — identity, tier, status, rate limits,
and today/week/all-time stats. omarchy-agent-usage-update runs every
collector it finds and writes the records atomically to
~/.local/state/omarchy/agents/usage/, so anything that displays usage only
ever reads JSON from there.

The Claude collector absorbs what the shell previously did in-process:
transcript scanning, the stats-cache/history fallback, credentials parsing,
and the OAuth limits probe, now with a probe throttle and last-good limits
kept across network failures. The Codex collector is the existing scanner
reshaped to the shared record contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Redo the model-usage plugin as omarchy.agents watching usage data files

The panel is now strictly a display. It discovers the JSON records that
omarchy-agent-usage-update maintains under
~/.local/state/omarchy/agents/usage/, watches them for changes, and draws
whatever appears — so adding an agent means shipping a collector, never
touching the panel. Marks resolve by convention (assets/<id>.svg with an
optional -light twin), the limits meters read a generic limits array, and
the per-provider QML adapters and in-plugin scanner scripts are gone.

Cross-device sync aggregation stays in the shell and keeps the snapshot
field names older versions wrote, so mixed-version fleets still merge in
both directions.

With the provider fan-out gone, the widget takes its real name: the plugin
id becomes omarchy.agents. A migration renames it wherever a user's config
mentions it — layout entries keep their settings and position, a disabled
widget stays disabled — then primes the data files once and drops the old
scanner cache. The migration test also drops a stale assertion that expected
migrations to restart the shell themselves, which c992cdff moved to
omarchy update.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address Codex review: synced-only tabs, limits retry, history fallback

Three data-availability gaps from review. An agent whose records only exist
in synced snapshots — a collector installed on just one machine — now gets
its tab by unioning the synced aggregate into the provider list, with rate
limits blank since those never travel. A Claude limits probe that reaches no
server at all writes retryAdvised into its record, and the shell honors it
with one 30-second retry instead of waiting out the full refresh interval,
restoring the old boot-before-DHCP behavior. And a machine with only
history.jsonl — no transcripts, no stats-cache — still reports today's
prompt and session counts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address second Codex pass: history-only visibility, targeted retries

Today's prompt and session counts now count toward an agent's presence in
the bar, so a machine whose only Claude source is history.jsonl shows up
without waiting for limits. And the 30-second limits retry passes the
advising agent ids to the updater, so an outage at one provider no longer
puts every other collector on a retry treadmill.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Drop omarchy-cmd-present jq guards from the agents migrations

jq ships in the default package set, which makes it a runtime invariant per
AGENTS.md — call it directly. The migration tests lose their now-unused
omarchy-cmd-present stubs with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Drop the scan infix from the collector command names

Collectors are omarchy-agent-usage-<agent>; the updater skips its own name
when globbing them, and the update test proves it with a decoy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Keep the credential store out of the printed usage record

The Claude collector now reads .credentials.json once into three scalars —
the access token, its expiry, and the plan label — instead of passing the
parsed store around. The token reaches nothing but the Authorization header
of the limits probe, and only the plan label may travel into the record,
which is what CodeQL's clear-text-logging alert on the record print was
unable to see when the whole dict flowed through.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 15:46:10 +02:00
48d77b5738 Persist on-screen notification popups across shell restarts (#6600)
* Persist on-screen notification popups across shell restarts

Mirror every popup to its own file under
~/.local/state/omarchy/notifications/ for exactly as long as it is on
screen: written when the toast appears, deleted when it expires, is
dismissed, is acted upon, or is replaced via freedesktop replaces_id.
On startup the directory is read back and still-valid popups re-shown,
so toasts survive the restart omarchy-update performs — critical
alerts, which never expire, always make it across.

Restored popups keep ids from the previous server generation, so the
replaces_id cleanup tracks them separately instead of mistaking a
fresh notification's reused id for a replacement, and the startup
restore only discards a persisted file when a live row with a
different timestamp has superseded it. Files are read back with awk
so a torn write can't glue itself onto the next file and take a valid
popup down with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Close the remaining cross-generation id collisions in popup persistence

Notification ids restart from 1 with every server process, so an id
alone never identifies a notification across a shell restart. The
first round of fixes guarded row removal, but review (and a live
repro) showed the same collision biting everywhere else an id was
used on its own:

- Dismissing or clicking a restored toast resolved liveRefs by id and
  could dismiss, or fire the action of, an unrelated fresh
  notification, and archive its pending row. Restored rows now never
  resolve to a live object, and pending rows are matched by id plus
  timestamp.
- parsePopupFiles deduped files by id, so a fresh notification reusing
  a restored critical alert's id got that alert's file deleted as a
  "stale duplicate" on the next restore. Files are never deduped now:
  each one is a popup that was on screen, and the rare genuine
  leftover from a crash re-shows once and cleans itself up.
- The restore only skips an entry when a live row matches both id and
  timestamp (it is that entry); an id-only match shows both toasts
  rather than guessing which one to drop.
- A same-millisecond replaces_id update shares its predecessor's
  filename; the replacement's file is no longer deleted alongside the
  replaced row.
- A restored popup's reset lifetime is persisted as an absolute
  deadline, so a second restart judges it by the clock that actually
  governs its display instead of dropping it while still on screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 15:14:56 +02:00
David Heinemeier HanssonandGitHub 3d033d1b00 Stop the menu from opening on the previous evaluation's answers (#6601)
* Evaluate menu guards one run at a time

A second evaluation starting while one was in flight could not replace it.
Process ignores a command change until the next run and `running = true` is
a no-op while running, so setting them did nothing -- but clearing
`collected` first threw away the lines the running script had already
emitted. Its tail then landed as the entire result, and every id missing
from it went back to showing, since `when:` only hides a row on an explicit
false. That is how Setup > Defaults > Browser ends up listing browsers that
are not installed.

Queue the evaluation instead and run it once the one in flight lands, the
way provider enumeration already waits its turn.

* Answer repeated menu guard questions once per evaluation

The menu opens on the last evaluation's answers, so however long the guard
batch takes is how long a row can contradict the state it describes: stop a
recording and Screenrecord still offers to stop it, because the `pgrep` that
would hide it is queued behind fifty package lookups.

Almost none of that time is the questions, it is asking them one process at
a time. The shipped menu runs `omarchy-pkg-present` 54 times and
`omarchy-cmd-present` 23, and reads `omarchy-default-browser` once per row
in Defaults > Browser. Prepend a prelude that answers all of it inside the
one guard process, off a single package listing, bash's own PATH lookup, and
one capture per reader command. The captures are eager because `checked:`
reads them inside `$()`, where a lazy memo would not outlive the subshell.

Takes the shipped batch from 1.49s to 0.25s with identical answers for all
175 guards.

* Make the guard prelude answer exactly as the commands it stands in for

The prelude only helps if it is indistinguishable from the commands it
shadows, and it was not:

- `pacman -Q` resolves a name through what installed packages provide, so
  with gvim installed it reports `vim` as present. A set built from
  `pacman -Qq` sees only names, so `install.editor.vim` came back and
  offered to install what was already there. Build the set from provides
  too, and send version constraints, which no set can answer, to pacman.
- `omarchy-cmd-present` uses `command -v`, which finds builtins; `type -P`
  searches PATH alone and disagreed on every one of them.
- Shadowing a reader with a function caught far more than the plain
  `$(reader)` the rows use: `command -v omarchy-dns` got the function name,
  and `VAR=x omarchy-channel-current` got an answer captured without the
  variable. Substitute the captured value into the expression instead and
  leave every other form to run the real command.
- A reader that exits nonzero could take the batch down under a login shell
  with errexit set.

Also keep the results of a batch that was killed rather than finished, since
a row whose `when:` went unanswered shows, which is the failure this set of
changes exists to remove.

Costs 0.25s -> 0.33s against 1.49s before any of this, still with answers
identical to evaluating each guard on its own.

* Read every provide pacman reports, wrapped or not

`pacman -Qi` wraps a long list onto indented continuation lines whenever
COLUMNS is set in the environment, which the login shell the batch runs
under may well have done. Reading only the line that starts with `Provides`
dropped the rest: at COLUMNS=80 that is 537 of 856 provides on this machine,
which puts back exactly the "offers to install what is already there"
failure the provides lookup was added to prevent. Follow the continuation
lines instead.

The version-constraint case was also not testing what it claimed.
Interpolating the argument into the shadow's script text let `bash>=1` parse
as a redirection, so the shadow was handed `bash` and quietly agreed for the
wrong reason -- and left an `=1` file behind, which got committed. Pass
arguments as argv to both sides, drop the file, and wrap gvim's provides in
the stub so the parser is held to the format pacman actually emits.
2026-08-07 14:52:44 +02:00
6ee243cc37 Split the end-user omarchy skill into topic guides (#6602)
* Split the end-user omarchy skill into topic guides

Move default/omarchy-skill to default/agents/skills/omarchy and break the
monolithic SKILL.md into on-demand topic files for Hyprland config, shell
plugins, theming, and hooks. Update the skill symlink wiring, relink
existing installs through a migration, and correct claims that had drifted
from the implementation: plugin hot-reload, terminal reload, menu
customization, refresh scopes, theme overlays, background locations, hook
timing, and the packaged (not git-managed) system directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add capture and contributing guides to the omarchy skill

Cover screenshots, screen recording, OCR text capture, and LocalSend or
Taildrop sharing, plus how to route bug reports, suggestions, and support
questions upstream with diagnostics and captures of the problem attached.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Scope Hyprland reload guidance to the Lua config files

hyprsunset.conf and xdph.conf are read by separate processes, so hyprctl
neither applies nor validates them. Document restarting hyprsunset after
editing its config, including in the night light example.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 14:36:42 +02:00
018f881840 Extract the Wi-Fi QR share card into its own omarchy.wifiqr panel plugin (#6598)
* Extract the Wi-Fi QR share card into its own omarchy.wifiqr panel plugin

omarchy-network-qr now leads with an iface/security/ssid meta line, so a
bare summon self-detects the connection and the plugin owns the whole
share flow. The network panel loses its overlay lifecycle: with no
centered card left inside it, the shadowed open/close collapses back to
the stock panel behavior, and the QR button just summons the plugin --
which a clone or third-party plugin can replace, like the speed test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Keep canceled QR and password runs from leaking into their replacements

Copilot review: the cancellation guards dropped in onExited while the
canceled run's collectors were still allowed to fire, so a stale stderr
could shadow a successful regeneration and a stale password could be
revealed under a new network's card. The guards now stay up until the
next run launches, good output settles any earlier error, and a bare
re-summon no longer inherits the previous card's SSID.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 12:50:04 +02:00
David Heinemeier HanssonandClaude Fable 5 474d9dd4eb Move agent guides into agents/skills/
Leave agents/ ready for other agent artifacts like definitions and
prompts alongside the skills.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 12:33:27 +02:00
David Heinemeier HanssonandClaude Fable 5 c2d81220d9 Split task-specific agent guidance into agents/ guides
Keep only universal rules in AGENTS.md and point to per-task guides
for shell development, acceptance tests, visual verification, command
metadata, and install scripts. Fold the migration notes into
docs/migrations.md and replace .claude/CLAUDE.md with a root CLAUDE.md
importing AGENTS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 12:33:27 +02:00
David Heinemeier HanssonandClaude Fable 5 bc75b03114 Show plugin ids as subtext in the plugin picker menus
Select-menu options gain an optional third field rendered under the
label, filtered alongside it, and returned with the selection. The
plugin picker uses it to show every plugin's id and act on the id the
selection hands back, replacing the duplicate-name label suffix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 02:38:06 -07:00
David Heinemeier HanssonandGitHub 9cb3640c9f Fix T2 Mac suspend and fan defaults (#6562)
* Fix T2 Mac suspend and fan defaults

* Avoid repeated T2 boot image rebuilds

* Harden T2 migration test matching
2026-08-07 11:15:28 +02:00
25a74f4e39 Stop Tuxedo backlight fix from aborting setup (#6583)
The orphaned module cleanup loop was the last statement in the script, so
when the glob matched nothing the trailing && list left an exit status of
1. run_logged propagates that, aborting hardware setup on every Tuxedo and
Slimbook install.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 11:14:30 +02:00
David Heinemeier HanssonandClaude Fable 5 dd2f434a7d Extract the speed test into its own omarchy.speedtest panel plugin
The network panel now summons it, so a clone or a third-party plugin
declaring clonedFrom: omarchy.speedtest can replace the whole speed
test -- dials and run orchestration alike -- for every caller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 02:13:31 -07:00
David Heinemeier Hansson 4519451655 Nostalgia for early Linux adoption 2026-08-06 16:50:10 +02:00
David Heinemeier Hansson 11a6ae2230 Move calculator to Super + Ctrl + Q to avoid conflict with resizing 2026-08-06 15:18:23 +02:00
David Heinemeier Hansson fd1034f71b Ship NordVPN directly from OPR 2026-08-05 22:57:56 +02:00
d9bc38a926 Apply the terminal tag to Omarchy's own terminal windows (#6561)
* Apply the terminal tag to Omarchy's own terminal windows

Omarchy launches TUIs and its own terminal windows under dedicated
app-ids (org.omarchy.btop, org.omarchy.terminal, TUI.float, ...), so the
class never matched the terminal that drew the window and those windows
went untagged.

Also drop the tag's opacity rule, which stripped default-opacity only to
re-apply the identical value. Themes still override through the tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Match terminals by tag for universal clipboard shortcuts

The binding kept its own list of terminal classes, so SUPER + C in a TUI
window sent CTRL + C instead of CTRL + Insert. Read the terminal tag
instead of duplicating the definition.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 22:56:01 +02:00
0269fe0031 Route menu ids ahead of app keyword aliases (#6563)
An installed app whose .desktop Keywords contain a menu id captured the
route: htop ships Keywords=system;..., so SUPER+ESCAPE opened an empty
"Htop" menu instead of the System menu once the Apps menu had merged its
rows. Exact ids now win, and app rows are no longer routable at all —
their keywords remain search-only.

Fixes #6554

Reported-by: Craig Derington (https://github.com/craigderington)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 22:50:41 +02:00
David Heinemeier Hansson 4c4e17077f Explain what the update means 2026-08-05 22:50:00 +02:00
David Heinemeier HanssonandClaude Fable 5 633f20f408 Move Plugins under Defaults and Direct Boot to the end of Setup
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 22:42:06 +02:00
David Heinemeier HanssonandClaude Fable 5 0a9cd98f3d Offer style installs only under Install > Style
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 22:40:54 +02:00
David Heinemeier HanssonandClaude Fable 5 b92510f4f5 Bind Super + Alt + Space to the apps menu
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 22:22:24 +02:00
David Heinemeier HanssonandClaude Fable 5 81778eaa02 Cap the menu height at the starting menu's height
The first submenu move or search keystroke already froze the card's top
edge; freeze the rows height at the same moment so drilling into a longer
menu scrolls behind the fold instead of growing the card.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 22:22:24 +02:00
David Heinemeier HanssonandClaude Fable 5 75538f5927 Headline Process and Hardware update submenus with Restart
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 19:57:44 +02:00
David Heinemeier Hansson e464ae193a No more OSD 2026-08-05 19:48:48 +02:00
David Heinemeier HanssonandClaude Fable 5 2daeaa7078 Remap bar and background surfaces when their monitor moves
Hyprland leaves an already-mapped layer surface at its old global
position when its monitor moves within the layout: undocking disables
the internal panel, the external monitor shifts to x=0, and the bar and
background keep rendering at the old offset until unmapped and remapped.
Watch each screen's origin and briefly unmap the window when it moves so
the compositor re-places the surface at the monitor's new origin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 17:23:17 +02:00
David Heinemeier HanssonandClaude Fable 5 b538dd545a Persist integer GDK_SCALE when monitor scale is fractional
GTK only honors whole-number GDK_SCALE values, so persisting 1.6 or 1.25
verbatim left GTK apps without a usable scale. Round to the nearest whole
factor when writing monitors.lua.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 08:30:12 -05:00
David Heinemeier Hansson fd02c26230 Matching browser icons 2026-08-04 12:49:28 -05:00
David Heinemeier HanssonandGitHub fe55ac264d Merge pull request #6498 from basecamp/add-default-agent
Add configurable default coding agent
2026-08-04 12:09:10 -05:00
David Heinemeier Hansson 3636c76e96 Rely on the session PATH for agent launches 2026-08-04 10:04:26 -07:00
David Heinemeier Hansson 884ee6943a Use official agent marks in default menu 2026-08-04 10:04:21 -07:00
David Heinemeier Hansson f293e9df26 Open agents after setting the default 2026-08-04 09:03:50 -07:00
David Heinemeier Hansson a7cc924a10 Show agent installation progress in a terminal 2026-08-04 07:45:36 -07:00