* perf(agents): cut codex usage collector memory with SQL filter and cache
The codex collector scanned every row of opencode.db (1.7 GB, 55k+ rows)
with Python-side json.loads, peaking around 716 MB of RSS on every run
-- including the panel's refreshLimits() call, which passed --limits-only
that the collector silently ignored.
Filter rows in SQL (LIKE gates + json_valid + json_extract authority,
mirroring the old Python filter semantics) so giant blobs are never
parsed, and cache the local stats scan in XDG_CACHE_HOME following the
claude collector's pattern (atomic writes, flock, schemaVersion).
--force rescans, --limits-only and normal mode reuse a fresh cache and
fall back to a full scan when it is missing, stale, or corrupt.
Measured: cold scan 716 MB -> 158 MB peak; warm --limits-only ~85 MB
and ~1.4 s. Output record schema and values are unchanged for the same
data (parity verified against the old filter, including malformed rows).
* Scope the codex scan cache's 15-minute reuse to --limits-only
A no-flag run is the widget's periodic refresh, and refreshIntervalSec is
configurable down to 30 seconds; holding every mode to a 15-minute cache
meant stats could lag far behind the interval the user asked for. Mirror
the claude collector: normal runs reuse a scan for ~20 seconds purely to
dedup concurrent collectors, and only --limits-only, which promises just
fresh limits, may reuse a scan for up to 15 minutes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Invalidate the codex scan cache across day boundaries
The cached stats embed date-dependent fields (todayPrompts,
todayTotalTokens, recentDays), but only the file's age was checked, so a
cache written at 23:58 served yesterday's numbers as "today" for up to
15 minutes past midnight. Stamp the envelope with the scan's local date
and treat any other date as a miss.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Reject codex scan caches with a future mtime
A cache whose mtime is ahead of the clock has a negative age, which the
freshness check accepted forever: setting the clock backwards froze the
stats until real time caught up with the file. Require a non-negative
age before trusting the cache.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Never cache an interrupted opencode scan
A transient lock, schema migration, or corrupted database aborts the
opencode scan mid-flight; the partial numbers still serve the current
run, but persisting them let a single bad read suppress opencode usage
for every cache reader until expiry. The claude collector already skips
its opencode cache write on a database error; do the same here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Make the json_valid guard order explicit in the opencode query
The query relied on json_valid(data) evaluating before json_extract(),
but SQLite does not promise that AND terms run left to right; a
reordered plan would let json_extract raise on a malformed row and
silently truncate the scan. Wrap each json_extract in a CASE so the
guard is structural rather than positional.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop a claude-collector comment that is false for codex
"These caches were world-readable before" was copied from the claude
collector; codex had no caches before this one existed. Explain the
chmod on its own terms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: markbusking <marcosbustos.dev@gmail.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
omarchy-hw-hybrid-gpu gates the Hybrid GPU menu entry, and it queried
supergfxctl unbounded — a wedged supergfxd stalled menu rendering
forever. Bound the query with the same TERM-then-KILL escalation the
toggle uses, and treat a daemon that cannot answer like a machine
without supergfxctl: fall back to counting GPUs rather than hiding
hardware that is really there. An ordinary supergfxctl failure still
hides the entry.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Only offer video capture devices as webcams
V4L2 exposes raw processing and output-only nodes beside usable cameras. The webcam picker and automatic recorder selection treated the first /dev/video node as a camera, so IPU6 laptops opened a black overlay instead of their loopback capture device.\n\nShare one device lister across detection, selection, and recording, and keep only groups whose first video node advertises Video Capture in Device Caps. Cover raw IPU nodes, ordinary capture devices, and capture-less systems.
* Fall through to a later capture-capable node in a webcam group
A group whose first video node is not capture-capable vanished entirely,
even when a later node in the same group could capture. Probe each node
until one qualifies, still emitting at most one device per group. Also
exit zero explicitly: a trailing filtered device used to leak the failed
capability check as the script's exit status.
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>
* Bound hybrid GPU mode queries
* Test blocked hybrid GPU queries
* Give the blocked-client test headroom over its 12s of kill cycles
The third case spends ~12s of real TERM/KILL escalation against its own
15s watchdog, which can tip to a spurious 124 on a loaded machine. Also
drop the TEST_LOG plumbing no stub ever wrote.
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>
The interval floor was applied with max(), so the zero that --force
picked could never win: max(0, 15) is 15. Forcing a refresh within
fifteen seconds of the last probe silently served the cache instead,
though --force documents itself as ignoring them.
The window exists to absorb a panel opened and shut repeatedly, which
arrives as --limits-only. --force is a person pressing refresh, and it
should outrank a window meant for flicks.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Stop closed network panels from leaving Wi-Fi scanning enabled
refresh() defaults scanWifi to false and its no-scan branch enabled the
scanner unconditionally. Five paths reach it with no panel on screen —
Component.onCompleted, clearNetworkAction(), failNetworkAction(), the
band-change actionProc exit, and the 30s actionTimeout — so the scanner
stayed on and Quickshell kept re-arming RequestScan behind a closed panel.
scanRestart had the mirror gap: it enabled the scanner 100ms after
refresh(true) without re-checking that the panel was still open.
Every sweep takes the radio off the operating channel, so this degraded
the link it was scanning from: one sweep every 17s, gateway RTT rising
from ~2ms to repeated 150ms+ spikes on an otherwise idle connection.
Gate the scanner block on the panel being open, cancel a pending restart
on close and re-check the panel when it fires, and track the WifiDevice
this instance enabled so close, device replacement and destruction
release the right object. Destruction matters on its own: a bar reload
with the panel open would otherwise die with opened still true and never
write scannerEnabled = false.
* Cover the scanner ownership helper's own invariants
The previous assertion only pinned that no write bypasses
setScannerEnabled(); it said nothing about what the helper does. Dropping
either the opened gate or the release-before-adopt from the helper still
passed, while a closed instance could reclaim scanning and a device swap
could leave the previous interface scanning.
Run the helper's actual JavaScript against stand-in devices instead,
following the extract-and-eval pattern the agents panel tests already use.
Removing either invariant now fails its own assertion.
* Measure the About layout in UTF-8 so its window is not fitted too narrow
wc -L only counts display columns in a UTF-8 locale. A session that never
set one leaves it counting the box-drawing and Nerd Font glyphs the About
layout is built from as nothing, which measured the content 21 columns
narrower than it renders and fitted the window to clip it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Open the About window at the size it last settled on
The window used to map at the float rule's starting size, paint, and only
then measure itself and resize, so every open flashed one window size and
reflowed into another. The size that hugs the content can only be measured
from inside the terminal, so remember it and apply it as a window rule
before the terminal is spawned: the window now maps at its final size and
never moves. A rebranded logo or a new font falls back to the float rule
for one launch, refits, and is remembered from then on.
The fit itself now moves the window by the cells it is off by, rather than
scaling it to the grid, which multiplied up the terminal's padding along
with them and left the fit a column or two short. It accepts a cell of
slack instead of chasing an exact grid, since a window lands where the
terminal's cell boundaries put it, not where it was asked to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Only the Claude Code CLI can refresh the OAuth token it saves; the
collector just reads it. A machine left alone long enough finds the
token lapsed, and that branch returned an empty limits list with no
status text at all, so the panel hid its whole limits section and
explained nothing.
Say what is wrong, and fall back to the cached limits already on disk
rather than discarding them. Cached windows are kept only until they
reset: a percentage from a window that has rolled over describes a
period that is over, and pinning a stale 78% on an allowance that is
now untouched would be worse than showing nothing. The probe-failure
path gets the same filtering for the same reason.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* 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>
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>
The discovery retry timer turned adapter.discovering on every second
while the panel was open, and nothing ever turned it off. The BlueZ
discovery session behind it is held by quickshell's D-Bus connection,
so one visit to the panel left the radio in inquiry until the next
shell restart — continuously starving A2DP audio on the same controller
into stuttering, and 'bluetoothctl show' kept reporting
'Discovering: yes' long after the panel was gone.
The panel now tracks the StopDiscovery it owes BlueZ and settles it
once closed. A timer bound to the confirmed discovery state does the
stopping, rather than a write in the close handler: quickshell only
forwards a discovering write that differs from the last state BlueZ
reported, so a stop issued while a just-fired StartDiscovery is still
awaiting confirmation would be swallowed and leak the session. Binding
to adapter.discovering re-arms the stop whenever the confirmation
lands, a reopen inside the first interval keeps the scan running
uninterrupted, and attempts are bounded so a session another BlueZ
client holds up cannot draw StopDiscovery calls forever.
One widget instance exists per monitor and they all share the default
adapter — the same shared-backend shape the network panel's wifi
scanner fix (#6772) dealt with — so the debt follows the session: an
instance opening onto a running scan adopts it, a closing instance
hands it to a panel still open on another monitor (the popout handoff
closes one instance as it opens the next), and a destroyed instance
passes it to a surviving sibling.
Fixes#6789
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Recover image picker after interrupted thumbnails
* Use arithmetic assertions in image cache tests
* Bound thumbnail lock waits and reap partial thumbnails
A hung generator (vips stuck on a corrupt file or slow mount) held its
flock forever, wedging every later picker open; the directory-lock era
capped that wait at 30 seconds, so keep the same bound. A generator
killed mid-write also stranded its partial .jpg.<pid>.jpg forever, since
nothing prunes the cache directory; only the lock holder writes those,
so reap them right after taking the lock.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Harden thumbnail locks and cache publication against races
Adversarial review caught three holes. The lock fd leaked into
vipsthumbnail, so an orphaned or hung vips kept holding the lock after
its shell died; close it for the child. Reaping legacy lock directories
unconditionally raced a still-running legacy generator through an
upgrade; only reap ones older than the longest plausible generation.
And cache publication was neither atomic nor exclusive, so a picker
killed mid-write, or two interleaving, could leave truncated or
mismatched rows behind signatures that still validated - the same
permanent hiding this branch set out to fix; publish via renames under
a per-key lock, rows first.
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>
* fix `--help` being skipped for bin routes that only partially resolve to a file
* Add regression tests for --help on partially resolved routes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Stop help-flag scanning at -- and match --json as an exact token
A `--` marks the rest of the args as belonging to the command itself, so
neither --help nor --json past it should be treated as router flags. The
--json check also matched substrings of flattened args, so a --json inside
one quoted argument switched --help output to JSON.
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>
The floating rule pinned every agent terminal to 1200x800, which
overflows small and scaled displays: window rules see logical pixels, so
a 2560x1440 monitor at scale 1.6 is only 1600x900 and the window covered
89% of its height. Tiling drops the fixed size along with the rule, and
the shared app-id still earns the terminal tag from terminals.lua.
* 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.
* 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.
Standing in for an abandoned compositor means binding a Unix socket, and the
sandboxes this guard exists for are the ones that deny it: the fixture raised
PermissionError and took the whole file down with set -e, adding a failure in
the environment the guard was written to keep clean. Run the cases that need no
socket first and skip the rest when one cannot be bound.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Read the keyboard being typed on rather than the one holding main
The main flag names no keyboard for long. fcitx5 takes it with the
virtual keyboard it binds to inject, and those are filtered out, so on a
seat running an input method the pick lands on nothing at all: no label,
and the widget hides itself off the bar. #6727 keeps polling in that
state rather than settling it, and the poll has nothing new to read.
Once fcitx5 unbinds, the flag lands on whichever device libinput listed
last, as easily a lid switch as a keyboard, and a device that never
receives the toggle reports the layout it started on forever, which is
the reading #6574 opened.
Every device carries the seat's layout list, but only the keyboard being
typed on advances through it, so read the furthest-advanced one.
activelayout names the keyboard it moved ahead of the layout, so take
that name and let it settle the pick, and the click that switches it.
* Leave the buttons out of the seat the widget reads
Reading the keyboard being typed on left keyboardName standing for two
things at once: the device a click switches, and the device activelayout
last named. Only the second was still being set, so the first went empty
until a switch happened -- which left the click doing nothing on a seat
whose only switch is the click, and left the poll running forever on the
one-keyboard install it was written to leave alone. Give each its own
property, and set the switch target from the reading that confirmed the
keyboard is there.
Layout progress only points at the keyboard being typed on while the
other devices stay where they started, and the ACPI power button, lid
switch and sleep key never do move on their own -- but they answer to
switchxkblayout and can hold the main flag, so anything that reads or
switches whatever the seat hands back can end up describing a button, and
unplugging the keyboard beside one leaves it standing in for the seat.
Drop them where the virtual keyboards are already dropped.
A reading that reaches hyprctl and finds no keyboard now clears the label
rather than leaving a device that is gone described on the bar, told
apart from the empty output a killed query leaves by the device list
itself, and the watchdog asks again rather than waiting for a poll that a
settled seat has already stopped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Skip shell tests when the compositor can't be reached, not just when WAYLAND_DISPLAY is unset
A set variable only proves the environment was inherited. Sandboxes pass it
through while blocking $XDG_RUNTIME_DIR, so Quickshell cleared the guard and
aborted inside QGuiApplication, leaving two core dumps per launch instead of a
clean skip. Probe the socket and, when there's a signature to ask with,
Hyprland itself. Disable core dumps on the way through for the compositor that
dies mid-run, which no probe can catch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Retry the compositor query before calling it dead
Hyprland can miss a query while it reconfigures outputs, and one miss was enough
to skip a whole file's runtime coverage. Retry the way omarchy-launch-shell
does. Only a leftover socket reaches the query at all, so the ordinary skip
still returns immediately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Quickshell watches the QML it loaded and reloads on change, so pacman
replacing /usr/share/omarchy/shell mid-transaction makes the running
shell reload against a half-written tree. That reload fails, and a
failure that reaches the config load is not harmless: it raises the
reload popup, which is a second engine generation.
EngineGeneration::currentGeneration() returns null unless exactly one
exists, so the IPC kill that omarchy-update sends moments later takes
the QCoreApplication::exit(0) branch instead of the generation's own
quit, and Quickshell tears the QML graph down after deleting the
QGuiApplication. The first GUI resource touched on the way out aborts:
FATAL: QPixmap: Must construct a QGuiApplication before a QPixmap
The user gets the crash dialog after an update and a coredump per
occurrence. Reported in #6748 with 3 crashes across 10 updates, always
following a failed reload.
Fixing this in omarchy-update — stopping the shell around the pacman
step — would cover one caller and cost the polkit agent and the
notification server for the length of the transaction, which the
migrations that run next still notify through. It would also have to
carry omarchy-restart-shell's refusal to restart a locked session, or
reintroduce the hazard that refusal exists for.
And omarchy-update is not the only thing that rewrites the tree. The
pacman guard turns away a bare pacman -Syu, but nothing turns away a
targeted pacman -S omarchy, a pacman -U of a locally built package, the
documented OMARCHY_ALLOW_DIRECT_PACMAN bypass, omarchy-dev-pkg-test, or
a checkout in a dev-linked tree.
Turn the watcher off instead. Omarchy has never reloaded through it:
omarchy-restart-shell is what picks up QML changes, and config and
plugin changes go through the shell's own IPC. Third-party plugin hot
reload is PluginRegistry's own inotifywait and FileView watches its own
files, neither of which this touches — QuickshellSettings::watchFiles()
gates the config scanner and nothing else. The popup goes off with it,
because QML can still ask for a reload directly and leave the same
extra generation behind.
Environment reaches Quickshell only at launch, so the update that
delivers this still runs under a watching shell. It takes effect from
the next one.
Verified against an isolated instance: breaking a config in place and
then sending the IPC kill reproduces the FATAL, and it stops with either
variable set. QS_DISABLE_FILE_WATCHER also keeps the failed reload from
happening at all.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drops the ChatGPT web app from the default set at the same time, so installing
the openai-codex-desktop package can't leave two identical-looking ChatGPT
entries in the launcher. Super + Shift + A still opens the web version, which
is the only place it was really used.
The bundled ChatGPT icon stays: the package's own chatgpt.desktop asks for
Icon=chatgpt and ships no hicolor icon of its own.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The vendor-ID guess rejects any device with a driver bound, on the
reasoning that libfprint drives readers from userspace so a real one sits
there unbound. But libusb claims interfaces through a synthetic usbfs
driver, so the reader binds one for as long as fprintd holds the claim —
which is exactly while it is being enrolled or verified against.
Readers that name themselves take the product-string branch and never
reach this, so the exposure is the ones that don't: Goodix 27c6:6594
reports "Goodix USB2.0 MISC", matches on vendor ID alone, and drops out
of detection mid-authentication. The menu entry disappears and the
first-run invitation stops firing while the reader is in use.
Ignore a driver link that resolves to usbfs, and keep rejecting the real
ones — usbio-bridge, usbhid, uvcvideo — including on a device that has
both.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Restore FPC fingerprint detection
* Anchor the FPC product match to a prefix
*fpc* is an unanchored three-letter token on the one branch that is
trusted outright, with none of the kernel-driver checking the vendor
guess gets. Every FPC reader on record leads with it — "FPC Sensor
Controller", "FPC Sensor Controller L:0002 FW:25.26.23.14", and this
branch's "FPC L:0000 FW:1425046" — so requiring the prefix costs no
coverage while keeping three letters from matching mid-string, where
FPC abbreviates unrelated things like flexible printed circuit.
Also restore the note about why Elan is kept out of the vendor list, so
the two signatures read as the same deliberate exception.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Write no product descriptor for a two-field device spec
${remainder#*:} returns the string unchanged when there is no second
colon, so a spec meant to describe a device with no product descriptor
wrote the product id out as its product string instead. Every call site
happened to pass a trailing colon, so the suite was right by accident.
Guard the split and drop the trailing colons, so the two vendor-match
cases exercise the path they were written for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Bind a named driver in the kernel-driver fixture
Touching a bare `driver` file asserts that any driver at all disqualifies
a vendor guess, which is more than the detector should promise: libusb
claims an interface through a synthetic `usbfs` driver, so a reader in
active use looks bound by that rule.
Link the interface at a named driver directory instead, the way sysfs
does. The case still covers what it was written for — a Synaptics bridge
or a camera on a fingerprint vendor ID — without fixing the shape of the
answer for drivers it was never about.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The widget polled hyprctl every 10 seconds per monitor, including on the single-layout install where it never shows. Keep the poll only where its answer can change - a seat with more than one keyboard, where Hyprland moves the main flag with no event to announce it - and stop it entirely once a one-keyboard seat has been read.
Also coalesce a refresh that arrives mid-query instead of dropping it, time out a query that never returns rather than letting it hold the guard shut for good, and re-read the layout on configreloaded.
Co-Authored-By: markbus-ai <markbus-ai@users.noreply.github.com>
* Replay the history a dismissal or a clear was still being written into
The popup files a replay reads are written by a serialized queue of shell
jobs, and the read ran as its own process alongside it. A dismissal issued a
moment earlier could still be queued when the directory was read, leaving the
notification out of the replay it was the newest entry of, and a clear issued
a moment earlier could still be queued too, replaying entries it was about to
remove.
The read now waits for the queue to go idle, so the replay shows the history
as of the moment it was asked for rather than whichever jobs happened to have
landed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Catch up on an update that arrived before its popup had a row
Watching a notification for in-place updates starts the moment it is handed
over, but the row those updates write to is inserted a tick later, deferred to
keep a mid-incubation Repeater from being mutated underneath. A client fast
enough to update inside that window found no row to write to, and a property
that has already changed does not change again — so the toast and its file sat
on the superseded content until something else moved.
The row is now refreshed from the live notification once it exists. That reads
the same object the signals would have, so an update that beat the insert is
picked up and one that did not costs nothing: a refresh whose content matches
the row it would write is dropped, which also collapses the several signals a
single multi-property update emits into one rewrite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Hold queued file work behind the replay's read, not just ahead of it
The read waited for everything queued before it, but nothing stopped the queue
from running on while it worked. A clear or an archive issued during the read
could delete or move files out from under awk mid-glob, so a replay could still
show a partial history — some of what a clear was in the middle of emptying.
The read is a barrier in both directions now: the queue holds until it exits,
and it releases on exit rather than on output, so a read that comes back empty
or fails cannot park the queue behind it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Queue the replay's read instead of waiting for the queue to empty
Waiting for the queue to go idle before starting the read still let work
overtake it. A clear or an archive enqueued after the replay was asked for,
while the current job was running, was dequeued the moment that job exited —
the read only starts once nothing is left — so the replay showed the state
after those jobs, which is the race this was meant to close. Unbroken file
traffic could postpone the read indefinitely for the same reason.
The read is now an entry in that queue rather than a process running beside
it. It takes its place in line behind the work queued before the request and
ahead of everything queued after, so no later job can overtake it and no
amount of traffic can push it back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pacman cache grows without bound across updates, and nothing in the
update flow ever reclaimed it. On a machine that has been updating for a
while it reaches several gigabytes of superseded versions that nothing
will ever install again.
Prune it with paccache -rk2 as the first step of an update. Both halves
of that placement are load-bearing.
Keeping two versions rather than one preserves the rollback path. The
cache is Arch's only offline downgrade: when an update breaks a single
package, reinstalling its predecessor from here is the surgical fix,
where a snapshot rollback would revert every other package too. Pruning
before the packages update means the installed version is still the
newest cached, so it survives along with a spare. Retention is by
version order and never consults what is installed, so that holds while
the installed version is among the two newest cached; a deliberate
downgrade or repeated failed transactions can stack newer archives on
top of it.
Running before the snapshot is what actually frees the space. The cache
sits on the snapshotted root subvolume, so a prune taken afterwards
leaves the fresh snapshot holding those extents and reclaims nothing
until it ages out of the number cleanup.
A failed prune warns and continues. Cache housekeeping should not trip
the update's ERR trap and tell the user their update went wrong.
This runs after omarchy-update-requires-free-space, so it reclaims space
during healthy updates but does not rescue a machine already under the
10 GiB gate.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A client that updates a notification through replaces_id does not produce a
second onNotification: Quickshell writes the new content onto the Notification
object the shell is already holding. The card draws a snapshot copied out of
that object — deliberately, since a live QObject in a ListModel role becomes a
dangling pointer the moment the server destroys it — so the toast kept showing
the superseded text, and archived it to history when it left the screen. A
Slack thread that updates in place read as stuck.
Every property the card draws is now watched on the notification we hold, and
a change rewrites both the model row and the file the popup was persisted
under. The file name is that popup's identity, so the rewrite lands in place:
a shell restart restores the version last shown, and so does the copy that
reaches history.
The countdown starts over when the content changes. New text arriving a second
before the toast was due to expire deserves a full look, not the remainder of
the clock the text it replaced had nearly run through.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Every supported agent spells it differently, so map each one to its own
bypass flag instead of leaving the launcher at each agent's default.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
The stub polled for the lock with its own flock, competing with the
holder it had just started. The holder took the lock non-blockingly and
never retried, so a lost race killed it and left the lock free. The
notifier then saw no update in progress and sent the toast the test
asserts it withholds. Wait on the holder's own signal instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sleep inhibitor deliberately outlives the start that spawns it, but
script tears its pty down as soon as the command returns, and the SIGHUP
that follows could kill the inhibitor before it managed to exec. The
sudo stub then never logged and the test failed about half the time.
Hold the session open from inside until the inhibitor has started.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
omarchy-monitor-state stopped shelling out to
omarchy-hyprland-monitor-focused when it started deriving the focused
name from its own hyprctl snapshot, so the stub the test installed was
never called and the assertion could never pass. Expect the focused
monitor from the fixture instead, and drop the dead stub.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* 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>
* 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>
Every other pointer at the community lives in an error path or a doc, so
there was no way to reach it from the menu. Prefer the Discord app when
it is installed, and fall back to the invite in a browser when it is not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The row list was capped at 60% of screen height so a card could never read
as a page. On a laptop-height display that folds the starting menu one row
early, hiding About behind the peek for no gain.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Herdr ships its own annotated keybindings menu, same as Tmux, but nothing
in the menu pointed at it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>