omarchy-mise-install gained --quiet on its `mise use -g` line so a wrapper
no longer prints mise's "tools: pkg@version" ahead of the tool's own
output. That only changes wrappers written from then on, and the migration
that installed the current ones is already marked complete, so every
wrapper already on disk keeps polluting stdout: `claude --version` still
answers with two lines, and a wrapper for a protocol-speaking command
answers with a line its caller cannot parse.
Rewrite them through omarchy-mise-install so the template stays in one
place. Wrappers were written by four different generated forms over time
and only the ones a later migration happened to regenerate moved forward,
so all four are recognized. The whole file has to match one of them
exactly, which leaves a wrapper someone has added a line to alone and
makes a second run a no-op.
Claude-Session: https://claude.ai/code/session_01Mv1FyKG2VGZRdtknQ7YjU8
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Use (( )) for the numeric argument test
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop the quotes on a variable inside [[ ]]
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Use omarchy-pkg-drop instead of raw pacman -Rns
omarchy-pkg-drop already filters to installed packages, so the
2>/dev/null || true suppression is no longer needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop defensive checks around default-set commands
ttfx, imagemagick, and networkmanager are all in the default package
set, so their commands are runtime invariants and should be invoked
directly. Removing the nmcli guard also removes the degraded wifi
fallthrough that only ran when nmcli was missing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
grant_covers re-implemented etc/sudoers.d/omarchy-dns in bash -- one of
the three providers, and %wheel -- but never asked whether the rule was
installed. It ships in the etc/ tree that omarchy-settings copies, so
every machine still on an older settings package answers yes to a grant
it does not have. require_root then execs into sudo with no way back,
and the panel's one-click toggle dies on a password prompt it has no
terminal to show.
Ask sudo instead. `sudo -l` alone reports whether a command is
permitted, which the blanket %wheel rule answers yes to for everything,
but the long listing prints the matched entry's tags -- !authenticate is
the grant and nothing else. It runs nothing, and under -n it prompts for
nothing, so a machine without the rule falls through to polkit and gets
a prompt on screen.
The provider list and the wheel check go away with it; sudo owns that
policy now, and it stays right if the rule is ever edited or removed.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Print the OEM Windows product key from firmware
Machines that shipped with Windows keep the OEM key in the ACPI MSDM
table. `omarchy windows license key` reads it with strings, then cat.
* Rename the firmware key command to omarchy-windows-key
* Launch claude and codex agents with auto-review instead of full bypass
Claude's auto permission mode and codex's --approve-for-me both run
unattended without prompting, but keep automatic review (and codex's
workspace-write sandbox) instead of skipping approval entirely. Grok
stays on bypassPermissions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Match the bash aliases to the agent launcher's auto-review modes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The dial already grouped its digits for the locale above 10, but below 10 it
went through toFixed, which hardcodes a dot. A German desktop therefore read
9.5 and 1.235 off the same dial, switching decimal convention halfway up the
scale.
Send both branches through the locale. A reading is a measurement rather than
interface text, so its separators follow the system's number conventions even
though the interface itself stays English.
English output is unchanged.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The calendar grid's header row and the week-start toggle label were the only
text in the shell that followed the system locale, so a German desktop drew
MO DI MI over an interface that is English everywhere else. Nothing chose that;
they were the only two places reading day names off Qt.locale().
Take them from en_US instead. Where the week starts still follows the locale:
that is a regional convention rather than a translation, and it stays
overridable through weekStartDay.
Dropping the trailing-period strip with it, since that existed only for the
locales this no longer renders.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
omarchy-plugin-clone only takes the source id as the first argument, but the
menu passed --edit ahead of it, so the id fell through to the unknown-option
branch and every clone from Setup > Plugins failed.
Closes#6913
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Bar widgets propagate their composed press-and-hold down to the center gesture
area without handing over the grab, so the gesture area started a bar move and
then received neither a release nor a cancel to end it. The move ghost stayed on
screen for the rest of the session. Ignore the gesture unless we hold the press.
Closes#6881
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
mise use -g announces the resolved tool on stdout, so every wrapped command
prepended a "tools:" line to its own output. That corrupts anything speaking a
protocol over stdout, such as codex app-server. Pass --quiet, which keeps errors
on stderr and preserves the exit status.
The obsolete-wrapper check in the agent migration matched the generated command
verbatim, so loosen it to match the package instead of the flags.
Closes#6908
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A 3.8.3 migration appended the section header with one backslash too many, so
sed wrote the literal characters instead of a newline plus the header. foot
rejects the line and stops reading the rest of the file. The later text-binding
migration matches the header with grep -qxF, misses the broken line, and appends
a second section, leaving the config broken across the Quattro upgrade.
Closes#6903
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A whitelisted bare git repo over $HOME driven only by constrained
omarchy dots commands: local snapshot history at batch boundaries plus
squash-published state sync across machines. Design survived adversarial
review; rejected alternatives and rationale recorded in the plan.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every reference doc was audited claim-by-claim against the code.
file-layout and omarchy-shell were the most decayed (renamed commands,
the etc/ overrides source split, dead IPC entry points and example keys);
update-process lagged the recent pipeline changes and gains a channels
section; theming and audio-tuning were accurate but thin around their
lifecycles.
New reference docs for the subsystems that had none: the menu system,
the CLI router, the notification daemon, and the non-acceptance test
architecture. AGENTS.md links the two of those agents will need most.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Providers never returned JSON rows: they are shell-defined row sources
emitting tab-delimited lines, and extensions cannot declare new names.
bar.shellQuote moved to Util.qml, and the UpperCamelCase widget id
migration no longer exists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task procedure lives in agents/skills/ (migrations.md moves there),
system-shape reference in docs/ (AUDIO-TUNING.md renamed to match),
end-user documentation in manual/. AGENTS.md now states the split.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hyprland reaps its own children, so os.execute() gets ECHILD from waitpid and
never sees an exit status. Every call reported failure, which meant the NVIDIA
env detection in hypr/nvidia.lua never set NVD_BACKEND, LIBVA_DRIVER_NAME or
__GLX_VENDOR_LIBRARY_NAME. Read a marker off stdout instead.
Closes#6914
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Install rows hid themselves with `when:"! <present>"`, so software you
already had vanished from the very list it was installed from. Add a
`disabled:` guard that keeps a row listed but dim, ✓-marked, unselectable
and out of search, and move every Install row onto it.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Install missing apps when choosing defaults
* Restore Chromium through browser installer
* Trust default app installer status
* Use full conditionals for install paths
* Host the manual's images in the repo under manual/images
Replaces all learn.omacom.io/manual.omakub.org hotlinks with local webp
files capped at 1600px wide (~20MB total), so the manual is fully
self-contained. Theme and unlock previews are converted from the
canonical themes/*/preview.png files. A handful of illustration shots
are interim conversions of the current images, pending retakes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Retake the illustration screenshots fresh at 3x scaling
Navigation, clipboard history, notices, tmux layouts, prompt, and About
reshot on a clean workspace at 3.13x (1920x1080 logical on 6K) across
ten themes: Tokyo Night, Catppuccin, Gruvbox, Kanagawa, Everforest,
Nord, Osaka Jade, Rose Pine, Matte Black, and Ristretto.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop the extra themes chapter from the manual
The community theme gallery will live elsewhere. Removes the chapter
and its 114 gallery images, renumbers the following chapters, and
repoints the two references (theme installs still work via
Install > Style > Theme in the menu).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Reference the shipped theme previews instead of copying them
The themes chapter now points straight at themes/*/preview.png and
preview-unlock.png, so previews can never drift from the source and
manual/images drops from 6.3MB to 3.5MB.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Restore preinstalls from the menu, and drop the Omacom apps with them
Remove Preinstalls missed omacut, omacalc, and omawrite, so the three Omacom
apps survived an opt-out that was supposed to clear the desk.
Opting out was also one-way. Install > Preinstalls now puts everything back:
the shipped .desktop launchers and mise stubs via omarchy-refresh-applications,
the dropped packages via pacman, and the opt-out marker deleted so the
preinstalled keybindings return on reload. The two menu entries guard on the
marker, so exactly one of them is ever visible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Trim the preinstall lists to what quattro actually ships
Remove Preinstalls was still dropping typora, spotify, 1password, 1password-cli,
signal-desktop, opencode, claude-code, and github-cli. None of those are in
omarchy-base.packages anymore: typora gave way to omawrite, the services moved
to on-demand menu installs, and the agent CLIs are mise-managed. Removing them
took out apps the user had deliberately installed, and restoring them would have
put back what we no longer ship.
Both lists are now the same twelve packages, all of them in omarchy-base.packages.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Keep the opt-out marker when a restore fails
omarchy-pkg-add exits non-zero when pacman cannot install a package, but the
restore ran straight past it, cleared the marker, and reloaded Hyprland. That
reported success and brought back keybindings for apps that never arrived. The
marker now falls last, behind a check on the transaction.
The new test also pins the two lists to each other and to omarchy-base.packages,
which is the drift that let retired packages linger in the removal list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Pacman answers its own conflict question with No under --noconfirm, so one
retired package can stop every update after it. Which package to drop is a
decision rather than a cleanup, so run the upgrade again with pacman asking
when there is a terminal to answer on, and report instead when -y promised
not to ask.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
LocalSend registers an Ayatana item with no ItemIsMenu and no Activate
handler, so its primary click is a silent no-op and the menu offers only
Open and Quit. Share > Receive already opens it, so drop the item the way
Dropbox's is dropped when its dedicated widget owns the surface.
Closes#6838
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The switchers chapter shifted everything from chapter 3 up by one, and
the TOC was still on the old numbers. Add its missing entry too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The network and bluetooth panels stopped launching them; the module
catalogue still said otherwise.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add "Coming From Mac or Windows" as chapter 3: a translation layer for
Cmd/Win instincts rather than a feature tour.
Fill the remaining coverage gaps: fcitx5 input methods, laptop speaker
tuning, touchpad haptics, crash capture in the toggle table, the
installable font list, and the ChatGPT Desktop and Grok Bot entries.
Correct claims that drifted from the source: the network and bluetooth
bar icons no longer launch nmtui/bluetoothctl, monitor scaling steps
rather than cycles, and the display widget's scroll steps brightness.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Rebind ghost Copy URL shortcut registrations to the pinned id
Chromium never hands a suggested shortcut to one extension while
another — even a long-gone one — still holds the registration. Profiles
that first loaded Copy URL before its id was pinned registered
Alt+Shift+L under an id derived from the extension's load path at the
time, so the pinned extension never receives the shortcut and the
keypress does nothing (#6816).
The quattro upgrade tried to repair this against one hardcoded
path-derived id, which only ever matched a single home directory. The
historical ids are unknowable in general — they hash long-gone absolute
paths through whatever symlinks existed then — but the registration
itself names the command, so a migration now rebinds any copy-url
command that points away from the pinned id, unless that id belongs to
an extension that is actually installed or the pinned extension already
holds a binding of its own.
Browsers rewrite Preferences on exit, which reverts any repair made
while one runs, so the migration asks for this user's browser windows to
be closed first — failing and staying pending when there is no terminal
to ask in or the prompt is declined. The backup a repair leaves behind
marks it as attempted but unverified: until a browser-free run confirms
the registration stayed repaired, the migration keeps itself pending
rather than trusting a disk state an open browser may still overwrite.
The upgrade-time repair is dropped: the upgrade already runs migrations,
so the migration is the single implementation.
Fixes#6816
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Pin the WhatsApp Slim extension id
Keyless unpacked extensions get path-derived ids, which go stale if the
load path or packaging ever changes — the same class of bug that broke
the Copy URL shortcut for pre-package installs. Pin the id with a
manifest key like the other bundled extensions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Crash capture stays on by default, but Trigger > Toggle > Crash Capture (or
`omarchy toggle crash-capture`) now turns the watcher off. The toggle writes the
usual flag file and stops the unit for this session; the unit checks the same
flag with ConditionPathExists, so the choice survives a logout without the unit
having to be disabled.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Keep AppLibrary scans out of login shells
A login shell sources the user's profile, and tools like mise touch
~/.local/share on activation. Quickshell's desktop-entry watcher monitors
that tree, so every scan retriggered another scan: a self-sustaining loop
that pinned ~20% of a core at idle. The scans only use bash builtins,
find, and sort, so they don't need the login environment.
Fixes#6806
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Pin the AppLibrary scans to non-login shells in tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Cover the KVM and disk requirements, the resources and credentials the
installer asks for, the launch/stop/status commands and --keep-alive,
what the RDP session carries, and what removal deletes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Persist notification images so history keeps avatars
Persisted popup and history entries stored image/appIcon as URLs into
resources that die with the live notification: Chromium-family senders
(every Omarchy web app, WhatsApp included) pass avatars as files in a
scoped /tmp dir deleted when the notification closes, and raw image-data
hints surface as in-process image:// URLs that die with the server
object. Replaying history then found dead references and hid the icon.
Copy file-backed images into the notification state dir when persisting,
keyed by the entry's file stem, and reference the copies from the JSON.
Blank dead image:// URLs so the card falls back to the app icon. The
copies die with their JSON: superseded-popup deletes, history trims and
clears remove them, and a startup sweep collects copies orphaned by a
restart killing a queued job mid-write.
Hold DND-silenced notifications open until their history write has run,
since untracking tells the sender to delete its avatar file, and carry
replayed on-screen rows over via their persisted copies, since the
replay dismisses their live notifications first.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Coalesce silenced updates and bound image copies through temp files
A replaces_id update lands on a held DND notification without a second
onNotification, so releasing after the first write could persist a stale
snapshot. Re-snapshot when the write completes and write again until the
content is stable, reusing the original file identity.
The image copy reopened the sender-controlled path after checking it, so
a file growing or becoming a FIFO mid-copy defeated the size bound. Read
through head -c under a timeout into a temp file, validate its size, and
rename it into place; the startup sweep clears temp files a killed job
leaves behind.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Neither group has any commands behind it, so both only ever printed
"Unknown Omarchy command" when browsed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add chapters for the top bar and its panels, screenshots and recording,
toggles/idle/screensaver, browsers, shell plugins, and networking, which
collects the Wi-Fi, DNS, firewall, and Tailscale material that was spread
across the FAQ, TUIs, and services chapters.
Fill smaller gaps in the existing chapters: user hooks, autostart, menu
extensions, printing, power profiles, factory reset, drive and user
passwords, passwordless sudo, firmware updates, display text size, and
the LocalSend share menu.
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>
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>
* 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>
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>
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>
* 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>
* 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>
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>
lspci reads PCI config space, and the kernel resumes a runtime-suspended
device to serve that read. On a hybrid laptop the discrete GPU idles in
D3cold, so the first lspci of a Hyprland config load spends over a second
waking it — longer than the 1.5s budget Hyprland gives the whole load.
The reload then fails at whichever line runs next, which is why the error
pointed at default/hypr/apps/1password.lua rather than at nvidia.lua.
Read the vendor, class, and device IDs from sysfs instead. Those are
served from cached fields and never touch config space, so nothing wakes
up. Classify by device ID while we're here: Turing is both the first
generation with GSP firmware and the first at 0x1e00 or above, and
Maxwell opens at 0x1340, one ID past the last Kepler part. Bounding the
older detector at both ends keeps pre-Maxwell cards off the 580xx driver
that cannot drive them, and picks up the Maxwell and Pascal parts the
lspci name regex used to miss.
omarchy-hw-nvidia was also checked in without its executable bit, which
it needs now that nvidia.lua runs it.
Fixes#6660
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>