Commit Graph
1028 Commits
Author SHA1 Message Date
ecd57bceee Drop the kms hook when the proprietary NVIDIA driver handles early KMS (#6791)
* Drop the kms hook when the proprietary NVIDIA driver handles early KMS

install/hardware/nvidia.sh early-loads nvidia_drm (modeset=1) for early KMS,
but HOOKS still carried the kms hook, so autodetect pulled nouveau and
~100 MB of its GSP firmware into every initramfs for a driver that never
runs. On a Limine UKI setup that meant a 256 MB image where ~144 MB is
normal, doubled again by the fallback history on /boot.

Filter kms out of HOOKS when nvidia_drm is in MODULES (nvidia.conf sorts
before this drop-in) and every PCI display controller is NVIDIA. Hybrid
systems keep kms so the iGPU retains early KMS at the LUKS prompt.

Verified on an RTX 4090 (nvidia-open-dkms 610.57.04): UKI shrinks
256,183,296 -> 144,066,048 bytes, nouveau and its firmware gone, the
nvidia-utils GSP blobs and all four nvidia modules retained, Plymouth
still owns the LUKS prompt via nvidia_drm.

Fixes #6790

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

* Address review: quote literals, harden PCI detection, add shell tests

Quote fixed string literals in the [[ ]] comparisons per AGENTS.md, and
read the PCI tree through OMARCHY_PCI_DEVICES_PATH, the same seam
bin/omarchy-hw-nvidia already uses.

Require a positively identified NVIDIA display controller before dropping
kms: an empty or unreadable PCI tree previously counted as "no non-NVIDIA
GPU" and would have dropped the hook. Unexpected trees now keep kms.

Cover the conditional in test/shell.d/nvidia-kms-hook-test.sh: nvidia-only,
hybrid, no nvidia_drm, MODULES unset under set -u, audio-function-only,
empty tree, and a device directory missing its sysfs attributes.

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

* Treat unreadable PCI devices as inconclusive and make the test hermetic

A device whose class/vendor attributes cannot be read could be another
GPU, so skipping it let a readable NVIDIA GPU beside it drop kms without
having verified the whole tree. Count it as a non-NVIDIA sighting so kms
stays, and cover the mixed case in the test.

The test also sourced the host's /etc/vconsole.conf under set -u, where a
valid KEYMAP-only file makes the XKBLAYOUT expansion fail in the subshell
and ties the result to the machine running it. Predefine XKBLAYOUT and
FILES before sourcing the config.

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

* Rebuild existing initramfses once the kms hook no longer applies

The settings package deploys the new omarchy_hooks.conf conditional, but
nothing rebuilds the initramfs when only a mkinitcpio drop-in changes, so
existing NVIDIA-only installs would carry nouveau's ~100 MB of GSP
firmware until their next kernel update. Following the precedent of
1784476564, add a migration that rebuilds via limine-mkinitcpio — once
per machine, and only where evaluating the installed drop-ins shows the
conditional actually dropped kms, so hybrid machines, non-NVIDIA
machines, and user-edited configs are left alone.

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

* Document the mid-sourcing MODULES caveat in the kms conditional

A later-sorting drop-in that resets MODULES outright (as
surface_device_modules.conf does) would strip nvidia_drm after kms was
already dropped. Every machine Omarchy writes such a file for is hybrid
Intel and keeps kms through the PCI scan, but that is worth stating so
the invariant is not broken by accident.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-13 10:48:18 +02:00
6fad76184f Unmask wpa_supplicant left masked from the iwd era (#6793)
Quattro hands wifi to NetworkManager, which starts wpa_supplicant through
D-Bus activation. Installs carrying a wpa_supplicant.service mask from the
iwd days break that activation: NetworkManager retries five times, gives
up, and every wifi device sits at "unavailable" with no network to
research the fix on. Remove the mask (including a runtime one) and restart
an active NetworkManager when a wifi device is stuck, so wifi comes back
without a reboot.

Fixes #6783

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 10:36:01 +02:00
David Heinemeier HanssonandGitHub 2cc3510d2a Offer an AI diagnosis when a process crashes (#6746)
* Offer an AI diagnosis when a process crashes

systemd-coredump journals every core dump under a known MESSAGE_ID with the
crashing program, pid, and signal as structured fields. omarchy-crash-watch
follows that stream and raises a "Process crashed: <program>" toast; clicking it
opens omarchy-agent-crash, which briefs the default agent on the crash.

The toast goes through omarchy-notification-send --exec rather than a libnotify
action, because the shell runs clicks from its own omarchy-exec hint and never
emits ActionInvoked. It keeps the default "omarchy-action" app name too, the
only one shouldBypassDnd() lets through -- a crash being the last notification
worth swallowing. It stays quiet until an agent is configured, since a
diagnosis is all it offers.

The method lives in a diagnose-crash skill rather than the prompt, so it is
edited in one place and works with whichever agent is default. It covers
investigating the core, and reporting a confirmed Omarchy bug upstream: scoped
to bugs Omarchy controls, searched for duplicates first, only with the user's
agreement, and signed with the model and harness that produced it.

A migration reaches existing installs, whose skill symlinks and unit enablement
would otherwise sit behind one-time setup paths.

* Let the diagnosis clean up the core it extracted

"Do not modify or delete anything" contradicted the symbolization step right
above it, which writes a core to a temp file and deletes it on exit. Read
literally, the core survives -- and the same section warns it holds passwords
and tokens. The prohibition is about the system, not about your own scratch.

* Do not spend a crash toast on a dead notification server

The shell owns org.freedesktop.Notifications, so its own crash takes the
notification server down with it -- and a shell crash is exactly what you want
told about. The toast was sent once into that gap and the dedupe window was
recorded regardless, so the rest of the crash loop went quiet for a minute and
`journalctl -n 0` never replays what was missed.

It now waits for the restarted shell to reclaim the bus name, as
omarchy-migrate-notify already does, and only a delivered toast starts the
dedupe window.
2026-08-12 18:37:40 +02:00
David Heinemeier HanssonandGitHub 9502b81f3b Reshape the agent launcher into omarchy agent (#6757)
* Reshape the agent launcher into omarchy agent

omarchy-launch-agent becomes omarchy-agent, with prompts on omarchy-agent-prompt
rather than the bare route: `omarchy agent` is both a command and a group, so a
positional prompt there would shadow any subcommand under it. The launcher takes
flags only and points at `omarchy agent prompt` when handed one.

Every agent window now launches under a fixed org.omarchy.agent app-id instead of
omarchy-launch-tui's default of org.omarchy.<binary>, so one rule floats them all
whichever agent is default.

Omarchy also stops picking an agent for you. omarchy-default-agent prints nothing
until one is chosen, leaving every entry under Setup > Defaults > Agent unchecked,
and a first-run invitation offers to take you there.

* Wordsmith

* Cover the agent routes and the invitation

The route split is the point of the change, so exercise `omarchy agent`,
`omarchy agent prompt`, and a rejected positional prompt through the router
rather than only the binaries behind them.

The invitation gets the same treatment as the Voxtype and fingerprint ones: it
notifies once, opens the agent defaults menu, and leaves both the notification
and the marker alone for anyone who already chose an agent.

* Offer the agent choice from the keybinding

Super + Shift + Ctrl + A now runs `omarchy-agent --pick`, which opens Setup >
Defaults > Agent when nothing is chosen yet. A keypress that writes to stderr
and opens nothing just looks broken.

* Reach existing installs with the agent invitation

first-run installs the invitation hook, and existing accounts marked it complete
long ago, so they would never see it -- while being the accounts most likely to
need it, since the old getter returned opencode implicitly and most have no
agent recorded at all. Post-update hooks run later in the same update, so the
invitation arrives without waiting for another one.

* Say what the Defaults submenus set

Setup > Defaults lists Agent, Browser, Terminal, Editor, but the header inside
each repeated the same bare word, which reads as a category rather than a
setting -- and says nothing at all when the menu is summoned straight into it.
The list keeps its short labels; the headers now name the setting.
2026-08-12 17:56:19 +02:00
David Heinemeier HanssonandClaude Opus 5 ab57ad65fd Make notification history the last ten notifications on disk
History was a pair of in-memory lists mirrored into notifications.json, split
into "pending" and "past" by a seen/unseen distinction no surface exposed,
capped at 100, deduped by an id that repeats across server generations, and
pruned by a 15-minute TTL. Replaying it showed five rows drawn from whichever
list happened to hold them.

Every toast already writes a file under ~/.local/state/omarchy/notifications
so it can survive a shell restart. That file is now the history record: when
the popup leaves the screen it moves into notifications/history instead of
being deleted, the newest ten are kept, and showHistory replays exactly what
is in there, including the toasts still on screen when it is asked for. A
notification DND silenced is written straight into the same directory, since
a toast that never showed is the one worth looking back at.

That leaves the models, notifications.json history payload, past pruning, and
the /tmp image cache that existed to keep century-old history thumbnails alive
with nothing to do, so they go.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 00:58:14 -07:00
David Heinemeier HanssonandClaude Opus 5 2fa490dc96 Rebuild boot images the Plymouth migration left stale
1784917531 gated its UKI rebuild on initramfs_async=0 being present in
the Limine config, but omarchy-settings ships omarchy-defaults.conf with
that parameter already in it. Any machine that installed the package and
ran the migration in the same update matched the config the package had
just written, skipped the rebuild, and kept booting an image baked
before the config existed — without initramfs_async=0, so encrypted
boots still fell back to an unthemed text LUKS prompt. Compare the
booted command line against the configured one and rebuild when they
disagree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:35:40 -07:00
b99fd91cf1 Simplify the Quattro upgrade, and stop it leaving an error bar behind (#6716)
* Accept either name for the lock authentication command

omarchy-setup-lock was renamed to omarchy-apply-lock in 536fcd5c, but
the upgrade calls into whatever the channel just installed, and every
released package still ships the old name. The rename only moves in
lockstep for the ISO, which installs the runtime from the mirror it
ships with; the upgrade has no such guarantee, so it aborted every run
with "omarchy-apply-lock is unavailable" right before the point of no
return.

Prefer the new name and fall back to the old one.

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

* Leave the Omarchy 3 session alone until the reboot

The upgrade tried to start Quickshell in the live session, and only
stopped waybar/walker/elephant if that worked. Every branch of it could
fail, so it needed a warning for each, and those warnings were the first
thing users read at the end of an upgrade that had otherwise succeeded.

The reboot is the cutover. Swapping the UI out underneath a running
session buys nothing, so drop the attempt and both functions with it.
The Omarchy 3 bar, launcher, and notifications keep working until the
reboot, which is what happened anyway whenever the start failed.

Also warn up front when the live Hyprland session cannot be reached, and
drop the package_mode label that was set on both branches of the
dev-package check and never read.

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

* Shim the legacy Hyprland defaults from the backup, not from GitHub

populate_legacy_hypr_defaults tried a sparse git clone of master, then a
curl of the master tarball, and only if both failed the backup taken a
few lines earlier. That backup is the checkout the running session is
sourcing right now: it is the correct content, it is already on disk,
and it needs no network in the middle of an upgrade. master is a guess
that is wrong for any machine not on master.

Try the backup first and drop the git clone, which fetched the same
thing as the tarball by a longer route. The network path stays for the
case that has no backup, where the legacy root was already a symlink.

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

* Clear the Hyprland error bar the upgrade leaves behind

The two settings were applied through both the keyword and the Lua API,
in four calls on the way out and three on the way in, split differently
each time. Fold that into one helper that takes the value.

The error bar needed more than suppression. An explicit hyprctl reload
re-reads the config from disk, which resets both keywords before it
reports what it found, so suppression cannot survive one; anything that
reloaded during the swap left the bar on screen. Hyprland then keeps it
up until a later clean reload, which this script deliberately never
performs, so it was still there when the upgrade finished. Clear the
overlay on the way out, once the shims have made the legacy config
resolve again.

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

* Run the packaged firewall config instead of a copy of it

configure_snapper_policy and configure_lock_authentication already call
into the installed tree; apply_firewall_defaults reimplemented
install/config/firewall.sh inline instead, and had already drifted from
it. The packaged script also installs the ufw-docker rules, so upgraded
machines came up without the Docker firewall protections a fresh install
gets.

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

* Stop the Voxtype toggle migration reloading a mid-swap session

This is the reload behind the "source= globbing error" bar users see
during a Quattro upgrade. The upgrade runs the packaged migrations
against a still-running Omarchy 3 session, and this one ends with
hyprctl reload. That re-parses a legacy config whose theme source= has
nothing to resolve to yet, so Hyprland paints an error bar and keeps it
up until a later clean reload the upgrade deliberately never performs.
It was still on screen when the upgrade finished.

The upgrade already exports OMARCHY_UPGRADE_TO_QUATTRO_LIVE for exactly
this, and 1782002156 honors it. Do the same here. Nothing in that
session reads the toggle being removed; the reboot applies it.

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

* Assert the retired session entry points stay gone

The ordering check would still pass if either came back, while the
comment above it claims they cannot. Name them.

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

* Upgrade onto the channel the machine is already on

The upgrade always defaulted to stable, so an rc machine taking it
landed on production packages from the stable repo. rc callers worked
around that by passing --channel rc, which then forced every caller
onto rc, stable machines included.

Read the channel off the mirrorlist the way omarchy-version-channel
does and follow it: stable machines get omarchy and omarchy-settings
from stable, and rc or edge machines get omarchy-dev and
omarchy-settings-dev from edge, which is what --dev already selects.
An explicit --channel or --dev still wins, and an unrecognized
mirrorlist still falls back to stable.

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

* Keep rc machines on the rc mirror when taking dev packages

--dev forced the edge channel, so an rc machine following its own
channel was moved onto the edge Arch mirror as well. The constraint is
narrower than that: the dev packages are only published to the edge
package repo, which the rc and edge channels both already point at.
Only stable is incompatible.

Reject --dev only for stable, and default to edge just when no channel
was chosen. rc machines now upgrade against rc-mirror with the dev
packages out of the edge repo, which is where Quattro lives until it
ships.

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 22:47:39 +02:00
1bb6600c76 Repair theme symlinks the state-move migration left dangling (#6717)
* Repair theme symlinks the state-move migration left dangling

1781043107.sh re-linked legacy theme symlinks whose targets were stored
with a literal "~/.config/omarchy/current/..." string. The replacement
used the same literal tilde, which the filesystem never expands inside a
symlink target, so btop, Helix, and VS Code/Cursor lost their theme and
the migration reported success anyway.

Fix the source migration to relink through the already-defined
$current_state_dir variable, and add a follow-up migration that repairs
the links the applied version left dangling — matching the existing
1785002349.sh pattern, so it is idempotent and leaves custom links alone.

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

* Only repair theme symlinks that could never have worked

The repair matched any target containing omarchy/current, so a working link
into a user's own dotfiles was rewritten to the state directory and their
setup was lost. Claim a link only when its target starts with a literal "~/",
which the filesystem never expands, and names this exact theme file: that is
what 1781043107.sh wrote, and no working link can look like it. A dangling
target is not enough on its own, since a dotfiles repo may just be unmounted.

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

* Clean up every scratch directory the CLI suite creates

Five of the eight mktemp directories were never registered with the exit
trap, so each run left them behind in /tmp. Route them all through a helper
that records them for cleanup.

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

* Drop the theme symlink migration tests

A migration runs once on each machine and is then inert, but a test for it
sits in the suite forever. Now that the repair behaves correctly, keep the
migration and let it go untested rather than grow the suite permanently.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-11 22:47:07 +02:00
4d0531f351 Add QR code capture (#6705)
Select a screen region and decode the QR code in it to the clipboard, so
an otpauth:// setup code shown on screen no longer needs a phone.

The decoded value is only ever placed on the clipboard, and marked
sensitive so clipboard history skips it. Decoding is restricted to QR so
a stray barcode elsewhere on screen can't take the clipboard instead.

Co-authored-by: Hlib Kanunnikov <hlibwondertan@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 13:56:09 +02:00
186668a70f Apply the Broadcom Wi-Fi quirk to Macs without a T2 (#6652)
* Apply the Broadcom Wi-Fi quirk to Macs without a T2

brcmfmac lets the Wi-Fi firmware run the WPA handshake itself, and on Apple
hardware that offload fails against an access point in WPA2/WPA3 transition
mode: the client associates, the four-way handshake never completes, and
NetworkManager reports the password as wrong. feature_disable=0x82000 turns off
the firmware supplicant and authenticator so wpa_supplicant does the handshake
in software.

That quirk already shipped, but only for Macs with a T2 chip. The bug is in the
Broadcom firmware rather than in the T2 bridge, so it was never the right thing
to gate on: a MacBookPro11,4 has BCM43602 with 2015 firmware, fails exactly this
way, and got nothing. Gate on the hardware that actually has the firmware — an
Apple machine with a Broadcom wireless part — which covers both.

Moving it out of fix-t2.sh also leaves one owner for the file. Two leaves writing
the same config would have meant the later one silently winning, decided by an
ordering in all.sh nobody would think to check.

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

* Gate the Broadcom Wi-Fi quirk on the T2 ID or a brcmfmac chip ID

Sniffing lspci for an Apple vendor with a Broadcom network controller made T2
Macs depend on a detection line they never needed: they carry a T2 PCI ID that
is always there, and the class name half of `lspci -nn` comes from the pci.ids
database. Keep their original gate untouched.

Naming the rest by DMI model does not hold up either, because the model year
does not predict the part. A MacBookPro11,4 from Mid 2015 carries a BCM43602
and needs this; a MacBookAir7,2 from Early 2015 carries a BCM4360 and does
not. Covering the lineup by name takes around twenty identifiers across four
product lines and grows every time Apple ships hardware.

The set has an exact definition already: the PCI IDs brcmfmac binds, from the
driver's own brcm_hw_ids.h. That reaches the 2016 and 2017 MacBook Pros and
the T2-less iMac19,1 and iMac19,2 that a hand-written list missed, and it
leaves out the BCM4360 Macs for free, since their out-of-tree wl driver would
never read a brcmfmac option anyway.

Matching an exact vendor:device ID also drops the piped `grep -q`, which
returns 141 under pipefail once the producer is killed by SIGPIPE (#6608). The
test runs the leaf with pipefail so the chatty lspci stub proves it.

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

* Fix Macs already installed without the Broadcom Wi-Fi quirk

The quirk is written at install time, so a machine set up before it shipped
never gets it, and no pre-T2 Mac ever did. Those installs still fail the WPA
four-way handshake against an access point in WPA2/WPA3 transition mode,
which is the state the reporter had to repair by hand.

Appending leaves anything else in the config alone: modprobe reads every
options line for a module, and nothing else sets feature_disable. Only an
active options line counts as already applied, and the driver keeps the old
behaviour until it reloads, so this asks for a reboot rather than pulling
brcmfmac out from under a connection that currently works.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-10 22:49:44 +02: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
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
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
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
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
81ecac4a60 Fix WhatsApp Slim migration targeting brave-origin-beta instead of brave-origin (#6482)
* Fix WhatsApp Slim migration targeting brave-origin-beta instead of brave-origin

* Add migration to append WhatsApp Slim to Brave Origin for existing installs

* Don't corrupt flags files that lack a trailing newline

Appending --load-extension= with echo assumed the file ended in a newline.
A hand-edited flags file without one got the flag concatenated onto the
previous line, losing both that flag and the extension.

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

---------

Co-authored-by: akashgagda <akashgagda@users.noreply.github.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 10:26:58 +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
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
c3bd4a86ae Fix unbound XKBLAYOUT under set -u in the keyboard-layout migration (#6539)
* Fix unbound XKBLAYOUT under set -u in the keyboard-layout migration

/etc/vconsole.conf only guarantees KEYMAP -- XKBLAYOUT is written by some
installers but not required, and vconsole.conf on a stock install may not
define it at all. omarchy-migrate runs every migration with set -euo
pipefail, so referencing the unset variable directly aborted the migration
run instead of just skipping a layout this migration doesn't care about.

Read it with a default-empty expansion first, then apply the existing
comma-strip separately -- keeps both failure modes (unset, and set with a
trailing keymap variant) handled explicitly instead of folding them into one
expansion that only covers one of the two.

* Survive a missing vconsole.conf in the keyboard-layout migration

Defaulting XKBLAYOUT fixed the unset variable but not the other way this
line takes the migration chain down. `.` fails when /etc/vconsole.conf is
not there at all, `&&` short-circuits, and the non-zero status leaves the
command substitution and kills the assignment under omarchy-migrate's
`bash -euo pipefail` -- the same abort, one branch over. The file is
optional enough that both other readers of it, omarchy_hooks.conf and
1781485962.sh, guard with `-f` first.

Run the echo unconditionally so the substitution reports its status
instead of the source's.

Read both paths from the environment, the way the zram migration already
does, and cover the layout cases plus both crashes with a test.

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

* Read the keyboard layout from vconsole.conf alone

Two ways the layout could come from somewhere other than the file, both
found reviewing the fix before it:

Sourcing does not clear an exported XKBLAYOUT, so on a machine that exports
one, a vconsole.conf that sets no layout -- or none at all -- left the
caller's environment deciding what the initramfs bundles. Unset it in the
subshell so only the file can answer.

Skipping the source when the file is missing, rather than letting the
substitution swallow the failure, also stops depending on errexit being
discarded inside command substitution. inherit_errexit takes that back and
the chain aborts again; nothing in Omarchy sets it today, but the other two
readers of vconsole.conf already check `-f` first and this now matches.

Test the layout list past its one Cyrillic entry, both orders of a
comma-separated pair, an exported XKBLAYOUT, and inherit_errexit. Each of
those catches a mutation that survived before.

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

* Drop the test scaffolding from the keyboard-layout migration

A migration runs once and then gets deleted, so a suite entry for this one
is upkeep with a short shelf life. The path overrides existed only so that
test could aim the migration at fixtures; with the test gone they are
indirection nothing exercises, so both go back to literal paths.

The fixes stay: check the file before sourcing it, unset XKBLAYOUT so an
exported one cannot answer for a file that sets none, and strip the keymap
variant before matching.

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

---------

Co-authored-by: Toni Nowak <t.nowak@ai-flow.no>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:43:45 +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
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
David Heinemeier Hansson ce93c31af6 Merge branch 'quattro' into add-default-agent
# Conflicts:
#	migrations/1785633225.sh
2026-08-04 05:33:49 -07:00
David Heinemeier HanssonandClaude Fable 5 c992cdff10 Restart the shell unconditionally after every update
Updates routinely replace the shell's QML, and a stale process can
lazy-load new files into old code. Restarting at the end of every
omarchy update removes the need for migrations to restart the shell
or defer one with the restart-shell-required marker: the login-time
migration path already runs a fresh shell that hot-reloads shell.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 16:52:02 -05:00
David Heinemeier HanssonandClaude Fable 5 9ca5f63f86 Speed up mouse scrolling in foot
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 21:56:51 -05:00
David Heinemeier Hansson 7b4fb0ad6b Fix default agent setup edge cases 2026-08-01 19:52:04 -07:00
David Heinemeier Hansson 1f8819318d Switch to Omacalc 2026-08-01 19:32:32 -07:00
David Heinemeier Hansson a9b84cea8e Refine default agent menu 2026-08-01 19:11:44 -07:00
David Heinemeier Hansson a0e2501e15 Add configurable default coding agent 2026-08-01 18:49:28 -07:00
David Heinemeier HanssonandClaude Fable 5 84e87ce67e Add oh-my-pi (omp) as a lazy-installed mise tool
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 19:55:02 -05:00
David Heinemeier HanssonandGitHub 2cff67fa45 Fix sleep lock session environment race (#6493)
Found by @tobi.
2026-08-01 15:38:55 -05:00
c0d4037237 Support external monitor brightness in Quattro (#6490)
* Support external monitor brightness

Route brightness through the focused Hyprland monitor so internal panels keep using the kernel backlight while compatible external displays use DDC/CI. Preserve the Apple Display backend and leave brightness unavailable when the focused display cannot be controlled.

Add cached DDC bus and VCP range handling, install ddcutil for new and existing systems, and cover backend selection and brightness conversion with shell tests.

* Harden external brightness caching

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-01 15:35:34 -05:00
David Heinemeier Hansson 66427571bc Revert WhatsApp theme bridge
The native messaging host, extension shim, and lifecycle machinery are too complicated for the value this integration provides.
2026-08-01 12:44:40 -07:00
237405215d Make WhatsApp Web follow your Omarchy light/dark theme (#6484)
* Make WhatsApp Web follow your Omarchy light/dark theme

WhatsApp Web's "System default" theme follows prefers-color-scheme and
repaints live, so a small theme bridge is enough to make it track the active
Omarchy theme with no reload and no WhatsApp-specific CSS.

- omarchy-chromium-theme-host: push-only native messaging host that reads the
  active theme and emits it on connect and on every theme-set. Unlike copy-url/
  yt-dlp (one-shot), it stays connected and pushes, since theme-following needs
  the page to learn about changes while it is running.
- omarchy-chromium-theme-refresh: SIGUSR1s the running host(s); called from
  omarchy-theme-set's post_theme_commands.
- whatsapp-theme extension: decides dark vs. light from the theme background's
  WCAG luminance and drives a prefers-color-scheme shim, so WhatsApp's own
  theme does the repaint.

Wired like copy-url/yt-dlp and whatsapp-slim: bundled under
default/chromium/extensions, added to --load-extension, host manifest
registered from the fresh-install/refresh/browser-install paths, existing users
covered by a migration.

The host is named com.omarchy.theme (a generic theme bridge) rather than
WhatsApp-specific, so other bundled web-app extensions can follow the theme by
connecting to it and adding their id to the host manifest's allowed_origins.

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

* Address review on the WhatsApp theme bridge

Light/dark was decided by weighting raw sRGB bytes, which the comment above
it already described as WCAG relative luminance. sRGB is gamma-encoded, so
the weights only mean anything once each channel is linearized — the two
steps the shell already does in Panel.qml. Every shipped theme classifies the
same either way; a mid-tone custom background does not (#808080 reads 0.502
unlinearized and 0.216 linearized).

Drop the `tabs` permission. The WhatsApp host permission is what lets
tabs.query filter by url and what populates tab urls in onUpdated, so `tabs`
only widened this to every tab's url and title. Tabs without permission
arrive with url unset and fall out on the existing guard.

Give the two new bin commands their metadata directives. Without a summary
they failed test/cli's command metadata check.

Cover all three: the classifier over unambiguous and mid-tone backgrounds,
and the manifest for the permission it should no longer ask for.

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

* Scope color-scheme listeners to their query and test the real host

Registrations all shared one Set keyed only by callback, so an app that gave
the same callback to both the dark and the light query and later detached one
detached the other too, leaving the query it still held deaf to theme
changes. Record the owning MediaQueryList and match on it. Adds native
dedupe behaviour while there: registering the same callback twice fired it
twice. addListener is a legacy alias of addEventListener("change"), so the
two share one registration space and either remover cancels either add —
which is also why useEvent had nothing left to select and is gone.

The refresh test signalled a synthetic sleeper carrying its own USR1 trap, so
it proved the refresh command sends a signal but would have stayed green
through any regression in the host's own trap, watchdog wait, or second
write. Drive the real host over a FIFO instead, count framed messages, and
assert the second one is a usable theme. Verified by neutering the host's
USR1 trap: the old test passed, this one fails.

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

* Harden Chromium theme bridge

* Address Chromium theme bridge review

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-01 13:16:06 -05:00
d47b4b0686 Collapse WhatsApp Web to a Signal-style avatar rail in slim windows (#6473)
* Make menu overflow visible with a half-row peek and edge fades

Menus used to cut off clean at ten rows, so anything below the fold was
undiscoverable. Now the list sizes to what fits within 60% of the screen
and always ends mid-row when items overflow, with scroll-position-driven
fades at both edges. Keyboard navigation keeps the next hidden row peeking
past the cursor so the fold affordance travels with the selection.

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

* Collapse WhatsApp Web to a Signal-style avatar rail in slim windows

Add a WhatsApp Slim Chromium extension that injects CSS into
web.whatsapp.com, collapsing the chat list to a 90px avatar rail with
floating unread badges when the window is narrower than 1100px. Wide
windows keep the stock layout.

Loaded through the existing --load-extension list in chromium-flags.conf,
with a migration to append it for existing users.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 21:42:23 -05:00
a79d1dc8da Add Wi-Fi QR sharing to network panel (#6463)
* Add Wi-Fi QR sharing to network panel

* Refine Wi-Fi QR sharing

* Use QR glyph for Wi-Fi sharing

* Add click-to-reveal password to the Wi-Fi share card

Scanning the QR is the fast path, but the person typing on a laptop needs
the actual password. A dimmed "Show password" hint under the QR toggles
the secret in place.

The password stays out of the shell until asked for: a click runs the new
omarchy-network-password helper (a private pipe, never an argument), and
closing the card drops it again. Open and enterprise networks never show
the control.

The card loses its Close button -- Escape and clicking outside already
cover it -- and now sizes itself to its content.

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

* Keep a dismissed Wi-Fi share card closed and make Escape reliable

Closing the card mid-generation killed the helper, but its buffered
stdout still arrived and repopulated the matrix, reopening the card the
user just closed. Both collectors now honor qrExpectedStop, and the flag
survives onExited because exit and stream-finished have no guaranteed
order. The password fetch gets the same treatment so a reveal in flight
during dismissal can't stash the secret into a closed card's state.

The content's focus was claimed while the window was still unmapped, so
Escape could land nowhere. Re-acquire it after mapping, the way
KeyboardPanel does.

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

* Share WEP networks correctly and restore the QR quiet zone

NetworkManager models WEP as key-mgmt "none" plus a wep-key, so the QR
helper encoded WEP networks as open -- a QR that scans fine and then
silently fails to join. Encode them as T:WEP and let the password helper
print the key.

Also widen qrencode's margin from 2 to the spec's 4-module quiet zone;
the card surround is dark, so that white border is all a scanner gets.

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

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:18:42 -04:00
1ea910f662 Let systemd-oomd kill a runaway app instead of losing the session (#6445)
Nothing stood between "memory is tight" and "processes die at random":
the kernel OOM killer only fires after an allocation has already failed,
so a machine under pressure thrashes in reclaim until something breaks
somewhere fatal — seen in practice as Hyprland taking SIGBUS mid-memcpy
from a client shm pool that memory pressure was tearing down.

Enable systemd-oomd, which keys on PSI stall time rather than free
pages, and mark app.slice — and only app.slice — as its kill candidate.
Hyprland runs in session.slice, so the compositor is structurally
ineligible as a victim: oomd takes the app that caused the pressure and
the session survives. Thresholds (50% stall over 20s, Fedora's desktop
defaults) live in an oomd.conf.d drop-in; candidacy ships as a vendor
drop-in under /usr/lib/systemd/user so existing users get it on package
upgrade with no per-user seeding.

New installs enable the daemon from enable-services.sh; a migration
covers existing ones, restarting an already-running oomd so it doesn't
keep stale thresholds until reboot.

Deliberately not earlyoom: it triggers only when free RAM and free swap
are both under threshold, so Omarchy's large, mostly-idle swapfiles
keep it asleep through exactly this failure.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 15:16:29 -04:00
David Heinemeier HanssonandClaude Opus 5 e39a275f5a Drop Kvantum
Kvantum is a QStyle plugin, so it only ever painted the three QWidget
apps we ship, and it painted them with its own stock look: Omarchy has
never shipped a .kvconfig, so nothing about it tracked the active theme.

Without it Qt falls back to built-in Fusion while the palette still comes
from QT_QPA_PLATFORMTHEME=gtk3, so everything stays dark and now follows
the theme instead of ignoring it. Measured across a full 1920x1080 frame,
dropping it moves obs-studio by 132 pixels -- it overrides the platform
style with its own stylesheet anyway -- kdenlive by 0.8%, and the
Hyprland share picker by 4.7%. Flatter buttons, classic tabs, a
conventional checkbox tick.

Existing machines get a migration, which the Qt5 stack never did. The
difference is that this removal cannot reach the greeter. kvantum-qt5
goes in the same transaction, since it is the only thing that requires
kvantum, and it takes qt5-svg and qt5-x11extras along with it -- but
qt5-wayland is explicitly installed, so the cascade stops there and
leaves qt5-declarative and qt5-base standing for any sddm theme still
starting the Qt5 greeter.

kvantummanager stays in launcher.hides. The entry costs nothing once the
package is gone, and it keeps Kvantum Manager out of the launcher on
machines that have not run the migration yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 12:27:38 -07:00
David Heinemeier HanssonandClaude Opus 5 39936d2982 Ship the model usage widget in the default bar
The widget hid itself well enough that nobody found it: it only ever
appeared if you knew the id and ran `omarchy bar plugin add`, since it was
in neither the shipped layout nor any migration.

Defaulting it on costs nothing on machines that don't use it. Panel.qml is
`visible: providers.length > 0`, and Main.qml only counts a provider that
is enabled and has actually recorded prompts, sessions, active days, or a
rate limit. A box that has never run Claude Code or Codex draws an empty
bar item, and the icon arrives on its own at the first scan that finds
usage — which is the behavior the widget already advertised.

The migration skips any config that already lists the widget in any
section, so a curated bar keeps its own placement rather than gaining a
second copy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 14:49:55 -04:00
Ryan Hughes 38c1352f6d Fix t2 module 2026-07-28 21:07:50 -04:00
David Heinemeier Hansson bdcdfeb428 This tmux alert system didn't work as nicely as I imagined 2026-07-27 15:32:56 -07:00
David Heinemeier HanssonandClaude Opus 5 6e07fd0e82 Supervise fcitx5 so XCompose sequences can't silently die
fcitx5 is what turns the CapsLock compose sequences in ~/.XCompose into
text for Wayland clients -- CapsLock m s for an emoji, CapsLock space n
for your name. It was launched fire-and-forget from Hyprland's autostart
via uwsm-app, so nothing supervised it, nothing restarted it, and it
logged nowhere. When it went away, every compose sequence stopped
working for the rest of the session with no visible symptom beyond
"emoji input is busted", and no record of why.

That is not hypothetical: it was found dead on a running machine with no
coredump, no OOM kill, and nothing in the journal to explain it.

Move it to a systemd user service:

- Restart=always, not on-failure. fcitx5 exits 0 when it finds another
  instance already owning org.fcitx.Fcitx5, and a clean exit still
  leaves the user with no input method.
- After=/PartOf=graphical-session.target. It needs WAYLAND_DISPLAY and
  DISPLAY, which uwsm imports into the user manager before reaching the
  target, and its wayland connection dies with the compositor.
- ConditionEnvironment=WAYLAND_DISPLAY. After= is ordering only and does
  not stop the unit from being started while the target is inactive. An
  update over SSH has a live user manager (pam_systemd) and no graphical
  session, and a fcitx5 started there comes up blind -- then stays
  active, so the later target activation won't pull in a working one,
  because Wants= does not restart what is already running. Skipping the
  start leaves the unit enabled and healthy for the next graphical login.

The migration hands over inside a live session only: it enables without
--now, and only when graphical-session.target is active does it drop the
autostart-launched process and start the unit. Because that kills a
fcitx5 that was working a moment ago, a failed start is reported instead
of leaving the session mute with the migration marked complete.

omarchy-restart-xcompose now drives the unit. It still clears any fcitx5
running outside it first: that process owns the bus name, so the unit's
instance would exit on arrival and the stale one would keep serving the
old table -- a restart that reports success and changes nothing.

Side benefit: fcitx5 now logs to the journal under its own unit, so the
next disappearance leaves a record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 09:49:15 -07:00
David Heinemeier HanssonandClaude Opus 5 4d93ad588f Register the Chromium native messaging hosts on fresh installs
Chromium ships in the base packages, so it never goes through
omarchy-install-browser, and a first install stamps every migration as
already applied without running it. That left both bundled extensions
loaded from chromium-flags.conf with no host to talk to: Copy URL did
nothing at all, since the clipboard write and the toast both live in the
host.

The user install now registers both hosts itself, and a migration
repairs the installs that already missed out — Copy URL since 1784763917
landed, Download Video since 1780517689, unless installing a browser or
refreshing Chromium happened to fix them along the way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VeAd2iERgi7bRSxq2gA1hp
2026-07-27 09:02:25 -07:00
Ryan Hughes 88ef6ca597 Unnecessary
Sets reboot, changes take effect on reboot anyway
2026-07-27 09:49:13 -04:00
David Heinemeier HanssonandClaude Opus 5 751165e201 Don't restart a zram device that exists but is swapped off
The resize guard reads the Used column for /dev/zram0 out of /proc/swaps
and treats a missing row as an empty device, which is right for a device
that doesn't exist yet: the restart is what brings it up against the
config daemon-reload just generated. A device that exists and is merely
swapped off reads the same, and there the restart resets it first, which
returns EBUSY for as long as anything still holds it open. That leaves a
bare "Job failed. See 'journalctl -xe' for details." in the migration
output and falls through to asking for the reboot that would have
resized it anyway.

Tell the two apart by whether /sys/block/zram0/disksize is there at all.

The test modelled an absent device as a blank disksize file, which no
longer stands in for one, so it removes the file instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:59:37 -07:00
David Heinemeier HanssonandGitHub 90f6525d63 Merge pull request #6354 from yuters/fix-snapshot-locate-and-timeline-leak
Fix locate index on Btrfs: skip snapshots, index /home, drain leaked timeline snapshots
2026-07-26 19:54:39 -07:00
David Heinemeier HanssonandClaude Opus 5 9b9d4b39eb Never leave updatedb.conf with two definitions of the same setting
updatedb refuses to run at all on a config that defines a variable twice
("variable `PRUNEPATHS' was already defined"), so any rewrite that misses
an existing line and appends a second one takes the locate index down
rather than fixing it. Two shapes updatedb accepts got missed: a trailing
comment after the value, and a setting indented by whitespace.

Read the existing paths out of the quoted value and write the whole
setting back canonically instead of splicing into a line of unknown
shape. Quotes are not optional to updatedb ("value in quotes expected
after `='"), so a bare value is already a broken config: rewriting it
quoted repairs the file as a side effect.

The tests now hand every rewritten file to the real parser through
`updatedb --config-file`, which is what caught this.

Read the Snapper config as root when the running user cannot read it.
snapper create-config leaves the config root-only, and a config the user
cannot read was passing for one that wants its timeline snapshots kept.

Report the snapshots the drain could not delete. omarchy-migrate writes
the completion marker whether or not the batches succeeded, so there is
no later run to pick up the remainder, whatever the comment claimed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:19:33 -07:00
David Heinemeier Hansson f22e8cd353 Merge remote-tracking branch 'origin/quattro' into fix-snapshot-locate-and-timeline-leak
# Conflicts:
#	docs/file-layout.md
2026-07-26 19:14:09 -07:00