Compare commits

...
Author SHA1 Message Date
ZacharyZhang-NY 4505badecb Add Dim and Kimi Code to the agent roster and Install > AI
Both install through the lazy mise wrappers (npm:dimcode and
npm:@moonshot-ai/kimi-code), preinstalled on new machines and migrated onto
existing ones unless preinstalls were removed, and each gets a menu entry in
both places: under Setup > Defaults > Agent as a default-agent choice, and
under Install > AI for installing without changing the default. The launch
spellings come from each CLI's actual flags — bare dim is the TUI and `dim
exec` the one-shot, kimi runs full-auto as `kimi --auto` with `--prompt` as
its one-shot. The Install > AI entries are guarded by `mise where`, since the
preinstalled stub would otherwise read as installed. The menu marks are the
Kimi simpleicon and Dim's traced from its app icon, since upstream publishes
no monochrome SVG.
2026-08-31 23:13:42 -04:00
Ryan Hughes 943d2fcbe9 Merge pull request #9002 from acrogenesis/remove-legacy-installer-privileged-files
Repair legacy paths and privileged files left by retired installers
2026-08-30 12:07:18 -04:00
Ryan Hughes 58c399de30 Revert "Drop unrelated privileged heredoc scanner"
This reverts commit 4c23077f80.
2026-08-30 11:54:31 -04:00
Ryan Hughes 15f26cbe1b Remove redundant migration command preflight 2026-08-30 11:49:54 -04:00
Ryan Hughes 4c23077f80 Drop unrelated privileged heredoc scanner 2026-08-30 11:49:50 -04:00
Ryan Hughes 40d0c9bbdf Require Quattro migrations to complete 2026-08-30 11:36:22 -04:00
Ryan Hughes 8add7b49de Repair legacy XCompose and vulnerable power paths 2026-08-30 11:36:22 -04:00
625c4a1603 Quote install-app and install-font names like install-and-launch (#7843)
* Quote install-app and install-font names like install-and-launch

* Quote the package list too, not just the display name

The display name was quoted but omarchy-pkg-add's own arguments were still interpolated into the bash -c string raw, so `omarchy install app Vim 'vim; id'` ran id. The list has to reach the helper as several words, so it cannot be quoted whole: it is split the way the unquoted expansion split it and each word is quoted on its own. Reading with -d '' keeps a newline-separated list intact instead of dropping every package after the first, which plain read -a would. install-font's package is singular and is quoted whole, and install-and-launch carried the same flaw.

Reported by acrogenesis in review of #7843.

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

* Test that install-font skips font-set when pkg-add fails

The hostile-package case was asserting the family still got set, which only held because the mock always exits 0. pacman would reject that name and the && chain would skip font-set.

* Keep the installers working when errexit is inherited

read -d '' always ends at EOF rather than on its delimiter, so it reports failure on every input. Under an inherited errexit the installers exited there and built no command at all.

Reported by Codex XHigh in review of #7843.

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

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-30 14:22:58 +02:00
e3b566bae8 Remove the last first-run sudoers grant the installer wrote
install/post-install/first-run-mode.sh shipped on quattro between 53e26115 and 75cb4f71, and its final body writes `Cmnd_Alias FIRST_RUN_CLEANUP = /usr/bin/rm -f /etc/sudoers.d/first-run, /bin/rm -f /etc/sudoers.d/first-run`. The predicate's case listed only the two `/bin/rm` spellings, so that line fell through to the user-spec test, failed it, and the whole file read as hand-written. The migration then left it alone and wrote its machine marker, which is permanent: on an offline install from that window the account keeps passwordless `/usr/bin/systemctl` for good, and nothing looks at the file again.

Adding the string is the whole fix. The test now carries all nine bodies the installer wrote across both locations rather than the eight from install/preflight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <codex@openai.com>
2026-08-30 09:06:22 +02:00
David Heinemeier HanssonandClaude Opus 5 96404be37b Catch a privileged heredoc redirected with >|
`>|` is a plain redirect with noclobber overridden, not a redirect followed by a pipe. command_destinations detached `>` from its target before looking at the bar, so the target read as `|` and the privileged path behind it was never examined: `cat <<EOF >| /etc/udev/rules.d/99-x.rules` with `$HOME` in the body produced no finding at all, while the same write through `>` produced one.

Normalizing `>|` to `>` alongside the existing `>>` handling closes it. The fixture fails without the normalization.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 08:58:12 +02:00
David Heinemeier HanssonandClaude Opus 5 844f320bbe Stop the legacy udev migration tripping the 4.0 upgrade guard
test/shell.d/config-test.sh greps every file under migrations/ for `upgrade-to-quattro` and fails the suite when one matches, because pre-4 layout work belongs in the upgrade command rather than in a migration. The comment explaining why this particular cleanup is the exception named that command literally, so it matched the guard and config-test.sh failed on this branch while passing on quattro.

The comment now names the Omarchy 4 upgrade command without spelling the file, which leaves the guard able to catch a migration that actually reaches for it. agents/skills/migrations.md still names `bin/omarchy-upgrade-to-quattro` in full, and it is not under migrations/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 08:58:07 +02:00
acrogenesis 8e41961c7d Close privileged cleanup review gaps 2026-08-29 23:55:42 -06:00
David Heinemeier Hansson 002c70a544 Merge pull request #9090 from omacom/add-codeowners
Add CODEOWNERS requiring org owner review
2026-08-30 07:54:37 +02:00
David Heinemeier Hansson e711066522 Add CODEOWNERS requiring org owner review 2026-08-30 07:46:48 +02:00
acrogenesis f91d2e5453 Restore strict migration ordering 2026-08-29 22:42:33 -06:00
acrogenesis 4d697a063c Harden privileged cleanup review fixes 2026-08-29 21:08:33 -06:00
acrogenesis 4996941513 Address privileged cleanup review findings 2026-08-29 20:03:46 -06:00
acrogenesis 7ed761fb31 Stop the heredoc check writing its own exemption
The failure advice printed a ready-to-paste annotation with the scan's
verdict already filled in, so the shortest way past the check was to copy
back what it had just concluded. That is worst exactly where the scan is
weakest: a path it cannot follow through a variable reads as an ordinary
value, and the annotation it offers for that case is paths=none.

Print the annotation with the path list left blank and say why the author
has to fill it in. The scan's own reading stays in the report above it, so
nothing diagnostic is lost.
2026-08-29 19:23:02 -06:00
acrogenesis 80e7c25b37 Fail the sudoers cleanup when it cannot elevate to look
Running the migration on a real machine with no cached sudo credentials
printed sudo's "a terminal is required to read the password" and still
exited 0. bin/omarchy-migrate writes the completion marker on a zero exit,
so the cleanup would have been recorded as done on every install that runs
migrations without a terminal, and never tried again.

Probe for elevation before the combined existence check and exit non-zero
when it fails, so the marker stays unwritten and the next run retries. The
probe is skipped when the directory is readable as-is, which is the case
when migrations run as root.
2026-08-29 19:23:02 -06:00
acrogenesis 394c1371c9 Model what each parser does with an empty and a dangling directive
Review of the previous commits turned up four places where the predicates
and their tests disagreed with the tools they are modelling, each checked
against udevadm verify, systemd-analyze verify and visudo -cf rather than
against reading of the sources.

An empty ExecStop= resets the list, so a unit an administrator neutralised
that way runs nothing at shutdown and is no longer ours to remove; the
predicate now tracks the last state instead of returning on the first home
path it sees. A file whose last line ends in a backslash still carries a
live directive for systemd, so the pending logical line is emitted at EOF;
udev ignores such a line and sudo rejects the file outright, so this costs
those two nothing. The scanner's taint pass now reads += appends, which its
own comment already promised: the value of an append is no use, but a name
that reaches a user root through one has to be judged on it.

Two regression guards passed against the implementations they were written
for. The udev continuation fixture put the whole RUN+= below the comment, so
it matched whether or not the pending half was carried across; the split now
falls inside the RUN+= value. The sudoers one kept its file on the strength
of a spec above the comment, so it could not fail either; the hand-written
spec now sits below. Both fail against a mutant that discards the pending
line. The comment above the second also claimed a continued comment stays a
comment, which visudo contradicts.
2026-08-29 19:23:02 -06:00
acrogenesis d593847728 Fail the suite on privileged writes through unquoted heredocs
An installer that writes a root-owned file through a heredoc with an
unquoted delimiter (<<EOF rather than <<'EOF') has the installing user's
shell expand the body first, so a user-controlled value is baked in as a
literal. Send that into /etc and root later reads or executes a path the
unprivileged user picked: a udev rule carrying
$HOME/.local/share/omarchy/bin/... resolves through a symlink that user
owns, so replacing the symlink gets their code run as root.

Add the static check. A heredoc is flagged when its delimiter is
unquoted, its body contains an install-time expansion (escaped \$VAR does
not count, since that is left for a root daemon to expand at runtime),
and its output reaches /etc, /usr, /opt, /srv, /boot or /var/lib via sudo
tee, sudo dd, a redirect, or an install/cp/mv of the generated scratch
file. Destinations written as variables are resolved from the file's own
assignments.

Sites that genuinely need install-time expansion declare it inline:

  # omarchy:heredoc-expands paths=none -- $servers is a validated IP list

paths= is machine-checked against the expansions the scanner finds to be
path-shaped, so this cannot become a rubber stamp: adding a $HOME/... to
an already-annotated heredoc makes the declaration false and trips the
check again. Path expansions anchored under a root-owned prefix, as in
"/etc/systemd/system/$unit", are correctly not path-shaped.

Annotate the sites the scan reports, each of which expands a scalar: DNS
addresses in omarchy-dns, a literal PAM line in
omarchy-setup-security-fingerprint, kernel cmdline parameters and
usernames in omarchy-upgrade-to-quattro. omarchy-provision-owner expanded
a unit name that was already a constant, so its delimiter is now quoted
and the name hardcoded; the generated unit file is byte-identical.
omarchy-windows-vm declares paths=storage,shared, the only site that
interpolates a user-chosen path.

Fixtures prove non-vacuity in both directions: the write routes other
than a pipe into sudo tee, the shapes that must stay quiet, udev rules
and a shutdown unit taken verbatim from this repository's history, and
the rubber-stamp case where a paths=none annotation on a baked $HOME path
still fails.
2026-08-29 19:23:02 -06:00
acrogenesis 96ed473ce1 Remove privileged files left behind by retired Omarchy installers
Three installers that no longer exist each left a root-owned file on
disk, and nothing in Omarchy has ever removed any of them.

/etc/sudoers.d/first-run granted the installing account passwordless
sudo for the rest of the first boot, unrestricted /usr/bin/systemctl
included from 2025-10-14 on. omarchy-first-run clears its first-run.mode
guard before eight set -e steps and only deletes the grant after them,
so any failure in between strands it with nothing left to retry.

/etc/sudoers.d/tsui named whatever $(which tsui) resolved to for the
installing user, normally a binary under their own home that the vendor
script had just written without sudo.

/etc/systemd/system/omarchy-plymouth-shutdown.service ran an ExecStop
under the installing user's home as uid 0 on every shutdown.

Each file is judged against what the installer that wrote it actually
produced. The first-run grant was rewritten eight times and only the
last four carry both Cmnd_Alias lines, so rather than key on those, every
active line must be one the installer emitted and one of them must be
its own self-cleanup. The shutdown unit is disabled but never stopped:
stopping it is what would run the ExecStop being taken away.

Generalize the migrations.md exception, which framed itself around pre-4
layout transitions and so did not cover installers retired on their own.
2026-08-29 19:22:44 -06:00
acrogenesis cd519283fe Remove Omarchy 3 power udev rules that run a command out of a user home
Omarchy 3 wrote 99-power-profile.rules and 99-wifi-powersave.rules with
an unquoted heredoc, baking the installing user's home into a rule udev
runs as root. That path resolves through ~/.local/share/omarchy, a
symlink the unprivileged user owns, so replacing it and provoking a
power_supply event runs their code as root. HEAD points the rules at
/usr/bin under new names, but the one-shot cleanup for the old
filenames was dropped, leaving the file on every install that came up
through the 3.x line.

Remove a legacy file only when an active RUN+= really does run that
filename's binary out of a home directory, so a rule of the same name a
user wrote themselves stays, comments and all.
2026-08-29 19:22:44 -06:00
acrogenesis a75551de4e Allow a migration to clear a pre-4 layout that is a security defect
The upgrade command only runs on a machine still crossing 3 to 4, so a
vulnerable file an old installer wrote never gets swept on an install
that crossed already. It ends by running omarchy-migrate, so a single
migration reaches both populations.
2026-08-29 19:22:44 -06:00
Ryan Hughes 158e8cfb3a Merge pull request #8419 from AFOliveira/security/windows-vm-mount-boundary
[codex] Secure Windows VM host mounts
2026-08-29 19:31:39 -04:00
Ryan Hughes e229927671 Merge pull request #8934 from ErikMelton/security/plymouth-publication-race
Secure Plymouth and SDDM asset publication
2026-08-29 18:45:37 -04:00
Ryan Hughes 70e79c40ae Harden Plymouth and SDDM reset publication 2026-08-29 18:19:51 -04:00
Ryan Hughes c720f0b981 Merge pull request #8951 from omacom/cups-browsed-temporarily-removed
Temporarily remove automatic printer discovery
2026-08-29 15:38:23 -04:00
David Heinemeier Hansson 5236f4426c Merge pull request #8203 from hjanuschka/fix-chromium-first-run-eula
Skip Chromium's new first-run EULA
2026-08-29 21:28:42 +02:00
Ryan Hughes 2002fb35ed Test installed CUPS state directly 2026-08-29 15:27:16 -04:00
David Heinemeier HanssonandClaude Opus 5 e47784be11 Name the dev-link authorization when it is the thing that failed
Validating /etc/omarchy.conf walks its parent chain to /, and that walk leaves
its own subject behind in the global failure_context. The comparison that
follows -- the one deciding whether the authorization actually names this
checkout -- sets no context of its own, so an unauthorized development tree
refused with:

  refusing to publish: directory / (must be root-owned and not group- or
  world-writable) failed validation

naming a directory that had just passed, and pointing the reader at a
filesystem problem that is not there. Seen on a worker VM running an
unauthorized checkout, where / is 0555 root-owned.

The hint line underneath was already right, which is what kept this from being
worse. Restore the context before the comparison so the first line agrees with
it, and assert both halves: that the refusal names the authorization, and that
it does not blame the root directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115LngksSpXLD9NSXBEP3ki
2026-08-29 21:03:02 +02:00
Ryan Hughes 96d5682460 Fix cups-browsed removal migration 2026-08-29 14:58:33 -04:00
David Heinemeier Hansson bf20c94ea0 Merge quattro into the Chromium first-run EULA branch
Quattro stopped making the Chromium managed-policy directory world-writable while this branch was open, and the block it deleted from the theme install leaf sat directly above the comment this branch rewrites, so the two edits landed in one hunk. The resolution keeps the hardening — the policy directory is set up through install/config/browser-policy.sh now — along with the first-run seed and the comment that names both things the seed does.
2026-08-29 20:55:09 +02:00
Ryan Hughes bb5b178e5f Remove unused cups-browsed install override 2026-08-29 14:24:05 -04:00
David Heinemeier HanssonandClaude Opus 5 1b92b7f5be Correct the dev-link help now that Plymouth follows the link
omarchy-dev-link told people /usr/share/plymouth was not covered by a link
and to reach for omarchy-dev-pkg-test. Since the publisher authorizes a
dev-linked checkout out of the root-owned /etc/omarchy.conf, the Plymouth and
SDDM themes are republished from the checkout like every other
$OMARCHY_PATH-resolved tree, so the help was left describing the old
behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 18:03:57 +02:00
Erik Melton aac1b009e4 Cover non-regular Plymouth logo descriptors 2026-08-29 17:47:42 +02:00
David Heinemeier HanssonandCodex XHigh 1dbc7d5bce Close each discovered queue before inspecting or removing it
Checking a queue for jobs and then deleting it leaves a window in between, and the sudo that does the deleting can sit at a password prompt for as long as someone takes to type. A job submitted in that window was cancelled by a deletion that had already decided the queue was empty. cupsreject closes the queue to new work first, which also stops more jobs piling onto one that is being left behind and can no longer route them. It comes from the cups package, which stays.

What a queue with jobs on it means is now said rather than implied: whatever is already at the printer finishes, whatever is still waiting cannot be routed with the daemon gone, and the person who owns them has to cancel what is left.

A queue that another administrator removed while this was running is the outcome wanted, not a failure to keep the package installed for, so a deletion that fails is checked against whether the destination is still there.

Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-29 17:44:31 +02:00
David Heinemeier HanssonandClaude Opus 5 d67a7c00bc Cover the ownership and mode checks the publisher rests on
Mutating each root-side control one at a time showed three that no test could
see: a destination directory root does not own, a single user-owned asset
inside an otherwise root-owned packaged directory, and an asset left group- or
world-writable by its own mode. Deleting any of the three left the suite green,
because the existing cases mark a whole tree untrusted and are caught by the
directory check before the per-file one is reached.

The harness already had the hook for the ownership pair: TEST_UNTRUSTED_SOURCE
makes the stat shim report a chosen prefix as uid 1000, so those two only need
it pointed at a destination directory and at a single file rather than at a
whole tree. A mode has to be real, so that case stages a copy of the packaged
tree the shim reports as root-owned and loosens one asset in it.

The empty logo is refused by the destination size bound rather than the
caller-side one, so that case pins the behaviour without isolating the check;
the two bounds are exactly redundant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0115LngksSpXLD9NSXBEP3ki
2026-08-29 17:39:33 +02:00
David Heinemeier HanssonandCodex XHigh 24c18df5b7 Temporarily remove automatic printer discovery
cups-browsed is the daemon that watches the network and creates print queues by itself. Hardening it took a root daemon with a predictable cache down to a confined service account, but a daemon that turns anything advertising itself on the network into a print queue is a lot of exposure for a convenience, so it comes out of the default install while that is reworked. Only the discovery half: CUPS itself stays and printing keeps working, with each printer added by hand in Print Settings.

The migration disables the unit before removing the package because that is the only order that works: pacman deletes the unit file but not the enable symlink, and once the unit is gone systemd can no longer resolve it by name to clean that up.

It then removes the queues discovery generated. cups-browsed keeps those when it stops, since KeepGeneratedQueuesOnShutdown defaults to Yes, and they route through its own implicitclass backend, which goes with the package, so they cannot print again. Idle ones go. A queue with jobs on it is left alone and named: implicitclass only needs cups-browsed to choose a destination, so a job already past that point finishes on its own, and deleting the queue would abort it. One printer's job does not hold up the removal. A printer added by hand has an ipp:// or usb:// device and is left where it is.

A queue whose jobs cannot be asked about is left alone rather than assumed idle, including one named so that lpstat would misread it -- "all" is its word for every destination, and a leading dash or a comma reads as another option or a list.

Where CUPS does not answer at all, or a queue will not delete, discovery is still stopped but the package stays and no marker is written. omarchy-migrate records a migration for the user as soon as it exits zero, so that is where the machine stays until someone removes the package by hand, and the message says so rather than implying a retry.

The queue list is read under LC_ALL=C because lpstat translates "device for", and captured rather than piped, so a cupsd it cannot reach is reported instead of reading like a machine with nothing to clean up.

It removes with plain pacman -R rather than omarchy-pkg-drop, which passes -n and would discard /etc/cups/cups-browsed.conf instead of keeping it as a .pacsave. A removal meant to be temporary should not delete the machine's copy of its own configuration. Without -s either, so it only ever removes the package it names: sweeping newly unneeded dependencies is nothing today, but it is not a promise a rolling dependency graph can keep.

Queue names come off the network, since cups-browsed names its queues after what the printer advertised. CUPS allows every printable character but space, tab, / and #, and lpstat and lpadmin take a destination as an option value, so a name with a leading dash or a comma is reported rather than passed to them and guessed at.

Migration state is per user, so a machine-wide marker records the one removal. Without it, an account whose first migration run came after someone deliberately reinstalled discovery would quietly take it back out again.

The install-time override for cups-browsed.conf now waits for cups-browsed rather than for CUPS. Guarding it on a file CUPS still ships would write a configuration file for a package nothing installed, and pacman would later land the package's own copy beside it as a .pacnew.

The hardened configuration stays in the tree. omarchy-settings still ships the cups-browsed.conf override, the sysusers account and the service drop-in, so they are what discovery returns onto.

Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-29 17:13:11 +02:00
David Heinemeier HanssonandCodex XHigh e9ba17e52e Keep the explanation for the Chromium color scheme defaults
The comment above the seed was the only thing recording that color_scheme and color_scheme2 are both zero in order to follow system appearance rather than force dark. Generalizing it to "first-run defaults" left two magic numbers with nothing to explain them, so the next person touching an unrelated first-run setting has no way to tell that changing them regresses theme following. Name both things the seed does.

Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-29 17:13:06 +02:00
David Heinemeier HanssonandCodex XHigh eb76684c60 Retrofit the Chromium first-run EULA opt-out onto existing installs
Chromium 151 flipped MasterPrefs::eula_required from false to true, so a first run with no seed now stops on a blank terms-of-service dialog before the browser opens. The opt-out is written in two places that each run exactly once: the install leaf that runs during ISO finalization, and the one-time 3.x upgrade. A machine already on Quattro runs neither again, so it keeps the old seed and still meets the dialog the first time anyone launches Chromium, including from every user account created after the install.

The migration writes the same seed those two paths write. It compares before writing so the second user on a shared machine no-ops rather than repeating a machine-wide repair, and the literal is duplicated rather than sourced because a migration repairs the state of its own moment and must not drift when the seed later changes.

Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-29 17:13:06 +02:00
David Heinemeier Hansson 56fbaf4689 Merge pull request #8162 from jmwall/fix/windows-vm-opacity
Keep the Windows VM display fully opaque
2026-08-29 17:10:14 +02:00
Erik Melton 363db1f569 Keep Plymouth publishing working in dev mode 2026-08-29 16:19:31 +02:00
Erik Melton 11fa6b9809 Race Windows VM mount sources concurrently 2026-08-29 15:15:51 +01:00
Erik Melton bf10b75150 Protect the Windows VM web console 2026-08-29 15:15:51 +01:00
079d116511 Cover the Plymouth parent-chain walk and guard its test harness
Deleting the whole ancestor walk from validate_trusted_directory and checking
only the immediate parent left the suite green, so the invariant the design
rests on had no coverage: a writable ancestor lets an attacker swap a validated
directory out from under the leaf. Reject a run whose destination grandparent
is world-writable while the destination itself is pristine.

The harness also rewrites the root script's fixed /usr/share paths by string
substitution. A drifted string silently no-ops that rewrite and would point the
simulation at the real system tree, so assert each substitution landed. Assert
the scratch directory exists too: a failed mktemp left it empty and the suite
carried on building paths from it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-29 16:01:01 +02:00
David Heinemeier HanssonandClaude Opus 5 d3b7810a76 Say why the privileged Plymouth transaction refused
Every check inside the root shell is a bare [[ ]] or (( )) assertion that
aborts under set -e, so a refusal exited with status 1 and no output at all.
The floating-terminal wrapper then printed its green "Done!" for any status
but 130, so a failed boot-theme change read as a success.

The refusal a working machine actually hits is omarchy dev link, which points
OMARCHY_PATH at a checkout the desktop user owns. Name that case outright and
point at omarchy dev unlink; report every other rejection through an ERR trap
that names what failed validation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 16:00:50 +02:00
Erik Melton 05fb36d3cc Build Plymouth assets in a root-owned stage 2026-08-29 15:05:13 +02:00
Erik Melton a465dfa681 Use the runtime path for Plymouth refresh 2026-08-29 14:45:17 +02:00
Afonso Oliveiraanditz4blitz 20a23b8c16 [Security] Complete Plymouth publication coverage
Keep SDDM color substitution collision-free for White themes, based on the fix proposed in #8469.

Co-authored-by: itz4blitz <itz4blitz@users.noreply.github.com>
2026-08-29 14:45:17 +02:00
Afonso Oliveira 0f15e2330f [Security] Harden Plymouth asset publication 2026-08-29 14:45:17 +02:00
Afonso Oliveira 2b91fdc0d3 [Security] Pin Plymouth asset reads before elevation 2026-08-29 14:44:37 +02:00
Ryan Hughes 0b3f1b7ead Merge pull request #8496 from Chessing234/security/webapp-http-only 2026-08-29 03:32:08 -04:00
Ryan Hughes f20bf0a21b Merge pull request #8473 from bastidotnet/fix/webapp-desktop-value-escaping 2026-08-29 03:27:39 -04:00
Ryan Hughes 9da8824098 Merge pull request #8416 from mdisec/theme-name-shell-syntax
Refuse a theme name that is shell syntax, and quote the one the unlock picker returns
2026-08-29 03:19:45 -04:00
Ryan Hughes 169ad00a84 Merge pull request #8627 from mdisec/security/harden-cups-browsed
Harden CUPS printer discovery
2026-08-29 02:19:53 -04:00
Ryan Hughes 74997fd523 Ship CUPS authorization through settings package 2026-08-29 02:18:44 -04:00
Ryan Hughes f0672772d2 Merge pull request #8268 from Skeptomenos/fix/copy-url-python-shim-recursion
Avoid mise shim recursion in Copy URL migration test
2026-08-28 21:54:19 -04:00
Ryan Hughes 62eb5182d0 Merge pull request #8835 from basecamp/fix-browser-policy-exit-trap
Fix migration 1787515927 failing on Bash 5.3
2026-08-28 21:24:32 -04:00
Ryan Hughes 6b10dbf191 Harden Firefox policy dirs even when the theme refresh fails 2026-08-28 19:00:03 -04:00
Ryan Hughes 5925929cb6 Stop the browser policy EXIT trap from reporting a clean run as failed 2026-08-28 19:00:03 -04:00
Ryan Hughes 7d58bb9a62 Merge pull request #7972 from acrogenesis/harden-browser-policy-dirs
Stop world-writable browser policy directories
2026-08-28 18:00:20 -04:00
David Heinemeier Hansson 6dd9aa55cc Merge pull request #6919 from AksharP5/agent/resolve-dialog-pointer-focus
Stop DaVinci Resolve dialogs from recapturing pointer focus
2026-08-28 23:41:00 +02:00
Ryan Hughes 468b511249 Merge pull request #8397 from ErikMelton/unauthorized-http-get-requests-from-notifications
Require textFormat declaration for all Text elements
2026-08-28 15:50:19 -04:00
Afonso Oliveira 4fc14173b7 [Security] Add Windows VM boundary race regressions 2026-08-28 19:37:04 +01:00
David Heinemeier Hansson 06e32d243d Merge pull request #8198 from bastidotnet/harden-apple-brightness-device-cache
Validate the cached Apple-display device path before use
2026-08-28 14:42:22 +02:00
Afonso Oliveira a165185a3f [Security] Complete Windows VM mount hardening 2026-08-27 23:53:44 +01:00
d1845245d3 Unquote the new variables inside [[ ]]
AGENTS.md asks for unquoted variables inside `[[ ]]`, with quotes reserved for string literals being compared. The three conditions added here quoted them.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 21:40:31 +02:00
9d8c0176d1 Make the cache tests fail when either check is removed
Neither half of the validation was covered. Dropping `&& -c $cached` from the wrapper left the whole file green: all three poison values fail on the pathname prefix, so none of them ever reached the character-device test. A path that matches the hiddev glob but is not a device now covers it, and it is the real case rather than a synthetic one -- the display replugs, the interface renumbers, and the cached node is gone. It is added only when the host has no such node, so a machine with the display attached cannot fail there spuriously.

The no-XDG_RUNTIME_DIR assertion had the same problem for the opposite reason: its decoy held a path the validation rejects on its own, so restoring the `${XDG_RUNTIME_DIR:-/tmp}` fallback left it passing. It asserts on the open now instead of on the contents -- a FIFO with no writer blocks whoever opens it, so a wrapper that consults the path hangs and one that ignores it exits. mkfifo is atomic and fails outright if the path is taken, so it still neither overwrites a file nor follows a symlink at the fixed path.

Clearing created_tmp_cache as soon as the decoy is removed keeps this run's EXIT trap from deleting a concurrent run's decoy at the same fixed path, which would have let that run pass against the old code.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 21:40:31 +02:00
9382410026 Validate the web app URL before anything fetches it
The scheme check ran after the interactive branch, which had already handed the URL to curl: a refused `file://` or `ftp://` URL was dereferenced first, the whole string went out to Google's favicon endpoint in a `domain=` query, and a fetch that happened to succeed left an orphan icon and a rebuilt icon cache behind. Validating immediately after normalization puts the refusal ahead of every dereference in both branches.

A leading space also kept the URL out of the scheme test entirely. `normalize_webapp_url` saw no scheme, prefixed it, and ` file:///etc/passwd` became `https:// file:///etc/passwd`, which passes `^https?://`. The desktop `Exec` field is unquoted, so it splits back into two arguments, and `omarchy-launch-webapp` forwards everything after the first to the browser, where a bare argument is another URL to open. Refusing whitespace closes that without touching desktop-entry escaping.

Scheme comparison is case-insensitive because schemes are: `HTTPS://example.com` installed before this check existed and has no reason to stop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 20:10:15 +02:00
b07374f03c Reserve the cups-browsed account name at install
The username prompt already refuses the service accounts a desktop user must not claim, cups and lp among them. A user who took cups-browsed would get a primary group of that name, and the CUPS authorization written here puts that group in SystemGroup, handing that desktop user the passwordless administration the rest of this change removes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 19:55:36 +02:00
68fc0cf6e6 Match the CUPS directives being rewritten the way cupsd reads them
cupsd compares directive names with _cups_strcasecmp, so a hand-edited "systemgroup sys root wheel" is live configuration, but matching $1 against the canonical spelling skipped it and appended a second directive at the end of the file. parse_groups accumulates the groups of every SystemGroup directive it reads rather than replacing them, so both lines took effect and wheel kept the passwordless administration this is meant to remove, with the migration reporting success.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 19:55:36 +02:00
David Heinemeier HanssonandCodex XHigh 2b923cf5bd Pin the theme name check to C, and read a colon before any slash as scp-style
A bracket range is collated, not ASCII, so `[a-z]` admits `é` under
en_US.UTF-8 and refuses it under C: the same theme URL installed on one
desktop and was refused on the next, and the manual's stated set was only
true in the C locale. Pinning the comparison makes the written set the
shipped one everywhere.

git reads a URL as scp-style when a colon appears before any slash, so the
path after it need not contain one. Requiring a slash left the prefix on
`git@host:omarchy-blue-theme.git` and derived the name
`git@host:omarchy-blue`, which the allowlist then refuses -- a repo that
clones fine could no longer be installed at all.

Co-Authored-By: Codex XHigh <codex@openai.com>
2026-08-27 19:52:37 +02:00
521f1ae9ac Resume cups-browsed on whether it is enabled, not on whether it was running
The migration recomputed whether cups-browsed was active at the start of every run, after an earlier run may already have stopped it. A run interrupted between that stop and the completion marker left the service down, and the retry that followed read it as inactive, skipped the restart, and wrote the marker anyway: printer discovery stayed off until the next reboot with no migration left to bring it back. Keying the restart to whether the unit is enabled survives the interruption, and reports not-enabled for a unit the user masked or disabled, which restarting would fail on and abort the migration short of its marker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 19:47:10 +02:00
c2587dff08 Escape .desktop values with parameter expansion, not sed
GNU sed's N auto-prints the pattern space and exits at end of input, so the
`:a;N;$!ba` slurp skipped every following s/// for a value with no newline in
it. That is every value except the injection attempt the escaping exists to
stop, so the Exec quoting's own backslashes were written to the file unescaped
and GLib refused to load the entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex GPT-5.6-sol (xhigh) <noreply@openai.com>
2026-08-27 19:45:45 +02:00
Omarchybot 83881e979b Merge pull request #7984 from Chessing234/fix/webapp-name-slashes
Keep a web app name out of the launcher's directory structure
2026-08-27 19:40:46 +02:00
ClaudeandClaude Opus 5 9ece53cede Prove the web app name guard, and reject before the icon is fetched
The slash guard was the only thing keeping a name out of the directory structure, and nothing tested it: deleting it left the suite green, because creating the launcher directly in the applications directory already makes the redirect fail on its own, with a raw bash error instead of the message. The assertion is on the message now, alongside the traversal case the guard actually closes -- on quattro a name of `../../../../escaped` writes its launcher clean outside the applications directory.

The interactive prompt read the name, fetched the favicon, wrote it and updated the icon cache before the name was ever checked, so a URL typed into the Name field left an icon behind on every attempt. Validating as soon as the name is read covers both paths from one place.

Removing by name also scanned unconditionally, so a machine with no applications directory printed a find error where omarchy-remove-gaming-xbox-cloud does not hide stderr.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 19:25:25 +02:00
Mehmet InceandDaybreak Blue 5c336885d2 Harden CUPS printer discovery
Run cups-browsed as a locked service account with a dedicated cache and a focused systemd sandbox. Restrict automatic queues to driverless IPP printers, remove wheel from passwordless CUPS administration, replace cups-pdf with Polkit-backed setup, and migrate existing systems safely.

Reported-By: Erik Hunstad (Bad Sector Labs)

Co-Authored-By: Daybreak Blue <noreply@openai.com>
2026-08-27 18:00:12 +01:00
David Heinemeier Hansson 946704f309 Merge pull request #8224 from kevinmcconnell/once-requires-sudo
Run ONCE with sudo when installing
2026-08-27 17:36:31 +02:00
Ryan Hughes c5a5e14e99 Merge pull request #8072 from basecamp/require-omarchy-repo-signatures
Require signed packages from the Omarchy repository
2026-08-27 11:29:12 -04:00
4be440b501 Close six ways the textFormat scan reported success without checking
Each of these is a Text rendering external data with no textFormat, written in
a form that passed silently. None exists in this tree, so they were holes in
the guard rather than live exposures — but a guard is only worth what it
catches, and every one of them is a single line someone could plausibly write.

  Text /* why */ {          strip_noise knew // and not /* */, so a block
                            comment between the type name and its brace hid
                            the element from every rule at once
  QQ.Text { ... }           a namespaced import made the name compare unequal
                            to `Text`, and the element was skipped outright
  visible: textFormatEnabled  textFormat was matched as a substring, so a
                            lookalike property exempted the whole block
  component Info:           a component root with its Text on the next line;
    Text {                  the one-line form was covered and this was not
  an unreadable subdirectory  rglob() swallows a directory it cannot enter, so
                            a locked subtree scanned as though it were empty

The scan moves out of the heredoc into qml-text-format-scan.py, taking its root
as an argument, because nothing could run it over anything but the real tree —
and a scanner whose only input always passes cannot be shown to fail. The test
now runs it over nineteen fixtures, one per form above and one per form the
scan already handled, so a later edit that loosens it fails here instead of
going unnoticed until something renders a remote image.

Two limits stay open and are written down in the module docstring rather than
papered over: text assigned from elsewhere (a Binding element, PropertyChanges,
an onCompleted assignment, a property alias onto a child) is invisible to a
scanner that reads each element's own declaration, and a regex literal holding
a brace throws off the brace depth. Neither shape exists in this tree and both
need a QML parser, not another regex.

Co-Authored-By: Codex XHigh <noreply@openai.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:28:13 +02:00
David Heinemeier Hansson 7026ede90b Strip image tags after the newline rewrite, not before it
The card binds the body Text to styledBody, which rewrites newlines to <br/>
*after* sanitizeBody has run. That rewrite inserts tag syntax into text the
stripper deliberately kept: a kept tag may hold a `<` of its own, and `<x`,
newline, `<img src="http://host/x.png">` is one tag named `x` to both the
stripper and Qt, so it survives whole — until the rewrite splits it into
`<x<br/>` and a live image tag the input never contained.

Measured against Qt 6.11.2 with an offscreen StyledText and a local HTTP
server: that body issues the GET after this branch's sanitizer and issues
nothing before it, because the one-pass /<img[^>]*>/gi it replaces deleted the
inner substring outright. The whole-tag bound is still the right trade — it is
what stops the stripper manufacturing tags — but it only holds if nothing edits
the string afterwards.

So move the rewrite into NotificationLogic, next to the reasoning it depends
on, and strip again after it. What Qt parses is then what was checked last. The
tests assert on styledBody for the same reason, since sanitizeBody's output is
no longer the string that reaches the renderer, and a regex assertion pins the
card's binding because no JavaScript assertion can see a QML property.
2026-08-27 16:53:55 +02:00
David Heinemeier Hansson 9d02bb08f8 Merge pull request #8549 from basecamp/crash-notification-mute
Let a crash diagnosis mute that program's notifications
2026-08-27 13:19:46 +02:00
Omarchybot b68d4142d7 Cut the crash-mute section of the skill to what it instructs
The section had grown a paragraph per review round, each one explaining why the last was right, until one offer took a third of the file. Most of it was reassurance about what the command refuses rather than anything an agent has to do, and the command enforces that itself whatever the prose says.

What is left is the instruction: offer it and never run it unprompted, say how to lift it, which of the two names to pass and why the binary is the better one, quote it because the name is the crashed program's to choose, and name the interpreter collision before muting python or node on someone's behalf. Fifty-four lines to thirty-two, with nothing dropped that changes what the agent does.
2026-08-27 12:28:44 +02:00
OmarchybotandCodex XHigh ea6ee9440a Add omarchy-crash-mute to mute and unmute one program
The mute was reachable only as `omarchy-toggle crash-ignore/<program>`, which asks whoever runs it to know the flag layout, to reduce a binary's path to the name the watcher keys on, and to have read the rule that a name climbing out of that directory writes an unrelated toggle. All of that was carried in the skill's prose, which is the wrong place for a rule that has to hold: prose is advice, and the thing being advised about is a name the crashed program chose.

So it is a command now. `omarchy crash mute hyprland` silences that program, `off` lifts it, `toggle` flips it, and no argument lists what is muted. It takes the binary's path as readily as the name and reduces it the way the watcher does, so the `Executable:` line from `coredumpctl` can be handed straight to it; it refuses what is not one component of a name, so it cannot be talked into writing outside its own directory whatever it is given; and it re-reads the flag afterwards and reports what is now true rather than what was asked for. The listing counts only regular files, because that is all the watcher honours -- anything else in there would read as muted while the crashes kept arriving. A leading `--` is consumed so a program named `-h`, which the router would otherwise answer with its own help, can still be muted.

The watcher gained an unrelated fix that this uncovered. Its fields are read with `IFS=$'\t'`, and tab is IFS whitespace, so an empty field collapsed into the next delimiter and shifted every field after it along one: a crash whose comm was empty had a path read as its pid and was discarded as somebody else's. A process can set its comm to nothing, so that was reachable. Empty fields now arrive as a dash like missing ones, and a dash joins the empty and dot cases that fall back to `unknown`.

Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 11:26:28 +02:00
Omarchybot eeb4206c7b Say in the manual what the skill already says about quoting
The manual had single quotes covering "punctuation your shell would otherwise read as its own", which is more than they do: a name containing a single quote closes them, and the rest of it is read as shell. The skill states that correctly and the manual did not, so the one document a person reads before typing the command was the one making the claim that does not hold.
2026-08-27 10:38:18 +02:00
OmarchybotandCodex XHigh 8d14869689 Let a crash diagnosis mute that program's notifications
A crash that is understood is not a crash that stops: an upstream bug waiting on a release, a program that dumps core every time it exits. The diagnosis explains it once and the toast keeps arriving, and the only answer Omarchy had was Crash Capture, which turns off every program's notifications in order to silence one.

The watcher already resolves a name to dedupe on and announces that same name in the toast, so the mute is keyed on it: a flag file under toggles/crash-ignore/, written by the existing omarchy-toggle and read by the existing omarchy-toggle-enabled. One flag per name rather than one list, so `on` mutes, `off` un-mutes, and `ls -A` shows what is muted, with no new file format and nothing to parse. It is the executable's basename wherever one was recorded, falling back to the process name, which the kernel truncates to fifteen characters -- muting the truncated form would match nothing, forever, while looking like it worked.

The name is not always a name, though, and the mute turns it into a path. A program picks its own comm and prctl takes anything, including slashes, and the watcher falls back to comm whenever a crash carries no absolute executable. So it is stripped to its last component first: without that, `a/../bar-off` is a legal comm aimed at an unrelated Omarchy flag, letting a crashing program suppress its own notification and letting a user who accepted the offered mute hide their bar instead. Stripping does not always leave a component either -- `/` leaves an empty string, which is no kind of array subscript and no kind of toast, and `.` or `..` names a directory that omarchy-toggle would touch and report success on, leaving a mute that never matches. Both fall back to `unknown`, the word omarchy-agent-crash already uses for a name it does not have, and which mutes like any other.

The skill offers this at the end of a diagnosis and never runs it unprompted, which makes it the single change a diagnosis may make to a system it otherwise only reads. It tells the agent to use the name it was handed rather than re-derive one, since the watcher resolved that name already and the two agree for ordinary names and not for strange ones; a diagnosis started by hand from `omarchy agent crash <pid>` is given no name and gets the derivation instead. It also says to treat the name as hostile text rather than as a word -- it is whatever the crashed program's author called a file, so a single quote inside one closes the quotes around it and the rest runs as the shell -- and to check the flag arrived rather than assume it.

Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 10:32:10 +02:00
Luke Parke eb7ecd13f3 Keep Ori interactive when launched with a prompt (#8455) 2026-08-27 09:21:07 +02:00
Taksh 877f1e96ef Keep web app launchers on http(s)
Chromium --app= will run javascript:, file:, and data: URLs. Prefix
schemeless input with https as before, then refuse anything else.
2026-08-27 07:17:44 +05:30
Mehmet InceandClaude Opus 5 3b0e899029 Let a theme name hold a plus or lead with an underscore, and document the set
The name a theme installs under is derived from its repo URL, and holding it to
an allowlist made that allowlist a naming convention nobody had written down. It
was also tighter than the harm it exists to stop: `+` is not shell syntax and a
leading `_` is neither the `..` climb nor the dash basename reads as an option,
so `omarchy-c++-theme` was refused for nothing.

Widen the set to those two and say what it is where a theme author is already
picking a name. The leading character stays out of `.` and `-`, which is the
part that does the work.

Reported-by: Luis Alvarez (lalvarezt)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fd3RCHxwjEbMXoSYB9Aiso
2026-08-26 23:37:44 +01:00
bastidotnet 187c268d68 Escape webapp .desktop values per freedesktop spec
- omarchy-webapp-install wrote all substituted values raw into the
  generated .desktop (Name/Comment/Icon/MimeType/Exec), with no escaping.
- Adds two spec-level escapers: Desktop Entry string escaping on every
  field (a raw newline could inject a second key line / second Exec=),
  and Exec-argument quoting for the default Exec's URL (spaces, %, reserved
  chars). $CUSTOM_EXEC stays file-syntax-only — it is a full command line
  by design, not a single value.
- No known exploit path: untrusted input reaches these values today only
  via Omarchy literals, interactive gum, or direct CLI. This is
  defense-in-depth for a latent sink.
- Verified end-to-end: generated .desktop has one escaped key per field;
  gio launch passes the URL to omarchy-launch-webapp as a single unchanged
  argument (Sunshine literal and normal gum path both byte-identical).
2026-08-26 23:59:38 +02:00
0260d2accb Stop the textFormat test from passing when it has not checked
The root rule matched only a file-level root Text, of which this tree has
exactly one. QML inline components are roots for the same reason — the
`text` of `component InfoValue: Text {` comes from every caller, so the
file it lives in never binds it — but they sit inside another element, so
the depth-1 test never saw them. Six went uncovered while the test
reported green, among them the network panel's InfoValue, which callers
bind to the IP address and gateway.

Six more ways to write a Text were read as clean rather than as unreadable:
an opening brace that is not last on its line, a brace on the line after
`Text`, a one-line block containing nested braces, a wrapped binding split
by a comment or a blank line before its `+` (which exempted a dynamic
binding as a literal), and a root Text indented from column zero. Require
the forms a line scanner can read instead of parsing QML; the tree already
writes every Text that way.

Last, a run that read no files reported success. A checkout with no shell/
QML now fails instead, since an all-clear from a scan that opened nothing
is the one answer this test must never give.

Each case is covered by a fixture that fails without its fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: OpenAI Codex (gpt-5, xhigh) <noreply@openai.com>
2026-08-26 17:42:07 +02:00
Mehmet InceandClaude Opus 5 75e51f0b95 Refuse a theme name that is shell syntax, and quote the one the unlock picker returns
A theme installed from a git repo is named after the repo URL, and that name
becomes its directory name under ~/.config/omarchy/themes. Style > Unlock built
a command line out of the name the picker returned and handed it to
omarchy-launch-floating-terminal-with-presentation, which runs its argument as a
shell string -- so a theme directory called `a';id;'b` ran `id`. Themes are
already held to contributing colour and nothing that executes, which is why
omarchy-theme-set stages no .lua, terminal config, or vscode.json from one.

Hold the derived name to the characters a theme name needs, which stops it from
being dangerous at every place it lands rather than at the one found, and quote
it with printf %q on the way into the action for the names already on disk.

omarchy-theme-remove keeps its existing path-climb guard: its name reaches only
a quoted rm, and the same charset would strand a theme installed before this.

Reported-by: Luis Alvarez (lalvarezt)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011WFcUm5HWFyxaVYdwAeWPP
2026-08-26 16:22:17 +01:00
David Heinemeier HanssonandClaude Opus 5 e428dc2627 Strip image tags whose separator Qt skips but \s does not
QQuickStyledText skips the characters between `<` and the tag name with
QChar::isSpace(), which counts U+0085 NEL. JavaScript's `\s` does not, so
isImageTag() read no name at all from a tag written as `<`, U+0085, `img`,
kept it, and Qt then read `img` and issued the GET the stripper exists to
prevent. Measured against Qt 6.11.2 with an offscreen StyledText and a
local HTTP server.

Read the name by skipping everything that is not part of it rather than by
matching the separator, so the two definitions cannot drift apart again.
Over-skipping is the safe direction: it can only classify more runs as
images, and dropping a run never manufactures a tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 17:10:17 +02:00
Erik Melton 6e962b4466 Address review comments: Enforce stricter tag handling and image sanitation in notifications. 2026-08-26 16:36:12 +02:00
Erik Melton 3af7675a10 Require textFormat declaration for all Text elements. 2026-08-26 16:14:51 +02:00
0ae1694830 Constrain the tzupdate sudoers rule to a single timezone argument (#8194)
The wildcard granted passwordless root for timedatectl set-timezone plus any trailing arguments, so -H/--host and -M/--machine reached the SSH and machine transports as root. Systemd 261 guards argv injection into ssh, but -H still drives root's SSH client at an attacker-chosen host, and the transport resolves its helper through PATH; only Defaults secure_path stands between that and a planted ssh running as root. Match the argument with an anchored POSIX ERE that admits exactly one timezone token (no whitespace, no leading-dash segment, no traversal component), so no second argument and no option can ever match. The sole caller, omarchy-menu-timezone, passes one list-timezones value and is unaffected.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-25 22:30:09 +02:00
Spencer BullandCodex XHigh 77305ed3b9 Enable Dell XPS 13 sidecar speaker amplifiers (#7032)
The Dell XPS 13 DX13260 drives its two CS35L56 sidecar speaker amplifiers through a quirk that Linux only gains in 7.2, so until Arch ships that kernel the machine plays through one amplifier with no bass. The dell-xps13-sidecar-amps package selects the same driver path with a module override; this installs it on that exact machine and nowhere else.

The detector requires both the DX13260 product name and SKU 0E53, because the override forces a quirk value rather than merging into one, and a machine that gets it wrong loses whatever quirk the kernel would have chosen for itself.

Pacman registers a package even when its post_install scriptlet fails, so the leaf calls dell-xps13-sidecar-amps-apply itself instead of trusting the install to have applied: a failed cleanup or boot-image rebuild has to reach the caller rather than hide behind a package pacman considers installed. That is also why the migration marks reboot-required only after the apply succeeds — a migration that exits non-zero keeps no completion marker and retries the apply on the next run, even though pacman already has the package.

The leaf runs after intel/ptl-kernel.sh rather than beside the other Dell leaf at the top of install/hardware/all.sh, so its boot-image rebuild sees the Panther Lake kernel that step swaps in rather than the stock one it removes.

Co-authored-by: Codex XHigh <codex@openai.com>
2026-08-25 22:29:27 +02:00
David Helmus 45749c5b68 test: cover Python shim bypass
Place a synthetic python3 shim first in PATH so CI catches any regression that resolves REAL_PYTHON through user-managed shims.
2026-08-25 21:09:45 +02:00
acrogenesis bafc9a1000 Write browser theme colour through a passwordless helper
Managed policy dirs are enterprise trust roots, so they stay 0755 root:root. The menu path takes root for that one write through a sudoers glob of six hex digits, the same shape as omarchy-dns, and falls back to pkexec where the grant is not installed. Drop omarchy-browser-policy; a group member could plant any JSON, not just a colour.
2026-08-25 13:01:01 -06:00
acrogenesis 44a186afe4 Replace planted policy directory symlinks instead of following them
install -d follows a managed or distribution symlink and would chmod the target. Unlink those paths first, and treat a dangling symlink as a directory the migration still has to repair.
2026-08-25 12:14:52 -06:00
acrogenesis bebe19bc70 Harden browser policy parent directories and validate theme RGB
install -d follows a planted ancestor symlink, and a writable parent can rename the managed leaf aside. chromium.theme is user-installed, so only a 0-255 RGB triple becomes a colour.
2026-08-25 12:10:08 -06:00
acrogenesis 87dfa14c56 Keep a trusted Firefox policies.json when repairing the directory
A world-writable distribution dir failed the hardened check even when
policies.json was already root-owned, and setup then overwrote it.
2026-08-25 12:04:03 -06:00
acrogenesis b0e6611c70 Require root-owned Firefox policies before skipping repair
The hardened gate only looked at the distribution directory. A regular
policies.json planted under the old 777 mode would then be left in place
if the directory later looked 755/root.
2026-08-25 12:04:02 -06:00
acrogenesis 95b791af16 Stop world-writable browser policy directories
Chromium managed policy is mandatory for every profile. World-writable
dirs let any local uid plant policy, including force-installed
extensions. Write goes through the omarchy-browser-policy group at 2775
so theme colour still works without other-write.
2026-08-25 12:04:02 -06:00
Afonso Oliveira c34d20ca14 [Security] Pin Windows VM mounts behind a root boundary 2026-08-25 18:39:00 +01:00
Kevin McConnell 2c93e66b0c Run ONCE with sudo when installing
The install user is no longer in the docker group by default, so a bare
`once` cannot reach the Docker socket. Run with `sudo` instead.

The script already requires sudo to install the command and enable the
service, so we can safely use it for the initial command launch as well.
2026-08-25 16:12:10 +01:00
Naeem Malik 9301092404 Update Omarchy on Mac guide link to omarchy-mac repo (#8192) 2026-08-25 17:04:13 +02:00
4cd8a081cb Fix Codex usage collection on 0.149 (#7649)
* Fix Codex usage collector approval policy

* Capture codex argv with boundaries in the scanner test

The stub joined its arguments with "$*", so the assertion compared one
flattened string and could not tell five arguments from fewer containing
spaces. Passing "-s read-only" and "-a on-request" as single arguments --
which codex rejects as an unexpected argument -- passed the test. NUL
separation and an array comparison keep the boundaries the assertion is
about.

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

---------

Co-authored-by: Omabot <omabot@omarchy.org>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-25 16:07:17 +02:00
Helmut Januschka b91180a808 Skip Chromium EULA on first run 2026-08-25 14:48:44 +02:00
bastidotnet e53548fae2 Harden the test's temp-file handling against a symlink race
The /tmp-fallback case did check-then-create on a fixed /tmp name, a
TOCTOU/symlink race, and the EXIT trap only cleaned $TMPDIR. Create the decoy
atomically with noclobber (O_EXCL) so it refuses to overwrite an existing file
or follow a symlink at that path, and remove it on exit only when this test
created it. The fixed path is required (it is exactly the path the old code
would form), so a random mktemp name cannot replace it. Addresses the Copilot
review on #8198; the wrapper fix is unchanged.
2026-08-25 13:51:39 +02:00
bastidotnet fe56d68e90 Validate the cached Apple-display device path before use
The cached device path was trusted for merely existing, not for being a hiddev node, and fell back to a predictable /tmp path when XDG_RUNTIME_DIR was unset. Validate the cache shape (hiddev char device) and cache only under the user-private runtime dir; asdcontrol already gates non-Apple devices downstream, so this is defense-in-depth in the layer Omarchy owns.
2026-08-25 13:24:00 +02:00
23dab9ec4d [Security] Stop the FIDO2 setup staging its authfile at a predictable /tmp path (#7904)
* [Security] Stop the FIDO2 setup staging its authfile at a predictable /tmp path

pamu2fcfg wrote to /tmp/fido2 and the registration was then moved into place
with `sudo mv`. Any other local user can pre-create /tmp/fido2, and rename(2)
does not dereference the final component, so the privileged move installed the
attacker's symlink itself as pam_u2f's global authfile -- a file consulted by
`sufficient` lines in /etc/pam.d/sudo and /etc/pam.d/polkit-1.

The same move also carried the staged file's ownership into /etc, so on every
install to date /etc/fido2/fido2 is owned by the invoking user at mode 0644.
That needs no attacker: anything running as that uid can add its own credential
and satisfy the machine's sudo prompt without root.

Stage under mktemp and hand the bytes to `install` instead, so the authfile is
always a fresh root-owned regular file rather than an inode a non-root user
still controls. Guard the already-registered check with -L, which -f would
otherwise follow, and reject a symlinked /etc/fido2 in the remove path for the
same reason. A migration takes ownership of authfiles left behind by the old
code; it reports a symlink rather than repairing one, since chown would follow
it and removing it would strip sudo from anyone whose only credential is the
token.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6WagC5iUigCGoK1tQeFz

* Install the FIDO2 authfile with -T and refuse a non-regular path

`install SRC DEST` without -T treats an existing directory at DEST as a
destination directory: it drops the credential inside as
/etc/fido2/fido2/tmp.XXXX, exits 0, and setup reports a successful registration
while PAM goes on reading a path that is not a file. -T makes that an error.

The already-registered check has the same blind spot from the other side. -f
follows symlinks, so it reads a symlinked authfile as a registration and leaves
it in place, and is false for a directory, so it tries to register over one.
Only a regular file is a pam_u2f authfile; anything else is now refused with the
same advice to remove it and set FIDO2 up again.

The test deleted every staged path that fell outside its scratch directory,
taking the path from the script under test and already resolved through any
symlink -- so a script staging through one would have named a file of the user's
and had it unlinked. It now unlinks only a file its own stub wrote into.

On a machine that already has /etc/fido2/fido2 the staging assertions cannot run
at all, and the file used to pass without exercising one of them. That branch now
asserts what the host state promises instead: a regular authfile still has to be
recognised as a registration and left alone, and anything else has to be refused.

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

* Replace the FIDO2 authfile inode rather than chowning it in place

Permission is checked at open(2), not at write(2), so a descriptor the
registering user opened on the authfile while it was still theirs stays writable
through chown and chmod alike. pam_u2f resolves /etc/fido2/fido2 to that same
inode, so the repair left the account it authenticates able to append a
credential it controls -- the exact state the migration exists to end, now
recorded as migrated and never revisited.

Installing a fresh root-owned copy and renaming it over the path leaves any such
descriptor writing to a file nothing reads. Credit to #7703, which reached the
same conclusion independently. An interrupted run heals: the staged copy is
root-owned 600 and inert, no marker is written, and the next run replaces it.

A directory or device at the authfile path is no more ours to rewrite than a
symlink is, and chmod 600 on a directory would only make it untraversable, so
both are now reported rather than repaired.

The repair had no test, because it names an absolute path no unprivileged suite
can write. It is exercised through a scratch copy with that one literal
retargeted, rather than by reading the path from the environment: the migration
hands `install` and `mv` root, and an operand the caller can choose is a
privileged write to anywhere. The copy is only as honest as the substitution, so
the test fails if the migration stops naming the path exactly once.

Covered: the no-op on a machine that never registered a key, which must not cost
a password prompt; the repair itself; the new inode; the absence of a staged copy
afterwards; a second account finding it done; and the symlink and non-regular
cases. Each assertion was checked against a mutation that defeats it -- notably a
repair with the right install call, mode, content and cleanup that writes through
the old inode, which only the inode assertion catches.

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

* Finish hardening FIDO2 authfile installation

* Guard the FIDO2 directory and the stage path the setup writes through

install -d follows a symlink at /etc/fido2 and applies the mode and ownership to whatever it points at, so the credential would be staged and published inside the link target and that directory silently reopened to root:root 755. The leaf guard above it only covered fido2 itself, and this is the same threat omarchy-remove-security-fido2 already names on its side.

mktemp's output is an operand for a privileged tee, chmod, mv and rm. The migration validates it before any of them run; the setup did not, so take only the name it asked for there too.

The suite was guarded on the host's own /etc/fido2/fido2 and exited early when one existed, which meant the staging assertions asserted nothing on exactly the machines that use FIDO2. Drive a retargeted copy the way the migration suite already does, so every branch is a fixture and all of them run everywhere.

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

* Stop the FIDO2 migration recording a repair it never made

omarchy-migrate writes the per-user completion marker on any zero exit, so the two states this migration cannot repair got one line in the update terminal and were then silenced for good: no login notice, no re-run, the migration recorded as done having repaired nothing. Those are precisely the machines where the authfile may already be under someone else's control, so raise them through omarchy-notification-send as well, where they outlive the scrollback. Delivery is best-effort: a machine with no user bus or no notification server must not abort the migration and take every later one with it.

The early exit had the same shape of problem. It read the authfile unprivileged, and the old setup created /etc/fido2 with `sudo mkdir -p`, which took the union of the caller's umask and sudoers' 0022 — so registering under `umask 077` left the directory mode 0700 with the user-owned authfile still inside. Absence and "cannot look" are the same answer to those tests, and the migration exited 0 and marked itself complete. Ask root whether a registration is actually behind an untraversable directory before reopening it, so an aborted setup that left an empty directory, or one an administrator keeps private, does not have its mode widened and its group and special bits discarded for a repair it does not need. A machine that never set FIDO2 up has no directory here and still reaches exit 0 without a password prompt.

The notification assertion checks argument shape rather than a substring of the command line. The glyph is a private-use codepoint, and losing it shifts every argument left: -g swallows the headline, the body becomes the title, and the message goes out with no description — which a substring match reads as a pass.

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

* Cover the FIDO2 removal's symlink guard

The -d to -e || -L change is load-bearing for the threat its own comment names — a dangling link at /etc/fido2 that -d reads as absent, left for the next setup to install an authfile through — and it was the one part of this work with no test behind it. Name the directory once so the suite can retarget a copy, the same seam the setup and migration suites use, and assert both halves: the link goes, and the directory it pointed at does not.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-25 12:09:17 +02:00
9285b19d6a [Security] Stop USB device names from being executed as Hyprland Lua (#8129)
* Stop device names from being executed as Hyprland Lua

Hyprland input-device and monitor names come from USB descriptors and
hyprctl output, so they are attacker-influenceable, yet the toggle and
monitor commands interpolated them straight into hyprctl eval and into
generated Lua that Hyprland re-executes on every reload. The input-device
toggle keys are bound with locked = true, so a malicious USB name reached
Lua code execution from the lock screen; a persisted disable made it run
on every start. This closes that class everywhere it appeared.

- The touchpad/touchscreen disable is now the device name in a plain-text
  sidecar file, read back by a packaged Lua module on reload, never a
  generated Lua file. hyprctl eval Lua-quotes the name and control
  characters are rejected outright.
- Dropped the shipped *-disabled.lua templates so nothing seeds a
  disabled state to /etc/skel, making the name file the single source of
  truth read from a hardcoded ~/.local/state to match the sibling tools.
- The reload loader excludes those two legacy filenames, so a leftover
  generated *-disabled.lua on a not-yet-migrated install can never be
  sourced as code again; a migration then recovers the device name from
  it and deletes it, sanitizing installs that ran the vulnerable version.
- All four monitor scripts (internal, mirror, clamshell, scaling) now
  validate an output name against a plain-connector-name pattern before
  writing it as Lua, closing the same latent pattern in the siblings.
- paths.lua treats a set-but-empty XDG_STATE_HOME as unset, matching the
  bash side so state is never read from the filesystem root.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0144ZDt44vtxjyF8j9Y88NrM

* Let a failing Lua assertion fail the test

lua discards the status of a chunk read from stdin, so a blown assert printed its traceback and still exited 0: the surrounding `set -euo pipefail` never fired and the following `pass` printed `ok`. Every Lua block in these two files was unenforced, including the assertion that a quoted `hyprctl eval` cannot reach `os.execute` and the negative control that proves the test can detect the injection at all. Passing the chunk as a script argument makes lua report the failure.

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

* Re-apply a recovered input-device disable to the running session

The package hook reloads Hyprland during `omarchy-update-system-pkgs`, before `omarchy-migrate` runs, and at that reload the generated Lua is already excluded while the name file does not exist yet — so a touchpad or touchscreen the user had switched off comes back on, and stays on until their next login. Reload once more once the name has been recovered, which is the same path a login already takes to read it.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Omarchybot <omabot@omarchy.org>
Co-authored-by: Codex XHigh <codex@openai.com>
2026-08-25 11:03:12 +02:00
4637735aa2 Pin trusted PATH in privileged DNS helper (#8172)
* Pin PATH to trusted dirs when omarchy-dns holds root

A dev link prepends a user-writable checkout bin/ to sudo's secure_path,
so the passwordless `omarchy-dns Cloudflare` sudoers rule lets root
resolve a bare helper (dirname, install, tee, nmcli, ...) out of that
checkout — turning checkout-write access into arbitrary root execution.
Pin PATH to trusted system directories once EUID is 0, leaving the
unprivileged wrapper phase free to locate sudo/pkexec on the caller's
PATH.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUWoHbBoKMjsjV6X3nu1H5

* Assert the trusted-PATH pin is gated on root, not merely present

The EUID assertion matched `(( EUID == 0 ))` anywhere in the file, and require_root has carried that exact test since long before the pin existed. Deleting the pin left the assertion passing, so it stood for nothing: a run with the pin neutered reached the behavioural probe with both greps green. Anchor on the unindented guard and require the pin to be the line it opens, which no other construct in the script satisfies.

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

* Skip the DNS trusted-PATH probe where user namespaces are unavailable

`fail` ends the file, so a sandbox or hardened kernel that refuses unprivileged user namespaces did not just lose the probe — it took the two elevation assertions below it down as well, reporting a product defect where there was only a missing capability. The non-graphical suites are meant to run on any machine and treat a skip as a passing test, the way require_compositor and plugin-add-test.sh already do. Gate the probe on the namespace it needs and say so when it is absent; the static checks above and the elevation checks below run either way.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-25 09:37:55 +02:00
68ab12f77d Share the git URL check, and refuse the transports Omarchy does not clone from (#8174)
* Share the git URL check between theme-install and plugin-add

Both commands clone a URL a stranger can choose, and each carried its own copy of the rule that refuses a git option or a `<helper>::<address>` transport helper before cloning. Two copies of a security check drift: the second one arrived four months after the first, and only because someone went looking for it.

The rule now lives in omarchy-git-url-check and the callers ask it. Its absence refuses the URL rather than waving it through, since the callers read a non-zero status as a refusal and a missing command exits 127.

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

* Refuse a git URL naming a transport Omarchy does not clone from

`<helper>::<address>` is only one of the two ways a URL reaches a remote helper. git also resolves git-remote-<scheme> for `<scheme>://<address>` whenever the scheme is not one it connects itself, so `ext::sh -c id` and `ext://sh -c id` arrive at the same helper while only the first was refused.

That shape cannot be refused outright, because it is also how every legitimate URL arrives, so the scheme is checked against the transports git still connects itself. `git+ssh` and `ssh+git` are on that list: they are spelled like a helper and read as plain ssh, and leaving them off would refuse a URL that clones today. `ext` and `fd` are off it deliberately -- git ships a helper for each, and `ext` runs whatever command the URL carries.

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

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-25 09:10:20 +02:00
David Heinemeier HanssonandClaude Opus 5 12c350e404 Match the Windows VM title regardless of the launcher's shell quoting
The new assertion pinned /title:"Windows VM - Omarchy" with the quote sitting
immediately after the colon. That quote is incidental shell syntax, not the
title the Hyprland rule matches on. Open PR #7902 moves the RDP arguments into
an array, where the same flag reads "/title:Windows VM - Omarchy", so the
assertion would fail for whichever of the two pull requests merged second.

Allow an optional quote after the colon. The assertion still fails if the title
itself drifts, in either direction, which is the coupling it exists to protect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 08:45:50 +02:00
Basti 30471bf35a Guard plugin-add against git transport-helper URLs (match theme-install) (#8067)
* Guard plugin-add against git transport-helper URLs

omarchy-plugin-add cloned a user-supplied git URL without the
transport-helper guard that omarchy-theme-install already applies
(added in #7884, which did not touch plugin-add). Port that guard
(reject ext::/fd:: and leading-dash forms, keep https/ssh/scp-style
incl. IPv6) and add a regression test. Stock systems are unaffected
(git default protocol.ext.allow=never); this removes the silent
dependency on that default and aligns the two install paths.

* Test the plugin-add guard's leading-dash arm via the gum input path

The prior leading-dash cases only exercised the argv option parser, not
the guard (removing the guard's -* arm left them green). Drive a dash
value through the interactive gum prompt under a pty so the post-input
guard is actually covered; skip cleanly where util-linux script is
unavailable.
2026-08-25 08:29:48 +02:00
Jason Wall 8bee78bc63 Keep the Windows VM display fully opaque 2026-08-24 22:11:16 -07:00
Akshar Patel 597f57a198 Allow Resolve Voiceover to release focus 2026-08-24 22:00:42 -04:00
Sameer Pokharel b86d4505c1 Renumber the README manual TOC after Extra themes was dropped (#8089)
Dropping manual/43-extra-themes.md shifted every chapter after it down
by one. The TOC rode along on the old numbers, so its last ten links
404.
2026-08-24 21:59:17 +02:00
06a3dbca42 Offer to reboot when toggling sudoless Docker; show only the relevant menu entry (#8098)
* Offer to reboot when toggling sudoless Docker; show only the relevant menu entry

Group membership only takes effect on a fresh session, and in practice a logout
or newgrp isn't enough — only a reboot reliably applies it. So the setup/remove
commands now flag the reboot and offer to do it now with a gum confirm (like the
GPU toggle), and the notices say "after a reboot" instead of pointing at logout
or newgrp. The existing-user migration passes OMARCHY_DEFER_REBOOT so it does not
prompt mid-update — omarchy-update-restart still handles the reboot once the whole
update finishes.

The Setup > Security menu also showed Sudoless Docker under both Setup and
Remove. Condition the Setup entry on the group being absent (Remove already
conditions on it being present), so only the applicable one appears.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gb7x6poap4hGCndPx5qt5T

* Ask omarchy-sudo-docker whether Docker needs sudo

Every place that chooses between talking to Docker directly and elevating was
testing group membership by hand, and the menu guards tested the wrong thing:
they read the running session's groups, which do not change until the reboot,
so after enabling sudoless Docker the menu still offered Setup — the one action
that could no longer do anything — while Remove stayed hidden.

Add omarchy-sudo-docker as the single answer, with the two questions that
actually differ in that window. By default it asks whether this session can
reach the socket, which is what decides if a command must elevate: lazydocker
and the Windows VM keep prompting until the reboot lands. With --configured it
asks whether the account is set up for sudoless Docker, which is what the menu
and the toggles need, so the menu switches to the action that can change state
as soon as the group is written.

Also correct a comment: nothing surfaces reboot-required in the bar; it is
omarchy-update-restart that reads it during a later update.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gb7x6poap4hGCndPx5qt5T

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 21:58:27 +02:00
David Heinemeier HanssonandClaude Opus 5 5d3299fb94 Wait for the keypress ourselves instead of asking gum to (#8082)
* Wait for the keypress ourselves instead of asking gum to

gum 2.0 runs a spun command without the terminal attached, so the
`gum spin -- read -n 1` that held the presentation terminal open returned
at once. Every menu command that ended in a failure took its window down
with it before the error could be read, which is how a failed update
looked like a terminal that just quit.

Read the key directly. gum's own terminal query replies are still sitting
on the tty when the spinner stops, so drain those first or they answer the
prompt on the user's behalf.

The green dot reads better than the globe did, so the provisioning notice
uses it too and drops its spinner along the way.

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

* Ask the terminal for itself before prompting on it

The /dev/tty node is there whether or not a terminal is behind it, so the
existence check passed on a headless run and left both reads failing with
"No such device or address". Open it instead.

Prompt on the terminal too, rather than stdout: a caller that redirects us
was sending the prompt to a file while the read waited on the terminal,
which looks like a hang with no instruction on screen.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 19:46:02 +02:00
1565919c87 Flag a reboot when the docker group changes (#8080)
Group membership is fixed at login, so removing (or adding) the docker group
does not take effect in the running session. The existing-user migration and the
Setup > Security toggles now call `omarchy-state set reboot-required`, so
omarchy-update-restart prompts for the reboot that actually applies the change
(and the bar shows it pending). A plain log out and back in still works.

The migration test now exercises the real removal command and omarchy-state
rather than a stub, asserting the reboot flag is set on removal and left alone
when the user is already out of the group.


Claude-Session: https://claude.ai/code/session_01Gb7x6poap4hGCndPx5qt5T

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 19:32:37 +02:00
b5ded31e2f Don't put the user in the docker group; make it opt-in (#8056)
* Don't put the user in the docker group; make it opt-in

The docker group is root-equivalent: anything in it can `docker run -v /:/host`
and rewrite the host as root with no password. On a single-user box that's not
an escalation (the owner is already a wheel/sudo user), but it hands any code
running as the user — a rogue plugin, a poisoned dependency — a silent, headless,
passwordless path to root that sudo's password prompt would otherwise gate.

Stop granting the docker group by default. The daemon still runs (docker.socket);
the Docker TUI and the Windows VM reach it through a polkit prompt, and the plain
`docker` CLI runs under sudo. Sudoless Docker is a warned opt-in via
Setup > Security (omarchy-setup-security-sudoless-docker).

No automatic path may re-grant it: install and first-boot provisioning never
record or apply the group (provisioning also filters a docker line left in an
older factory snapshot), and the Quattro upgrade no longer adds it.

The Windows VM keeps needing the root daemon for a privileged container (KVM,
NET_ADMIN), so it is reworked to run without the group and without becoming a new
way in:

- The compose lives in a root-owned dir and is only written by an elevated,
  input-validated writer. A root-invoked bring-up must never consume a file a
  user-process could rewrite to bind-mount / into the guest — the old
  ~/.config/windows compose was exactly that. Volume paths are rebuilt from
  $HOME on migration rather than trusted from the (user-writable) legacy file,
  path validation rejects traversal, and the privileged sub-action is checked
  against an allowlist before dispatch (a slash in it would otherwise run as a
  path).
- pkexec elevates a verified root-owned command path, not a PATH-resolved one,
  so an authorized prompt can't be redirected to an attacker's binary.
- The guest password is kept in a private 0600 per-user file for RDP instead of
  a world-readable compose, and a declined authorization is reported as such,
  never as a completed stop.

Existing installs auto-migrate the VM (no redownload) and refresh the stale
Docker launcher entry.

🤖 Generated by Opus 4.8 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <codex@openai.com>
Claude-Session: https://claude.ai/code/session_01Gb7x6poap4hGCndPx5qt5T

* Migrate existing installs off the docker group

The default flip only reaches new installs; existing users keep their docker
group membership and stay exposed. Extend the migration that already refreshes
the Docker launcher to also remove the current user from the group when present,
reusing omarchy-remove-security-sudoless-docker so there is one source of truth
for the change and its notice. It takes effect at next login (the current
session keeps working), and passwordless docker can be turned back on from
Setup > Security > Sudoless Docker.

Migrations run with sudo available — during `omarchy update`, or in the terminal
the pending-migrations notification opens — so the privileged removal does not
prompt at an unattended login. The no-op path (already out of the group) needs
no privilege.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gb7x6poap4hGCndPx5qt5T

* Refuse symlinked VM mount sources; correct the docker CLI docs

Review follow-ups.

valid_path keeps a traversal string (/./, //, ..) out of the compose, but it is
a string check: a symlink planted at ~/.windows or ~/Windows redirects the
privileged bind mount exactly as traversal would, because docker follows it. So
verify the mount sources as root immediately before bringing the VM up — refuse
a source that is a symlink or resolves through one — which is where the string
check cannot help. A missing source stays fine (docker creates a plain dir).

Also correct the development-tools manual: the CLI is not transparently elevated
(there is no docker wrapper and `d` is still plain docker), so say plainly that
docker on the command line takes `sudo` until sudoless Docker is enabled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gb7x6poap4hGCndPx5qt5T

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Codex XHigh <codex@openai.com>
2026-08-24 18:56:00 +02:00
Ryan Hughes e66c27f1e7 Require signed packages from the Omarchy repository 2026-08-24 12:53:24 -04:00
Ryan Hughes 5afc9e1495 Revert "Require signed packages from the Omarchy repository"
Reverts 39cffb8f, which landed on quattro through an accidental direct
push. The change returns for review as a pull request.
2026-08-24 12:53:17 -04:00
Ryan Hughes 39cffb8f51 Require signed packages from the Omarchy repository 2026-08-24 12:37:46 -04:00
4baae6bf2a Draw text as ASCII art in the Omarchy logo font (#8037)
* Add omarchy-ascii for drawing text in the logo font

Renders text as ASCII art in Delta Corps Priest 1, the FIGlet font the Omarchy wordmark itself is drawn in, so branding art can be words rather than a picture. The font is embedded in the script and the layout is done in awk, so the command adds nothing to the default package set.

The layout runs on one-byte stand-ins for the five block characters the font draws with. Column arithmetic over the characters themselves counts bytes in one locale and characters in another, and the stand-ins keep length() and substr() counting columns either way.

Delta Corps Priest 1 carries letters and spaces only, and every mirror of it ships the same file with the digit and punctuation glyphs empty. Anything else is dropped and named on stderr, and text with nothing drawable at all exits 1 rather than printing silence.

🤖 Generated by Opus 5 in Claude Code.

* Correct what the renderer did with input it could not draw

The route never ran on piped text. The metadata declared `<text...>` as required, so `omarchy ascii` with nothing on the command line resolved to the router's help while `omarchy-ascii` run directly worked, which is why the tests missed it: they all called the binary. The argument is optional now, and a test goes through the route.

Text reached awk as a command-line variable, where awk reads backslash escapes of its own, so `omarchy ascii 'A\nB'` drew two blocks instead of naming the backslash as a character the font lacks. A text longer than the argument list could not be passed at all. It arrives as awk's input now, with the font on a descriptor of its own.

A line with nothing drawable printed nothing at all, so a blank line between two words closed the gap up rather than keeping it. Every line draws its block now, blank ones included, which is what figlet does with a newline.

Placing a glyph scanned and copied the whole width of the art so far, costing the square of the line's length: four thousand characters took forty-six seconds. A row is now held without its trailing blanks, counted separately instead, so a glyph costs its own width and those four thousand characters take a tenth of a second.

A skipped control character was named on stderr by writing it out, which sends it to the terminal as a control character; those are named by code now. An unknown option was drawn as art rather than refused, so a mistyped `--width 40` quietly rendered the word "width".

figlet.c trims the column of blanks that every row of an `M` shares when it is the first glyph on a line, and figlet.js keeps it. asciiart.eu runs figlet.js, so the rendering follows figlet.js and a test pins that `M`, because the wordmark alone does not catch the difference and the next reader would have no way to tell the choice from an oversight.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Codex XHigh <codex@openai.com>

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Codex XHigh <codex@openai.com>
2026-08-24 17:58:52 +02:00
d99d4fc6de Remove the sudo lockout reset command (#8046)
Nothing in the repository called omarchy-sudo-reset, and its one line interpolated an environment-supplied $USER into a string handed to a root shell: `su -c "faillock --reset --user $USER"`. $USER is an environment variable rather than a kernel-supplied identity, so whatever set it before the command ran chose the rest of what root's shell executed. That is not a way past PAM on its own — su still has to authenticate — but the installer sets root's password to the user's own, so the prompt this raises is one the user answers by habit.

It bought little for that. Omarchy sets `deny=10 unlock_time=120` in /etc/pam.d/system-auth and in the lock screen's PAM stack, so a lockout takes ten wrong passwords to reach and clears itself two minutes later, and the manual documents the root-TTY reset for anyone who would rather not wait.

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-24 15:49:17 +02:00
OmarchybotandClaude Opus 5 20400badda Stop a psmouse quirk from failing every install (#7236)
* Stop a psmouse quirk from failing every install

install/hardware/fix-synaptic-touchpad.sh calls modprobe against the running
kernel. Under 4.0 the only thing that runs it is the ISO finalizer, inside
arch-chroot, where uname -r still names the live ISO's kernel while /lib/modules
holds the target's. The live ISO always boots linux-t2 and the configurator
gives every machine that is not a T2 Mac stock linux, so those two never match:
modprobe exits 1 with "Module psmouse not found in directory /lib/modules/<live
kernel>". run_logged returns that status and omarchy-apply-hardware runs under
set -euo pipefail, so a fresh install stops on the first machine with a device
named "synaptics" and no psmouse loaded -- reported from a ThinkPad in #6985,
but nothing about it is Lenovo-specific.

Skip the load when the running kernel's modules are not reachable, and warn
instead of failing when modprobe declines for any other reason. An optional
touchpad improvement should never be able to halt an install.

This does not make InterTouch reach the installed system: a module loaded into
the live kernel is gone at reboot, so on 4.0 this script has never applied
anything to an installed machine. Persisting it means writing options psmouse
synaptics_intertouch=1 to /etc/modprobe.d, which forces the SMBus transport past
the kernel's own allowlist on any touchpad merely named "synaptics" in
/proc/bus/input/devices. That is a hardware-behaviour change on a wide class of
machines, so it is left for a maintainer to decide separately.

Fixes #6985

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

* Ask modprobe whether psmouse resolves rather than guessing at /lib/modules

The reachability check ran through OMARCHY_SYNAPTIC_MODULES_DIR, an override
modprobe itself never saw: it decided whether the load was attempted but could
not change where modprobe looked, so the guard and the load consulted different
places and the seam read as though it configured module lookup. modprobe -qn
answers the same question directly -- it resolves psmouse against the running
kernel without loading it -- so the guard and the load now agree by
construction and the override goes away. The arch-chroot case that broke
installs is still skipped silently, for the same reason it always was: the live
kernel's modules are not the ones on disk.

Inline the remaining /proc/bus/input/devices override at its only use. These
leaves are sourced one after another into a single shell, so a variable left at
the top level outlives the script that set it.

Pin the wiring assertion to the run_logged call instead of any mention of the
path. A commented-out line satisfied the old grep, so the test could pass with
the quirk no longer running at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013L5zwTiZ2CsgazyxXBiPa8

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 15:33:03 +02:00
David Heinemeier HanssonandClaude Opus 5 535d8f3485 Regenerate mise wrappers that still print mise's output to stdout (#8041)
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>
2026-08-24 14:36:09 +02:00
David Helmus 44b00a4e80 test: avoid mise shim recursion 2026-08-24 08:52:25 +02:00
Taksh 7c896d3521 Keep a web app name out of the launcher's directory structure
The app name becomes a filename, and omarchy-webapp-install ran
`mkdir -p "$(dirname "$DESKTOP_FILE")"` over it, so every slash turned into a
directory level. Typing a URL into the Name field -- the reported way in --
wrote the launcher to
`~/.local/share/applications/http:/127.0.0.1:4000/.desktop`.

Removal could then never reach it. The picker lists the file but displays a
name derived from the path, and the removal rebuilt a flat
`$DESKTOP_DIR/$APP_NAME.desktop` from that name, so `rm -f` deleted nothing and
the app stayed in the launcher with no error.

Refuse a name containing a slash rather than silently renaming what the user
typed, and delete the file the scan actually found instead of a path rebuilt
from its display name. The second half also clears up whatever earlier versions
nested, which a reconstructed path cannot address.
2026-08-24 07:16:53 +05:30
Ryan Hughes 43bfe9b9d8 Merge pull request #7926 from basecamp/harden-notification-exec-argv
Run notification click actions as safe argv
2026-08-23 19:51:57 -04:00
Ryan Hughes 3b0d9b5734 Merge remote-tracking branch 'origin/quattro' into harden-notification-exec-argv
# Conflicts:
#	test/shell.d/notification-send-test.sh
2026-08-23 18:31:42 -04:00
Ryan Hughes 8f245e59dc Restore notify-send option parity and prove legacy fail-closed
The direct-Notify rewrite dropped notify-send options that callers rely on, which
the momus review caught: omarchy-display-text-size uses -r/-p to refresh one
toast in place, and the acceptance suite uses the --expire-time=15000 equals
form. Re-add -r/--replace-id (replaces_id), -p/--print-id (emit the returned id),
and the --flag=value form for every long option; a dash-leading description like
"-50% off" is now kept as body text rather than erroring, and --exec "" is
rejected.

Add a fixture proving the deliberate upgrade behavior: a popup persisted by a
pre-upgrade shell with a legacy `exec` shell string restores with an inert click
(execArgv empty, the old string never run) rather than executing it.
2026-08-23 17:18:58 -04:00
Ryan Hughes e3729a385b Send notifications via the Notify D-Bus method, never notify-send
omarchy-notification-send now calls org.freedesktop.Notifications.Notify
directly with `busctl --user`, instead of shelling out to notify-send. Each
value is one typed D-Bus parameter, so there is no argv/option layer that could
reinterpret a relayed headline like `--hint=…` or `-rf` as an option or a hint:
the summary and body are strings, and omarchy-exec-argv is built only from
--exec. A leading `--` keeps busctl's own getopt from reading a dash-leading
value as a busctl option.

Map -i to app_icon, -t to expire_timeout, and urgency to the byte hint; unknown
options are now a hard error rather than a silent pass-through. Route the unused
hypr o.notify helper and the sample hooks through the wrapper too, and tighten
the bin-style test so nothing under bin/ may call notify-send. The test stubs
busctl and trips if notify-send is invoked.
2026-08-23 17:02:29 -04:00
Ryan Hughes be63983d16 Describe parseExecArgv as a structural check, not intent
The comment claimed the validation kept a hostile hint from reaching a shell,
but it is purely structural: a well-formed ["bash","-c",…] passes. Say so,
and point at the separate sender-trust boundary.
2026-08-23 16:47:13 -04:00
7e469f962d Let a received Taildrop file wait to be answered (#7953)
* Let a received Taildrop file wait to be answered

A delivery can land hours after it was sent, and the toast announcing it was expiring after five seconds -- so a file that arrived while nobody was at the machine was gone from the screen before anyone could click it open. Critical urgency is what the shell reads as a popup that lives until it is clicked or dismissed, the same thing omarchy-crash-watch uses to keep its click-to-diagnose toast around.

The wrapper takes options after the headline and description as well as before, which is how this argument list is built. That path had no test, and it fails quietly rather than loudly: the wrapper appends its own default urgency last, so an urgency it stopped parsing would reach notify-send as `-u critical ... -u low` and the toast would go back to expiring.

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

* Say it in the commit message, not above the code

`-u critical` next to a line that builds a notification says what it does, and the five lines explaining why it is there were a recap of the change rather than something the code could not say. The reasoning stays where it belongs, in the commit that made the change.

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>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-23 22:12:07 +02:00
Claude Opus 5 (1M context)andCodex XHigh 1b15120d27 Keep relayed text out of notify-send's option parser
The argv click command closed injection through the hint's value, but the
sender still handed the headline and description to notify-send bare. A
value beginning with a dash is parsed there as flags, and one shaped like
`--hint=string:omarchy-exec-argv:[...]` is read as a hint of its own --
libnotify keys hints in a hash table, so the later of two replaces the
earlier and a forged headline outranks the vector --exec built.

That is reachable without any --exec in sight: omarchy-tailscale-send
passes a single file's basename verbatim as the description, so a file
named like the hint gives its click action to whoever chose the name.

Put the headline and description behind a `--` so notify-send reads them
as text, and refuse any pass-through word carrying omarchy-exec-argv --
--exec is the only thing that may build a click command.

Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-23 21:44:06 +02:00
Ryan Hughes 21cbbf8194 Recognize --exec only after the positionals
A greedy pre-scan matched the first --exec token anywhere on the line, so an
untrusted headline or description literally equal to "--exec" could be mistaken
for the delimiter. It always failed closed (the following token became a
dash-option argv[0] that parseExecArgv rejects), but it was a latent footgun
surfaced by an adversarial review.

Detect --exec only in the trailing-option loop, after the headline/description
positionals are captured. A headline that is the string "--exec" is now kept as
text and the real trailing --exec still wins. The residual — a description
exactly equal to "--exec" losing its click action — is an inherent, harmless CLI
ambiguity for a value identical to the delimiter.
2026-08-23 14:41:56 -04:00
Ryan Hughes bf2013e6f3 Make --exec take the command as rest-of-line words
Replace --exec-arg with an ergonomic --exec that consumes the rest of the line
as the click command. The caller's shell tokenizes the words into discrete
arguments before the tool sees them, and the shell runs them as positional
parameters (never a re-parsed string), so safety is identical to the argv form
while the call sites read naturally: `--exec omarchy toggle something`.

Crucially the tool never splits a string itself — a single quoted whole-command
argument is rejected and points at the unquoted form, because whitespace-
splitting a string hands argument boundaries to whoever controls its content
(the injection we are avoiding). --exec must come last; migrate every caller.
2026-08-23 14:26:25 -04:00
David Heinemeier HanssonandClaude Opus 5 f4f3d4c71a Add plan for finishing Sunshine/Moonlight into a remote desktop
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5FKtT9KPZcx3ksyfPeXCv
2026-08-23 20:02:02 +02:00
Ryan Hughes eb988b42e6 Remove --exec entirely; --exec-arg is the only click-command form
A free-form shell-string --exec sitting next to the safe --exec-arg is a
standing invitation for the next caller to interpolate untrusted data and
reintroduce the RCE. Remove it: omarchy-notification-send --exec now errors and
points at --exec-arg, and the shell drops the omarchy-exec string hint and its
bash -lc execution path, leaving only the argv path.

Migrate the remaining string callers (the first-run invitation hooks, wifi and
welcome prompts) to --exec-arg, and update their notification mocks. Trim the
verbose security comments added along the way.
2026-08-23 13:35:02 -04:00
Ryan Hughes d2fd2e11c6 Run argv click actions through a login shell as positional params
Quickshell.execDetached(argv) ran the click target with only the shell
process's stripped environment, so GUI actions like the screenshot editor
(tensaku-edit) — resolved on the login-shell PATH the old `bash -lc` string
exec provided — stopped launching on click.

Run the argv through `bash -lc 'exec "$@"'` instead: the script text is a
constant and the arguments are passed as positional parameters, which bash
expands without re-tokenizing or re-evaluating, so injection safety is intact
while PATH and session env match the old behavior exactly.
2026-08-23 12:25:48 -04:00
Ryan Hughes 07443f3970 Run notification click actions as argv, not shell strings
The click action of a notification was a free-form shell string run through
`bash -lc`, safe only when every sender shell-quoted every interpolated value
perfectly. One slip is RCE: a hostile yt-dlp video title forged an output
record and injected an mpv option into the click command (mehmetince.net RCE,
partially addressed by #7847).

Add a parameterized transport: omarchy-notification-send gains --exec-arg
(repeatable), encoding a JSON argv into the omarchy-exec-argv hint. The shell
runs it with Quickshell.execDetached(argv) and no shell, so data an attacker
controls is only ever one argument and can never be reparsed as a command. The
shell fails closed on a malformed argv hint.

The legacy free-form --exec string is retained but honored only from Omarchy's
own omarchy-action toasts, and deprecated. Migrate all in-repo callers
(screenshot, screen recording, taildrop receive, migrate-notify, crash-watch,
yt-dlp host) to --exec-arg. Update docs and tests.
2026-08-23 12:00:03 -04:00
OmarchybotandCodex XHigh ef6d9e6605 Stop an installed theme from running code (#7884)
* Stop an installed theme from shipping code

`omarchy theme install <url>` clones a stranger's git repository into ~/.config/omarchy/themes, and omarchy-theme-set then copied that whole directory into the staged theme. Most of the files in a staged theme are code rather than colour: Hyprland requires hyprland.lua and gum_env.lua from it at login, Neovim loads neovim.lua at startup, and alacritty.toml, kitty.conf, foot.ini and ghostty.conf each name the program the terminal launches. Installing a theme was the same act as running its author's code, and nothing on disk distinguishes an installed theme from one the user wrote.

Stage only what a theme needs in order to be a theme: colors.toml, light.mode, the preview and unlock images, and image files under backgrounds/. Everything else is ignored, named on stderr, and generated from default/themed/*.tpl instead. Symlinks are never followed, because in an untrusted theme they point wherever the author chose. A theme older than colors.toml keeps its palette: its alacritty.toml is read for colours in a scratch directory and only the resulting colors.toml is staged, so the terminal config never lands.

The filter belongs in omarchy-theme-set rather than in omarchy-theme-install because staging is the choke point. It also covers themes installed before this change, themes copied in by hand, and files a theme gains later through `omarchy theme update`.

First-party themes under $OMARCHY_PATH/themes are unaffected. Per-theme overrides of a generated file are no longer available to user themes; the template at ~/.config/omarchy/themed/<file>.tpl replaces that, and icons.theme is the one setting with no replacement.

🤖 Generated by Opus 5 in Claude Code.

* Stop a theme URL or name being read as an option or a path

Three paths in the theme commands took an attacker-shaped string straight into git, into basename, or into rm.

`git clone "$REPO_URL"` passes the URL as the first positional argument, so a URL beginning with a dash is parsed as an option instead and the destination path becomes what git tries to clone. Pass `--` before the URL so a URL is always a URL. git also treats `<helper>::<address>` as a remote helper to run; git's own protocol.allow default already refuses `ext::`, so rejecting that shape here is a second line rather than the fix, and it keeps holding if that default ever moves. The helper name is a bare word at the very start of the URL, which is what the guard matches: an scp-style IPv6 host such as git@[2001:db8::1]:org/repo.git carries `::` of its own and still clones.

`basename "$REPO_PATH" .git` has the same problem one step later, after the scp-style prefix has been stripped: `host:-s/foo.git` leaves basename reading `-s` as an option and returning `.git` as the theme name. Take the name with `--`.

That name is then joined into a path that is about to be `rm -rf`'d, so a repo whose basename came out as `..` would take ~/.config/omarchy with it. omarchy-theme-remove had the same shape from its own argument, and omarchy-theme-set's sed/tr normalization does not stop a name containing a slash. Reject empty, anything starting with a dot, and anything containing `/` in all three, before the name reaches a path.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Codex XHigh <codex@openai.com>

* Re-stage the current theme for installs that already applied one

Dropping a theme's code at staging time only takes effect the next time a theme is staged. An install that already applied an extra theme keeps that theme's hyprland.lua, gum_env.lua, neovim.lua and terminal configs in ~/.local/state/omarchy/current/theme, which Hyprland requires at login and the terminals include at launch, and nothing forces a theme change — so for those installs the fix would arrive whenever the user next happened to switch themes, which may be never.

Re-stage once through omarchy-theme-refresh. First-party themes stage identically, so the cost for everyone else is a single retint during an update they are already running.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Codex XHigh <codex@openai.com>

* Stop a theme's unlock image republishing a file it points at

omarchy-plymouth-set-by-theme reads unlock.png straight out of ~/.config/omarchy/themes, which is an installed theme's own directory and outside the staging filter, and hands the path to omarchy-plymouth-set. That path was copied twice into world-readable /usr/share — once by the user into the Plymouth theme, and once by `sudo cp` into the SDDM theme. A symlink there was followed both times, so a theme could name a file it cannot read and have root publish it.

Refuse a symlinked logo, and copy the staged logo to SDDM instead of rereading the caller's path as root. The staged copy is made by the user, so nothing privileged opens a path the caller chose.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Codex XHigh <codex@openai.com>

* Limit only what an installed theme could run

Two corrections to the rule this branch introduced, both narrowing it to what it was actually for.

It applied to every theme under ~/.config/omarchy/themes, which swept up themes the user wrote themselves. Their machine, their file: a theme they wrote is theirs to fill however they like, and Omarchy's own themes were never in scope. Only a theme that came from someone else needs limiting, and the repo already knows which those are — omarchy-theme-extras calls a theme with a `.git` directory an extra and a symlink someone's working copy, because that is what `omarchy theme install` leaves behind when it clones. Use the same test.

It was also an allowlist, which dropped files that carry nothing but colour and left theme authors worse off for no gain. Drop only what can run: any `*.lua`, since Hyprland requires a theme's hyprland.lua and gum_env.lua at login and Neovim loads neovim.lua at startup; the four terminal configs, since each names the program the terminal launches; and vscode.json, whose extension field reaches `code --install-extension` and a VS Code extension is arbitrary JavaScript. Everything else an installed theme ships is kept, so btop.theme, chromium.theme, helix.toml, icons.theme, keyboard.rgb and shell.toml go back to being the theme's to set.

Symlinks are still dropped, now at any depth rather than only where an allowlist happened to look.

A denylist is wrong the moment someone adds a template and does not think about it, so the decision is forced rather than remembered: the test fails on any default/themed/*.tpl whose output is recorded as neither code nor colour, and a new terminal or a new Lua-loading editor cannot be added without classifying it.

What this does not cover, and is written down in docs/theming.md rather than implied: a theme shipped as an archive and unpacked by hand looks exactly like one the user wrote. `omarchy theme install` only takes git URLs, so the supported path is always filtered, but this marks where a theme came from and is not a sandbox.

🤖 Generated by Opus 5 in Claude Code.

* Fix what the review found

Four things, all confirmed against the source before changing anything.

The migration failed permanently when the active theme had been removed. `omarchy theme remove` deletes the directory without repointing theme.name, so the name survives, the staged copy survives, and omarchy-theme-refresh exits 1 because neither source directory exists — leaving the migration pending forever and the stale staged Lua exactly where it was, which is the one thing it existed to remove. Seed the default theme in that case: there is nothing to re-stage from, and the removal should have left a working theme behind anyway.

The staging test skipped the strict-mode header that docs/testing.md makes the contract for every shell test. Adding it means the patterns that fail on purpose have to stop being bare `cmd && fail` compounds, which errexit reads as the script itself failing; the mutations were re-run afterwards to confirm the assertions still fire rather than the run dying early and looking like something else.

The guards in omarchy-theme-install and omarchy-theme-remove had no coverage — they were checked by hand and left that way. theme-install-guards-test.sh stubs git and the themes directory and proves an option-shaped URL, a transport helper, and a name that would climb out all stop before git or rm runs, that a dash inside the path no longer becomes a basename option, and that an ordinary URL still clones and applies.

The new docs/theming.md prose was hard-wrapped, which AGENTS.md forbids for docs/. Unwrapped. The rest of that file is wrapped from before and is left alone rather than churned through this change.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh and Copilot.

---------

Co-authored-by: Codex XHigh <codex@openai.com>
2026-08-23 16:43:31 +02:00
b71c60fe30 [Security] Stop a video title from becoming the Download Video play command (#7847)
* Stop a video title from becoming the Download Video play command

The host parsed yt-dlp's after_move line as title plus path, so a newline in page metadata could forge the path. Clicking the toast then handed that value to mpv as options. Print only the real file, ignore anything that is not inside the download dir, and invoke mpv with --.

* Refuse downloads whose video title contains control characters

The hoodie page still offered a real hidden clip, so yt-dlp saved it even after the play-action fix. A title with newlines is not a legitimate name; abort before the download and tell the user it was refused.

* Test the forged record in the order yt-dlp emits it

The records ran forged-first and good-last, so the assertion measured recovery after
bad records rather than preservation of an already-captured path when a forged record
arrives afterwards. That is the shape a hostile title actually produces, because a
title ending in a newline closes its own record and leaves the genuine path on a line
the loop ignores. As written the assertion passed with resolve_download_file replaced
by a no-op.

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

* Resolve the download path without dropping a trailing newline

Command substitution strips trailing newlines, so an in-directory symlink pointing at
a regular file whose name ends in one canonicalised to a different path -- which may
itself exist -- and that path then passed the containment check and reached ffmpeg and
the click command. Reading realpath's NUL-terminated output keeps the name intact, and
a resolved path carrying a control character is refused outright.

Not reachable through a yt-dlp download, since --restrict-filenames strips control
characters from the name it writes; it is the helper's contract that was wrong.

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

* Accept a download directory that resolves to /

realpath returns "/" for the root directory, which made the containment pattern "//*"
and rejected every file saved directly under it, so the host reported a failed download
after saving the file.

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

* Stop refusing a download because its title has control characters

The gate cannot tell a hostile title from a legitimate one. --print emits one line per
extracted video and --no-playlist does not collapse a multi_video result, so a page
holding two clips arrives as two titles separated by a newline and is refused exactly
like a forged record would be.

It also guaranteed nothing it was read as guaranteeing. The simulate run and the
download run are separate fetches, so a site is free to answer them differently, and
the check never constrained the metadata the download actually used.

What stands between a record and the click command is resolve_download_file, which is
untouched here. Leaving a check that refuses valid pages while securing nothing invites
the path validation to be relaxed later on the strength of it. A gate that would work
is possible -- --print '%(title)j' encodes each title as JSON on its own line, which
separates a newline in the metadata from a newline between videos -- but it belongs
with a use for the title rather than as a bare refusal.

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

* Disarm the legacy exec-before-download hook too

--no-exec clears the modern --exec map but leaves --exec-before-download stored
separately, and yt-dlp restores it as a before_dl postprocessor, so a hook configured
in the user's yt-dlp config still ran during the download this host drives.

The accompanying test runs download_url itself against stubbed tools. Everything else
in this file exercises the helpers in isolation, which left the invocation uncovered:
restoring the title to the record template, or dropping --no-exec or the trailing --,
passed every assertion here.

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

* Toast the page title again instead of the saved filename

Deriving the toast text from the sanitised filename cost the title it was meant to
show: "My Great Clip" arrived as "My_Great_Clip [My_Great_Clip]". The title is safe as
notification text -- it is an argv element, never part of a command -- so the only
question was getting it out of yt-dlp without reopening the record forgery.

It now comes from the download run, so it describes the file that was actually saved,
and it is printed as %(title)j. JSON-encoding is what makes that safe: a newline or tab
in page metadata becomes an escape sequence inside one quoted string rather than a
record boundary, so a title can no longer split itself across lines. The decoder keeps
only what precedes the first control character, refuses anything notify-send would read
as an option, and leaves the filename-derived title as the fallback when a page offers
nothing usable.

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

* Name the saved file after the page title

The download landed as "My_Great_Clip [My_Great_Clip].mp4" when the page called it
"My Great Clip". --restrict-filenames was carrying more weight than it earns here: it
folds spaces to underscores and strips non-ASCII, which is what mangles the name, and
it is not what keeps the record stream safe. yt-dlp removes control characters from a
filename either way -- a newline becomes a space, tabs and DEL and NUL are dropped --
so a path printed after the move is still only ever one line, which is the property
resolve_download_file depends on.

Dropping the [%(id)s] suffix is the other half of matching the title, and it trades
away the uniqueness that suffix bought: two videos sharing a title now share a name,
and yt-dlp skips a download whose file already exists, so the second one toasts as a
failure. Restoring the suffix is a one-line change if that trade is the wrong way
round.

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

---------

Co-authored-by: Omabot <omabot@omarchy.org>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-23 16:18:38 +02:00
David Heinemeier Hansson 19266119ea Clipboard history needs to be bigger! 2026-08-23 11:49:20 +02:00
OmarchybotandCodex XHigh 1cc5c72e2c Add Ori as a lazy-loaded agent and a default-agent choice (#7709)
Ori is OpenRouter's harness: `ori claude`, `ori codex` and `ori opencode` start those agents against OpenRouter's model catalogue, and `ori code` is Ori's own agent. That last one is what the default-agent entry launches, bare — Ori has no approval prompt to skip, so there is no "don't stop to ask" flag to pass it the way the other agents get one.

The package is `github:OpenRouterLabs/ori-releases`, because upstream ships prebuilt binaries as release assets and publishes nothing to npm. mise's `github` backend picks the right asset per platform and verifies GitHub's artifact attestations on the way in; `ubi` resolves the same release but is deprecated for removal in mise 2027.1.

The menu glyph at U+E909 is OpenRouter's own mark. Ori publishes no logo of its own and its product page renders that one, so there was no Ori-specific mark to prefer over it.

Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-23 11:44:23 +02:00
2c247e390e Stop the About logo moving when the animation starts (#7786)
* Ask the terminal whether the layout scrolled

Whether the logo is on the rows the frames address was decided by arithmetic: count the lines of a second fastfetch run, and animate if the window has one more. That holds only while the second run and the real one agree, and they need not — a module that appears between them, a wrap the measurement never sees, anything that makes the painted layout a row taller than the counted one. The screen then scrolls, the logo sits a row above where these frames draw it, and the animation paints a copy of the logo one row below the real one.

fastfetch has just painted when the question is asked, so the terminal already knows the answer: an unscrolled layout leaves the cursor one row past its last line. Ask for it, and keep the arithmetic for a terminal that will not say.

The reply has to be read without a command substitution around it, or the query goes into the substitution's own pipe instead of to the terminal, and what comes back is the escape rather than a row.

🤖 Generated by Opus 5 in Claude Code.

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

* Find the logo in the render instead of assuming where it is

The row and column the frames draw on came from the padding in this repo's fastfetch config. The config that runs is the one in /etc, and `omarchy dev link` says plainly that it does not replace files at fixed system paths — so the two can disagree, and nothing in the code would notice. A logo fastfetch drew on row 1 or row 5, redrawn on row 3, is a logo that jumps the moment the animation starts and stays where the animation put it.

fastfetch's own output is already being captured to measure the layout's height, so the logo can be found in it: take the longest line of the file as a landmark, look for it in the render, and turn where it was found back into where the logo starts. Padding of any size lands correctly, and a render that does not contain the file's own text is not this logo at all — a config that restyled it, a placeholder that was substituted — so there is nothing to animate and it is left alone.

That last part subsumes what the config-path check was guessing at. It is kept, because it also decides whether the window may be fitted, which happens before there is a terminal to measure anything in.

🤖 Generated by Opus 5 in Claude Code.

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

* Take the cursor query back out

Asking the terminal where the cursor came to rest was meant to see a scrolled layout rather than infer one. It cannot: a linefeed at the bottom margin scrolls the screen and leaves the cursor on the bottom row, so a render that fitted exactly and one that scrolled both answer with the same number. Measured on a real terminal — a 29-line layout in windows of 30, 29 and 28 rows answers 30, 29, 28. The guard therefore only ever agreed with the arithmetic it replaced, and only in the case the arithmetic already had right.

What it did add was a read on the same stdin the keypress that closes About arrives on. A key pressed while the query is outstanding is swallowed; a reply that arrives after the timeout is read by the next tick as the keypress, and the window closes on its own; typed text of the right shape is accepted as an answer, because nothing checked for the introducer; and the query is written before the read turns echo off, so a fast reply can be echoed onto the screen.

None of that buys anything the line count did not already give, so it goes.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

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

* Fit the About window with a little room to spare

The window was sized to exactly what was measured: the columns the widest module line needed, and one row past the layout's last line for the cursor. That measurement is taken once, and then the content goes on living. An uptime turns minutes into hours and hours into days, a version string grows, a module shows up after the next boot — and a window with nothing in hand clips at the right edge or scrolls the top padding away the moment any of it happens. A scrolled layout also moves the logo off the row it was drawn on, which is where a shifting logo comes from.

Two columns and a row, which is invisible on screen and enough for anything that ticks over. The remembered size in about.fit is keyed on the logo, so the first launch after this still opens at the old size and the fit corrects it there and then.

🤖 Generated by Opus 5 in Claude Code.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-22 20:18:14 +02:00
eca89f9518 Animate the About logo with a passing glint (#7768)
* Animate the About logo with a passing glint

fastfetch has no animation of its own, so the sweep is ours. Every frame is composed once up front and a tick writes one of them, repainting only the cells fastfetch drew the logo on, which holds 40fps for under 1% of a core and never reaches the module column six columns to its right.

The logo is handed back exactly as it arrived: frames carry the colour fastfetch drew it in rather than a plain reset, so between glints the window is byte-for-byte the one it was before this change. Where the logo on screen might not be the text in the file, it stays still rather than guess — a fastfetch config in any directory searched ahead of Omarchy's own, a logo built from $1 colour placeholders or from a tab fastfetch expands itself, a window too small for the layout to fit without scrolling, and a shell whose locale is counting bytes instead of characters.

A resize moves those cells, so a WINCH trap ends the sweep on the frame it happens rather than up to a poll later, which measured 10ms against 830ms. The polling stays as the backstop for a signal that arrived while it could not be taken.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

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

* Give the sheen a file of its own

The launcher was carrying two things: what a glint looks like, and what it is drawn over. Only the second is about fastfetch. bin/omarchy-logo-sheen now knows how to lean a band of light across an ASCII logo and nothing about About, and it is handed where the logo sits, what colour to give its cells back, and how much room it has. When a frame may be written stays with the launcher, because that is inseparable from how the window closes and resizes.

The tests split along the same seam, and the sheen's half no longer strips the launcher's tail to reach the code it tests.

🤖 Generated by Opus 5 in Claude Code.

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

* Leave a logo still when one character is not one cell

The frames slice the logo by character and the terminal draws it by column, so the sheen only puts a row back where it found it while those two agree. A double-width glyph, a combining mark and a joined emoji each break that, and a cut through one of them renders as something else — pushing the rest of the row right, into the module column, with nothing on screen to say the logo moved. A user can put any of the three in the logo by editing the text.

Comparing the row's character count against its width in columns is the assumption itself, so it is what gets checked, rather than a list of the glyph classes that happen to break it today.

🤖 Generated by Opus 5 in Claude Code.

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

* Size the About window from the layout fastfetch drew

The fit predicted the content height from the logo and the module column, taking the taller of the two. Once the logo is the taller one fastfetch writes a row more than that arithmetic expects, so every logo of 27 rows or more got a window one row short and scrolled its top padding away. The shipped logo is 26 rows, which is why it never showed.

Ask fastfetch how tall the layout came out instead of predicting it. That measurement was already being taken for the sheen, which refuses to animate a layout that scrolled — so a tall logo used to lose the glint as well as the padding row.

🤖 Generated by Opus 5 in Claude Code.

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

* Leave the logo still when NO_COLOR asks for none

fastfetch drops the logo's colour when it writes to a terminal that set NO_COLOR, but not when it writes to the substitution that measures it. The colour to hand those cells back in was therefore measured as green while the logo on screen had none, and the first glint would have left the whole logo green. It stops suppressing autowrap there too, so a narrow window soft-wraps and scrolls where the measurement cannot see it and the frames go on addressing rows that moved.

Both follow from animating a screen drawn in a mode the measurement did not reproduce. A glint is colour besides, which is the thing NO_COLOR asks for none of.
🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

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

* Read fastfetch's config paths whole, and do not read silence as an answer

Splitting each listed path on whitespace dropped everything after the first space, so a config under a home directory containing one was missed and the fit and the sheen went ahead against a layout neither had measured. The marker fastfetch prints beside the config it settled on is not part of the path either.

A listing that failed was also indistinguishable from one that found nothing, because the status of a process substitution is discarded, so a fastfetch that could not enumerate read as "no custom config". Fall back to the directory fastfetch has always looked in first rather than take silence for an answer.
🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

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

* Notice a resize that lands while the grid is being read

The sweep read the WINCH flag before the grid rather than after it. A signal arriving during the two command substitutions that read the grid is delivered only once they finish, so the flag was still false when it was tested, the grid still compared equal, and one more frame went out at coordinates the resize had already moved. Read the flag last, after the check it could have landed during.
🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

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

* Check that the render loop is what plays the sheen

Every builder the tests drive can be exercised while nothing on screen ever animates. Replacing the render loop's animated branch with the still one it replaced left all of them passing, so nothing was holding the animation onto the screen it belongs on.
🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

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

* Name the animation after the branding it animates

omarchy-logo-sheen sat in a group of its own, and a group whose only command is hidden still gets a header printed for it with nothing underneath. It belongs beside omarchy-branding-about, whose art it animates: the group already exists, the two halves of About branding are next to each other, and `omarchy branding about` still routes to the command rather than the helper now sharing its prefix.

The tests take the names of what they cover, the way the rest of the suite does.

🤖 Generated by Opus 5 in Claude Code.

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

* Fewer moving parts in the sheen

Four questions about a logo turned out to be one. Whether the shell is counting characters or bytes, whether a tab or an escape is in the line, whether a glyph is double-width or a combining mark or a joined emoji — each was asking whether one character is one cell, so that is the only thing asked now. It is also less strict in the one case that deserves it: plain ASCII art animates in a C locale, where the locale probe used to refuse everything.

One band of light instead of a white core inside a green halo, which is three colour spans a row rather than five and no helper to clamp four cuts with. Only the left cut needs clamping at all: a slice starting past the end of a line is already empty, while a negative offset would count from the end of it.

One loop instead of two, because an empty frame list plays nothing. A logo that cannot be animated now waits in the loop the animated one rests in rather than in a second copy of it, and the build that failed leaves no frames behind, since that loop plays whatever it finds.

The logo's colour comes out of one match rather than a loop eating one escape at a time.

🤖 Generated by Opus 5 in Claude Code.

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

* Say in the manual which art the glint leaves alone

The line promised the glint to any art in the file, and the code does not: art whose characters are not one column wide is left still on purpose, as is the whole screen for anyone keeping a fastfetch config of their own. Somebody reading the old line and seeing a still logo would take deliberate behaviour for a broken feature.

Name the condition rather than the list of glyph classes behind it, and say that both modes of Set From Image meet it, because that is the path almost everyone is on.

🤖 Generated by Opus 5 in Claude Code.

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

* Draw the band in a colour bold cannot collapse

A terminal can be told to render bold text in a brighter colour — foot's bold-text-in-bright, whose palette-based form brightens a bold regular colour into its bright counterpart. fastfetch draws the logo bold green, so under that setting the logo is already bright green, which is the colour the band was using: the glint came out the same green as the art and nothing appeared to happen at all. None of the four terminal configs set it, so it was waiting on whoever turned it on.

Bright white instead, because no regular colour brightens into it, and the band shows whatever the terminal does with bold. Narrower with it, since white against green needs less width to read than a second green did.

🤖 Generated by Opus 5 in Claude Code.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-22 17:19:23 +02:00
OmarchybotandCodex XHigh 07fccef41c Add Super + Q as a second chord for closing a window (#7767)
* Add Super + Q as a second chord for closing a window

Super + W stays the documented default. Super + Q is the chord people
arrive with from macOS, where Command + Q quits the app, and typing it
into Omarchy did nothing at all until now.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

* Put an action's alternative chord on one keybindings row

Super + W and Super + Q both read "Close window" in the menu, two rows
apart, with nothing to say they were the same thing -- and the
alternative sorted above the default. The scratchpad and the calculator
had the same trouble, each bound to a chord and to a second key.

Four actions are named as having an alternative, one at a time, and the
second chord joins the first one's row. A rule would be wrong here: Alt +
Tab and Shift + Alt + Tab both say "Reveal active window on top" while
cycling opposite ways, and a media key is nobody's idea of an alternative
to a Super chord. Both halves still have to agree on what they dispatch,
since a label is only what a chord is called, and an unresolved
dispatcher never counts as agreement.

Nothing is allowed past the 35-character column: a pair that would
overrun it stays as two rows rather than pushing its arrow out of line.
The menu elides a row that outgrows its card -- 754px of label, 78
monospace characters at the heading size -- and the longest entry already
sits at 74, so widening the column to fit the widest pair would have cost
two dozen rows the end of their description.

Priority ordering reads the rendered row, so the chord sharing it would
otherwise reclassify the entry: XF86Calculator alone belongs in the tail
kept for media keys, and it took the calculator down there with it.
Ranking now reads the chord that leads the row.

The key left of 1 reads as ~ rather than Hyprland's name for it, whether
a bind names it or reports the keycode for the keymap to resolve. Cached
records predate all of this, so the cache version moves with it.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-authored-by: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-22 16:55:01 +02:00
OmarchybotandCodex XHigh 13a969e1ab Only offer Update > Extra Themes when there is one (#7775)
* Only offer Update > Extra Themes when there is one

omarchy-theme-update pulls the themes under ~/.config/omarchy/themes that came from a git clone, so on a machine that has never installed one by hand the row opens a terminal that prints nothing and closes. Guard it with the same predicates the command itself applies, since a row that shows over a symlinked theme or a worktree's `.git` file is the same dead end in a narrower shape, and pin the two to each other in the guard test.

Co-Authored-By: Codex XHigh <noreply@openai.com>

* Extract the Extra Themes guard into omarchy-theme-extras

The row's `when:` and omarchy-theme-update each carried their own idea of which themes came from a git clone, and the two only matched because a test held them together. Name it once instead: omarchy-theme-extras lists those directories and exits nonzero when there are none, so the row asks exactly the command its action runs. Living in a script also puts the glob out of reach of whatever shopt a login shell left set for the guard batch.

Co-Authored-By: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-22 16:24:21 +02:00
OmarchybotandCodex XHigh 2c593dbbaa Switch back to the packaged quickshell now that 0.3.1 kills synchronously (#7769)
* Switch back to the packaged quickshell now that 0.3.1 kills synchronously

Omarchy shipped the quickshell-git build for a single fix: 0.3.0's `kill` returned before the instance had exited, so the kill loop in omarchy-restart-shell could race a dying shell. Upstream 0.3.1 ships that fix, which makes extra/quickshell the better package to be on again — signed, versioned, and not rebuilt from a moving branch on every update.

The migration swaps unconditionally instead of first checking which version the mirror offers. A machine left holding quickshell-git while the shipped package list names quickshell has no way to reconcile the two: omarchy-reinstall-pkgs installs that list with --needed, which does not skip a name that is not installed, and the conflict it then walks into has no answer under --noconfirm. A mirror that is briefly behind installs 0.3.0 instead and the next upgrade carries it to 0.3.1, which is much the cheaper way to be wrong.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Codex XHigh <codex@openai.com>

* Drop the quickshell version note from the shell restart loop

The comment qualified the kill loop as needing 0.3.1 or newer, but omarchy-restart-shell ships in the same package upgrade that brings quickshell along, so a machine running this code already has the version the loop depends on. The caveat could never be false where it was read, which left it as version archaeology rather than something the code could not say for itself.

🤖 Generated by Opus 5 in Claude Code.

---------

Co-authored-by: Codex XHigh <codex@openai.com>
2026-08-22 15:02:12 +02:00
David Heinemeier Hansson c539f036aa Explain things better
Given Finley's confusion
2026-08-22 14:12:14 +02:00
David Heinemeier Hansson 9d93807594 Tooltip for better discovery of alt date formats 2026-08-22 13:56:22 +02:00
Matthias NitschandClaude Fable 5 b63616422f Leave an auto-scaled internal panel alone in clamshell recovery (#7581)
With the default scale = "auto", sync_internal_scale read the config,
rejected "auto" as non-numeric, fell back to the hardcoded default 2,
and force-applied it whenever the compositor's auto resolution differed.
Since the script runs from omarchy-system-wake after every idle cycle,
the panel flapped between 2 and auto's own value (1.5666667 on a 198 DPI
panel) on every wake/reload pair.

A config without a usable number -- "auto", or an expression only
Hyprland's Lua can evaluate -- delegates the scale to the compositor:
whatever it resolved for the enabled panel is the configured scale, so
there is nothing to correct. Recovery of a disabled panel is unchanged
and still re-enables it with the remembered scale, falling back to the
historical default 2.

Fixes #7265. Also the scale-revert half of #7301.


Claude-Session: https://claude.ai/code/session_01L4Z6GimYhR1Kpsir24VAPF

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 13:43:46 +02:00
ed7bae4ac5 Replace Gemini coding agent with Antigravity (#6900)
* Replace Gemini coding agent with Antigravity

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Remove the dead Gemini mise wrapper in the Antigravity migration

Remove Preinstalls no longer lists gemini, so the wrapper Omarchy created
would have stayed in ~/.local/bin with nothing left to clean it up.

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

* Install Antigravity when it is the default a Gemini user is migrated onto

The opt-out check skipped the install but the rewrite ran anyway, so anyone
who had removed the preinstalls was left with a default agent naming a
command that is not there.

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

* Fix Antigravity skill provisioning and Gemini wrapper migration

- Wires Omarchy's default skills into Antigravity by linking them to ~/.gemini/config/skills/ in bin/omarchy-provision-user and migrations/1786719479.sh.
- Fixes the Gemini wrapper migration in migrations/1786719479.sh to recognize and remove wrappers containing either `mise use -g "gemini"` or `mise use -g --quiet "gemini"`, while leaving hand-written wrappers intact.
- Adds regression tests for both skill provisioning and wrapper removal in test/shell.d/default-agent-test.sh and test/shell.d/provision-user-test.sh.

* Stop the provisioning test from retheming the session it runs in

The test ran the real omarchy-provision-user, which sources install/user/all.sh and so reached omarchy-theme-set: hyprctl reload against the live compositor, gsettings against the live desktop, and a global Node install, none of which the skill symlinks it asserts need. Its mocks for omarchy-done and omarchy-refresh-applications were shadowed anyway, because provisioning prepends $OMARCHY_PATH/bin ahead of them, so stubbing the install suite at its own path is what a mock cannot do here. The exit status is checked rather than discarded: the assertion held even when provisioning died outright, because the symlinks are made twenty lines before the suite runs.

* Match the Gemini default and wrapper the way Omarchy writes them

The migration decided both questions differently from the code that owns them. It read the default agent with grep -qxF, while omarchy-default-agent takes the first line through read, so a padded "  gemini  " that the launcher still resolves was left naming an agent the launcher no longer supports. The wrapper it deletes was matched anywhere in the file, so a hand-written one that only mentions the installer's line in a comment went with Omarchy's own. Reading it the launcher's way and anchoring the match settles both against whoever wrote the file. The skills loop guards its glob the way migrations/1786539345.sh does, so an empty source cannot leave a symlink named "*" behind a migration already marked complete.

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

* List Antigravity among the skill directories

The manual named Claude Code, Codex, Pi and the generic location; provisioning now links ~/.gemini/config/skills too.

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

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Omabot <omabot@omarchy.org>
2026-08-20 22:28:31 +02:00
Stanko Krtalić cf29ce1867 Add hey (hey-cli) as a lazy-installed mise tool (#7626) 2026-08-20 21:13:27 +02:00
d3d9bea1ee Add a clock format with live seconds (#7586)
* Add a clock format with live seconds

Right-clicking the clock now reaches "Thursday 09:39:23" and its AM/PM twin, and the widget's SystemClock ticks once a second only while a format that prints seconds is showing — every other format keeps the minute precision it had, so nobody pays for a repaint a second to read a label that changes once a minute.

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

* Read an unterminated literal in a clock format as text

Qt reads an opening quote with no closing one as a literal running to the end of the format, so "HH:mm 'sec" prints "09:39 sec" and never a second count — but the seconds test stripped only balanced quotes, saw the s, and put the widget on a per-second tick for a label that changes once a minute. The wiring assertions went the other way: each passed while the feature was broken, so hard-coding showsSeconds to false, dropping the label's onDateChanged, or commenting the precision line out and leaving the text behind all shipped green. Comments now come out of the source before it is matched, and both halves of the tick are asserted.

Co-Authored-By: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-20 11:44:11 +02:00
Omarchybot 688d7df5d2 Drop the Windows VM start notification (#7585)
The shell already shows a "Launching Windows…" OSD from the moment the desktop entry is activated until the RDP window appears, so the notification duplicated feedback the user is already looking at. The failure notification stays: nothing else reports a VM that never came up.
2026-08-20 10:48:47 +02:00
David Heinemeier HanssonandClaude Fable 5 9455496990 Fix style inconsistencies in bin/ (#7518)
* 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>
2026-08-19 20:16:47 +02:00
David Heinemeier Hansson 3451719ca5 Reorder remove to match 2026-08-19 18:03:55 +02:00
David Heinemeier Hansson b3ba358104 Rearrange the order 2026-08-19 17:51:06 +02:00
OmarchybotandCodex XHigh 023021ad2d Add Remove > AI for the apps Install > AI offers (#7504)
* Add Remove > AI for the apps Install > AI offers

Install > AI grew five entries and Remove grew one: Dictation, sitting on its own at the top level. Everything else installed from that menu had to come back out through Remove > Package by name.

Mirror the install tree instead. Dictation moves under the new AI submenu where its installer already lives, and ChatGPT Desktop, Grok Bot, LM Studio, Ollama and T3 Code get removers beside it. Each entry is conditional on the thing actually being installed, so the submenu only ever lists what is there.

What each remover deletes was read off a machine that had all five installed and launched, not guessed. That matters most for T3 Code, which bootstraps the agents it drives: ~/.claude.json, ~/.grok, ~/.npm and ~/.local/share/opencode all appear the first time it runs, and all of them outlive it, so it takes only ~/.config/t3code and ~/.t3. Grok Bot is the same trap in miniature -- ~/.grokbot is its own, ~/.grok belongs to the Grok CLI.

Ollama drops every acceleration variant rather than the one the installer happened to pick, and disables the service before the package, since that is what holds the models open.

🤖 Generated by Opus 5 in Claude Code.

* Remove only what these apps own, and only where removal works

Three defects from an independent review of the previous commit.

ChatGPT Desktop was deleting ~/.cache/codex-runtimes, which belongs to the Codex CLI rather than the desktop app: the `codex` binary resolves its runtime and plugins out of that directory, and it ships in a package this remover does not touch. Removing the desktop app took the CLI's prepared runtime with it, leaving a separate, still-installed tool to rebuild it -- and unable to, offline.

Ollama's row appeared whenever the `ollama` command existed, but omarchy-pkg-drop removes exact package names. With ollama-bin, ollama-git or a hand-built binary the entry offered a removal it could not perform: the service went down, /var/lib/ollama and ~/.ollama were deleted, and it reported success with the program still installed. Every acceleration variant depends on the base package, so testing for that package covers each one the installer can produce and nothing it cannot remove.

LM Studio keeps its models under a relocatable home, and ~/.lmstudio-home-pointer is the only record of where they went. The remover deleted the pointer and the default path, so a user who had moved their models kept every one of them while being told they were gone. Read the pointer before deleting it, and refuse one aimed at / or at the home directory itself, since following it there would take everything.

Co-Authored-By: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-19 17:29:36 +02:00
Omarchybot 260a729104 Add T3 Code to the AI install menu (#7496)
t3code-bin is in the Omarchy repo now, so the menu can offer it the way it offers Cursor and Grok Bot: install the package, then launch the desktop entry it ships.

The mark is a trace rather than a download. T3 publishes no monochrome SVG — the app icon is a black rounded tile with the letters knocked out of it, and a tile flattens to a solid square once the menu recolors every path with the theme foreground. Tracing the lettermark out of that icon keeps the silhouette that actually reads.

The font is package-owned, so the glyph reaches a desktop through an omarchy-settings release rather than omarchy update. Until that release lands, a pulled checkout draws the entry with no icon.

🤖 Generated by Opus 5 in Claude Code.
2026-08-19 16:22:46 +02:00
David Heinemeier HanssonandClaude Opus 5 1e70cca144 Fall back to polkit when the DNS sudoers grant is missing (#7492)
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>
2026-08-19 14:59:09 +02:00
3765e8010b Switch DNS providers without a password prompt (#7472)
* Switch DNS providers without a password prompt

The network panel and the menu run omarchy-dns from a process with no
terminal, so require_root reached for pkexec and put a polkit password
prompt in front of what is meant to be a one-click toggle.

Grant %wheel passwordless sudo for the three stock providers and take
that path whenever the grant covers the invocation. Custom stays out of
the grant: it points the machine at servers the caller supplies, and it
already runs in a terminal that can ask.

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

* Pick the elevation path without asking sudo

The `sudo -n -l` probe answered the wrong question. It reports whether a
command is permitted, not whether it is passwordless, and the %wheel rule
every Omarchy install ships permits everything -- `sudo -n -l /usr/bin/rm
-rf /tmp/x` exits 0. So the probe passed for Custom too, and the exec
below it ran `sudo -n`, which fails outright with no terminal and no way
back to pkexec.

Decide from what the sudoers rule actually says instead: sudo when there
is a terminal to type into, or when the resolved path and the provider
are both ones the rule names. Everything else keeps going through polkit.

Pin a root-owned PATH once elevated, too. `omarchy dev link` puts a
user-writable checkout ahead of sudo's secure_path for every command, so
a passwordless grant on a script that resolves nmcli, tee, and install
through PATH would otherwise hand root to whoever can write there.

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

* Keep users outside %wheel on the polkit path

The rule grants %wheel, so path and provider alone do not mean sudo will
take it. A user outside the group was sent to sudo anyway, and with no
terminal to answer the prompt that is a dead end -- polkit at least
offers to authenticate as somebody else.

Two holes in the test alongside it: it accepted any file containing the
expected rule, so a second, argument-free line would have widened the
grant unnoticed, and run as root it would have sailed past the stubs and
rewritten the host's own DNS config.

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

* Elevate the system install, whatever copy was invoked

The rule names /usr/bin/omarchy-dns, so a dev-linked checkout handed sudo
a path nothing could match and fell back to a polkit prompt. Re-exec the
packaged path instead: the privileged half is the system install
everywhere, the grant matches everywhere, and the path comparison and the
PATH pinning that existed to work around the checkout both go away.

Dev-linked checkouts run their own unprivileged half and the installed
one as root, which is the trade for not carrying a second code path.

---------

Co-authored-by: Omabot <david@hey.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 14:28:56 +02:00
OmarchybotandClaude Opus 5 a4219f8f4a Store theme backgrounds as webp (#7477)
* Decode webp in the shell

The background and the lock screen are drawn by Quickshell, so they
decode through Qt, which ships handlers for png, jpeg and gif but not
webp. QImageReader answers "Unsupported image format" and the layer
comes up blank. Any third-party theme shipping a .webp background hits
this today, even though every path that goes looking for a background
already globs the extension.

qt6-imageformats supplies the missing plugin for 71 KB downloaded. Its
one new dependency of substance, libwebp, is already on every machine
by way of libvips.

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

* Store theme backgrounds as webp

WebP codes both of the things these backgrounds are made of better than
the formats they were in: the photographs, where its lossy mode is worth
a third or more over JPEG at matched quality, and the flat art and dot
patterns, where its lossless mode undercuts an oxipng-packed PNG.

28 of them become lossless webp and decode bit-for-bit identically
(AE=0), so the dot patterns and flat-shaded pieces carry no quality
question at all. That includes 0-launch, whose alpha channel comes
through intact. The other 51 are photographs held to the same 38 dB
PSNR floor as the JPEG pass, landing between 38.0 and 54.6 dB. Every
image keeps its exact pixel dimensions, for 29.8 MB.

Each one is encoded from the original as it stands in quattro rather
than from the file the earlier commits produced, so nothing picks up a
second generation of loss on the way here.

13 stay JPEG. WebP is plainly larger for most of them, and three are
grainy enough that its filter smooths the grain instead of coding it:
PSNR plateaus near 34 dB however high the quality goes, well under the
floor.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 14:21:30 +02:00
David Heinemeier Hansson 2684c4b02e Print the OEM Windows product key from firmware (#7480)
* 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
2026-08-19 13:59:25 +02:00
David Heinemeier Hansson 238021cd67 Decode mixed UTF-16 clipboard text (#7466)
* Decode mixed UTF-16 clipboard text

* Harden UTF-16 clipboard detection

* Finish UTF-16 decoder hardening
2026-08-19 11:56:51 +02:00
David Heinemeier Hansson fa955bfa9d Better without gaps 2026-08-18 22:33:53 +02:00
83987718cf Give the Quake console its own file, an agent, and half a screen (#7420)
* Open the scratchpad with the default agent already in it

on_created_empty fires when the special workspace is created empty, so the
agent starts the first time the console drops down instead of at boot, and
comes back on the next open if you close it.

The exec rule pins the workspace rather than trusting the spawn to inherit
it: Hyprland only tags a process with its origin workspace while
misc.initial_workspace_tracking is on, and we turn that off.

Nothing to guard for a missing default agent. Omarchy picks none for you,
and omarchy-agent exits without opening a window when none is set, so the
scratchpad just opens empty until one is chosen.

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

* Move the console into its own file and size it to half the screen

The scratchpad's presentation was scattered through looknfeel: a dim in the
decoration block, a workspace rule below it, two animation leaves further
down again. Gathered into qconsole.lua, where the whole console is one
readable thing.

Sized to half the screen while it moved. A window rule cannot do that: its
size expressions resolve once, when the window maps, so rescaling the
monitor afterwards leaves a console that is no longer half of anything.
Gaps are re-applied by the layout, so the console is sized by the gap left
underneath it, recomputed from the monitor whenever the layout changes.

Monitor dimensions come back in physical pixels while gaps are logical, so
the scale comes out before the reserved area comes off. That arithmetic is
the whole trick, and the test pins it at 1x, 2x and 1.5x.

The test runs lua with an explicit "-". Bare `lua <<EOF` reads stdin as a
REPL and exits 0 even after an error, which would leave its assertions
unable to fail.

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

* Only rewrite the console rule when its size actually changes

Refitting replaces the rule in place rather than stacking a new one, so
there was no leak, but each write still schedules a monitor and window state
refresh and monitor.focused fires on every hop between screens. Remember
what was last written and skip the write when the number has not moved.

Also say out loud that the scale guard is what keeps the arithmetic below it
safe: a monitor handle that has outlived its output answers nil to every
field, and a layout change is exactly when that happens.

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

* Drop the active window border inside the console

The gradient border marks which window has focus, which the console does not
need: it is only ever focused while it is open, and the dimmed workspace
behind it already sets it apart. On a single agent terminal the highlight
just reads as a frame around the panel.

no_border on the workspace rule pins the border to 0 at workspace-rule
priority, so it applies to whatever ends up in there without touching the
global border.

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

---------

Co-authored-by: Omabot <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:24:56 +02:00
David Heinemeier Hansson d18cb4f880 Stick to the theme's rounding 2026-08-18 21:43:36 +02:00
0b5111702e Give scratchpad a Quake-style console presentation (#7409)
* Give scratchpad a Quake-style presentation

* Keep the Quake scratchpad from restyling every window

The presentation was bought with global decoration defaults: rounding went
0 -> 5 and both shadow and blur were switched back on for every window on
the system, undoing 935283c8. Blur in particular is close to invisible
here, since every window is already tagged 0.985/0.96 opacity and no layer
rule asks for it, so it was GPU load on every frame for almost nothing.

Put the globals back and scope the rounding to the scratchpad with a window
rule, the same way popped windows already get theirs. The inset, dim_special
and the slide carry the effect on their own. dim_special only applies while
a special workspace is open, so it stays.

Also drop the four workspace-rule booleans that only restated Hyprland's
defaults, fold the stale scratchpad rows in the hotkey table into the new
ones, and give the binding assertions their own fixture instead of borrowing
the bar-panel one.

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

---------

Co-authored-by: Omabot <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 21:40:20 +02:00
ec779715bd Shrink theme backgrounds without touching resolution (#7266)
* Re-encode over-encoded theme backgrounds

Several backgrounds shipped at quality 95-100, which buys nothing visible
on a wallpaper but costs 20 MB. Re-encode those at quality 85, leaving
resolution untouched -- every file keeps its original pixel dimensions.

Only files stored above quality 90 are touched, and only when the result
saves at least 15% and stays above 38 dB PSNR against the original. The
34 backgrounds already stored at quality 85 or below are left alone
rather than pushed through another lossy generation for a few hundred KB.
osaka-jade/2-shaded-entrance is skipped for that reason: nothing clears
both bars. ristretto/2-coffee-beans is re-encoded at 90 instead of 85,
where 85 fell below the PSNR floor.

Theme backgrounds drop from 107 MB to 87 MB.

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

* Store nord's night-hawks background as a palette PNG

The file uses only 8 distinct colors but was saved as truecolor RGB,
costing 1.8 MB for an image an indexed palette stores in half the space.
Converting to a palette PNG is lossless: same dimensions, and zero pixels
differ from the original.

1782 KB -> 909 KB.

The other palette-eligible PNGs are already indexed, and re-compressing
them with ImageMagick only makes them bigger, so they are left alone.

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

* Recompress theme PNGs losslessly with oxipng

Every background PNG was left at its authoring tool's default deflate
settings. Running oxipng over them re-packs the same pixels: all 31 files
decode bit-for-bit identically (AE=0) at unchanged resolution, for 981 KB.

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

* Store photographic backgrounds as JPEG instead of PNG

Four backgrounds were shipped as PNG despite being photographs and
painterly illustrations, where lossless coding buys nothing the eye can
see. Re-encoding them at the same q85 used by the other backgrounds keeps
every pixel dimension and stays above 40 dB PSNR, for 3.3 MB.

The remaining PNGs stay PNG: JPEG is larger for the dot patterns and the
flat-shaded pieces, and 0-launch.png genuinely uses its alpha channel.

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>
2026-08-18 18:29:23 +02:00
David Heinemeier Hansson f32ebbdb73 Omarchy Server Edition 2026-08-17 06:11:02 -04:00
David Heinemeier HanssonandClaude Fable 5 f8b0d97ce4 Revise backup plan after codex xhigh review
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 06:11:02 -04:00
David Heinemeier HanssonandClaude Fable 5 1c8f728b25 Document full-line style for markdown docs in AGENTS.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 06:11:02 -04:00
David Heinemeier HanssonandClaude Fable 5 0962523c0c Plan the backup service
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 06:11:02 -04:00
David Heinemeier Hansson 006460ad57 Decode UTF-16 clipboard text (#7249) 2026-08-17 06:09:51 -04:00
Omarchybot 262d6f0681 Switch to the mise-bin package (#7244)
* Switch to the mise-bin package

mise-bin carries mise's own release artifacts from the Omarchy repo --
PGO+BOLT-optimized on x86_64, glibc-native on both arches -- instead of
Arch's mise, and tracks jdx/mise releases directly.

Existing installs need a migration because the two packages conflict, and
omarchy-pkg-add cannot make the swap: pacman answers its own conflict
question with No under --noconfirm and fails the transaction. --ask=4
answers that one question, so mise-bin replaces mise in a single
transaction -- which is also what keeps omarchy-zsh and omarchy-fish, both
of which depend on mise, satisfied through the swap by its provides.

* Guard the swap with a conditional instead of an early exit

Two-path control flow takes an if, per the style guide; the early exit only made the swap line unreachable from a distance.
2026-08-17 05:11:56 -04:00
David Heinemeier Hansson 1c1116b626 Fix passwordless OWE Wi-Fi handling (#7238) 2026-08-17 03:36:59 -04:00
John WilsonandClaude Sonnet 5 30f7a06090 Update tokyo-night winding-road background (#7057)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 07:51:30 -04:00
Omarchybot 7488eaded4 Document remapping the CapsLock compose key (#7091)
* Document remapping the CapsLock compose key

* Clarify restoring CapsLock behavior
2026-08-16 05:47:41 -04:00
Aditya Garud a3de80d647 Keep shell tests inside their fixtures (#6763) 2026-08-16 05:45:06 -04:00
OmarchybotandClaude Opus 5 33cda8b602 Add --gh-keys so sshd setup can run without prompts (#7086)
* Add --gh-keys so sshd setup can run without prompts

Grabbing keys from GitHub was reachable only through the interactive menu: pick
"Grab key from GitHub", then type the username into a second prompt. So the one
path that needs no secret pasted around was also the one path a script could not
take, and setting a machine up over ssh or from a provisioning run meant falling
back to --key with a key copied by hand.

--gh-keys <username> takes the same path the prompt did. The fetch and authorize
logic is unchanged and now shared, with the prompt reduced to asking for the
username and handing it over.

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

* Reject a missing --gh-keys username before setting anything up

The username was only checked for being absent entirely, and only after the
server was installed and the firewall opened. So `--gh-keys=` with an unset
variable behind it configured the machine and then dropped into the interactive
menu, and `--gh-keys --help` took --help as the username and set the server up
on its way to failing the fetch -- a help flag that changes the system.

Check the value where it is parsed, and reject one that is empty or shaped like
an option.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 10:14:42 +02:00
Ryan Hughes 7be59e1f4b Merge pull request #6939 from basecamp/fix-shell-succeeds-hyprland
Fix o.shell_succeeds() always returning false inside Hyprland
2026-08-15 15:02:49 -04:00
David Heinemeier HanssonandClaude Fable 5 dd9dee417f Launch claude and codex agents with auto-review instead of full bypass (#7001)
* 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>
2026-08-15 19:23:15 +02:00
David Heinemeier HanssonandClaude Opus 5 9f109a7f3e Format the whole speed test dial for the locale (#6994)
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>
2026-08-15 17:27:35 +02:00
David Heinemeier HanssonandClaude Opus 5 25bee1d2a3 Keep the calendar's day names in English (#6988)
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>
2026-08-15 17:21:26 +02:00
David Heinemeier HanssonandClaude Opus 5 ff4e92e63b Fix Clone Plugin failing with "unknown clone option" (#6942)
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>
2026-08-15 17:04:04 +02:00
David Heinemeier HanssonandClaude Opus 5 c0a20e2e43 Stop the bar sticking in move mode after a press-and-hold on a widget (#6943)
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>
2026-08-15 17:01:03 +02:00
David Heinemeier HanssonandClaude Opus 5 1c3da94906 Keep mise wrappers from writing to stdout (#6940)
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>
2026-08-15 17:00:19 +02:00
David Heinemeier HanssonandClaude Opus 5 b5bb8dac05 Repair foot.ini configs holding a literal \n[text-bindings] line (#6938)
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>
2026-08-15 16:50:43 +02:00
David Heinemeier HanssonandClaude Fable 5 022f6993ba Plan the dots feature for preserving and syncing user configs
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>
2026-08-15 16:29:51 +02:00
David Heinemeier HanssonandClaude Fable 5 f4189398bc Bring docs/ up to date and cover the undocumented subsystems
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>
2026-08-15 16:21:25 +02:00
David Heinemeier HanssonandClaude Fable 5 b68c22208a Correct stale claims in menu jsonc headers and bar README
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>
2026-08-15 16:21:25 +02:00
David Heinemeier HanssonandClaude Fable 5 16c8888f96 Sort documentation by genre and document the layout
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>
2026-08-15 16:21:25 +02:00
David Heinemeier HanssonandClaude Opus 5 0965ac2e4f Fix o.shell_succeeds() always returning false inside Hyprland
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>
2026-08-15 07:12:25 -07:00
75b99f7fd4 Fix Windows VM launch never opening an RDP window (#6893)
* Wait for the current Windows boot before connecting RDP

docker logs retains output across stop/start, so grepping the whole log
matched "Windows started successfully" from an earlier boot and returned
immediately, firing xfreerdp3 while the guest was still booting. Anchor the
scan to the container's current StartedAt, and run it even when the container
was already running, since the image restarts the guest in place on reboot.

* Skip Kerberos when connecting to the Windows VM

FreeRDP 3 attempts Kerberos before NTLM for NLA, and Arch's stock
/etc/krb5.conf declares default_realm = ATHENA.MIT.EDU, so every launch tries
to reach MIT's KDC. Off the network each attempt blocks ~23s and xfreerdp3
sits in CLOSE-WAIT without drawing a window, which reads as the VM failing to
start. Point FreeRDP at a realm-less krb5 config so it falls through to NTLM,
which is what the local Windows account uses anyway.

* Re-read the container start time on every readiness poll

A failed docker inspect left STARTED_AT empty, and docker logs drops the
--since filter when it is, putting the scan back on the whole retained log
and its stale success line. Sampling per poll also keeps the window on the
current boot if the container restarts mid-wait.

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>
2026-08-15 15:42:43 +02:00
David Heinemeier HanssonandClaude Fable 5 4b93f8d84d Dim installed software in the Install menus instead of hiding it (#6955)
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>
2026-08-15 15:31:07 +02:00
David Heinemeier Hansson b724f76156 Install missing apps when choosing defaults (#6950)
* Install missing apps when choosing defaults

* Restore Chromium through browser installer

* Trust default app installer status

* Use full conditionals for install paths
2026-08-15 12:34:28 +02:00
David Heinemeier HanssonandClaude Fable 5 9b72edcc94 Add hover-revealed close button to notification toasts (#6946)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 11:42:57 +02:00
Akshar Patel 94a7b70ed4 Explain Resolve pointer focus override 2026-08-14 23:50:12 -04:00
Akshar Patel ec9da050ee Stop Resolve dialogs from recapturing pointer focus 2026-08-14 23:47:31 -04:00
David Heinemeier Hansson f0020448ca More manual tweaks 2026-08-14 16:57:59 +02:00
David Heinemeier HanssonandClaude Fable 5 ebdc0263e0 Point the theme installer at the extra themes page on omarchy.org
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 16:41:06 +02:00
David Heinemeier HanssonandClaude Fable 5 e6d7c620de Extra themes are added via PRs to omarchy-site now
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 16:38:54 +02:00
David Heinemeier HanssonandClaude Fable 5 1fe471dc53 Link the manual to the extra themes page on omarchy.org
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 16:34:31 +02:00
Luis Fernando Jiménez b15ec6c6b3 Fix typo(Primeagen) in Neovim documentation (#6867) 2026-08-14 16:19:47 +02:00
David Heinemeier HanssonandClaude Fable 5 8b70f01583 Point Learn > Omarchy at the manual on omarchy.org
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 12:56:39 +02:00
David Heinemeier HanssonandClaude Fable 5 e7ea031f98 Host the manual's images in the repo under manual/images (#6861)
* 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>
2026-08-14 12:47:38 +02:00
David Heinemeier HanssonandClaude Opus 5 4559f2d5fc Add Moonlight to the preinstall remove/install lists
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019DPqQk3igpstNXSTkfSbJa
2026-08-14 12:30:32 +02:00
David Heinemeier HanssonandClaude Opus 5 28dcbae376 Restore preinstalls from the menu, and match the lists to what quattro ships (#6854)
* 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>
2026-08-14 12:24:47 +02:00
Fayi FB fa8359359b Fix Pi prompt launch (#6857) 2026-08-14 12:23:07 +02:00
David Heinemeier Hansson ca5add0463 Words 2026-08-14 12:21:59 +02:00
David Heinemeier Hansson 3bd6ed02fe Words 2026-08-14 09:51:17 +02:00
David Heinemeier Hansson 864b0d050a Recognize colored package conflict errors 2026-08-14 00:15:07 -07:00
David Heinemeier HanssonandClaude Opus 5 5ca3030c5a Put a blocked package upgrade back to whoever is updating (#6830)
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>
2026-08-14 09:08:34 +02:00
David Heinemeier HanssonandClaude Fable 5 625c66301d Point the headless theme test at the renumbered Tokyo Night background (#6851)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 08:59:38 +02:00
Shrijit Srivastav d35f4b6d59 Wait on the profile being repaired, not on every browser (#6837)
The migration asked a user to close every running browser before repairing
the Copy URL shortcut, but a browser only ever rewrites its own Preferences
on exit. Waiting on all browsers deadlocks `omarchy update` for anyone whose
main browser is effectively never closed: the pending ghosts commonly sit in
a stale profile nobody has open, yet the migration blocks on the always-open
browser until the prompt is declined, failing the whole update.

A running Chromium-family browser holds a SingletonLock (and socket) inside
its user-data-dir, so whether the profile being repaired is open is
mechanical. Gate on that instead of on the sheer presence of a browser
process — the repair proceeds where the affected profile is closed, and
browsers attached to other profiles no longer hold the update hostage.

The gate stays conservative while an affected profile actually is open, and
the existing post-repair verification still catches a browser that starts
mid-repair and restores stale Preferences on exit.

The migration test now simulates an open profile with its SingletonLock
instead of a pgrep stub; every prior scenario still passes.
2026-08-14 08:59:14 +02:00
David Heinemeier HanssonandClaude Opus 5 dc698e5df0 Suppress LocalSend's redundant tray item (#6852)
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>
2026-08-14 08:23:45 +02:00
Adrian RangelandClaude Opus 5 df708831b6 Keep the Copy URL migration's browser prompt visible (#6842)
gum draws its confirm UI on stderr, so the migration's `2>/dev/null` threw
away the whole prompt while gum still held the terminal in raw mode reading
keys. With a browser open, an update stopped after "Running migration
(1786643346)" on an unpainted screen with no way to tell it was waiting for
an answer.

Nothing else in the repo suppresses gum's stderr; the redirect only ever hid
gum's own error in the no-terminal case, where the migration already explains
itself on stderr before deferring.

Fixes #6841


Claude-Session: https://claude.ai/code/session_01So4yKzTDdJJp7orjnF8i1J

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 08:19:25 +02:00
David Heinemeier HanssonandClaude Fable 5 5e5197a081 Point the README manual TOC at the renumbered chapters
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>
2026-08-13 22:53:23 +02:00
David Heinemeier HanssonandClaude Fable 5 3a4eda7f08 Drop retired nmtui and bluetoothctl interactions from the bar README
The network and bluetooth panels stopped launching them; the module
catalogue still said otherwise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 22:52:13 +02:00
David Heinemeier HanssonandClaude Fable 5 1ae83f1606 Add a switchers chapter and fix drift found on a second manual pass
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>
2026-08-13 22:52:13 +02:00
David Heinemeier HanssonandClaude Fable 5 de854d3f0c Free the Copy URL shortcut from ghost extension registrations (#6821)
* 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>
2026-08-13 22:37:29 +02:00
David Heinemeier HanssonandClaude Opus 5 30f8f191c0 Add a toggle for crash capture (#6824)
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>
2026-08-13 21:26:08 +02:00
Eduardo EscobarandClaude Opus 5 f73740ff07 Blank the lock screen while the fingerprint reader waits (#6817)
The blank timer was gated on `authenticating`, which is
`authenticatingPassword || fingerprintAuthenticating`. The fingerprint
PAM sits armed for the entire lock waiting for a finger, so on any
machine with a reader enrolled the gate is true from lock until unlock:
the timer is stopped when the lock begins and never re-armed, and the
display stays lit indefinitely.

Gate on `authenticatingPassword` instead. A password check in flight
still holds the display up, and the passive fingerprint wait no longer
does.


Claude-Session: https://claude.ai/code/session_01EDpyC9793TKZBS2jXUNECG

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 21:04:47 +02:00
Jitendra dara 5c51ad6d54 Fix broken manual links in README (#6809)
* Fix README having broken links

Reordered and updated the list of manual sections in the README.

* Update README with new section ordering and links
2026-08-13 21:02:47 +02:00
82ae514609 Fix notification focus for agent terminals (#6801)
* Fix notification focus for agent terminals

* Restrict notification title fallback to agents

* Simplify the focus fallback to a lazy two-tier query

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

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 20:28:43 +02:00
David Heinemeier Hansson 14dc3a31d1 Add omarchy-dev-font for adding marks to the icon font (#6819) 2026-08-13 19:46:46 +02:00
Rob Zolkos ed795d8a13 Select packaged Cursor for theme sync (#6808) 2026-08-13 19:33:22 +02:00
David Heinemeier HanssonandClaude Fable 5 b08598086c Keep AppLibrary scans out of login shells (#6815)
* 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>
2026-08-13 19:30:56 +02:00
David Heinemeier HanssonandClaude Fable 5 5b8fef406f Flesh out the Windows VM chapter
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>
2026-08-13 18:18:46 +02:00
David Heinemeier Hansson b508541c88 Use official marks for the AI installers (#6814)
* Add LM Studio and Ollama marks to the Omarchy icon font

* Use the official marks for the AI installers in the menu
2026-08-13 18:18:17 +02:00
David Heinemeier Hansson e2cffe1771 Add Grok Bot to the Install > AI menu (#6811) 2026-08-13 18:02:15 +02:00
David Heinemeier HanssonandClaude Fable 5 b2207c3357 Persist notification images so history keeps avatars (#6804)
* 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>
2026-08-13 18:01:48 +02:00
David Heinemeier HanssonandClaude Fable 5 edcbcb4691 Point the hyprsunset autostart hint at autostart.lua
The comment still described the old exec-once conf syntax.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:04:28 +02:00
David Heinemeier HanssonandClaude Fable 5 93372d5fad Drop the tz and style command group descriptions
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>
2026-08-13 17:04:28 +02:00
David Heinemeier HanssonandClaude Fable 5 55434792d1 Cover the shell, capture, browsers, and networking in the manual
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>
2026-08-13 17:04:09 +02:00
David Heinemeier Hansson 144f4d1e31 First pass on updates for Quattro 2026-08-13 15:18:33 +02:00
David Heinemeier Hansson 9c9e082954 New launch backgrounds for Omarchy 2026-08-13 15:18:33 +02:00
David Heinemeier Hansson fa78633596 Not needed 2026-08-13 05:42:02 -07:00
25c2b3aed2 perf(agents): cut codex usage collector memory with SQL filter and cache (#6780)
* 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>
2026-08-13 13:47:44 +02:00
David Heinemeier Hansson 929849c7b9 Add the manual to the source code for better agent evolution 2026-08-13 13:37:57 +02:00
David Heinemeier HanssonandClaude Fable 5 78d3224846 Bound the hybrid GPU gate's supergfxctl query (#6799)
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>
2026-08-13 12:37:43 +02:00
Aditya Garud 090574e3a1 Treat LVDS panels as internal displays (#6752)
* Treat LVDS panels as internal displays

* Test empty DRM monitor discovery
2026-08-13 12:25:25 +02:00
8b9e43d3e5 Only offer video capture devices as webcams (#6732)
* 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>
2026-08-13 12:23:48 +02:00
860503f56a Stop hybrid GPU mode queries from hanging (#6738)
* 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>
2026-08-13 12:22:58 +02:00
David Heinemeier HanssonandClaude Opus 5 ce21845407 Let --force actually re-probe the Claude limits endpoint (#6798)
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>
2026-08-13 12:13:41 +02:00
Grant Wiley f6fd2e705a Wait for ttfx before closing its terminal (#6773) 2026-08-13 11:46:45 +02:00
Francisco Cabral 5f74a996c7 Stop closed network panels from leaving Wi-Fi scanning enabled (#6772)
* 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.
2026-08-13 11:46:16 +02:00
David Heinemeier HanssonandClaude Opus 5 8ca61d6b8b Open the About window at the size it last settled on (#6770)
* 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>
2026-08-13 11:32:20 +02:00
David Heinemeier HanssonandClaude Opus 5 c8fb5be42e Explain an expired Claude sign-in instead of hiding the limits (#6795)
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>
2026-08-13 11:13:07 +02:00
ecd57bceee Drop the kms hook when the proprietary NVIDIA driver handles early KMS (#6791)
* Drop the kms hook when the proprietary NVIDIA driver handles early KMS

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

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

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

Fixes #6790

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

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

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

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

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

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

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

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

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

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

* Rebuild existing initramfses once the kms hook no longer applies

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

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

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

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

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

---------

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

Fixes #6783

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 10:36:01 +02:00
David Heinemeier HanssonandClaude Fable 5 457aec6a8c Stop Bluetooth discovery when the panel closes (#6794)
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>
2026-08-13 10:35:22 +02:00
f075a789f4 Recover the wallpaper picker after interrupted thumbnails (#6775)
* 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>
2026-08-13 09:58:15 +02:00
9672d852f0 fix --help being skipped for bin routes that only partially resolve to a file (#6788)
* 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>
2026-08-13 09:53:31 +02:00
David Heinemeier Hansson 38542a1f51 Open the agent in a normal tiled window (#6769)
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.
2026-08-12 22:37:16 +02:00
David Heinemeier Hansson 438f7b3340 Stop ttfx before closing the screensaver terminal (#6764) 2026-08-12 20:05:10 +02:00
David Heinemeier Hansson f5893ddd9e Launch the default agent from the agents widget right-click (#6759) 2026-08-12 18:46:17 +02:00
David Heinemeier Hansson 2cc3510d2a Offer an AI diagnosis when a process crashes (#6746)
* Offer an AI diagnosis when a process crashes

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

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

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

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

* Let the diagnosis clean up the core it extracted

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

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

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

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

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

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

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

* Wordsmith

* Cover the agent routes and the invitation

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

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

* Offer the agent choice from the keybinding

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

* Reach existing installs with the agent invitation

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

* Say what the Defaults submenus set

Setup > Defaults lists Agent, Browser, Terminal, Editor, but the header inside
each repeated the same bare word, which reads as a category rather than a
setting -- and says nothing at all when the menu is summoned straight into it.
The list keeps its short labels; the headers now name the setting.
2026-08-12 17:56:19 +02:00
David Heinemeier HanssonandClaude Opus 5 b97a1480dc Treat the guard test's socket fixture as optional (#6755)
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>
2026-08-12 16:58:46 +02:00
66e3f479a6 Follow the keyboard being typed on in the layout widget (#6740)
* 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>
2026-08-12 16:57:57 +02:00
David Heinemeier HanssonandClaude Opus 5 0fa3170504 Skip shell tests when the compositor can't be reached (#6749)
* 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>
2026-08-12 16:49:15 +02:00
David Heinemeier HanssonandClaude Opus 5 0900855a28 Stop hot-reloading the shell when a package upgrade rewrites it (#6751)
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>
2026-08-12 16:42:37 +02:00
David Heinemeier HanssonandClaude Opus 5 106320ab11 Install ChatGPT as a desktop app under Install > AI (#6747)
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>
2026-08-12 15:56:40 +02:00
David Heinemeier HanssonandClaude Opus 5 970ec26bb0 Don't let a usbfs claim count as a kernel driver (#6744)
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>
2026-08-12 14:01:58 +02:00
a9c159a1f4 Restore FPC fingerprint detection in Quattro (#6737)
* 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>
2026-08-12 13:59:01 +02:00
markbus-aiandmarkbus-ai 5c74f82300 Ask for the keyboard layout only when the answer can change (#6727)
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>
2026-08-12 13:28:44 +02:00
David Heinemeier Hansson 0fcdd716a1 Drop backgrounds no longer worthy 2026-08-12 12:18:57 +02:00
David Heinemeier Hansson 3da9eaf61c Add quattro background by vulturetone 2026-08-12 12:18:47 +02:00
David Heinemeier Hansson 21b610a7fa Need an extra CR for presentation 2026-08-12 11:58:25 +02:00
David Heinemeier HanssonandClaude Opus 5 9b8bf1da71 Fix two races in the notification popup and history handling (#6735)
* 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>
2026-08-12 11:57:05 +02:00
David Heinemeier HanssonandClaude Opus 5 fd23ca023a Prune the package cache before updating (#6734)
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>
2026-08-12 11:56:02 +02:00
David Heinemeier HanssonandClaude Opus 5 cd84583b56 Show a notification the sender updated, instead of the version it replaced
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>
2026-08-12 00:58:14 -07:00
David Heinemeier HanssonandClaude Opus 5 ab57ad65fd Make notification history the last ten notifications on disk
History was a pair of in-memory lists mirrored into notifications.json, split
into "pending" and "past" by a seen/unseen distinction no surface exposed,
capped at 100, deduped by an id that repeats across server generations, and
pruned by a 15-minute TTL. Replaying it showed five rows drawn from whichever
list happened to hold them.

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:35:40 -07:00
David Heinemeier HanssonandClaude Opus 5 4727bad5eb Stop the update-lock stub racing the holder it spawns
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>
2026-08-11 14:16:32 -07:00
David Heinemeier HanssonandClaude Opus 5 3888dca7e8 Stop script hanging up the inhibitor before it starts
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>
2026-08-11 14:16:32 -07:00
David Heinemeier HanssonandClaude Opus 5 1e3c43a59b Assert the focused monitor the panel actually reports
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>
2026-08-11 14:16:32 -07:00
David Heinemeier HanssonandClaude Opus 5 b99fd91cf1 Simplify the Quattro upgrade, and stop it leaving an error bar behind (#6716)
* Accept either name for the lock authentication command

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

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

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

* Leave the Omarchy 3 session alone until the reboot

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

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

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

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

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

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

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

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

* Clear the Hyprland error bar the upgrade leaves behind

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

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

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

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

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

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

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

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

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

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

* Assert the retired session entry points stay gone

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

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

* Upgrade onto the channel the machine is already on

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 22:47:39 +02:00
1bb6600c76 Repair theme symlinks the state-move migration left dangling (#6717)
* Repair theme symlinks the state-move migration left dangling

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

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

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

* Only repair theme symlinks that could never have worked

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

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

* Clean up every scratch directory the CLI suite creates

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

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

* Drop the theme symlink migration tests

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-11 22:47:07 +02:00
David Heinemeier HanssonandClaude Opus 5 96a50cf65e Add the Discord community to the Learn menu
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>
2026-08-11 22:26:52 +02:00
David Heinemeier HanssonandClaude Opus 5 64e20f8f38 Let the menu fill 70% of the screen
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>
2026-08-11 22:21:38 +02:00
David Heinemeier HanssonandClaude Opus 5 7d08473de4 Add Herdr keybindings to the Learn menu
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>
2026-08-11 22:21:38 +02:00
David Heinemeier Hansson 912a4adfea Better order 2026-08-11 22:09:41 +02:00
David Heinemeier HanssonandClaude Opus 5 08204846ef Detect NVIDIA GPUs without waking them (#6712)
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>
2026-08-11 18:34:07 +02:00
f4b832eba5 fix(monitor): fix display mirroring recovery and UI state (#6457)
* fix(monitor): prevent mirror toggle deletion during recovery and fix UI state

* Assert the external monitor helper counts mirrors as active

The helper now asks `hyprctl monitors all -j`, so the test that pinned it
to plain `monitors` failed. A mirrored external is absent from plain
`monitors`, which reads as a disconnect and hands the mirror toggle to
recovery.

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

* Keep the mirror state line when nothing is mirrored

Piping the first mirroring monitor into the branch fed jq's test() a null
whenever no output mirrored, and jq aborts there rather than falling back
to "". The panel reads this output by line, so the missing line shifted
the focused monitor, the scale, and the display list up one, and left
mirroring reading as on whenever the external display had focus.

Select first and branch inside the pipeline, so the branch only ever sees
a monitor and the empty case falls to "" as the lines around it do.

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

* Cover the monitor panel state the shell reads by line

Nothing exercised omarchy-monitor-state, so both the mirror direction it
reported and the jq that reported it went unguarded. The panel reads the
output by line index, where a helper dying mid-script costs a line and
shifts every field below it into the wrong property without failing.

Assert the line count alongside the fields, over extended, mirrored both
directions, and clamshelled displays.

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>
2026-08-11 18:25:44 +02:00
David Heinemeier HanssonandClaude Opus 5 41818ecb59 Start the agent in ~/Work when launched from home
Coding agents refuse to persist trust for $HOME, so the keybinding and
menu launches -- which inherit the session's ~ cwd -- re-asked on every
single session. ~/Work is already the work root Omarchy creates, and
trust there sticks.

Only applies when the cwd is exactly $HOME, so the inline alias still
starts in whatever project directory it was run from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 17:59:55 +02:00
5b2c02dee3 Fix unclickable tray submenus by drilling down inside the popup (#6703)
* Fix unclickable tray submenus by drilling down inside the popup

Clicking a tray menu entry that has children was a silent no-op: the
row called QsMenuEntry.display(), which renders a *platform* menu, and
Quickshell refuses that unless the shell root sets `//@ pragma
UseQApplication` -- shell.qml does not. The log shows "Cannot display
PlatformMenuEntry as quickshell was not started in QApplication mode"
and nothing opens. Apps whose whole menu is submenus, like
radiotray-ng's station list, were unusable.

Adding the pragma would be the wrong fix: it switches the entire shell
from QGuiApplication to QApplication, dragging QtWidgets into the
process and changing application-class behavior for the sake of one
popup -- which would then render as an unstyled platform menu beside
omarchy's own popup styling anyway.

Instead, submenus drill down inside the existing popup. A child
QsMenuEntry inherits QsMenuHandle, so it can feed a nested QsMenuOpener
and render through the same row delegate. Each level keeps its own live
opener on a stack -- a child entry is owned by its parent opener's
model, so collapsing to a single reassigned opener would destroy the
very entry being displayed. A back header row walks out one level; at
the root the menu renders exactly as before, and items without a
DBusMenu still use the platform fallback.

* Destroy submenu openers deepest-first and reset before switching items

resetTrayMenu() destroyed openers front-to-back and only cleared
submenuStack afterward. A deeper opener's menu entry is owned by its
parent's children model, so destroying the parent first could
invalidate an entry a still-live child opener referenced. Clear the
stack before tearing anything down, then destroy deepest-first so a
child is always gone before the parent whose model owns its entry.

openTrayMenu() reassigned activeTrayItem before calling resetTrayMenu().
trayMenuOpener.menu binds to activeTrayItem.menu, so that reassignment
immediately swaps what the root opener's children expose -- invalidating
entries any live submenu opener still referenced, before resetTrayMenu()
got a chance to tear them down. Reset first, then switch items.

Thanks @Copilot for catching both.

* Defer submenu reset until the popup's fade-out actually finishes

onTrayMenuOpenChanged reset the submenu stack the instant trayMenuOpen
went false, but the popup stays visible for the whole 140ms opacity
fade (PopupCard's own visible: open || card.opacity > 0) -- dismissing
from a submenu flashed the root menu mid-fade, and could resize or
reposition the fading popup if the two have different geometry.

Moved the reset to trayMenuPopup's own onVisibleChanged, which only
fires once the fade has genuinely completed. Switching to a different
tray item is unaffected: openTrayMenu() already resets explicitly
before assigning the new item, independent of whether the popup ever
dips to invisible (rapid reopen mid-fade never does).

Thanks @Copilot for catching this.

* Ignore tray menu clicks for a beat after changing submenu level

Changing level swaps the Repeater's model, which rebuilds the row
delegates synchronously -- a fresh row lands under a cursor that hasn't
moved. Submenu clicks used to be silent no-ops, which trained users to
click them twice, so that second click now fires whatever entry took
the spot. On radiotray-ng that means an accidental station switch.

Gate row and back-header clicks for 250ms after each level change. A
deliberate follow-up click is slower than that; a double-click is not.

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

* Pin the submenu back header above the scrolling menu rows

The back header lived inside the Flickable's Column, so in a submenu
taller than the 420px cap -- exactly the long station list this
drill-down exists for -- scrolling down pushed the only way back off
screen, with no Escape or right-click alternative.

Move it into a pinned Column above the Flickable and account for its
height in the popup's contentHeight.

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

* Reset the tray menu scroll offset when the drill-down is torn down

Flickable keeps its contentY across a model swap whenever the new
content is still tall enough to hold it. A menu dismissed while
scrolled therefore reopened part-way down with its first entries off
screen: reproducible on any tray app whose root menu outgrows the
420px cap, and now reachable on every app once a long submenu has
been scrolled.

Zero the offset in resetTrayMenu(), which runs both on teardown and
before switching items.

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

---------

Co-authored-by: Toni Nowak <t.nowak@ai-flow.no>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 17:48:27 +02:00
Ryan Hughes 05bb82b34e Add checkout bin to sudoers path 2026-08-11 11:04:50 -04:00
Kevin McConnell 5817feb93f Mute all audio on right-click (#6708)
Previously, right-clicking the audio icon muted only the output. Whereas
the switch at the top of the panel mutes all audio (inputs and outputs).

Pairing the right-click to the switch seems a bit less confusing, and
also matches the behaviour with other panels (e.g. Bluetooth, Tailscale,
etc).
2026-08-11 16:30:11 +02:00
David Heinemeier HanssonandClaude Opus 5 1c9dfc55f4 Greet the first login with a keybindings toast again
Opening the cheatsheet outright put a menu in front of someone who had not
asked for one, and it blocked first run until they dismissed it. Go back to a
toast that opens the same menu when clicked.

The body carries real newlines now. It was written with a literal \n, which the
card renders as the two characters rather than a line break.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 05:32:47 -07:00
David Heinemeier HanssonandClaude Opus 5 77bf2ef704 Share files and folders with the desktop file chooser (#6707)
Sharing a file or folder over LocalSend opened a terminal to run an fzf
pick over a find of the whole home directory, which is slow on a large
home, shows no previews, and looks nothing like the rest of the desktop.
The portal chooser is already how the other pickers here ask.

The chooser has a directory mode, so folder sharing asks for one the same
way, and neither entry needs a terminal to host a picker anymore.

A chooser that never opens is told apart from nobody picking anything, so
a portal failure says so rather than passing for a cancelled share.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 14:24:17 +02:00
sergioandClaude Opus 5 27d1b6bebc Resolve omarchy_monitor_scale variable reference in clamshell recovery (#6688)
A pinned internal monitor rule that referenced the omarchy_monitor_scale local
had the variable name captured as the scale, so clamshell recovery fell all the
way to the hardcoded 2.

Read the config the way Hyprland writes it: a key's value is whatever sits
between the `=` and the next separator, and a bare word resolves against a local
only when one exists, so a quoted string stays a string. Comments are cut before
anything is matched, and position gets the same resolution since it had the
identical bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 13:58:47 +02:00
4d0531f351 Add QR code capture (#6705)
Select a screen region and decode the QR code in it to the clipboard, so
an otpauth:// setup code shown on screen no longer needs a phone.

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

Co-authored-by: Hlib Kanunnikov <hlibwondertan@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 13:56:09 +02:00
David Heinemeier HanssonandClaude Opus 5 3b08a85ad1 Recover monitors Hyprland brought up with no mode (#6701)
A monitor powered off when the machine boots — a smart strip cutting AC, the PC
coming back on its own — still answers DDC, but with a partial EDID that carries
no video modes. Hyprland takes the connector as present and brings the monitor
up at 0x0. Powering it on afterwards changes nothing: the connector never
dropped at DRM level, so no hotplug fires, nothing re-reads the EDID, and the
screen stays black until a reboot.

Only a reload re-reads it. Forcing a DRM re-probe would work too but needs root,
and the kernel's cached mode list stays empty without one, so there is nothing
cheaper to poll: the reload is both the fix and the only way to learn whether it
was needed. Poll only while a monitor is in that state, back off from three
seconds to a minute, and stop as soon as one reports a mode — the machine can
sit black all night, and powering the monitor on fires no event to stop on.

Nothing will ask again if this loop gives up, so an unreadable answer is not
taken for a healthy monitor. It is also not waited on forever: a compositor that
stays silent has gone, and with it the session and any reason to keep asking.

Reloading on our own schedule means minding the reload guard, which exists to
keep Hyprland out of package-owned config mid-transaction, and which only
disables the automatic reloads. The guard can now be asked, and recovery holds
off while a transaction is in flight.

A reload lands a monitor at 0x0 the same way a boot does, so configreloaded is
watched alongside the hotplug events. The recovery's own reload comes back
through it, and a lock keeps that from stacking a second loop. Contention waits
rather than drops: a trigger arriving while a loop is exiting is the last one
that will come, and one arriving while a loop is running is answered by its next
pass anyway.

Mirrors are dropped from `hyprctl monitors`, so the check asks for all of them
and filters the disabled ones itself. Monitors turned off on purpose sit at 0x0
too, and re-applying config would fight the user over those.

The existing poll here recovers internal panels on docked laptops and never runs
on a desktop, which is where this happens.

Reported in #6668.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 13:26:24 +02:00
David Heinemeier Hansson 9f0c4b9792 Ensure we don't register duplicate bindings 2026-08-11 13:20:23 +02:00
66f3155f0c Label the keyboard widget with the xkb language code (#6699)
* Label the keyboard widget with the xkb language code

The label was the first word of the layout description cut to three
characters, so a US layout read ENG and a Portuguese one read POR.

xkb already pairs every layout and variant with a short language code,
which is the code GNOME shows in its own indicator. Read that table once
at startup from xkbcli list and key it by description, which is what
hyprctl reports as the active keymap, so the same layouts read EN and PT.

The code is a language rather than a country, so it stays sensible for
the layouts named after neither: Esperanto is EO, Arabic is AR, and Latin
American Spanish is ES. Layouts missing from the table keep the old
truncated description.

* Read the exotic xkb rulesets for the keyboard label

xkbcli list leaves out the exotic rulesets, so layouts like trans were
missing from the table and fell back to the truncated description: the
IPA layout read INT rather than IPA. Those layouts ship in the same
xkeyboard-config package and set just as well, so read them too.

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

* Keep the keyboard label to three characters

The brief was used verbatim while the fallback was truncated, but not
every brief is two or three characters: Burmese (Zawgyi) is my-zwg and
Shan (Zawgyi) is shn-zwg. Selecting either widened the widget past its
neighbours on the bar. Drop the script suffix and cap the brief the same
way the fallback is capped, so those read MY and SHN.

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

* Stop an xkb brief carrying past its own block

The brief was only cleared once a description consumed it, so a block
printing a brief without one would hand its code to the next block's
description and label it wrongly rather than falling back. Nothing in
the current xkb data does that, and the option groups were skipped only
because the last layout happened to consume its brief first. Clear the
brief when a line starts a new block so the pairing is explicit, and
cover the option list the 2-space match is what keeps out.

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

* Fall back when a layout description names a built-in

A custom xkb group called constructor or toString reached an inherited
member of the lookup rather than a brief, and splitting it threw a
TypeError that took the whole label binding down instead of falling back
to the truncated description. Take the lookup only when it returns a
string.

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>
2026-08-11 13:05:11 +02:00
David Heinemeier HanssonandClaude Opus 5 5edc3497fa Bind SUPER + CTRL + a number to the bar's right panels (#6702)
The letters name a panel; the numbers count them. One is the leftmost
panel in the right section, so the number matches the icon a user would
point at: a widget with no panel of its own is passed over, and so is one
that is hiding itself.

Counting rather than naming means the hotkeys follow the bar. Rearranging
the section, or adding a widget to it, renumbers the panels with no
binding to rewrite.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 13:03:20 +02:00
David Heinemeier HanssonandClaude Opus 5 efe805387e Title a model-scoped limit the way the flat ones title themselves
A scoped window read as "Fable weekly" beside "Session" and "Weekly", so
the one row that names a model was also the one row in lowercase.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 12:43:03 +02:00
0ad64a59df Fix command injection in theme install, drop tzupdate NOPASSWD (#6694)
* Fix theme install code execution and drop tzupdate NOPASSWD

VULN-01 (C, D, E): a malicious theme can execute arbitrary code
during install through three injection sinks:

  C: colors.toml values reach a sed script unsanitized.
     GNU sed's `e` flag runs the pattern space as a shell command.

  D: vscode.json `.name` is interpolated into a sed replacement
     string without escaping sed metacharacters.

  E: keyboard.rgb content is interpolated into a python3 -c
     argument without validation.

Fix C by validating keys and values in omarchy-theme-color's parser
with a character allowlist. Byte-identical output for all 22 shipped
themes.

Fix D by escaping backslash, ampersand, and slash in the theme name
before sed interpolation.

Fix E by gating on ^[0-9A-Fa-f]{6}$ before interpolation, in both
the Framework 16 and ASUS ROG keyboard scripts.

VULN-02: the tzupdate sudoers grant has no argument constraint.
tzupdate -l lets any wheel user write a root-owned symlink to any
path. Drop it; nothing has invoked tzupdate since omarchy-cmd-tzupdate
was removed. Keep timedatectl set-timezone.

* Harden keyboard and vscode theme scripts

keyboard-f16: pass hex as sys.argv instead of interpolating into
python3 -c. The hex validation gate stays as the primary defense;
argv separation is defense-in-depth per OWASP guidance.

vscode: replace sed interpolation of theme name with jq, which
handles arbitrary strings safely via --arg. Validate extension IDs
against ^[a-zA-Z0-9._-]+$ before passing to --install-extension.

* Keep VS Code settings edits JSONC-safe

settings.json is JSONC, so routing the write through jq dropped theme sync
entirely for anyone with a comment or trailing comma in the file, including
the `{ "workbench.colorTheme": "",\n}` shape Omarchy itself creates. Edit in
place again and close the injection by validating the theme label instead.

Scope the extension-id guard to the install so a malformed id no longer skips
the colorTheme write, and treat a missing descriptor field as empty rather
than the literal string "null".

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

* Widen the accepted colors.toml value charset

The sanitizer dropped gradient angles, decimals, underscored palette
references, and paths, which vanish from --raw/--all and leave a raw
{{ placeholder }} in the generated config. Allow the punctuation real
palettes use, keep out everything sed treats as special, and say so on
stderr rather than dropping a key silently.

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>
2026-08-11 12:31:52 +02:00
David Heinemeier Hansson 9ea9f804cd Missed the glyph 2026-08-11 12:16:25 +02:00
2b38f75506 Show the per-model weekly limit Claude's usage endpoint reports (#6691)
* Show the per-model weekly limit Claude's usage endpoint reports

Model-scoped allowances arrive in the payload's limits array, not in the
seven_day_<model> buckets, which come back null. Read them so a window
like Fable's own weekly limit stops being spent against invisibly.

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

* Read every model-scoped window, and title it for what it is

A model can hold more than one scoped window, so keying the dedupe on the
model alone dropped whichever came second — including a fuller one that
decides the headline. The model and the window kind together make the key,
and both make the title, so one model's two rows read apart.

The panel guesses a window out of the label, and that guess cannot survive a
model name: "Opus 5 (1M context)" parses as a one-minute window and renders
as a second "Session". The collector states the title outright now and the
panel takes it, eliding a long one rather than running it into the percentage.

Scoped percentages are read on whatever scale the payload speaks, the way the
flat buckets already are, rather than assuming percentages, and a model that
names only an id still names a window worth showing.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-11 12:15:04 +02:00
David Heinemeier HanssonandClaude Opus 5 f97ba7375e Wait for a connection before prompting to update
A ping at hyprland.start answers for a machine that has not finished coming up.
Ethernet is still negotiating DHCP, so a working desktop was told to set up
Wi-Fi and offered an update it could already have run. Ask NetworkManager
instead: -s returns once it has tried every connection it could auto-activate,
which is the first moment the answer means anything, and -x then takes that
answer as it stands rather than waiting out a timeout that a laptop with
nothing to connect to would spend in silence.

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

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

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

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

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

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

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

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

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

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

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

* Retake a session lock stranded by a dead shell

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

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

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

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

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

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

* Relaunch the shell when it dies without a signal

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

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

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

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

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

---------

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

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

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

Fixes #6678

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

* Keep asking a shell that is still starting

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

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

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

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

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

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

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

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

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

* Wait for a shell that has not appeared yet

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

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

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

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

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

* Trim the comments on the bar put path

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

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

---------

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:58:16 -07:00
186668a70f Apply the Broadcom Wi-Fi quirk to Macs without a T2 (#6652)
* Apply the Broadcom Wi-Fi quirk to Macs without a T2

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

* Carry installed machines over to the rfkill block

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

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

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

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

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

* Read every controller and bound the power-up wait

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

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

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

* Change the radio through sudo in the migration

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

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 20:27:40 +02:00
567e24cd90 Hold the indicator peek open while the pointer is on the bar (#6663)
* Hold the indicator peek open while the pointer is on the bar

Revealing the hidden indicators widens their section, and a section that
grows can slide a neighbouring widget under a pointer that never moved.
Collapsing the peek on that un-hover narrowed the section again, moved the
neighbour back out, and re-opened the peek, so a pointer resting in the bar
space beside a grown section stuttered the bar until it moved away.

Hold the peek while the pointer is anywhere on the bar and close it only
once the pointer has left, which keeps the reveal-on-empty-space gesture
and drops the feedback loop.

Fixes #6581

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

* Assert the whole-bar hover helper does what the peek depends on

The earlier assertions all held against a no-op setBarHovered, which would
leave barHovered false and let the oscillation straight back in. Pin the
assignment and the collapse re-run too, so the helper cannot be emptied
without the suite noticing.

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

* Let the delayed peek re-check collapse only, never open

The timer assigned centerSectionRevealHeld outright, so it opened the peek
from bar hover alone. A pointer resting on the left section that dipped off
the bar and returned inside 120ms left the timer pending with barHovered
true again, and the indicators revealed without the pointer ever touching
the center section.

Opening stays the center section's own gesture in setCenterSectionHovered.
The timer now only closes what that opened, and the test asserts the
invariant against the whole file rather than one helper body that never
had the offending assignment in it.

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

* Tally bar hover per monitor instead of sharing one flag

Every screen's bar wrote the same barHovered bool, last writer wins. Sliding
along the top edge from one monitor's bar to the next can deliver the enter
before the leave, leaving the flag false under a live pointer; the collapse
then fired on a peek the user was still hovering, and no further hover change
arrived to correct it until the pointer left and came back.

Counting each surface's hover makes the order irrelevant. A bar destroyed
mid-hover — unplugging a monitor — never sends a leave, so it hands its
tally back on destruction rather than holding the peek open for good.

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

---------

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 10:17:08 -07:00
366c708e44 Keyboard window capture in the region picker: Return, Tab, arrows (#6466)
* Keyboard window capture in the region picker: Ctrl+Return, Tab, arrows

Return already captures the entire focused monitor while slurp is open,
but grabbing a single window required the mouse: hover highlights a
window, a click captures it. Now the keyboard can do the same.
Ctrl+Return captures the selected window — the one under the cursor,
falling back to the focused window. Tab and Ctrl+Tab cycle that
selection through the workspace's windows in reading order, and the
arrow keys move it spatially; both warp the cursor to the target
window's center, so slurp's own hover highlight tracks the selection.

Ctrl+Return is implemented like --take-fullscreen: a layer-scoped bind
flags the intent via a marker file and dismisses slurp, and the picker
resolves the window under the cursor. On an empty workspace the pick
resolves to nothing and exits as cancelled.

Ctrl is the chord modifier because slurp reacts to held Shift by
squaring the selection's aspect ratio, which visibly reshapes the
hover highlight mid-chord. None of the keys slurp itself uses (Escape,
held Space to move a selection, held Shift) are bound, so its own
keyboard behavior is unchanged.

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

* Resolve overlapping windows the way slurp highlights them

slurp highlights the smallest box under the cursor, but the capture and
selection hit-tests returned the first geometric match in hyprctl
clients order. With a floating window over a tiled one, Ctrl+Return
could capture the window underneath the visibly highlighted one. Track
the smallest containing window instead, in both the capture resolution
and the selection-movement origin.

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

* Document the region picker's keyboard modes in its usage line

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

* Resolve keyboard capture and navigation from one rectangle list

Duplicate window geometry (tabbed groups, stacked floating windows) stalled
the Tab cycle on the first copy, and the smallest-window-under-cursor
tie-break was applied to two differently ordered lists, so navigation could
cycle from one window while Ctrl+Return captured another. Ctrl+Return over a
gap or an empty workspace also ignored the monitor rectangle slurp was
highlighting.

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

* Capture the highlighted window with Return, the display with Ctrl+Return

The picker highlights a window under the cursor far more often than a whole
display, so the unmodified key takes the highlighted rectangle.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-10 19:12:11 +02:00
David Heinemeier Hansson 03b825f59a Show Herdr keybindings with SUPER + CTRL + K
Mirrors the Tmux keybindings menu on SUPER + ALT + K. Herdr has no CLI
that dumps resolved bindings, so the action list and its defaults come
from `herdr --default-config`, where every action appears as a commented
assignment, and the user config overrides what it sets.

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 05:58:56 -07:00
VivekandDavid Heinemeier Hansson 7633d8dee4 Keep the bar mapped while hidden so revealing it is instant (#6677)
* Keep the bar mapped while hidden so revealing it is instant

Hiding the bar set the panel invisible, which unmaps the layer surface and
releases the scene graph with it. Every reveal then had to rebuild all of
it: a new layer surface, a configure roundtrip, re-shaped glyphs and
re-uploaded textures, and a first frame before anything appeared.

Measured on a 2560x1440 screen, showing took 155-175ms against 20ms to
hide, and 400-595ms on the first reveal after a cold start. Splitting the
cost showed the exclusive-zone reflow was not to blame: show latency was
the same on an empty workspace as on a tiled one, and windows finished
moving ~15ms after the bar was already on screen.

Park the bar one bar-width past its anchored edge instead, and drop its
exclusion zone while hidden. The surface stays alive, so showing is only
a margin change: 10-14ms in both directions, at every bar position.

Since a hidden bar is now mapped, layer_present no longer proves the bar
is visible; the session acceptance test asserts on-screen geometry.

* Fix layer visibility checks on offset monitors

* Handle rotated outputs in layer visibility checks

* Cover hidden bar behavior in acceptance tests

---------

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

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

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

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

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

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

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

* Need a migration

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

* Always grant wheel sudo in OEM first-boot setup

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

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

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

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

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

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

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

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

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

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

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

* Harden reset and first-boot setup failure paths

Review findings from codex and Copilot:

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

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

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

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

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

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

Codex xhigh round 2:

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

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

* Expose factory reset in the Setup menu

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

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

* Defer keyboard selection to first boot for OEM installs

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

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

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

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

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

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

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

* Rename OEM vocabulary to provisioning (runtime)

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

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

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

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

* Drop remaining OEM mentions from the provisioning groups test

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Add a Welcome greeter before first-boot setup

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

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

* Center the greeter and show the Omarchy tagline

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

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

* Animate the greeter logo with a skippable ColorShift

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

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

* Theme the greeter ColorShift to the Omarchy palette

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

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

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

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

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

* Kill the greeter animation cleanly so the form starts fresh

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

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

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

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

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

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

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

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

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

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

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

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

* Redraw the greeter on console resize instead of measuring once

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

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

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

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

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

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

Three changes to the deferred first-boot setup:

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

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

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

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

* Autologin only the first deferred boot on unencrypted installs

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

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

* Ask the owner's hostname at first boot too

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Bring back the greeter ColorShift animation

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Drop Bosnian and Khmer from the keyboard picker

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

* Always grant wheel sudo in OEM first-boot setup

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

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

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

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

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

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

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

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

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

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

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

* Harden reset and first-boot setup failure paths

Review findings from codex and Copilot:

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

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

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

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

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

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

Codex xhigh round 2:

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

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

* Expose factory reset in the Setup menu

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

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

* Defer keyboard selection to first boot for OEM installs

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

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

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

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

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

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

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

* Rename OEM vocabulary to provisioning (runtime)

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

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

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

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

* Drop remaining OEM mentions from the provisioning groups test

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

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

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 21:54:21 +02:00
9b03f15b4f Detect Elan match-on-chip fingerprint readers again (#6578)
* Detect Elan match-on-chip fingerprint readers again

Elan readers report "ELAN:ARM-M4" as their product string, so the
*fingerprint* and *biometric* checks miss them. Elan's 04f3 is also left
out of the vendor list on purpose, because Elan makes touchscreens too.
Both checks fail, so the machine looks like it has no reader.

Add "elan:arm-m4" to the product string check. The comment above the
vendor list already says the excluded vendors should still match there,
so this makes that true. The vendor list and its has_kernel_driver guard
are unchanged, and touchscreens still cannot cause a false positive.

The string is a family name, not one device. libfprint uses it for
04f3:0c9c and 04f3:0ca7 as well as 04f3:0ca8.

Tested on an HP EliteBook X G2i with 04f3:0ca8.

* Point the Elan comment at the vendor list above it

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

* Configure PAM only after a fingerprint enrolls and verifies

Detection proves a reader is present, not that libfprint can drive it.
Elan MOC sensors outside the elanmoc table pass the gate and then fail
to enroll, which left pam_fprintd in the sudo and polkit stacks with no
print to match.

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>
2026-08-09 21:54:01 +02:00
David Heinemeier HanssonandClaude Opus 5 e1d0c4e0a8 Ship the keyboard layout widget on the bar and make clicking it work (#6659)
* Hide the keyboard layout widget on a single-layout install

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

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

* Put the keyboard layout widget on the bar by default

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

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

* Cycle the layout with the hyprctl command that exists

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

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

* Add an idempotent bar add command

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

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

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

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

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

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

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

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

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

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

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

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

* Rename bar add to bar put

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

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

* Place a newly added bar widget with bar put

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

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

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

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

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

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

---------

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

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

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

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

* Detect dead SSH connections within a minute

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

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

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

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

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

* Tolerate the explicit RemoteCommand none when probing ssh -G

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 19:15:21 +02:00
9d61915b2e Fix KeyboardLayout plugin label permanently displaying ENG on some systems (#6646)
* Fix hyprctl output parsing in KeyboardLayout plugin

* Never fall back to a non-active keyboard for the layout label

find(k => k.main) returning nothing fell through to keyboards[0], which is
the case the fix is for: on hardware whose first device is a permanently
English (US) radio-control keyboard, the label was wrong and the 10s poll
kept it wrong. The seat can also hold no active keyboard while a device is
re-added, and older Hyprland has no main field at all. Keep the last known
value instead, and skip entries without an active_keymap, since assigning
undefined to the string property throws before the label is ever set.

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

* Run hyprctl directly from the keyboard layout widget

The shell wrapper only existed for a pipeline that is gone, so spawn the
command directly, as Style.qml already does for its own hyprctl query.

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

* Read the layout from the keyboard the user types on

Every Omarchy install runs fcitx5 for ~/.XCompose, and it binds a virtual
keyboard that takes the seat's main flag whenever it injects. That keyboard
keeps the us layout the input method gave it, so on a machine configured for
another layout the widget flipped to ENG and the poll kept it there until the
next physical keypress. Skip virtual keyboards and hold the last known layout
instead, which the next poll corrects once a real keyboard is active again.

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

* Keep tracking the keyboard the layout was last read from

Holding a frozen label while fcitx5 owns the main flag went stale as soon as
the layout changed underneath it, and cycling still dispatched against
"current", which is that same virtual keyboard. Remember the keyboard the
label came from, re-read its layout on every poll, and cycle it by name so
the widget shows and switches one device.

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 05:11:43 -07:00
c4dda58ba2 Stop the clipboard picker freezing on huge pastes (#6568)
Every keystroke in the search box scanned, lowercased, and split the
full text of every history entry, and the preview pane laid out the
entire selection with WrapAnywhere. A single 1.6MB paste (or a large
file selection) turned that into hundreds of megabytes of work on the
shell thread and stalled the render thread — freezing the whole
desktop.

Cap each entry once as it enters the display, so searching, previewing,
and rendering all work on a bounded prefix. Pasting reads the full entry
back from history by index, so nothing is actually lost. The cut lands
on a line break, keeping a file:// URI from truncating into a bogus path.

Co-authored-by: markbusking <marcosbustos.dev@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:01:37 +02:00
David Heinemeier HanssonandClaude Opus 5 dd61d4a75b Ship herdr alongside tmux (#6406)
* Ship herdr with a config that mirrors our tmux setup

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

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

* Add herdr versions of the tmux dev layout functions

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

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

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

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

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

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

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

* Lay hsl's grid out in visual order

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

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

* Report herdr config reload failures instead of swallowing them

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

* Hide herdr's pane scrollbars to match tmux

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

* Escape queued herdr layout commands

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

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

* Rename the omarchy-herdr package to herdr

---------

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 04:29:26 -07:00
Luke Hsiao 0795ede770 Fix invisible Helix cursorline, ruler, and picker highlights (#6638)
afa2839a5a (Rename bg/fg palette keys to background/foreground)
aliased `color0` to plain background in theme color resolution, but
the Helix template still uses `color0` as the subtle surface shade
behind `ui.cursorline.primary`, `ui.virtual.ruler`, `ui.highlight`, and
`ui.text.focus`. All four now render at exactly the background color
and vanish: no cursorline, no ruler, and the picker's focused row is
only distinguishable by its bold text. This is the same failure that
d80c98f025 ("Make color0 distinct from background/foreground and fix
helix theme", #5538) fixed back when themes defined color0 directly.

Point those scopes at `lighter_background`, the semantic key for a
surface one step off the background. Every first-party theme defines it
distinct from background except Last Horizon and Solitude, which
set the two equal on purpose; those, and legacy themes whose
`lighter_background` falls back to background, render exactly as they
do today.

Assisted-by: Claude:claude-fable-5

Signed-off-by: Luke Hsiao <luke@hsiao.dev>
2026-08-09 13:28:17 +02:00
LucaandLuca 2a0c7371a5 Claude collector: attribute pi usage by provider, not api prefix (#6655)
Pi/omp sessions kept falling into the Claude record when the api
field merely started with 'anthropic'. Kimi and other providers that
speak the anthropic-messages protocol (kimi-coding) were therefore
charged against Claude Code, showing k3 buckets under a Claude tab
even for a user without a Claude login.

Match the codex collector's provider-only attribution: only sessions
whose provider is exactly 'anthropic' count toward Claude usage.

Add a test proving a kimi-coding session sharing the
anthropic-messages api does not land in the Claude record.

Co-authored-by: Luca <luca@itwasarch>
2026-08-09 13:27:38 +02:00
David Heinemeier HanssonandClaude Opus 5 8be0ca9d43 Expect Tailscale on the right in the bar defaults test
99293aa gave the Tailscale widget a right defaultSection, but the test
still asserted it landed in center after the weather widget, and that
dropbox was the first widget after the tray.

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

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

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

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

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

* Check for the legacy UKI as root when refreshing limine

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

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

---------

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

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

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

* Recover a locked session whose lock client died

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

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

---------

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

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

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

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

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

* Keep a failed toast from failing the work it announces

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

Fixes #6465

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

* Spacing

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 11:05:05 +02:00
David Heinemeier Hansson 47fa3ce3a5 Match style from other updates 2026-08-08 01:44:56 -07:00
81ecac4a60 Fix WhatsApp Slim migration targeting brave-origin-beta instead of brave-origin (#6482)
* Fix WhatsApp Slim migration targeting brave-origin-beta instead of brave-origin

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

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

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

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

---------

Co-authored-by: akashgagda <akashgagda@users.noreply.github.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 10:26:58 +02:00
David Heinemeier HanssonandClaude Opus 5 318bf2c43a Keep the Quattro upgrade from aborting silently into an unsafe state (#6617)
The script is fetched from the branch but calls into the installed
/usr/share/omarchy tree, which can lag it. A packaged build without
bin/omarchy-done aborted apply_user_transition under set -e two thirds of
the way through: NetworkManager was already enabled, iwd was not yet
disabled, and nothing was printed, so the run read as finished.

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

Fixes #6575

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 10:16:41 +02:00
Mr. Mendez 5e5a6e8c89 stop omarchy-mise-install from overwriting symlink targets (#6619) 2026-08-08 08:58:12 +02:00
Scott JonesandClaude Opus 5 2b9e2720b3 Run every test file instead of stopping at the first failure (#6622)
test/shell and test/all inherit `set -euo pipefail`, so the first failing
test file aborts the whole run. One failure then hides every file behind it:
you fix it, rerun, discover the next one, and repeat a file at a time. On a
140-file suite a single unrelated failure can keep most of the suite from ever
reporting.

Keep going after a failing file, then list the files that failed and exit
non-zero. Individual files still stop at their own first failed assertion, so
per-file isolation is unchanged, and a clean run still exits 0.

The files are already independent of each other -- the set of failures is the
same whether the run continues or stops at the first one -- so nothing was
relying on the early abort.

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

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

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

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

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

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

Fixes #6558

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

---------

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

Fixes #6590

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

Fixes #6576

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Drop the menu aliases from the speed test entries

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

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

* Group dial readouts with thousands separators

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

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

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

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

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

* Publish the specific speed test error whichever handler fires last

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

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

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

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 23:04:42 +02:00
Mike Dalessio b16ac48693 Add commented-out debug symbol repositories to the edge pacman config (#6610)
The pacman configuration says nothing about debug symbol packages. Someone who
wants `glibc-debug` so that gdb or Valgrind has symbols to work with has to
discover on their own that Arch publishes `-debug` repositories, find a mirror
that serves them, and work out that the versions only line up while the system
tracks Arch current.

Appending the three repository sections to `pacman-edge.conf`, commented out,
will put them within reach without turning them on. The comment records that the
packages match the edge channel only, and that debuginfod already covers most
debugging on edge, so enabling them is often unnecessary. Only the edge template
gets them, because on stable and rc the upstream debug packages are newer than
the pinned packages they would need to match.

ref: https://github.com/basecamp/omarchy/discussions/6185
2026-08-07 20:33:56 +02:00
3bfea9b840 Fail loudly when a pre-update snapshot isn't actually created (#6580)
* Fail the snapshot when Snapper is installed but has no configs

omarchy-snapshot create loops over the configs snapper reports. With none,
the loop body never runs, so it prints "Create system snapshot" and exits 0
without capturing anything. Every update then reports a snapshot it never
took, and the absence only surfaces when a rollback is needed and the
snapshot list turns out to be empty.

* Say so when the update proceeds without a snapshot

The update ignores exit 127 so a system without snapper updates quietly.
Any other snapshot failure was being swallowed by the same expression,
which let the update continue with no indication that it was now
unprotected. Keep continuing, but say it out loud.

* Point the snapshot repair hint at how the installer runs it

Also hold the green header until a snapshot will actually be attempted,
so the no-config failure doesn't open with a success banner.

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

* Continue the quattro upgrade when the pre-upgrade snapshot fails

The upgrade runs under set -e, so the new non-zero exit from an
unconfigured Snapper would have aborted a re-run at the snapshot step
instead of proceeding like omarchy-update does.

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

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 18:44:37 +02:00
9d5c6e25e7 Keep root= in the kernel cmdline when upgrading to Quattro (#6579)
* Keep root= in the kernel cmdline when upgrading to Quattro

The packaged drop-in /etc/limine-entry-tool.d/omarchy-defaults.conf sets
KERNEL_CMDLINE[default] with the += operator. limine-entry-tool.conf documents
what that costs: "+= appends parameters to an existing cmdline ... Ignores
/etc/kernel/cmdline and /proc/cmdline". As soon as the drop-in lands, the tool
stops auto-detecting the cmdline.

Fresh installs are unaffected, because the ISO writes /etc/default/limine from
default/limine/default.conf with @@CMDLINE@@ substituted. The upgrade path never
created that file. A pre-quattro install that relied on the auto-detected root=
therefore ends up with a cmdline that has no root= at all, in both limine.conf
entries and in the cmdline embedded in the UKIs. The next boot fails with
"ERROR: Failed to mount '' on real root" and drops to an emergency shell, where
the error gives no hint that the cmdline is the cause.

Capture the boot-critical parameters from /proc/cmdline before the reboot, while
the still-correct cmdline of the running kernel is readable, and write them to
/etc/default/limine, which is loaded last so += keeps the drop-in parameters
instead of replacing them. Copy root=, rootflags, rootfstype, resume,
resume_offset, the cryptdevice and rd.luks keys and rw/ro verbatim rather than
reconstructing them, so LUKS and hibernation setups survive too.

Re-running the upgrade on an already-broken system has no root= left to copy, so
fall back to deriving it from the mounted root, including the subvolume on
btrfs. Any config layer that already pins root= is treated as authoritative and
left untouched.

* Anchor the cmdline guard and harden the repair path

The early-return guard searched for the bare string root=, which matches the
commented example limine-entry-tool.conf ships at line 53:

  #KERNEL_CMDLINE[default]+=rw root=UUID=...

That file is present on every stock machine, so the guard always fired and the
function never wrote anything. Match assignments instead, and check
/etc/kernel/cmdline separately since it holds bare parameters rather than shell
assignments.

Three fixes on the repair path:

Assigning boot_params discarded every parameter the collection loop had just
captured, so cryptdevice, cryptkey, resume and ro were dropped, and rw was
forced over a captured ro. Prepend the derived root= instead, and only add rw
when the booted cmdline stated no mount mode.

On an encrypted root, findmnt reports the unlocked mapper device, whose UUID
says nothing about which container to unlock. Emitting it produced a cmdline
that still could not boot while satisfying the final check, so the user rebooted
into the same emergency shell believing it was repaired. Warn and write nothing
in that case.

The allowlist gained rd.luks.key, rd.luks.crypttab, rd.md.uuid, rd.dm.uuid,
rootwait, rootdelay and dm-mod.create.

Verification now also reads the .cmdline section of each UKI. With
omarchy-uki.conf among the drop-ins that embedded copy is what actually boots,
so a green limine.conf alone did not prove the machine would come up.

* Filter guard paths and narrow the dm-crypt and UKI checks

The guard passed /etc/default/limine to grep unconditionally, and that file is
absent on exactly the machines this targets. A missing operand makes grep exit 2
without -q, so a drop-in pinning a real root= went undetected and the function
appended a second one, overriding the explicit setup it promises to leave alone.

Rather than relying on -q returning 0 despite the error, which is a GNU grep
special case and not true of every implementation, filter the paths first and
only grep the ones that exist. The exit status is then unambiguous.

The dm-crypt check gated on the /dev/mapper/* prefix, which also matches plain
LVM, dm-raid and multipath. Those roots need no unlock parameters and were
repairable before, so the prefix test denied them a working root=UUID= and told
them they were encrypted. Gate on the device mapper target type instead.

root_filesystem_encrypted() is not reused here on purpose: it treats every
/dev/mapper/* path and any non-empty /etc/crypttab as an encrypted root, which
suits its own call site but would reintroduce the same false positive.

UKI verification now runs through as_root, since a restrictive ESP fmask would
otherwise make find return nothing and the check pass in silence, and is scoped
to the omarchy_linux*.efi images limine-entry-tool generates so a shared ESP or a
stub without a .cmdline section cannot raise a false "do not reboot" warning.

The allowlist gained rd.lvm.lv and rd.lvm.vg.

* Strip the subvolume before resolving the root device type

findmnt appends the subvolume for btrfs mounts, so the source read back for an
encrypted btrfs root is /dev/mapper/cryptroot[/@]. lsblk cannot resolve that
path, the device type came back empty, and the crypt gate never fired. The
function then wrote root=UUID= with no unlock parameters, limine.conf ended up
carrying a root= so the final check stayed quiet, and the machine still booted to
an emergency shell. That is the layout Omarchy installs when encryption is
picked, so the gate missed exactly the roots it exists for.

The previous /dev/mapper/* prefix test matched the bracketed form by accident.
Moving to the device mapper target type is still the right call, it just needs
the unbracketed source, which findmnt --nofsroot provides.

Also give root_type an explicit empty default. It is assigned inside a branch and
read outside it, and set -u treats a declared-but-unassigned local as unbound, so
a findmnt that cannot answer would abort the upgrade with the quattro packages
already installed and everything from configure_snapper_policy onward skipped.

* Look for the crypt layer across the whole device stack

lsblk -no TYPE reports only the target's own type. On the standard full-disk
encryption layout, LUKS container -> LVM PV -> root LV, that type is lvm and the
crypt layer sits in the parents, so the gate never fired: the function wrote
root=UUID= with no unlock parameters, the final check found a root= and stayed
quiet, and the machine still booted to an emergency shell.

Walk the parents with lsblk -s and look for a crypt layer anywhere in the chain.
That keeps LVM, dm-raid and multipath roots on the repair path, since they carry
no crypt layer and root=UUID= is enough once mkinitcpio assembles them.

root_type is replaced by root_stacks_crypt, which says what is actually being
tested and drops the LVM-versus-crypt caveat the old target-type check needed.

Also drop a vacuous test assertion: piping a bracketed literal through grep -qv
'\[' selects nothing, so the branch was unreachable and the case passed whatever
the script did. The --nofsroot assertion above it is what holds that fix.

* Keep the crypt gate off a pipeline exit status

Capture the device stack and match it from a here-string rather than piping lsblk
into grep -q. Under pipefail a short-circuiting grep can leave the producer with
SIGPIPE and turn the pipeline into 141, which reads as "no crypt layer" and
disarms the gate silently. lsblk writes its whole table in one go, so this is out
of reach in practice, but nothing about the gate should depend on how much output
a helper happens to buffer.

Also correct a stale test comment that described the target-type check the
previous revision used, four lines above the comment explaining why that check
was insufficient.

* Harden the kernel cmdline preservation against false root= pins

The /etc/kernel/cmdline early return trusted a file limine-entry-tool
ignores once a += drop-in sets KERNEL_CMDLINE[default], leaving exactly
the targeted machines unbootable. The pin guard now reads only the
*.conf layers the tool loads, only the default key, and tokenizes the
assignment value so quoted decoys and volatile-root= cannot pin.
/proc/cmdline is tokenized quote-aware so dm-mod.create="..." survives
verbatim, the root= verification is token-anchored, and an unverified
cmdline now blocks the reboot instead of only warning.

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

* Ask limine-entry-tool for the effective cmdline instead of parsing its configs

--get-cmdline default answers whether root= survives the tool's own
config merge, replacing the glob, grep and quote-aware tokenizer walk
over the config layers, and the quote-aware /proc/cmdline parsing
reverts to plain word splitting. The verification and the reboot gate
stay: they are what catches anything the simpler paths miss.

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: shrijit <shrijitsrivastav@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 17:32:36 +02:00
f490b69a20 Reopen the wifi passphrase prompt after a wrong saved password (#6584)
* fix: reopen wifi passphrase prompt after a wrong saved password

A failed first connection attempt leaves the network profile saved, so the
network shows up as known. Clicking it again reconnects with the stored
wrong PSK and fails with WifiAuthTimeout, but the inline passphrase prompt
only reopened on NoSecrets, leaving no way to re-enter the password short
of forgetting the network.

Treat an auth timeout on a protected network as a wrong saved passphrase
and reopen the prompt; connectWithPsk overwrites the stored PSK on submit.

Fixes #6582

* Scope the wifi passphrase reprompt to panel-initiated connects

Background auto-connect retries also fire connectionFailed; without a
gate they would pop the passphrase prompt open unbidden, stealing focus
and wiping a passphrase mid-entry when another network fails.

For the gate to see the failure, the action safety-net timer must
outlast NetworkManager's 25s supplicant timeout -- at 15s it cleared the
action state before WifiAuthTimeout arrived, so a wrong saved password
showed "Timed out connecting" instead of "Wrong password". Bump it to
30s.

Also share the one ConnectionFailReason map between the Model.js
helpers instead of building a second partial copy inline.

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Drop the scan infix from the collector command names

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

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

* Keep the credential store out of the printed usage record

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 15:46:10 +02:00
c3bd4a86ae Fix unbound XKBLAYOUT under set -u in the keyboard-layout migration (#6539)
* Fix unbound XKBLAYOUT under set -u in the keyboard-layout migration

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

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

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

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

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

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

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

* Read the keyboard layout from vconsole.conf alone

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

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

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

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

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

* Drop the test scaffolding from the keyboard-layout migration

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

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

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

---------

Co-authored-by: Toni Nowak <t.nowak@ai-flow.no>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:43:45 +02:00
David Heinemeier HanssonandClaude Fable 5 48d77b5738 Persist on-screen notification popups across shell restarts (#6600)
* Persist on-screen notification popups across shell restarts

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

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

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

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

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

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

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

---------

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

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

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

* Answer repeated menu guard questions once per evaluation

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

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

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

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

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

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

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

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

* Read every provide pacman reports, wrapped or not

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

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

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

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

* Add capture and contributing guides to the omarchy skill

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

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

* Scope Hyprland reload guidance to the Lua config files

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

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

---------

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

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

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

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

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

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

---------

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

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

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

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

* Avoid repeated T2 boot image rebuilds

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

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

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

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

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

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

* Match terminals by tag for universal clipboard shortcuts

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

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

---------

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

Fixes #6554

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 22:22:24 +02:00
5d13b53eb5 Pin network overlay text to a fixed light palette so every theme stays legible (#6529)
* fix(network): pin overlay text to a fixed on-scrim palette

SpeedTestPanel and WifiQrPanel both draw over a hardcoded near-black
scrim, but their text/tick colors came from bar.foreground -- a color
themed to contrast with the *bar's* own background, which flips
dark/light per theme. On themes with a dark bar.foreground, digits
and labels went invisible against the black scrim, leaving only the
accent-colored arc/needle (Color.accent) visible.

Add a fixed white-based on-scrim palette (onScrim/onScrimDim) to both
overlays and route all text/tick colors through it, independent of
theme. bar.urgent stays theme-driven since it's a semantic color
already legible on near-black.

* Trim the on-scrim palette comments to the constraint

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

* Pin overlay error text to a fixed on-scrim urgent color

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

---------

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 08:30:12 -05:00
Luke WalkerandDavid Heinemeier Hansson caeffdc27b fix: add bg/fg aliases for theme color resolution (#6546)
* fix: add bg/fg aliases for theme color resolution

Themes define bg/fg but the template system expects
background/foreground. The fallback chain only checked color0/color7,
leaving background/foreground empty for themes using bg/fg naming.

* Complete legacy theme palette compatibility

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-04 13:26:26 -05:00
David Heinemeier Hansson fd02c26230 Matching browser icons 2026-08-04 12:49:28 -05:00
Diogo FerreiraandDavid Heinemeier Hansson 40f92eabdf Launch apps in their own scope instead of the compositor's cgroup (#6541)
* Launch apps in their own scope instead of the compositor's cgroup

The launcher ran desktop entries through gtk-launch, so the app inherited
quickshell's cgroup, which belongs to wayland-wm@hyprland.desktop.service.
A kernel OOM kill there fails the compositor unit and tears down the whole
session, dropping the user at SDDM with every window lost. A single runaway
app took the desktop down three times in one afternoon.

Route launches through uwsm-app so each app gets its own scope under
app-graphical.slice. A runaway app now fails its own scope and the session
keeps running.

The post-install launches had the same inheritance bug in a milder form,
where the app landed in the installer terminal's scope and died with it.
0aedef58 patched that with setsid, which detaches the session but leaves
cgroup membership behind. A scope fixes it properly.

* Detach post-install app launches

* Preserve desktop entry launch compatibility

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-04 12:16:54 -05:00
David Heinemeier Hansson fe55ac264d Merge pull request #6498 from basecamp/add-default-agent
Add configurable default coding agent
2026-08-04 12:09:10 -05:00
David Heinemeier Hansson 3636c76e96 Rely on the session PATH for agent launches 2026-08-04 10:04:26 -07:00
David Heinemeier Hansson 884ee6943a Use official agent marks in default menu 2026-08-04 10:04:21 -07:00
David Heinemeier Hansson f293e9df26 Open agents after setting the default 2026-08-04 09:03:50 -07:00
David Heinemeier Hansson a7cc924a10 Show agent installation progress in a terminal 2026-08-04 07:45:36 -07:00
David Heinemeier Hansson 2324851da1 Fall back to OpenCode in agent launcher 2026-08-04 06:06:04 -07:00
David Heinemeier Hansson 5ea4fb56f4 Add default agent shortcuts 2026-08-04 05:53:30 -07:00
David Heinemeier Hansson ce93c31af6 Merge branch 'quattro' into add-default-agent
# Conflicts:
#	migrations/1785633225.sh
2026-08-04 05:33:49 -07:00
David Heinemeier HanssonandClaude Fable 5 c992cdff10 Restart the shell unconditionally after every update
Updates routinely replace the shell's QML, and a stale process can
lazy-load new files into old code. Restarting at the end of every
omarchy update removes the need for migrations to restart the shell
or defer one with the restart-shell-required marker: the login-time
migration path already runs a fresh shell that hot-reloads shell.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 16:52:02 -05:00
Scott JonesandClaude Opus 5 72ffd58316 Offer AM/PM clock formats when right-clicking the bar clock (#6536)
Every preset in the right-click ring was 24-hour, so a 12-hour label was
something you had to hand-write into shell.json. Pair each locale-shaped
time preset with its AM/PM twin, and give vertical bars one stacked
variant. The ISO preset keeps its 24-hour clock, since ISO 8601 writes
time that way.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 16:29:14 -05:00
David Heinemeier Hansson 929eca317d Add a Super + Shift + = hotkey for calculator 2026-08-03 16:24:14 -05:00
David Heinemeier HanssonandClaude Fable 5 7e9cc153db Retry failed weather fetches so the bar icon can't stay stuck
The Open-Meteo fetch is the only thing that updates the bar icon when a
location is configured, but a failed response was dropped silently with
no retry, leaving a stale icon until the next refresh tick. Give it the
same short retry loop the wttr fetch already has, and reset both retry
budgets on each full refresh cycle so an exhausted round (e.g. waking
before the network is back) doesn't starve retries for the session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 15:40:22 -05:00
David Heinemeier Hansson 12af188304 We don't need rust any longer
Was only needed for before ruby precompiles via mise
2026-08-01 22:31:12 -05:00
David Heinemeier HanssonandClaude Fable 5 c2889be493 Add Setup > Network menu with DNS, QR Code, and Speed Test (#6499)
* Make omarchy-network-qr detect the connected Wi-Fi interface

The interface argument is now optional so IPC and menu callers can
summon the QR card without knowing the device name.

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

* Move the speed test into a modal card with cluster dials

The network panel's Run button and the new omarchy.network speedTest
IPC route open a centered card where download and upload dials sweep
on open and track the live readings, Tucson style. Dismissing the card
stops the traffic workers. The QR card gains a showQr IPC route.

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

* Add Setup > Network menu with DNS, QR Code, and Speed Test

DNS switches providers through omarchy-dns with the current choice
checked. QR Code only shows while connected over Wi-Fi.

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

* Hold the speed test card steady and add a corner dismiss

The Run Again button now fades instead of unmounting so the card keeps
its size across runs, and a small X in the corner closes the card
alongside Esc and the scrim.

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

* Move the speed test action into the network panel hero

A speedometer icon beside the QR share replaces the dedicated inline
section, and the keyboard chain loses its speed stop accordingly.

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

* Float the speed test cluster on the scrim

Drop the bordered card and the pulsing halo: like the Tucson's floating
cluster, the dials now sit directly on a near-black scrim with a soft
under-glow along the value arc, fainter ticks, and hubless needles that
fade toward the pivot.

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

* Center the retry button between the dials

The measuring status lines and the corner dismiss go away; the retry
button moves into the gap between the two dials like a cluster's center
display, anchored out of the column flow so nothing ever shifts. The
fast.com attribution lives on as its tooltip, and only errors still
print below the cluster.

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

* Put the retry button back beneath the dial pair

The dials close ranks again and the retry button returns below them,
centered on the pair and still fading in place so nothing shifts.

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

* Harden the network IPC routes against stale panel state

The QR menu route forces interface self-detection instead of trusting
details that stop refreshing while the panel is closed, and the
widget's canonical close now tears down the centered cards and their
traffic instead of only hiding the compact panel.

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

* Re-range the speed dials for every run

The scale latched upward forever, so one unusually fast run would
compress every later one for the lifetime of the shell process. Each
dial now returns to the base scale when its measurement starts.

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

* Float the Wi-Fi QR share like the speed test

Same presentation as the dials: no bordered card, just the code on a
heavy scrim. Only the dark modules paint now, so the white canvas can
round its corners while the spec quiet zone keeps the code clear.

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

* Pick the default-route device and pin the locale in QR detection

nmcli localizes state names, so the detection fallback pins LC_ALL=C
and accepts states like "connected (externally)". Detection now prefers
the default-route device, matching the connection the panel and the
menu's visibility gate describe when several Wi-Fi adapters are up.

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

* Make every network summon path overlay-aware

Opening the widget while a centered card is up now dismisses the card
instead of raising the compact panel behind an exclusive overlay --
the shadowed open() covers the keybind toggle, the bar icon, and IPC.
Re-summoning a card while its process is still tearing down queues the
fresh request for onExited instead of dropping it, and the speed test
route refreshes connection details like the QR route does.

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

* Shrink the overlays to fit narrow outputs

The dial cluster and the QR card scale down instead of clipping when a
portrait or heavily scaled display is narrower than their natural size.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 22:15:46 -05:00
David Heinemeier HanssonandClaude Fable 5 abe5b1ff9f Silence jq noise when the reload guard probes dead Hyprland instances
hyprctl prints "Couldn't connect ..." on stdout for stale instance dirs
left in /run/user/*/hypr/, so jq's parse error leaked into pacman's
pre-transaction hook output. The dead instances were already skipped
correctly; only the stderr noise escaped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ls3ump7hcv4oNnjWW5AXmn
2026-08-01 22:12:01 -05:00
David Heinemeier HanssonandClaude Fable 5 9ca5f63f86 Speed up mouse scrolling in foot
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 21:56:51 -05:00
David Heinemeier Hansson 7b4fb0ad6b Fix default agent setup edge cases 2026-08-01 19:52:04 -07:00
David Heinemeier Hansson 1f8819318d Switch to Omacalc 2026-08-01 19:32:32 -07:00
David Heinemeier Hansson 6009e8a73b Sort default agents alphabetically 2026-08-01 19:16:11 -07:00
David Heinemeier Hansson a9b84cea8e Refine default agent menu 2026-08-01 19:11:44 -07:00
David Heinemeier Hansson a0e2501e15 Add configurable default coding agent 2026-08-01 18:49:28 -07:00
David Heinemeier HanssonandDataDave-Dev 5b69af693f Fix background cycling for glob characters
Co-authored-by: DataDave-Dev <153755137+DataDave-Dev@users.noreply.github.com>
2026-08-01 18:13:45 -07:00
David Heinemeier HanssonandClaude Fable 5 87940c3cf4 Pick branding images with the portal file chooser
omarchy-file-select gains an --extensions filter, passed through the
portal's native filters option, so the branding commands can offer
only png/svg instead of walking a find-generated menu of Pictures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 19:55:02 -05:00
David Heinemeier HanssonandClaude Fable 5 84e87ce67e Add oh-my-pi (omp) as a lazy-installed mise tool
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 19:55:02 -05:00
David Heinemeier HanssonandClaude Fable 5 f54edbeba8 Expose the shell's remaining UI-only toggles over IPC
Every toggle you could only reach by mouse or panel hotkey is now
scriptable through omarchy-shell:

- omarchy.bluetooth toggleBluetooth — the radio (was right-click / B)
- omarchy.network toggleNetwork — Wi-Fi on/off (was W in the panel)
- omarchy.tailscale toggleTailscale — the connection as one call,
  matching the widget's right-click, instead of separate up/down
- omarchy.clock toggleWeekStart — calendar week start (was W)
- shell toggleBarTransparency — the bar background (was manage UI only)

Bluetooth and network follow the power/monitor pattern: manageIpc: false
so the panel owns the target's single IpcHandler and can extend it past
the inherited lifecycle methods.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 15:44:07 -05:00
David Heinemeier Hansson 2cff67fa45 Fix sleep lock session environment race (#6493)
Found by @tobi.
2026-08-01 15:38:55 -05:00
David Heinemeier HanssonandClaude Fable 5 0af39c5260 Expose the battery percentage toggle through omarchy.power IPC
omarchy-shell omarchy.power togglePercentage now flips the same setting
the widget's right-click does. Follows the monitor/tailscale pattern:
manageIpc: false so the panel owns the target's single IpcHandler and
can extend it past the inherited lifecycle methods.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 15:36:34 -05:00
David Heinemeier HanssonandClaude Fable 5 7020a6ea57 Span the power widget's open-panel mark across the percentage block
The widget declared no indicator hint, so the mark fell back to 55% of
the slot — sized for a lone icon, too short once the percentage doubles
the block. Hint the glyph's painted width, the way the clock hints its
label width.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 15:36:34 -05:00
David Heinemeier HanssonandClaude Fable 5 1f10c78c6a Patch settings-only bar config changes in place instead of rebuilding every widget
A shell.json write used to reassign the whole layout, and the module
Repeaters recreate every delegate when their array model changes — so
toggling an inline widget setting (battery percentage, clock format,
tray pinning) tore down and rebuilt every widget on every monitor,
closing any open panel along the way. When the layout structure is
unchanged, hand the new settings to the running widgets instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 15:36:34 -05:00
ssuptandDavid Heinemeier Hansson c0d4037237 Support external monitor brightness in Quattro (#6490)
* Support external monitor brightness

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

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

* Harden external brightness caching

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-01 15:35:34 -05:00
Gavin Nugent fa3d28ca61 Stop long fastfetch values from wrapping over the About logo (#6491)
Values longer than the remaining terminal width wrap back to column 0
and overwrite the logo column, which breaks the whole About layout. The
packages line triggers this on most systems.

Set display.disableLinewrap so overlong values clip at the right edge
instead. fastfetch documents this as the default but does not apply it
unless set explicitly.
2026-08-01 15:16:04 -05:00
David Heinemeier Hansson 3ffd9cdfc5 Add battery percentage toggle to power widget 2026-08-01 12:58:45 -07:00
David Heinemeier Hansson 3d59f9127e Make WhatsApp follow the system theme 2026-08-01 12:53:30 -07:00
David Heinemeier Hansson 66427571bc Revert WhatsApp theme bridge
The native messaging host, extension shim, and lifecycle machinery are too complicated for the value this integration provides.
2026-08-01 12:44:40 -07:00
237405215d Make WhatsApp Web follow your Omarchy light/dark theme (#6484)
* Make WhatsApp Web follow your Omarchy light/dark theme

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

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

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

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

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

* Address review on the WhatsApp theme bridge

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

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

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

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

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

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

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

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

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

* Harden Chromium theme bridge

* Address Chromium theme bridge review

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-01 13:16:06 -05:00
David Heinemeier HanssonandClaude Fable 5 1bc89105d2 Prefix plugin clones with the username so shared clones don't collide
Clones of omarchy.clock become dhh.clock instead of local.clock, so a
published clone carries its author's namespace. The clone command owns
the id derivation and gains --edit to open the result in $EDITOR, and
the shell exposes clonedFrom in listPlugins so the plugin menu no longer
reconstructs clone ids.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 13:13:39 -05:00
David Heinemeier HanssonandClaude Fable 5 2050f28d55 Hot-reload every installed plugin, not just local clones
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 13:13:39 -05:00
Abdul Moiz ShahzadandDavid Heinemeier Hansson 19144983e5 Sync Omarchy themes to Claude Code (#6480)
* Sync Omarchy themes to Claude Code

Claude Code 2.1.118 added custom themes as JSON files in
~/.claude/themes, watched and hot-reloaded, so this follows the same
shape as the Pi integration (da53c0ff, bin/omarchy-theme-set-pi).

A claude.json template renders from each theme's colors.toml — no
per-theme files, so every stock and community theme is covered — and
omarchy-theme-set-claude copies the result into
~/.claude/themes/omarchy.json on theme switch, retinting running
sessions without a restart.

The theme just appears as "Omarchy" in the /theme picker, and picking
it once (or running omarchy-theme-set-claude --activate) makes every
future theme switch carry Claude Code along. No ~/.claude directory,
no-op.

* Respect custom Claude config directory

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-01 13:06:19 -05:00
HusamandDavid Heinemeier Hansson 3cb3861fbc Dismiss bar panels when clicking on another monitor (#6487)
* Dismiss bar panels when clicking on another monitor

* Preserve keyboard focus for reopened panels

* Harden cross-monitor panel dismissal

* Wait for panel mapping before releasing focus

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-01 13:00:24 -05:00
David Heinemeier HanssonandClaude Fable 5 fa95901b6f Quantize GTK text scaling to whole point sizes to stop menu clipping
Raw size/12 ratios hand GTK a fractional point size (11px -> 10.08pt),
which clips ascenders in GTK4 menus on scale-1 monitors. Round the
scaled interface font to a whole point and derive the factor from that,
keeping 12px anchored at exactly 1.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 22:05:00 -05:00
David Heinemeier HanssonandClaude Fable 5 d47b4b0686 Collapse WhatsApp Web to a Signal-style avatar rail in slim windows (#6473)
* Make menu overflow visible with a half-row peek and edge fades

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

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

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

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

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 21:42:23 -05:00
David Heinemeier HanssonandClaude Fable 5 1960d18572 Make menu overflow visible with a half-row peek and edge fades
Menus used to cut off clean at ten rows, so anything below the fold was
undiscoverable. Now the list sizes to what fits within 60% of the screen
and always ends mid-row when items overflow, with scroll-position-driven
fades at both edges. Keyboard navigation keeps the next hidden row peeking
past the cursor so the fold affordance travels with the selection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 21:41:26 -05:00
David Heinemeier Hansson ad9fdc39f6 Merge pull request #6446 from richardlences/fix-non-ascii-ssid
Fix non-ASCII SSID characters displayed as \xNN hex in network panel
2026-07-31 22:20:10 -04:00
David Heinemeier Hansson a734713c42 Keep control escapes safe in SSID labels 2026-07-31 19:13:52 -07:00
David Heinemeier Hansson bac31ff892 Decode escaped SSIDs after status parsing 2026-07-31 19:08:37 -07:00
David Heinemeier HanssonandClaude Fable 5 15cecc9748 Rank apps matching a whole word of the query above exact menu entries
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 20:22:05 -04:00
a79d1dc8da Add Wi-Fi QR sharing to network panel (#6463)
* Add Wi-Fi QR sharing to network panel

* Refine Wi-Fi QR sharing

* Use QR glyph for Wi-Fi sharing

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

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

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

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

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

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

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

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

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

* Share WEP networks correctly and restore the QR quiet zone

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

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

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

---------

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

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

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

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 15:16:29 -04:00
Ryan Hughes 5844e02d2e Merge pull request #6459 from basecamp/agents-md-corrections
Correct three AGENTS.md claims against the repo
2026-07-31 13:10:06 -04:00
Jeremy Daer a737ec4ce3 Describe what omarchy-refresh-config validates, not what it doesn't
The previous wording claimed the argument must name a file under
$OMARCHY_PATH/config/ and that anything else is rejected. The check is
`[[ -e $default_config_file ]]` against an interpolated path, so `../AGENTS.md`
resolves in both trees and copies over $HOME/AGENTS.md, exit 0.
2026-07-31 01:24:51 -07:00
Jeremy Daer e872d3c0b9 Correct three AGENTS.md claims against the repo
- The privilege-escalation rule told agents to use pkexec for all privileged
  work, which contradicts default/omarchy-skill/SKILL.md — a file this repo
  ships — and the code follows the skill, not this line. Defer to it rather than
  restate it, so the two cannot drift apart again.
- config/hypr/hyprlock.conf does not exist, so the refresh example fails with
  "Not a shipped user config". Use hypr/hyprland.lua, the example
  bin/omarchy-refresh-config prints in its own usage.
- Neither IPC example was right: no target named `bar` is registered anywhere,
  and `image-selector` is registered by shell.qml rather than the background
  switcher, which registers `background`. Name the targets that exist.
2026-07-31 00:43:54 -07:00
Richard Lences b64f5e51fb Fix non-ASCII SSID characters in connected network display
Wrap awk output in printf '%b' to decode iw's \xNN hex escapes
back to the actual characters.
2026-07-30 19:39:28 +02:00
David Heinemeier Hansson 283276bec0 Fix #6440 2026-07-30 08:10:56 -07:00
David Heinemeier HanssonandClaude Opus 5 14f1bb6c88 Add a Wi-Fi band toggle to the network panel (#6437)
Pins the active connection to 2.4/5/6GHz, or leaves the choice automatic.
The section only appears on Wi-Fi when the network answers on more than one
band, or while a pin is in force so it stays clearable.

bin/omarchy-network-band reports and sets the band. It sets NetworkManager's
802-11-wireless.band rather than pinning a BSSID: NM 1.44+ accepts a third
band value so 5 and 6GHz can be pinned apart, and a band survives an AP
rotating its BSSIDs while leaving roaming between APs intact. It refuses a
band the network does not answer on, and restores the previous setting if
reassociation fails rather than leaving the machine offline.

Under Automatic the pills collapse and the header reads the live band
("WI-FI BAND: 2.4GHZ"); pinning reveals them and the header goes plain. The
height is animated, and the section stays mounted through the reconnect a
band change causes -- otherwise `kind` briefly stops being "wifi" and the
whole segment would tear down and rebuild.

Two fixes to the same panel fall out of this:

- Every stat row stays mounted and reads "--" until it has data, instead of
  appearing a beat after the panel opens and shoving the rows below down.
- The speed test's Run button is scaled to match the band header's controls
  and joins the keyboard cursor chain.

ToggleSwitch gains a settable trackHeight so a compact placement can render a
genuinely small switch instead of scaling one down onto fractional pixels.
Defaults are unchanged.


Claude-Session: https://claude.ai/code/session_01XcKqYe1n5bnRBuAZwikwsr

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 10:11:23 -04:00
David Heinemeier Hansson e47d895ffe Release sleep inhibitor after locking (#6436) 2026-07-29 23:21:45 -04:00
David Heinemeier Hansson 8554801537 Split update coordination helpers 2026-07-29 20:04:49 -07:00
David Heinemeier Hansson 695614ce6a Preserve Plymouth theme directory permissions 2026-07-29 19:45:43 -07:00
David Heinemeier Hansson 57ea0b4cd0 Simplify bar plugin management (#6435) 2026-07-29 22:39:55 -04:00
David Heinemeier Hansson 3304b79c92 Simplify model usage panel metrics 2026-07-29 19:26:55 -07:00
David Heinemeier Hansson 19a6eedb11 Add metadata for package conflict helper 2026-07-29 19:26:26 -07:00
David Heinemeier Hansson eb0f41cd5d Disable Windows VM restart at boot 2026-07-29 19:26:26 -07:00
Jeremy Daer 559ab04209 Add .claude/CLAUDE.md so Claude Code loads AGENTS.md (#6434)
Claude Code reads CLAUDE.md, not AGENTS.md, so this repo's AGENTS.md
guidance was invisible to it while Codex and Cursor read it fine.

The bridge lives at .claude/CLAUDE.md and imports @../AGENTS.md, keeping
the repository root uncluttered. Content stays in AGENTS.md; this file
only points at it.
2026-07-29 22:15:07 -04:00
David Heinemeier Hansson 4d05d39d33 Fix model usage token counting 2026-07-29 18:48:15 -07:00
David Heinemeier Hansson f8835df644 Plugin cloning via menu (#6433)
* Add plugin cloning via menu

* Split plugin commands by action

* Keep plugin enablement in action commands

* Remove unused plugin edit command

* Simplify plugin rescan arguments

* Assume Omarchy shell is running for plugin commands

* Remove plugin compatibility dispatcher

* Flatten plugin clone command

* Keep only shared plugin helpers

* Remove plugin rescan wrapper

* Keep plugin commands self-contained

* Simplify plugin clone lifecycle
2026-07-29 21:34:47 -04:00
David Heinemeier Hanssonand28allday fb564e3ac1 Fix DaVinci Resolve window behavior
Open the main window fullscreen so the bar does not cover Resolve's menu, and let parent windows yield focus so their dialogs remain usable. Match both free and Studio editions.

Co-authored-by: 28allday <gavin.nugent@hey.com>
2026-07-29 17:45:27 -07:00
taxinandtaxin 35212998b5 feat(notifications): add right-click to dismiss popup (#6400)
Add right-click handling to the notification card MouseArea so users
can manually dismiss notifications by right-clicking on them.

Previously, only left-click was handled (which triggered the default
action). Now right-click emits closeRequested() to dismiss the popup.

Co-authored-by: taxin <unknownbrofrombd@duck.com>
2026-07-29 19:17:09 -04:00
Grigoriy Sidorov fa6b5fce0d Fix silent launch failure for apps whose desktop id ends in .desktop (Telegram) (#6418)
* Launch apps by their full desktop file name

gtk-launch only appends the .desktop suffix when the argument doesn't
already end with it. Apps whose desktop id itself ends in .desktop --
notably Telegram (org.telegram.desktop.desktop) -- were therefore looked
up as a nonexistent file and silently failed to launch from the menu,
while every other app worked.

Passing the full file name to gtk-launch resolves the entry
unambiguously for both naming styles.

* Clarify launch comment wording
2026-07-29 18:56:09 -04:00
Eric van Riet PaapandDavid Heinemeier Hansson bf4aac95ad Warn when disk space is low before updating in Quattro (#6432)
* Warn when disk space is low before updating

* Simplify update free space warning

* Stop updates without enough free space

* Allow forcing updates with low disk space

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-07-29 18:47:27 -04:00
2093d1c9c7 Show OSD and sync touchpad steps when scrolling bar widgets (audio, monitor) (#6421)
* Show OSD and sync touchpad steps when scrolling bar widgets

Scrolling the volume or brightness icon in the bar changed the value
directly without ever calling omarchy-osd, so only the keyboard media
keys showed the popup. On top of that, a touchpad's stream of many
small wheel events per finger-drag wasn't matched to a mouse's single
±120 notch per click, so touchpad scrolling felt uncoordinated and
uneven next to the keyboard/mouse behavior.

- shell/plugins/panels/audio/Panel.qml: accumulate raw wheel delta and
  only apply a step once it crosses a full mouse-notch's worth, so
  touchpad and mouse move the volume in identical 5% increments, and
  ping the OSD each time a step actually lands.
- shell/plugins/panels/monitor/Panel.qml: same accumulator for
  brightness, with a 5% floor so scrolling down can never blank the
  screen.
- shell/plugins/osd/Osd.qml: animate the progress bar's width instead
  of snapping, so successive steps glide smoothly.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Fix bar wheel OSD behavior

* Normalize scaled wheel events

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-07-29 18:36:18 -04:00
David Heinemeier HanssonandClaude Opus 5 09b955dc75 Manage plugins from Setup > Plugins (#6420)
* Give built-in plugins an honest on/off state

Every built-in reported itself enabled no matter what. A bar widget said
"enabled" while sitting nowhere near the bar, and disabling a built-in service
silently did nothing, because enabled meant "listed in plugins[]" and a
built-in never is. Nothing surfaced that, since the only caller listing plugins
was the CLI.

For a widget, on and off is its place in the bar, so listPlugins reports layout
membership -- what enable/disable actually toggles. For everything else built
in, loading by default is the right behaviour to keep, so switching one off is
recorded the other way round, in disabledPlugins[]. shell.json still carries
only the deviation from the defaults: the key is dropped the moment nothing is
switched off, leaving a config that never disabled anything byte-identical.

isEnabled still answers a separate question -- whether the component loads at
all -- and deliberately does not follow a widget out of the bar. omarchy.menu
is both a widget and the menu itself, so tying the two together would let
taking its button off the bar lock the menu out of the shell, with no way back
that isn't the CLI.

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

* Manage plugins from Setup > Plugins

Plugins were CLI-only. Setup > Plugins now offers Enable, Disable, Add, and
Remove, each list living in the menu itself so picking a row acts on it.

Enable and Disable cover the built-ins as well as anything installed -- the bar
widgets you can put in the bar, the services and overlays you can switch off.
Remove is limited to plugins the user installed, since a built-in has no
checkout to delete, and stays hidden until there is one. Whole-bar
replacements are left out; those are chosen under Style.

Enabling a bar widget asks for a section first, because enabling alone drops it
on the right and the only way to move it was a follow-up bar plugin move. The
CLI asks the same question after its own add, so both paths place a widget the
same way. Add and Remove run in a terminal: one needs a git URL and shows the
trust warning before cloning, the other deletes a checkout and prints where it
backed it up.

Providers grew two hooks for this. placementFor turns a row into a submenu
instead of an action, and volatile re-runs the enumeration when its submenu is
entered -- picking from these lists is what changes them, and rows a provider
no longer returns now drop out instead of lingering forever.

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

* Trim the plugin menu after review

Menu.qml carried its own shellQuote while already importing Util and calling
Util.shellQuote a few lines up; two copies of the same escaping is one place
for a future fix to miss. isDisabled walked the array by hand to compare values
it writes itself, and dropDisabled was an eight-line helper with one caller.

Two bugs came out of the same pass. A whole-bar replacement belongs under Style
rather than these lists, but the exclusion sat in the shared row builder, so a
third-party bar could be installed and never removed -- Remove would show an
empty list under a guard that said something was there. The exclusion now sits
on the two lists that mean it.

Rows are keyed by id, and distinct plugin ids can slugify alike: acme.foo,
acme_foo and acme-foo all give acme-foo. The merge keeps the first row per id,
so the rest simply vanished from the list with nothing to say why. Row ids are
now made distinct before merging.

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

* Pick a plugin the way we pick a theme

Setup > Plugins listed plugins as menu rows, which needed three providers, a
placement submenu, a volatile-refresh hook and a row-swap in the merge. Only
Font and Apps are built that way. Theme, Background, Unlock, Timezone and
Keybindings all pipe a list into omarchy-menu-select instead, which is one
action string and a small script -- so that is what these use now.

The trade is search: a plugin name is no longer findable from the root prompt.
Neither is a theme name or a timezone, and Enable Plugin still is, so the loss
sits where the rest of the menu already puts it.

Two pieces of the row machinery stay, because they are worth having for the
lists that remain. A volatile provider re-runs when its submenu is entered, so
a font installed since the shell started now shows up without restarting it,
and rows a provider stops returning drop out. Row ids are still made distinct
before merging: Fira Code and Fira-Code both slug to fira-code, and a repeated
id was silently dropped.

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

* Let a picked option carry an icon

Moving the plugin lists onto omarchy-menu-select cost them their glyphs: the
select mode has always hardcoded an empty icon, which is why Timezone and
Keybindings have none either. An option may now lead with one, as
"<glyph><TAB><label>". The menu shows the glyph, filters on the label, and
hands the label back, so a caller never strips a glyph off its own selection
and a list of plain strings behaves exactly as before.

The plugin picker uses it for the puzzle glyph on each plugin and the align
glyphs on the sections, which also regain the capitals they lost when the
section names were passed through raw.

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

* Switch bars by enabling one

A bar option was kept out of Enable and Disable on the grounds that picking
which bar to run belongs under Style -- but nothing under Style ever offered
it, so an installed bar could be added and removed and never actually put to
use. The menu was guarding a door to a room that was never built.

Enabling one is the switch. setEnabled already assigns bar.id for a bar
option, so a bar has always replaced the one before it; only the picker's
filter stood in the way. Dropping it costs nothing else, because enabled for a
bar option means active: the bar in use is the one row absent from Enable,
every other installed bar is one pick away, and the built-in is just another
entry, so going back to it is enabling Bar.

Disable keeps the exclusion. That is the one verb a bar cannot answer -- there
is no off, only a successor -- and offering it would have listed the built-in
bar on a stock system, where turning it off deletes a bar.id that was never
set and nothing happens.

A bar carries the bar glyph rather than the puzzle one, so a row that replaces
the whole bar does not read like one more widget to switch on, and enable now
says "Now using X as the bar" instead of "Enabled X", which understated a
whole-bar swap in both the enable and the freshly-added path.

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

* Refuse a plugin that declares a kind it cannot load

A kind is a promise to supply something to load, and the shell reads that
something from a fixed key: entryPoints.bar to draw a bar, entryPoints.menu to
open a menu. Nothing checked the promise. A manifest could claim kinds ["bar"]
with no bar entry point, pass validation, install, and enable -- and then the
bar would fall back to the built-in and the widget would be skipped, leaving a
plugin that does nothing, explained only by a console.warn nobody reads.

Our own plugins have been held to this table by plugins-test.sh all along.
This holds third-party ones to the same table, at add and update time, where
there is still someone to tell.

A kind outside the table is left alone rather than guessed at, so a shell that
learns a new kind does not need this list updated first. The cost is that a
misspelled kind still installs quietly.

omarchy-plugin-validate had no tests; it has some now.

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

* Act on the plugin whose row was picked

The picker showed a name and then looked that name up again across every
plugin, filtered set or not, taking the first match. Two plugins can share a
name: cloning one keeps the name it was cloned from, so the documented
`omarchy plugin clone omarchy.clock local.clock` leaves two plugins called
Clock. Enable listed the clone -- the built-in was already enabled, so only the
clone was eligible -- and then enabled omarchy.clock, moving the built-in
widget instead. Remove listed the clone and tried to delete a built-in that has
no checkout to delete.

A row now carries its id alongside its label, and the id is read back off the
row that was picked instead of being derived from the name a second time. Where
a name is not unique among the rows on offer, the label carries the id too, so
two rows that would both say Clock can be told apart at all -- which they could
not before, whichever one the pick resolved to.

The verb prompt only ever sees the first two fields, so the menu shows what it
always did.

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

* Never ask a bar where to sit in the bar

A manifest may declare both bar and bar-widget, and validation accepts it. The
picker saw bar-widget, asked for a section, and passed it to enable. setEnabled
takes bar as the dominant kind: it writes bar.id and returns, adding nothing to
any layout, so the move that followed had no widget to find and failed -- after
the bar had already been switched. A partial success with an error on the way
out.

Bar wins ahead of bar-widget now, in the picker and in the placement prompt
`plugin add --enable` asks, so a bar is enabled without a placement it cannot
use. The CLI refuses a placement on a bar outright, before the bar is switched
rather than after, since `omarchy plugin enable <bar> --section left` could
reach the same half-applied state without going through either.

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

* Only replacement, no off

* Add default placement for bar widgets

* Simplify plugin menu actions

* Document plugin placement behavior

* Allow dropping widgets in empty bar space

* Treat plugin dependencies as runtime invariants

* Reject duplicate plugin ids on add

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 18:11:32 -04:00
Kushida 6593403345 fix: fall back to XDG MIME browser handler (#6424) 2026-07-29 17:15:27 -04:00
David Heinemeier HanssonandClaude Opus 5 e39a275f5a Drop Kvantum
Kvantum is a QStyle plugin, so it only ever painted the three QWidget
apps we ship, and it painted them with its own stock look: Omarchy has
never shipped a .kvconfig, so nothing about it tracked the active theme.

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 12:27:38 -07:00
Saman ShirdelandClaude Opus 5 1045975946 Stop section headers rendering clipped at the top of a list
JetBrainsMono Nerd Font draws 10% of the em above the ascent Text
reserves for it (yMax 1120 against ascent 1020), and a patched or
user-chosen family can be worse. In normal flow that sliver costs
nothing; at the top of a clipping list it is sliced off and the header
renders beheaded.

Reserving it in PanelSectionHeader covers every panel at once, rather
than each list working around it in its own geometry.

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 14:49:55 -04:00
David Heinemeier HanssonandClaude Opus 5 63e57e1bd5 Let a stray both-Shifts Caps Lock clear itself
CapsLock is the compose key, so Caps Lock has to live somewhere else, and
both Shifts together is where it went. That combination is easy to hit by
accident while typing, and the lock then sticks until you notice it and
hit both Shifts again.

shift:both_capslock_cancel sets the lock exactly the same way, but types
the Shift keys as ALPHABETIC rather than TWO_LEVEL, so the next lone
Shift releases it. Since a misfire happens while reaching for Shift
anyway, it clears itself almost immediately.

Compose is untouched: <CAPS> stays [Multi_key, Multi_key], so the
~/.XCompose emoji sequences work as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 14:49:55 -04:00
David Heinemeier HanssonandClaude Opus 5 a439069b0d Let a ListView own the bluetooth device list
The panel drove a Flickable by hand: two Repeaters in a Column, plus an
ensureCursorVisible() that mapped each row into content coordinates and
moved contentY itself. That is the machinery a ListView already has, and
the hand-rolled version came with a 6px margin that scrolled the list
whenever the mouse merely rested near the viewport edge.

Flatten the remembered and discovered groups into one model whose entries
carry the section they came from, and let the view keep the current row
visible. Section headers are computed per index the way the network panel
does it. The cursor keeps working in section-relative terms, so
activation, forget, hover, and the hero toggle are untouched.

Positioning has to be deferred a turn: called straight out of
onCurrentIndexChanged it silently does nothing, because the model is
rebuilt every time discovery reports and the swap resets the view out
from under the call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 11:47:33 -07:00
David Heinemeier HanssonandClaude Opus 5 fae4a9e029 Drop the Qt5 stack (#6431)
kvantum-qt5 only ever existed to dark-mode VLC, and VLC is retired.
qt5-wayland was the Qt5 Wayland platform plugin, but it also quietly
dragged in qt5-declarative, which is what kept sddm's Qt5 greeter alive.
No application we ship runs on Qt5 any more, so the whole stack goes
from fresh installs: about 110 MiB across kvantum-qt5, qt5-wayland,
qt5-declarative, qt5-base, qt5-svg, qt5-x11extras and qt5-translations.
fcitx5-qt keeps a few Qt5 objects it builds regardless, and they simply
go unused.

sddm picks its greeter from the theme's SddmGreeterTheme/QtVersion and
defaults that key to 5, so a theme without it execs the Qt5
/usr/bin/sddm-greeter. That tie is only an optdepend, so pacman would
happily take qt5-declarative and leave a login screen that cannot start.
The theme declares QtVersion=6 now, and the Qt6 greeter renders it
unchanged.

kvantum, the Qt6 style engine behind QT_STYLE_OVERRIDE, was only ever
present as a dependency of kvantum-qt5, so the package list swaps to it
rather than dropping the line. obs, kdenlive and the share picker keep
the styling they have today.

Machines already installed keep their Qt5 stack. Shedding it there means
deciding which greeter sddm will actually launch, and that means
reimplementing its config loader: sddm reads every file in sddm.conf.d
whatever its extension, QSettings trims keys and lets the last duplicate
win, and ThemeDir can move the themes entirely. Not worth risking a
login screen for 110 MiB that a reinstall reclaims anyway.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 14:13:16 -04:00
David Heinemeier Hansson 6895136eed Merge pull request #6428 from yuters/agent/speed-up-bluetooth-pairing
Skip Bluetooth startup delay when already powered
2026-07-29 10:41:54 -07:00
David Heinemeier HanssonandClaude Opus 5 9939a00d17 Guard the Bluetooth power check and cover it
Every other bluetoothctl call in the script is silenced and time-boxed;
the new power check was neither, so a wedged D-Bus could hang pair before
it reached its own timeout and spill dbus assertions onto stderr.

Cover both paths with a stubbed bluetoothctl: the power-on sequence is
skipped when the adapter reports Powered: yes, and still runs when it
does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 10:41:22 -07:00
David Heinemeier Hansson e3ff2b80bc Better comments 2026-07-29 10:08:49 -07:00
Ryan Hughes 00376ec43c Merge pull request #6422 from OldJobobo/fix/quickshell-asymmetric-border-corners
Fix asymmetric rounded shell borders
2026-07-29 12:44:48 -04:00
David Heinemeier HanssonandClaude Opus 5 9174fbf851 Recognize the omarchy-dev package in omarchy-version
The edge channel installs omarchy-dev, but omarchy-version only queried
omarchy, so it exited 1 there. omarchy-snapshot runs under set -e, so the
failed lookup aborted the whole update over a snapshot label.

It only worked at all because omarchy-dev declares provides=(omarchy) from
the installer repo. Builds without it fail, so check both packages here
instead of relying on a declaration from another tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 09:15:53 -07:00
Martin Bastien 79a75d7159 Skip Bluetooth startup delay when already powered 2026-07-29 09:10:21 -04:00
J. S. Brown 6675f6b711 Fix asymmetric rounded shell borders 2026-07-28 21:28:31 -07:00
Ryan Hughes 38c1352f6d Fix t2 module 2026-07-28 21:07:50 -04:00
David Heinemeier HanssonandClaude Opus 5 24dc914d28 Recover model-usage limits from a boot-time probe failure
The shell starts before DHCP finishes, so Claude's first probe of the
OAuth usage endpoint came back as a transport failure and pinned "Claude
limits unavailable" for a full refresh interval. Opening the panel could
not clear it either: the open path refreshed unforced and hit the same
throttle it was trying to escape.

A transport failure now arms a short retry that goes straight to the
probe, since a retry answering to the throttle never gets off the
ground. Any real answer disarms it — a server that replied, 429
included, is one to stop pestering. Probes are single-flight, a fresh
token skips the throttle its predecessor set, and opening the panel asks
for the wire without re-walking every transcript on disk.

The module also earns its place in the bar now instead of sitting there
dimmed with nothing to say: it collapses out entirely until a provider
has actually recorded usage, here or on a synced machine. That made both
presence probes redundant, so they are gone. Selection follows the
provider rather than its slot, so one appearing while the panel is open
no longer swaps out what you were reading.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 11:00:43 -07:00
David Heinemeier Hansson 9b693cca63 Refresh the look and layout of the model-usage plugin 2026-07-28 09:18:24 -07:00
David Heinemeier HanssonandClaude Opus 5 234ac7b3b6 Bring back clipboard watchers that die
Clipboard history is fed by two wl-paste --watch processes started once
when the shell loads. When one dies, nothing notices: copying still
works, the picker still opens, and the old entries are all still there,
so history just silently stops recording until the next shell reload.

Respawn them on exit instead. The one-shot timer both coalesces the case
where both watchers die together and keeps a wl-paste that cannot start
at all from spinning as fast as fork allows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 08:19:57 -07:00
David Heinemeier HanssonandClaude Opus 5 cf8102af92 Mute both channels from the audio hero, one from each slider
The hero switch is the panel's on/off, so it now carries output and input
together rather than output alone. It reads as on while anything is still
audible, which keeps muting a single channel from flipping the master switch.
Right-clicking a volume bar mutes just that channel -- output, input, or one
app's stream -- matching what the `m` key already does for the focused row.

Only channels that exist get a vote: a machine with no default source would
otherwise report its input unmuted forever, leaving the switch able to mute but
never to unmute.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 07:55:36 -07:00
David Heinemeier HanssonandClaude Opus 5 4a8091cd41 Give PanelSlider a right-click signal
Sliders only ever accepted the left button. Audio wants right-click as a
secondary action on the track, so emit rightClicked() for it and keep dragging
left-button-only: press and release both ignore anything else, so a right-click
can neither start a drag nor move the value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 07:55:29 -07:00
4cfb2c2fae Toggle panel heroes with a switch instead of the status icon (#6408)
* Add Tailscale header on/off toggle

* Address Tailscale toggle review feedback

* Hide Tailscale tooltip while busy

* Keep Tailscale labels clear of header toggle

* Extract the switch from Toggle into a reusable ToggleSwitch

Toggle rendered its own track and knob inline, so anything else wanting a
switch had to copy the geometry. Pull it into Ui/ToggleSwitch.qml and let
Toggle compose it, and give PanelHero a trailingControl slot so a hero can
pin a control to its trailing edge without the caller doing the layout.

The switch draws its cursor as a ring outside the track: themes give normal
chrome a stronger border than hover-cursor, which is right for controls that
are borderless at rest but would make a bordered track go fainter under the
cursor.

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

* Keep the panel cursor on the virtual header section

clampCursor resets focusSection whenever it is not in visibleSections, but
"header" is virtual and never appears there. Any refresh of the underlying
model therefore threw the cursor off the hero toggle: muting republishes the
PipeWire snapshot, and toggling the Bluetooth adapter empties and refills the
device lists. moveCursor already special-cases "header"; clampCursor now does
too.

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

* Toggle panel heroes with a switch instead of the status icon

The hero icon doubled as the on/off control, which was invisible as an
affordance and made the icon carry two jobs at once. Give Tailscale, Dropbox,
Bluetooth, Audio, and Network a ToggleSwitch on the trailing edge of the hero
and leave the icon to report status. The switch is the header's only cursor
target, so the keyboard reaches it the same way the mouse does.

Dropping the icon's focus ring also drops heroRingPad, which lets each hero
line up with the rows beneath it. Network's link detail moves inline after the
name -- "Ethernet (2.5gbit)" -- since the pill crowded the switch.

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

* Make the network hero switch honestly a Wi-Fi switch

The switch reads and writes Networking.wifiEnabled, but its tooltip claimed to
turn "network" on and off whenever Ethernet was the active connection. A click
asserted nothing; a switch asserts state, so on a wired machine with the radio
off it sat there reading "off" beside a perfectly live Ethernet connection.

Say Wi-Fi, and only offer the switch when there is a radio to switch.
headerActionCount follows the same condition so the keyboard cannot reach a
control that is not there.

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

* Stop the header cursor claiming an absent switch

The switch hides when the thing it toggles is unavailable -- no Tailscale CLI,
no Dropbox CLI, no Wi-Fi radio -- but "header" stayed reachable, so the cursor
could sit on a target that never rendered. The old clickable icon was always on
screen, so there was always something to highlight.

"header" stays navigable and Enter still no-ops safely; the cursor just stops
claiming a spot that is not there.

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

* Wrap the gallery's switch caption

The caption sat unbounded inside the switch row. The gallery has a 560px
minimum width, horizontal scrolling off, and clipping on, so at that size the
end of the line was simply unreachable. Move it below the row and wrap it, the
way every other description in the gallery already does.

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>
2026-07-28 07:44:30 -07:00
David Heinemeier Hansson 1d38842c3e Merge pull request #6413 from husamemadH/fix-stranded-launching-osd
Stop stranding the Launching… OSD when a second app is launched
2026-07-28 07:41:58 -07:00
David Heinemeier HanssonandClaude Opus 5 3914c65147 Document the launch OSD flag where it lives
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 07:41:45 -07:00
David Heinemeier HanssonandClaude Opus 5 3d9706c281 Hide install options for things already installed
Every install row that leaves a durable artifact now carries a `when`
that mirrors its remove counterpart, negated. Packages check
`omarchy-pkg-present`, Ollama checks the command since the action picks
between three packages, and the rest check whatever the installer
actually leaves behind: Games/battlenet, the Xbox Cloud and Windows VM
desktop files, the flatpak, the mise install dirs, ~/.rustup, ~/.opam,
the composer laravel binary, the phx_new archive.

Chromium Account now shows only when chromium-flags.conf exists and
lacks the oauth client id. It was a visible no-op both before Chromium
was installed and after the flags had been added.

The pickers stay unguarded: Package, AUR, Web App, TUI, Style, Docker
DB, and the RetroArch Game Launcher, which builds one launcher per game.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 07:28:21 -07:00
husamemad 43c15a4015 Stop stranding the Launching… OSD when a second app is launched 2026-07-28 13:18:48 +03:00
David Heinemeier Hansson f4e8470c3a Merge pull request #6405 from basecamp/recover-pacman-file-conflicts
Recover from package file conflicts instead of predicting them
2026-07-27 15:57:38 -07:00
David Heinemeier HanssonandClaude Opus 5 646587e316 Recover from package file conflicts instead of predicting them
pacman refuses to install over a file it doesn't own, so any path an
Omarchy package starts shipping that a script had already written by
hand aborts the whole upgrade:

  omarchy-settings-dev: /usr/lib/systemd/user/omarchy-fcitx5.service exists in filesystem
  Errors occurred, no packages were upgraded.

The mitigation was a hand-maintained --overwrite allowlist, and it only
worked with foresight: an entry had to ship a release before the package
took the path, because pacman checks conflicts during transaction
prepare, so the script running the upgrade is the one already on disk. A
missed entry left people hard-stuck, since the upgrade that would
deliver the entry is the one refusing to run.

So react instead. omarchy-update-system-pkgs now does the ordinary thing
and hands a failed transaction to omarchy-update-system-pkgs-when-
conflicted, which moves the offending files out of the way and runs the
upgrade again. Nothing has to be predicted, and the allowlist is gone.

Moving rather than overwriting is what makes it small: no --overwrite
argument to build, no glob escaping, no separate backup step, and a
leftover directory is cleared too, which --overwrite cannot do at all.

Files go to /var/lib/omarchy/replaced/<original path>, not next to the
original. A sibling copy is not inert -- SDDM reads every file in
sddm.conf.d whatever its extension, and systemd-sleep runs every
executable in system-sleep -- and the directory a future path lives in
is unknowable, which is the whole point of a mechanism for paths nobody
predicted.

What it will not do:

- Take a file another package owns. pacman reports those with a
  "(owned by x)" suffix, so the end anchor excludes them, and pacman -Qo
  re-checks the live database before anything moves.
- Move a subset. If any reported conflict isn't recoverable the retry is
  doomed anyway, and moving leaves that config inactive for nothing --
  worse than the stuck-but-intact state.
- Leave anything inactive that was live. A failed retry, a failed move
  partway through the loop, or an interrupt all put back whatever the
  upgrade didn't install.
- Act on a report handed to it by hand. It is internal to the update,
  and an old report would clear live files for an upgrade that isn't
  happening.

Also adds a test that fails when a script writes a path under /usr that
no PKGBUILD installs, since not creating these is cheaper than
recovering from them. It reads the destination off the command, so a
path assembled from variables still slips through; the two known cases
are recorded with their reasons.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 15:50:28 -07:00
David Heinemeier Hansson bdcdfeb428 This tmux alert system didn't work as nicely as I imagined 2026-07-27 15:32:56 -07:00
David Heinemeier HanssonandClaude Opus 5 b6e1a43278 Detach the pending-migrations toast from the notifier
omarchy-migrate-notify handed its notification to `systemd-run --scope`,
which is synchronous: the calling process becomes the payload, so
systemd-run does not return until the toast has been answered -- and if
the toast is clicked, not until the migration terminal it opens has been
closed.

omarchy-migrate-notify.service is Type=oneshot, which defaults to
TimeoutStartUSec=infinity, so the unit sat in activating for exactly that
long. On a machine that had not yet picked up c7e327b0, which orders the
notifier after graphical-session.target, that held the target open --
and wayland-wm-app-daemon.service is ordered after the same target, so it
never started. Every keybinding goes through uwsm-app, which waits ten
seconds for that daemon's pipes and then reports "App failure -- Timed
out waiting for pipes!" instead of launching anything. Keybindings were
dead for two minutes and seventeen seconds, until a pacman hook ran
`systemctl reload user@*.service`, whose re-exec of the user manager
broke the scope's bus connection and let the oneshot finish. That also
made systemd-run exit non-zero, so the notifier fell through to the
terminal fallback meant for having no user manager at all, and printed
the migration list into the journal.

Hand the notification to a transient service instead. systemd-run
returns once the unit has started rather than once the payload is done,
so the oneshot completes in milliseconds and nothing ordered after the
target waits on a toast. Put it in background-graphical.slice, which
systemd ships with PartOf=graphical-session.target, so an unanswered
toast ends at logout rather than outliving the session.

The terminal a clicked toast opens is unaffected: uwsm-app re-registers
it into app-graphical.slice, outside this unit's cgroup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:19:20 -07:00
David Heinemeier HanssonandClaude Opus 5 1472fc1b38 Bail out of the mise stub if the install fails
Saves a second, murkier error from the exec that would follow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 10:49:01 -07:00
David Heinemeier Hansson e55f130d55 Use Edge specific glyph 2026-07-27 10:14:36 -07:00
David Heinemeier Hansson c1647faba5 Merge pull request #6397 from brink-lab/quattro
Enable cd/dvd ejection by default
2026-07-27 10:10:29 -07:00
David Heinemeier Hansson c5d2dcebeb Merge pull request #6398 from basecamp/supervise-fcitx5
Supervise fcitx5 so XCompose sequences can't silently die
2026-07-27 09:51:02 -07:00
David Heinemeier HanssonandClaude Opus 5 6e07fd0e82 Supervise fcitx5 so XCompose sequences can't silently die
fcitx5 is what turns the CapsLock compose sequences in ~/.XCompose into
text for Wayland clients -- CapsLock m s for an emoji, CapsLock space n
for your name. It was launched fire-and-forget from Hyprland's autostart
via uwsm-app, so nothing supervised it, nothing restarted it, and it
logged nowhere. When it went away, every compose sequence stopped
working for the rest of the session with no visible symptom beyond
"emoji input is busted", and no record of why.

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

Move it to a systemd user service:

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 09:49:15 -07:00
brink-lab af017ae83b Enable cd/dvd ejection by default
Enable cd/dvd ejection by default for all systems with a key mapped to "XF86Eject". 
This e.g. activates the eject button on vintage Macbook Pro laptops with optical drives.
2026-07-27 18:42:56 +02:00
David Heinemeier HanssonandClaude Opus 5 03902f2460 Never notify about pending migrations during an update
The retired omarchy-update-user-notify.path stays loaded in sessions that
started before the update removing it, and pacman writes the migrations
directory mid-transaction, so it fired a critical toast for migrations that
omarchy-migrate was about to apply a step later. Migration 1785095882 stops
that watcher, but migrations run after pacman, so it lands 11 seconds too
late to prevent the toast it exists to retire.

Check the lock omarchy-update holds for its whole pipeline instead of
trusting that no trigger exists. That covers the stale watcher and anything
added later: during an update every pending migration is by definition
already being applied. The check repeats after waiting for the notification
server, which is long enough for an update to start underneath it.

Only this user's runtime directory is read, never the /tmp path the updater
falls back to without XDG_RUNTIME_DIR. A shared lock file belongs to whoever
created it first, so honouring it would let one user silence another user's
notification; a redundant toast is the better failure.

The sleep inhibitor now starts with the lock descriptor closed. It outlives
the step that starts it, so an update killed before restore_update_inhibitors
left it holding the flock indefinitely. That already blocked later updates,
and now that the notifier reads the same lock it would have silenced
migration notices at every login.

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VeAd2iERgi7bRSxq2gA1hp
2026-07-27 09:02:25 -07:00
David Heinemeier HanssonandClaude Opus 5 8362c0990a Drop the test for the removed zram resize migration
The migration went away in 88ef6ca5, and the test resolved it by grepping
migrations/ for its banner, so it could only fail from that commit on. It
used the shipped 90-omarchy.conf as a fixture only; config-test and
zram-migration-test still cover that file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 08:41:59 -07:00
David Heinemeier HanssonandClaude Opus 5 c7e327b05a Order the migration notifier after graphical-session.target
omarchy-migrate-notify.service is a Type=oneshot wanted by
graphical-session.target, and systemd complements a target's Wants= with an
implicit After=, so the target waited for the notifier to exit. The notifier
does not exit quickly: it sends the notification through systemd-run --scope,
which is synchronous, and omarchy-notification-send -a blocks until the user
clicks. The target stayed in activating for as long as the toast was up.

wayland-wm-app-daemon.service is After=graphical-session.target and nothing
wants it, so uwsm-app starts it on demand. Clicking the notification runs
omarchy-launch-floating-terminal-with-presentation, which execs uwsm-app,
which blocks on a systemctl --user restart of that daemon -- a job queued
behind the very target the clicked notifier was holding open. The terminal
never opened; uwsm-app gave up on its own pipe timeout instead.

Declaring After= on the wanted unit suppresses the implicit dependency rather
than forming a cycle, so the target is reached without waiting and the
notifier runs behind it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 08:41:59 -07:00
Ryan Hughes 2fb33a2a6a Prevent showing disconnected icon when connected on another device 2026-07-27 11:29:03 -04:00
Ryan Hughes 88ef6ca597 Unnecessary
Sets reboot, changes take effect on reboot anyway
2026-07-27 09:49:13 -04:00
Ryan Hughes 72dfbb43cd Additional zram + swap tuning 2026-07-27 09:48:20 -04:00
David Heinemeier Hansson c7f7e837b2 Merge pull request #6391 from basecamp/bar-startup-churn
Load each bar widget's component once
2026-07-26 22:32:26 -07:00
David Heinemeier HanssonandClaude Opus 5 7a6e34f153 Scale the IPC handler check to the number of screens
The bar is instantiated once per screen, so each widget registers its IPC
handler once per screen and Quickshell reports a collision for every screen
past the first. Failing on any collision would have failed the suite on
multi-monitor desktops for behaviour that is correct; allow one per screen
and fail only beyond that, which is still what duplicate component loads
produce.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:29:48 -07:00
David Heinemeier HanssonandClaude Opus 5 64810d815b Load each bar widget's component once
Qt.createComponent is asynchronous, but a widget was only recorded in
pluginWidgetComponents once its component finished. syncPluginWidgets runs
several times while the shell starts, and every pass that ran while a load
was still in flight could not tell it apart from one that had never
happened — so it started a second load of the same URL. All twenty bar
widgets were loaded twice.

The duplicate component then replaced the first in the registry, and
swapping a slot's component makes its Loader build the replacement before
dropping the original. Both were briefly live, and both registered the
widget's IPC handler, which is where the "another handler is registered
for target" warnings came from.

Claim the key when the load starts instead, and release it if the
component fails so a later rescan can retry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:19:57 -07:00
David Heinemeier Hansson e31355bbaf Merge pull request #6390 from basecamp/clock-calendar
Add a calendar popup to the clock
2026-07-26 22:11:29 -07:00
David Heinemeier HanssonandClaude Opus 5 58aa2d5f3f Merge quattro into clock-calendar
Both sides tightened the same center-layout assertion. Taking quattro's:
it asserts the weather/update adjacency the test name is about instead of
pinning the whole row, which is what kept breaking it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:07:08 -07:00
David Heinemeier HanssonandClaude Opus 5 8728791c93 Build only the module list the bar is showing
The center section declares both an anchored and an unanchored
arrangement and shows whichever fits, but a hidden ModuleList is still a
loaded Loader. With a center anchor set — the default — every center
module was therefore mounted twice for the life of the session: two IPC
handlers registered for the same target, two clocks ticking, two of every
timer and network fetch behind them, one set of which nothing could
reach.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 20:30:52 -07:00
David Heinemeier HanssonandClaude Opus 5 9e3da4e27f Count the other progress bar too
Double-tapping the year bar asks for a birth year and a life expectancy,
and a second bar appears below measuring one against the other. Tab moves
between the two, Enter commits the pair, Escape drops them. Hovering the
bar names the thing, and double-tapping it puts it away again.

A birth year rather than an age, so the bar keeps counting on its own
instead of going stale the moment it is entered. Expectancy defaults to
ninety and falls back to it when what is entered makes no sense, so the
bar always has something to measure against; a birth year that makes no
sense leaves the bar hidden instead, which is also where it starts.
Putting the bar away keeps the expectancy, so setting a birth year again
brings your own number back rather than the default.

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:59:37 -07:00
David Heinemeier HanssonandClaude Opus 5 b4d6b775c7 Point the Snapper test at paths that still exist
Its setup check grepped omarchy-setup-system for config/snapper.sh, which
the config phase has run since the install was split into phases.

The omarchy-pkgs and omarchy-iso lookups also missed the sibling checkout
layout that f375113f taught config-test about, so they failed on machines
where config-test passed. Both now take an explicit path override.

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:19:33 -07:00
David Heinemeier HanssonandClaude Opus 5 cc4771fd91 Clone widgets that import a sibling JS module
A cloned bar widget is copied out on its own, so a relative JS import
lands in a directory without the file and the clone fails to load. Point
those imports back at the bundled module, the same way Qt.resolvedUrl()
references were already rewritten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:16:05 -07:00
David Heinemeier HanssonandClaude Opus 5 c619865432 Keep the center hover reveal honest when panels hand off
Claiming the shared suppression flag before showing meant the outgoing
panel's close cleared it again, leaving the incoming panel open with the
indicators still revealed. Guarding the clear instead only moved the
problem: handing off to a panel that does not manage the flag left it
stuck on, and the center indicators stopped revealing on hover for good.

Claim it after the handoff instead. The panel taking over always wins,
and a handoff to a panel that knows nothing about the flag still leaves
it cleared.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:16:05 -07:00
David Heinemeier Hansson f22e8cd353 Merge remote-tracking branch 'origin/quattro' into fix-snapshot-locate-and-timeline-leak
# Conflicts:
#	docs/file-layout.md
2026-07-26 19:14:09 -07:00
David Heinemeier HanssonandClaude Opus 5 e2d655d265 Add a calendar popup to the clock
Clicking the clock reveals a month grid with ISO week numbers, a year
progress meter, and month stepping. Right click walks the common label
formats and writes the chosen one back to shell.json, so the bar shows
what the config stores. The week start toggles from the grid's "W"
heading and persists as weekStartDay, defaulting to the locale's own
first day.

Rich popup widgets live in their own plugin directories, so the clock
moves out of bar/widgets/ into panels/clock/. The id is unchanged, so
existing layouts and centerAnchor keep working.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:10:06 -07:00
David Heinemeier HanssonandClaude Opus 5 c2610c3788 Give the weather panel its host widget's identity in the bar
The bar identifies a panel by the widget mounted in its slot, but the
nested panel handed the popout coordinator itself. The open-panel mark
never lit under the weather pill, and Tab could not leave the panel.
Closing for a popout switch also cleared the shared hover-reveal flag the
incoming panel had just set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:10:06 -07:00
David Heinemeier HanssonandClaude Opus 5 6d4fa7acaf Make the open-panel mark find and fit its module
A center-anchored module is mounted twice: the copy that is drawn, and a
zero-size placeholder holding its place in the flow beside the anchor.
findPanelWidget returned whichever registered first, and that order is
not stable across a live bar reconfiguration, so a panel could open
anchored to the invisible copy -- mispositioned, with the drawn slot
never lighting up and switchPanelFrom unable to find it again.

The mark was also always 55% of the slot, which fits an icon but
underlines only a fraction of a text label, and runs the full height of a
multi-line module on a vertical bar. Modules can now say how long the
mark should be along the bar; anything that does not answer keeps the old
proportion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:10:06 -07:00
David Heinemeier HanssonandClaude Opus 5 d4c835afb5 Fix the default center layout assertion
The center layout was reordered when the indicators moved next to the
clock, but the assertion kept the old order, so this has been failing on
quattro since.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:10:06 -07:00
David Heinemeier HanssonandClaude Opus 5 e57f3b286c Send and receive files with Taildrop (#6375)
* Send files to a tailnet machine with Taildrop

The panel gets a send button next to the copy one on every machine that
Tailscale grades as a Taildrop target, and `s` does the same from the
keyboard. Picking runs through the XDG portal chooser, so it looks like
the file dialog every other app opens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJQJfHXXApUk6En8EZisHg

* Save incoming Taildrop files and say so

Linux keeps Taildrop files in the daemon's inbox until someone asks for
them, so nothing arrived until you ran `tailscale file get` by hand. A
user service now stages each delivery next to the downloads directory,
hands it over under a free name, and announces it — with a preview when
it's an image, and a click to open it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJQJfHXXApUk6En8EZisHg

* Float every portal dialog, not just the titled ones

The portal only ever shows dialogs, and the title regex missed any
chooser an app names something else — ours included.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJQJfHXXApUk6En8EZisHg

* Re-run the Taildrop enable now that the unit ships

The unit was never installed to /usr/lib/systemd/user/, so the enable had
nothing to act on and machines that already ran the migration carry a marker
for a no-op. Rename it so they get a working pass, and report what systemctl
says instead of a bare failure line.

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

* Wait for the file chooser on the connection that asked for it

The portal answers a request with a Response signal directed at the connection
that made it, and dbus-daemon delivers directed signals only to that
connection. gdbus monitor registers with AddMatch rather than BecomeMonitor, so
it never saw the reply: every pick left omarchy-file-select blocked on a read
that could not arrive, taking omarchy-tailscale-send down with it before it
reached either its notification or the transfer.

Make the call and wait for the signal on one connection, and give up after ten
minutes so an unanswered dialog cannot strand the caller.

Drop the "Sending to" notification while here, so a send reports once.

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

* Mark Taildrop notifications with the panel's send glyph

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

* Stop a hung tailscale poll from freezing the panel

Each poll is skipped while its own process is still running, so one that never
exits leaves the panel showing whatever it last read, for good: the peer list
keeps a woken machine missing, and opening the panel cannot help because open
runs the same refresh that hits the same guard.

Reap anything still running fifteen seconds after a refresh, well inside the
thirty second interval, so the next tick starts clean.

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

* Place a moved widget where an added one lands

'move omarchy.media left' named a section, not a slot, but the section went
through as an explicit target, which resolves a missing index by appending. The
widget landed on the far end of the row instead of after the section anchor
where 'add' puts it.

Its test has never run: the assertion covering this went in four hours after an
unrelated layout change had already stopped the file, and the runner stops the
whole suite at the first failure.

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

* Keep the config test from failing on things it is not about

The center layout assertion pinned the whole row, so parking the indicators
left of the clock broke a test named for update sitting next to weather. Assert
that adjacency instead.

The package-defaults check reads PKGBUILDs from the omarchy-pkgs repo and blew
up with a traceback wherever that is not a sibling checkout. Skip it when the
checkout is absent, honour OMARCHY_PKGS_ROOT when it is somewhere else, and
keep failing when it is present and wrong.

Between them these stopped the suite eighty files early.

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

* Make the file chooser a Python command rather than a bash host for one

The portal work was a heredoc wedged inside a bash script that existed only to
parse two flags. Drop the host: argparse covers the flags, and the file says at
the top why it is the one command here not written in bash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Tell a chooser that never opened apart from one that was dismissed

Three fixes from review:

The poll watchdog rearmed on every refresh, so a refresh interval shorter than
its timeout — the setting goes down to five seconds — pushed the deadline ahead
of a hung process forever. Arm it on the launch that needs watching and leave
it alone.

omarchy-file-select exited 1 both for nothing picked and for a chooser that
could not run, and omarchy-tailscale-send read it through a process
substitution, which drops the status anyway. A session bus that was not there
looked exactly like someone changing their mind. Separate the two exits and
read them with a command substitution.

Delivery picked a free name and then renamed, which overwrites anything that
takes the name in between. Link to the name instead: link(2) refuses one that
is taken, so the check and the claim are the same step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:07:11 -07:00
David Heinemeier HanssonandClaude Opus 5 a6a13cf3e6 Replace the in-flight updatedb and read PRUNEPATHS out of any quoting
The machines this targets are the ones with an updatedb already grinding
through every snapshot, and `systemctl start` on an active unit is a
no-op. updatedb reads /etc/updatedb.conf once at startup, so a run that
began before the rewrite keeps burning CPU on the old config until it
finishes. Restart the service instead: it's Type=oneshot and plocate
builds into a temp db, so nothing is lost by replacing the run.

Quotes are optional in updatedb.conf, so parse the existing paths out of
whatever quoting the file uses and write the setting back in one
canonical form. `PRUNEPATHS=/tmp` previously fell through to the append
branch and got a second PRUNEPATHS line, which drops /tmp from the
pruned set. Comparing whole paths rather than substrings also keeps a
config that already prunes something like /var/lib/machines/.snapshots
from being mistaken for one that prunes /.snapshots.

Prefer $OMARCHY_PATH over the packaged copy when locating the config
script, per docs/migrations.md, so the migration test exercises the
checked-out script rather than whatever release is installed. Skip when
neither exists: omarchy-migrate runs under set -e, so a missing script
would take down every migration queued behind it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:00:02 -07:00
David Heinemeier Hansson 6465513534 Merge pull request #6344 from husamemadH/fix-non-latin-keyboard-layout
Fix unusable system when non-Latin keyboard layout selected at install
2026-07-26 18:22:39 -07:00
David Heinemeier HanssonandClaude Opus 5 e232c99ffd Only lead with us when the layout can't type Latin letters
Prepending us whenever the layout wasn't already us would have demoted
every Latin layout — de, fr, dk — to a secondary group, so those users
would have booted into a US layout they never asked for. Latin layouts
resolve the default keysym bindings fine, so only the layouts that can't
type Latin letters need us in front. That list already exists for the
initramfs hook, and a test keeps the two copies in sync.

Also match on the first layout rather than looking for us anywhere in
the list: Hyprland resolves bindings against the leading entry, so
"il,us" needed the fix just as much as "il" did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 18:22:16 -07:00
husamemad b018719338 Prepend 'us' to kb_layout when non-Latin layout is set
Choosing a non-Latin layout (e.g. ara, il, ru) via /etc/vconsole.conf
leaves Hyprland unable to match Omarchy's Latin-keysym bindings, since
Hyprland resolves bindings against the first layout in kb_layout only.
Prepending 'us' when the detected layout doesn't already include it
keeps shortcuts reachable, and activating grp:alts_toggle lets the user
switch to their layout with Left Alt + Right Alt.
2026-07-26 18:19:00 -07:00
d3fdaca1a9 Fix Arabic falling back to Nastaliq in Chromium and Electron apps (#6377)
* Fix Arabic falling back to Nastaliq in Chromium and Electron apps

* Cover the widened patterns too, and leave Urdu in Nastaliq

The weak binding only won the last-resort race for patterns fontconfig
had not widened. Anything whose family expands through the latin and
nonlatin alias chains -- Arial, Helvetica, Verdana, sans-serif, and so
the bulk of real CSS -- still resolved Arabic to Kufi, a display face
that suits body text no better than Nastaliq did. A strong binding wins
those too. Charset matching outranks family matching either way, so a
font that does not cover the codepoint still cannot be pulled in: Latin,
monospace, emoji, Nerd Font glyphs and CJK all resolve as before.

The untargeted rule also captured Urdu, which #6322 had deliberately
left alone by scoping itself to lang=ar. Urdu is conventionally set in
Nastaliq, so name it for lang=ur ahead of the Naskh append. Appended
rather than prepended, or it would displace the family the app asked for
and render Latin text in an Urdu locale as Nastaliq.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mb1gPTJ6s7QyQZck7Z6F9v

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:39:04 -07:00
ada53b090e Anchor the webcam overlay to the recorded region, not the monitor (#6384)
* Anchor the webcam overlay to the recorded region, not the monitor

Recording a single window on an ultrawide put the camera outside the frame.
The overlay is placed twice, and neither placement knows what is being
captured: the window rules in default/hypr/apps/webcam-overlay.lua position it
from monitor_w/monitor_h, and omarchy-capture-webcam-resize recomputes the same
corner from hyprctl monitors. Both resolve to the monitor's bottom-right, so a
window anywhere but the far right of the display records without the camera in
it, and dragging it into frame by hand is undone by the next resize.

The geometry is already known. select_capture_target resolves the picked window
to a region rectangle before start_webcam_overlay runs, and a bare click in
smart mode snaps that rectangle to the window's own bounds. gpu-screen-recorder
takes the region in the compositor's logical coordinate space, which is the
same space window moves take, so the value needs no conversion to be reused as
an anchor.

Publish it for the resize helper, which becomes the single place that positions
the overlay. It anchors to the region when one is recorded and falls back to
the monitor otherwise, so full-monitor captures, the portal backend, and manual
resizes outside a recording all keep their current behaviour. A malformed or
empty file falls back the same way.

Presets scale from the anchor's height rather than the monitor's, so the camera
keeps its proportion of the frame instead of covering a small capture outright.
A tall, narrow region cannot fit a preset derived from its height, so widths are
capped to the space available and heights follow at the same 8:9 aspect.

Placement happens inside start_webcam_overlay rather than after it returns.
Correcting the position once that function had returned left the move adjacent
to gpu-screen-recorder starting, and the camera was recorded sliding the last
stretch into its corner over the opening frames. The overlay is waited for
explicitly instead, positioned, and only then does capture start.

Waiting for the map is what the blind second was partly guessing at, so the
remainder is trimmed to hold the delay before capture where it was. Starting
later is not free: it eats the opening words of whatever is being narrated.

* Keep the webcam size ladder usable in a narrow region

Capping each preset's width to the region separately collapsed small,
medium and large onto the same width, so Super + Alt + [ and ] had
nothing to step between, and integer division left small taller than
medium. Cap the height the presets scale from instead, which shrinks
the ladder as a whole and keeps the three sizes ordered and distinct.

Cover the anchoring in the test suite: a region the camera follows, the
fallback for an unreadable one, and the narrow-region ladder. Point
XDG_RUNTIME_DIR at the test's own directory while doing so — the resize
helper now reads a region file from there, and the existing geometry
assertions would pick up a real one from a live recording.

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>
2026-07-26 15:27:06 -07:00
Gavin Nugent d8397ee654 Merge pull request #6382 from 28allday/resolve-full-opacity
Exempt DaVinci Resolve from default window opacity
2026-07-26 15:22:08 -07:00
David Heinemeier Hansson 3f18182ef3 Use consistent firefox glyph 2026-07-26 15:13:52 -07:00
David Heinemeier Hansson 35ebe2dfab Merge pull request #6383 from scottjones/menu-search-rank-apps
Rank installed apps above equally matching menu entries
2026-07-26 15:09:04 -07:00
David Heinemeier HanssonandClaude Opus 5 f375113fe3 Find the omarchy-pkgs checkout for PKGBUILD coverage
The three hardcoded candidates are all relative to a sibling of the omarchy
checkout, so a clone anywhere else, such as ~/Work/omacom/omarchy-pkgs, found
nothing. The block then crashed on read_text() and the whole test exited on an
unhandled traceback, which reads as an unrelated failure rather than a missing
checkout.

Add the omacom layouts, an OMARCHY_PKGS_PATH override taking either the
checkout or its pkgbuilds directory, and a named failure listing where it
looked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:53:29 -07:00
David Heinemeier HanssonandClaude Opus 5 425c3ff84d Only check for pending migrations at login
omarchy-update-user-notify.path watched /usr/share/omarchy/migrations, but
pacman writes that directory during every update, including the blessed
omarchy update, which runs omarchy-migrate a step later. The watcher fired a
critical notification for the migrations the update was already applying in
the visible terminal. A watcher cannot tell that apart from a bypassed
pacman -Syu, so the only trigger that never collides with a running update is
a once-per-login check.

The service that already ran at graphical-session.target is now the whole
mechanism, renamed after the command it runs. That is also all the second-user
case needs: markers are per-user, so anyone who did not run the update finds
them missing at their next login.

Login timing means the toast can be sent before the shell has claimed
org.freedesktop.Notifications, so the notifier waits for a live server first.
The wait is omarchy-first-run's, lifted into omarchy-notification-wait rather
than duplicated.

The package keeps omarchy-update-user-notify.service as a symlink onto the new
unit. Existing users hold an absolute wants symlink to the old path, and the
migration that repoints it only runs for users who run an update, which is the
opposite of who the notifier is for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:53:29 -07:00
David Heinemeier HanssonandClaude Opus 5 171b6374c6 Keep the zram fallback until its replacement is installed
zram-generator creates no device at all when nothing configures one, so the
/etc copy is the only thing holding up swap until the vendor drop-in lands.
Removing it early costs a machine its zram entirely, not just its tuning.

The update pipeline installs packages before it runs migrations, so a packaged
machine always has the drop-in by then. A dev checkout does not: omarchy-update-dev
pulls migrations from a release the installed package has never seen, and no
ordering of the pipeline can produce a file that has not been built yet.
Checking for the drop-in is what makes the removal safe rather than well
sequenced.

The tests pinned the drop-in path to a fixture as well. Left at the real path
they would pass or fail on whether the machine running them happened to carry
the packaged copy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:37:22 -07:00
David Heinemeier HanssonandClaude Opus 5 9ec7916c0b Keep the inhibit-delay check alive when there is nothing to check
Migrations run under bash -euo pipefail, where an assignment from a failed
command substitution ends the script. Reading InhibitDelayMaxSec out of a
drop-in that is not there exits sed 2, so the migration died at exactly the
condition it was written to detect: instead of flagging reboot-required, it
aborted before reaching the flag. The busctl read had the same shape, with
pipefail standing in for the failed substitution.

An aborted migration is never marked complete and takes omarchy-migrate's own
-e down with it, so the two migrations queued behind this one stopped running
as well.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:37:22 -07:00
David Heinemeier HanssonandClaude Opus 5 187c2ecf7f Size zram the way Fedora does
zram-size = min(ram, 8192) is Fedora's default since F34, where
zram-fraction=1.0 with max-zram-size=8192 has run on desktops for five
years. The previous min(ram / 2, 8192) halved that on every machine
below 16G with nothing behind the fraction; at 16G and above the two
already agree.

A changed size only reaches a running device on the next boot, so the
migration resizes an empty device now and asks for a reboot otherwise.
It asks the generator what the config evaluates to rather than
repeating the expression here, and does neither when the device already
matches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:58:02 -07:00
David Heinemeier HanssonandClaude Fable 5 f01acc1de9 Go back to stock libfprint now that 1.94.100 ships FocalTech MOC
libfprint-git only existed to carry the focaltech_moc driver and the
FocalTech FT9349 (2808:a97a) ID ahead of a release. libfprint 1.94.100
has both and is in the Arch repos, so fingerprint setup installs stock
libfprint again and a migration swaps existing installs back.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 12:31:04 -07:00
Scott JonesandClaude Opus 5 0c6b07e2c4 Trim ranking comments
Say it once and match mergeAppRows: app rows sort after all menu items,
not interleaved among them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:34:43 -04:00
Scott JonesandClaude Opus 5 ab798d80b2 Rank installed apps above equally matching menu entries
Searching the menu for an installed app buried it: "brave" listed Setup >
Defaults > Browser, Install > Browser and Remove > Browser ahead of the
Brave app itself. All four are exact label matches scoring 0, so the
tiebreak falls to declaration order, and mergeAppRows appends app rows
after every static item.

Bias app rows ahead of menu entries that match equally well. The bias is
smaller than the gap between match tiers, so a menu entry that matches the
query better still sorts first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:25:31 -04:00
David Heinemeier Hansson a8977c4433 Only show linux-ptl kernel when that's installed so we don't end up with two options on XPS 2026-07-26 10:53:18 -07:00
David Heinemeier HanssonandClaude Opus 5 2e04f093c1 Ship zram tuning as a vendor drop-in
pacman checks file conflicts during transaction prepare, so the --overwrite
entry added alongside /etc/systemd/zram-generator.conf never got to run: the
copy of omarchy-update-system-pkgs driving the upgrade is the one already on
disk. Every machine carrying archinstall's copy aborted the transaction.

zram-generator.conf(5) reserves /etc for the local admin and has vendors ship
snippets under /usr/lib/systemd/zram-generator.conf.d/, where drop-ins outrank
the main config file. Nothing collides, and the tuning wins whether or not
archinstall's file is still around.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LjeyQZsNBxqyYy9z8KaKm7
2026-07-25 14:01:38 -07:00
David Heinemeier Hansson ec59ac70f8 Hide copilot alias 2026-07-25 13:39:42 -07:00
David Heinemeier HanssonandClaude Opus 5 6ebdfa3824 Stop offering fingerprint setup on machines with no reader
A Dell XPS 14 with no fingerprint reader was invited to set one up on first
run. Its Synaptics webcam bridge, 06cb:0701 "SVP7500" bound to usbio-bridge,
matched omarchy-hw-fingerprint on the bare 06cb vendor ID.

The vendor list already excludes Elan and STMicro for being multi-purpose, but
Synaptics is just as multi-purpose and was left in. Dropping 06cb is not an
option: Synaptics and Validity are among the most common real reader vendors,
and many of those readers report no product string at all, so the vendor ID is
the only signal that finds them.

The vendor guess is now qualified instead. libfprint drives every reader it
supports from userspace over libusb, so a real reader sits with no kernel
driver bound to any of its interfaces, while the bridges, touchpads and cameras
that share these vendor IDs all bind one. The guard applies only to the fuzzy
vendor-ID path; a device whose product descriptor names it a fingerprint reader
is still trusted outright, since that signal is precise enough on its own and
should not be lost to a driver that happened to claim the device.

The menu entry was a second surface for the same bug. Setup > Security >
Fingerprint carried no when clause, unlike the Remove entry beside it, so it
appeared on every machine and dead-ended in the setup script's own hardware
check. It now gates on the same detector.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017q97Rsk8KwgaPUVq5ArLgM
2026-07-25 13:37:17 -07:00
David Heinemeier HanssonandClaude Opus 5 1756864129 Swap the Panther Lake kernel in before the camera DKMS modules
A fresh install on a Dell XPS 14 spent 73s in "Configuring system", and two
silent gaps in the install log account for 66s of it. Both are the same
redundant work.

hardware/all.sh ran ipu7-camera.sh before ptl-kernel.sh. intel-ipu7-camera
pulls in ipu7-drivers, vision-drivers and v4l2loopback, so DKMS compiled all
three against the stock linux kernel and built a UKI for it. ptl-kernel.sh
then installed linux-ptl, which recompiled the same three modules against the
new kernel, and its pacman -Rdd of linux followed with three dkms removes and
yet another UKI rebuild. The log shows four UKI builds where one would do, and
everything the first script produced was deleted 42 seconds after it was made.

Running the kernel swap first means the DKMS modules are only ever built
against the kernel the machine actually keeps, which returns roughly 25s.

ptl-kernel.sh still installs linux-ptl before removing linux, so one redundant
UKI build for the stock kernel remains. Removing first would save it but would
leave the machine with no kernel at all if the linux-ptl install failed, so the
safe order stays.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017q97Rsk8KwgaPUVq5ArLgM
2026-07-25 13:37:07 -07:00
David Heinemeier HanssonandClaude Opus 5 9ddcec272d Lock the screen before suspend instead of racing logind for it
Closing the lid could suspend the machine with the session still exposed.
omarchy-sleep-lock held a delay inhibitor and waited for Quickshell to report
the session secure, but a delay inhibitor is a timer rather than a promise:
logind suspends once InhibitDelayMaxSec expires, locked or not. The default is
five seconds, and closing the lid also reconfigures displays -- exactly when
the lock service is waiting for the screen set to settle before it can secure.

The race is now off the critical path. switch:on:Lid Switch runs
omarchy-system-lid-close, which requests the lock the moment the lid closes,
before logind has decided to suspend at all, so the inhibitor window usually
finds the session already secure. A docked lid close does not suspend
(HandleLidSwitchDocked defaults to ignore) and must not lock either, since that
is clamshell mode still in use on the external display, so the handler gates on
the same closed-and-undocked pair logind itself keys on.

Suspends that never touch the lid -- idle timeout, the system menu, low battery
-- still arrive through the inhibitor, so that path gets room to work too. The
shipped drop-in raises InhibitDelayMaxSec to 15s, and the helper derives its
budget from logind's live InhibitDelayMaxUSec rather than assuming the drop-in
landed: a machine that has not reloaded logind yet, or that carries its own
override, gets a budget that fits what logind will actually allow. It leaves
logind a fifth of its own window and caps at 12s, so a hand-raised window
cannot strand a closed laptop awake in a bag.

The wait itself had three defects. Its deadline arithmetic read EPOCHREALTIME
assuming a period, so under any comma-decimal locale the subtraction parsed as
bash's comma operator and silently voided the deadline, leaving only the
attempt counter to stop it. The lock request shared the status polls' timeout
and exited on first failure, so a shell 300ms slow meant suspending unlocked;
it now has its own budget and is simply retried, since asking again is
idempotent. And a refusal the shell reports on stdout with a zero exit --
missing-pam -- read as success, burning the whole window on a lock that could
never happen.

Every call is bounded by what is left of the budget rather than by an estimate
of what the step should cost, so the deadline holds on hardware slower than
anything the constants were fitted to.

Failure is still possible and it used to be silent. It now writes to the
journal and raises a critical notification, which lands on the screen the user
unlocks into.

Incidentally, monitor-recovery-test asserted lid probing against
omarchy-hw-clamshell after that logic moved to omarchy-hw-laptop-closed. It
aborted the file under set -e, skipping the nine assertions behind it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 12:35:17 -07:00
David Heinemeier Hansson f9bd0fd9d5 Use consistent preview 2026-07-25 11:56:39 -07:00
David Heinemeier Hansson 2fb65d0a14 Swap the default background for Lupine 2026-07-25 11:16:48 -07:00
David Heinemeier Hansson 57923f0378 Support positional section when moving bar widgets 2026-07-25 11:14:04 -07:00
David Heinemeier HanssonandClaude Opus 5 195df4f5c7 Repair Neovim theme symlinks the earlier relink missed
1781158082.sh matched an explicit list of three legacy spellings for
~/.config/nvim/lua/plugins/theme.lua. At least one more exists in the
wild -- "../../../../.config/omarchy/current/theme/neovim.lua" -- so
those installs fell through the case, kept a dangling link, and Neovim
loaded no colorscheme spec at all. The failure looks theme-specific
because it only surfaces when you next switch themes.

Editing that migration would not help anyone, since it is already marked
applied. Add a new one that matches the shared suffix instead of an
enumerated list, so every spelling of the pre-state-dir path is caught.
It exits early when the link already points at the state directory and
ignores targets outside omarchy/current, leaving custom links alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LjeyQZsNBxqyYy9z8KaKm7
2026-07-25 11:04:55 -07:00
David Heinemeier Hansson fc4caf3c60 Extract notification center bar widget 2026-07-25 10:59:09 -07:00
David Heinemeier HanssonandClaude Opus 5 6cea478581 Separate Lupine's green from its yellow
Both slots held #026fde, so strings and numbers rendered in one color
and syntax highlighting lost the distinction entirely. It mattered less
when the theme shipped its own nvim and vscode files; now that both
generate from this palette, the collision is visible everywhere.

The green slot takes a violet. Lupine's hues sit at 210, 214, 224, 267
and 301, leaving one real gap between blue and magenta, and that gap is
the only place a new color fits without either crowding an existing hue
or breaking the blues-and-purples character. Its bright variant becomes
a lavender, which suits a theme named for the flower. Nothing here reads
as green; the slot keeps its ANSI name, not its usual hue.

Yellow, orange and cyan remain close to each other. Pulling those apart
means either warm hues or a wider spread than the theme allows, which is
a bigger change than this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CC9kSQv8ZEogaxDoeKBzL
2026-07-25 10:42:15 -07:00
David Heinemeier HanssonandClaude Opus 5 4e43932c95 Redraw Lupine's omarchy wallpaper from the theme palette
The JPEG drew the wordmark in #358fff on pure white, matching neither
the accent nor the background. Every other theme's omarchy wallpaper is
a flat two-color image: the theme's background behind its accent. Lupine
now follows that, so the wallpaper, the lock screen, and the focused
window border are all one blue.

Recolored from the shared wordmark rather than redrawn, so the glyphs
land on the same pixels as every other theme. JPEG was the wrong
container for two flat colors anyway; at one bit per pixel the PNG is
2KB against 71KB, with no ringing around the letters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CC9kSQv8ZEogaxDoeKBzL
2026-07-25 10:42:15 -07:00
David Heinemeier HanssonandClaude Opus 5 09e387a846 Cut Lupine's wallpapers to six and renumber
Drops city, lavender-bloom, dusk-horizon, purple-mountains, lake-dawn,
and snowy-dawn, then renumbers the survivors so the sequence stays
contiguous. The remaining set is uniformly pale, which is what a light
theme needs; the cut ones were mid-tone or saturated enough that the
bar and window borders lost contrast against them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CC9kSQv8ZEogaxDoeKBzL
2026-07-25 10:42:15 -07:00
David Heinemeier HanssonandClaude Opus 5 003a60b55f Generate Lupine's app themes from colors.toml
Rename the palette keys to the canonical background/foreground names the
resolver actually reads. The short bg/fg spellings were never aliased, so
every derived key was silently falling back.

Fill in the orange and brown slots too. Lupine is a deliberately cool,
near-monochrome blue palette, so both stay in family rather than
introducing warm hues the theme never had.

Drop neovim.lua and vscode.json. They pinned third-party extensions that
had to be installed separately; the generated templates cover both from
colors.toml alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CC9kSQv8ZEogaxDoeKBzL
2026-07-25 10:42:15 -07:00
Bjarne Øverli 70d6f12169 Adopt Omarchy 4 theme conventions for Lupine
Convert the Lupine theme to the semantic color system introduced in the
quattro/Omarchy 4 line:

- Rewrite colors.toml from the old accent/foreground/background/color0-15
  scheme to the semantic palette (mode, bg/fg ramp, named ANSI hues, and
  bright_* variants). The chromatic palette is preserved exactly; only the
  keys are renamed.
- Replace the empty light.mode marker with mode = "light".
- Drop the hand-shipped btop.theme and chromium.theme; both are now
  generated from default/themed/*.tpl at theme-set time.

Selection now follows the convention used by the other light themes
(selection_foreground = bright_fg over a light selection background)
instead of the previous inverted dark selection bar.
2026-07-25 10:42:15 -07:00
Bjarne Øverli 6e29b1416a Renumber Lupine wallpapers to contiguous zero-padded sequence 2026-07-25 10:42:15 -07:00
Bjarne Øverli 4581140b8a Remove unused wallpapers from Lupine theme 2026-07-25 10:42:14 -07:00
Bjarne Øverli 4a2a228de0 Add 10 more light wallpapers to Lupine theme 2026-07-25 10:42:14 -07:00
Bjarne Øverli 60393e8269 Remove twilight-sky wallpaper from Lupine theme 2026-07-25 10:42:14 -07:00
Bjarne Øverli c828149045 Add lavender and pastel sky wallpapers to Lupine theme 2026-07-25 10:42:14 -07:00
Bjarne Øverli 1008776ddf Add Lupine theme 2026-07-25 10:42:14 -07:00
David Heinemeier HanssonandClaude Opus 5 e43ddf0eaa Say when there's no notification history to replay
Replaying an empty history did nothing at all on screen, which reads as
a dead keybinding rather than an empty log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 09:41:14 -07:00
David Heinemeier Hansson 5952c016a6 We don't need these
Enough to change via Omarchy menu
2026-07-25 09:40:40 -07:00
David Heinemeier HanssonandClaude Opus 5 b65dde39fc Keep fingerprint unlock offered on the lock screen with the lid shut
The reader is still reachable on an external keyboard or a docked
laptop, so hiding the icon and skipping the scan just forced the
password. sudo and polkit keep their pam_exec lid gates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 08:52:31 -07:00
David Heinemeier HanssonandClaude Opus 5 1bc98a7dd5 Make the font choice actually stick
The user override was written with mode="assign", but it loads after the
package default has already assigned JetBrainsMono, so it never displaced
it. prepend_first puts the chosen family at the head of the list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 08:46:39 -07:00
David Heinemeier HanssonandClaude Opus 5 b5d57ab133 Seed Tensaku's annotation size so first use skips the welcome prompt
Tensaku shows a "Pick a default annotation size factor" dialog until
its persisted state names one. Neither config.toml nor the CLI flag
suppresses it, so ship the state file itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 08:44:52 -07:00
David Heinemeier HanssonandClaude Opus 5 5cd753b206 Give Solitude and Last Horizon their own unlock screens
Solitude had no unlock assets at all, so it fell back to the default
Plymouth logo. Last Horizon shipped a preview-unlock.png derived from
Nord's that still carried Nord's navy #2e3440 entry-box fill; only the
background and logo had been recolored.

Both are now generated the way the rest of the set was: the logo mask
recolored to the theme's accent, and the preview rendered by the same
pipeline as `omarchy plymouth preview` using each theme's background
and foreground. Last Horizon's unlock.png was already correct and is
untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CC9kSQv8ZEogaxDoeKBzL
2026-07-25 08:39:50 -07:00
David Heinemeier HanssonandClaude Opus 5 5884397e46 Drop the bar.json nobody reads
It was the user override file for the first Quickshell bar, back when the
bar layered it over bar-defaults.json. Unifying everything into
shell.json left an empty file that installs and the quattro upgrade kept
faithfully copying around.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 08:21:04 -07:00
David Heinemeier HanssonandClaude Opus 5 22f534f6db Only list the indicators once
Moving them ahead of the clock left the old entry in place, so a fresh
config rendered the widget on both sides of the time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 08:21:04 -07:00
David Heinemeier HanssonandClaude Opus 5 e880cf77cc Alert on tmux output while its terminal is unfocused
tmux only raises an activity flag for windows that are not currently
selected, so a long-running command in the window you left selected --
the common case, since you switch away by moving your Hyprland focus
elsewhere rather than by selecting another tmux window -- finished
silently and the indicator never lit.

Windows now also count as waiting when they are selected in an attached
client, every client showing them is unfocused, and their window_activity
is newer than an @omarchy_unfocused_activity watermark. The watermark is
stamped by a new `track` subcommand wired to client-focus-in/out and the
existing select-window hooks, so it records where attention last was.
The hooks pass #{window_id} and #{window_activity} as arguments, which
tmux expands when the hook fires; run-shell -b would otherwise let output
arriving during the handoff be swallowed by the new watermark. The focus
hooks take index 100 to leave a user's own bindings alone.

That state has no hook of its own, so it needs polling to be noticed.
The probe therefore moves out of the indicator and into a service plugin,
alongside nightlight and battery. A bar surface exists per monitor and
each one instantiates every indicator twice, once per block, so a timer
on the indicator meant a shell-out per instance per tick -- four probe
processes every three seconds on a two-monitor machine, forever. One
service polls for the whole shell instead.

Sharing the state also fixes what per-instance polling would have papered
over: each indicator used to own its own count, so a timer-driven update
only refreshed the bar it ran on and left the other monitors stale.
Refreshes still arrive over the existing indicator broadcast, which now
coalesces into a single run no matter how many bars relay it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CC9kSQv8ZEogaxDoeKBzL
2026-07-25 07:19:02 -07:00
David Heinemeier Hansson 2503edadfc Jiggle active terminal alert indicator 2026-07-25 07:19:02 -07:00
David Heinemeier Hansson 657c427a78 Do restart too and drop needless guard 2026-07-25 07:18:30 -07:00
David Heinemeier HanssonandClaude Opus 5 896beb0c85 Park the indicators just left of the clock
The lineup now reads toward the clock instead of away from it, so Stay
Awake sits nearest the time, then do-not-disturb, night light, reminder,
screen recording, dictation, and tmux alerts trailing off to the left.

Active indicators render after the hover-revealed ones so they stay
against the clock, and a newly active indicator joins on the far side
rather than shoving the ones already showing sideways.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 07:14:58 -07:00
David Heinemeier HanssonandClaude Opus 5 ec1553d720 Let tmux alerts actually reach the bar
The tmux alert hooks fire omarchy-shell through run-shell, and run-shell
builds its own environment rather than passing the server's along, so the
hook ran without WAYLAND_DISPLAY. qs matches instances by display, so every
refresh from a bell died at "omarchy-shell is not running", which -q then
swallowed by design.

That left the indicator with no way to turn itself on. Its five second poll
only runs while the indicator is already active, so an alert raised after
startup was invisible until the shell happened to restart with the window
still flagged. The icon appeared roughly never.

Recovering the display from the socket in XDG_RUNTIME_DIR fixes it in the
one place that needs it, and covers any other caller reaching the shell from
a stripped environment rather than just the tmux hooks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 07:03:58 -07:00
David Heinemeier Hansson ec7cd9f039 It's only these two for now 2026-07-24 19:49:13 -07:00
David Heinemeier HanssonandClaude Opus 5 4dcbed7728 Tune swap on zram instead of leaving it at kernel defaults
Omarchy installed zram-generator but never shipped a config for it, so the
only zram tuning any machine had was whatever the ISO wrote once, and the VM
sysctls were untouched kernel defaults aimed at spinning disks.

The generator's own default caps the device at 4G, which on a large machine
sends reclaim to the hibernation swapfile far earlier than it needs to go.
Ship min(ram / 2, 8192) instead, matching what Fedora settles on, and state
the priority explicitly since it has to sit above the pri=0 that
omarchy-hibernation-setup gives the disk swapfile.

The four sysctls follow from swap being RAM rather than a disk. swappiness
above 100 says evicting an anonymous page beats dropping a page-cache page
that would have to be read back, which is true once swap is compressed
memory. page-cluster drops to one page per fault because the default of 8
buys readahead for a seek zram doesn't have and pays a decompression for
each page. Zeroing watermark_boost_factor stops fragmentation from producing
reclaim bursts while memory is still free, and raising watermark_scale_factor
gives kswapd room to reclaim in the background instead of letting allocations
stall in direct reclaim. The last two are what actually addresses the stutter
people notice and misread as swap being used too eagerly: proactive swapping
to zram is the cure, and synchronous direct reclaim is the disease.

Every ISO-installed machine already has an unowned zram-generator.conf, which
pacman would refuse to overwrite, so both upgrade paths need it listed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 19:48:00 -07:00
David Heinemeier HanssonandClaude Opus 5 c6ad61448f Stop the launcher from listing an app more than once
The apps list rebuilt its rows by writing into the maps held by the menu's
items and itemOrder var properties. Writing into an object owned by a QML var
property is not reliable: the same row object written into a plain JS object
always lands, but written through the property it occasionally arrives with the
key created and the value undefined. One write per rescan was lost, on a
different app each time.

A lost write left an id in itemOrder with no item behind it. The old purge only
deleted app rows it could find in items, so the orphan survived the next merge,
the add loop appended a second row for the same app, and the list grew by one --
permanently, and again on every later rescan. Touching a single desktop file
fires around a dozen merges, because the entry model emits valuesChanged per
insert and removal while it reconciles, so duplicates piled up quickly: nine
YouTube rows on the reporting machine, and Alacritty doubled before that.

The bookkeeping moves into MenuModel as two pure functions that build fresh maps
for the caller to assign in one shot, so the fragile write disappears. They also
make the merge self-healing rather than merely correct-when-nothing-is-lost: an
id with no item is dropped instead of carried forward, and an id is listed once
even when two desktop entries claim it -- so no single dropped write can compound
into a duplicate row again. The bash-backed providers behind the font and power
profile lists had the same latent bug and get the same treatment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJQJfHXXApUk6En8EZisHg
2026-07-24 19:42:07 -07:00
David Heinemeier HanssonandClaude Opus 5 a7285cfecb Put Stay Awake first among the default indicators
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CC9kSQv8ZEogaxDoeKBzL
2026-07-24 19:11:57 -07:00
David Heinemeier Hansson be781574d1 Let the invitation terminals actually open
Clicking the Voxtype or fingerprint invitation did nothing. The launcher
execs setsid, which forks because the transient unit's main process is
already a process group leader, so the unit exits within milliseconds and
systemd's default control-group kill took the still-starting terminal down
with it. Run those units with KillMode=process instead.

The invitation tests had also drifted from the two-unit design and were
failing; teach the systemd-run mock to run the command it is given.
2026-07-24 18:57:51 -07:00
David Heinemeier Hansson 0b564255ea Stack the Wi-Fi toast above the update toast on first run
Both notifications are sent from background subshells, so whichever
notify-send registered first won the bottom slot. Give the update toast a
tick to register so Wi-Fi lands newest and stacks on top.
2026-07-24 18:52:26 -07:00
David Heinemeier HanssonandClaude Opus 5 a466dcc04f Cover the XPS 16 with the XPS 14 speaker tuning
Reported to suit the XPS 16 as well, so the tuning now matches both machines and
the directory is named for the pair rather than the one model.

Gating moves from the DMI product name to the product SKU, which is what Dell keys
the Cirrus speaker firmware on -- 10280db9 for the XPS 14, 10280dba for the XPS 16
-- so it identifies the speaker hardware itself instead of a marketing name. SKUs
are compared as whole values, not substrings, so this cannot widen to the rest of
the XPS line the way a short product-name match would: product_family on these
machines is "Dell Laptops".

match_sku and match_dmi are now lists, letting one tuning name every model it
covers. A plain string still works as a single entry.

The measurements in tuning.conf were taken on the XPS 14 (0DB9). The XPS 16 is
covered on report rather than measurement, and tuning.conf and the graph both say
so, because the profile's bass lift and high-pass were fitted against one
machine's excursion and distortion limits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 18:32:31 -07:00
David Heinemeier HanssonandClaude Opus 5 aa9f0c54c5 Add per-laptop speaker tunings, starting with the XPS 14
Laptop speakers ship voiced by the vendor's Windows DSP layer, which Linux does
not get. A tuning restores that as a PipeWire filter-chain in front of the
internal speaker sink, matched to the machine by DMI string and expected sink.

Adding a laptop is a directory under default/audio/tunings with two files and no
new code: matching is data. The XPS 14 DA14260 tuning included here was derived by
measuring the xps-audio-linux EasyEffects profile (MIT) and fitting a biquad chain
to it, so no impulse response or other upstream asset is redistributed. It measures
1.24 dB RMS against that reference, and matches its dynamic range within 0.1 LU --
the reference's multiband compressor turned out to contribute nothing, so a linear
chain replaces it. Bass Q is capped deliberately: a closer magnitude fit swung
group delay 31 ms across 63-80 Hz, which smears bass transients.

The graph runs as its own PipeWire client under its own config name rather than
loading into the audio daemon. The daemon only reads its config at startup, so a
daemon-loaded tuning could only be switched by restarting PipeWire -- which drops
every PulseAudio client's connection, and applications that do not reconnect
(Spotify) then have to be restarted by hand. Hosting it separately also contains
failure, since a malformed tuning breaks only that service.

Three things about the surrounding audio graph needed fixing for this to behave:

- Volume must live downstream of the tuning. omarchy-audio-output-sink is now the
  single definition of which sink an output's volume really uses, shared by the
  volume keys, the output switcher's OSD and the audio panel, so they cannot
  disagree. It resolves the current default output, which keeps it correct when
  headphones are selected while a tuning exists.
- The tuning's own output is a movable sink input, so rerouting "all streams" to a
  newly selected output would drag the processing onto headphones, or into the
  tuning's own sink, which is a cycle. It is pinned, and stream moves are limited
  to streams carrying an application.name.
- The physical sink a tuning fronts is not independently selectable, since picking
  it would only bypass the tuning, so it is kept out of the output list.

Applying happens at first-run, not finalize-user, because finalize-user also runs
in the ISO chroot where there is no audio server and nothing would retry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 18:25:02 -07:00
David Heinemeier HanssonandClaude Opus 5 248659de5a Give every OSD card exact padding on all four sides
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LfqzqpU1T6pQRsmpbGxBs5
2026-07-24 16:48:14 -07:00
David Heinemeier Hansson 1e152b0d9e Merge pull request #6370 from basecamp/fix-invitation-reshow-after-install
Stop install invitations from re-showing after they run
2026-07-24 16:39:23 -07:00
David Heinemeier HanssonandClaude Opus 4.8 555cb4db75 Stop install invitations from re-showing after they run
Voxtype/fingerprint invitations run as a transient
omarchy-*-invitation.service that shows a critical toast and waits for a
click. On click the service execs the install terminal, so the service
stays active for the whole life of that terminal.

The installer ends with omarchy-restart-shell, which re-triggers any
still-running omarchy-*-invitation.service on the assumption that a
still-running unit is an unanswered toast wiped by the shell restart. But
an answered invitation is still running only because it is the parent of
the very install terminal that triggered the restart, so it gets falsely
re-fired and the invitation toast pops a second time right after the
install finishes.

Launch the installer in its own transient unit so the invitation service
exits immediately after the click. Genuinely unanswered invitations still
block in notify-send and are still recovered by restart-shell; answered
ones are gone and no longer match the re-show glob.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHPDqx5hwLTwq27f7KpM9d
2026-07-24 16:36:50 -07:00
David Heinemeier HanssonandClaude Opus 5 d2e1587ceb Refresh bar widgets on every monitor, not just one
A bar surface is built per monitor, so a widget in the layout is live once
per screen — but an IPC target only ever routes to the handler that
registered first. `omarchy.indicators refresh` therefore reached a single
bar, and since indicators only re-read their state on that signal, the
other screens kept showing a stale reminder count, tmux alert, or DND
state until the next reload. Clock and system-update refreshes had the
same reach.

Let the bar resolve every live instance of a widget id and relay the call
to all of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 14:16:22 -07:00
David Heinemeier HanssonandClaude Opus 5 49efe2dafa Drop the bluetooth widget's dead middle-click launcher
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:30:28 -07:00
David Heinemeier HanssonandClaude Opus 5 528a4343a9 Add omarchy-hyprland-monitor-laptop for internal display lookup
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:28:55 -07:00
David Heinemeier HanssonandClaude Opus 5 fe036a82fa Give every install group an all.sh driver
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:27:01 -07:00
David Heinemeier HanssonandClaude Opus 5 e1af4dbfac Drive the config install leaves through install/config/all.sh
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:26:16 -07:00
David Heinemeier HanssonandClaude Opus 5 d407454511 Use arithmetic conditionals for numeric tests in bin
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:25:20 -07:00
David Heinemeier HanssonandClaude Opus 5 f15a91cbe2 Read the BIOS vendor once in the direct boot setup
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:24:30 -07:00
David Heinemeier HanssonandClaude Opus 5 8d6ea817dd Title-case theme names in one pass
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:24:09 -07:00
David Heinemeier HanssonandClaude Opus 5 a2ec713603 Collapse the duplicated run_logged invocation branches
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:23:23 -07:00
David Heinemeier HanssonandClaude Opus 5 3b4d8616a9 Drop the redundant tee helper and unused vars in the lock setup
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:22:36 -07:00
David Heinemeier HanssonandClaude Opus 5 f804d5c9c0 Name the selection layer rule after the tool that draws it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:21:42 -07:00
David Heinemeier HanssonandClaude Opus 5 38794ca82f Replace the retired walker example binding with the emoji picker
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:21:27 -07:00
David Heinemeier HanssonandClaude Opus 5 f8e4f4d4fb Remove leftover waybar theme files
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:20:52 -07:00
David Heinemeier HanssonandClaude Opus 5 e0b889fb98 Reset Plymouth by reusing the refresh commands
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:20:04 -07:00
David Heinemeier HanssonandClaude Opus 5 2996ee6327 Add omarchy-theme-dir and use it to resolve theme paths
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:19:39 -07:00
David Heinemeier HanssonandClaude Opus 5 6aaa5b764e Share one implementation for the touchpad and touchscreen toggles
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:18:25 -07:00
David Heinemeier HanssonandClaude Opus 5 fbb4cc16b1 Flip the toggle once in the screensaver and suspend toggles
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:17:16 -07:00
David Heinemeier HanssonandClaude Opus 5 2baf967e06 Extract shared shell.json editing helpers for the bar commands
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:16:51 -07:00
David Heinemeier HanssonandClaude Opus 5 603408a73d Drop the never-set binary override in the CLI router
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:15:02 -07:00
David Heinemeier HanssonandClaude Opus 5 3f7e4c4708 Share the command record emitter in the CLI router
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 12:14:19 -07:00
David Heinemeier Hansson 69cc29d7b2 Merge pull request #6329 from scottjones/emacs-aur-quattro
Install Emacs via the omarchy-emacs AUR package
2026-07-24 12:06:36 -07:00
David Heinemeier HanssonandClaude Fable 5 3f72553110 Unpack the initramfs synchronously so Plymouth survives early boot
Kernel 7.1 unpacks the initramfs asynchronously, which races /init: the
early /proc, /sys, /dev, and /run mounts fail with EPERM while unpacking
settles, so plymouthd exits when it can't read /proc/cmdline and encrypted
boots fall back to an unthemed text LUKS prompt. Boot still completes only
because the kernel passes key=value cmdline params to init as environment
variables and devtmpfs is automounted.

Force synchronous unpacking via initramfs_async=0 until the race is fixed
upstream: in the packaged defaults for fresh installs, and via migration
for existing systems.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 11:30:55 -07:00
David Heinemeier Hansson b31036f604 Merge pull request #6334 from KazeTachinuu/enterprise-8021x-wifi
Connect to enterprise (802.1X) Wi-Fi networks from the network panel
2026-07-24 10:51:04 -07:00
David Heinemeier HanssonandClaude Fable 5 6963432600 Send the 802.1X password over stdin instead of argv
argv is world-readable in /proc for every local process while the
connection attempt runs. Create the profile without the secret, then
set it through nmcli's scriptable connection editor, which reads from
stdin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 10:50:58 -07:00
David Heinemeier Hansson 3cc3aa7ea3 Merge pull request #6368 from basecamp/drop-wifi-powersave-toggle
Keep Wi-Fi power save off instead of toggling it with AC state
2026-07-24 10:37:47 -07:00
David Heinemeier HanssonandClaude Fable 5 da157fb9c0 Drop Wi-Fi power save toggling in favor of keeping it off
The on-battery toggle bought a fraction of a watt while the radio
idled -- twenty-some minutes across a full discharge in the best case
-- and paid for it with 20-300ms latency spikes on idle links and
outright link drops on Intel BE200/BE211 firmware. NetworkManager can
keep power save off natively with one conf.d line, which replaces the
udev rule, the setter script, and any per-card deny-listing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 10:34:19 -07:00
David Heinemeier Hansson 1d4077287b Merge pull request #6353 from zainsyedz/fix-clamshell-internal-monitor-config
Respect internal monitor config in clamshell recovery
2026-07-24 10:31:00 -07:00
David Heinemeier Hansson a62e95d2e1 Merge pull request #6367 from khpark43/fix-bar-plugin-json-values
Fix bar plugin JSON value validation
2026-07-24 10:24:34 -07:00
Scott JonesandClaude Opus 4.8 7faf53c4c0 Name the monitor SCALE targets when multiple displays are connected
SCALE only applies to the focused monitor, which is easy to lose track
of with several displays. Show the focused monitor's name on the right
of the SCALE header (matching the BRIGHTNESS/TEXT SIZE value
convention), hidden when only one display is enabled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 10:12:28 -07:00
Kyunghyun Park 876d6bcd23 Fix bar plugin JSON value validation 2026-07-25 01:33:30 +09:00
David Heinemeier Hansson ea549a74d5 Merge pull request #6364 from scottjones/fix-screensaver-multimonitor
Run the screensaver on every monitor, not just the last one
2026-07-24 09:32:02 -07:00
David Heinemeier HanssonandClaude Fable 5 26630a9796 Check terminal support once and bound the window wait overall
An unsupported terminal notified once per monitor and each monitor
paid a wasted wait; check before the loop instead. The per-read
timeout also let a chatty event stream extend the wait indefinitely,
so spend the 5 seconds against one deadline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:31:55 -07:00
David Heinemeier Hansson 5a046035d5 Fix shell restart across dev path changes 2026-07-24 09:31:09 -07:00
David Heinemeier Hansson abc9ff03e1 Merge pull request #6366 from scottjones/fix-notification-surface-resize
Fix notification popups stretching/squishing when the count changes
2026-07-24 09:24:51 -07:00
David Heinemeier HanssonandClaude Opus 4.8 b6bd486c39 Bind notification popup mask to the toast column item
Use Region { item: popupColumn } instead of manually mirroring the
column's x/y/width/height. It tracks the item's mapped geometry directly,
so it stays correct without assuming the column shares the window's
coordinate space.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 09:24:30 -07:00
David Heinemeier Hansson 4e550b75c2 Merge pull request #6350 from husamemadH/fix-mise-install-wrapper-recursion
Fix infinite loop in omarchy-mise-install wrappers
2026-07-24 09:20:15 -07:00
David Heinemeier HanssonandClaude Fable 5 5c1b31ada9 Regenerate existing mise wrappers via migration
The generator fix only helps fresh installs; existing wrappers in
~/.local/bin keep the recursive exec line until rewritten. Detect the
generated wrapper shape, extract package and bin, and reinstall each
one, which also repairs wrappers users created for their own packages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:19:56 -07:00
David Heinemeier Hansson bc33381d84 Merge pull request #6348 from InfeCtlll3/agent/scroll-long-tray-menus
Make long tray menus scrollable
2026-07-24 09:09:17 -07:00
David Heinemeier HanssonandClaude Fable 5 332a982a1d Drop source-grep regression assertion
A regex over the QML text breaks on innocent reformatting and passes
even when the scrolling behavior regresses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:09:02 -07:00
David Heinemeier Hansson e135508257 Merge pull request #6356 from khpark43/fix-default-browser-xdg-settings
Fix XDG browser defaults when BROWSER is set
2026-07-24 09:01:39 -07:00
David Heinemeier HanssonandClaude Fable 5 e61ff1ef4f Unset BROWSER for xdg-settings reads and don't abort removal on fallback failure
xdg-settings get also consults $BROWSER, so unset it for the getters too.
A failed best-effort fallback default should not abort the browser removal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:01:17 -07:00
Scott JonesandClaude Opus 4.8 b1a9d7912e Give notification popups a fixed-size surface to stop resize scaling
The popups used one Wayland layer surface per output sized to their content
(implicitWidth/Height bound to the toast column), so every time a toast was
added or removed the surface resized. During that resize the compositor
briefly scaled a stale buffer, stretching or squishing the cards mid-
transition -- worst with only one or two toasts, where each resize is a
larger relative change.

Make the surface full-screen and fixed, like the OSD overlay, so adding or
removing a toast only changes the content inside it and the surface never
resizes. Mask the input region to the toast column so the otherwise-
invisible full-screen overlay stays click-through.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:47:53 -04:00
Scott JonesandClaude Opus 4.8 6ecb1dbb56 Run the screensaver on every monitor, not just the last one
omarchy-launch-screensaver loops over the monitors, focusing each and
spawning a terminal on it -- but the spawn is asynchronous. Slow-starting
terminals mapped their windows after focus had already moved to the next
monitor, so every screensaver landed on the last-focused monitor and the
others got none (and the fullscreen rule applied inconsistently when two
windows mapped at once).

Listen on Hyprland's event socket and wait for each monitor's screensaver
window to open before moving focus to the next one, so there is exactly one
screensaver per monitor. The stream is opened before spawning so a
fast-mapping window can't be missed, with a read timeout as a safety net.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 01:14:09 -04:00
David Heinemeier HanssonandClaude Fable 5 2c8639ffbd Persist workspace layout toggles across Hyprland reloads
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 21:03:43 -07:00
David Heinemeier Hansson 202296324a Keep display scaling options consistent 2026-07-23 20:37:25 -07:00
David Heinemeier Hansson f20da3f94a Tmux jumps are not that important 2026-07-23 20:09:50 -07:00
David Heinemeier Hansson 3e664286cc Use upcase consistently for menu tooltips 2026-07-23 20:09:39 -07:00
David Heinemeier HanssonandClaude Fable 5 ffd3c79ab2 Resend pending invitations after restarting the shell
Restarting the shell kills its notification server, and a pending
invitation's notify-send waiter then hangs forever: the dying server
never emits NotificationClosed. So a still-running
omarchy-*-invitation unit means an unanswered invitation — re-run it
once the new shell is up so its toast reappears.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 19:25:11 -07:00
David Heinemeier HanssonandClaude Fable 5 00eae9a5ef Invite fingerprint setup after the first system update
The setup installs packages (libfprint-git, fprintd, usbutils), which
isn't reliable on a fresh install before the databases have been
synced. Defer the invitation with a post-update hook, like Voxtype.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 19:25:06 -07:00
David Heinemeier Hansson 86f616cfb3 Add emoji picker to Trigger menu 2026-07-23 19:23:50 -07:00
David Heinemeier Hansson 9b09a3c142 Clear update inhibitors before reboot prompt 2026-07-23 18:58:55 -07:00
David Heinemeier HanssonandClaude Fable 5 a0e6d65ba8 Keep universal clipboard shortcuts working in terminals
Sending the shortcut through wtype's virtual keyboard let the physically
held SUPER merge into the injected chord, so terminals saw
SUPER+CTRL+Insert instead of CTRL+Insert, and injected CTRL+V could even
trigger the SUPER+CTRL+V clipboard manager bind. Send via send_key_state
with no window target instead: explicit mods go straight to the focused
surface, layer-shell panels included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XnMDbwJXQA3fvW8o8Pwr97
2026-07-23 18:08:31 -07:00
David Heinemeier Hansson 63955e787f Hide unavailable display scale options 2026-07-23 18:06:19 -07:00
David Heinemeier Hansson 58ec29f20a Move direct boot command to setup 2026-07-23 17:43:36 -07:00
David Heinemeier HanssonandClaude Fable 5 5b130c1063 Make tmux alert parsing and focus more robust
Session names can contain pipes but never colons, so split fields on
colons instead. Remember refreshes that arrive while the indicator is
already polling, and jump to the most recently used tmux client rather
than an arbitrary one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 17:40:47 -07:00
David Heinemeier HanssonandClaude Opus 4.8 a382be2645 Indicate and jump to tmux panes waiting for attention
Show a bar indicator whenever tmux has flagged a window, the same state
that highlights the tab, and jump to it on click or with Super + Ctrl + J.
Tmux hooks push the state to the shell, so nothing polls while no pane is
waiting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:40:47 -07:00
David Heinemeier HanssonandClaude Opus 4.8 98e88723ae Reload tmux config when the server is running
pgrep -x tmux never matched, since the processes are named "tmux: server"
and "tmux: client", so config reloads were silently skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:40:47 -07:00
David Heinemeier Hansson 6d7268eb65 Support clipboard shortcuts in shell panels 2026-07-23 17:12:39 -07:00
David Heinemeier Hansson c844277749 Activate dev checkout before updates 2026-07-23 17:05:29 -07:00
David Heinemeier Hansson ed9e68957f Match display scale highlights to clean scales 2026-07-23 16:48:29 -07:00
David Heinemeier Hansson 534746e05b No needless exit wounds 2026-07-23 16:47:20 -07:00
David Heinemeier Hansson 85a8453e20 Stick with one-liners 2026-07-23 16:42:59 -07:00
David Heinemeier Hansson 2d3d1e2a3c Only offer webcam recording when available 2026-07-23 16:33:12 -07:00
David Heinemeier Hansson 7798ed34b1 Keep apps menu alphabetically sorted 2026-07-23 16:32:31 -07:00
David Heinemeier Hansson 5d83402e75 No need to count the displays 2026-07-23 16:19:29 -07:00
David Heinemeier HanssonandClaude Opus 4.8 cb9485f216 Gate sudo fingerprint behind lid state too
Extend the clamshell gate to /etc/pam.d/sudo, not just polkit-1. When the
lid is shut the reader is unreachable, so a terminal sudo would block on
"Place your finger" until pam_fprintd timed out before letting you type the
password. The same pam_exec gate (success=1 skips fingerprint when the lid
is closed) now runs ahead of pam_fprintd in the sudo stack as well.

setup and removal share one gate definition across sudo and polkit; the
migration now gates both stacks on existing installs.

Resolves the clamshell case in #856 and supersedes #6003.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:29:22 -07:00
David Heinemeier Hansson 8e549c27d0 Update active dev checkout with system 2026-07-23 15:28:32 -07:00
David Heinemeier Hansson 7a9947a732 Combine the Omarchy menu and the launcher
Now that we can deep search, they don't need to be different
2026-07-23 15:04:22 -07:00
David Heinemeier Hansson 347997871e Defer channel switch reboot until after update 2026-07-23 14:19:24 -07:00
David Heinemeier Hansson b2f1bb6ae3 Merge branch 'fingerprint-first-run-invite' into quattro 2026-07-23 14:11:19 -07:00
David Heinemeier Hansson b6e75d5e48 Language improvements 2026-07-23 14:08:08 -07:00
David Heinemeier HanssonandClaude Opus 4.8 e2bf0daaa8 Invite fingerprint setup on first run when a reader is present
Add a first-run notification, alongside the keybindings/Wi-Fi toasts, that
invites anyone with a fingerprint sensor to enable it. Clicking launches
omarchy-setup-security-fingerprint in a floating terminal.

Detection lives in a new omarchy-hw-fingerprint helper that reads sysfs
(device product descriptor plus a fingerprint-vendor allowlist), so it
works before fprintd/usbutils are installed and without nagging machines
that have no reader. The setup script reuses the same helper as an early
gate, bailing before installing any packages when no reader is found
(replacing the old post-install fprintd-list probe).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:05:31 -07:00
David Heinemeier HanssonandClaude Opus 4.8 1fce43e8eb Invite fingerprint setup on first run when a reader is present
Add a first-run notification, alongside the keybindings/Wi-Fi toasts, that
invites anyone with a fingerprint sensor to enable it. Clicking launches
omarchy-setup-security-fingerprint in a floating terminal.

Detection lives in a new omarchy-hw-fingerprint helper that reads sysfs
(device product descriptor plus a fingerprint-vendor allowlist), so it
works before fprintd/usbutils are installed and without nagging machines
that have no reader. The setup script reuses the same helper as an early
gate, bailing before installing any packages when no reader is found
(replacing the old post-install fprintd-list probe).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:03:19 -07:00
David Heinemeier Hansson f911851319 Link dev channel to home checkout 2026-07-23 14:01:36 -07:00
David Heinemeier Hansson e68b0f025d Merge branch 'fingerprint-lid-aware' into quattro 2026-07-23 13:52:31 -07:00
David Heinemeier HanssonandClaude Opus 4.8 540e411edf Show fingerprint on lock screen and polkit, gated by lid state
Bring the fingerprint affordance to the Quickshell lock screen and polkit
dialog, matching what hyprlock did on master.

Lock screen: render the md-fingerprint glyph inside the password field's
right edge when a sensor is enrolled, reserving space so long passwords
never run under it.

Polkit dialog: show one method at a time. When a sensor is enrolled and
the reader is reachable, the dialog is just the centered fingerprint icon
(square card); the moment PAM asks for a password it switches to the
password field. Detects pam_fprintd anywhere in the auth stack now that a
gate can precede it.

Lid awareness: a closed lid means the reader is unreachable, so both
surfaces fall back to the password. polkit gets a pam_exec clamshell gate
(auth [success=1 default=ignore] before pam_fprintd) so a shut lid drops
straight to the password prompt instead of blocking on the reader for the
pam_fprintd timeout; the lock screen hides the icon and skips scanning.

The gate points at the fixed /usr/bin path the package always provides so
it survives switching between package installs and dev-link. A migration
adds the gate for existing fingerprint setups.

New helper omarchy-hw-laptop-closed (pure lid state); omarchy-hw-clamshell
now composes it with the external-monitor check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:51:07 -07:00
David Heinemeier HanssonandClaude Fable 5 141432639f Keep the menu anchored to a stable top line and drop its minimum height
The menu card re-centered on every resize, so searching or moving into
a submenu made it jump around. It still opens centered like before, but
the first search keystroke or submenu move freezes the top line where
it sits — from then on the card only grows and shrinks downward.
Closing unfreezes. Also drop the 220px minimum height so the card
always hugs its content instead of leaving a void under short lists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 13:48:16 -07:00
Kyunghyun Park e99a4343bb Fix Ollama install menu action (#6357) 2026-07-23 13:42:24 -07:00
Kyunghyun Park e5924ef7d1 Fix XDG browser defaults when BROWSER is set 2026-07-24 03:26:49 +09:00
Martín Ramírez Espinosaandalmondsun 39ca9135c4 Fix headless bar widget test skip (#6338)
Co-authored-by: almondsun <maramirezes@outlook.com>
2026-07-23 07:53:57 -07:00
Martin Bastien d548c064ee Remove Snapper timeline snapshots leaked by earlier defaults
Installs from before the Snapper setup was normalized ran hourly
timeline snapshots. Newer configs stopped creating them but never
deleted the existing ones, and number cleanup skips snapshots marked
Cleanup=timeline, so they sit there forever: one machine installed from
the 2026-05-11 ISO had accumulated 592 of them, silently pinning 219 GB
of disk. The limine-snapper-sync limit-mismatch warning that would have
surfaced this is disabled by default since the notifier migration.

Delete leaked timeline snapshots in batches of 20 (a single mass delete
can die on a DBus timeout partway through), and only when
TIMELINE_CREATE="no" so anyone who deliberately re-enabled timeline
snapshotting keeps their setup untouched.

The drain is best effort: a batch that fails is skipped rather than
aborting the migration run and everything queued behind it, since the
next run re-lists whatever is left.
2026-07-23 09:10:16 -04:00
Martin Bastien 8ef4eb24a2 Keep Btrfs snapshots out of the locate index and put /home in it
The stock Arch updatedb.conf interacts badly with Omarchy's Btrfs
layout in both directions:

- Snapper snapshots under /.snapshots are nested subvolumes reached by
  plain directory traversal, so updatedb indexes the entire system once
  per snapshot. On machines that accumulated snapshots this means
  multi-hour updatedb runs at full CPU, gigabytes of RAM, and a
  multi-gigabyte plocate.db (observed: 18 GB db, 7.5 h runs at 96% CPU
  with 592 snapshots; 43 MB and ~1 min after the fix).
- PRUNE_BIND_MOUNTS="yes" treats Btrfs subvolume mounts like /home as
  bind mounts, so locate finds nothing in home directories at all.

Configure updatedb.conf at install time and migrate existing installs,
then rebuild the index in the background. Both settings are matched
tolerantly and appended when absent, so a hand-edited updatedb.conf is
fixed rather than silently skipped.
2026-07-23 09:10:16 -04:00
husamemadH 4480a14236 Fix infinite loop in omarchy-mise-install wrappers
exec via `mise exec "$package"` so the tool resolves from mise's
install dir instead of by name through PATH. Previously `exec "$bin"`
could re-find the wrapper itself when ~/.local/bin precedes mise's
install path (e.g. when starship/zoxide clobber mise's PROMPT_COMMAND
hook, #3685), recursing forever. Scoping to $package keeps lazy install
and avoids leaking other runtimes into PATH.
2026-07-23 11:26:32 +03:00
Zain Syed e7fc47a44a Respect internal monitor config in clamshell recovery 2026-07-23 13:53:49 +05:30
David Heinemeier Hansson 1b6ab15331 Fix command detection in Hyprland Lua 2026-07-22 21:48:30 -07:00
David Heinemeier HanssonandClaude Fable 5 70f18e9a1c Keep notification QObjects out of the ListModels to stop shell crashes
Model rows stored the live Notification object in a ref role. When the
server destroyed the notification (sender close, DND untrack, dismiss)
the role became a dangling C++ pointer, and the next read segfaulted in
QQmlListModel::data — typically when replaying history over IPC. Track
live notifications in a JS map keyed by originalId instead, cleaned up
on close and untrack, where a stale reference degrades to a catchable
error instead of a crash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:39:15 -07:00
David Heinemeier Hansson fc6168af86 Load Voxtype hotkeys when installed 2026-07-22 21:39:15 -07:00
Scott JonesandClaude Opus 4.8 b1499e2950 Open Emacs after installing from the menu
Match the other editors (VSCode/Zed/Cursor) by launching once install
finishes. Uses gtk-launch emacsclient so it opens a frame on the
emacs.service daemon that omarchy-install-emacs just started, rather
than spawning a second standalone instance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 00:03:40 -04:00
Carlos Armando be71149500 fix(shell): scroll long tray menus 2026-07-22 18:49:18 -07:00
David Heinemeier Hansson e294d8278f Fix jsonc 2026-07-22 17:43:26 -07:00
David Heinemeier Hansson 9212939038 We dropped keywords 2026-07-22 17:32:47 -07:00
David Heinemeier HanssonandClaude Fable 5 d411c90a2a Restore Unlock screen selection to the Style menu
Bring back per-theme Plymouth unlock theming, dropped when walker +
elephant were removed. Selection now uses the same image carousel as
the theme and background switchers instead of a name list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:32:20 -07:00
David Heinemeier Hansson c544440469 Rename CLI benchmark command 2026-07-22 17:32:01 -07:00
David Heinemeier Hansson 568d7548e0 Included in the skill 2026-07-22 17:32:01 -07:00
David Heinemeier Hansson 35dd454d66 Document acceptance and visual verification 2026-07-22 17:31:14 -07:00
David Heinemeier Hansson 1de4c89030 Expand graphical acceptance coverage 2026-07-22 17:31:14 -07:00
David Heinemeier Hansson 83d20b07d1 Use current .NET runtime package 2026-07-22 17:31:14 -07:00
David Heinemeier Hansson 0daa397061 Remove redundant default command checks 2026-07-22 17:31:14 -07:00
David Heinemeier Hansson 7361b48461 Refresh shell test fixtures 2026-07-22 17:31:14 -07:00
David Heinemeier Hansson 43e95217f2 Reload weather location when opening panel 2026-07-22 17:31:14 -07:00
David Heinemeier Hansson 475211a5d4 Make Copy URL use native clipboard host 2026-07-22 17:25:28 -07:00
David Heinemeier HanssonandClaude Fable 5 6a047462e8 Decode launcher and tray icons at physical resolution
Quickshell's IconImage decodes at logical size, so on HiDPI displays
PNG icons were rendered from a texture at half the needed resolution
and looked blurry next to SVG icons. Use plain Image with sourceSize
scaled by Screen.devicePixelRatio, as the notification widgets
already do.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:02:52 -07:00
David Heinemeier HanssonandClaude Fable 5 4c38afeac3 Fetch high-res site icons for web apps instead of low-res favicons
Prefer the site's own apple-touch-icon (typically 180-512px) over
Google's favicon service, which often serves an upscaled 16-32px
favicon. Validate downloads are actual images so an HTML error page
never gets saved as an icon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:02:52 -07:00
David Heinemeier HanssonandClaude Fable 5 f12abbdebb Use sudo for terminal DNS changes and drop needless command checks
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 16:45:17 -07:00
David Heinemeier Hansson fedf12c43c Make transcoded MP4 audio compatible 2026-07-22 16:39:00 -07:00
David Heinemeier Hansson 60a550a646 Add --fit OSD option and use it for system shutdown/reboot/logout 2026-07-22 16:33:54 -07:00
David Heinemeier Hansson 36329cee7a Launch 1Password installer when missing 2026-07-22 16:10:45 -07:00
David Heinemeier Hansson 1e928d58db Remove menu keywords feature 2026-07-22 16:06:55 -07:00
David Heinemeier Hansson 40ede7d0c0 Dev is more dangerous than edge 2026-07-22 15:54:55 -07:00
David Heinemeier Hansson 3177f7fd2c Use Netflix/fast.com endpoints for network speed test 2026-07-22 15:52:58 -07:00
David Heinemeier Hansson c899172c05 Use 'yy instead of yyyy in vertical clock date mode 2026-07-22 15:37:25 -07:00
David Heinemeier Hansson 0fc1884281 Show monitor hero top and hide brightness slider when no backlight 2026-07-22 15:35:07 -07:00
1c30b83733 fix(clock): display date, week, and year vertically in vertical bar. (#6336)
* fix(clock): display date, week, and year vertically in vertical

* fix vertical date alignment in panel

* Update Clock.qml

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Address review feedback on Clock vertical format

Use the BarWidget-provided `vertical` property instead of repeating the
`bar && bar.vertical` ternary, and clean up stray tab/trailing whitespace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 15:31:39 -07:00
ssupt 3481b5614b Fix pointer navigation in Quattro launcher menus (#6343)
Accumulate sub-threshold movement so slow motion with a flat acceleration profile updates selection. Sample pointer state on row entry and carry pointer intent through subordinate menus while preserving predictable keyboard selection.
2026-07-22 15:27:56 -07:00
David Heinemeier HanssonandClaude Fable 5 4a02da20d5 Relaunch the shell through Hyprland so restarts match boot conditions
Spawning quickshell directly from omarchy-restart-shell leaked transient
environment variables from the calling terminal, SSH connection, or
development tool into the fresh shell. Dispatch the launch through
Hyprland instead so it inherits the canonical session environment, same
as autostart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 12:44:09 -07:00
David Heinemeier HanssonandClaude Fable 5 6ff61289fa Show Remove... as the title of all submenus under Remove
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 12:04:25 -07:00
David Heinemeier HanssonandClaude Fable 5 43260b29cd Drop Moonlight from the Remove menu since it ships in the base install
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 18:37:40 -07:00
David Heinemeier HanssonandClaude Fable 5 e747c385b0 Hide Remove menu entries for things that aren't installed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 18:37:40 -07:00
David Heinemeier HanssonandClaude Fable 5 5aa9e4173d Add in-guest acceptance test suite
Runs inside a live Omarchy session (typically a VM installed by
omarchy-iso-test) and verifies the desktop actually works: session
health, application launches by window class, and shell surfaces via
IPC — including typing into the launcher and launching the top hit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 18:36:13 -07:00
David Heinemeier HanssonandClaude Fable 5 c70ae3a405 Group bar position under Style > Menu Bar and add transparency toggle
omarchy bar transparent now accepts toggle, flipping the current value
in one jq update (absent counts as opaque, matching the shell).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 16:47:36 -07:00
David Heinemeier Hansson dc71875ada Better naming and ordering 2026-07-21 16:32:57 -07:00
David Heinemeier HanssonandClaude Fable 5 2bf4f4a9fb Hide menu submenus when none of their children are visible
Static submenus and links now inherit visibility from their descendants,
so hardware sections like Touchpad Haptics disappear on machines without
the hardware instead of opening empty. Laptop Display and Mirror Display
are guarded by omarchy-hw-laptop.

The guard evaluator brace-wraps every when/checked condition before
silencing it, so conditions in the jsonc no longer need their own
>/dev/null plumbing, and parents no longer need to repeat their
children's guards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 16:30:36 -07:00
David Heinemeier Hansson 9d3cb70ee7 More appropriate placement 2026-07-21 16:06:12 -07:00
David Heinemeier HanssonandClaude Fable 5 3107ec94e9 Resend nightlight temperature until it sticks on hyprsunset cold start
A freshly-started hyprsunset applies its default temperature at the end
of boot, silently overriding a set sent while it was still starting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 16:01:49 -07:00
David Heinemeier Hansson 15fbc08ec0 Match indicator language 2026-07-21 15:59:40 -07:00
David Heinemeier Hansson d3cce6e68f Make menu left arrow navigate back 2026-07-21 15:45:08 -07:00
David Heinemeier Hansson 0d3cb4bf48 More appropriate placement 2026-07-21 15:41:37 -07:00
David Heinemeier HanssonandClaude Fable 5 afcbcc0e36 Replace shell instances with quickshell's synchronous kill
With quickshell-git, qs kill blocks until the instance has fully
exited, so restarting no longer needs to resolve pids and wait for them
to die. Kill in a loop until none remain - each call takes the oldest,
and duplicates from stale sessions are the reason this script exists.
The timeout bounds a wedged shell that can't process the quit message:
its kill would otherwise report success after Qt's 30s wait gives up,
re-killing the same undead instance forever. The test mock now kills
real processes synchronously to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:27:12 -07:00
David Heinemeier HanssonandClaude Fable 5 21eacb324a Ship quickshell as quickshell-git for synchronous kill
quickshell 0.3.0's qs kill returns before the instance exits, forcing
restart tooling to babysit pids. Upstream fixed kill to block until the
process is gone, but only on master, so the Omarchy repo now carries
quickshell-git pinned past that fix. New installs get it from the base
packages; existing installs migrate with a single --ask 4 transaction so
the conflicting quickshell package is replaced in place, then restart
the shell after the update to pick up the new binary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:27:12 -07:00
David Heinemeier HanssonandClaude Fable 5 094619fbd5 Target the newest shell instance for IPC calls
qs parses -n before the ipc subcommand as its top-level --no-duplicate
launch flag and silently ignores it, so IPC still went to the oldest
matching instance - exactly the stale one left behind around a restart.
The --newest flag belongs to the ipc subcommand: qs ipc -n. Verified
against two live instances; the test now pins the flag position.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:27:00 -07:00
David Heinemeier HanssonandClaude Fable 5 289e6d12fc Simplify service lookups with optional chaining
The bar-null-shell-null-typeof-function ternary guarded against our own
shell missing a method it always defines. bar?.shell?.firstPartyServiceFor()
handles the only real case, delayed bar injection, in one line. The
typeof checks that probe genuinely third-party plugin objects stay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:26:30 -07:00
David Heinemeier HanssonandClaude Fable 5 ba91bad3a4 Move Night Light onto a first-party nightlight service
Same treatment as Stay Awake: the indicator polled the toggle CLI over
a Process with a timer to paper over the race after clicking, and the
CLI ended by asking the shell to refresh every indicator over IPC. A new
omarchy.nightlight service owns hyprsunset instead - it probes the
temperature on startup, applies changes itself for in-shell toggles, and
answers on the nightlight IPC target. The indicator becomes a plain
binding. The CLI still drives hyprctl directly so keybindings, the menu,
and ssh work without the shell, but now just nudges the service to
re-probe since hyprsunset has no state file to watch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:26:20 -07:00
David Heinemeier HanssonandClaude Fable 5 61b7cd1c12 Size the indicator tray from block implicit sizes
Deriving the tray's implicit size from childrenRect fed layout results
back into the bindings that produced them, tripping implicitWidth
binding loop warnings. Compute it from the active and inactive blocks'
own implicit sizes instead, and center the loaded blocks rather than
anchor-filling containers that are sized by their content. The contract
test now instantiates the tray to check the collapse/expand cycle and
fails on any implicitWidth binding loop in the log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:26:10 -07:00
David Heinemeier HanssonandClaude Fable 5 bee9ab476c Bind the Stay Awake indicator to the idle service
The indicator polled omarchy-toggle-idle over a Process and re-ran it on
a timer to toggle, while the CLI called back into the shell over IPC to
apply and refresh the state it had just changed. Now the indicator binds
straight to the idle service's stayAwake property and flips it in
process. The CLI only touches the state file, which the service already
watches, so toggling from keybindings and scripts still reaches the
shell without any reentrant IPC.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:25:49 -07:00
David Heinemeier Hansson 51d6d3c291 Fix Voxtype invitation action 2026-07-21 11:22:51 -07:00
Hugo Sibony 840832cfde Connect to enterprise (802.1X) Wi-Fi networks from the network panel 2026-07-21 22:40:18 +09:00
David Heinemeier HanssonandClaude Fable 5 0526ebefcc Snap monitor scaling up to the nearest clean scale Hyprland accepts
Hyprland only takes scales where the mode divides into whole logical
pixels (in 1/120 steps), so picking 3x on a 1280x800 QEMU display threw
a red error overlay and silently kept the old scale. Round the request
up to the nearest clean divisor instead, so 3x becomes 3.2x.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 21:52:32 -07:00
David Heinemeier HanssonandClaude Fable 5 503388b4d6 Retune opacity for Hyprland 0.56's corrected alpha premultiplication
Hyprland 0.56 fixed premultiplication in the color-management shader
(hyprwm/Hyprland#14403), so the same opacity values now render visibly
more transparent than on 0.55. Raise each value to its perceptual
equivalent (a^(1/2.2)) to restore the previous look.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 21:22:16 -07:00
David Heinemeier HanssonandClaude Fable 5 f4a7b30366 Retune opacity for Hyprland 0.56's corrected alpha premultiplication
Hyprland 0.56 fixed premultiplication in the color-management shader
(hyprwm/Hyprland#14403), so the same opacity values now render visibly
more transparent than on 0.55. Raise each value to its perceptual
equivalent (a^(1/2.2)) to restore the previous look.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 21:15:47 -07:00
David Heinemeier HanssonandClaude Fable 5 994e4dfa7d Parse plain hyprctl binds output to survive Hyprland 0.56's broken JSON
Hyprland 0.56.0 emits invalid JSON from `hyprctl -j binds`: the new
allow_input_capture field was added to the format template at slot 16
but its argument at position 8, shifting every value from
has_description onward and landing unquoted strings in numeric slots.
The jq parse failure emptied the keybindings menu down to the two
static web-app entries — and the empty result got cached under a
stable key, permanently.

Parse the plain-format output instead, which is unaffected (and also
immune to the older quotes-in-args JSON breakage), refuse to cache a
record set with zero dynamic binds, and bump the cache version so
poisoned caches invalidate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 20:42:37 -07:00
David Heinemeier Hansson 79e9f38c0c Go with a consistent Reboot name 2026-07-20 19:10:27 -07:00
David Heinemeier HanssonandClaude Fable 5 c99c723b67 Show OSD feedback when rebooting or shutting down
Adds named reboot and shutdown OSD icons, and gives the touchscreen
toggle a proper gesture-tap glyph instead of the restart arrow it
was accidentally using.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 18:35:41 -07:00
David Heinemeier HanssonandClaude Fable 5 bf15e896b9 Pin power action timers to 100ms accuracy
Transient systemd timers default to AccuracySec=1min, so the 2s
reboot/shutdown could fire up to a minute late.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 18:35:28 -07:00
David Heinemeier HanssonandClaude Fable 5 ab0f6a8f2c Fast-poll Tailscale and Dropbox at shell startup so icons go live promptly
Both services fire one status poll when the shell starts, but after a fresh
boot that lands before tailscaled has connected or dropboxd has finished its
respawn dance, leaving the bar icons stale for a full 30s/60s refresh
interval. Re-poll every 2s until the service shows up (or ~30s pass), then
hand off to the normal periodic refresh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 18:35:19 -07:00
David Heinemeier HanssonandClaude Opus 4.8 0f691d60fb Scale the Dell XPS 13 default via display text size
Use omarchy-display-text-size 11 instead of a lone GTK text-scaling-factor,
so the XPS 13 nudge moves shell, GTK, and terminal text together.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:08:17 -07:00
David Heinemeier HanssonandClaude Opus 4.8 ab49ec3787 Add a notched text-size slider to the display panel
A macOS-style slider snapping to curated stops (9-20px), applied via
omarchy-display-text-size. Suppresses hover-driven focus changes while a
size change reflows the panel, so h/l on the slider can't let a shifted
row steal keyboard focus.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:08:17 -07:00
David Heinemeier HanssonandClaude Opus 4.8 84ad03b8d2 Add omarchy display text size to scale text everywhere
One knob for apparent text size, anchored to the 12px shell default: it sets
the shell font base-size (~/.config/omarchy/shell.toml), GNOME/GTK's
text-scaling-factor (12px -> 1.0), and the terminal font point size
(12px -> 9pt) in lockstep. Accepts 9-20px; reset returns all three to
default. Terminals live-reload where they can (alacritty/kitty/ghostty);
foot gets a single restart nudge that replaces itself instead of stacking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:08:17 -07:00
David Heinemeier HanssonandClaude Opus 4.8 c681396a47 Let PanelSlider draw macOS-style notches
Optional tickCount cuts evenly-spaced marks into the track for stepped
sliders. Defaults to 0, so existing sliders are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:08:17 -07:00
David Heinemeier HanssonandClaude Opus 4.8 25fb9cf916 Layer a machine-level shell.toml override on top of the theme
Read ~/.config/omarchy/shell.toml as a user override merged over the active
theme's shell values, so a personal font/spacing/bar tweak survives theme
switches. The file is watched, so edits re-flow the shell live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:08:17 -07:00
David Heinemeier Hansson cc9a3aadb3 Report live battery power draw 2026-07-20 16:37:33 -07:00
David Heinemeier Hansson af513d3206 Preserve power panel keyboard selection 2026-07-20 16:37:33 -07:00
Scott JonesandClaude Opus 4.8 15318d261f Install Emacs via the omarchy-emacs AUR package
Point the Emacs entry in the install-editor menu at a dedicated
omarchy-install-editor-emacs script, matching the pattern already used
for Helix, VSCode, and Zed. The script installs the omarchy-emacs AUR
package and runs its installer, which pulls in emacs-wayland, applies
the theme/font integration, and enables the user emacs.service.

This is the AUR-packaged approach dhh suggested on #4807 (superseding
the in-tree integration proposed there), now that the package exists and
is maintained on the AUR. End-user behavior is unchanged except that a
freshly installed Emacs now tracks the active Omarchy theme and font out
of the box.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 19:37:11 -04:00
David Heinemeier HanssonandClaude Opus 4.8 228b5ac3ba Give every hero toggle a keyboard focus ring
Each panel's hero on/off toggle (Tailscale, Dropbox Wi-Fi, Bluetooth,
network, audio mute) now has a reachable "header" cursor section, a focus
ring around the hero icon, Enter/Space activation, a letter shortcut, and
hover parity. The hero is inset by heroRingPad so the ring stays inside
the Flickable/ScrollView clip box instead of being cut off.

Bluetooth and audio get a virtual "header" section above their device
sections, so the adapter/mute can be toggled by keyboard even when no
device rows exist. Network repurposes its previously dead header section.

For the PanelHero-based panels (Tailscale, Dropbox), route panel state
through the wrapper's `header` id: inside a PanelHero iconComponent `root`
resolves to PanelHero, not the Panel, so `root.headerHasCursor` and
`root.setHeaderCursor()` silently referenced the wrong object.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 15:50:05 -07:00
David Heinemeier HanssonandClaude Fable 5 835181f112 Keep nudging Bluetooth discovery on while the panel is open
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 14:32:23 -07:00
David Heinemeier HanssonandClaude Fable 5 000fa2a972 Let the Tailscale panel toggle on and off like Dropbox
The hero icon is now the optimistic on/off toggle with a tooltip,
replacing the connect block and the progress lines beneath the hero.
The disconnected cross is drawn grey rather than red, and action
errors expire after a couple of seconds instead of sticking forever.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 14:26:19 -07:00
David Heinemeier HanssonandClaude Opus 4.8 a7c0d0f6a1 Let the Dropbox panel pause and resume syncing
Click the Dropbox hero icon to pause or resume the daemon (dropbox-cli
stop/start), with a tooltip on hover. When paused, both the hero and the
bar icon grey out and the phrase reads "Syncing paused". The greying is
optimistic so it reacts the instant you click, then reconciles with the
real daemon state once it settles (reverting if the command failed).

Also fix status.py's running detection, which was stuck reporting true:
this dropbox-cli inverts the `running` exit code and reports "isn't
running" (not "not running") when stopped, so the paused state never
showed. Detection now keys off the status text alone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 14:00:50 -07:00
David Heinemeier HanssonandClaude Fable 5 ff1d1c325e Suppress Hyprland reload errors for the whole quattro upgrade
Suppression was enabled only after cleanup_legacy_user_paths had already
moved ~/.local/share/omarchy aside, so the live session could auto-reload
during the swap window and pop "source= globbing error: found no match"
for every legacy hyprland.conf source line. Enable suppression as the
first step of the upgrade sequence instead; the EXIT trap still restores
it if the upgrade dies early.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 11:45:23 -07:00
David Heinemeier HanssonandClaude Fable 5 f96827c909 Simplify the quattro upgrade script
- Extract run_as_user_omarchy and run_as_user_wayland_session helpers to
  replace ten hand-rolled run_as_user env blocks
- Declare the retired user-unit list once instead of three times
- Use git ls-files --others to find user-added theme backgrounds instead
  of diffing against the tracked list by hand
- Stop rewriting the SDDM 10-theme/10-wayland confs the omarchy package
  already installs, and drop the duplicate [Theme] section from
  99-omarchy-login.conf
- Drop the legacy-path rewrite in the theme hyprland.conf shim; the user
  transition already rewrote those references
- Remove dead code: the unreachable channel re-validation, the declare -F
  guard in cleanup_on_exit, and the unset block at the end of the
  user-transition script

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 11:44:50 -07:00
David Heinemeier HanssonandClaude Fable 5 88d60e1468 Point gum prompts at the terminal in the quattro upgrade
The user-transition script is fed to bash on stdin, so the browser-close
gum confirm read the script itself: it auto-declined and swallowed the
remaining script text, leaving bash to fail on a mid-construct syntax
error. The outer confirm and reboot prompts had the same exposure when
the script runs via curl | bash. Read all three from /dev/tty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 11:39:50 -07:00
David Heinemeier HanssonandClaude Fable 5 4f1b1fceb8 Pass the --yes flag into the quattro user transition
The Copy URL shortcut repair checked the outer script's yes variable,
which never reached the user-transition heredoc's bash process, so the
upgrade died with "yes: unbound variable" under set -u whenever a
browser was running with the old extension id in its Preferences.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 11:26:33 -07:00
David Heinemeier Hansson 8719b0d031 Fix pending migration notifications 2026-07-20 11:07:44 -07:00
David Heinemeier HanssonandClaude Fable 5 5232505c12 Don't block the desktop on network-online at boot
graphical.target was gated on network-online.target: cups-browsed orders
itself After=network-online.target and sits in multi-user.target, so the
session waited for NetworkManager-wait-online (DHCP/Wi-Fi association)
before coming up. On the bench VM that was ~140ms, but it's the classic
multi-second boot stall on real Wi-Fi.

Mask NetworkManager-wait-online.service so network-online.target is
reached immediately and nothing in the boot waits for connectivity;
NetworkManager still comes up and cups-browsed browses once the link is
live. Mirrors the existing systemd-networkd-wait-online mask. Enabled at
install (enable-services.sh) and masked on existing installs via
migration.

Verified on a fresh boot: the graphical.target critical chain no longer
passes through network-online.target / NetworkManager-wait-online.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:34:26 -07:00
David Heinemeier HanssonandClaude Opus 4.8 86fd228cda Refresh gum theming when launching presentation terminals
gum_env.lua is fed into the compositor environment via hl.env at login,
but that environment is captured once and is not refreshed on a theme
switch. Terminals launched through uwsm-app inherit the caller's
environment (scopes don't read systemctl --user set-environment), so a
floating terminal would show the login-time theme's gum colors.

Extract the export logic into a sourced omarchy-restart-gum helper and
source it from the presentation terminal launcher so gum widgets match
the active theme.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:44:48 -07:00
David Heinemeier HanssonandClaude Fable 5 e85cde2c9c Don't autostart the print-queue applet
system-config-printer ships /etc/xdg/autostart/print-applet.desktop, so
installing it for the printer GUI also starts a ~24 MiB Python tray applet
in every session whose only job is print-job notifications. Override it
with a Hidden=true entry in config/autostart (same mechanism already used
for fcitx5 and limine-snapper-notify), so the applet no longer autostarts.
Printing and the system-config-printer GUI are unaffected.

Verified: with the override in place, systemd's xdg-autostart generator no
longer produces app-print\x2dapplet@autostart.service.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 09:19:35 -07:00
David Heinemeier HanssonandClaude Fable 5 9314c12718 Test that the clamshell poll is gated to docked laptops
Guards the dock-gating in omarchy-hyprland-monitor-watch: the poll runs
only when omarchy-hw-laptop and an external monitor are both present, via
sync_poll_state, with the event loop fed by process substitution so it
can start/stop the poll. Behaviourally simulated (stubbed hw checks +
synthetic monitor events): the poll runs only while docked and stops on
undock; a desktop or undocked laptop never polls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 09:03:26 -07:00
David Heinemeier HanssonandClaude Fable 5 f1237e1962 Drive media play-order off MPRIS signals instead of polling
The media service ran a 500ms repeat timer calling syncPlayingOrder() for
the whole session — two CPU wakeups a second even with nothing playing.
syncPlayingOrder only depends on the set of MPRIS players and each
player's isPlaying state, both of which the Mpris service already signals.

Replace the timer with onPlayersChanged (players appearing/disappearing)
plus an Instantiator that connects isPlayingChanged for each live player,
and a Component.onCompleted for the initial sync.

Verified with a test MPRIS player: play/pause and player add/remove all
update the active player with no periodic timer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:33:00 -07:00
David Heinemeier HanssonandClaude Fable 5 d548b730e0 Only run the clamshell poll while docked, not for every laptop
The reconciliation poll (added in "Fix clamshell display recovery") is a
backstop for lid transitions the Hyprland switch binds can miss across
suspend/resume. But the internal panel is only ever disabled while the
laptop is docked — lid shut with an external monitor active — so the poll
has nothing to reconcile when undocked. Running it 24/7 on every laptop
wakes the CPU every 2s for no reason, which is exactly the case where
battery matters most.

Start the poll when an external monitor is present and stop it when the
last one goes away (driven by the socat monitor-add/remove watch), gated
on omarchy-hw-laptop. Undocked laptops and desktops now never poll; a
docked laptop keeps the 2s recovery poll. Switch the event reader from a
pipe to process substitution so it runs in the main shell and can manage
the poll's lifetime.

Desktop verified: no poll runs even with external monitors attached. The
docked-laptop start/stop path needs a check on real laptop hardware.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:15:09 -07:00
David Heinemeier HanssonandClaude Fable 5 63f147ef85 Only run the clamshell poll on laptops
omarchy-hyprland-monitor-watch ran poll_clamshell_state() every 2s for
the whole session, and each pass forks omarchy-hyprland-monitor-clamshell
plus its hyprctl/jq/monitor-* children. Clamshell (lid) handling can only
ever apply to a machine with a lid, so on desktops and VMs this was pure
churn — the single largest remaining source of idle process wakeups
(~3 forks/sec on an otherwise idle desktop).

Gate the poll behind a new omarchy-hw-laptop helper (lid button, or a
laptop DMI chassis type as a fallback). The event-driven socat watch on
Hyprland monitor add/remove is unchanged, so monitor hotplug still
reconciles everywhere; only the periodic lid poll is now laptop-only.

Verified on a desktop: steady-state omarchy-hyprland-* forks drop from
~3/sec to zero, socat watch still present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:09:07 -07:00
David Heinemeier HanssonandClaude Fable 5 ad023413c6 Derive the network bar pill from the native Networking service
The pill was populated by polling omarchy-network-status (previously
every 3s, then 10s). Derive kind/signalStrength directly from the
Quickshell.Networking service instead: wired-connected → ethernet
(preferred, matching the default-route device), else a connected Wi-Fi
network → wifi with its signal, else disconnected. The service's
properties are change-notified, so the pill updates on connect/
disconnect and signal changes with no polling and no subprocess.

Removes the last periodic poll from the bar; the label/frequency pill
fields were write-only and are dropped.

Verified live: pill tracks ethernet → disconnected → ethernet in real
time as the wired device goes down and up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 07:56:43 -07:00
husamemadH b956069465 Prefer Noto Naskh Arabic over Nastaliq Urdu for Arabic text (#6322) 2026-07-20 07:41:05 -07:00
David Heinemeier HanssonandClaude Fable 5 a64d895a02 Spawn shell subprocesses with bash -c instead of bash -lc
Each `bash -lc` starts a login shell that re-sources the profile
(mise activation, /etc/profile.d) on every invocation — ~16 forks per
call versus ~2 for `bash -c` — which taxes every menu/panel/launcher
action the shell shells out for. The session already exports PATH and
env to the shell, so omarchy commands resolve fine under `bash -c`.

Switch the internal/omarchy-owned spawns (theme+background switches,
brightness, monitor scaling, DNS, lock/fingerprint, keyboard-layout
probe, voxtype status, and the `:`/printf state-file writes) to
`bash -c`. Leave `bash -lc` on the sites that run user-configurable
commands (custom bar-widget exec, menu provider/guard scripts,
launcher scan commands, configurable idle/screensaver command), where
a user's command may rely on their login environment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 22:46:27 -07:00
David Heinemeier HanssonandClaude Fable 5 4d630c9f30 Stop polling monitor/network state on an idle desktop
The monitor panel polled `omarchy-monitor-state` every 5s (26 forks per
call) and the network panel polled `omarchy-network-status` every 3s,
both with `running: true` rather than gating like the sibling panels.

Monitor: gate the poll on `opened` (brightness/scale are only shown in
the open panel, which already refreshes on open) and drive the bar glyph
from Quickshell.screens instead of the polled display list, so monitor
count stays correct without polling.

Network: the bar pill needs live-ish state, so keep polling but at 10s
instead of 3s. (Deriving the pill from the native Networking service
would remove the poll entirely; left as a follow-up since it needs Wi-Fi
state testing.)

Idle fork rate drops ~20/s to ~14/s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 22:46:27 -07:00
David Heinemeier HanssonandClaude Fable 5 f619c397f2 Make status indicators event-driven instead of polling
The indicators widget broadcast a refresh every 2s, fanning out to four
status subprocesses (nightlight, idle, reminder, screen-recording), and
NightLight/StayAwake each ran an additional 5s poll. On an idle desktop
this was the dominant source of process churn (~33 of ~53 forks/sec in a
VM). The state-changing commands already push `omarchy.indicators
refresh` over IPC, so the polling was redundant.

Drop the 2s broadcast and the two 5s timers; indicators now refresh at
startup and on the IPC push. Also fix three callers that pushed to the
wrong target `Indicators` instead of `omarchy.indicators` (screen
recording, notification silencing, and the omarchy-shell help example) —
those pushes silently failed and only appeared to work because the poll
masked them.

Idle fork rate drops ~53/s to ~20/s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 22:46:27 -07:00
David Heinemeier HanssonandClaude Fable 5 ca4b0b3b61 Stop polling the background symlink every 100ms
The background plugin ran a 100ms Timer that spawned a readlink
subprocess ~10x/sec for the life of the shell, purely to notice
wallpaper changes. Wallpaper updates already arrive over IPC
(omarchy-theme-bg-set calls `background set`, omarchy-theme-set calls
`background themeTransition`), and the initial wallpaper is read once
in Component.onCompleted, so the poll was redundant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 22:46:27 -07:00
David Heinemeier HanssonandClaude Fable 5 b87de89372 Stop the migration notifier from busy-looping the session
omarchy-update-user-notify.path used PathExistsGlob= on the packaged
migrations directory. That directive is level-triggered: systemd re-checks
it every time the triggered unit deactivates and fires again while the glob
still matches. Since applied migrations stay on disk forever (state lives in
~/.local/state/omarchy/migrations), the glob always matches, so the oneshot
notifier re-triggered itself in a tight loop (~26-66 starts/sec) — burning
about a core and flooding the journal for the whole session.

The loop existed since the unit was introduced, but the default start-rate
limit killed it after 5 iterations, taking the .path unit down with
'unit-start-limit-hit'. That symptom was reported as #6174 and fixed
yesterday by setting StartLimitIntervalSec=0 — which removed the only brake
and turned the capped hiccup into an unbounded busy-loop.

Fix the actual cause instead:

* Drop PathExistsGlob= from the .path unit, keeping the edge-triggered
  PathModified= watch for updates that land mid-session.
* Revert the StartLimitIntervalSec=0 override; with the level trigger gone
  there is no self-re-fire to trip the limit, and the default limit is a
  useful backstop again.
* Preserve the once-per-login pending check the glob used to provide by
  giving the service its own WantedBy=graphical-session.target, enabled at
  first-run alongside the other user units.
* Add a migration that daemon-reloads, revives a rate-limit-killed .path,
  restarts the watcher, and enables the login-time notifier on existing
  installs.

Verified with transient path/service units: the old config runs the service
200 times in 3 seconds; the new config runs it zero times while idle and
exactly once when a new migration file lands.

Thanks to @HANCORE-Linux for finding and diagnosing the problem.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 21:49:41 -07:00
David Heinemeier HanssonandClaude Fable 5 844859a000 Always overwrite unowned strays under /usr/share/omarchy on update
The tree is wholly package-owned, but files can land there unowned
(in-place extension work, script-written files), and pacman then aborts
the entire upgrade on the file conflict — as happened when copy-url's
service worker was renamed to background-2.js. The conflict check runs
before any hooks or scriptlets, so the package itself can't recover;
the update command has to allow the overwrite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 21:31:10 -07:00
David Heinemeier HanssonandClaude Fable 5 f3878b341c Share a single power profile parser between list and set
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 21:29:15 -07:00
David Heinemeier HanssonandClaude Fable 5 d211de2ffc Match Wi-Fi first-run notification format to the other invitations
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 21:17:04 -07:00
David Heinemeier HanssonandClaude Fable 5 e96163c075 Give glyph notifications with bodies the large icon slot
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 21:17:04 -07:00
Ty SmithandClaude Fable 5 8147489f1d Allow toggling Caps Lock via both Shift keys
Adds shift:both_capslock alongside compose:caps so pressing both Shift
keys together toggles real Caps Lock. Caps Lock alone remains the
Compose key, and single-Shift behavior is unchanged. Provides an escape
hatch when Caps Lock gets stuck on (e.g. after an fcitx5 desync).

Closes #5981

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 20:05:39 -07:00
Othon Ferreira ca0d78e718 Fix documentation example
Fixing to screenrecording instead screenrecord that works only when
using the alias
2026-07-19 20:03:52 -07:00
David Heinemeier HanssonandClaude Fable 5 54d79d9d16 Fix screen flash when waking from sleep into the unlock screen
The lock service arms a 5s blank timer whenever the screen locks, and
input at the lock screen re-arms it. Closing the lid sprays pointer
noise over the lock surface, so the timer was routinely armed right
before suspend, froze mid-countdown, and fired moments after resume --
blanking the freshly woken unlock screen under the user.

Guard the timer with a wall-clock check: if far more time elapsed than
the interval, the countdown slept through a suspend, so take a fresh
run-up instead of blanking. This also blanks the lock screen 5s after
an untouched resume.

Two accomplices made the flash worse and hid the real bug:

- The clamshell watcher's 2s poll fired an unconditional global DPMS
  enable whenever no external monitor was active, relighting any blank
  within 2 seconds (lock-screen blanking never stuck on undocked
  laptops) and racing the resume modeset. Recovery now only wakes
  displays when it actually re-enables one.

- Every keystroke at the lock screen dispatched a redundant DPMS
  enable via omarchy-system-wake, forcing extra modesets in the
  fragile just-resumed DRM state. Brightness "on" now skips the
  dispatch when every active display is already lit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:42:18 -07:00
David Heinemeier Hansson 324fd387c0 Convert two more themes that are a good fit to be generic 2026-07-19 19:07:07 -07:00
David Heinemeier HanssonandClaude Fable 5 4cbe47c14d Generate ristretto neovim/vscode configs from the colorset too
The colorset is the Monokai Pro Ristretto palette, so the generated
themes keep all the accent colors. Brightened muted to Monokai Pro's
actual comment color, since comments sit on that key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:07:07 -07:00
David Heinemeier HanssonandClaude Fable 5 cc4b92f367 Match Brave Origin refresh on binary path to avoid installer self-match
The stable Brave Origin's process is named plain "brave" (the wrapper
execs /opt/brave-origin-bin/brave), so pgrep -x brave-origin never
matches, and pgrep -f brave-origin also matches the installer's own
floating terminal - which made omarchy-install-browser launch a
headless browser and hang on first install.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:05:42 -07:00
Abishek NeupaneandClaude Fable 5 c50273e93a Switch Brave Origin from the beta to the stable release (#6021)
Brave now publishes brave-origin-bin on AUR, so use it instead of
brave-origin-beta-bin. The stable wrapper also parses flags files
correctly, so Brave Origin can use the normal chromium flags like
the other Chromium-based browsers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:05:42 -07:00
David Heinemeier HanssonandClaude Fable 5 81217e5749 Generate vantablack neovim/vscode/btop configs from the colorset too
Tuned muted, dark_foreground, and lighter_background to the tiers the
explicit themes used, which also fixes muted falling back to pure white
in the generated terminal configs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 18:43:04 -07:00
David Heinemeier Hansson 3f2f2dd245 Can now generate these configs just from the expanded colorset 2026-07-19 18:37:25 -07:00
c442ad7d93 Make YouTube and Zoom webapps opaque on any browser and profile
Ports #6092 from master: the video-app opacity opt-out only matched
Chrome/Chromium on the default profile, so Brave, Edge, and non-default
profiles kept the translucent default-opacity styling.

Co-Authored-By: Zeus-Deus <github.commits@widow.cc>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 18:28:07 -07:00
a1e0875eb4 Pin browser password store to gnome-libsecret (#6065)
Chromium-based browsers auto-detect their os_crypt backend at launch. On
Hyprland the xdg-desktop-portal Secret backend has no provider, so the
autodetect can fall back to the 'basic' (v10) store, making previously
v11-encrypted cookies and saved passwords undecryptable — the user is
silently logged out of everything. Pin gnome-libsecret (the stock
omarchy keyring) so the backend is deterministic, matching what we
already do for VSCode.

Migration covers chromium, brave, chrome, and edge flags confs for
existing installs.

Co-authored-by: Niklas Tscheppe <ntscheppe@drgt.net>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 18:07:02 -07:00
7721228319 shell: handle CTRL+BACKSPACE to clear filter (#6307)
* emoji panel: handle CTRL+BACKSPACE to clear filter

* shell: handle CTRL+BACKSPACE to clear filter in all search overlays

* Indentation fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* menu filters: align keyboard editing with Qt standard shortcuts

* menu filters: extract shared filter-editing helpers

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* potential fix for pull request findings

* Only treat filter edit keys as edits when they change the text

Backspace/Ctrl+U on an empty filter no longer calls setFilter(""),
which was resetting the list selection back to the top. This also lets
the menu's empty-filter Backspace fall through to goBack() with any
modifier held, as it did before the Util extraction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:49:16 -07:00
David Heinemeier HanssonandClaude Fable 5 ddab62ea0d Only brace the expansions zsh misparses
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:48:17 -07:00
Paul Wagner 26bca7d834 Fix fip function on zsh 2026-07-19 17:48:17 -07:00
David Heinemeier Hansson d4d1b518e0 Remove redundant Hyprland launch wrapper
With initial workspace tracking disabled, windows naturally open on the active workspace. Remove the explicit Hyprland workspace dispatch and let shell actions, shell restarts, and presentation terminals launch directly.
2026-07-19 17:36:34 -07:00
David Heinemeier Hansson 146f73bf85 Don't need to have this much explaining 2026-07-19 17:28:00 -07:00
Scott JonesandClaude Opus 4.8 498fa284f5 looknfeel: open new windows on the active workspace (fix portal file dialogs) (#6317)
Set misc:initial_workspace_tracking = 0 so new windows appear on the currently
active workspace rather than the one their process was first launched on.

Without this, file-picker dialogs drawn by xdg-desktop-portal-gtk (browser
Open/Save, GTK app choosers, etc.) always open on the workspace where the portal
process started at login (typically workspace 1) instead of following the app
that requested them. So a Save dialog triggered from a browser on workspace 2
pops up on workspace 1, out of view.

Trade-off: an app launched on one workspace that switches away before its window
maps will now open on the new active workspace. In practice the dialog-follows-app
behavior is the more intuitive default.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:27:36 -07:00
David Heinemeier HanssonandClaude Fable 5 12dc7ae558 Shrink lock screen password dots to fit the field
Long passwords used to overflow the input field and clip with no
feedback that typing was still registering. Scale the dot size and
letter spacing down as the password grows, like macOS, so every
keystroke stays visible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:03:00 -07:00
David Heinemeier HanssonandJon Kinney ec9f8241c1 Fix power action terminal scope race
Co-authored-by: Jon Kinney <jonkinney@gmail.com>
2026-07-19 10:31:27 -07:00
1feb0250b4 Update Omarchy skill for Quattro's Lua configuration
The skill still described pre-Quattro .conf files and workflows, sending
agents to inactive config files and stale commands. Now covers the Lua
config layout, o.bind/hl.unbind/hl.monitor forms, .d hook directories
with omarchy hook install, plugin cloning, and corrected command routes.

Distilled from #6310.

Co-Authored-By: ssupt <149246609+ssupt@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 10:20:52 -07:00
David Heinemeier HanssonandClaude Fable 5 17c8af3f14 Simplify shell restart around quickshell instance management
omarchy-restart-shell now kills by config dir via quickshell kill and
relaunches with --no-duplicate, polling kill until every instance is
gone since kill returns without waiting and -n silently exits if one
remains. The locked-session refusal reads Hyprland monitor state alone,
which covers a hung shell holding the lock; the redundant shell IPC
probe, availability guards, and omarchy-shell path indirection are gone.

Shell IPC calls now time out (2s default, OMARCHY_SHELL_IPC_TIMEOUT to
override) so probing an unresponsive shell fails fast instead of
hanging the caller.

omarchy-hyprland-launch checks dispatch output for "ok" rather than the
exit code, which is 0 even on Lua errors, so a failed dispatch falls
through to the bash -lc fallback instead of silently launching nothing.

The Quattro upgrade cutover delegates to omarchy-restart-shell instead
of carrying its own copy of the lock check, pkill, and readiness loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 10:09:54 -07:00
David Heinemeier HanssonandClaude Fable 5 3edf254a12 Translate tmux key notation back into readable hotkeys
Restore the key translation the awk parser used to do (C-M-S-Left →
CTRL + ALT + SHIFT + LEFT, PPage → PAGE UP, etc) as a display pass over
the tmux list-keys output, along with the PREFIX header line and the
COPY MODE table label.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 10:00:46 -07:00
David Heinemeier Hansson 008f3a2212 Fix Kitty terminal cwd detection 2026-07-19 09:59:13 -07:00
3754c2c031 Use tmux to list keybindings (#6308)
* Use tmux to list keybindings

* Annotate tmux keybindings

* List unannotated bindings too and let the isolated server exit on its own

list-keys -aN falls back to the raw command when a binding has no note,
so user-added bindings in an existing tmux.conf still show up. Dropping
exit-empty off means the throwaway server exits when the client
disconnects instead of relying solely on the trap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 09:50:32 -07:00
David Heinemeier HanssonandClaude Fable 5 0f2f3f11cb Remember power profile choices per power source across reboots
Explicit profile selections from the power panel or menu are now saved
under an ac/battery key in ~/.local/state/omarchy/powerprofiles and
reapplied on boot and on plug/unplug. Only successful, user-made
selections are persisted, so the performance/balanced defaults still
apply when nothing has been chosen.

All entry points key off the same power signal, UPower's OnBattery:
the shell reads it natively and omarchy-powerprofiles-set autodetect
queries it via busctl. This replaces the sysfs online checks, which
disagreed with the shell on desktops without power_supply entries and
lagged behind plug events on some USB-C laptops.

Plug/unplug switching moves from the udev rule into the shell's battery
service, which already receives UPower's debounced state changes, so
the udev rule and its settle-sleep workaround are gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 09:33:23 -07:00
David Heinemeier Hansson 6ba0e9c234 Fix monitor scaling down getting stuck at 3x due to floating point drift
Hyprland can report a scale set to 3 as something like 3.0000000000000004.
The previous directional comparison treated that as above the 3 preset, so
Super+Alt+Slash (scale down) would stay at 3 instead of reaching 2.

Snap the reported scale to the nearest preset first, then step up/down.
Add a regression test for the floating-point case.
2026-07-19 09:10:14 -07:00
David Heinemeier HanssonandClaude Fable 5 c030337cb6 Only bundle vconsole.conf in the initramfs for Latin keyboard layouts
Bundling vconsole.conf makes Plymouth apply the user's layout at the LUKS
prompt, but for layouts whose letter keys aren't Latin (Hebrew, Greek,
Cyrillic, Arabic, ...) that makes the necessarily-Latin passphrase
untypeable and locks the user out of their machine.

mkinitcpio sources its conf.d drop-ins as shell, so omarchy_hooks.conf now
checks XKBLAYOUT at every rebuild and only bundles the file for Latin
layouts; non-Latin layouts keep the US fallback the passphrase was set
under. A migration strips the unconditional line and rebuilds the UKI on
affected installs.

Closes #6229.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 09:04:09 -07:00
David Heinemeier Hansson 7bfb8cf954 Not where we keep doc links 2026-07-19 08:12:43 -07:00
Stephen Taylor 9030ba870c Force software cursors on nouveau
Adapt the fix from PR #6161 to Quattro's per-user hardware setup and add coverage for detection, idempotency, and proprietary-driver handling.
2026-07-18 21:49:47 -07:00
David Heinemeier Hansson 73efa7d926 Remove stale nofile config during Quattro upgrade 2026-07-18 21:42:00 -07:00
David Heinemeier Hansson be4a9ed6dd Move Tmux keybindings to menu end 2026-07-18 21:33:10 -07:00
David Heinemeier Hansson 7b5c915c8c Clear bar move outline on release 2026-07-18 21:23:09 -07:00
David Heinemeier Hansson 6742adce3d Focus weather city input on return 2026-07-18 21:19:14 -07:00
David Heinemeier Hansson be1aab859d Prevent weather icon flicker 2026-07-18 21:19:10 -07:00
David Heinemeier Hansson c7952c980f Synchronize weather hero icon 2026-07-18 21:11:52 -07:00
Mateus PegorimandClaude ef1db32774 Prevent migration notifier from rate-limiting itself off (#6174)
omarchy-update-user-notify.path watches /usr/share/omarchy/migrations
with PathModified. A single update commonly drops several migration
files at once, so the oneshot service is re-triggered in a burst and
exceeds the default start-rate limit (StartLimitBurst=5 / 10s):

  omarchy-update-user-notify.service: Start request repeated too quickly.
  omarchy-update-user-notify.path: Failed with result 'unit-start-limit-hit'.

When the limit trips, the watching .path unit itself fails and stops
watching, so migrations that land later in the session are never
surfaced until the next login.

Set StartLimitIntervalSec=0 on the service. omarchy-migrate-notify is
idempotent, so there is no downside to letting it run on every trigger.

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-18 18:40:12 -07:00
cb989615f0 Align MAX_SNAPSHOT_ENTRIES with Snapper retention (off-by-one) (#6175)
* Align MAX_SNAPSHOT_ENTRIES with Snapper retention

Snapper is configured with NUMBER_LIMIT="5", but limine-snapper-sync
also counts the freshly-created / currently-booted snapshot, so five
retained snapshots are reported as six entries and it warns on every
snapshot and update:

  Snapshot limit mismatch: 6 Snapper snapshots exceed configured
  MAX_SNAPSHOT_ENTRIES=5

Raise MAX_SNAPSHOT_ENTRIES to 6 (NUMBER_LIMIT + 1) so the boot-menu cap
and Snapper's retention agree and the off-by-one warning stops. (An
alternative is MAX_SNAPSHOT_ENTRIES=auto, which derives the cap from
Snapper directly; kept an explicit value here to preserve the existing
boot-menu limit.)

Co-Authored-By: Claude <noreply@anthropic.com>

* Clarify Limine snapshot limit

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-07-18 18:40:11 -07:00
58e13cd079 Prevent empty drive encryption passwords (#6306)
* Prevent empty drive encryption passwords

* Use sudo, quote the confirmation compare, and cover mismatches in test

The unquoted [[ $new_password == $confirmation ]] made the confirmation
a glob pattern, so a confirmation like * matched any password. And pkexec
brought nothing over the repo-standard sudo here while failing outright
in sessions without a polkit agent.

Verified against a loopback LUKS device: new key lands from stdin without
a trailing newline while the current passphrase is prompted on the tty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: mplaczek99 <mplaczek99@gmail.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:21:39 -07:00
David Heinemeier HanssonandClaude Fable 5 afa2839a5a Rename bg/fg palette keys to background/foreground
The canonical colors.toml keys are now background/foreground, including
all permutations (dark_background, darker_background, lighter_background,
dark_foreground, light_foreground, bright_foreground). The old short
names are gone entirely, not kept as aliases; legacy alacritty-generated
themes still resolve through the ANSI color0/color7 fallback.

Theme files are also regrouped: accent/selection/muted first, then the
backgrounds, then the foregrounds, then the named colors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:24:51 -07:00
David Heinemeier HanssonandClaude Fable 5 72d7646c64 Add SSHD setup and removal under Setup/Remove > Security
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:09:03 -07:00
David Heinemeier HanssonandClaude Fable 5 fa2b997513 Keep software-composited cursors out of screenshots
Hyprland falls back to compositing the cursor into the frame on GPUs
without working hardware cursors, so grim would capture it. Force
hardware cursors for the duration of the capture and restore after.

Closes #6142

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 15:42:01 -07:00
David Heinemeier HanssonandClaude Fable 5 96bb617a9c Match Mullvad hosts by suffix
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:56:46 -07:00
David Heinemeier Hansson 1620b3467b Refresh applications during Quattro upgrade 2026-07-18 12:56:46 -07:00
1024bf5e7a Backfill pre-quattro hardware support and tmux fixes for existing installs
Existing quattro installs get sof-firmware, the Mesa Vulkan drivers,
the tmux terminal-features and pane-binding fixes, and the XPS 13 text
scaling via migration, while Omarchy 4 upgraders get the same hardware
support and scaling through omarchy-upgrade-to-quattro.

The Vulkan driver backfill adapts da46ad85 from master.

Co-authored-by: Sai Sridhar <tarrasridhar1154@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:31:34 -07:00
David Heinemeier HanssonandClaude Fable 5 46df0cf495 Nudge XPS 13 text scaling down just ever so slightly
The 2560x1600 panel on the 2026 XPS 13 (DX13260) renders the default
scaling a touch large. Ports f1fa7f69 from master.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:31:25 -07:00
David Heinemeier HanssonandClaude Fable 5 804d138909 Widen sof-firmware install to all Intel SOF audio platforms
The sof-audio-pci-intel-* driver family needs sof-firmware to boot the
DSP, but mainline linux only optdeps it, so affected installs came up
with a Dummy Output sink only. Replace the Panther Lake check with
omarchy-hw-intel-sof, which detects any Intel PCI audio controller.

Ports ffa81021 from master, which adapts basecamp/omarchy#6200.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:31:25 -07:00
Zeus-DeusandClaude Fable 5 6a4ba9df7c Bundle vconsole.conf in the initramfs so Plymouth uses the right keymap at the LUKS prompt
Ports e891e5c2 from master.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:31:13 -07:00
DiegoandClaude Fable 5 f322f0332b Drop fixed unit name from power-profile udev rule to prevent wakeup failures
With a fixed --unit name, the rule fails to start when a unit from a
previous power event still exists, leaving the power profile stale.
Letting systemd-run generate unique unit names keeps every trigger
working; --collect still garbage-collects each unit on exit.

Ports 749a8c04 from master.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:31:13 -07:00
David Heinemeier HanssonandClaude Fable 5 2f081f57b2 Append the kitty extkeys terminal feature instead of overwriting slot 3
Ports efcd7805 from master.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:31:03 -07:00
David Heinemeier HanssonandClaude Fable 5 a4c13bdefb Make package removal match exact installed names only
pacman -Q resolves provider names, so dropping a virtual package could
remove whichever package provides it. Read pacman -Qq once and match
requested names exactly, ignoring providers and duplicate arguments.
Ports the provider-ignoring semantics of 9791b227 from master with a
single pacman query instead of one per package.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:31:03 -07:00
David Heinemeier HanssonandClaude Fable 5 f7152cac17 Remove dropped bsnes2014 and mame2016 RetroArch cores
libretro-bsnes2014 and libretro-mame2016 no longer exist in the repos,
which made the whole retroarch install fail. Ports fe30a07d from master.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:30:54 -07:00
David Heinemeier Hansson d39c791e68 Merge pull request #6236 from basecamp/fix-clamshell-dpms-wake
Fix internal laptop screen re-lighting itself 2sec after locking
2026-07-18 12:09:04 -07:00
David Heinemeier HanssonandClaude Fable 5 9aa1dcd664 Drag the bar to move it, drop the config panel
Position the bar by dragging (or click-and-holding) empty bar space
toward a screen edge, with a ghost slab previewing the target edge.
With drag for position and double-click for transparency, the bar
config panel, its inline gear button, and the omarchy-launch-bar-settings
CLI are no longer needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:44:35 -07:00
David Heinemeier Hansson 66cdec78c2 Align bar settings control spacing 2026-07-18 10:56:19 -07:00
David Heinemeier Hansson 52ecb4eaf7 Equalize bar status icon spacing 2026-07-18 10:51:14 -07:00
David Heinemeier Hansson b39313abcf Simpler finish 2026-07-18 10:45:47 -07:00
David Heinemeier Hansson d0cc4b9674 More explicit clear 2026-07-18 10:40:43 -07:00
David Heinemeier Hansson 79e408b323 Clear before the logo 2026-07-18 10:33:02 -07:00
David Heinemeier Hansson 4d3165d8c1 Keep new browser windows on current workspace 2026-07-18 10:29:38 -07:00
David Heinemeier Hansson 4db7a03235 Let's try a logo 2026-07-18 10:26:55 -07:00
David Heinemeier Hansson 3f87af5ef4 Quote Omarchy paths in commands 2026-07-18 10:08:43 -07:00
David Heinemeier Hansson 8862fb22bd Use Omarchy command helpers consistently 2026-07-18 10:08:43 -07:00
David Heinemeier Hansson bd34704f7b Clarify privilege escalation guidance 2026-07-18 10:08:43 -07:00
David Heinemeier Hansson 77353e9670 Slim down celebration text 2026-07-18 10:03:15 -07:00
Stanko K.R. bfefcc7e11 Wake internal display only when leaving clamshell
The clamshell watcher polls omarchy-hyprland-monitor-clamshell every two
seconds and calls enable_internal unconditionally which re-enabled DPMS
on the internal panel each time. When the lock screen blanks the displays,
the next poll of  omarchy-hyprland-monitor-clamshell re-lights the laptop
screen, leaving it showing the lock screen indefinitely while external
monitors stayed off.

Only issue the DPMS wake when the clamshell flag was actually consumed,
i.e. on the lid-open transition the wake was meant for.

If the lid-open even fails to turn on the screen for whatever reason,
then pressing a key or moving the mouse will re-light the screen. This
is configured in `input.lua` via `key_press_enables_dpms = true` and
`mouse_move_enables_dpms = true`
2026-07-18 15:18:05 +02:00
David Heinemeier Hansson 784996c378 Scale webcam overlay to monitor resolution 2026-07-17 18:28:44 -07:00
David Heinemeier Hansson ca0cf8bff9 Fix webcam resize controls and placement 2026-07-17 18:28:44 -07:00
David Heinemeier Hansson 1174f2a70b Add live webcam overlay sizing 2026-07-17 18:28:44 -07:00
David Heinemeier Hansson 0b7c440009 Use gum to prompt for consent 2026-07-17 18:27:32 -07:00
David Heinemeier Hansson 839fb547e3 Update the skill to talk about pkexec 2026-07-17 17:50:52 -07:00
David Heinemeier Hansson 73e5968c64 Show context in polkit prompt 2026-07-17 17:45:29 -07:00
David Heinemeier Hansson 28d853402e Make the Copy URL extension reliable across install and upgrade
The Copy URL extension was intermittently failing to copy anything, and
its notification looked oversized with no icon. Several distinct problems:

- The MV3 service worker was pinned to stale cached code. Chromium caches
  the worker script for a --load-extension command-line extension and does
  NOT re-register it when the file changes in place — not on restart, not on
  a manifest version bump, not even after a clean shutdown. So updated code
  never took effect: the old worker kept calling chrome.scripting.executeScript,
  which throws once the scripting permission is gone, and the copy silently
  failed. Renaming the worker to a versioned filename (background-2.js) is a
  new script URL, which forces a fresh registration for everyone — new
  installs and existing installs alike. Rename it again on any future worker
  change (see the note at the top of background-2.js).

- Clipboard writes now go through an offscreen document (MV3's sanctioned
  path) using a textarea + execCommand('copy'), replacing executeScript +
  navigator.clipboard. This works on chrome:// pages and needs no scripting
  permission. execCommand is used deliberately: navigator.clipboard.writeText
  rejects in an unfocused offscreen document.

- Add a toolbar action (chrome.action.onClicked) so the extension is
  clickable, not keyboard-only — it was greyed out in the extensions menu
  with nothing to invoke.

- Slim the "URL copied" notification: lead the summary with a glyph so the
  omarchy notification shell collapses it to a single-line toast with an
  icon, instead of an oversized card with a blank icon slot.

- Guard the Quattro shortcut-repair migration: it edits Chromium Preferences
  to move the Alt+Shift+L binding to the new extension id, but a running
  browser rewrites Preferences on exit and reverts the edit. Prompt (via gum)
  to close the browser first, only when a browser is running and there is
  actually a stale binding to repair.
2026-07-17 16:28:18 -07:00
David Heinemeier Hansson 182330465a Simplify omarchy-bar into a bar-settings and bar-plugin split
The omarchy-bar command had grown three overlapping ways to inspect the
bar (show/layout/list/options, plus selected/active/available/widgets
aliases) and mixed layout mutation in with bar-level settings. Untangle
it into two focused commands:

- omarchy-bar keeps only the bar-level settings that write shell.json:
  use, reset, position, transparent, and settings. reset now delegates
  to `use omarchy.bar` rather than duplicating the del(.bar.id) write.
- omarchy-bar-plugin owns all layout mutation: add, move, remove, set,
  and replace, with the placement flags and jq resolve/anchor helpers.
  `omarchy bar plugin ...` routes here via the dispatcher.

Drop the inspection commands entirely: the layout is visible on the bar,
the config is shell.json, and widget/option ids come from
`omarchy plugin list`. Nothing consumed the show output programmatically
except tests. This also removes omarchy-bar-position, whose jq write was
a duplicate of `omarchy bar position`.

Strip environment-invariant guards (require_command, require_omarchy_path)
that defended against jq or OMARCHY_PATH being absent — neither happens on
a real system. Keep the user-input validation (--section/--index) and the
atomic shell.json write.

Update callers (service install/remove, refresh-shell, plugin-clone,
plugin enable), keybindings, the menu, tests, and docs to the new split.
2026-07-17 16:28:17 -07:00
David Heinemeier Hansson f8fec7cccf Tighten compact status icon spacing 2026-07-17 16:17:49 -07:00
David Heinemeier Hansson 8211b86ac6 Restore compact status icon spacing 2026-07-17 16:15:01 -07:00
David Heinemeier Hansson 6867f16b78 Restore compact bar indicator sizing 2026-07-17 16:08:33 -07:00
David Heinemeier Hansson 22791803ed Align vertical bar widgets to the icon grid 2026-07-17 16:03:49 -07:00
David Heinemeier Hansson 0c8c4ea26a Align bar icons on a shared baseline 2026-07-17 15:37:01 -07:00
David Heinemeier Hansson 6e3b69b8da Standardize bar icon geometry 2026-07-17 15:20:55 -07:00
David Heinemeier Hansson fe48e83222 Repair Tailscale authorization flow 2026-07-17 10:20:08 -07:00
David Heinemeier Hansson 16b5f2157f Preserve user state during theme setup 2026-07-17 07:59:20 -07:00
David Heinemeier Hansson 09aac40eae Gate first-run setup as one lifecycle 2026-07-17 07:59:16 -07:00
David Heinemeier Hansson 39756d485d Synchronize weather location updates 2026-07-16 21:54:57 -07:00
David Heinemeier Hansson e82df1d4d0 Track one-time setup with completion markers 2026-07-16 21:53:15 -07:00
David Heinemeier Hansson 7b824b6e1b Fix saving typed weather locations 2026-07-16 21:23:32 -07:00
David Heinemeier Hansson b79ae75833 Fix graceful window closing before reboot 2026-07-16 21:19:28 -07:00
David Heinemeier Hansson 93181b6a22 Restore service widgets with bar defaults 2026-07-16 21:15:00 -07:00
David Heinemeier Hansson af601381ca Ensure lock is setup on migration 2026-07-16 20:46:41 -07:00
David Heinemeier Hansson c58d00f203 Repair Copy URL shortcut during Quattro upgrade 2026-07-16 16:50:29 -07:00
David Heinemeier HanssonandClaude Fable 5 5044e711a0 Let the weather panel be pinned to a chosen location
The weather widgets asked wttr.in to geolocate by IP, which lands on
whatever city the ISP registers its address block under -- Downey when
you're in Malibu. Now the location can be set explicitly:

* Click the location in the weather panel to search via open-meteo
  geocoding, pick a suggestion (disambiguated by region), and it sticks.
  The X next to the field returns to IP auto-detect, as does committing
  an empty search.

* State lives in ~/.local/state/omarchy/settings/weather.json as
  {name, latitude, longitude}, owned by the new omarchy-weather-location
  command (bare call prints the current location, --set/--clear write).
  The panel watches the file, so hand edits apply live.

* Stored coordinates make wttr.in and open-meteo exact. Open-meteo also
  now supplies current conditions alongside the daily forecast it
  already served, which fills the panel within a second of a location
  change instead of waiting out wttr.in's multi-second responses; wttr
  results still win once they land. Failed wttr fetches retry instead
  of sitting stale until the next refresh cycle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:46:56 -07:00
David Heinemeier HanssonandClaude Fable 5 95c1ff0901 Fix stale monitor assertion that silently halted the test suite
9914513a unified internal monitor detection but left the test grepping
for the old jq expression. Since the suite runner stops on first
failure, every test file after monitor-recovery went silently unrun --
281 assertions were being skipped while the suite looked green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:44:05 -07:00
David Heinemeier HanssonandClaude Opus 4.8 6f6c991936 Remove dead code from plugin scripts
canonical_widget_id was a pure pass-through, making its reserved-id
check a tautology; require_omarchy_path and choose_value were defined
but never called.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 08:03:01 -07:00
David Heinemeier Hansson ad8ab1a7fa Rename text capture command 2026-07-15 18:23:40 -07:00
David Heinemeier HanssonandClaude Fable 5 51bd33d7ab Keep tmux cursor colour on theme after apps reset it
Apps like nvim clear the per-pane cursor colour with OSC 112, which made
tmux reset the outer terminal's cursor. Terminals that can't reload their
config (foot) then restore the cursor colour from whatever theme was
active at launch, leaving a mix of old and new cursor colours across
panes. Set the global cursor-colour option as a fallback so cleared panes
land on the current theme instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 18:13:39 -07:00
David Heinemeier Hansson 38f516c430 I never use a dedicated web app for this
So let's just drop it
2026-07-15 17:57:24 -07:00
David Heinemeier Hansson 47f3074e4e Fix launcher application icon resolution 2026-07-15 17:45:27 -07:00
David Heinemeier Hansson 298e14071b Take a system snapshot before upgrading to quattro 2026-07-15 17:08:24 -07:00
David Heinemeier Hansson 2f7a07edbe Better wording for unlink as well 2026-07-15 15:28:09 -07:00
David Heinemeier Hansson dd00f20608 Offer the reboot, simplify wording 2026-07-15 15:25:44 -07:00
David Heinemeier Hansson aac579a2df Add rsyncing to remote host intended for easy forwarding of screenshots to remote AI runners 2026-07-15 10:54:30 -07:00
David Heinemeier Hansson 45a9d4ed1b We ride with danger or not at all 2026-07-14 17:23:01 -07:00
David Heinemeier Hansson d25abba046 Merge pull request #6221 from kevinmcconnell/systray-action-fix
Fix bar plugin array guards
2026-07-14 11:05:22 -07:00
David Heinemeier Hansson 07568e165a Merge pull request #6218 from kevinmcconnell/fix-transparent-bar-flicker
Prevent using an opaque surface for bar
2026-07-14 11:04:57 -07:00
Kevin McConnell d5492db404 Fix panel array guards
The `Bar` uses a `Repeater` when building out the models. This has the
effect of marshalling the configuration, and causing JSON arrays to
become sequence wrappers.

A couple of the plugins had been checking for the existence of array
values with `Array.isArray`, which fails for the sequence wrappers,
causing the values to be ignored. This breaks associated functionality
(the "hide" and "pin" actions in the systray had stopped working, as had
exit node selection in Tailscale).

Switching the guards to use `instanceof Array` works correctly for
sequence values (as well as arrays).
2026-07-14 15:36:43 +01:00
Kevin McConnell be595c72ad Prevent using an opaque surface for bar
The bar supports switching between transparent and opaque. That means we
can't use an opaque surface for it; if we did, it would be unable to
reliably render as transparent when needed.

This fixes a visual glitch where the background of a transparent bar
would flicker, particularly during mouse hover.
2026-07-14 10:00:19 +01:00
David Heinemeier Hansson d570d99e16 Turn off fade switching between window groups 2026-07-13 12:37:38 -07:00
David Heinemeier Hansson 935283c849 Simplify rendering
It's adding so little visual flair and is a source of GPU load
2026-07-13 10:11:32 -07:00
David Heinemeier Hansson cb8ab2d9f4 Properly render tray icons 2026-07-13 09:56:20 -07:00
David Heinemeier Hansson 17efd5e493 Ensure EDITOR is exported for non-uwsm envs too
Like SSH'ing into an Omarchy machine
2026-07-11 11:42:55 -07:00
David Heinemeier Hansson cc64c3e27c Add mup alias for skipping mise up delayed updates 2026-07-09 11:25:34 -07:00
David Heinemeier HanssonandClaude Fable 5 7ff91d9d5c Only carry over user-added theme backgrounds during the quattro upgrade
The trailing slash from the themes/*/ glob put a double slash in the
path comparison, so stock backgrounds never matched git ls-files output
and were all moved into ~/.config/omarchy/backgrounds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 18:16:16 -07:00
David Heinemeier HanssonandClaude Fable 5 4abd084f7f Don't glob dotfiles twice when building the live omarchy overlay
With dotglob set, * already matches dotfiles, so the extra dot patterns
made the second ln -s fail and silently skip the legacy hypr shim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 18:16:16 -07:00
David Heinemeier HanssonandClaude Fable 5 f1bc8dad08 Purge stale omarchy package cache before the first quattro transaction
Legacy repos cached omarchy packages under the same name and version as
quattro rebuilds with different bytes, so pacman aborted --noconfirm
transactions with checksum errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 18:16:16 -07:00
David Heinemeier Hansson aabae6fffe Group all the omarchy-bar bins together 2026-07-08 11:49:56 -07:00
David Heinemeier HanssonandClaude Fable 5 c4b1e33669 Route the weather toggle through the shell target too
Weather wraps its panel in a BarWidget that only exposed togglePanel(),
so it couldn't ride the new shell-routed hotkey path and was left on the
stale-prone per-plugin target. Expose open/close/opened on the wrapper
(open maps to the panel's hotkey path so the center hover reveal stays
suppressed) and switch omarchy-notification-weather to the new syntax.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 09:18:03 -07:00
David Heinemeier HanssonandClaude Fable 5 e1cdc9005e Route panel hotkeys through the shell IPC target so they survive reloads
Bar-widget panels (audio, bluetooth, network, power, monitor) used to be
toggled via their own per-plugin IpcHandler targets. Quickshell resolves
duplicate targets first-handler-wins, so after a plugin or bar reload the
stale handler of the destroyed widget instance kept claiming the target
and the hotkeys went dead.

The shell root's IpcHandler lives outside the reload cycle, so summon,
hide, and toggle now go through `omarchy-shell shell toggle <plugin>`
and the shell routes to the live widget instance via the bar's slot
registry. Panel plugins that are also panel/overlay/menu kinds keep
using the panel loader path. Failures to find a live widget are logged
so a widget missing from the bar layout stays diagnosable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 09:17:54 -07:00
David Heinemeier HanssonandClaude Fable 5 6bb3cb666b Rename debugModuleSlots to moduleSlots
The slot registry is about to become the routing table for panel
hotkeys, so it has outgrown its debug-only name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 09:17:37 -07:00
David Heinemeier Hansson af828481b9 Give it a little more time
Felt too abrupt at 3s
2026-07-04 12:08:34 -07:00
David Heinemeier Hansson 09a189e49f Set title from tmux so it's easier to use with multiple servers 2026-07-04 11:44:39 -07:00
David Heinemeier HanssonandClaude Fable 5 1e996609a0 Capture the entire screen with Return during a region pick
A previous submap attempt was brittle because the script owned the
keyboard state: if it died between enter and exit, the submap stuck.
This version lets the compositor own both edges. The Lua config binds
plain Return when a layer surface with slurp's "selection" namespace
opens and unbinds it when the last one closes, so the bind exists
exactly while a pick is on screen. slurp dying for any reason (cancel,
crash, pkill, SIGKILL of the picker) fires layer.closed and the bind
evaporates — there is no code path where it leaks.

Return invokes omarchy-capture-region --take-fullscreen, which flags a
marker in XDG_RUNTIME_DIR and dismisses slurp; the picker treats an
empty slurp result with a fresh marker as the focused monitor, reusing
the existing fullscreen path. Since monitor focus follows the cursor,
that is the monitor under the slurp crosshair. The recording flow gets
monitor:NAME from the existing match, so Return during a recording
pick starts a native full-monitor capture.

The signal mode no-ops unless slurp is actually running, and both the
signal protocol and the layer-scoped bind lifecycle are verified live:
bind present only while slurp is open, real picks resolved by the
signal on both the screenshot and recording paths, no leftover binds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:13:19 -07:00
David Heinemeier HanssonandClaude Fable 5 b72aa8f65e Rename region picker to omarchy-capture-region
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:30:41 -07:00
David Heinemeier HanssonandClaude Fable 5 362d97c151 Share one region picker between screenshot and recording
Both capture scripts carried the same pipeline: workspace rectangles
from hyprctl, a hyprpicker freeze, slurp, and the bare-click snap loop.
The copies had already diverged once (rotated-display handling), so the
pipeline now lives in one hidden helper, omarchy-capture-region-pick,
which prints the picked geometry in slurp format.

The two callers differ only in what they layer on top. The screenshot
needs the freeze to outlive the pick so grim captures frozen content:
--keep-freeze leaves hyprpicker running and prints its PID first, with
the caller owning the kill. The recording prefers native full-monitor
capture: --match-monitor prints monitor:NAME when the geometry exactly
matches a display.

One deliberate fix along the way: the screenshot snap regex rejected
negative coordinates, so bare clicks on monitors positioned left of or
above the origin never snapped. The helper uses the recording variant,
which handles them.

Verified with an 11-case shim harness (snap precedence, rotated-monitor
match, freeze ownership, cancellation, negative coordinates) and the
live end-to-end screenshot sanity test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:26:46 -07:00
David Heinemeier HanssonandClaude Fable 5 3bd6e72211 Share one theme color resolver across consumers
Five scripts each hand-rolled colors.toml parsing plus their own
alias/fallback cascade, and the tables had drifted: theme-osc grew a
divergent color8/color9 chain, tmux and gnome carried private mode
detection, and the preview tool duplicated the lot. The differences
never mattered for shipped themes but were a standing bug factory for
sparse user themes.

omarchy-theme-color is now the single resolver. Its cascade is a
verbatim port of the canonical theme-set-templates logic (semantic
keys, legacy colorN aliases, derived shades, selection/cursor
derivation, mode precedence), exposed as --all, --raw, or single-key
lookup with fallback.

Generated output is byte-identical across all 21 shipped themes on
every surface: 17 template files, OSC byte stream, tmux and gsettings
command logs, and previews. Sparse legacy themes now resolve the same
palette everywhere instead of five slightly different ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:06:31 -07:00
David Heinemeier HanssonandClaude Fable 5 03f7420d72 Pass empty placement explicitly in bar replace
cmd_replace fed $PLACEMENT_FROM_SECTION/$PLACEMENT_FROM_INDEX to jq
without ever calling parse_placement, so they were always empty
strings that implied flag support which does not exist. Pass literal
empties with a comment stating replace resolves by widget id only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:14:33 -07:00
David Heinemeier HanssonandClaude Fable 5 54539b4a83 Handle rotated monitors in the recording picker
The screenshot picker swaps monitor dimensions for transform 1/3
displays; the recording copy had drifted and did not, so portrait
monitors got landscape rectangles in the picker. The full-monitor
match had the same blind spot, silently downgrading rotated-monitor
selections to region capture instead of native -w capture.

Both now share the screenshot script's format_geo jq helper, which
also lets the monitor match compare one geometry string instead of
four fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:14:33 -07:00
David Heinemeier HanssonandClaude Fable 5 9914513a31 Unify internal monitor detection across scripts
Four scripts matched internal panels with contains("eDP") while
omarchy-monitor-state used test("^(eDP|LVDS|DSI)-"), so laptops with
LVDS or DSI panels were internal to monitor-state but invisible to the
clamshell, mirror, and external-active logic. All five sites now share
the inclusive anchored pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:14:33 -07:00
David Heinemeier HanssonandClaude Fable 5 1d87fa0188 Fix multi-file sharing losing separate paths
FILES="$*" space-joined multiple CLI path arguments into one bogus
argument before handing them to LocalSend. Build the file list as an
array from the start, which also collapses the single/multi send
branch into one path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:14:33 -07:00
David Heinemeier HanssonandClaude Fable 5 54d9cdc328 Collapse keymap resolution and prune keybindings menu
The keymap cache parsed xkbcli compile-keymap into a bash associative
array, serialized that array back into a string, and re-parsed it
inside a second awk's BEGIN block. One awk now reads the compiled
keymap directly, seeded with the fallback table so xkbcli failure
still degrades gracefully. (The xkbcli child must read /dev/null or
it consumes the binding records arriving on stdin.)

Also gone: the never-called lookup_keycode_cached, the DEBUG timing
scaffold, the redundant explicit noops in the Lua mock that the
metatable already provides, the walker-era XML entity escaping that
nothing renders as markup anymore, and the monitor-height computation
in favor of a fixed height like every other menu.

The cache key drops the double sha256 and now includes the active
keymap, so switching XKB layout invalidates cached symbol names
instead of serving stale ones.

Binding records are byte-identical before and after: 215 of 215.

686 lines to 573.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:09:15 -07:00
David Heinemeier HanssonandClaude Fable 5 fef59cece8 Parse alacritty colors in one awk pass
Each color lookup spawned a 60-line awk program with a character-by-
character quote-state machine for comment stripping, about 22 spawns
per run counting the dotted-key retries. One pass now tracks the
current [section], normalizes dotted keys to the same path, and emits
key/hex pairs into a single associative array the shell reads once.

The strict value pattern (a lone, optionally quoted and 0x/#-prefixed
six-digit hex, followed by nothing or a comment) makes quote-aware
comment stripping unnecessary. Section form still wins over dotted
form and first valid occurrence still wins, matching the original
search order. Output is byte-identical across fixtures covering both
key forms, comments, prefixes, and fallback paths, plus real themes.

197 lines to 151.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:09:14 -07:00
David Heinemeier HanssonandClaude Fable 5 b14735aacb Merge duplicate ASCII renderers and trim dead options
The braille and block renderers were two near-identical awk programs
sharing a verbatim PBM tokenizer, header check, and blank-line trim,
differing only in the per-cell rendering. One awk with a mode variable
now branches where they actually differ.

The option surface served two in-repo callers, both passing constants:
the duplicate positional loop behind --, the undocumented --block and
--braille aliases, and the --flag=value forms all go. The two magick
probes for the alpha range collapse into a single invocation.

Output is byte-identical across images and flag sets for everything
that remains, including both real callers' exact invocations.

353 lines to 243.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:09:14 -07:00
David Heinemeier HanssonandClaude Fable 5 cacd22c671 Drop vestigial env reconstruction from shell restart
The /proc/*/environ scraping, wayland/X11 socket probing, and LANG/DBUS
guessing arrived with the Omarchy 4 live upgrade cutover, whose upgrade
script no longer exists. Every remaining caller runs inside the
graphical session with full env. Keep a five-line guard deriving
HYPRLAND_INSTANCE_SIGNATURE from the newest instance runtime dir so
restarting over ssh still works.

Launching now goes straight to the Lua dispatcher: on quattro, classic
hyprctl dispatch arguments are evaluated as Lua and fail with exit 0,
so the old exec fallback silently did nothing. Check for "ok" output
instead of trusting the exit code, with setsid as the real fallback.

180 lines to 94.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:08:56 -07:00
David Heinemeier HanssonandClaude Fable 5 4a2502cb63 Replace hand-rolled shell IPC with qs ipc call
The embedded Perl program parsed instance.lock QString framing and spoke
the Quickshell IPC socket protocol by hand. Quickshell ships this
natively: qs -p <config> ipc call does instance discovery, the call, and
response printing, byte-identical for every in-repo call pattern.

The bash wrapper keeps what qs doesn't do: the -q best-effort mode, the
{} default argument for summon/toggle, and the familiar error wordings.
Two qs quirks are handled: function names like "show" shadow qs's own
CLI subcommands, so arguments are always passed after --, and IPC-level
failures (unknown target/function, bad arguments) are reported on stdout
with exit 0, so those are matched and mapped to stderr with exit 1.

165 lines to 62.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:08:56 -07:00
David Heinemeier HanssonandClaude Fable 5 798d6af8b0 Replace the plugin package manager with plain git
A plugin is now just a git repo cloned into ~/.config/omarchy/plugins/<id>/.
That one idea replaces the entire homegrown package-manager half of the
plugin suite: trusted-source registry, clone cache, catalog scanning,
semver comparison, staging dirs, and timestamped backups — 1,025 lines
across five binaries whose jobs git already does.

Gone:

- omarchy-plugin-source: the trusted-repo registry (sources.json) and its
  clone cache under ~/.cache/omarchy/plugin-sources/. The trust decision
  now happens once, at add time, with the same unsandboxed-code warning.
- omarchy-plugin-scan + omarchy-plugin-available: the catalog machinery
  over cached clones. Discovery belongs on a web page, not in the CLI.
- omarchy-plugin-add: copying folders out of cached source clones with
  hand-rolled staging and .bak backups. Replaced by a git clone.
- omarchy-plugin-update: manifest version comparison via sort -V and
  re-installs. Replaced by fetch + diff + fast-forward; git is the version
  and git is the backup.
- omarchy-plugin-remove and omarchy-plugin-edit as separate binaries:
  folded into omarchy-plugin, much slimmer.

The consolidated omarchy-plugin now handles the full lifecycle:

- add <git-url>: warn, clone into a dot-prefixed staging dir (invisible
  to the plugin scanner), validate, then move into place named by the
  manifest id. Plugins land disabled — enabling is the single consent
  moment, replacing the old review-before-copy flow.
- update [id | --all]: fetch origin HEAD, show the diff (delta when
  available), confirm, fast-forward. Updates are code the shell will run,
  so the result is re-validated and rolled back to ORIG_HEAD if upstream
  turned invalid (e.g. smuggled a symlink).
- remove [id]: git checkouts are deleted outright since upstream keeps
  the history; hand-made plugin folders still get a backup, and dev
  symlinks are just unlinked.
- edit [id]: opens the user plugin directory in a shell.

All commands keep the interactive/unattended split: gum prompts in a
terminal, hard refusal without --yes otherwise, so scripts and agents
never hang on a hidden prompt.

Kept as siblings: omarchy-plugin-catalog (omarchy-bar reads it),
omarchy-plugin-validate (the security boundary, now pruning .git from its
symlink scan since installs are git checkouts), and omarchy-plugin-clone
(local development of built-in widgets, a separate concern).

Trade-offs accepted: one repo = one plugin (no more multi-plugin source
repos), and ref pinning or branch switching is no longer a CLI feature —
an installed plugin is a plain checkout, so that is ordinary git in the
plugin directory.

None of the removed machinery ever shipped: it existed only on this
branch, so there is no migration. The net effect is 11 scripts / 2,040
lines down to 4 scripts / 1,080 lines, and one less concept for users to
learn — everyone already knows what a git repo is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:47:33 -07:00
David Heinemeier HanssonandClaude Fable 5 60e0a2a0cd Simplify clipboard watcher lifecycle with pdeathsig
Replace the three-layer process management (trap cleanup, PPID-polling
watchdog subshell, /proc-scanning reaper) with kernel-level lifetime
binding: each wl-paste watcher is spawned under setpriv --pdeathsig TERM,
so it dies with the shell no matter how the shell exits.

- Delete watch.sh: the shell now spawns both watchers directly as
  declarative Process commands instead of a wrapper script with traps
  and a 1s polling loop
- Delete init.sh: reaping stale watchers from a crashed pre-pdeathsig
  shell is a single pkill at startup
- Collapse capture.sh's dual stream/snapshot modes: wl-paste forwards
  arguments to its watch command, so the mime rides along as $1 instead
  of an env var, letting both modes share one emit_image function
- Turn the six-branch image mime chain into a loop

Net: two fewer files, three fewer runtime processes, and the watch-mode
capture tests now exercise the same argument protocol the shell uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:32:30 -07:00
David Heinemeier Hansson 055ffaccd4 Split omarchy-plugin into siblings and consolidate bar config into omarchy-bar
omarchy-plugin mixed plugin lifecycle, clone/edit, and bar-layout mutation.
The bar layout was mutated by two separate engines (an embedded Python
mutate_bar here and jq in omarchy-config-shell-bar), each with its own
manifest walker.

- omarchy-plugin is now a ~220-line router; clone and edit move to
  omarchy-plugin-clone and omarchy-plugin-edit, matching the existing
  plugin-{source,add,update,remove,available,validate,scan} sibling pattern
- omarchy-config-shell-bar is replaced by omarchy-bar, the single bar engine
  (show/layout/list/options/use/reset/add/move/remove/set/replace/position/
  transparent/settings) with one jq pipeline; the Python mutate_bar and its
  python3 dependency are gone
- omarchy-plugin-catalog is the shared manifest scanner used by omarchy-bar
  (widget/option enumeration, add validation) and omarchy-plugin-clone
  (source enumeration), replacing three find|jq re-implementations
- service install/remove and refresh-shell call omarchy-bar; docs and tests
  updated to the new command surface
2026-07-02 11:10:24 -07:00
David Heinemeier Hansson 738821c3ab Fix clipboard watcher lifecycle 2026-07-02 11:09:42 -07:00
David Heinemeier Hansson 76e0b718b2 Use symbolic slash for scaling binds 2026-06-29 15:49:30 -05:00
David Heinemeier Hansson 9dcb7c78dd Audit monitor scaling changes 2026-06-29 15:43:11 -05:00
David Heinemeier Hansson e88174fd4b Make clamshell scale recovery idempotent 2026-06-29 15:28:16 -05:00
David Heinemeier Hansson beff26d8e0 Replay recent notification history 2026-06-29 15:19:32 -05:00
David Heinemeier Hansson 6ef0c0192b Resolve launcher device icons 2026-06-29 15:04:57 -05:00
David Heinemeier Hansson d4731eca25 Assume gum is always available and drop fallbacks 2026-06-29 14:49:39 -05:00
David Heinemeier Hansson 38553effa3 Save the default ai harness setup until we have a better use case 2026-06-29 14:42:16 -05:00
David Heinemeier Hansson 89615a526d Render Omarchy menu icon with Omarchy font 2026-06-29 14:12:36 -05:00
David Heinemeier Hansson 8732306820 Use menu font icon for Omarchy update entry 2026-06-29 13:56:27 -05:00
David Heinemeier Hansson 7c6189c3d3 Hide clipboard open/paste commands from default listings 2026-06-29 13:45:11 -05:00
David Heinemeier Hansson f58f1e8dd4 Preserve internal scale during monitor recovery 2026-06-29 13:36:31 -05:00
David Heinemeier Hansson e4b5eb2d1e Restore monitor scaling shortcut 2026-06-29 13:35:17 -05:00
David Heinemeier Hansson dafc400891 Move monitor scaling off Super slash 2026-06-29 13:24:56 -05:00
David Heinemeier Hansson fed2fd8e3d Fix notification expire timeout being treated as seconds instead of milliseconds 2026-06-29 13:24:04 -05:00
David Heinemeier Hansson 1220a031b3 Limit monitor scale stepping to normal range 2026-06-29 13:19:14 -05:00
David Heinemeier Hansson b9f2d41e86 Add clipboard history Home End navigation 2026-06-29 12:58:04 -05:00
David Heinemeier Hansson 472b5cd557 Share pointer movement gate across menus 2026-06-29 11:56:21 -05:00
David Heinemeier Hansson a3f8c494e3 Ignore stationary pointer hover in clipboard 2026-06-29 11:44:31 -05:00
David Heinemeier Hansson a0abb1e0d1 Ignore stationary pointer hover in launcher 2026-06-29 11:40:31 -05:00
David Heinemeier Hansson 3820ec7925 Guard ISO Snapper assertions against older omarchy-iso checkouts 2026-06-29 11:36:35 -05:00
David Heinemeier Hansson 2cd73f39d2 Close launcher after delete confirmation 2026-06-29 11:30:42 -05:00
David Heinemeier Hansson e49c74eee8 Keep rc on its own mirror but use the edge package repo 2026-06-29 11:28:32 -05:00
David Heinemeier Hansson 987588f512 Launch shell commands on active workspace 2026-06-29 11:26:23 -05:00
David Heinemeier Hansson 4ab3377950 Drop OMARCHY_PATH fallback and conf reading in channel detection 2026-06-29 11:18:33 -05:00
David Heinemeier Hansson 987addd7da Launch presentation terminals on active workspace 2026-06-29 11:14:07 -05:00
David Heinemeier Hansson b034fc1905 Rework Omarchy channel detection: dev is a linked checkout, edge is the dev package channel 2026-06-29 11:12:35 -05:00
David Heinemeier Hansson d824c1bd33 Remove Figma as a default install 2026-06-29 11:04:51 -05:00
David Heinemeier Hansson 924254ed11 Update battery icon promptly on power changes 2026-06-29 10:49:43 -05:00
David Heinemeier Hansson f67ed326df Let's save this for a direct install 2026-06-29 10:19:28 -05:00
David Heinemeier Hansson 98c7f5b28c Add network ping and speed test sections 2026-06-29 09:44:29 -05:00
David Heinemeier Hansson ca91dc3fec Add alternative media controls for next/prev track for keyboards that only have a play button
Like the Dell XPS 13
2026-06-29 09:12:16 -05:00
David Heinemeier Hansson 29b87ca03e Cache keybindings menu records 2026-06-29 09:02:04 -05:00
David Heinemeier Hansson 6454e7ac8a Fix phantom display scaling by reading configured scale in clamshell 2026-06-29 08:54:10 -05:00
David Heinemeier Hansson 1ed0a1061b No need for the v2 now 2026-06-29 08:51:15 -05:00
David Heinemeier Hansson 86e5581496 Refresh clock after timezone change 2026-06-29 08:51:15 -05:00
David Heinemeier Hansson 67084b153f Fix launcher keyboard scrolling 2026-06-28 20:46:46 -04:00
David Heinemeier Hansson ba0d2a764f Fix launcher entry removal 2026-06-28 20:46:23 -04:00
David Heinemeier Hansson 2f4fc48791 Add ping to network panel 2026-06-28 20:11:21 -04:00
David Heinemeier Hansson 6565a4032f Pin Copy URL extension id 2026-06-26 19:52:17 -04:00
David Heinemeier Hansson 91ee7f20fc Fix media skip OSD metadata 2026-06-25 06:55:11 -04:00
David Heinemeier Hansson b043a9f90e Fix panel IPC hotkeys 2026-06-24 18:26:02 -04:00
David Heinemeier Hansson b7f3be6293 We can always depend on gum 2026-06-24 13:59:38 +02:00
David Heinemeier Hansson 1d364fc988 Simplify brightness no-osd parsing 2026-06-24 13:52:37 +02:00
David Heinemeier Hansson 0bf29e2f4a Cleaner flow: Either Apple controls or normal 2026-06-24 13:49:50 +02:00
David Heinemeier Hansson 89abbddc77 Fix image selector IPC reloads 2026-06-24 13:49:22 +02:00
David Heinemeier Hansson c43afa52ac Simpler 2026-06-24 13:48:41 +02:00
David Heinemeier Hansson 1320628829 Fold battery helpers into status command 2026-06-24 13:45:36 +02:00
David Heinemeier Hansson beea78084e Use PATH for panel helper commands 2026-06-24 13:40:16 +02:00
David Heinemeier Hansson d136585ef4 Rename AC power helper 2026-06-24 13:38:09 +02:00
David Heinemeier Hansson 267af23efc Make Battle.net launcher installer-only 2026-06-24 13:31:17 +02:00
David Heinemeier Hansson 1db2323fd9 Remove legacy bar widgets 2026-06-23 16:22:47 +02:00
David Heinemeier Hansson b30758dd2d Add optional service widgets on shell refresh 2026-06-23 16:22:27 +02:00
David Heinemeier Hansson 3fd36802ac Simplify the system-update check
No need for a panel, just focus on the main omarchy package
2026-06-23 15:59:27 +02:00
David Heinemeier Hansson aa7350ab60 Fix clamshell display recovery 2026-06-23 09:23:29 +02:00
David Heinemeier Hansson ee65304b92 Handle clamshell monitor hotplug 2026-06-22 21:48:35 +02:00
David Heinemeier Hansson 4772f01148 Was used by walker but no longer used 2026-06-22 21:01:34 +02:00
David Heinemeier Hansson e8e3b7ef54 Fix weather panel clipping and units 2026-06-22 20:52:02 +02:00
David Heinemeier Hansson 4773202ab0 Apply pending shell theme after background polling 2026-06-22 19:57:05 +02:00
David Heinemeier Hansson cdd833facb Apply shell theme if background reveal stalls 2026-06-22 19:52:35 +02:00
David Heinemeier Hansson eb3cb5225c Delay session lock during display hotplug 2026-06-22 19:44:37 +02:00
David Heinemeier Hansson 8f5336e0d5 Protect against hyprlock crash when there are no screens at all 2026-06-22 19:28:43 +02:00
David Heinemeier Hansson 62ada9e9ec Need to copy configs from $OMARCHY_PATH so it'll work with the dev link 2026-06-22 18:30:16 +02:00
David Heinemeier Hansson 383423036f Add omacut as a default package 2026-06-22 17:39:53 +02:00
David Heinemeier Hansson 95888e69ff Replace Typora with Omawrite 2026-06-22 17:38:50 +02:00
David Heinemeier Hansson 9a20742ea7 Stop image picker thumbnail flicker
The May 17 picker performance change made activated thumbnail loads asynchronous, which undid the earlier anti-flicker fix and let carousel movement flash blank thumbnails. Keep lazy activation, but load activated thumbnails synchronously so theme/background picker navigation remains stable. Add regression coverage for the thumbnail loading mode.
2026-06-22 17:32:56 +02:00
David Heinemeier Hansson 97c4101319 We don't need this notification service
It's using a staggering 250mb RAM
2026-06-22 17:07:09 +02:00
David Heinemeier Hansson 8f6e815c78 Recover internal monitor before sleep
Run internal monitor toggle recovery before the sleep lock path and retry recovery after Hyprland monitor removal events. This gives dock/lid-close races time to settle before suspend or external-display reconnects leave the session without a safe internal display fallback.
2026-06-21 15:42:24 +02:00
David Heinemeier HanssonandClaude Opus 4.8 efd8352dfb launcher: show icons for apps installed mid-session
The launcher resolves each entry's icon through Quickshell.iconPath(),
which bottoms out in Qt's QIconLoader / QIcon::fromTheme. That engine
caches the theme's directory listing — and null results — for the life
of the process, and only re-scans when the icon theme or search paths
change. Nothing in Quickshell 0.3 exposes a way to invalidate it, and a
QML reload doesn't reset it either (QIconLoader is a QtGui process
global, not tied to the QML engine).

So when a user installs a new app while the shell is running, its
.desktop file is picked up live (DesktopEntries is file-watched and the
name appears immediately), but its icon resolves to the cached miss and
renders blank until the whole shell is restarted. That's a poor "I just
installed this" experience.

Fix it without a restart by keeping our own on-disk icon index as a
fallback. When themed lookup returns empty, iconSource() consults a
name->path map built by scanning the XDG icon dirs and /usr/share/
pixmaps (SVGs first, so scalable wins). The scan runs on startup and on
a debounced DesktopEntries change — the same signal that already makes
names appear — and swapping the iconIndex property re-evaluates every
iconSource() binding, so freshly installed icons show up live. Icons Qt
can already resolve are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 15:05:06 +02:00
David Heinemeier Hansson 4823e05c00 Make the highlight more subtle
And stick with the same gaps setup as all other themes
2026-06-21 15:02:49 +02:00
David Heinemeier Hansson 36c1bc7dc5 Refer to Quattro 2026-06-21 14:59:18 +02:00
David Heinemeier Hansson 428b9c7b29 Use omarchy-upgrade-to-quattro 2026-06-21 12:31:06 +02:00
David Heinemeier Hansson c2e5614b65 Add missing overwrites 2026-06-21 12:25:45 +02:00
Ryan Hughes c048b2c745 Normalize Snapper snapshot setup 2026-06-21 02:25:42 -04:00
Ryan Hughes 34ddf5d54e Fix NetworkManager transition during upgrade 2026-06-21 02:25:42 -04:00
David Heinemeier Hansson 35a6940992 Just 3 seconds of screen on after lock 2026-06-20 22:27:03 +02:00
David Heinemeier Hansson 8ae975357a Make sure we never get in-panel scrolling when we shouldn' 2026-06-20 22:19:53 +02:00
David Heinemeier Hansson d74ed24db7 No notifications for udiskie 2026-06-20 22:19:53 +02:00
Ryan Hughes 2b95eddfa6 Fix remote copy / paste for nvim 2026-06-20 15:44:37 -04:00
David Heinemeier Hansson c62108fa0b Fix current theme state migration 2026-06-20 14:36:27 +02:00
David Heinemeier Hansson a2620e2fe0 Add missing migration for udiskie 2026-06-18 16:53:33 +02:00
Ryan Hughes d63fdd2a23 Avoid transient Hyprland binding module errors 2026-06-16 01:27:43 -04:00
Ryan Hughes 2fdc6f0573 Move stock Hyprland bindings into defaults 2026-06-16 01:25:04 -04:00
Ryan Hughes fd5063dc30 Fix notification popup placement 2026-06-16 01:25:04 -04:00
Ryan Hughes 2a7602a084 Complete upgrade-to-4 update flow 2026-06-15 11:27:54 -04:00
Ryan Hughes 227240d280 Collapse inactive indicator spacing 2026-06-14 21:12:42 -04:00
Ryan Hughes 8d490d525e Select Codex icon by surface background 2026-06-14 20:14:09 -04:00
Ryan Hughes e0eaf8eea8 Use light Codex icon on light themes 2026-06-14 20:09:29 -04:00
Ryan Hughes d8cff94711 Fix model usage layout on vertical bar 2026-06-14 17:40:00 -04:00
Ryan Hughes 4bfa5a6949 Switch default editor to tensaku so we can move things 2026-06-12 22:27:20 -04:00
Ryan Hughes c582e7133a Unify Omarchy migrations 2026-06-12 13:49:46 -04:00
Ryan Hughes 04b0fab64d Remove old test repo 2026-06-12 02:16:01 -04:00
Ryan Hughes fec52e53d2 Cleanup hyprland conf 2026-06-12 02:15:51 -04:00
Ryan Hughes b1e585fec5 Fix nvim theme location 2026-06-12 02:15:22 -04:00
Ryan Hughes 221fb296fc Simplify theme palette tokens 2026-06-11 02:08:02 -04:00
Ryan Hughes 4b2a15b6ea Avoid polkit prompts from shell network status 2026-06-10 13:10:03 -04:00
Ryan Hughes cd0b78cd0f Restore lock surface after shell restart 2026-06-10 12:17:35 -04:00
Ryan Hughes 15614cd4d3 Rename live dev channel to edge 2026-06-10 11:18:36 -04:00
Ryan Hughes c1b9788506 Fix channels 2026-06-09 23:55:58 -04:00
Ryan Hughes 0f5e81143e Move current theme state to local state 2026-06-09 19:48:37 -04:00
Ryan Hughes 29c4a20a86 Fix Claude weekly usage display 2026-06-09 18:11:47 -04:00
Ryan Hughes 7d30407a23 Namespace Discord icon 2026-06-09 15:21:52 -04:00
Ryan Hughes bce9fef4e3 Streamline Hyprland reload guard 2026-06-09 15:13:23 -04:00
Ryan Hughes f79a231cc7 Pause Hyprland reloads during settings updates 2026-06-09 14:20:36 -04:00
Ryan Hughes 9b8c9fd537 Update Omarchy channel switcher 2026-06-09 13:05:30 -04:00
Ryan Hughes 22a42e073a Infer edge channel on --dev 2026-06-09 10:22:55 -04:00
Ryan Hughes 64581ec289 Remove hot link / unlink bits 2026-06-08 12:03:40 -04:00
David Heinemeier Hansson 38bb61b685 Overly defensive 2026-06-08 17:48:09 +02:00
David Heinemeier Hansson 73d47390c4 Improve Bluetooth panel connected devices 2026-06-08 17:20:59 +02:00
David Heinemeier Hansson 42a0b25e01 Merge branch 'omarchy-shell' into quattro 2026-06-08 16:56:51 +02:00
David Heinemeier Hansson 8e65151fb8 Show the whole disk 2026-06-08 16:44:17 +02:00
David Heinemeier Hansson baff7f8080 Make user for now 2026-06-08 16:41:56 +02:00
David Heinemeier Hansson 01bdd06e10 No more elephant 2026-06-08 16:40:54 +02:00
David Heinemeier Hansson cbc7c8f30a Use dua 2026-06-08 16:38:17 +02:00
David Heinemeier Hansson 8807a9c889 Merge branch 'dev' into quattro 2026-06-08 16:36:51 +02:00
Carmine Paolino 9cf1852525 Use dua for disk usage (#6052) 2026-06-08 16:36:05 +02:00
David Heinemeier Hansson 6cec42d97d Merge branch 'omarchy-4' into quattro 2026-06-08 16:28:52 +02:00
Ryan Hughes 17f024d481 Make dev unlink reset Omarchy path defensively 2026-06-07 16:35:54 -04:00
David Heinemeier Hansson 3d4bb9ab81 Add splitters to all panels... for now 2026-06-07 20:58:02 +02:00
Ryan Hughes 5178405d19 Merge remote-tracking branch 'origin/omarchy-shell' into omarchy-4 2026-06-07 14:56:08 -04:00
Ryan Hughes 8baae47d48 Fix wifi frequency displays 2026-06-07 14:55:21 -04:00
Ryan Hughes 132e67bf4c Default pkg-test to dev packages 2026-06-06 20:36:21 -04:00
Ryan Hughes fc6feedae5 Load Omarchy fontconfig after generic aliases 2026-06-06 19:31:51 -04:00
Ryan Hughes d2c1544d78 Fix sleep lock service during Omarchy 4 upgrade 2026-06-06 19:31:51 -04:00
Ryan Hughes 4c17f6a675 Merge remote-tracking branch 'origin/omarchy-shell' into omarchy-4
# Conflicts:
#	bin/omarchy-debug
#	bin/omarchy-upload-log
2026-06-06 13:28:47 -04:00
Ryan Hughes 92246a50fb Enable plugin hot reloading for add / remove 2026-06-06 13:25:54 -04:00
Ryan Hughes 42bdf374dd Use Omarchy log upload endpoint 2026-06-06 13:25:54 -04:00
David Heinemeier Hansson 740f4e2c82 Fix power panel discharge state 2026-06-06 18:01:14 +02:00
David Heinemeier Hansson b83505d738 More small adjustments 2026-06-06 17:21:09 +02:00
David Heinemeier Hansson 43e758ed32 Align network panel details 2026-06-06 17:08:02 +02:00
David Heinemeier Hansson 166b395203 Polish bar icon spacing 2026-06-06 16:43:00 +02:00
David Heinemeier Hansson d7d08d1b17 Improve Tailscale panel controls 2026-06-06 15:48:56 +02:00
Ryan Hughes 2c44fdf5f3 Merge remote-tracking branch 'origin/omarchy-shell' into omarchy-4 2026-06-05 18:35:34 -04:00
Ryan Hughes 1409f85745 Make the bar more easily swapable 2026-06-05 16:49:30 -04:00
Ryan Hughes e5516d882d Simplify system update popover 2026-06-04 19:23:54 -04:00
Ryan Hughes 92de15214c Reconcile omarchy-shell rebase 2026-06-04 18:40:57 -04:00
Ryan Hughes cb3fc79385 Add dev package mode to Omarchy 4 upgrade 2026-06-04 18:38:25 -04:00
Ryan Hughes ddd7e058a2 Auto-mount removable drives by default with udiskie 2026-06-04 18:38:25 -04:00
Ryan Hughes 23f8418ed6 Move boot drop-ins into Omarchy repo 2026-06-04 18:38:25 -04:00
Ryan Hughes 20d79fe443 Improve update widget 2026-06-04 18:38:25 -04:00
Ryan Hughes d0cac7346a Finish v4 upgrade cleanup 2026-06-04 18:38:25 -04:00
Ryan Hughes 0804962619 Improve update and migration flow 2026-06-04 18:38:25 -04:00
Ryan Hughes c8ed1e77ae Fix Chromium copy URL extension path on v4 upgrade 2026-06-04 18:38:25 -04:00
Ryan Hughes 3f5b92b413 upgrade-to-4: inject [omarchy-dev] above [omarchy] in pacman.conf
TEMPORARY: while the omarchy-4 stack is being iterated, the production
edge/stable/rc channels at pkgs.omarchy.org/<ring>/x86_64 don't have the
new omarchy and omarchy-settings packages — only omarchy-keyring and
omarchy-nvim are published. The work-in-progress packages live in the
dev repo at share.heyoodle.com/omarchy/dev/<arch> via
omarchy-pkgs/bin/publish-dev-repo.

To stop hand-editing pacman.conf after every run of this script,
configure_pacman_channel() now emits a [omarchy-dev] block right above
[omarchy] with SigLevel = Never and the share.heyoodle URL. Pacman
honors repo declaration order, so dev packages take precedence when
both ring publish them.

The awk also dedupes any pre-existing [omarchy-dev] block (so rerunning
the script doesn't pile up duplicate entries) and falls back to
appending both blocks at the end if no [omarchy] section was present.

Revert this block once the production rings have caught up.
2026-06-04 18:38:25 -04:00
Ryan Hughes 6f73eb5d9d upgrade-to-4: drop omarchy-limine from core_packages install list
omarchy-limine no longer exists as a separate package — it was folded
into omarchy-settings (configs) and omarchy (bootloader stack hard dep).
Listing it in install_omarchy_4_packages's pacman -Syu invocation
produced 'error: target not found: omarchy-limine' on every run.

Also fix the now-stale 'shipped by omarchy-settings / omarchy-limine'
comment in the retire-action header (line 1119) — the files come from
omarchy-settings alone now.

The bootloader stack (limine, limine-mkinitcpio-hook, limine-snapper-sync,
snapper) is hard-depended by the omarchy package, so dropping
omarchy-limine from the explicit list doesn't change what gets installed
on a real upgrade.
2026-06-04 18:38:25 -04:00
Ryan Hughes 4e10d41f0e Start user units immediately in first-run, not just enable
systemctl --user enable only writes the .wants symlinks; the services
don't actually run until next login. Switching to --now enables AND
starts them, so the first session has bt-agent (bluetooth pairing) and
omarchy-sleep-lock active immediately rather than waiting a reboot.

omarchy-recover-internal-monitor is Type=oneshot with a
ConditionPathExists guard, so --now is harmless for it (it'll skip if
the toggle isn't set).
2026-06-04 18:38:25 -04:00
Ryan Hughes aa12358201 Update file layout for the two-package architecture
Reflects the two upstream changes in this work:
- omarchy-dev-tools folds into omarchy (bin/omarchy-dev-* ship with the
  main package).
- omarchy-limine folds into omarchy-settings (limine + snapper configs,
  mkinitcpio hooks, and the default/{limine,snapper}/ trees ship from
  omarchy-settings; the package was always installed in lockstep with
  it in the early bootstrap anyway).

Mental-model section: from four packages to two (omarchy +
omarchy-settings), plus the two unchanged independents (omarchy-keyring,
omarchy-nvim).

Build-time map: drop the omarchy-dev-tools and omarchy-limine columns;
add the new limine source files (omarchy_hooks.conf,
thunderbolt_module.conf, omarchy-defaults.conf, omarchy-uki.conf) under
omarchy-settings.
2026-06-04 18:38:25 -04:00
Ryan Hughes 041f280f68 Document env-bootstrap and first-run in file layout
- Add default/bash/env-bootstrap to the build-time map and a dedicated
  section describing what it sets, who sources it, and the dev-link gate
  on the PATH prepend.
- Drop 'bluetooth' from the install/user/all.sh hardware-quirks list
  (the per-hardware install script is gone now that the wireplumber
  config ships through /etc/skel and bt-agent enables at first-run).
- Add a first-run section listing the post-login steps, including the
  new enable-user-units.sh that defers user systemctl enables.
2026-06-04 18:38:25 -04:00
Ryan Hughes babfafa5e9 Split user defaults into skel seed, finalize, and resync
Reorganizes Omarchy 4 around three layers for populating $HOME:

  Seed:     omarchy-settings ships defaults to /etc/skel; useradd -m
            copies them on user creation
  Finalize: omarchy-finalize-user (renamed from omarchy-setup-user)
            handles only the runtime tweaks /etc/skel can't do — skill
            symlinks, xdg-user-dirs, default browser/mailto, vconsole→hypr
            keyboard sync, and install/user/all.sh
  Resync:   omarchy-reinstall-configs is the explicit, destructive
            resync of /etc/skel into an existing user's $HOME

Package-owned files move out of config/ into default/, where the
omarchy-settings PKGBUILD installs them to real system paths:

  config/environment.d/fcitx.conf            -> /usr/lib/environment.d/
  config/fontconfig/fonts.conf               -> /usr/share/fontconfig/conf.avail/
  config/mimeapps.list                       -> /usr/share/applications/
  config/omarchy.ttf                         -> /usr/share/fonts/omarchy/
  config/systemd/user/*.service              -> /usr/lib/systemd/user/
  config/uwsm/default                        -> /usr/share/omarchy/default/uwsm/
  config/uwsm/env                            -> /usr/share/uwsm/env.d/10-omarchy
  config/xdg-terminals.list                  -> /usr/share/xdg-terminal-exec/

omarchy-upgrade-to-4 grows a 'retire' action (renamed from 'move' to
clarify nothing is copied — the system path is owned by the new package
once the user's hash-matched ~/.config copy is removed). Mismatched
copies are kept as backups so user overrides survive the upgrade.

Other simplifications:

  - Single env bootstrap at default/bash/env-bootstrap sourced by
    /etc/profile.d/omarchy.sh, /etc/skel/.bashrc,
    /usr/share/uwsm/env.d/10-omarchy, and default/bash/envs. PATH
    prepend only in dev-link mode (production uses /usr/bin/omarchy-*).
  - omarchy-refresh-config reads from /etc/skel/.config so refresh
    means 'snap to skel'.
  - omarchy-reinstall-configs collapses to 'cp -af /etc/skel/. ~/'
    plus limine/plymouth/nvim refresh.
  - omarchy-font-set uses awk against our own 30-omarchy.conf instead
    of xmlstarlet; xmlstarlet dropped from omarchy-base.packages.
  - Defer user systemd enables (bt-agent, sleep-lock,
    recover-internal-monitor) to first-run via
    install/user/first-run/enable-user-units.sh; delete
    omarchy-user-systemctl-enable and the per-hardware install
    scripts that called it.
  - Wireplumber bluetooth-a2dp-autoconnect.conf moves to config/ so
    /etc/skel ships it; install/user/hardware/bluetooth.sh deleted.
  - Default terminal switched to foot.desktop.
  - docs/file-layout.md documents the three-layer model and the
    build-time repo→path map.
2026-06-04 18:38:25 -04:00
Ryan Hughes 85da6af886 Make retired package cleanup dependency-aware 2026-06-04 18:38:25 -04:00
Ryan Hughes 5214bfd1ad Harden Omarchy 4 live upgrade cutover 2026-06-04 18:38:25 -04:00
Ryan Hughes f1bdec3e67 add cleanup 2026-06-04 18:38:25 -04:00
Ryan Hughes e707ca116f Remove retired upgrade packages together 2026-06-04 18:38:25 -04:00
Ryan Hughes 85bf7f108d Harden Omarchy 4 upgrade and first-run retry 2026-06-04 18:38:25 -04:00
Ryan Hughes 06db866467 Prepare installer for ISO-owned finalization 2026-06-04 18:38:25 -04:00
Ryan Hughes bcf07e424a Add Omarchy 4 upgrade bridge
Introduce a self-contained omarchy-upgrade-to-4 command for legacy installs and remove the 4.0 package-layout transition from the normal migration runner.
2026-06-04 18:37:32 -04:00
Ryan Hughes 75cb4f7195 Make setup ISO-only
Remove legacy online installer entrypoints, collapse migrations for 4.0, and move setup responsibilities into target-side system, hardware, and user commands.
2026-06-04 18:37:32 -04:00
Ryan Hughes b45e8464ef Move NVIDIA env setup to Hyprland Lua
Evaluate NVIDIA session env vars from default/hypr/nvidia.lua during Hyprland startup instead of appending them to each user's envs.lua during setup. This removes the per-user NVIDIA setup script execution while preserving the GSP/non-GSP detection.
2026-06-04 18:35:01 -04:00
Ryan Hughes 27f6d9d364 migrations: keep system helpers sudo-compatible 2026-06-04 18:35:01 -04:00
Ryan Hughes bf14f69017 installer: require target user for system finalizer 2026-06-04 18:35:01 -04:00
Ryan Hughes 67c401c125 docs: refresh SDDM crosswalk 2026-06-04 18:35:01 -04:00
Ryan Hughes 487fcb8038 install: rely on packaged SDDM assets 2026-06-04 18:35:01 -04:00
Ryan Hughes 844278ad19 docs: add installer review crosswalk 2026-06-04 18:35:01 -04:00
Ryan Hughes acdb1d1b57 install: split SDDM static config from autologin 2026-06-04 18:35:01 -04:00
Ryan Hughes d12d449b03 install: split and harden hardware config scripts 2026-06-04 18:35:01 -04:00
Ryan Hughes 6056f8800c install: drop helpers replaced by package-owned config 2026-06-04 18:35:01 -04:00
Ryan Hughes f451b504eb install: route theme setup through split scripts 2026-06-04 18:35:01 -04:00
Ryan Hughes e393a3a836 install: centralize service enablement 2026-06-04 18:35:01 -04:00
Ryan Hughes 7ef5bc56fd install: make system scripts root-finalizer aware 2026-06-04 18:35:01 -04:00
Ryan Hughes 0cfa1b66c4 installer: route install through system and user phases 2026-06-04 18:35:01 -04:00
Ryan Hughes 53e2611507 install: add split system and user targets 2026-06-04 18:35:01 -04:00
Ryan Hughes b29747a54b installer: reduce offline finalizer work 2026-06-04 18:35:01 -04:00
Ryan Hughes 724f46f145 installer: trace finalizer in debug builds 2026-06-04 18:35:01 -04:00
Ryan Hughes b5ed8cad4d installer: slim finalizer bootstrap 2026-06-04 18:35:01 -04:00
Ryan Hughes 96a020f756 installer: tolerate missing tty in offline finalizer 2026-06-04 18:35:01 -04:00
Ryan Hughes e67128e4c9 installer: keep chroot log output plain 2026-06-04 18:35:01 -04:00
Ryan Hughes 250799af81 installer: tolerate missing archinstall duration 2026-06-04 18:35:01 -04:00
Ryan Hughes 41b2fa0174 installer: preserve error menu exit status 2026-06-04 18:35:01 -04:00
Ryan Hughes e01c233431 installer: keep offline finalizer non-interactive 2026-06-04 18:35:01 -04:00
Ryan Hughes 830c798bde installer: honor configured Limine ESP path 2026-06-04 18:35:01 -04:00
Ryan Hughes 9feab5081f installer: preserve explicit exit status in exit trap 2026-06-04 18:35:01 -04:00
Ryan Hughes 2bf3315719 installer: avoid gsettings in chrooted installs 2026-06-04 18:35:01 -04:00
Ryan Hughes 74aaff5ed3 installer: calculate install duration from epoch 2026-06-04 18:34:35 -04:00
Ryan Hughes d6cdefcf6b installer: recover install start time from log 2026-06-04 18:34:35 -04:00
Ryan Hughes ddee039cd2 installer: leave offline snapper setup to orchestrator 2026-06-04 18:34:35 -04:00
Ryan Hughes a1a15ef663 installer: configure snapper without dbus in offline finalizer 2026-06-04 18:34:35 -04:00
Ryan Hughes 88f3181124 installer: split runtime and installer paths in finalizer 2026-06-04 18:34:35 -04:00
Ryan Hughes 58f03a9216 installer: preserve exit code in error handler 2026-06-04 18:34:35 -04:00
Ryan Hughes bd9692d6e3 omarchy-upload-log: avoid hostname dependency 2026-06-04 18:34:35 -04:00
Ryan Hughes f2821bb657 limine-snapper.sh: validate parsed CMDLINE in pre-populated branch
The harvest branch already enforces non-empty cmdline + presence of
root=. The pre-populated branch (added in 0d2688a5) was missing the
same validation, so a malformed /etc/default/limine could slip
through and the later cryptdevice= check would silently no-op
because $CMDLINE was empty.
2026-06-04 18:34:35 -04:00
Ryan Hughes e9aa8835ee limine-snapper.sh: skip harvest dance when /etc/default/limine is pre-populated
The ISO orchestrator now writes /etc/default/limine directly from the
install config (with the real cmdline already substituted) before the
chroot phase runs. The previous unconditional find+harvest+copy+sed
sequence would (a) find our orchestrator-staged /boot/limine.conf which
has no `cmdline:` line and (b) exit 1 on "failed to extract kernel
cmdline".

Skip the harvest entirely when /etc/default/limine already exists
without the @@CMDLINE@@ placeholder. Parse CMDLINE from the existing
file so the downstream cryptdevice= validation still has it to compare.

The harvest path remains as a fallback for online reinstalls and any
flow where /etc/default/limine wasn't pre-populated by the orchestrator.
2026-06-04 18:34:35 -04:00
Ryan Hughes 9800b5b73b Drop orchestrator from omarchy-installer (moved to omarchy-iso)
The orchestrator only runs on the live ISO, so it lives in omarchy-iso
where the rest of the ISO-specific code (configurator, .automated_script
.sh, archinstall pacman config) lives. omarchy-installer stays the
monorepo of Omarchy source content (configs, themes, default files,
install scripts) without a Python install driver mixed in.

bin/omarchy-install simplifies to a one-liner: it's the online entry
point now, no --config dispatcher. The orchestrator never invokes it;
it runs finalize.sh directly via arch-chroot.
2026-06-04 18:34:35 -04:00
Ryan Hughes c6ccd62550 Wire archinstall adapter + concrete phase implementations
Chunks 2-6 of the C3 refactor. With the adapter in place, the phase
list collapses from 12 stubs to 5 real phases:

  prepare_live          parse user_*.json into archinstall handlers
  arch_install          partition + base + bootloader + limine config +
                        early omarchy + useradd + runtime omarchy (all
                        inside a single Installer context manager)
  run_chroot_finalizer  arch-chroot -u $user → finalize.sh
  validate_boot         halt if /boot/limine.conf, /etc/kernel/cmdline,
                        or UKI are missing/malformed
  finish                reboot prompt

Key reordering vs upstream guided.py: write_limine_config runs BETWEEN
add_bootloader and the first add_additional_packages call. By the time
omarchy-limine pulls limine-mkinitcpio-hook (in the runtime package
install), /etc/default/limine and /etc/kernel/cmdline are in place, so
the UKI is built correctly on the first try — no stale-UKI purge,
no follow-up limine-update.

add_additional_packages is split into two calls so /etc/skel is
populated (via omarchy-settings in EARLY_PACKAGES) BEFORE useradd:

  EARLY_PACKAGES = base-devel git omarchy-keyring omarchy-settings omarchy-installer
  → installer.create_users(users)
  → runtime: omarchy + omarchy-base.packages

archinstall_adapter.py is the ONLY module that imports from archinstall.
If archinstall's API churns, the blast radius is contained here. Tested
against archinstall 4.3 / Python 3.14.

Limine template lookup tries the new install/assets/limine/ path first
and falls back to the legacy default/limine/ location during the
template-ownership migration (lands in a follow-up chunk).
2026-06-04 18:34:35 -04:00
Ryan Hughes df645a202d Simplify finalize.sh and install.sh's preflight ownership
finalize.sh just does the in-target work: env setup, source helpers,
mark current migrations as done (idempotent), run packaging/config/
login/post-install. No preflight orchestration, no install_mode_is
offline branches.

install.sh (online) inlines the preflight bits that are online-specific:
guard.sh, begin.sh, show-env.sh. The first-run-mode setup that used to
live in preflight (offline-only) moves to an orchestrator phase in a
later chunk; disable-mkinitcpio.sh is obsolete entirely because the C3
phase ordering means the limine UKI hook fires exactly once with
correct config in place.

install/preflight/{all,disable-mkinitcpio,first-run-mode}.sh still
exist on disk but are no longer sourced. They'll get removed in a
later chunk once the orchestrator absorbs first-run-mode.
2026-06-04 18:34:35 -04:00
Ryan Hughes 2beda6abed Scaffold Python orchestrator + split install.sh / finalize.sh
Chunk 1 of the C3 refactor: replace the two-pass install model (archinstall
CLI + custom shell dance + chroot bash installer) with a single Python
orchestrator that owns phase ordering, using archinstall as a library
subsystem.

Splits the bash entry point into two:
- install.sh: online entry point. Ensures omarchy runtime + base.packages
  are installed/up to date, then exec's finalize.sh.
- finalize.sh: the in-target portion (preflight + packaging + config +
  login + post-install). Called by install.sh (online) AND by the
  orchestrator after arch-chroot -u $USER (offline).

Adds the orchestrator skeleton under install/orchestrator/:
- main.py: entry point, builds + runs the phase list
- context.py: InstallContext (parsed configurator JSON + invocation paths)
- phases.py: phase state machine (logging + state.json + error wrapping)
- phases_impl.py: stubbed phase implementations (filled in by chunks 2-6)
- archinstall_adapter.py: thin compat wall around archinstall lib imports
  (only this module imports from archinstall.*)
- ui.py: gum subprocess wrappers so the orchestrator keeps the same
  styled-terminal UX as the bash installer

Updates bin/omarchy-install to dispatch:
- --config <json> in args → python -m orchestrator.main (ISO install)
- anything else → bash install.sh (online rerun on installed system)

Concrete phase logic lands in subsequent chunks. All phases currently
raise NotImplementedError; the orchestrator imports cleanly and --help
works as a smoke check.
2026-06-04 18:34:35 -04:00
Ryan Hughes d0ec7c481f Add qmk-hid to omarchy-other.packages
install/config/hardware/framework/qmk-hid.sh installs the qmk-hid
package on Framework 16 hardware, but it wasn't listed in any of the
ISO builder's package sources, so it wasn't being pulled into the
offline mirror. Adding it under the Framework 16 section in
omarchy-other.packages so the ISO build picks it up.
2026-06-04 18:34:35 -04:00
Ryan Hughes cd05c69820 omarchy-upload-log: find omarchy install log under /mnt in ISO context
The omarchy install runs inside an arch-chroot to /mnt, so its log file
(/var/log/omarchy-install.log from the installer's perspective) is at
/mnt/var/log/omarchy-install.log from the live ISO. The previous script
only read /var/log/omarchy-install.log, and 'cat $ARCHINSTALL_LOG
$OMARCHY_LOG 2>/dev/null' swallowed the resulting 'No such file' error,
so uploaded logs from the ISO contained only the archinstall portion.

Prefer the /mnt path when it exists (live ISO post-install), fall back
to /var/log when running on the installed system. Also print a visible
separator between the two log sections and warn on stderr if the omarchy
log is missing entirely (instead of silently producing an incomplete
upload).
2026-06-04 18:34:35 -04:00
Ryan Hughes 99de786ce1 Drop install/first-run/gdk-scale.sh
The lua-based monitors config (~/.config/hypr/monitors.lua) ships from
skel with sensible defaults (omarchy_gdk_scale = 2, omarchy_monitor_scale
= "auto"). Hyprland handles the dynamic monitor scale at runtime via
"auto"; 'omarchy hyprland monitor scaling <N>' already updates both
omarchy_monitor_scale and omarchy_gdk_scale in monitors.lua when the
user wants to adjust.

The first-run 'detect what hyprland picked and write it back' dance
was leftover from the .conf-based config; it's redundant on the lua
flow and was the script that aborted omarchy-first-run under set -e
when it sed'd a missing monitors.conf.
2026-06-04 18:34:35 -04:00
Ryan Hughes 0dd91106e7 omarchy-first-run: run setup-user first; fix gdk-scale.sh for lua config
Two bugs caused fresh users to land without branding files / AI skill
symlinks / nautilus extensions / xdg dirs / etc.:

1. install/first-run/gdk-scale.sh sed-edited ~/.config/hypr/monitors.conf,
   but the lua-based config flow uses ~/.config/hypr/monitors.lua. sed -i
   on the missing .conf returned non-zero, set -e fired, omarchy-first-run
   aborted mid-block AFTER deleting ~/.local/state/omarchy/first-run.mode.
   omarchy-setup-user (called at the bottom of the script, outside the
   if-block) never ran, so setup-user.done never got written and the
   per-user defaults (~/.config/omarchy/branding/*, ~/.agents/skills/omarchy,
   xdg dirs, etc.) never got created.

   Replace the .conf sed with a call to omarchy-hyprland-monitor-scaling
   which already knows how to persist into monitors.lua.

2. Move 'omarchy-setup-user || true' to the TOP of omarchy-first-run, before
   the privileged first-run block. setup-user is idempotent and gated on
   its own marker, so running it first is safe and means a failure in any
   of the privileged steps can't prevent user setup from completing.
2026-06-04 18:34:35 -04:00
Ryan Hughes 71659cd42b Switch from 1password-beta back to 1password
The edge mirror temporarily lacked a current 1password build (315e60d
hadn't been rebuilt), so install/omarchy-base.packages was pointing at
1password-beta as a workaround. Reverting now that the stable package
is available, and matching the entry in omarchy-remove-preinstalls so
that bin actually drops the package we installed.
2026-06-04 18:34:35 -04:00
Ryan Hughes a086b03dd1 Replace ~/.local/share/omarchy runtime paths with $OMARCHY_PATH
The git-era flow installed Omarchy to ~/.local/share/omarchy. In the
package-based flow it lives at /usr/share/omarchy (or, in dev mode, at
the path /etc/omarchy.conf's OMARCHY_PATH points at). Files that hard-
coded the old path broke on package installs:

- default/bash/rc: sourced ~/.local/share/omarchy/default/bash/*, so a
  fresh user's terminal logged ENOENT for every line. Now sources from
  $OMARCHY_PATH with /usr/share/omarchy as the safe default if the var
  is unset.

- config/systemd/user/omarchy-recover-internal-monitor.service:
  ExecStart pointed at %h/.local/share/omarchy/bin/. The script ships
  to /usr/bin via the omarchy-installer package; point straight there.

- install/config/xcompose.sh: wrote an XCompose include line pointing
  at the user-home path; use /usr/share/omarchy/ directly.

- default/hypr/paths.lua and config/hypr/hyprland.lua: fallback when
  OMARCHY_PATH is unset is /usr/share/omarchy, not the home path.

Dev-only scripts (omarchy-dev-add-migration, omarchy-reinstall-git) and
the SKILL.md docs still reference the old path; those are separate
cleanups since they don't affect a fresh install's runtime.
2026-06-04 18:34:35 -04:00
Ryan Hughes 612f947e1d limine-snapper.sh: move validation after snapper + hook re-enable
Per apollo audit: the /boot/limine.conf assertion grep was firing
BEFORE snapper config setup, btrfs quota disable, service enable, and
the mkinitcpio pacman hook re-enable. A validation failure (the failure
path the validation exists for) would leave the system half-configured.

Move the validation block to after 'mkinitcpio hooks re-enabled' so all
the post-config steps run first. Guard it with LIMINE_CONFIGURED=true so
non-limine setups still pass straight through to efibootmgr cleanup.
2026-06-04 18:34:35 -04:00
Ryan Hughes caa47c7595 limine-snapper.sh: use limine-update + purge stale UKIs
mkinitcpio -P alone doesn't trigger limine's UKI pipeline. The
limine-mkinitcpio-hook package installs /etc/pacman.d/hooks/90-mkinitcpio
-install.hook (overriding mkinitcpio's normal hook) which executes
/usr/share/libalpm/scripts/limine-mkinitcpio-install only on pacman
transactions, OR via the limine-mkinitcpio / limine-update CLI wrappers.

The previous fix (mkinitcpio -P) rebuilt initramfs-linux.img but left
the stale empty-cmdline UKI at /boot/EFI/Linux/<machine-id>_linux.efi
untouched. limine-update then booted that file.

Changes:
- Validate the cmdline extracted from /boot/limine.conf: fail loudly if
  empty or missing root=
- Delete every alternate /boot/limine.conf so limine-update can't pick
  the wrong one
- Delete stale UKIs for every installed kernel pkgbase before rebuilding
- Replace 'mkinitcpio -P' with 'limine-update' (which does
  limine-install + limine-mkinitcpio, reading KERNEL_CMDLINE from
  /etc/default/limine via limine-entry-tool and embedding it into a
  fresh UKI)
- Assert /boot/limine.conf has the Omarchy entry and (for encrypted
  installs) the cryptdevice= cmdline before continuing
2026-06-04 18:34:35 -04:00
Ryan Hughes 53a07811c8 limine-snapper.sh: write /etc/kernel/cmdline and rebuild UKI
In the package-based install, omarchy-limine is a hard dep of omarchy,
so archinstall pacstraps limine-mkinitcpio-hook (via omarchy-limine's
depends) early. That triggers a UKI build BEFORE limine-snapper.sh
runs, which means the UKI is built with no kernel cmdline (the
'Kernel command line is not available' warning in the archinstall log).
The system then can't unlock the LUKS root on boot because the encrypt
hook has no cryptdevice= to act on, and drops to an emergency shell.

The previous flow worked because 'pacman -S limine-mkinitcpio-hook'
inside limine-snapper.sh installed the hook for the first time, firing
its post-transaction UKI rebuild AFTER /etc/default/limine had been
written. With it pre-installed, '--needed' makes that line a no-op.

Fix: after writing /etc/default/limine, write the same cmdline to
/etc/kernel/cmdline (the source mkinitcpio --uki reads) and explicitly
run mkinitcpio -P to rebuild the UKIs with the correct cmdline.
2026-06-04 18:34:35 -04:00
Ryan Hughes a481c17846 Drop install/packaging/fonts.sh, fix omarchy-setup-lock exit code
fonts.sh: obsolete. omarchy-settings installs omarchy.ttf to
/usr/share/fonts/omarchy/ system-wide and the fontconfig package's
post-install hook updates the cache, so copying it to ~/.local/share/
fonts and re-running fc-cache is redundant. The script was also failing
because omarchy-settings explicitly removes the source path
/usr/share/omarchy/config/omarchy.ttf during package install.

omarchy-setup-lock: the final line was 'omarchy-shell lock status
>/dev/null && echo "..."' which inherits the failed exit code when
omarchy-shell can't reach a running shell (always the case during chroot
install). set -e exempts cmd1 of && from triggering, but the script
still returns that non-zero as its own exit code, which the install
wrapper treated as a script failure. Wrap in an if/then so a failed
status check doesn't poison the script's exit code.
2026-06-04 18:34:35 -04:00
Ryan Hughes ed06892970 Switch log uploader from 0x0.st to logs.omarchy.org
0x0.st has been intermittently down. logs.omarchy.org accepts the same
curl -F 'file=@-' POST and returns the resulting URL in the response
body, so the swap is purely the endpoint. Dropping the -Fexpires=24
parameter since logs.omarchy.org doesn't honour it (and doesn't expire
uploads by default).
2026-06-04 18:34:35 -04:00
Ryan Hughes ad5c942725 install.sh: pacman -S omarchy-base.packages alongside omarchy meta
omarchy no longer hard-depends on the full install set; ensure_omarchy_runtime
now installs everything in install/omarchy-base.packages in addition to
the omarchy meta. Offline mode's assertion stays the same (the ISO does
the equivalent via .automated_script.sh).
2026-06-04 18:34:35 -04:00
Ryan Hughes 8e05d4d92a Move full-runtime dep to omarchy meta; thin omarchy-installer
Apollo-recommended dep split:
- omarchy-installer: bash gum jq git only (just the runner)
- omarchy: depends on omarchy-keyring + omarchy-settings + omarchy-limine
  + the runtime stack
- omarchy-settings, omarchy-limine: standalone

install.sh now installs the runtime (pacman -Syu --needed omarchy) before
sourcing preflight/all.sh in online mode, and asserts it's already
present in offline mode (ISO pacstraps it before user creation so
/etc/skel is populated).

boot.sh installs only omarchy-keyring + omarchy-installer (small + fast),
then exec's omarchy-install which handles the full-runtime pull.

User-visible effect: 'pacman -S omarchy-installer' now installs ~5 files
instead of the entire Omarchy stack. Users get a clean choice between
'I just want the installer' and 'I want the full Omarchy desktop'.
2026-06-04 18:34:35 -04:00
Ryan Hughes f852edfbd6 Split user-level setup into bin/omarchy-setup-user
User-level files (~/.config, ~/.local/share, ~/.agents, etc.) shouldn't
be written by install.sh — it runs as root or with system-level sudo,
and in offline (ISO chroot) mode the user doesn't exist yet. Moved into
a per-user idempotent command, called from omarchy-first-run.

Moved into bin/omarchy-setup-user (deleted from install/config/):
- omarchy-ai-skill.sh    AI skill symlinks
- omarchy-toggles.sh     hypr toggle flags
- nautilus-python.sh     nautilus extensions
- branding.sh            ~/.config/omarchy/branding/{about,screensaver}.txt
- user-dirs.sh           xdg-user-dirs-update + gtk bookmarks
- detect-keyboard-layout.sh  layout/variant sed into ~/.config/hypr/input.lua
- toggles.sh             ~/.local/state/omarchy/toggles mkdir
- mimetypes.sh           omarchy-refresh-applications + xdg-mime/xdg-settings

Kept in install/config/ (rely on OMARCHY_USER_NAME / OMARCHY_USER_EMAIL
that only exist during install):
- git.sh        git config --global user.{name,email}
- xcompose.sh   ~/.XCompose with name/email substituted

omarchy-setup-user:
- Refuses to run as root
- Idempotent via ~/.local/state/omarchy/setup-user.done marker
- --force escape hatch for re-running after dev-link or upstream changes
- Defaults OMARCHY_PATH to /usr/share/omarchy so it works outside install

bin/omarchy-first-run now invokes omarchy-setup-user unconditionally
(it self-gates), so first user login picks up the user setup.

install/config/all.sh: dropped the eight deleted entries.

All three packages (omarchy, omarchy-settings, omarchy-installer) still
build clean.
2026-06-04 18:34:35 -04:00
Ryan Hughes c26bce6235 Rewrite boot.sh as a thin bootstrap for the package install flow
Old boot.sh cloned the repo to ~/.local/share/omarchy and sourced the
checkout's install.sh. The package refactor moves all of that into the
omarchy-installer package, so boot.sh's job shrinks to:

  1. Configure /etc/pacman.d/omarchy.conf for the right channel
     (stable, edge for OMARCHY_REF=dev, rc for OMARCHY_REF=rc).
     Include it from /etc/pacman.conf if not already.
  2. pacman -Sy && pacman -S --needed omarchy-installer (depends pull in
     omarchy, omarchy-settings, omarchy-limine).
  3. exec omarchy-install (shipped by omarchy-installer at /usr/bin/).

The OMARCHY_REPO and clone logic are gone. The install.sh on disk
under /usr/share/omarchy is now the source of truth, and is invoked via
omarchy-install with OMARCHY_INSTALL_MODE=online (default).

OMARCHY_REF=dev/rc still maps to the right pacman channel for testing
pre-release builds.
2026-06-04 18:34:35 -04:00
Ryan Hughes 26b9ce5f96 Add bin/omarchy-install wrapper (shipped via omarchy-installer)
Brings back the omarchy-install command as a 5-line shell wrapper that
just exec's /usr/share/omarchy/install.sh. Not the 258-line Python
refactor-poc version — that one stays dropped.

The omarchy package keeps excluding bin/omarchy-install exactly so it
doesn't conflict with the omarchy-installer package's copy at /usr/bin.
All 25+ user-facing omarchy-install-* commands (gaming, browser, etc.)
still ship via omarchy.
2026-06-04 18:34:35 -04:00
Ryan Hughes 7fffc5f3c4 Simplify OMARCHY_INSTALL_MODE to online/offline only
Drop online-git: the new world has a single package-backed install path
that's either online (pacman pulls from omacom-pkgs.org) or offline (ISO
chroot pulls from the bundled mirror). The git-clone-and-run-installer
path is gone — boot.sh becomes a thin bootstrap that installs the
omarchy-installer package and invokes its installer.

- install/helpers/mode.sh: two modes, validation message updated. Default
  fallback is 'online' (the boot.sh path); OMARCHY_CHROOT_INSTALL=1 still
  shims to offline, OMARCHY_ONLINE_INSTALL=true still shims to online.
- install/preflight/all.sh: first-run-mode + disable-mkinitcpio gated on
  offline (offline is the from-scratch chroot path; online runs on an
  existing system).
- install/preflight/pacman.sh: DELETED. boot.sh now handles repo config
  before install.sh runs.
- install/packaging/all.sh: drop base.sh entirely. Both modes have
  base packages installed via omarchy-installer's depends before
  install.sh runs. base.sh remains on disk in case the ISO mirror build
  flow wants to invoke it directly.
- install/helpers/chroot.sh, errors.sh, post-install/finished.sh,
  config/mise-work.sh: rename mode checks to use online/offline.
2026-06-04 18:34:35 -04:00
Ryan Hughes 81e81fe0c8 Address momus review of OMARCHY_INSTALL_MODE work
- install.sh: use realpath to follow symlinks when deriving OMARCHY_PATH
  from script location. Logical pwd left dev-link symlink chains pointing
  at the wrong dir; realpath resolves to the underlying checkout.
- install/helpers/mode.sh: validate explicit OMARCHY_INSTALL_MODE values
  and exit on typos. Also export_legacy_mode_flags now UNSETS the
  contradictory legacy flag for each mode so unmigrated callers can't
  see both OMARCHY_CHROOT_INSTALL=1 and OMARCHY_ONLINE_INSTALL=true.
- install/helpers/errors.sh: retry uses "$OMARCHY_PATH/install.sh"
  and preserves OMARCHY_INSTALL_MODE explicitly; the previous hardcoded
  ~/.local/share/omarchy/install.sh broke package-mode retries.
- bin/omarchy-install-mode: soften the summary; the helper only knows
  the current env, not any persisted post-install state.
2026-06-04 18:34:35 -04:00
Ryan Hughes 6f74091fb6 Make install.sh mode-aware with OMARCHY_INSTALL_MODE
Three modes the installer now branches on:

  iso-chroot      Running inside the new system's chroot from the ISO build.
                  pacman is configured by archinstall; systemd isn't running,
                  so service enables skip --now.
  online-package  Running on an existing Arch where the omarchy-* packages
                  are already installed. Skip base.sh and the bootstrap
                  preflight steps; only the per-host runtime ops in
                  config/login/post-install need to run.
  online-git      boot.sh path: clone into $HOME/.local/share/omarchy and
                  bootstrap everything from scratch.

install.sh derives OMARCHY_PATH from its own location, so the same script
works whether it's at /usr/share/omarchy/install.sh (package mode) or
$HOME/.local/share/omarchy/install.sh (git mode). install_mode_is helper
replaces direct ${OMARCHY_CHROOT_INSTALL:-} / ${OMARCHY_ONLINE_INSTALL:-}
checks in chroot.sh, finished.sh, errors.sh, mise-work.sh, preflight/pacman.sh.

Legacy vars still work through export_legacy_mode_flags, which sets
OMARCHY_CHROOT_INSTALL=1 / OMARCHY_ONLINE_INSTALL=true based on the
canonical mode for any caller that hasn't been updated.

bin/omarchy-install-mode prints the current mode for scripts/users that
need to introspect.

Smoke-tested all four entry paths (explicit mode, both legacy shims,
auto-detection from OMARCHY_PATH); each resolves correctly and the
legacy flags are exported in matching modes.
2026-06-04 18:34:35 -04:00
Ryan Hughes 6e0079a66d omarchy-dev-link/unlink: strip prior dev-link bin/ from PATH
Sequential 'dev-link A' then 'dev-link B' previously left A/bin in
PATH because the prepend didn't strip the old one. unlink had the
same issue (it was trying to strip $(dirname $0), which only worked
if you invoked unlink via an absolute path that happened to match
the dev-link bin).

Both now read the previous OMARCHY_PATH from /etc/omarchy.conf before
overwriting/deleting it, and strip that path's bin/ from PATH before
prepending the new one (or nothing, for unlink).
2026-06-04 18:34:35 -04:00
Ryan Hughes b887d18b84 Trim over-commented Chunk 3+/4 work
A pass over the install scripts, dev-tools commands, and Hyprland Lua
files that I had stuffed with explain-everything preambles. Most of
those rationales (which files ship where, why hyprctl setenv doesn't
suffice, etc.) belong in commit messages or PR descriptions, not in
code people have to read forever. Kept the few comments that document
genuinely non-obvious behaviour: the keybind-env reason for hl.env in
envs.lua, why the runtime PAM seds stay scripted in
increase-lockout-limit, the chroot/--now distinction in chroot.sh, and
the dev-pkg-test split-install reason.
2026-06-04 18:34:35 -04:00
Ryan Hughes ea54e25bba Tighten omarchy-tzupdate sudoers to set-timezone subcommand only
Mirrors the upstream tightening in install/config/timezones.sh (which
this etc-overrides file replaced): scope the wheel passwordless rule
to 'timedatectl set-timezone *' instead of the whole timedatectl, so
the rule can't be used to flip other system clock settings.

Adopted while rebasing onto current origin/omarchy-shell.
2026-06-04 18:34:35 -04:00
Ryan Hughes 9e1ed8f85c omarchy-dev-pkg-test: install with pacman -U --overwrite=*; add plymouth to transition overwrites
Two related fixes for installing package builds onto a system that has
the legacy script-installed Omarchy files already in place:

bin/omarchy-dev-pkg-test: split makepkg -s (build) from sudo pacman -U
(install) so we can pass --overwrite='*' to pacman. makepkg -i forwards
a fixed flag set to pacman and doesn't expose --overwrite, so dev pkg
tests on an existing script-install fail on every conflicting file
(plymouth themes, /etc drop-ins, sudoers). --overwrite='*' is correct
for the dev use case: the build IS the new authoritative state.

bin/omarchy-update-system-pkgs: add /usr/share/plymouth/themes/omarchy/*
to the transition --overwrite list. The old install/login/plymouth.sh
cp -r'd the theme files into that directory, so existing installs
conflict on the omarchy-settings package's plymouth payload.
2026-06-04 18:34:35 -04:00
Ryan Hughes ac452dd311 Drop the shell-expansion guard from omarchy-dev-link's conf
The previous format wrote 'export OMARCHY_PATH="${OMARCHY_PATH:-<path>}"'
to /etc/omarchy.conf, intended so install.sh's script-mode OMARCHY_PATH
could win over the dev-link value. But install.sh doesn't source
/etc/omarchy.conf (only profile.d/uwsm/bash do, all post-install), so the
guard was protecting a non-scenario.

It also caused a real bug: paths.lua parses /etc/omarchy.conf as text
and returned the literal '${OMARCHY_PATH:-<path>}' string as the
omarchy_path, breaking dev-link in Hyprland.

Switch to plain 'export OMARCHY_PATH="<path>"'. dev-link is the only
writer, and when it's active, its value should win unconditionally.
2026-06-04 18:34:35 -04:00
Ryan Hughes 4f031e10c8 Propagate OMARCHY_PATH and PATH to Hyprland-spawned processes; dev-link-aware paths.lua
Two related fixes for omarchy-dev-link to actually affect Hyprland keybinds:

default/hypr/paths.lua: prefer /etc/omarchy.conf over the process env.
hyprctl setenv updates Hyprland's process env, but the user-reported case
shows it doesn't reach bind-exec dispatchers because their env is
captured at config-load time. Reading the dev-link conf directly means
paths.omarchy_path is correct on every hyprctl reload regardless of
whether the launching Hyprland session re-read its env.

default/hypr/envs.lua: explicitly hl.env("OMARCHY_PATH", ...) and
hl.env("PATH", "$OMARCHY_PATH/bin:$PATH") so keybind dispatchers (and
everything else spawned by Hyprland) inherit the dev-link values. PATH
is built with a dedup pass so reloads don't accumulate the bin/ prefix.

Tested locally: paths.lua reads /etc/omarchy.conf correctly; lua syntax
of both files validates with luac -p.
2026-06-04 18:34:35 -04:00
Ryan Hughes f4e68d5bb2 Refuse omarchy-dev-{link,unlink} when invoked under sudo
When the script is run under outer sudo, the environment loses
HYPRLAND_INSTANCE_SIGNATURE, XDG_RUNTIME_DIR, and DBUS_SESSION_BUS_ADDRESS,
so the 'hyprctl version' check silently fails and the live-session
refresh (setenv / import-environment / restart shell / hyprctl reload)
gets skipped. The user is left with /etc/omarchy.conf written but no
session updates — confusing because the env in the current shell still
shows the old value.

Reject EUID=0 invocations with a clear message pointing at the right
pattern: invoke as your user, the script prompts for sudo only when it
needs to write /etc/omarchy.conf.
2026-06-04 18:34:35 -04:00
Ryan Hughes a2feffa803 Add omarchy-dev-{link,unlink,status,pkg-test} commands
The four dev-tools commands that complete the developer-ergonomics
story for the package refactor:

- omarchy-dev-link <path>: writes /etc/omarchy.conf so OMARCHY_PATH
  resolves to <path> in all new shells, the Hyprland session env, and
  systemd --user. Updates the live session via hyprctl setenv +
  systemctl --user import-environment, restarts omarchy-shell, and
  reloads Hyprland so the changes are visible immediately. Affects
  every tree resolved via $OMARCHY_PATH: bin/, default/, shell/,
  themes/, applications/, config/.

- omarchy-dev-unlink: removes /etc/omarchy.conf and reverses the live
  session updates back to /usr/share/omarchy.

- omarchy-dev-status: reports current dev-link state (configured path,
  current shell, hyprland session env).

- omarchy-dev-pkg-test [pkg] [checkout]: builds and installs a package
  from the checkout via OMARCHY_SRC + makepkg -si --skipchecksums.
  Used for changes that land at fixed system paths (/etc/, /usr/lib/,
  udev rule bodies, plymouth themes, /etc/skel) that dev-link can't
  shadow. The built package's pkgver is tagged 'dev.<short-sha>[.dirty]'
  so 'pacman -Q' makes its source obvious. Defaults: package
  omarchy-settings, checkout ~/Work/omarchy/omarchy-installer,
  PKGBUILDs read from ~/Work/omarchy/omarchy-pkgs/pkgbuilds/.

All four ship via omarchy-dev-tools (existing PKGBUILD wildcard).
Verified package build picks them up at /usr/bin/omarchy-dev-*.
2026-06-04 18:34:35 -04:00
Ryan Hughes dfc8065915 Clear cached omarchy Lua modules on hyprctl reload
Lua's require() caches modules in package.loaded; without explicit
invalidation, changes to default/hypr/*.lua or ~/.config/hypr/*.lua
won't take effect after hyprctl reload even though hyprland.lua itself
re-runs.

The clear runs unconditionally — benefits both dev-link workflows
(checkout files re-evaluate after every reload) and normal users who
edit their own ~/.config/hypr/*.lua and run omarchy-restart-hyprctl.
Cost is negligible: package.loaded has a handful of entries.

Pattern matches the require_all.lua reload option (which sets
package.loaded[module] = nil before require()), but applied broadly to
the omarchy module namespace at the entry point so individual modules
don't need to opt in.
2026-06-04 18:34:35 -04:00
Ryan Hughes 7e7a665663 Switch dev-link mechanism from shadow-dir to /etc/omarchy.conf
omarchy-dev-link (lands in a follow-up) writes OMARCHY_PATH=<checkout>
to /etc/omarchy.conf. Three boot/session entry points now source it
before falling back to /usr/share/omarchy:

- /etc/profile.d/omarchy.sh   (login shells, SSH)
- config/uwsm/env             (Hyprland session via UWSM)
- default/bash/envs           (non-login interactive bash; duplicated for
                              SSH and similar)

All three use 'export OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"'
so /etc/omarchy.conf wins over the default, and install.sh's script-mode
export wins over /etc/omarchy.conf (because both use the :- guard).

Hyprland updates session env on the fly via hyprctl setenv +
systemctl --user import-environment, so omarchy-dev-link can re-trigger
those to make live sessions pick up the new path without restart.
2026-06-04 18:34:35 -04:00
Ryan Hughes 0777b20967 Make profile.d prefer /usr/share/omarchy-dev when present
Sets up the shadow-dir pattern that omarchy-dev-link will use: when
/usr/share/omarchy-dev exists (as a symlink to a local checkout),
$OMARCHY_PATH resolves there instead of /usr/share/omarchy. The PATH
prepend then makes the checkout's bin/ override the package's
/usr/bin copies.

This covers every hot-edit surface that resolves via $OMARCHY_PATH:
bin/, default/, shell/, and themes/. Things that don't go through
$OMARCHY_PATH (udev rules, systemd drop-ins, /etc-installed configs,
plymouth themes, /etc/skel seeds) remain unaffected by dev-link and
require omarchy-dev-pkg-test to verify changes — which is also what
you'd want before shipping any of those anyway.
2026-06-04 18:34:35 -04:00
Ryan Hughes a4c4e5964f Clarify why profile.d/omarchy.sh prepends $OMARCHY_PATH/bin
The previous comment said 'mainly relevant for the script-install path',
but install.sh and default/bash/envs already prepend it there. The real
reason is dev-link: when /usr/share/omarchy is a symlink to a local
checkout, this prepend lets the checkout's bin/ override the /usr/bin/
copies installed by the package.
2026-06-04 18:34:35 -04:00
Ryan Hughes 0ebbe34760 Add chrootable_systemctl_enable_only for services we shouldn't start during install
The previous centralization into enable-services.sh accidentally changed
semantics for three services: docker.socket, iwd.service, and
power-profiles-daemon.service used to be enabled with bare
'sudo systemctl enable' (no --now); chrootable_systemctl_enable promoted
them to 'enable --now' outside chroot.

For iwd this is the riskiest: it's typically what's keeping the installer
online, so starting it mid-install is at best a no-op and at worst can
disrupt the active network connection. For docker.socket and
power-profiles-daemon the change is cosmetic (the socket is already
inactive, the daemon does no harm to start) but the original intent was
deferral to first boot.

New helper in install/helpers/chroot.sh:

  chrootable_systemctl_enable_only <unit>  # plain enable, no --now

enable-services.sh uses it for the three services that were previously
bare-enabled. The other five (bluetooth, cups, cups-browsed,
avahi-daemon, linux-modules-cleanup) keep the --now behavior they had
under chrootable_systemctl_enable.
2026-06-04 18:34:35 -04:00
Ryan Hughes 0e91948b8f Stop runtime defaults from clobbering OMARCHY_PATH
The /etc/profile.d/omarchy.sh default for OMARCHY_PATH was being clobbered
by shipped config files that re-export the variable to the old script-mode
path. In package mode, every login shell, UWSM session, and Hyprland
autostart was effectively reversing the migration. Fix:

- default/bashrc: source from $OMARCHY_PATH/default/bash/rc; default
  OMARCHY_PATH to /usr/share/omarchy if not set (so the source line
  works in any boot order, including non-login interactive shells).
- default/bash/envs: change 'export OMARCHY_PATH=$HOME/...' to
  'export OMARCHY_PATH=${OMARCHY_PATH:-/usr/share/omarchy}' so the
  profile.d default and install.sh script-mode override both win.
- config/uwsm/env: same defaulting pattern. UWSM may not source
  profile.d, so the inline default still matters.
- default/hypr/autostart.lua: quickshell launched from $OMARCHY_PATH/shell
  (not literal $HOME path). The bash that runs hl.exec_cmd sees
  OMARCHY_PATH propagated from UWSM env.
- config/chromium-flags.conf: --load-extension hard-codes
  /usr/share/omarchy/... because chromium doesn't expand env vars or
  '~'. The omarchy-dev-link workflow symlinks /usr/share/omarchy to a
  checkout.
- bin/omarchy-install-browser: same chromium-flag path correction for
  brave-origin-beta-flags.conf.
2026-06-04 18:34:35 -04:00
Ryan Hughes f02f4a25b3 Ship /etc/profile.d/omarchy.sh defaulting OMARCHY_PATH
For shells outside the install flow (login shells, cron jobs, dev work)
the install.sh-set OMARCHY_PATH isn't in scope. Without a system-wide
default, omarchy-* commands shipped at /usr/bin can't find runtime data
under /usr/share/omarchy/.

profile.d sets OMARCHY_PATH=/usr/share/omarchy if not already set, so
install.sh's override (to $HOME/.local/share/omarchy during script-install)
still wins. The omarchy-dev-link workflow manages a symlink at
/usr/share/omarchy that points at a local checkout when active.

Also prepends $OMARCHY_PATH/bin to PATH if the directory exists (mainly
matters for script-install mode; /usr/bin is on PATH for package mode).
2026-06-04 18:34:04 -04:00
Ryan Hughes 25bd9aec9e Normalize remaining ~/.local/share/omarchy refs in bin/
Missed in the previous pass:
- bin/omarchy-launch-screensaver: alacritty/ghostty config paths
- bin/omarchy-refresh-applications: source paths for icons and .desktop
- bin/omarchy-refresh-config: docstring/comment references

Still NOT touched (intentional):
- bin/omarchy-reinstall-git: stages '~/.local/share/omarchy-{old,new}' for
  the script-mode upgrade dance; needs redesign for package mode.
- bin/omarchy-dev-add-migration: uses 'cd ~/.local/share/omarchy' to read
  a git log; only meaningful in dev mode.
- bin/omarchy-install-browser:69: writes a string into a chromium config file.
- install/helpers/errors.sh:125: boot.sh relaunch (separate redesign).
2026-06-04 18:34:04 -04:00
Ryan Hughes 510636b8c9 Normalize hard-coded ~/.local/share/omarchy refs to $OMARCHY_PATH
The package-installed paths live at /usr/share/omarchy; the script-install
paths live at ~/.local/share/omarchy. $OMARCHY_PATH is set by install.sh
during the install flow and by /etc/profile.d/omarchy.sh in package mode.
Scripts that hard-code the script-install path don't honour that and will
misbehave once shipped to /usr/bin.

16 files migrated via sed s|~/\.local/share/omarchy|$OMARCHY_PATH|g
and s|$HOME/\.local/share/omarchy|$OMARCHY_PATH|g:

bin/:
- omarchy-plymouth-{preview,reset,set}
- omarchy-refresh-{limine,plymouth}
- omarchy-reinstall-configs
- omarchy-show-logo
- omarchy-games-retro-install
- omarchy-install-gaming-battlenet

install/:
- config/branding.sh
- packaging/{fonts,icons}.sh
- post-install/{finished,pacman}.sh
- preflight/{migrations,pacman}.sh

Deliberately NOT migrated:
- bin/omarchy-dev-add-migration: uses 'cd ~/.local/share/omarchy' to
  resolve a git repo for 'git log'. The packaged binary doesn't have a
  git repo to read; this command only makes sense in dev mode where
  the home path is the right one.
- bin/omarchy-install-browser:69: writes a string into a chromium
  config file that chromium itself interprets; not a bash-resolved path.
- install/helpers/errors.sh:125: the boot.sh-style relaunch behaviour
  needs a separate redesign.
2026-06-04 18:34:04 -04:00
Ryan Hughes 12826d4076 Centralize service enables into install/config/enable-services.sh
The previous setup scattered systemctl enables across docker.sh,
printer.sh, bluetooth.sh, network.sh, powerprofilesctl-rules.sh, and
kernel-modules-hook.sh. Some used chrootable_systemctl_enable; others
used bare 'sudo systemctl enable'. Centralizing makes the install-time
service surface auditable in one place and consistent in chroot/non-chroot
behavior.

install/config/enable-services.sh enables:
- bluetooth.service
- cups.service, cups-browsed.service, avahi-daemon.service
- docker.socket
- iwd.service
- linux-modules-cleanup.service
- power-profiles-daemon.service

Each via chrootable_systemctl_enable, which uses 'enable --now' on a live
system and bare 'enable' in chroot (set OMARCHY_CHROOT_INSTALL=1).

Stays in original script (deliberately):
- sddm.sh: bare 'systemctl enable sddm.service' (no --now during install
  would log the user out; lives in install/login/)
- limine-snapper.sh: limine-snapper-sync (bootloader-specific path)
- first-run/firewall.sh: ufw (deferred to first-run for UX)
- hardware/{t2,intel/lpmd,intel/thermald,apple/fix-suspend-nvme}: gated
  on hardware detection

Side cleanups:
- install/config/kernel-modules-hook.sh: DELETED (sole line was the enable
  for linux-modules-cleanup, now in enable-services.sh).
- install/config/hardware/printer.sh: DELETED (sole purpose was the
  three cups/avahi enables; nothing else to do).
- install/config/hardware/network.sh: keep the systemd-networkd-wait-online
  disable+mask; drop the iwd enable.
- install/config/hardware/bluetooth.sh: keep the AutoEnable=false sed and
  the wireplumber/bt-agent user-session setup; drop the system enable.
- install/config/docker.sh: drop the docker.socket enable.
- install/config/powerprofilesctl-rules.sh: drop the
  power-profiles-daemon enable.
- install/config/all.sh: register enable-services.sh; remove the two
  deleted entries.
2026-06-04 18:34:04 -04:00
Ryan Hughes 080c8ba38c omarchy-wifi-powersave: nullglob; plocate drop-in daemon-reload; idempotent legacy cleanup
- bin/omarchy-wifi-powersave: shopt -s nullglob. Previously, machines
  with no wireless interfaces ran the loop once with the literal glob
  ('iface=*'), failed iw, and exited 237. With the new always-installed
  udev rule that invokes this on every AC transition, the failure
  would surface as failed transient omarchy-wifi-powersave-* units on
  desktops. Now it cleanly no-ops.
- migrations/1779307845.sh: add 'systemctl daemon-reload' so the
  plocate-updatedb.service.d/ac-only.conf drop-in is picked up on
  upgrade. Arch's systemd pacman hook only triggers on
  /usr/lib/systemd/system/*, not /etc/systemd/system/*.
- install/config/powerprofilesctl-rules.sh and wifi-powersave-rules.sh:
  rm -f the pre-rename legacy paths before udevadm reload, so the
  cleanup is idempotent outside the one-shot migration (matters for
  re-install/downgrade testing).
2026-06-04 18:34:04 -04:00
Ryan Hughes dcac0ed953 Convert plocate-ac-only, power-profile, wifi-powersave, unmount-fuse to package files
Four scripts that wrote static /etc or /usr files become package-shipped
files. The other config-script audit candidates (omarchy-ai-skill,
nautilus-python, omarchy-toggles, input-group) correctly stay scripts —
they're user-level operations, not /etc writes.

New package-owned files (omarchy-installer/etc/, shipped by
omarchy-settings):
- etc/systemd/system/plocate-updatedb.service.d/ac-only.conf
- etc/udev/rules.d/99-omarchy-power-profile.rules
- etc/udev/rules.d/99-omarchy-wifi-powersave.rules

The two udev rule files are renamed for namespacing (99-power-profile
-> 99-omarchy-power-profile; 99-wifi-powersave -> 99-omarchy-wifi-powersave)
and rewritten to invoke /usr/bin/omarchy-powerprofiles-set and
/usr/bin/omarchy-wifi-powersave instead of $HOME/.local/share/omarchy/bin/...
(both binaries ship in the omarchy package at /usr/bin/).

The battery-present gate from the original scripts is dropped — the rules
trigger on power_supply udev events, which are benign on desktops without
batteries. (The runtime commands no-op on AC-only systems.)

Script changes:
- install/config/plocate-ac-only.sh: DELETED (file ships, daemon-reload
  happens via pacman hook).
- install/config/unmount-fuse.sh: DELETED (file ships at
  /usr/lib/systemd/system-sleep/unmount-fuse via omarchy-settings).
- install/config/powerprofilesctl-rules.sh: SHRINK to runtime ops
  (enable power-profiles-daemon, udevadm reload+trigger).
- install/config/wifi-powersave-rules.sh: SHRINK to udevadm reload+trigger.
- install/config/all.sh: drop the two deleted entries.

bin/omarchy-update-system-pkgs: add --overwrite for plocate-updatedb
drop-in and unmount-fuse paths (the udev rule renames need no overwrite
since the new paths are virgin).

migrations/1779307845.sh: remove the legacy 99-power-profile.rules and
99-wifi-powersave.rules paths on existing installs, then reload udev.
2026-06-04 18:34:04 -04:00
Ryan Hughes 5683750b29 Add pacman --overwrite for transition paths; fix mimeapps/HEY and PAM idempotency
Four corrections to the omarchy-settings transition:

- bin/omarchy-update-system-pkgs: pass --overwrite for the 13 paths
  omarchy-settings now owns. Existing Omarchy installs would otherwise
  fail pacman conflict checks on the upgrade that ships the package
  (the files exist as unowned filesystem entries from the previous
  install scripts). The flags are no-ops once the transition release
  is everyone's baseline; remove then.
- config/mimeapps.list: drop the HEY.desktop mailto mapping. HEY.desktop
  is generated by install/packaging/webapps.sh at install time, not
  shipped under applications/, so it isn't valid for a fresh /etc/skel
  user before the installer runs.
- install/config/mimetypes.sh: add 'xdg-mime default HEY.desktop
  x-scheme-handler/mailto' as a runtime op since the mapping no longer
  lives in mimeapps.list.
- install/config/increase-lockout-limit.sh: delete existing
  pam_faillock.so authsucc lines before re-adding, so re-running the
  installer doesn't duplicate the authsucc entry in
  /etc/pam.d/sddm-autologin.
2026-06-04 18:34:04 -04:00
Ryan Hughes 3636f57f0c Migrate: clean up legacy /etc paths from prior installs
Existing users upgrading from script-installed Omarchy still have files
the install scripts wrote at the OLD paths. The new package can't own
those old paths (they were either renamed or replaced by drop-ins), so
this migration tidies them up:

- /etc/sysctl.d/99-sysctl.conf: strip the appended net.ipv4.tcp_mtu_probing
  line (new file is /etc/sysctl.d/99-omarchy-sysctl.conf)
- /etc/modprobe.d/disable-usb-autosuspend.conf: removed
  (new file is /etc/modprobe.d/omarchy-usb-autosuspend.conf)
- /etc/sudoers.d/{passwd-tries,asdcontrol}: removed
  (new files are /etc/sudoers.d/omarchy-{passwd-tries,asdcontrol})
- /etc/systemd/system/user@.service.d/faster-shutdown.conf: removed
  (new file is 10-faster-shutdown.conf)
- /etc/systemd/logind.conf HandlePowerKey=ignore line: reset to its
  commented default so the logind.conf.d/ drop-in is the only source

After cleanup, systemctl daemon-reload + sysctl --system to pick up the
new state immediately.
2026-06-04 18:34:04 -04:00
Ryan Hughes 89242fafc6 Ship mimeapps.list via /etc/skel; shrink mimetypes.sh
config/mimeapps.list ships at /etc/skel/.config/mimeapps.list (via
omarchy-settings's existing config/** -> /etc/skel/.config copy).
New users get the full Omarchy MIME defaults on first login without
running install scripts.

install/config/mimetypes.sh shrinks to two runtime ops that a static
file can't replace:
- omarchy-refresh-applications: copies .desktop entries and icons into
  the user home and refreshes the desktop database
- xdg-settings set default-web-browser chromium.desktop: sets the
  system-default browser; omarchy-install-browser overrides this if
  the user later picks a different browser.
2026-06-04 18:34:04 -04:00
Ryan Hughes 993e6a0812 Shrink printer.sh to service enables only
The three /etc writes (resolved.conf.d drop-in, nsswitch.conf override,
cups-browsed.conf override) now ship via omarchy-settings. The service
enables stay scripted because they need chrootable_systemctl_enable to
work both inside the ISO chroot and on a live system.
2026-06-04 18:34:04 -04:00
Ryan Hughes 26bcbb7dd8 Shrink fast-shutdown, gpg, increase-file-watchers, increase-lockout-limit to runtime-only ops
Each script's static-file write moves to omarchy-settings. The scripts
keep only the runtime side: reload systemd, restart dirmngr, sysctl --system,
or (for lockout-limit) the PAM seds that have to stay scripted because
/etc/pam.d/system-auth and /etc/pam.d/sddm-autologin are upstream-owned and
need targeted line edits rather than a full-file override.

faillock.conf's deny=10 (formerly inside increase-sudo-tries.sh sed) now
rides through the etc-overrides dance in omarchy-settings.
2026-06-04 18:34:04 -04:00
Ryan Hughes df1ea87331 Shrink docker.sh to runtime-only ops
The three Docker config files (etc/docker/daemon.json,
etc/systemd/system/docker.service.d/no-block-boot.conf,
etc/systemd/resolved.conf.d/20-docker-dns.conf) now ship via the
omarchy-settings package. The script keeps only the actions a static
file can't do:

- restart systemd-resolved so the new drop-in takes effect
- usermod -aG docker $USER (user-specific, can't be packaged)
- systemctl enable docker.socket and daemon-reload
2026-06-04 18:34:04 -04:00
Ryan Hughes 2b749124ef Delete install scripts whose job is now done by package files
Each of these scripts only wrote a single /etc file (or copied
config/bashrc into the user home). The corresponding files now ship
from the omarchy-settings package — either directly into /etc (drop-ins
the package fully owns) or via the /usr/share/omarchy/etc-overrides
post_install dance (for upstream-owned paths).

Deleted:
- install/config/timezones.sh          -> etc/sudoers.d/omarchy-tzupdate
- install/config/sudoless-asdcontrol.sh -> etc/sudoers.d/omarchy-asdcontrol
                                          ($USER rewritten to %wheel)
- install/config/increase-sudo-tries.sh -> etc/sudoers.d/omarchy-passwd-tries
                                          (faillock.conf side now in etc-overrides)
- install/config/hardware/ignore-power-button.sh
                                        -> etc/systemd/logind.conf.d/10-ignore-power-button.conf
                                          (replaces sed-edit of logind.conf)
- install/config/hardware/usb-autosuspend.sh
                                        -> etc/modprobe.d/omarchy-usb-autosuspend.conf
- install/config/ssh-flakiness.sh       -> etc/sysctl.d/99-omarchy-sysctl.conf
- install/config/config.sh              -> /etc/skel seeds ~/.config from config/**;
                                          ~/.bashrc seeded from etc-overrides
- install/login/plymouth.sh             -> theme files already shipped by
                                          omarchy-settings; etc/plymouth/plymouthd.conf
                                          handled via etc-overrides

A separate migration tidies up the old paths on existing installs.
all.sh entries updated to drop these scripts.
2026-06-04 18:34:04 -04:00
Ryan Hughes e6e6328db7 Add etc/ source tree for package-shipped /etc files
This tree is the source for omarchy-settings to install into /etc
(for paths Omarchy fully owns) or stage at /usr/share/omarchy/etc-overrides
(for paths upstream packages own, which the post_install copies into place).

Package-owned drop-ins (no upstream conflict):
- etc/docker/daemon.json
- etc/systemd/system/docker.service.d/no-block-boot.conf
- etc/systemd/resolved.conf.d/20-docker-dns.conf
- etc/systemd/resolved.conf.d/10-disable-multicast.conf
- etc/systemd/system.conf.d/10-faster-shutdown.conf
- etc/systemd/system/user@.service.d/10-faster-shutdown.conf
  (renamed from .../faster-shutdown.conf for consistency)
- etc/systemd/logind.conf.d/10-ignore-power-button.conf
  (replaces the previous in-place sed-edit of logind.conf)
- etc/gnupg/dirmngr.conf
- etc/sysctl.d/99-omarchy-sysctl.conf  (renamed from the 99-sysctl.conf
  appendage; a migration cleans up the old path on existing installs)
- etc/sysctl.d/90-omarchy-file-watchers.conf
- etc/modprobe.d/omarchy-usb-autosuspend.conf
  (renamed from disable-usb-autosuspend.conf)
- etc/sudoers.d/omarchy-asdcontrol
  (renamed from asdcontrol; $USER -> %wheel for packaging)
- etc/sudoers.d/omarchy-tzupdate
- etc/sudoers.d/omarchy-passwd-tries
  (renamed from passwd-tries for namespacing)

Etc-overrides (upstream-owned paths; staged by the PKGBUILD at
/usr/share/omarchy/etc-overrides/ and cp'd in by post_install):
- etc/security/faillock.conf (owned by pam)
- etc/nsswitch.conf (owned by filesystem)
- etc/cups/cups-browsed.conf (owned by cups-browsed)
- etc/plymouth/plymouthd.conf (owned by plymouth)

All sudoers files validate with visudo -cf.

The install scripts that previously wrote these files are updated or
removed in follow-up commits.
2026-06-04 18:34:04 -04:00
Ryan Hughes add570dac2 Fix one-sided menu row borders 2026-06-04 12:11:41 -04:00
David Heinemeier HanssonandClaude Opus 4.8 e5290b0a12 Add yt-dlp "Download Video" Chromium extension
Download the current page's video with yt-dlp via Alt+Shift+D or a click
on the toolbar icon. A native-messaging host runs the download, shows live
progress on the Quickshell OSD, and posts a clickable "Download complete"
toast that opens the file in mpv.

- Extension: pinned key for a stable id, green download-video icon,
  keyboard command + toolbar action (reads the active tab URL).
- Native host (omarchy-chromium-ytdlp-host): verifies the URL with
  yt-dlp --simulate (else "No video found"), streams progress to the OSD
  (time-throttled to ~4/s), saves to ~/Videos, opens mpv on click.
- Installer (omarchy-install-chromium-ytdlp) writes the native-messaging
  manifest into installed Chromium/Chrome/Brave/Edge profiles; wired into
  browser install and chromium refresh, with a migration for existing users.
- omarchy-osd: add -d/--duration so the OSD can persist during a download.
- Add yt-dlp to base packages, load the extension via --load-extension,
  and document the Alt+Shift+D binding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:15:28 +02:00
David Heinemeier Hansson 1d6467c91c Add explicit toggle states 2026-06-04 11:14:04 +02:00
David Heinemeier Hansson c31825dca4 Apparently this needs to be said 2026-06-04 10:16:00 +02:00
David Heinemeier Hansson 4884f8c64a Persist watched clipboard text 2026-06-04 10:09:38 +02:00
David Heinemeier Hansson 79470dde92 Refine shell panel and lock theming 2026-06-03 22:14:48 +02:00
David Heinemeier Hansson 3cdfb52a74 Didn't actually like the stock hyprland active color
Especially not on the notifications or lock screen
2026-06-03 21:05:19 +02:00
David Heinemeier Hansson 7a47604e27 Show which panel is open 2026-06-03 14:48:47 +02:00
David Heinemeier Hansson d35957b23c Better spacing 2026-06-03 13:59:33 +02:00
David Heinemeier Hansson e1099e4413 Match lock border to Hyprland style 2026-06-03 13:10:48 +02:00
David Heinemeier Hansson a8080d4016 All click targets should have click hands 2026-06-03 11:27:09 +02:00
David Heinemeier Hansson 198d01f8bc Reveal all hidden tooling when hovering over the center part of the bar 2026-06-03 08:23:32 +02:00
David Heinemeier Hansson ddc013aea9 More subtle tweaks to the lock screen 2026-06-03 08:22:50 +02:00
David Heinemeier Hansson 807f500888 More finessing of lock screen proportions 2026-06-03 07:56:48 +02:00
Ryan Hughes 0f715dff88 Lock screen improvements 2026-06-02 23:32:53 -04:00
Ryan Hughes e8a8fcf183 Fix theme switcher preload scrim race 2026-06-02 22:38:31 -04:00
Ryan Hughes d52b69c49b Fix button hover border layout shift 2026-06-02 22:19:09 -04:00
Ryan Hughes 62f3af2300 Merge pull request #4541 from bjarneo/normalize-color-names
Implement a new color system with inspiration from base24
2026-06-02 22:08:06 -04:00
Ryan Hughes 53357ace61 Rename purple theme color to magenta 2026-06-02 20:39:36 -04:00
Ryan Hughes 357b77c92c Register generated VS Code theme extension 2026-06-02 20:28:18 -04:00
Ryan Hughes 4087be7f79 Clean up semantic theme compatibility 2026-06-02 19:35:07 -04:00
Ryan Hughes 89bfa841c4 Restore semantic theme compatibility 2026-06-02 17:23:28 -04:00
Bjarne Øverli b1505a085d Implement semantic theme color system 2026-06-02 17:23:28 -04:00
Ryan Hughes 98ba4a9697 Bring on gradient borders 2026-06-02 11:17:13 -04:00
Ryan Hughes 30bafe7bb0 Make lock wallpaper lazy 2026-06-02 11:13:35 -04:00
Ryan Hughes fe70006736 Prevent disappearing wallpaper 2026-06-01 21:13:41 -04:00
Ryan Hughes 9c4ff68a43 Restore themed Hyprland gradients 2026-06-01 17:37:24 -04:00
David Heinemeier Hansson 5812a7d04a No leading zeros on the date in the bar 2026-06-01 11:59:30 +02:00
Token User 99bb924f37 Make OSD click-through
Set an empty input mask on the visual-only OSD layer surface so it no longer captures pointer input while visible.
2026-06-01 08:19:34 +02:00
Ryan Hughes 1bb439476a Add third-party plugin sources and installer
Let users add trusted plugin source repos and add/update/remove plugins
from them, alongside the existing in-shell plugin commands:

  omarchy plugin source <add|list|remove|refresh>
  omarchy plugin available
  omarchy plugin add | update | remove | validate

Each command is interactive (gum/fzf pickers, confirmation, an update
diff) in a TTY and fully flag-driven with --yes for scripts and agents.
Sources live in ~/.config/omarchy/plugins/sources.json and clone into
~/.cache/omarchy/plugin-sources/. The installer only copies files,
validates manifests against the shell's schema, and toggles enabled
state over IPC -- it never runs plugin code, hooks, or sudo.

Also guard 'plugin bar add' so only known bar-widget ids enter the
layout (rejecting typos/non-widgets like a stray '--help').
2026-05-29 16:55:15 -04:00
Ryan Hughes 5bab2dd230 Run Voxtype configure from dictation indicator 2026-05-29 14:36:26 -04:00
David Heinemeier Hansson a4bb7a46f1 Fix tiled fullscreen toggle 2026-05-29 15:59:13 +02:00
David Heinemeier Hansson 6f0d9d53cd Keep sensitive clipboard contents out of history
Restore the clipboard watcher behavior that drops CLIPBOARD_STATE=sensitive events, which password managers use for copied secrets.

Use the same sensitive foreground clipboard path for emoji insertion instead of a custom history marker, and keep the password-manager MIME hint ignored as a fallback.
2026-05-29 15:25:21 +02:00
David Heinemeier Hansson cd2be9da25 Insert emojis without persisting clipboard entries
Keep the first emoji result selected whenever results exist, including after search filtering, so Enter inserts immediately.

Route emoji insertion through a hidden helper that serves a transient sensitive clipboard entry long enough for Shift+Insert, then stops that clipboard owner.

Mark transient emoji inserts so clipboard history skips only that event while continuing to record normal terminal copies.
2026-05-29 14:54:49 +02:00
David Heinemeier Hansson 16a4bc8106 Migrate terminal Shift+Return bindings
Update existing terminal configs to encode Shift+Return as CSI-u so Codex and other TUIs can distinguish it from Return without tmux seeing it as Alt+Return. Also ensures Alt+Shift+Return CSI-u bindings remain present for tmux horizontal splits across Kitty, Ghostty, and Foot.
2026-05-29 14:28:03 +02:00
David Heinemeier Hansson bef9a80c25 Fix Shift+Return terminal encoding
Encode Shift+Return as CSI-u across supported terminals so TUIs such as Codex can distinguish it from plain Return. This replaces Alacritty's ambiguous ESC+Return mapping, which tmux interpreted as Alt+Return and used to split panes, while preserving Alt+Shift+Return as CSI-u for tmux's M-S-Enter binding.
2026-05-29 14:18:56 +02:00
David Heinemeier Hansson a0cf4b07b2 Show clipboard file entries without inline video playback 2026-05-29 14:14:40 +02:00
David Heinemeier Hansson f60c038050 Open clipboard entries from history 2026-05-29 13:54:55 +02:00
David Heinemeier Hansson 766c3b9a3f Polish clipboard history display 2026-05-29 13:35:28 +02:00
David Heinemeier Hansson 4b003d2e39 Simplify clipboard preview divider 2026-05-29 13:15:11 +02:00
David Heinemeier Hansson bb0d9dd2e3 Copy clipboard history selection with Shift Return 2026-05-29 13:15:00 +02:00
David Heinemeier Hansson cf3a481224 Confirm clearing clipboard history 2026-05-29 12:57:46 +02:00
David Heinemeier Hansson 49ca79081f Extract confirmation dialog 2026-05-29 12:57:43 +02:00
David Heinemeier Hansson 5bdd264ce0 Delete selected clipboard history entries 2026-05-29 12:23:53 +02:00
David Heinemeier Hansson e79013adbe Fix large clipboard history pastes
Clipboard history selections passed the full selected text as a process argument to omarchy-clipboard-paste-text. Large entries can exceed Linux's per-argument exec limit, so the helper never starts reliably for big copied blocks.

Pass the original history index instead and have the helper read that entry from clipboard-history.json before wl-copy and Shift+Insert. Also focus the first row when opening the manager, keep filtered rows mapped to their original history indexes, and drop whitespace-only text entries.
2026-05-29 12:21:28 +02:00
David Heinemeier Hansson 6be9b8b6d9 Fix night light indicator state 2026-05-29 09:42:24 +02:00
David Heinemeier Hansson c795d52201 Paste emojis via clipboard 2026-05-29 09:32:01 +02:00
David Heinemeier Hansson 5104daf212 Restore gap 2026-05-28 21:21:33 +02:00
David Heinemeier Hansson 7bb18c1dd3 Refine Mullvad exit node picker 2026-05-28 21:21:33 +02:00
David Heinemeier Hansson d3666c5e98 Group Mullvad exit nodes by country 2026-05-28 21:21:33 +02:00
Ryan Hughes 007393387a Fix Bluetooth panel device list handling 2026-05-28 15:04:41 -04:00
Ryan Hughes 801c2b00c8 Fix and improve bar drag and drop XP 2026-05-27 22:59:46 -04:00
Ryan Hughes f8e82dac6b Improve omarchy-config-shell-bar 2026-05-27 16:44:43 -04:00
Ryan Hughes 752ecaf6e7 add model-usage plugin 2026-05-27 16:44:24 -04:00
David Heinemeier Hansson ae7365f937 Fix repeated fingerprint setup 2026-05-27 21:20:39 +02:00
David Heinemeier Hansson dfc450f2cb Fix bar config panel reload 2026-05-27 15:20:30 +02:00
David Heinemeier Hansson bc32980cca Replace bar settings with inline config panel 2026-05-27 15:11:36 +02:00
David Heinemeier Hansson eef5668d89 Fix test 2026-05-27 15:11:36 +02:00
David Heinemeier Hansson 7ef5999038 Tighten center bar widget spacing 2026-05-27 14:25:28 +02:00
David Heinemeier Hansson d6fa701543 Separate transparent bar foreground from panel text 2026-05-27 14:23:13 +02:00
David Heinemeier Hansson e490f743db Keep glyph notification hints compact 2026-05-27 14:09:26 +02:00
David Heinemeier Hansson a09cfc3460 Fix bar module position reset on click 2026-05-27 13:58:07 +02:00
David Heinemeier Hansson b9e82a5e53 Hide Dropbox tray icon when bar widget is present 2026-05-27 13:48:40 +02:00
David Heinemeier Hansson ff2a418a25 Use correct highlight for kanagawa 2026-05-27 13:34:39 +02:00
David Heinemeier Hansson f269adfd25 Add ydotool dev installer 2026-05-27 12:36:47 +02:00
David Heinemeier Hansson 419cd37e14 Fix transparent bar contrast timing 2026-05-27 12:36:45 +02:00
David Heinemeier Hansson 9f81a65732 Add visible bar drag and drop 2026-05-27 12:36:13 +02:00
David Heinemeier Hansson 3986522d6e Extend shell bar config command 2026-05-27 11:20:42 +02:00
David Heinemeier Hansson 8914d30be7 Use hyphenated gaming command routes 2026-05-27 11:15:21 +02:00
David Heinemeier Hansson 0acac408e4 Namespace service and editor installers 2026-05-27 10:53:30 +02:00
David Heinemeier Hansson 88816ab9be Extend notification popup lifetime 2026-05-27 10:50:31 +02:00
David Heinemeier Hansson ee97329aee Add service removal commands 2026-05-27 10:48:08 +02:00
David Heinemeier Hansson e91aa9647b Add shell bar config commands 2026-05-27 10:41:12 +02:00
David Heinemeier Hansson b03a7cc73c Use Tab to switch bar panels 2026-05-27 10:40:32 +02:00
David Heinemeier Hansson 848499a76b Massage it 2026-05-27 10:29:01 +02:00
David Heinemeier Hansson f520f9ff22 Simplify 2026-05-27 10:28:49 +02:00
David Heinemeier Hansson 8b4525458b pkexec > sudo 2026-05-27 10:15:12 +02:00
David Heinemeier Hansson 5a08ef33fb Fix icon placement 2026-05-27 08:56:29 +02:00
David Heinemeier Hansson 8350a792ac Add exit nodes to the panel 2026-05-27 08:44:32 +02:00
David Heinemeier Hansson 67dd7a2953 Add Tailscale panel connection controls 2026-05-26 22:07:20 +02:00
David Heinemeier Hansson 48a741e3ee Make the current user a tailscale operator on install 2026-05-26 19:03:17 +02:00
David Heinemeier Hansson 9100bc96be Launch shell actions through Hyprland 2026-05-26 18:50:24 +02:00
Ryan Hughes c70ed3cdcd Add tailscale plugin 2026-05-26 11:27:11 -04:00
David Heinemeier Hansson 492b420aad Open recording menu from indicator 2026-05-26 02:11:45 +02:00
David Heinemeier Hansson dd3080251d Fix Voxtype install notification hook 2026-05-26 00:11:51 +02:00
David Heinemeier Hansson 953616dd62 Remove borders from menu highlights 2026-05-26 00:11:03 +02:00
David Heinemeier Hansson af5e918dd5 Improve actionable notification wrapper 2026-05-25 23:51:48 +02:00
Jason Zimdars 8075b8b0dc Update Basecamp logo asset for version 5 release (#5769)
* Update Basecamp logo asset for version 5 release

* Final Basecamp 5 logo asset
2026-05-25 23:36:54 +02:00
David Heinemeier Hansson 322d1bc30d Gate bluetooth pairing agent on active bluetooth service 2026-05-25 23:05:00 +02:00
David Heinemeier Hansson 9fb52fa37a Fix cleaning up the first-run sudo rm 2026-05-25 22:50:24 +02:00
David Heinemeier Hansson e1075e3cd7 No need to scream about important notifications
They're already there until you dismiss them
2026-05-25 22:42:53 +02:00
David Heinemeier Hansson 2d86736fb0 Update for the new monitors.lua 2026-05-25 22:41:01 +02:00
David Heinemeier Hansson 106b25e65a No transparency on the webcam overlay 2026-05-25 21:19:26 +02:00
David Heinemeier Hansson 0d8e1e2dcd Add shell screenshot sanity test 2026-05-25 20:57:19 +02:00
David Heinemeier Hansson 5300a668d6 Add shell bar widget contract test 2026-05-25 20:57:19 +02:00
David Heinemeier Hansson 2859ec06c7 Add shell plugin registry contract test 2026-05-25 20:57:19 +02:00
David Heinemeier Hansson 38e06bba2d Add shell manifest entrypoint load test 2026-05-25 20:57:19 +02:00
David Heinemeier Hansson 595dc30e48 Expand shell runtime smoke coverage 2026-05-25 20:57:19 +02:00
David Heinemeier Hansson 0d67bc6cda Know thy nvidia 2026-05-25 20:28:39 +02:00
David Heinemeier Hansson 8caf5e4b4f Fix screenrecording webcam selector 2026-05-25 20:28:22 +02:00
David Heinemeier Hansson 32b642e085 Add launcher uninstall confirmation 2026-05-25 18:32:41 +02:00
David Heinemeier Hansson 45e9322d2a So AI can better test its work (and its tiny) 2026-05-25 18:23:12 +02:00
David Heinemeier Hansson 18fd2a812f Don't actually need this given that it's just a simple package 2026-05-25 18:20:25 +02:00
David Heinemeier Hansson 984f3368b4 On-demand Signal install 2026-05-25 18:19:06 +02:00
David Heinemeier Hansson 0ff8b5eece Move Spotify to optional service install 2026-05-25 18:12:40 +02:00
David Heinemeier Hansson 59ef9c44f7 Ensure we always have gum colors 2026-05-25 17:46:34 +02:00
David Heinemeier Hansson aba64ca864 Switch to the new lighter basic jetbrains mono nerd package 2026-05-25 17:29:27 +02:00
David Heinemeier Hansson d7adaee06b Move 1pw to an optional install since we need to install the chrome extension anyway as well as login 2026-05-25 17:29:02 +02:00
David Heinemeier Hansson 3c8aba55e5 Fix tray DBus menu activation 2026-05-25 16:57:53 +02:00
David Heinemeier Hansson 74fb9b0e80 Install GitHub CLI via mise 2026-05-25 16:56:59 +02:00
David Heinemeier Hansson 977e762619 Install Claude via mise 2026-05-25 16:52:23 +02:00
David Heinemeier Hansson f1f75ee11b Switch from npm to mise for the lazy-loaded tools 2026-05-25 16:49:18 +02:00
David Heinemeier Hansson 8ffb498168 Move shell tests under shell runner 2026-05-25 15:16:53 +02:00
David Heinemeier Hansson f05ad16eaf Add shell bar runtime regression coverage 2026-05-25 15:09:37 +02:00
David Heinemeier Hansson 37e4da524b Move shell scripts into plugin directories 2026-05-25 14:53:21 +02:00
David Heinemeier Hansson 9a5e80f2be Fix compatibility of old tiling.conf with HL 0.55+
Closes #5973
2026-05-25 14:50:56 +02:00
David Heinemeier Hansson 10898aea16 Move panel plugins under panels 2026-05-25 14:47:08 +02:00
David Heinemeier Hansson e6d990e31d Use component-like naming without a repeating namespace 2026-05-25 14:32:41 +02:00
David Heinemeier Hansson 829c1fa4f7 Add shell plugin model tests 2026-05-25 14:18:39 +02:00
David Heinemeier Hansson 4277f5346b Add shell emoji tests 2026-05-25 13:16:47 +02:00
David Heinemeier Hansson 81f4896879 Get more formal about testing 2026-05-25 13:08:56 +02:00
David Heinemeier Hansson 162e6ce006 Fix launcher filtering and add tests for it 2026-05-25 12:57:58 +02:00
David Heinemeier Hansson 8ad51075c3 Ensure ghostty + alacritty can also use the new tmux extended keys for panes 2026-05-25 11:47:45 +02:00
David Heinemeier Hansson e9d275298d Give kitty alt+shift+enter power for tmux vertical splits 2026-05-25 10:30:16 +02:00
David Heinemeier Hansson 41593cd448 Don't show power panel if there's no battery 2026-05-25 10:12:39 +02:00
David Heinemeier Hansson b48dca29e6 Make sure system updates sit in the right place 2026-05-24 15:44:50 +02:00
David Heinemeier Hansson cac3342868 Fix clicks 2026-05-24 15:44:38 +02:00
David Heinemeier Hansson d6113e5133 Now it's a default 2026-05-24 14:57:43 +02:00
David Heinemeier Hansson 3a6fca6889 Add moonlight as a default package
It's only 1mb and I use it everywhere
2026-05-24 14:56:46 +02:00
Ryan Hughes f9e41548c1 Bump version 2026-05-23 18:27:38 -04:00
David Heinemeier Hansson 67d2511984 Improve shell lock acquisition before sleep 2026-05-23 22:59:31 +02:00
Berend de BoerandDavid Heinemeier Hansson 961d4241f0 Install mpv MPRIS support (#5937)
* Install mpv MPRIS support

* omarchy-pkg-add already checks if installed

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-23 20:49:00 +02:00
David Heinemeier Hansson ea25179b7c Fix HL 0.55 compatible unified clipboard hotkeys 2026-05-23 20:05:46 +02:00
David Heinemeier Hansson a910b5dcb4 Give reminders an indicator and a full QS flow 2026-05-23 18:04:21 +02:00
David Heinemeier Hansson 60fbb2a9e4 Ensure linebreaks are respected 2026-05-23 16:49:30 +02:00
David Heinemeier Hansson b92a44d572 Persist StayAwake indicator state 2026-05-23 15:15:55 +02:00
David Heinemeier Hansson 548882527f Refine menu navigation and search 2026-05-23 15:10:09 +02:00
David Heinemeier Hansson 0e3e4da6a3 Capture the holding status of the battery better 2026-05-23 15:02:11 +02:00
David Heinemeier Hansson ad299fe8a2 No breadcrumbs and always have first item activated 2026-05-23 14:49:49 +02:00
David Heinemeier Hansson 4030bb4813 Ensure tmux coloring is updated live too 2026-05-23 13:46:27 +02:00
David Heinemeier Hansson e65246642d Add power to save/restore window widths on a per-app/per-workspace basis with Super + Home and Super + Alt + Home 2026-05-23 13:37:57 +02:00
David Heinemeier Hansson 5fc4921cfc Make defaults live where defaults always live 2026-05-23 11:28:36 +02:00
Ryan Hughes 4cd6593ba3 Use plugin IPC routes 2026-05-23 04:32:20 -04:00
Ryan Hughes 25b7dede1d Teach plugin command manifests 2026-05-23 04:32:20 -04:00
Ryan Hughes 4f0bdb790b Make built-in widgets plugins 2026-05-23 04:32:20 -04:00
Ryan Hughes fc180af54e Add default AI and launch command 2026-05-23 04:32:20 -04:00
David Heinemeier Hansson 734e7d9ff2 Revert "Add close tab on SUPER + W"
This reverts commit dcfedbf89c.
2026-05-23 10:25:41 +02:00
David Heinemeier Hansson d2e2544529 Fallback to app icon if there's nothing else 2026-05-23 10:25:33 +02:00
Ryan Hughes db7e393104 Better universal copy / paste
Works in Nautilus and other places as expected now
2026-05-23 02:10:51 -04:00
Ryan Hughes b4939c8ccb Fail hard if we don't get the right branch 2026-05-23 02:04:14 -04:00
Ryan Hughes dcfedbf89c Add close tab on SUPER + W 2026-05-22 23:36:05 -04:00
Ryan Hughes 7e81e04110 Add plugin management util 2026-05-22 23:35:41 -04:00
Ryan Hughes d23d46915f Namespace omarchy plugins 2026-05-22 23:35:27 -04:00
Ryan Hughes 1e2a3156a9 Add google meet pip 2026-05-22 16:16:57 -04:00
Ryan Hughes 877bef80d0 Add always show option 2026-05-22 16:16:48 -04:00
Ryan Hughes 952b33b981 Make indicators customizable 2026-05-22 15:05:55 -04:00
Ryan Hughes e8fc2ef08f Re-implement better scaling with new icon spacing 2026-05-22 13:30:53 -04:00
David Heinemeier Hansson 20cfa2f84e Try to tackle crash bug from hyprland locking due to open pipewiressss 2026-05-22 18:59:24 +02:00
David Heinemeier Hansson 07cebed06d Account for the threshold state 2026-05-22 18:18:19 +02:00
Ryan Hughes 703581cee2 Fix logs uploading 2026-05-22 12:04:45 -04:00
David Heinemeier Hansson b5079bdbad Revert "Provide better scaling"
This reverts commit 3ea8d9ffb7.
2026-05-22 17:41:30 +02:00
David Heinemeier Hansson bbdf873aec Extract the runner logic into its own file so the stubs can be minimal 2026-05-22 17:37:43 +02:00
David Heinemeier Hansson 32bfddefc5 Refresh npm wrappers 2026-05-22 17:37:43 +02:00
David Heinemeier Hansson b7df0b9a2e Always use OMARCHY_PATH 2026-05-22 17:37:43 +02:00
David Heinemeier Hansson 5e33dac017 Fix paths and alacritty desktop app 2026-05-22 17:37:43 +02:00
David Heinemeier Hansson fc7a522782 Fix the 5-day lag on new npm package installs 2026-05-22 17:37:43 +02:00
Ryan Hughes 3ea8d9ffb7 Provide better scaling 2026-05-22 11:30:43 -04:00
David Heinemeier Hansson baffca520f Use proper off temp in night light
Closes #5941
Co-authored-by: @m-kudahl
2026-05-22 15:20:36 +02:00
David Heinemeier Hansson bb6632ad82 Unmute when turning volume up/down
Closes #5942
Co-authored-by: @Pegrorim
2026-05-22 15:18:14 +02:00
David Heinemeier Hansson ccfee4e94d Just use shift 2026-05-22 14:51:44 +02:00
David Heinemeier Hansson 1bd3c8a9b0 Add codex yolo mode 2026-05-22 14:43:45 +02:00
David Heinemeier Hansson 7b83d847a6 Use shift instead as more reliable with these special keys 2026-05-22 14:39:18 +02:00
David Heinemeier Hansson 923b450d7f Update the editor change 2026-05-22 14:24:15 +02:00
David Heinemeier Hansson d13aeacabe Fix the launcher to do inline replacement so it can be used as $EDITOR properly 2026-05-22 14:24:04 +02:00
David Heinemeier Hansson e16b0f5201 Recover audio services blocked on USB 2026-05-22 13:26:46 +02:00
David Heinemeier Hansson dfd6e2a7ca Preserve playback when switching audio outputs 2026-05-22 13:26:41 +02:00
David Heinemeier Hansson 2901a95d56 Refine audio panel source presentation 2026-05-22 13:26:35 +02:00
David Heinemeier Hansson 1636242191 Add media source switching 2026-05-22 13:26:26 +02:00
David Heinemeier Hansson 94530d89de Refine Bluetooth panel device actions 2026-05-22 13:26:19 +02:00
David Heinemeier Hansson 274259934f Use headphone icon for audio sinks 2026-05-22 11:48:42 +02:00
David Heinemeier Hansson c522757acf Suppress brightness OSD from display panel 2026-05-22 11:35:36 +02:00
David Heinemeier Hansson 7b6a03fa08 Refine shell panel transitions and layout 2026-05-22 11:22:42 +02:00
David Heinemeier Hansson 4055fa7cae Use uppercase Bluetooth panel headers 2026-05-22 11:02:46 +02:00
David Heinemeier Hansson e9eeb0bdfc Refine network panel connection details 2026-05-22 10:55:42 +02:00
David Heinemeier Hansson 590921d397 Add new email hotkey 2026-05-22 10:19:22 +02:00
David Heinemeier Hansson a12fdf96c4 Keep glyph-only notifications compact 2026-05-22 10:16:29 +02:00
David Heinemeier Hansson 336e7a790f Track image clipboard history 2026-05-22 10:10:33 +02:00
David Heinemeier Hansson ab78fd07ef Copy screenshots as PNG clipboard data 2026-05-22 10:10:30 +02:00
David Heinemeier Hansson 23b6cc29c9 Improve network panel Wi-Fi grouping 2026-05-22 09:46:41 +02:00
David Heinemeier Hansson d0795793a3 Add media source switcher 2026-05-22 09:46:17 +02:00
David Heinemeier Hansson 72ee480599 Prioritize oldest playing media source 2026-05-22 09:46:11 +02:00
David Heinemeier Hansson 4f9aa96de0 Beef up audio recovery when something goes wrong 2026-05-22 09:39:26 +02:00
David Heinemeier Hansson bfbf8e689a Tweaks 2026-05-22 09:10:33 +02:00
David Heinemeier Hansson 8cc5dde373 Unified look 2026-05-22 08:59:00 +02:00
David Heinemeier Hansson 181ce06a5b Tab to move quicker between sections 2026-05-22 08:46:19 +02:00
David Heinemeier Hansson 127a43a700 Live up/down stats and unified look 2026-05-22 08:46:19 +02:00
Ryan Hughes d13854857e Add MultiSelect 2026-05-21 16:35:26 -04:00
David Heinemeier Hansson 640e8056a0 Tweaks 2026-05-21 21:14:19 +02:00
David Heinemeier Hansson b28aaf4edb Bring in network panel to the same aesthetic 2026-05-21 21:08:32 +02:00
David Heinemeier Hansson df48130677 Bring in line 2026-05-21 20:43:37 +02:00
David Heinemeier Hansson d52354406d Fun panels 2026-05-21 20:19:40 +02:00
David Heinemeier Hansson fed2298235 Consistency 2026-05-21 19:30:30 +02:00
David Heinemeier Hansson 486c73e6df Preserve Wi-Fi password input during scans 2026-05-21 19:03:42 +02:00
David Heinemeier Hansson 328a9c007a Style 2026-05-21 16:00:03 +02:00
David Heinemeier Hansson faa9cecb34 Don't need to quote 2026-05-21 15:45:36 +02:00
David Heinemeier Hansson bb95c2f26b Don't need 2026-05-21 15:39:25 +02:00
David Heinemeier Hansson 8510a81921 Toggle weather panel from shortcut 2026-05-21 13:31:28 +02:00
David Heinemeier Hansson c3cabffa44 Use launcher hides list 2026-05-21 12:43:03 +02:00
David Heinemeier Hansson 4cd23d9584 Extract stats and clipboard helpers 2026-05-21 11:58:43 +02:00
David Heinemeier Hansson 8734464439 Extract audio default device helpers 2026-05-21 11:53:48 +02:00
David Heinemeier Hansson e5b585d6b3 Extract notification window focus helper 2026-05-21 11:44:11 +02:00
David Heinemeier Hansson d132e0ec61 Use power profile helper in panel 2026-05-21 11:39:55 +02:00
David Heinemeier Hansson 25c3752051 Simplify Bluetooth device actions 2026-05-21 11:29:55 +02:00
David Heinemeier Hansson d9622d6b99 Extract shell panel status commands 2026-05-21 10:54:23 +02:00
David Heinemeier Hansson deae101d6a Extract network status shell logic 2026-05-21 10:44:05 +02:00
David Heinemeier Hansson b19dc7eae2 Switch Wi-Fi setup to NetworkManager 2026-05-21 10:19:36 +02:00
David Heinemeier Hansson 190c31503c Merge branch 'dev' into omarchy-shell 2026-05-21 09:56:50 +02:00
David Heinemeier Hansson e36ba09d02 Updated btop theme for solitude theme by HANCORE 2026-05-21 09:55:14 +02:00
Jisnu 9c8f6cf2cb fix: remove pseudotile and fix -1 gradient colors for Hyprland 0.55.x (#5922) 2026-05-21 08:21:42 +02:00
David Heinemeier Hansson aa5896766b Mirror lock input across screens 2026-05-21 08:08:59 +02:00
David Heinemeier Hansson 80469fc51e Fix night light status when hyprsunset is unavailable 2026-05-21 07:59:35 +02:00
David Heinemeier Hansson d1b452cc5f Lock before suspend with sleep inhibitor service 2026-05-21 07:57:08 +02:00
Pavel Kadera b911a6f8ba Only bind F9 to Voxtype if installed (#5882) (#5897) 2026-05-21 06:48:20 +02:00
Ryan Hughes 0762b20e3e Fix freezing before setting 2026-05-20 22:29:15 -04:00
David Heinemeier Hansson e1066e67d5 Switch media controls to QS MPRIS 2026-05-20 22:53:50 +02:00
David Heinemeier Hansson d4d0c6d7bb Try more detail 2026-05-20 22:41:40 +02:00
David Heinemeier Hansson 34cd776671 Use glyphs 2026-05-20 22:35:39 +02:00
David Heinemeier Hansson e021863b67 Convert tui and web app remove to QS menus 2026-05-20 22:33:45 +02:00
David Heinemeier Hansson 733f1661be Adjust for when there are no icons 2026-05-20 22:20:58 +02:00
David Heinemeier Hansson 5bc151f994 Convert theme remove to a QS menu 2026-05-20 22:18:38 +02:00
David Heinemeier Hansson 16193e7c93 Convert tz setting to a QS menu 2026-05-20 22:10:46 +02:00
David Heinemeier Hansson f9a2685aab Use capitalized camel for all classes 2026-05-20 22:00:10 +02:00
David Heinemeier Hansson ac859b5157 Halve Style.gapsOut relative to Hyprland's general:gaps_out
The Hyprland gaps_out value (10px out of the box) reads well as a
window-to-window gap but felt cavernous when used as the screen-edge
margin for panels and notifications. Halve it at the source — panels
that anchor via PopupCard.margin, the launcher / clipboard / emojis /
polkit / menu / settings card-width clamps, and the notification popup
margins all read Style.gapsOut, so one tweak moves them in lockstep.

Revert the per-call /2 in the notifications service, since
Style.gapsOut now already encodes the smaller value.
2026-05-20 21:44:36 +02:00
David Heinemeier Hansson 06a312240e Simplify 2026-05-20 21:44:08 +02:00
David Heinemeier Hansson 9211b4b9c3 Halve the gap around notification popups on the top + right edges
Sit notifications closer to the top and right edges than the default
gapsOut. The bar-height clearance still applies to the top — only the
free gap portion is halved.
2026-05-20 21:40:37 +02:00
David Heinemeier Hansson 3878ad3a57 Make clock's alt format include the ISO week number
Set the default formatAlt to "dd MMMM 'W'ww yyyy" (was "dd MMMM yyyy")
in shell-defaults.json, the bundled settings-panel defaults, and the
form placeholder text — clock's own widget fallback already used this
spec but the shipped config overrode it.

Qt.formatDateTime doesn't recognize `ww` and renders it literally
(`20 May Www 2026`), so add an `isoWeek` helper to clock.qml that
pre-substitutes `ww` with a Qt-quoted literal of the ISO 8601 week
number before handing the format off to Qt. Users get `20 May W21 2026`
without anyone needing to know about the substitution.
2026-05-20 21:37:48 +02:00
David Heinemeier Hansson 2b03aec510 Capitalize 2026-05-20 21:34:19 +02:00
David Heinemeier Hansson 6090d46dfd Drop the daytime widget, use clock everywhere
Clock and daytime were near-duplicates with different click semantics
(clock toggles between formats on left-click and opens the timezone
selector on right-click; daytime was non-pressable with a hover tooltip).
Standardize on clock, since it's the more capable of the two.

- Delete widgets/daytime.qml and its FirstPartyWidgets entry.
- Migrate shell-defaults.json, shell.qml's builtin fallback, Bar.qml's
  fallback bar config, and the settings panel's builtin defaults so the
  center anchor and center-section entry both reference "clock".
- Rename the daytimeSettings form to clockSettings and rephrase
  "Tooltip format" → "Alternate format (click to toggle)" so the field
  label matches clock's actual behavior.
- Update the gallery dropdown options and bar/README docs.
2026-05-20 21:23:30 +02:00
David Heinemeier Hansson 9732279832 Fix off-by-two indentation on ImagePicker card properties 2026-05-20 20:37:50 +02:00
David Heinemeier Hansson b54d8e04ae Collapse focus/hover/normal ternaries into Style.controlFill helpers
TextField, NumberField, Dropdown, SearchableDropdown, and Toggle all
painted their background, border color, and border width with the same
three-line ternary ladder (`_focused ? focusFill : _hot ? hoverFill :
normalFill`). Five components × three properties × three branches is a
lot of room for one of them to drift from the others when a new state
ever gets added.

Add controlFill / controlBorder / controlBorderWidth on Style and rewrite
each call site as a single binding. No visual change.
2026-05-20 20:37:04 +02:00
David Heinemeier Hansson 8ed6e302b0 Small cleanups: reuse Util.clamp in OSD; PanelSlider hover via _hot
OSD reimplemented clamp() inline when Util already has one.

PanelSlider's knob-scale binding read `mouseArea.containsMouse` directly
instead of going through a `_hot` like the other interactive controls
(TextField, NumberField, Dropdown). Mirror them so future style work
has a single property to override.
2026-05-20 20:33:27 +02:00
David Heinemeier Hansson 04e7820ebe Drop Bar.qml's private ModuleButton, reuse WidgetButton
ModuleButton was a near-duplicate of WidgetButton with the same text /
active / keepSpace / tooltip / fontSize / fixedWidth / fixedHeight /
pressed signal surface, only differing in that it closed over the
enclosing root via QML's scoping rules instead of taking `bar` as a
property. Switch CustomCommandModule onto WidgetButton with
`bar: root` so we have one bar button component to maintain.
2026-05-20 20:32:31 +02:00
David Heinemeier Hansson 783cccac3c Consolidate OMARCHY_MENU_FONT lookups onto Style.font.menuFamily
Five plugins (menu, launcher, clipboard, emojis, polkit) each looked up
OMARCHY_MENU_FONT with the same env-or-monospace fallback. Hoist the
resolution into Style so plugins read one source of truth, and so the
fallback follows the real fontconfig family Style already tracks instead
of the literal string "monospace".
2026-05-20 20:31:04 +02:00
David Heinemeier Hansson c1fca7cbe4 Use Color.foreground / Style.font.family for fallbacks
Five sites hardcoded "#cacccc" or "JetBrainsMono Nerd Font" as the
fallback when no bar context was available. Those magic strings are
the *current* palette defaults — they diverge silently when the theme
changes. Bind to the Color / Style singletons so the fallback tracks
whatever the theme says the foreground / monospace family should be.

Color.qml itself keeps the literal "#cacccc" — that's the seed value
the singleton is bootstrapped with before the theme loads.
2026-05-20 20:29:23 +02:00
David Heinemeier Hansson 73e6127d4e Lift setting() onto the Panel base too
Matches the BarWidget extraction in the previous commit. The weather
panel was the only panel reimplementing the helper inline; future
panels with inline settings get it for free.
2026-05-20 20:27:59 +02:00
David Heinemeier Hansson 16604c67b3 Lift setting / vertical / barSize onto BarWidget
Six widgets (clock, daytime, keyboardLayout, lockKeys, activeWindow,
media) shipped identical copies of `function setting(name, fallback)`.
Eight widgets reimplemented `readonly property bool vertical`, and
three reimplemented `readonly property int barSize`. Move all three
onto the BarWidget base so widgets read the same wiring everyone else
gets for free.

While in here, drop the dead `setting()` declaration on media.qml — it
was defined but never called.
2026-05-20 19:50:10 +02:00
David Heinemeier Hansson 4d58dfa27a Honor the user's font on the lock screen
LockView hardcoded font.family to "monospace" in two places, ignoring
the user's OMARCHY_MENU_FONT / fontconfig choice that every other
summoned surface (menu, launcher, polkit, emojis, clipboard) respects.

Bind to Style.font.family so a font change via omarchy-font-set or
fontconfig propagates to the lock screen too.
2026-05-20 19:47:16 +02:00
David Heinemeier Hansson 9baf3f7f79 Fix parseModuleJson undefined call in custom command modules
CustomCommandModule.update() in Bar.qml called root.parseModuleJson(raw)
but the function was removed in 3b970068 (the indicator split). Any
custom command module emitting waybar-style JSON would have crashed on
first poll.

Hoist the helper into Util so both call sites — the bar's custom command
module and BarIndicator.extractData — share one definition.
2026-05-20 19:46:47 +02:00
David Heinemeier Hansson e29394a133 Start cleanup of menu 2026-05-20 19:25:57 +02:00
David Heinemeier Hansson 2a61df0254 Fix vertical bar indicator alignment 2026-05-20 19:11:15 +02:00
David Heinemeier Hansson bdf7bf76cb Extract clock and tray bar widgets 2026-05-20 19:11:00 +02:00
David Heinemeier Hansson d9ec5b82f2 Move weather popup into panel 2026-05-20 18:43:15 +02:00
David Heinemeier Hansson e0b62f7535 Fix indicator hover reveal target 2026-05-20 18:38:21 +02:00
David Heinemeier Hansson cf4729ca2b Extract indicators bar widget 2026-05-20 18:32:00 +02:00
David Heinemeier Hansson 1e63e86133 Extract system update bar widget 2026-05-20 18:28:13 +02:00
David Heinemeier Hansson 4835119e48 Move first-party bar widget catalog out of Bar 2026-05-20 18:27:10 +02:00
David Heinemeier Hansson 647bb88ab2 Extract menu opener widget 2026-05-20 17:53:05 +02:00
David Heinemeier Hansson 1ff343428c Extract workspace switcher widget 2026-05-20 17:53:05 +02:00
Ryan Hughes f0f58fea2f Fix display not showing current scale 2026-05-20 11:49:25 -04:00
David Heinemeier Hansson 4941fe4bf9 Replace calendar widget with daytime label 2026-05-20 16:59:38 +02:00
David Heinemeier Hansson 532db800d7 Remove legacy idle inhibitor widget 2026-05-20 16:33:48 +02:00
David Heinemeier Hansson 234cc5354d Keep active indicator delegates stable 2026-05-20 16:08:56 +02:00
David Heinemeier Hansson dc6468cdfa Stabilize indicator activation rendering 2026-05-20 16:03:51 +02:00
David Heinemeier Hansson fca1393c8c Append newly active bar indicators 2026-05-20 15:58:38 +02:00
David Heinemeier Hansson 5110b0d753 Guard bar tooltips with live hover state 2026-05-20 15:52:43 +02:00
David Heinemeier Hansson 9e77ea3aca Better tooltips 2026-05-20 15:49:24 +02:00
David Heinemeier Hansson b22b0f8640 Prevent stacked bar tooltips 2026-05-20 15:43:21 +02:00
David Heinemeier Hansson 9e48d9658b Add Night Light bar indicator 2026-05-20 15:31:28 +02:00
David Heinemeier Hansson 296ee20e98 Rename notification timer state 2026-05-20 15:23:29 +02:00
David Heinemeier Hansson 32a5938819 Remove notification progress indicator 2026-05-20 15:22:18 +02:00
David Heinemeier Hansson c1b8b18b8f Remove stale bar font config 2026-05-20 15:16:22 +02:00
David Heinemeier Hansson b7cee1a95e Poll power panel details only while open 2026-05-20 15:15:49 +02:00
David Heinemeier Hansson 998e22bb78 Encode background image file URLs 2026-05-20 15:15:38 +02:00
David Heinemeier Hansson ac925e8f3e Show notification countdown progress 2026-05-20 15:15:25 +02:00
David Heinemeier Hansson 8703a46a70 Scope bar interactions to their screen 2026-05-20 15:15:16 +02:00
David Heinemeier Hansson a69b64e1dd Ignore stale image picker scans 2026-05-20 15:14:54 +02:00
David Heinemeier Hansson c12f0b5399 Reset dynamic menu providers on reload 2026-05-20 15:14:39 +02:00
David Heinemeier Hansson 1e9a7a971e Load enabled service plugins 2026-05-20 15:14:04 +02:00
David Heinemeier Hansson 93db08b217 Fix notification focus and image cache races 2026-05-20 15:13:57 +02:00
David Heinemeier Hansson 20ff942481 Fix bar icon positioning in horizontal mode 2026-05-20 14:56:42 +02:00
David Heinemeier Hansson fab0d69e87 Make settings and transparency controllable on vertical bars 2026-05-20 14:51:19 +02:00
David Heinemeier Hansson b337652cd6 Pull closer to use 2026-05-20 14:46:08 +02:00
David Heinemeier Hansson dbdcd862e7 Simplify 2026-05-20 14:35:47 +02:00
David Heinemeier Hansson f6c6a6d323 Simplify 2026-05-20 14:34:15 +02:00
David Heinemeier Hansson ce9afec2a8 Just keep alpha in util 2026-05-20 14:28:50 +02:00
David Heinemeier Hansson c87bfb8da8 Introduce omarchy-shell -q for fire and forget 2026-05-20 14:24:19 +02:00
David Heinemeier Hansson ed3320cb8d Just emojis 2026-05-20 14:12:06 +02:00
David Heinemeier Hansson b7c0e10288 Just clipboard 2026-05-20 14:09:09 +02:00
David Heinemeier Hansson cdd3175b5b So we don't need to pass empty args 2026-05-20 14:08:54 +02:00
David Heinemeier Hansson 4f1ef8d95f Give services their own place 2026-05-20 14:03:56 +02:00
David Heinemeier Hansson 41425481db app-launcher to just launcher 2026-05-20 13:57:22 +02:00
David Heinemeier Hansson ea1b0e7ced Extract panels out into their own top-level concern 2026-05-20 13:51:39 +02:00
David Heinemeier Hansson 3b9700682c Split out indicators 2026-05-20 13:05:56 +02:00
David Heinemeier Hansson 8476fa7d82 Refresh with latest 2026-05-20 13:05:40 +02:00
David Heinemeier Hansson 99162e6548 Cleanup dead 1pw lock instances 2026-05-20 08:42:00 +02:00
David Heinemeier Hansson ba6d38bd81 Don't show cursor when typing 2026-05-20 08:15:22 +02:00
Ryan Hughes da53c0ff60 Add Pi theme based on system theme
Based entirely on the work of @robzolkos in #5711. That attempt was
adapted to better fit the standard templating system while also adding
color mixing for use in other themes.
2026-05-20 01:53:34 -04:00
Ryan Hughes ae6f2e7ac7 Add color mixing 2026-05-20 01:45:20 -04:00
Ryan Hughes 8678dac385 Edge to edge blur 2026-05-19 21:59:06 -04:00
Ryan Hughes 26281b3863 Prevent maching the check causing spontaneous locking 2026-05-19 21:01:15 -04:00
Ryan Hughes ad271c1fcb Fix stream displays 2026-05-19 20:44:54 -04:00
Ryan Hughes b6803aa269 Fix bluetooth 2026-05-19 19:59:27 -04:00
Ryan Hughes 1f3a827701 consolidate logic 2026-05-19 19:18:56 -04:00
Ryan Hughes ccc796a3e0 Consolidate utils 2026-05-19 19:03:11 -04:00
Ryan Hughes 0422716374 More cleanup 2026-05-19 18:06:08 -04:00
Ryan Hughes 72c9331c72 Make lock screen behave like a lock screen 2026-05-19 17:20:45 -04:00
Ryan Hughes 201a003537 More tokens 2026-05-19 16:55:56 -04:00
David Heinemeier Hansson 97d4e908af Use the helpers where applicable 2026-05-19 22:18:13 +02:00
David Heinemeier Hansson 42f03655df Extract commonality 2026-05-19 22:13:09 +02:00
David Heinemeier Hansson 4f41a6c4fd Ensure hidden desktop files aren't in the launcher 2026-05-19 22:08:39 +02:00
David Heinemeier Hansson 86fec6d5a5 Add notice if app is taking a while to launch 2026-05-19 22:08:39 +02:00
David Heinemeier Hansson 463ea96910 Simpler 2026-05-19 22:07:53 +02:00
Ryan Hughes b45795127d Rearrange common tokens 2026-05-19 16:06:48 -04:00
Ryan Hughes 6f72604f2c Add missing alpha tokens 2026-05-19 15:54:00 -04:00
Ryan Hughes c26a957b8f Add color tokens for app-launcher 2026-05-19 15:54:00 -04:00
David Heinemeier Hansson 109d17b95b One rule 2026-05-19 21:53:57 +02:00
David Heinemeier Hansson 0dfbd74663 Drop old compatibility functions 2026-05-19 21:45:52 +02:00
David Heinemeier Hansson d468c392a9 Make config/uwsm something you should never need to edit 2026-05-19 21:42:02 +02:00
Ryan Hughes 877c5d02f8 Add color tokens for menu 2026-05-19 15:34:19 -04:00
David Heinemeier Hansson bffc8f1fae Make more resilient 2026-05-19 21:33:12 +02:00
David Heinemeier Hansson 1a630b1239 Pull low battery warning into shell 2026-05-19 21:28:32 +02:00
Ryan Hughes 15f461c1df Add tooltip color tokens 2026-05-19 15:16:10 -04:00
David Heinemeier Hansson 441072db52 No longer needed with HL 0.55 2026-05-19 21:15:33 +02:00
David Heinemeier Hansson c7f3a8e049 Use new style bindings 2026-05-19 21:13:40 +02:00
David Heinemeier Hansson 61bb42ce73 Remove menu options that now live elsewhere in bar panels 2026-05-19 21:09:39 +02:00
David Heinemeier Hansson 28b572efb8 Clarify for new reality 2026-05-19 21:05:24 +02:00
David Heinemeier Hansson 0f4490c9fc Leave this to the themes 2026-05-19 21:01:51 +02:00
David Heinemeier Hansson abf0a68b54 Drop walker + elephant 2026-05-19 20:54:20 +02:00
David Heinemeier Hansson 7fe472bf8a Simplify 2026-05-19 20:54:20 +02:00
David Heinemeier Hansson 9d81863a63 Not actually used at the moment 2026-05-19 20:54:20 +02:00
David Heinemeier Hansson bad94996ac Simplify as we don't need to do any watching
Just rely on explicit applyTheme/reloadTheme
2026-05-19 20:54:20 +02:00
David Heinemeier Hansson 695d2c6e7c No longer used 2026-05-19 20:54:20 +02:00
David Heinemeier Hansson 6849d0f0f3 Stop using the beta 2026-05-19 20:54:20 +02:00
Ryan Hughes b59a8d0bb0 Add Battle.net installer 2026-05-19 14:09:04 -04:00
David Heinemeier Hansson a157b98d5b No longer need wiremix 2026-05-19 18:51:05 +02:00
David Heinemeier Hansson 7ea4e1ab04 Switch hyprlock to QS 2026-05-19 17:08:14 +02:00
David Heinemeier Hansson 0f5044167c Move from hypridle to QS 2026-05-19 12:02:28 +02:00
David Heinemeier Hansson 48dacca6ea Not needed 2026-05-19 12:00:15 +02:00
David Heinemeier Hansson 2d966c2b90 Ensure static wallpaper doesn't ask for updates 2026-05-19 11:29:03 +02:00
David Heinemeier Hansson 1a3de2a071 Kill the old instances 2026-05-19 11:20:53 +02:00
David Heinemeier Hansson c6d7aa9494 Drop the systemd running on advice from outfoxxed 2026-05-19 11:20:29 +02:00
David Heinemeier Hansson 2896fc5c98 Merge branch 'dev' into omarchy-shell 2026-05-19 10:50:47 +02:00
David Heinemeier Hansson 8fa463975a Remove dead code 2026-05-19 10:50:36 +02:00
David Heinemeier Hansson 8cd9e34294 Tweak idle inhibitor 2026-05-19 10:48:28 +02:00
David Heinemeier Hansson 8da9c27aef Consistent naming 2026-05-19 09:50:20 +02:00
David Heinemeier Hansson eff07700e0 Respect style corner radius 2026-05-19 09:47:26 +02:00
Iqbal AttilaandIqbal Attila ed93a047ee Fix power profile race on plug/unplug events (#5898)
The udev power_supply rule fires before sysfs `online` is updated on
some laptops (notably Lenovo Yoga Pro 7 14IAH10 with USB-C charging).
The autodetect branch reads the stale value and sets the wrong profile,
so the system stays on `performance` after unplug (or vice versa).

A single plug/unplug typically fires 3-4 udev events within ~1s (ACAD +
each USB-C port). With `systemd-run --unit=omarchy-power-profile
--collect`, duplicate invocations against the same unit name can be
dropped while the first is still running, so the surviving event isn't
guaranteed to be the latest one.

This adds a 0.3s settle delay at the top of the autodetect branch only.
Explicit `ac` and `battery` invocations remain instant.

Tested on Lenovo Yoga Pro 7 14IAH10 (Intel Core Ultra 9 285H, Omarchy
3.8.1).

Co-authored-by: Iqbal Attila <iqbal@kcmon.id>
2026-05-19 09:42:13 +02:00
Axel Fontaine df82125953 Enable Docker multi-arch builds by default (#5901)
* Enable Docker multi-arch image building

* Added migration for Docker multi-arch image building
2026-05-19 09:40:54 +02:00
David Heinemeier Hansson 2ca043c8b9 More massaging 2026-05-19 08:42:53 +02:00
David Heinemeier Hansson c6df292c80 Don't need that duplex info 2026-05-19 08:42:49 +02:00
David Heinemeier Hansson 8613ead8c1 Signify when there are multiple displays 2026-05-19 08:20:49 +02:00
David Heinemeier Hansson 2ac069e72b Drop bluetui and impala in favor of shell controls 2026-05-19 08:20:13 +02:00
Ryan Hughes 7800937b29 Use accent border for popup surfaces 2026-05-19 01:54:08 -04:00
Ryan Hughes d76cf3d0d2 Align muted volume OSD glyph 2026-05-19 01:37:43 -04:00
Ryan Hughes 75962b98ea Fix audio panel scroll clipping 2026-05-19 01:32:32 -04:00
Ryan Hughes 8a74d28449 Show forget action for saved Wi-Fi networks 2026-05-19 01:15:07 -04:00
Ryan Hughes 832244c10e Refine shell cursor highlights 2026-05-19 01:02:16 -04:00
Ryan Hughes efd46234f0 Improve dynamic widget settings forms 2026-05-18 23:46:21 -04:00
Ryan Hughes 61e7d2f307 Use singular microphone label 2026-05-18 23:10:24 -04:00
Ryan Hughes fca58df0a5 Clean up audio device choices 2026-05-18 22:46:30 -04:00
Ryan Hughes 34ff56ae19 Detect PipeWire playback streams by sink role 2026-05-18 21:51:21 -04:00
Ryan Hughes f1e6fe86b7 Remove legacy battery bar module 2026-05-18 21:27:00 -04:00
Ryan Hughes 6fa6e6cc91 Merge remote-tracking branch 'origin/omarchy-shell' into omarchy-shell
# Conflicts:
#	shell/Ui/PillButton.qml
#	shell/plugins/bar/widgets/monitorPanel.qml
#	shell/plugins/bar/widgets/networkPanel.qml
2026-05-18 21:11:37 -04:00
Ryan Hughes 8b2bb217d6 Standardize shell UI theme tokens 2026-05-18 20:13:12 -04:00
David Heinemeier Hansson 34bb950830 Keyboard nav 2026-05-18 22:50:38 +02:00
David Heinemeier Hansson bc1b824891 More tweaks 2026-05-18 22:47:06 +02:00
David Heinemeier Hansson 1a457118a5 Ensure you can click to open another panel while one is open 2026-05-18 22:38:53 +02:00
David Heinemeier Hansson 9daa0e6ed3 More tweaks 2026-05-18 22:10:26 +02:00
David Heinemeier Hansson 74f71236e0 More tweaking 2026-05-18 22:00:09 +02:00
David Heinemeier Hansson e8bfdbb13b Little more tweaks 2026-05-18 21:24:48 +02:00
David Heinemeier Hansson d99ccaf550 More power panel layouting 2026-05-18 21:13:42 +02:00
David Heinemeier Hansson 196dcd2472 Open power panel on super + ctrl + p 2026-05-18 21:13:37 +02:00
David Heinemeier Hansson 1dc2f1ec79 Introduce power panel 2026-05-18 20:47:38 +02:00
David Heinemeier Hansson c517997922 Spinning when scanning 2026-05-18 20:47:38 +02:00
Ryan Hughes 44dff2d23d Bar settings: use qs.Ui.ButtonGroup for the position picker
The custom PositionButtonGroup component wrapped Repeater + Button
directly without setting bordered: true on the delegates. After the
recent Button changes (idle non-bordered Buttons have zero border
width), the non-selected position chips lost their visible chip
structure entirely \u2014 they rendered as floating text labels and read
as unclickable.

Replace the bespoke component with qs.Ui.ButtonGroup (which sets
bordered: true on each chip) inside a Column with a 'Position' label,
and drop the now-unused PositionButtonGroup definition. All four
options now read as clickable chips and the chosen value still paints
the accent fill+border.
2026-05-18 12:41:55 -04:00
Ryan Hughes d4bf8d0eea omarchy dev ui-preview: jump to a specific section via argument
Pass a section name to open the gallery with the cursor already on
that component instead of scrolling from the top. Useful when
iterating on a single widget.

  omarchy dev ui-preview              # default position
  omarchy dev ui-preview button       # land on the Button section
  omarchy dev ui-preview button-group # land on ButtonGroup
  omarchy dev ui-preview slider       # land on Slider, etc.

The bin script forwards the argument as {"section": "..."} in the
shell summon payload. The gallery's open(payloadJson) parses it,
clamps against visibleSections (unknown names fall back to default),
and sets focusSection + selectedIndex inside the existing Qt.callLater
so the cursor's hasCursor bindings fire ensureCursorVisible
automatically.
2026-05-18 12:36:46 -04:00
Ryan Hughes b25dd100e3 Make Button state composition visible in ButtonGroup and the gallery
Three small fixes around the new Button + ButtonGroup so the cursor is
always visible and the section reads as a single Button showing flag
combinations.

ButtonGroup now exposes cursorIndex + hovered(index, isHovered) so
panels can drive a single Button's hasCursor without reaching past
ButtonGroup's API. Every chip gets bordered: true so the row reads as
a real picker — distinct outlines, not just a tinted fill that vanishes
into the background.

Button's border treatment grows two paths so the cursor is always
visible regardless of other state: bordered + hot recolors the border
to the accent and thickens to the focus-ring width, and selected + hot
thickens the existing accent border to the focus-ring width (so the
cursor landing on the chosen option still reads as cursor presence,
instead of disappearing into the selected fill).

Gallery's Button section pairs each demo button with a small caption
('idle', 'active', 'icon only', 'icon + active', 'bordered +
focusable') so the row reads as one Button with its state flags side
by side rather than five unrelated buttons. The ButtonGroup section
gets the cursor wiring (cursorIndex + onHovered) plus an
onFocusedChanged auto-scroll so the cursor visual is reachable via
j/k h/l from the panel cursor model.
2026-05-18 12:33:24 -04:00
Ryan Hughes 8b858d4093 Add outline cursor mode to CursorSurface for slider rows
Wide content rows (slider rows in audio / monitor panels) opted out of
the cursor fill by setting fill: "transparent" on their CursorSurface,
which left them with no cursor visual at all — j/k landed there
silently with no way to know what was selected.

Add an `outline: bool` flag to CursorSurface. When true, hasCursor
paints an accent border (Style.focusBorderColor at
Style.focusBorderWidth) instead of a fill, leaving the row's chrome
fully visible underneath. Use for slider rows where a fill would
obscure the track.

Audio output / input slider rows and the monitor brightness row opt
in. The dev gallery's slider section replaces its bespoke Rectangle
wrapper with the same CursorSurface { outline: true }, so what the
gallery documents is what the panels ship.
2026-05-18 12:19:23 -04:00
Ryan Hughes b5f7ba02c2 Unify PillButton, CursorPill, ChoiceButton into qs.Ui.Button + ButtonGroup
The three components were three takes on the same shape \u2014 a clickable
rectangle with text/icon, a hot/hover state, an optional persistent
border, and an optional 'selected' or 'active' highlight. CursorPill
was a 30-line PillButton wrapper that added a hovered() signal;
ChoiceButton was effectively PillButton with selected: bool painting
an accent fill+border.

Collapse them into a single qs.Ui.Button. State flags compose
independently:

  hasCursor / hover         hot fill
  active                    persistent foreground-tint fill
  selected                  accent fill + accent border
  bordered: true            persistent 1px idle border (form primaries)
  focusable: true           Tab focus paints the accent ring
  pressed                   pressed fill

The hovered(bool) signal is now built-in, so CursorPill's wrapper is
unnecessary. ButtonGroup wraps a Row+Repeater for the form-style
'pick one of N' pattern; panel-cursor-driven cases still compose
Buttons directly in a Row with per-instance hasCursor wiring.

Theme tokens move into a new [style] section in shell.toml:

  border-width        = 1
  focus-border-width  = 3
  idle-border-alpha   = 0.4
  hot-fill-alpha      = 0.08
  selected-fill-alpha = 0.18
  pressed-fill-alpha  = 0.22
  focus-fill-alpha    = 0.22

Style.qml parses these out of the same shell.toml [font] / [bar]
already reads, and exposes pre-computed Style.hotFill / selectedFill /
pressedFill / idleBorderColor / selectedAccentFill / borderWidth +
the existing focusBorder* tokens. Themes that don't ship a [style]
section get the previous defaults unchanged.

Dev gallery consolidates three sections (PillButton, CursorPill,
ChoiceButton) into Button + ButtonGroup, with the cursor model
sections renamed accordingly.
2026-05-18 12:09:13 -04:00
Ryan Hughes 42dee4d399 Standardize cursor chrome to fill-only across qs.Ui
CursorSurface defines the canonical cursor chrome: a tinted fill, no
border. Three components had inconsistent treatments that the recent
standardization pass left half-finished:

- PillButton kept a 1px foreground border on hot (cursor or hover)
  via a cursorBordered: true default.
- CursorPill specifically had to override cursorBordered: false to
  match the rest of the kit.
- ChoiceButton thickened its border to 2px on hasCursor, which mixed
  the cursor visual with the Tab-focus visual.

Drop the cursorBordered escape hatch from PillButton (no caller other
than CursorPill ever used it), drop CursorPill's now-redundant
override, and remove hasCursor from ChoiceButton's border.width
thickening. Update Toggle's class comment (it still claimed cursor
mirrored Tab focus, which f3ba5055 broke without updating the doc).

Now: hot/hasCursor = fill only, activeFocus = accent border ring,
selected/active = stronger fill or accent border per component
semantic. One canonical pattern across the kit.
2026-05-18 11:52:23 -04:00
Ryan Hughes 648bc54db1 Unify font sizes through qs.Commons.Style
Themes now drive typography the same way they drive colors: one [font]
base-size in shell.toml is the rem root, and every Style.font.<token>
(caption, bodySmall, body, subtitle, title, heading, display,
displayLarge, iconSmall, icon, iconLarge) derives from it via a fixed
multiplier. Themes can also pin individual tokens for stylistic
emphasis. base-size is clamped 11..13 until row-height tokens exist.

Bar dimensions move to the same singleton: [bar] size-horizontal /
size-vertical replace the hardcoded 26/28 in Bar.qml, exposed as
Style.bar.sizeHorizontal / sizeVertical.

Style.qml also resolves the fontconfig 'monospace' alias via fc-match
and exposes Style.font.resolvedFamily so panels can display the
concrete family. Watches ~/.config/fontconfig/fonts.conf so it tracks
'omarchy font set <name>'.

The qs.Ui kit (PillButton, Dropdown, Toggle, TextField, etc.) and
every first-party plugin (bar widgets, settings, menu, clipboard,
emoji, polkit, notifications, osd, image-picker, dev-gallery) now
bind to Style.font.* instead of pixel literals. Only three deliberate
display-scale outliers remain: the notification empty-state glyph and
the weather flyout's hero temperature pair, all commented.

Background plugin's applyTheme IPC fast-path also pushes shell.toml to
Style so theme swaps update typography and bar size without waiting
for inotify debounce.

Dev gallery (omarchy dev ui-preview) now ships a Typography section
that renders the full scale and theme tokens live, and its summon
command is fixed (omarchy-shell-ipc -> omarchy-shell).
2026-05-18 11:33:19 -04:00
Ryan Hughes c26cf49f7d Fix settings widget dialog and fallback defaults 2026-05-18 10:17:01 -04:00
Ryan Hughes f48eef58e0 Trim doc and comment drift in qs.Ui kit 2026-05-18 10:17:01 -04:00
Ryan Hughes e9e5040bf3 Refresh idle indicator on bar IPC 2026-05-18 10:17:01 -04:00
David Heinemeier Hansson bbdc485a62 Open the right panels 2026-05-18 15:54:21 +02:00
David Heinemeier Hansson f3ba505558 More tweaking and standardization 2026-05-18 15:54:14 +02:00
David Heinemeier Hansson acf617e05e No animations for any of these pickers 2026-05-18 15:53:49 +02:00
David Heinemeier Hansson debda5cef8 Just make bluetooth panel show human options 2026-05-18 15:41:21 +02:00
David Heinemeier Hansson cddc18b1cd Panel tweaks 2026-05-18 15:30:29 +02:00
David Heinemeier Hansson 0fe985b45d Promote shell to its own top-level directory 2026-05-18 14:56:59 +02:00
David Heinemeier Hansson d782705878 Match offset and color to existing notifications 2026-05-18 14:34:11 +02:00
David Heinemeier Hansson ff40026b7b Use consistent corner radius for themes 2026-05-18 14:30:49 +02:00
David Heinemeier Hansson 4551b8920a Fix glyph notifications to be slim 2026-05-18 14:29:43 +02:00
David Heinemeier Hansson b0f5d941ba Switch to running quickshell as a systemd service that is auto-restarted if it crashes 2026-05-18 13:58:07 +02:00
David Heinemeier Hansson 8424718d1b No animations on bar location changes 2026-05-18 13:56:26 +02:00
Ryan Hughes ae17436089 Update bindings 2026-05-18 03:19:58 -04:00
Ryan Hughes d6708ffe69 Consolidate nested READMEs into docs/; trim drift 2026-05-18 03:16:28 -04:00
Ryan Hughes 2314c821a3 Move Ui kit notes into the gallery, drop Ui/README.md 2026-05-18 03:05:14 -04:00
Ryan Hughes d01d1dcdb3 Add qs.Ui.NumberField; migrate weather + spacer settings 2026-05-18 02:58:56 -04:00
Ryan Hughes 259cda3342 Bar settings: widget settings open as modal overlay inside the panel 2026-05-18 02:53:25 -04:00
Ryan Hughes a1e11f68b8 Add omarchy dev ui-preview to open the qs.Ui dev gallery 2026-05-18 02:49:04 -04:00
Ryan Hughes c17ab99fa4 Stay Awake: filled red coffee cup when active, empty cup at rest 2026-05-18 02:45:19 -04:00
Ryan Hughes 2a6749dc6c Stay Awake: fix state detection, rename, notify on toggle 2026-05-18 02:40:19 -04:00
Ryan Hughes 8272c6d881 Remove legacy bar widget modules 2026-05-18 02:33:43 -04:00
Ryan Hughes 713a32e61d Bar settings + Add widget: include widget description in option rows 2026-05-18 02:20:00 -04:00
Ryan Hughes 135d21cae2 Bar settings + Add widget: button-style trigger, fit inside panel 2026-05-18 02:18:07 -04:00
Ryan Hughes 030963768b Dropdown/SearchableDropdown: arrow keys + row padding 2026-05-18 02:08:03 -04:00
Ryan Hughes 7cdf35e462 Fix gallery navigation: arrow keys, cursor stuck past ChoiceButton, scroll bounce 2026-05-18 01:57:26 -04:00
Ryan Hughes 54da1a90ab Gallery: scroll initial cursor target into view on open 2026-05-18 01:47:29 -04:00
Ryan Hughes b8188ce46c Gallery: replicate the panel cursor model from wifi/audio/bluetooth/monitor 2026-05-18 01:43:23 -04:00
Ryan Hughes 91a87470a2 Add hasCursor + hovered surface to four kit primitives 2026-05-18 01:31:37 -04:00
Ryan Hughes 23f1ba1b3f Dropdown/SearchableDropdown: 4px spacing between popup rows 2026-05-18 01:17:35 -04:00
Ryan Hughes 4b05472768 Gallery PanelToolTip swatch follows theme corners, PanelActionButton uses MDI cog 2026-05-18 01:15:50 -04:00
Ryan Hughes 9c656df0ac Fix monitor brightness h/l doing nothing: id shadowing root property 2026-05-18 01:05:33 -04:00
Ryan Hughes 72939ac530 Fix Dropdown/SearchableDropdown integration issues 2026-05-18 00:59:40 -04:00
Ryan Hughes 8ec794a1b4 Fix brightness slider h/l bouncing to zero 2026-05-18 00:55:46 -04:00
Ryan Hughes 0d6c58a2df Bar settings: nerd font glyphs + SearchableDropdown for "+ Add widget" 2026-05-18 00:54:53 -04:00
Ryan Hughes db4fa1f760 Add qs.Ui.Dropdown, SearchableDropdown, Toggle rounded variant 2026-05-18 00:52:59 -04:00
Ryan Hughes 4b4df8b4f5 Fold settings ActionPill + IconButton into qs.Ui kit 2026-05-18 00:29:26 -04:00
Ryan Hughes 4529988676 Migrate monitorPanel onto the keyboard-driven panel chassis 2026-05-18 00:24:53 -04:00
Ryan Hughes 1dd30e5c28 Extract qs.Ui.TextField, migrate wifi passphrase + settings + dynamic form 2026-05-18 00:22:50 -04:00
Ryan Hughes 64115a9cb4 Read cornerRadius from Hyprland instead of the quickshell-menu.json toggle 2026-05-17 23:31:47 -04:00
Ryan Hughes b1e3a23088 Gallery covers new components; add qs.Ui kit README 2026-05-17 23:19:54 -04:00
Ryan Hughes 3e9104de60 Extract CursorPill, consolidate panel-cursor pill bases 2026-05-17 23:16:56 -04:00
Ryan Hughes b751109c6b Extract PanelKeyCatcher, share key dispatch across wifi/audio/bluetooth 2026-05-17 23:14:12 -04:00
Ryan Hughes daed74733b Extract ChoiceButton + Toggle from bar settings into qs.Ui 2026-05-17 23:10:42 -04:00
Ryan Hughes 5e3659fb8d Promote cornerRadius + focus tokens to qs.Commons.Style singleton 2026-05-17 23:08:08 -04:00
Ryan Hughes 2167bdbe95 Fix gallery ScrollView sizing so Slider and Composed render 2026-05-17 22:52:28 -04:00
Ryan Hughes 6b915926b2 Promote bar/common to qs.Ui kit module 2026-05-17 22:33:08 -04:00
Ryan Hughes 408dada45d Panel UI atoms, keyboard-driven audio + bluetooth, dev gallery 2026-05-17 22:07:51 -04:00
Ryan Hughes d73690b945 Unified cursor chrome across DNS pills and wifi rows
The wifi panel now has full keyboard nav over both sections:
  h/l            walk the DNS provider pills
  j/k            walk the wifi network list (j from DNS drops into the
                 list; k from the top row goes back to DNS)
  Enter/Space    apply the highlighted DNS provider / connect to the
                 highlighted wifi network
  Esc            close the panel

Single-cursor model. The visible cursor lives at the panel root via
focusSection + dnsIndex + selectedIndex. Mouse hover doesn't draw its
own highlight — onContainsMouseChanged on each row/pill MOVES the
cursor instead. Mouse leaving doesn't clear it (the cursor stays where
attention was last; subsequent j/k pick up from there). Last action
wins, whether mouse or keyboard.

New Common.CursorSurface holds the shared chrome (fill + 1px border +
fade) and is the base for both NetworkRow and the new local
DnsProviderPill. Items pass only hasCursor + current and let the
surface render. Contract: items never read containsMouse for visuals.
That's what guarantees exactly one highlight on screen at any time.

DnsProviderPill replaces Common.PillButton for the DNS row because
PillButton's hover > active > background cascade fights the
single-cursor model — its built-in hover would draw a parallel
highlight on whatever pill the pointer is parked on, even after the
cursor moved elsewhere. PillButton stays where it fits (refresh icon,
other bar widgets) and is unchanged here.
2026-05-17 20:15:00 -04:00
Ryan Hughes e3fff41c0e Keyboard-driven wifi panel
The wifi panel was a bar-widget xdg-popup which Hyprland doesn't grant
keyboard focus to until the user clicks or hovers it — keys typed after
a SUPER+CTRL+W summon went to whatever previously had focus.

New shared Common.KeyboardPanel scaffolding: layer-shell PanelWindow
with WlrKeyboardFocus.Exclusive (granted at map time, the protocol-level
equivalent of focus-on-launch for xdg-toplevels), full-screen anchored
with a Region mask that subtracts the bar strip so bar widgets stay
clickable, MouseArea for outside-click dismissal, TransformWatcher for
reactive anchor-position tracking. API is a subset of Common.PopupCard
(no centerOnBar / triggerMode / containsMouse yet — adding when the
other bar popups migrate).

networkPanel.qml uses it. Adds full keyboard navigation:
  j/k or Up/Down  — move selection
  Return/Space    — connect (or disconnect if already connected)
  x               — forget the highlighted network
  r               — refresh scan
  Esc             — close

ListView (with positionViewAtIndex on currentIndex) replaces the
Repeater+Column inside Flickable so the selected row stays visible as
j/k walks past the cap. A 1.5s Timer re-polls detailsProc while open
so the connection details (IP, gateway, signal) populate when routing
actually comes up after a connect rather than waiting for re-summon.

Rows: lock glyph on the right for protected-and-not-connected, X
(forget) for connected — aligned to the same 22-wide column. Connect
glyph in the passphrase prompt switched to a check, no separate cancel
(Esc handles it).

SUPER+CTRL+W in utilities.lua now toggles this panel via
omarchy-shell-ipc instead of launching Impala. Right-click on the bar
icon still launches Impala as a fallback for hidden SSIDs / enterprise
auth / iwd edge cases.
2026-05-17 18:18:53 -04:00
Ryan Hughes fa4d2bc566 Faster restart-shell, sidestep upstream IPC kill crash
quickshell kill currently trips Quickshell #539 (notifyInternal2 event
loop crash via IpcKillCommand::exec), which leaves the Wayland surface
lingering long enough that the new instance starts on top of the dying
one — a duplicate bar. SIGKILL drops the client connection and the
compositor cleans up surfaces immediately; the shell has no persistent
state to flush. Sub-20ms restart, no duplicates.
2026-05-17 18:16:03 -04:00
David Heinemeier Hansson 75f04857f5 No longer used 2026-05-17 22:11:10 +02:00
David Heinemeier Hansson 6ecca2962d Positioning without anim and with explicit buttons 2026-05-17 22:10:43 +02:00
David Heinemeier Hansson 0d98d7e2af Don't show notifications for things that have other visual indicators
Like the bar indicators
2026-05-17 22:09:31 +02:00
David Heinemeier Hansson 671dd4698c Kill this weird pattern 2026-05-17 21:55:53 +02:00
David Heinemeier Hansson c706a244d7 Correct the forecast 2026-05-17 21:55:42 +02:00
David Heinemeier Hansson a41b094baf Always set 2026-05-17 21:51:09 +02:00
David Heinemeier Hansson 9426196a47 Always set 2026-05-17 21:50:35 +02:00
David Heinemeier Hansson d03234fa06 Simplify 2026-05-17 21:47:50 +02:00
David Heinemeier Hansson 89a6517018 Simplify scaling 2026-05-17 21:44:08 +02:00
David Heinemeier Hansson 96f356af27 Slimming down
We are moving to controlling this directly
2026-05-17 21:32:02 +02:00
David Heinemeier Hansson ac3dd76e83 Errant - 2026-05-17 21:30:58 +02:00
David Heinemeier Hansson a60c2a4367 Combine here too 2026-05-17 21:09:24 +02:00
David Heinemeier Hansson 2df003ccc5 Slim 2026-05-17 21:02:18 +02:00
David Heinemeier Hansson 50ed525c69 Combine and cache for faster reactions 2026-05-17 21:00:39 +02:00
David Heinemeier Hansson 01df3669b8 Unused 2026-05-17 20:47:48 +02:00
David Heinemeier Hansson c6917585cd Simpler 2026-05-17 20:47:23 +02:00
David Heinemeier Hansson 04d3170e07 Simpler 2026-05-17 20:47:21 +02:00
David Heinemeier Hansson 20aa4bf205 Don't need fallback for earlier versions 2026-05-17 20:05:58 +02:00
David Heinemeier Hansson d6e43ee6ff Simplify control logic 2026-05-17 20:05:49 +02:00
David Heinemeier Hansson b719de7774 We should do all or not 2026-05-17 20:02:01 +02:00
David Heinemeier Hansson 8718962d13 Not needed 2026-05-17 19:59:49 +02:00
David Heinemeier Hansson 73d3d3a8af Truncate osd text 2026-05-17 19:58:34 +02:00
David Heinemeier Hansson 9878b5b791 Don't use notifications for toggles that have clear visual feedback already 2026-05-17 19:52:11 +02:00
David Heinemeier Hansson 4e2da8480a Standardize all notification sending 2026-05-17 19:51:29 +02:00
David Heinemeier Hansson 682c6406bd No need to quote glyphs 2026-05-17 19:51:29 +02:00
David Heinemeier Hansson 27497b19bf Expose DNS setting in the network widget 2026-05-17 19:51:29 +02:00
David Heinemeier Hansson 8c01563c46 Don't need notifications for changes that are obvious 2026-05-17 19:51:29 +02:00
Ryan Hughes 5faee9cf76 Remove unreleased Noctalia shell compatibility 2026-05-17 12:06:18 -04:00
David Heinemeier Hansson 7517c8b624 Smooth transition of bar with theme changes 2026-05-17 18:03:11 +02:00
David Heinemeier Hansson 2c265f361c Even smoother 2026-05-17 17:52:00 +02:00
David Heinemeier Hansson c24b9b0e31 Speed up changes 2026-05-17 17:46:42 +02:00
David Heinemeier Hansson 3887c53b41 Bump notifications font size 2026-05-17 17:29:52 +02:00
David Heinemeier Hansson dc4618c28c Sanitize notifications from chromium browsers 2026-05-17 17:29:52 +02:00
David Heinemeier Hansson 540aaf956d Add new display widget for the bar 2026-05-17 17:29:52 +02:00
David Heinemeier Hansson cf4935f964 Use existing mechanism 2026-05-17 17:29:52 +02:00
David Heinemeier Hansson 048efdb10e Tweak urgencies 2026-05-17 17:29:52 +02:00
David Heinemeier Hansson 8ad2f75758 Faster and smoother theme changes 2026-05-17 17:03:28 +02:00
David Heinemeier Hansson 4e50960c54 Faster theme and bg changes 2026-05-17 16:41:44 +02:00
Ryan Hughes 19a3622e7e Only when opened 2026-05-17 10:04:36 -04:00
David Heinemeier Hansson 523803e99e Performance improve pickers 2026-05-17 11:40:05 +02:00
David Heinemeier Hansson b9670a1ec7 Fix open/close 2026-05-17 11:04:45 +02:00
David Heinemeier Hansson 2918b4fd33 Instant 2026-05-17 11:04:41 +02:00
David Heinemeier Hansson df9f5c84e7 More emojis and pg up/down 2026-05-17 11:02:57 +02:00
David Heinemeier Hansson e8a8422408 Slim copy-url notification 2026-05-17 10:59:12 +02:00
David Heinemeier Hansson 4243c4d32f Any type detect 2026-05-17 10:49:53 +02:00
David Heinemeier Hansson a379b77312 Make clipboard picker hide 1password contents 2026-05-17 10:49:53 +02:00
David Heinemeier Hansson 2381d015e0 Make clipboard picker use a master/detail layout 2026-05-17 10:49:53 +02:00
David Heinemeier Hansson 32527e9201 Make clipboard picker use a 2-column layout 2026-05-17 10:49:53 +02:00
David Heinemeier Hansson 9ccd3aaa79 Make clipboard images full width and disable auto-pasting 2026-05-17 10:49:53 +02:00
David Heinemeier Hansson a0267e032d Fix clipboard manager launch helper signature 2026-05-17 10:49:53 +02:00
David Heinemeier Hansson 2713d43f35 Replace Walker clipboard with native Quickshell clipboard picker 2026-05-17 10:49:53 +02:00
David Heinemeier Hansson d81589e2be Full day names 2026-05-17 10:47:29 +02:00
David Heinemeier Hansson 048a73020f Use the more accurate location and tweak the look 2026-05-17 10:45:24 +02:00
David Heinemeier Hansson 414bfada8d Merge branch 'dev' into omarchy-shell 2026-05-17 10:35:55 +02:00
David Heinemeier Hansson 25c718e3df Add QS emoji picker 2026-05-17 09:47:45 +02:00
David Heinemeier Hansson c0c480b24f npm 2026-05-17 09:42:02 +02:00
David Heinemeier Hansson 54abdbaadc It's npm, not npx 2026-05-17 09:41:25 +02:00
David Heinemeier Hansson d92c277e87 It's npm, not npx 2026-05-17 09:41:04 +02:00
David Heinemeier Hansson 52ce411c2b Use our built-in notifications 2026-05-17 09:36:13 +02:00
David Heinemeier Hansson 881b5d177f Use Liberation Sans for notifications but with glyph fallback 2026-05-17 09:12:41 +02:00
David Heinemeier Hansson f54c0fb377 Fade in/out 2026-05-17 08:53:14 +02:00
David Heinemeier Hansson d771787e6b Just use single clicks 2026-05-17 08:51:17 +02:00
David Heinemeier Hansson 80da45dd1c Use the new weather overlay instead 2026-05-17 08:49:35 +02:00
David Heinemeier Hansson 6c41ab69d3 More massaging of the layout 2026-05-17 08:44:25 +02:00
David Heinemeier Hansson 19a4dffd3f Layouting 2026-05-17 08:40:50 +02:00
David Heinemeier Hansson f8e9755b1e Fix panel spacing and borders 2026-05-17 08:39:10 +02:00
David Heinemeier Hansson a902ac427b No system stats by default 2026-05-17 08:35:47 +02:00
David Heinemeier Hansson ae3d7efc6e Better weather placement 2026-05-16 22:48:56 +02:00
David Heinemeier Hansson b37386f084 Toggle transparency with double left, go to settings with double right 2026-05-16 22:48:10 +02:00
David Heinemeier Hansson 768d246944 Open theme switcher on double right click on the desktop 2026-05-16 22:44:01 +02:00
David Heinemeier Hansson f2815a8601 Theme feels wrong without round corners 2026-05-16 22:38:33 +02:00
David Heinemeier Hansson 6858e85659 Double-click the empty space to open the bar settings 2026-05-16 22:38:08 +02:00
David Heinemeier Hansson 0e108c82ae Add transparency option 2026-05-16 22:34:46 +02:00
Manuel 14ddd6ee85 Fix gradient border syntax in last-horizon and solitude themes (#5869) 2026-05-16 22:20:38 +02:00
David Heinemeier Hansson 8f643282b9 Switch from gnome polkit to QS 2026-05-16 22:20:25 +02:00
David Heinemeier Hansson ad20041c22 Just go straight to the bar settings 2026-05-16 22:06:02 +02:00
David Heinemeier Hansson eaea4cd577 Reduce Settings to just Bar Settings
The other don't add enough over the Omarchy Menu
2026-05-16 21:47:54 +02:00
David Heinemeier Hansson db5c09fc2e Get rid of the quick settings
It ended up just duplicating the omarchy menu
2026-05-16 21:15:04 +02:00
David Heinemeier Hansson 63abbf0be1 Prevent voxtype process zombies 2026-05-16 21:01:09 +02:00
David Heinemeier Hansson 1eab0cffc9 Integrate swaybg and swayosd into the new shell 2026-05-16 20:34:09 +02:00
David Heinemeier Hansson 83e7f41308 Add --fullscreen to make it easier for agents to record 2026-05-16 16:47:22 +02:00
David Heinemeier Hansson 49f19b617a Better spacing 2026-05-16 15:55:33 +02:00
David Heinemeier Hansson 74a3aa70cc Nix useless tooltips 2026-05-16 15:55:15 +02:00
David Heinemeier Hansson ab2fb2b85f Better night light toggle 2026-05-16 15:40:43 +02:00
David Heinemeier Hansson bdf8daa0a1 Slim down menu even more 2026-05-16 15:38:41 +02:00
David Heinemeier Hansson 3b2fc3a080 Bring back dispatch from keybindings menu 2026-05-16 15:09:07 +02:00
David Heinemeier Hansson f7f2838776 Hyprland expects lowercase comma 2026-05-16 15:09:07 +02:00
Manuel 8c506b30a9 Fix gradient border syntax in last-horizon and solitude themes (#5869) 2026-05-16 14:31:43 +02:00
Taha 9a846e95b2 Set corner radius to 0 for notification elements (#5872) 2026-05-16 14:31:08 +02:00
David Heinemeier Hansson 3eda4948b2 Incorporate outfoxxed feedback 2026-05-16 13:28:37 +02:00
David Heinemeier Hansson 122adc6bb9 Run steps of theme changes in parallel to speed the up 2026-05-16 13:09:20 +02:00
David Heinemeier Hansson 2aa3cae7f7 More menu tweaks 2026-05-16 13:07:31 +02:00
David Heinemeier Hansson 5d938dac92 Convert to lua 2026-05-16 13:07:18 +02:00
David Heinemeier Hansson 8610312db3 Quiet the rule adding 2026-05-16 13:00:30 +02:00
David Heinemeier Hansson e4d9d0d297 Explain to the agents how to remove a package 2026-05-16 13:00:03 +02:00
David Heinemeier Hansson fcde1057ee Make sure foot responds to all the conventional copy/paste keybindings 2026-05-16 12:59:43 +02:00
David Heinemeier Hansson fc271c1406 Merge branch 'dev' into omarchy-shell 2026-05-16 12:58:57 +02:00
David Heinemeier Hansson 8779c635c5 Tweak text 2026-05-16 12:58:45 +02:00
David Heinemeier Hansson 80ab790879 Similar fade on all menus 2026-05-16 12:58:39 +02:00
David Heinemeier Hansson 372f582b3d Loop menu 2026-05-16 12:49:53 +02:00
David Heinemeier Hansson 43a8406d44 We don't need action icons 2026-05-16 12:48:43 +02:00
David Heinemeier Hansson 007db7c016 Reminder to restart shell after changing qml 2026-05-16 12:48:37 +02:00
David Heinemeier Hansson 5de0c40a7e Try smaller toggles 2026-05-16 12:45:35 +02:00
David Heinemeier Hansson c751d0cac9 Pull theme and background into quick settings 2026-05-16 10:05:42 +02:00
David Heinemeier Hansson c172ed264f Get human name of current background 2026-05-16 10:05:42 +02:00
David Heinemeier Hansson 0e446f70f2 Use pointer hand for click target 2026-05-16 10:05:42 +02:00
David Heinemeier Hansson 7e9528b7a3 No need for this either 2026-05-16 10:05:42 +02:00
David Heinemeier Hansson 525d2855ca No need for this tool tip 2026-05-16 10:05:42 +02:00
David Heinemeier Hansson ecd9dc6ffe Furter notification tweaks 2026-05-16 10:05:30 +02:00
Abdulloh adeba8aa95 Convert last-horizon and solitude themes from hyprlang to Lua (#5867) 2026-05-16 09:37:15 +02:00
Ryan Hughes 1513adb339 Merge pull request #5860 from jweaver60/settings-widget-styling
Restyle Quickshell settings inputs to match Omarchy aesthetic
2026-05-16 00:45:47 -04:00
Ryan Hughes fd36836eec Narrow weather flyout and center forecast row 2026-05-16 00:41:41 -04:00
Ryan Hughes 079be0a1a7 Slim down comments in shell.toml.tpl
Keep only the non-obvious bits: the override mechanism, what counts as
"active" on the bar, the flyout text-color constraint, the
notifications-border convention, and the image-picker alpha modulation.
Drop comments that just restated the key names.
2026-05-16 00:37:12 -04:00
Ryan Hughes f404a81c6a Consolidate omarchy-shell colors into a single shell.toml surface
Five plugins each owned their own colors.toml watcher with subtly
different parsers, plus two ad-hoc per-theme override files
(notifications.json, image-picker-colors.json). This collapses all of
that into one source of truth: Commons/Color.qml watches colors.toml +
shell.toml and exposes Color.bar.*, Color.popups.*, Color.notifications.*,
Color.menu.*, Color.imagePicker.* for every surface to bind to.

shell.toml is generated by the existing template pipeline from
default/themed/shell.toml.tpl. Themes can ship their own shell.toml to
override individual keys; everything missing falls back to the
foundational palette via root.pick(). Settings panel is intentionally
not themable beyond the foundational tokens.

last-horizon and solitude ship a minimal shell.toml to preserve their
historical 'notification border matches Hyprland active border'
behavior, which previously came from parsing the theme's hyprland.conf
(now removed).
2026-05-16 00:30:27 -04:00
Ryan Hughes 227d32544f Fix battery indicator scale on omarchy-shell bar
Quickshell's UPowerDevice.percentage is a 0.0-1.0 real (energy /
energyCapacity), not 0-100. The bar was treating it as 0-100, so a 99%
charge picked the lowest icon, the tooltip read "1%", and the
low-battery active state was always true.
2026-05-16 00:27:18 -04:00
Ryan Hughes 620ab05e9d Merge pull request #5838 from jweaver60/weather-flyout-forecast
Add multi-day forecast to weather widget; click-only popup
2026-05-16 00:20:56 -04:00
Jake Weaver ae70db3c53 Restyle Quickshell settings inputs to match Omarchy aesthetic
Settings dialogs were rendering text/spin/combo controls with Qt's
default light style, which clashed with the dark Omarchy theme — black
text on dark fills, white dropdown popups, rounded corners, and a
pill-style toggle that didn't match the native StyleToggleRow switch.

- Style.qml: collapse all radius tokens to 0 so Noctalia-compat widgets
  inherit Omarchy's sharp-corner convention
- NToggle: rewrite to mirror the native 48x22 switch with accent fill
  and a sliding 16x16 thumb
- NComboBox: skin contentItem, indicator, popup, and delegate so the
  dropdown reads on dark themes
- NSpinBox: add a styled background, contentItem, and up/down indicators
- SettingsPanel calendar form + DynamicSettingsForm string field: drop
  default TextField chrome for the same translucent foreground fill used
  by WidgetCard, accent border on focus
2026-05-15 16:46:56 -04:00
David Heinemeier Hansson 37d6ae7f4b Make sure foot responds to all the conventional copy/paste keybindings 2026-05-15 22:06:13 +02:00
Jake Weaver 66f902e50c Widen and enlarge weather popup right-side details
Bump location and stat label/value font sizes a couple pixels and add
breathing room between the FEELS/WIND/HUMIDITY columns.
2026-05-15 16:02:38 -04:00
David Heinemeier Hansson 855377e7a1 Slim down migration 2026-05-15 21:56:26 +02:00
Jake Weaver 9c859a4c9b Reorganize weather popup hero layout
Move the weather icon next to the big temperature on the left and stack
the location and stats columns on the right, matching the new mockup.
2026-05-15 15:55:14 -04:00
David Heinemeier Hansson ef5669e896 Explain to the agents how to remove a package 2026-05-15 21:50:18 +02:00
David Heinemeier Hansson e22b2ba8ba Quiet the rule adding 2026-05-15 21:34:38 +02:00
David Heinemeier Hansson b98d3833f8 No zoom effect on clicks 2026-05-15 21:23:35 +02:00
David Heinemeier Hansson 692b291678 Workspaces without windows are greyed out 2026-05-15 21:17:43 +02:00
Jake Weaver 8eb91cd06b Enlarge hero icon and tighten popup spacing 2026-05-15 15:14:21 -04:00
David Heinemeier Hansson e8fb83de11 Improve notification look 2026-05-15 20:58:09 +02:00
Jake Weaver 15c7cbfc91 Redesign weather popup and auto-refresh forecast
Replace the inline detail flyout with a two-column hero: location chip
and large temperature on the left, big condition icon on the right.
Stats (Feels / Wind / Humidity) move below as caps-labeled columns,
separated from the forecast by a thin divider. Forecast cells now lead
with the day icon and put the day name above hi/lo temps.

Drop the Refresh and wttr.in pill buttons; a Timer refreshes the
forecast every refreshMinutes (default 15). The interval is exposed
through the Settings panel — Bar -> Weather has a gear that opens a
SpinBox-backed form, persisted as "settings": { "refreshMinutes": N }
in shell.json.
2026-05-15 14:09:26 -04:00
Jake Weaver 1855eeca47 Show multi-day forecast in weather widget and drop hover popup
Replace the hover-driven detail flyout with a click-toggled popup that
includes a Noctalia-style row of upcoming days. Switching the wttr.in
fetch to the j1 JSON gives us structured current conditions plus a
3-day forecast; each day's representative icon comes from the hourly
entry nearest noon, mapped through the same code → glyph table as the
bar pill.
2026-05-15 13:40:53 -04:00
David Heinemeier Hansson ce44f7b3f8 Fix tooltip format 2026-05-15 19:28:42 +02:00
David Heinemeier Hansson 3b523edeca Fix indicator spacing 2026-05-15 19:22:58 +02:00
David Heinemeier Hansson 36edee945c Closer on the weather 2026-05-15 19:20:31 +02:00
David Heinemeier Hansson 6cb880908a More spacing tweaks 2026-05-15 19:14:52 +02:00
David Heinemeier Hansson 52817ec465 Fix spacing 2026-05-15 19:06:14 +02:00
David Heinemeier Hansson d6e8149580 Slim down the quick controls 2026-05-15 18:56:39 +02:00
David Heinemeier Hansson 470503bed3 Prevent flickering on theme/backgrounds selector 2026-05-15 18:48:20 +02:00
David Heinemeier Hansson 421bbb3e79 Improve font rendering 2026-05-15 18:41:25 +02:00
David Heinemeier Hansson 78de3c76d2 Merge branch 'dev' into omarchy-shell 2026-05-15 18:33:09 +02:00
Ryan Hughes 5b4043a3dc Merge pull request #5835 from jweaver60/fix-tooltip-border-clip 2026-05-15 12:29:49 -04:00
David Heinemeier Hansson 3d3c77f53d Fix the gum coloring 2026-05-15 18:03:21 +02:00
David Heinemeier Hansson 05fee802fc Don't remove wtype 2026-05-15 17:58:31 +02:00
David Heinemeier Hansson 0bfb545033 Ensure the service is fully removed
Closes #5788
2026-05-15 17:57:44 +02:00
David Heinemeier Hansson c0275ea9f2 Foot as the default terminal (#5831)
* Bind maps too

* Make foot the default terminal
2026-05-15 17:44:09 +02:00
David Heinemeier Hansson e72b8a6dbd Skip the certificate errors 2026-05-15 17:39:32 +02:00
David Heinemeier Hansson 612dead716 Make sure we setup autostart as well 2026-05-15 17:03:39 +02:00
David Heinemeier Hansson 2d5e3cefe0 Install Sunshine Admin app as well 2026-05-15 16:59:52 +02:00
David Heinemeier Hansson b486a43985 Add sunshine service installation 2026-05-15 16:24:21 +02:00
David Heinemeier Hansson e2938ae952 Performance improvements 2026-05-15 16:24:21 +02:00
Jake Weaver 182f71a61b Ceil tooltip popup size so right/bottom borders render
Text.implicitWidth/Height can be fractional. Passing those values
straight to the PopupWindow rounded the Wayland surface down, clipping
the 1px right/bottom border of the bubble.
2026-05-15 10:13:39 -04:00
David Heinemeier Hansson 43134875b5 Ensure that $BROWSER runs omarchy-launch-browser and doesn't hand ownership to parent process
This prevents gh auth login from working without an existing open
browser
2026-05-15 16:07:35 +02:00
David Heinemeier Hansson daf301c416 Don't ask to restart multiple times 2026-05-15 15:21:03 +02:00
David Heinemeier Hansson a4635f7657 Make o available without error 2026-05-15 14:44:50 +02:00
David Heinemeier Hansson 4146600f5c Refresh the theme 2026-05-15 14:18:57 +02:00
David Heinemeier Hansson 51d998bee0 Need exclusive placement 2026-05-15 14:14:16 +02:00
David Heinemeier Hansson 95b0bfed4d Ensure that the old style is always run 2026-05-15 14:12:12 +02:00
David Heinemeier Hansson 7e99e3eff0 Ensure toggles are refreshed when doing omarcy-refresh-hyprland 2026-05-15 14:02:12 +02:00
David Heinemeier Hansson 47cd93143a Explain reasoning 2026-05-15 13:57:01 +02:00
David Heinemeier Hansson 8d107ed04c Drop outdated migrations 2026-05-15 13:56:15 +02:00
David Heinemeier Hansson 719b7df91a No longer has its own flags 2026-05-15 13:55:45 +02:00
David Heinemeier Hansson 870fe16b0a Add Alt+esc to close a pane too 2026-05-15 13:39:17 +02:00
David Heinemeier Hansson 2f56b525c6 Simpler additional tmux splits 2026-05-15 13:37:53 +02:00
David Heinemeier Hansson 883c6a6267 Bind maps too 2026-05-15 11:13:20 +02:00
Brosseau Valentin 39eb3df863 Remove suspend-then-hibernate cleanup steps (#5829)
Those files are not present anymore (removed here e4b7372666)

Removed suspend-then-hibernate configuration cleanup steps from the script.
2026-05-15 11:03:21 +02:00
Ryan Hughes 96c5e048cb Drive Wi-Fi widget from iwctl with scrollable network list 2026-05-15 02:40:26 -04:00
Ryan Hughes 697e4b3c73 Drop dead hyprland conf-only migration references
Same logic as the waybar/mako sweep: migration 1778321093 converted the
user's top-level hypr configs (autostart/bindings/hyprland/input/
looknfeel/monitors + envs + apps/*) from hyprlang `.conf` to Lua, so
any earlier migration that seds those files is editing a path that no
longer exists on the new layout.

These were already applied on existing systems and skipped on fresh
installs (preflight marks them done before they would run), so the
change is purely repo cleanliness — the migrations dir stops mentioning
files the new omarchy doesn't have.

Deleted 18 migrations that only touched dead .conf paths:

  1754389057 1755870033 1757866485 1757877123 1757877852 1760462260
  1763386443 1767138576 1769543550 1770159912 1770375655 1771606249
  1771847961 1772293693 1776410469 1776781957 1777620904 1778008689

Stripped dead .conf sed/cp blocks from 5 mixed migrations whose
remaining work still matters (webapp launcher fixup, uwsm env/term
config, xdg-terminal-exec install, Obsidian flags relocation, swayosd
systemd service):

  1755507891 1758019332 1762121828 1776434586 1778171768

The four genuinely-alive .conf files (hyprlock.conf, hypridle.conf,
xdph.conf, hyprsunset.conf) are untouched. The converter migration
(1778321093) is untouched. The lua-targeting migration 1757879836 is
untouched.
2026-05-14 19:14:22 -04:00
Ryan Hughes dcce74401b Drop dead waybar/mako migration references
The dev -> omarchy-shell flip migration (1778743515.sh) uninstalls
waybar and mako and moves both user-config dirs aside, so any older
migration that tweaks ~/.config/waybar/ or invokes
omarchy-{restart,refresh}-waybar / omarchy-restart-mako is editing
something that's about to be removed.

These migrations are already marked applied on existing systems and
skipped on fresh installs (preflight touches every migration name into
the state dir before they would run), so removing/editing them is a
no-op for anyone currently on omarchy. What it does fix is the repo
itself referring to commands that no longer exist on disk.

Deleted 26 migrations that were entirely waybar/mako tweaks:

  1752793122 1754228071 1754331529 1756371020 1757279511 1757511571
  1760693222 1760787158 1761269603 1763409505 1763560161 1763634099
  1763653692 1764067068 1767227973 1767685679 1767716691 1768270644
  1771602477 1771602647 1775208227 1777017528 1777464602 1777578316
  1778139028 1778539119

Stripped the waybar/mako bits from 10 mixed migrations whose real work
is still relevant (impala install, light themes, wiremix, tzupdate,
xdg-terminal-exec, bluetui, terminal app-ids, voxtype GPU, walker
toggle):

  1751887718 1752725616 1752896442 1753495989 1762121828 1762417304
  1763386443 1773506226 1775241210 1778405462
2026-05-14 19:10:13 -04:00
Ryan Hughes 565d11ba0b Update stale command references to renamed bins
Two leftover spots still pointed at command names that were renamed in
this branch:

  - default/quickshell/omarchy-shell/README.md:124 said
    `omarchy restart quickshell` (renamed to `omarchy restart shell`).
  - config/hypr/bindings.lua:35 had a commented-out example using bare
    `omarchy-menu`. The wrapper still works that way for now, but the
    canonical hot path is `omarchy-shell-ipc menu toggle root` \u2014 update
    the example so users copying it learn the right pattern.
2026-05-14 19:06:48 -04:00
Ryan Hughes 765b068e86 Add omarchy-menu wrapper for ergonomic CLI use
`omarchy-shell-ipc menu toggle root` is what the keybinds use \u2014 it
skips the omarchy CLI dispatch hop and stays sub-ms. But humans don't
want to type that. New tiny wrapper restores `omarchy menu …` as the
CLI entry point:

  omarchy menu                  → toggle root (bare invocation)
  omarchy menu toggle [route]   → toggle, route defaults to root
  omarchy menu summon [route]   → always open
  omarchy menu close            → close if open
  omarchy menu refresh          → re-parse the menu JSONCs
  omarchy menu ping             → health check

Internally each verb just execs `omarchy-shell-ipc menu …`. The
omarchy-CLI dispatcher overhead measures ~2ms on top of the direct IPC
path (32ms → 34ms keybind-to-visible), so the wrapper is fine for any
human-typed invocation; keybinds and the bar icon stay on
omarchy-shell-ipc to keep that ~30ms hot path.

Sibling `omarchy menu file`, `omarchy menu input`, etc. still resolve
through their own bins \u2014 the dispatcher picks the longest matching
prefix, so longer routes ("omarchy menu file") win over the bare
"omarchy menu" entry.
2026-05-14 18:26:50 -04:00
Ryan Hughes 679e14c37f Restore menu toggle behavior on keybinds and bar clicks
The old bash bin short-circuited with close_visible_quickshell_menu at
the top of open_quickshell_menu, so pressing any menu keybind while the
menu was open just closed it. The IPC path I introduced summoned every
time, so keyboard shortcuts couldn't dismiss \u2014 only outside clicks did
(which is why the bar icon appeared to toggle: clicking it while the
menu was open hit the menu's outer-click MouseArea, not the icon).

Add a menu.toggle IPC method:
  - if root.opened: cancel() and return "closed"
  - otherwise: behaves exactly like summon

Switch every Hyprland binding and the two bar-icon click handlers
(omarchy logo, battery right-click \u2192 power menu) to toggle. summon stays
as the always-open primitive for callers that want explicit summon
semantics (rare \u2014 right now nothing internal uses it).
2026-05-14 18:04:25 -04:00
Ryan Hughes edfd68d48e Strip the bash-bin compatibility layer from Menu.qml
With bin/omarchy-menu gone there's no caller that summons the menu by
writing a JSON tempfile and polling a done file. Drop everything that
existed only to support that handshake:

  - menuJsonFile / menuJsonFileView / loadMenuJson / openMenu(json,...)
  - pendingSelectionFile / pendingDoneFile / selectionFile / doneFile
  - mergeProviderJson (the JSON-wrapper for provider output)
  - applyProc / releaseProc / doneFilesToRelease / finishDoneFile /
    releaseNextDoneFile / resetRequest / requestActive / closeMenu
  - the menuBin property pointing at the deleted bin

applySelected is now four lines (close + execDetached); cancel is two
(close + clear filter). open() takes either {menu: id} or a bare initial
menu name, and hands off to openExistingMenu which uses the cached items
from the JSONC FileViews.

Net: 91 lines smaller, no perf change.
2026-05-14 17:45:53 -04:00
Ryan Hughes 4a665af581 Drop bin/omarchy-menu; sweep JSONC actions to bare commands
The shell now owns the menu — both data and dispatch. The 821-line bash
bin was carrying the open path (cold spawn + jq pipeline + tempfile dance
+ IPC + poll) and a grab-bag of helpers that only existed inside it. Both
go away in this commit.

New bins for the few composite helpers that were genuinely worth keeping:

  - omarchy-install-app NAME PKG
  - omarchy-install-and-launch NAME PKG DESKTOP_ID
  - omarchy-install-font LABEL PKG FAMILY
  - omarchy-launch-config-editor PATH
  - omarchy-reminder-set-interactive
  - omarchy-capture-screenrecording-with-webcam

Sweep through default/omarchy/omarchy-menu.jsonc rewrites:

  - present_terminal X   -> omarchy-launch-floating-terminal-with-presentation X
  - install_terminal X   -> omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal X'
  - install / install_and_launch / install_font / open_in_editor -> new bins above
  - terminal X           -> xdg-terminal-exec --app-id=org.omarchy.terminal X
  - default_browser_is X -> [[ "$(omarchy-default-browser)" == "X" ]]
  - default_terminal_is X / default_editor_is X / haptic_touchpad_is X same shape
  - $(hypr_config_file X) -> ~/.config/hypr/X.lua
  - show_custom_reminder_input -> omarchy-reminder-set-interactive
  - screenrecord_with_webcam   -> omarchy-capture-screenrecording-with-webcam
  - stop_active_screenrecording -> omarchy-capture-screenrecording --stop-recording

Hyprland bindings switch from `omarchy-menu X` to
`omarchy-shell-ipc menu summon X` (the keybind hot path now skips bash
entirely). The Bar.qml omarchy widget and the battery right-click do the
same. ALT+PRINT becomes a one-liner: stop the recording if one is going,
otherwise summon the screenrecord submenu.

Measured: keybind-to-visible is ~30ms (was ~235ms). The shell's plugin
keepLoaded:true stops being theoretical \u2014 the menu data lives in memory
across opens, and the only work between keypress and paint is the IPC
roundtrip and the layer-shell window mount.
2026-05-14 17:43:12 -04:00
Ryan Hughes a960d2b1bf Move omarchy-menu data and dispatch into the shell
omarchy-menu used to spawn an 821-line bash bin on every keybind: 11 jq
invocations against omarchy-menu.jsonc, mktemp dance, summon, poll. The
shell waited ~235ms (keybind to visible) before painting the first row.

The bar plugin host is already a persistent process with the menu plugin
loaded (keepLoaded: true) — it just wasn't allowed to own the menu data.
Now it does:

  - parseMenuJsonc / normalizeItem ports the bash bin's jq pipeline to
    JS: strip // comments + trailing commas, infer parent from dotted
    ids, dedupe keyword tokens across id/aliases/keywords, fill defaults.
  - Two FileViews with watchChanges: true on the default and user
    extension JSONCs. Edit either file and the in-memory item map
    refreshes without a restart.
  - IpcHandler exposes menu.summon(initialMenu) / refresh() / close() /
    ping(). summon resolves aliases the way the old bash route_target
    did, including the leaf-alias-is-an-action shortcut.
  - Action dispatch is now direct: when summoned via IPC (no selection
    or done file), Quickshell.execDetached(['bash','-lc', action]) runs
    the action and the menu closes. The legacy tempfile dance is kept
    only for the duration of the migration window.
  - provider sub-menus (fonts, power-profiles) are inline: tab-delimited
    bash one-liner per provider, parsed by mergeProviderRows.
  - when:/checked: bash expressions are batched into one Process at
    shell startup (and again whenever the JSONCs change). The menu
    opens instantly; visibility/check marks settle within ~50ms.

Measured: keybind-to-visible via the new IPC path is ~30ms (was ~235ms).
The bash bin still works for now and is removed in the next commit.
2026-05-14 17:39:04 -04:00
Ryan Hughes f470dcc27a Consolidate dev -> omarchy-shell migration; drop premature widget removal
Replace the mako-only migration with a single "complete the flip"
migration that handles everything needed to bring a dev-channel user
onto omarchy-shell:

  - install quickshell if it isn't already on the box
  - stop and uninstall mako; pacman -Rns deletes the .service unit too,
    so D-Bus activation can't conjure it back into existence
  - stop and uninstall waybar with the same uninstall-not-mask approach
  - move ~/.config/{mako,waybar} aside (timestamped backup) rather than
    rm -rf, so anyone with hand-tuned rules can mine them
  - clear the stale toggle-state markers
  - call omarchy-restart-shell so the bar and notification daemon come
    up mid-session without a relog

Drop migrations/1778715864.sh: shell.json hasn't shipped to any user
yet, so a migration removing widgets nobody has is dead code.
2026-05-14 16:34:40 -04:00
Ryan Hughes 883d8554f3 Polish first-party shell plugin docs, Hyprland rules, and bar-position seed
- omarchy-bar-settings.lua is gone: the Hyprland rules it carried for
    the old standalone bar-settings window no longer match anything,
    since settings is now a panel plugin summoned via IPC. The still-
    needed "Widget settings" sub-dialog rules move into
    omarchy-shell.lua next to the existing "Omarchy Settings" rules.

  - plugins README documents the `bar` plugin kind so third-party
    plugin authors aren't surprised to see it on the first-party
    omarchy.bar manifest. The note also calls out that they should ship
    `bar-widget`s, not replace the host bar.

  - omarchy-style-bar-position seeds from shell-defaults.json before
    mutating, so a user without a shell.json (or with a malformed one)
    no longer ends up with a stub config that blanks the bar layout.
2026-05-14 16:34:32 -04:00
Ryan Hughes 85199197af Drive the bar font from fontconfig (drop shell.json round-trip)
omarchy-font-set already updates ~/.config/fontconfig/fonts.conf, which
is what fc-match, Qt, and every other XDG app reads from. Storing the
same family in shell.json gives the same value two homes that can drift
apart, and stops the bar from picking up font changes without a shell
restart.

  - omarchy-font-set stops writing bar.fontFamily to shell.json and stops
    poking ~/.config/waybar/style.css. fontconfig + terminals + hyprlock
    + swayosd are still updated as before.
  - omarchy-font-current reads fc-match monospace -f '%{family}' and
    takes the first comma-separated entry; the old jq-on-shell.json
    path is gone.
  - Bar / SettingsPanel / NDropdown.qml default fontFamily to
    "monospace" and stop reading config.fontFamily. Qt resolves the
    family through fontconfig at paint time, so omarchy-font-set updates
    the bar live with no reload.
  - shell-defaults.json drops bar.fontFamily.

User-visible effect: pick a Nerd Font, the bar follows it. Pick a font
without glyphs and you get tofu — same failure mode the terminals and
hyprlock already had.
2026-05-14 16:34:21 -04:00
Ryan Hughes 90f0475a9d Rename refresh-bar -> refresh-shell, restart-quickshell -> restart-shell
The bin names should reflect the thing they manage. `refresh bar` is
misleading because the command resets the entire shell.json (which holds
plugins beyond just the bar), and `restart quickshell` names the runtime
rather than the product. Both now end in `-shell` to match the rest of
omarchy-shell.

Updates every caller in autostart, the menu, voxtype helpers, and the
shell README. No behavior changes.
2026-05-14 16:33:51 -04:00
Ryan Hughes 2ca15dc767 Remove waybar; relocate bar helper scripts into the shell tree
omarchy-shell's bar plugin replaces waybar on every default path, so the
waybar package, configs, indicator scripts, and toggle/restart/refresh
binaries all go away in this commit.

The four small helper scripts that Bar.qml shells out to (weather.sh and
indicators/*.sh) move with the shell rather than disappear \u2014 they live
at default/quickshell/omarchy-shell/scripts/ now, alongside the rest of
the shell code.

Companion script changes:
  - omarchy-shell-ipc gains --if-running so fire-and-forget refresh
    callers (toggle-idle, toggle-notification-silencing,
    update-available-reset, capture-screenrecording) don't accidentally
    spawn the shell when it isn't already up.
  - omarchy-theme-set, omarchy-tz-select, omarchy-voxtype-{install,model},
    omarchy-toggle-nightlight, omarchy-voxtype-config drop their waybar
    branches and comments.
  - omarchy-voxtype-status's summary stops claiming it's a Waybar helper.
  - omarchy-base.packages drops waybar; install/packaging/fonts.sh's
    comment points at the new bar consumer.
  - SKILL.md / AGENTS.md / omarchy-menu.jsonc lose their waybar entries.
  - test/omarchy-cli-test.sh substitutes a remaining toggle command.
2026-05-14 16:33:20 -04:00
Ryan Hughes 020cb537c7 Dedupe notifications by id and persist history to ~/.local/state
Chat apps (Vesktop, etc.) can rapid-fire the same Notify D-Bus call when
they sync, leaving the pending tab with hundreds of byte-identical rows
because the old handler appended unconditionally and never re-entered the
`markSeenByOriginalId` path for follow-up arrivals.

  - addToPending + popup-insert now remove any existing row matching the
    same `originalId` before insertion, honoring the freedesktop
    "replaces_id" semantics that chat apps rely on.
  - loadHistory dedupes by originalId on hydration, so existing on-disk
    files with the old duplicate-laden schema collapse on next start.
  - loadHistory also guards against re-entry: FileView fires onLoaded
    more than once during startup (initial preload + Component.onCompleted
    reload), and the old version would double the model each fire.

While in here, move the persistent notifications.json out of `~/.cache`
to `~/.local/state/omarchy` so a "clear cache" habit no longer drops
DND preference and notification history. The image thumbnail cache stays
in ~/.cache where regeneratable artifacts belong.
2026-05-14 16:31:47 -04:00
Ryan Hughes 806bd034b4 Fix bluetooth panel pair flow, scroll, and X-button click
- Discovered devices now populate via a new `discoveredDevices` filter; the
  Repeater was referencing a property that did not exist.
- Pairing chains into trust + connect at root scope, so freshly-paired
  devices (especially audio headphones) persist in BlueZ and actually
  finish connecting instead of flashing "Connected" and disappearing.
- Wrap the device list in a Flickable capped at 400px with a scroll bar
  so crowded RF environments do not grow the popup off-screen.
- Reorder MouseAreas so the per-row X button receives clicks instead of
  the full-row click handler swallowing them.
2026-05-14 14:40:14 -04:00
Ryan Hughes c101591284 Replace mako with quickshell-backed notification daemon
Adds first-party omarchy.notifications service plugin that hosts a
freedesktop notification server and renders popups + a history popup
inside the shell. Uninstalls mako and retargets every helper, keybind,
indicator, and migration entry to the new daemon.

Plugin (default/quickshell/omarchy-shell/plugins/notifications/):
- Service.qml: NotificationServer, popupModel + pendingModel + pastModel
  (two-tier history, see below), DND via PersistentProperties +
  cache-file backstop, image cache for /tmp screenshots, IpcHandler with
  toggleDnd/setDnd/isDnd/showHistory/clear/clearPending/markAllSeen/
  dismissAll/dismissOne/invokeLast/dismiss, per-theme override file
  ~/.config/omarchy/current/theme/notifications.json honoring
  borderColor/backgroundColor/textColor/countdownColor.
- components/NotificationCard.qml: theme-driven card (Color.foreground/
  background/border tokens from Commons/Color.qml), 32x32 icon slot,
  Nerd Font glyph fallback via omarchy-glyph hint, hero image strip
  for screenshot/image-path notifications, hover-pause progress bar,
  uses bar.fontFamily so all surfaces share one font.

Filtering and DND:
- transient hint and CLI-style senders (app_name in notify-send /
  omarchy-action) bypass history but still pop.
- DND only allows omarchy-action toasts and notify-send -u critical
  through; real-app urgency=critical (Discord, Slack, Vesktop) is
  silenced and lands in pending instead.
- Pending vs past split surfaced via tabs in the bar widget popup;
  past tab is auto-pruned at the 15-minute mark.
- Click-to-jump: notifications without a libnotify default action focus
  the matching Hyprland window via class lookup.

Shell host:
- shell.qml: generic first-party service loader (mirrors the existing
  noctalia-compat path) and an alias for the bar so plugins can read
  barSize / barHidden / position for anchoring.
- Commons/Color.qml: parses the theme's hyprland.conf for
  $activeBorderColor so notifications match Hyprland window borders;
  picks the explicit accent= key over the color4= alias.

Bar widget rebase (plugins/bar/widgets/notificationCenter.qml):
- Drops the chunk-1 stub server, binds count/dnd state to the service,
  hosts the history popup via PopupCard so it drops down from the
  notification glyph the same way Quick Settings does.
- Pending/Past tabs, dismiss-individual close X, mark-all-as-seen and
  clear-recent action buttons, theme-driven palette.

Quick Settings rework (plugins/bar/widgets/controlCenter.qml):
- DND tile binds directly to service.doNotDisturb for instant feedback.
- Drops the volume slider (already in audioPanel) and the no-op Theme
  tile; adds a Bluetooth toggle bound to Quickshell.Bluetooth.
- Bigger 44x44 wallet was scaled back to 32x32 for tighter rows.

Notification scripts (bin/omarchy-*):
- omarchy-notification-send: passes glyph as a custom hint instead of
  prepending to the summary; adds -a omarchy-action and -u urgency
  automatically; supports -e/--transient passthrough.
- User-action toasts in the capture / toggle / hyprland / default-*
  scripts and bindings/utilities.lua now tag themselves
  -a omarchy-action so DND treats them as intent-based bypass.
- omarchy-toggle-notification-silencing, omarchy-notification-dismiss,
  default/waybar/indicators/notification-silencing.sh, and the Hyprland
  comma-keybinds all route through omarchy-shell-ipc notifications.
- omarchy-capture-screenshot / -screenrecording set the image-path
  hint properly so the hero-image rendering kicks in.

Mako removal (migrations/1778743515.sh):
- pkill -x mako, systemctl --user stop mako.service, pacman -Rns mako
  (uninstalling deletes /usr/lib/systemd/user/mako.service so D-Bus
  activation can't respawn it). Removes ~/.config/mako/ and the legacy
  toggle file. Restarts quickshell so it claims the bus name.
- Drops mako from install/omarchy-base.packages, autostart.lua,
  install/config/theme.sh + toggles.sh, default/themed/mako.ini.tpl,
  default/mako/, the omarchy-menu Mako restart row, bin/omarchy
  GROUP_DESCRIPTIONS, the settings panel catalogue, and the
  default/omarchy-skill paths table.
- Removed scripts: bin/omarchy-restart-mako,
  bin/omarchy-style-corners-mako.
- bin/omarchy-style-corners summary updated; corner radius for the
  notification card reads ~/.local/state/omarchy/toggles/quickshell-menu.json
  alongside the rest of the shell.
2026-05-14 14:21:07 -04:00
Ryan Hughes 9b4a411e5b Pin omarchy-reinstall-git to master branch
GitHub's default branch on basecamp/omarchy is no longer master, so
a plain clone pulls the development branch. Pass --branch master so
reinstalls always land on stable regardless of repo default.
2026-05-14 13:52:46 -04:00
Ryan Hughes 82f99928d0 Bump version from 3.8.0 to 3.8.1 2026-05-14 13:23:48 -04:00
Ryan Hughes 6b5baf06ad Merge pull request #5816 from basecamp/fix-reinstall-git-default-branch
Pin omarchy-reinstall-git to master branch
2026-05-14 13:22:35 -04:00
Ryan Hughes 32a82dd15b Pin omarchy-reinstall-git to master branch
GitHub's default branch on basecamp/omarchy is no longer master, so
a plain clone pulls the development branch. Pass --branch master so
reinstalls always land on stable regardless of repo default.
2026-05-14 13:18:46 -04:00
David Heinemeier Hansson e916491c9f More helpers to cleanup configs 2026-05-14 13:15:03 +02:00
David Heinemeier Hansson a4e4b4b1ed Extract launch_sole 2026-05-14 12:58:39 +02:00
David Heinemeier Hansson 8f237f1858 Slim 2026-05-14 12:55:34 +02:00
David Heinemeier Hansson 596638aa73 Don't need the drop shadow 2026-05-14 12:52:45 +02:00
David Heinemeier Hansson 84de3d3a2e Extract more helpers 2026-05-14 12:51:03 +02:00
David Heinemeier Hansson 6840242c44 Fix indention 2026-05-14 12:37:53 +02:00
9d7474095f Keep brightness OSD popup width uniform across all percentages (#5806)
Pad the percent text to 3 chars with printf so "  5%", " 50%", and
"100%" render at identical width in the JetBrainsMono monospace label.
Previously the popup container resized between events because the
label string varied between 2 and 4 characters, which caused the
popup to visibly jump in width on consecutive brightness key presses.

Builds on #5525, which made the percent values themselves step
uniformly; this completes the loop by making the rendered popup match.

Co-authored-by: Mateus Pegorim <14262063+Pegorim@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:35:36 +02:00
David Heinemeier Hansson 7f85ee7d9d Make the selected image bigger 2026-05-14 12:33:51 +02:00
David Heinemeier Hansson 4a11ca7ae9 Loop around 2026-05-14 12:32:19 +02:00
David Heinemeier Hansson 6891c996b5 Add migration for new tmux keybinding 2026-05-14 12:04:09 +02:00
David Heinemeier Hansson 4c48d83952 More helpers 2026-05-14 12:02:53 +02:00
David Heinemeier Hansson 41988fda2a Cleanup 2026-05-14 12:02:53 +02:00
David Heinemeier Hansson 35cf232bcb Extract built-in notifications 2026-05-14 12:02:53 +02:00
Vitor Melo e766f0ac28 Add bind M-Enter to open new pane em tmux (#5785) 2026-05-14 11:40:20 +02:00
David Heinemeier Hansson 3e098f6dd5 Next up is 4.0 2026-05-14 11:34:55 +02:00
David Heinemeier Hansson b72eb206cb Extract launch_on_start helper 2026-05-14 11:34:26 +02:00
David Heinemeier Hansson 83c2e2f84c Extract Omarchy's own bootstrap 2026-05-14 11:32:22 +02:00
David Heinemeier Hansson 9e1310106a Too defensive 2026-05-14 11:32:13 +02:00
David Heinemeier Hansson cfc7bc0cda Better naming 2026-05-14 11:21:45 +02:00
David Heinemeier Hansson dca3bfd951 Cleaner selection without exposed filtering 2026-05-14 11:17:33 +02:00
David Heinemeier Hansson 72bd02ca89 Start extracting omarchy helpers for cumbersome lua calls 2026-05-14 11:13:37 +02:00
David Heinemeier Hansson b26cd62334 Extract the nautilus cwd launcher 2026-05-14 11:12:25 +02:00
David Heinemeier Hansson 6ae8dc555e Extract launching the tmux terminal 2026-05-14 10:57:44 +02:00
David Heinemeier Hansson 9e20c4899d Extract terminal launcher with cwd 2026-05-14 10:56:10 +02:00
Ryan Hughes a7f8f2495f Drop microphone widget from default bar layout 2026-05-14 03:25:15 -04:00
Ryan Hughes b37a8cbaca Anchor tray to section's inner edge with pin/hide support
The tray drawer reserves space for its slide-in animation; placed
mid-section that gap was always visible. Now:

- normalizeLayout moves any tray entry to the section's inner edge
  (start of right, end of left/center) so the reserved gap sits next
  to the bar center instead of stranded mid-section.
- TrayModule reads its own entry settings to support pinned[] and
  hidden[] arrays of SNI ids. Pinned icons render to the right of the
  chevron (always visible); hidden icons never render; the rest fall
  into the drawer.
- Right-click the chevron opens a management popup listing every
  reporting tray item with Pin/Unpin and Hide/Show pills. Toggles
  persist via shell.updateEntryInline.
- Bar imports common/ as BarCommon so the tray can reach PopupCard
  and PillButton for the popup.
2026-05-14 03:24:53 -04:00
Ryan Hughes eda549356a Drop media and activeWindow from default bar layout
Aligns shell-defaults.json with the practical default — no media widget
in center, no activeWindow on the left.
2026-05-14 03:24:45 -04:00
Ryan Hughes 5047c4c66e Collapse systemStats widget to a single chip icon
Bar now shows just the chip glyph; hover opens the existing CPU+Memory
graphs popup, left-click launches btop. Removes the inline sparklines,
the right-click→alacritty binding, and the redundant Open btop pill.
2026-05-14 03:24:22 -04:00
Ryan Hughes 08bf6d075d Consolidate omarchy-shell Hyprland rules; float settings panel
Rename apps/quickshell.lua to apps/omarchy-shell.lua and gather every
rule that targets a Quickshell surface owned by the shell:

  - menu and image-selector layer rules (image-selector moved out of
    apps/system.lua, where it didn't belong with generic floating /
    media / idle rules)
  - new window rules so the settings panel floats centered at 960x700
    instead of tiling

The settings panel is a transient dialog, not a workspace surface, so
tiling it disrupts the layout and leaves it awkwardly tall when it lands
in a half-screen split. Class+title scoping keeps the rule from leaking
into other org.quickshell windows (menu, image picker, etc.).
2026-05-14 02:25:05 -04:00
Ryan Hughes 1c559d2231 Default settings panel to Defaults category on open 2026-05-14 02:25:05 -04:00
Ryan Hughes 3595279dce Park focus on a sink Item when leaving body
The previous clearBodyFocus() set focus = false on navRoot.activeFocusItem
and called navRoot.forceActiveFocus(), which works for plain Rectangles
but not for Controls 2 widgets like ComboBox: the combo keeps an internal
focused child that the surrounding FocusScope happily restores, so the
accent focus border on the Position/Center anchor dropdowns lingered
after backing out to the sidebar.

Add an invisible 1x1 navFocusSink Item inside navRoot and force focus
onto it whenever the user leaves the body (h/Esc/Shift-Tab, category
changes from sidebar nav, or panel reopens). The sink reliably owns
activeFocus so every body control \u2014 ComboBox included \u2014 falls back to
its unfocused style.
2026-05-14 02:25:05 -04:00
Ryan Hughes 9bb35dd7c1 Differentiate focus from selected; clear focus when leaving body
Two related fixes from user feedback on the keyboard nav:

1. Focused and selected controls were drawing the same 2px accent border
   so it was impossible to tell where the keyboard cursor was on a row
   that happened to also be the current value. Focused now uses a 3px
   accent border plus a translucent accent fill; selected keeps its 2px
   border with no fill. Centralized into focusBorderColor /
   focusBorderWidth / focusFillColor so all components share the look.

2. Backing out of the body to the sidebar (h/Left/Esc) left the focus
   ring lingering on the body item. FocusScope was restoring activeFocus
   to its remembered descendant when navRoot reclaimed focus.
   exitBodyZone (and onActiveCategoryChanged when not in body mode) now
   clears focus from the previously active descendant before forcing
   focus back onto navRoot.
2026-05-14 02:25:05 -04:00
Ryan Hughes 6f5fac3f13 Add keyboard navigation to settings panel
Two zones — sidebar and body — with vim-style bindings:

  sidebar  j/Down  next category
           k/Up    previous category
           l/Right/Tab/Enter/Ctrl-L  enter body
           Esc     close panel

  body     j/Down/Tab        next focusable
           k/Up/Shift-Tab    previous focusable
           Enter/Space       activate focused control
           h/Left/Esc/Ctrl-H back to sidebar

Wraps panel content in a FocusScope with Keys.priority: Keys.BeforeItem
so j/k always reach the nav handler even when a child control has focus.
Body focusables are gathered by walking the visible subtree of the active
category for items with activeFocusOnTab — automatically excluding
hidden category branches. Sidebar shows an accent-tinted highlight when
focused; each interactive control (DefaultsRow, StyleOptionTile,
StyleChip, FontRow, StyleToggleRow, ActionPill, IconButton, PluginRow)
renders an accent focus ring while focused.
2026-05-14 02:25:05 -04:00
Ryan Hughes c7da0f74aa Replace bar-settings with unified settings plugin
Consolidates bar-settings into a single 'settings' plugin with sidebar
categories: Defaults, Style, Bar, System, Plugins. Updates supporting
commands (omarchy-launch-settings, omarchy-style-corners-quickshell,
omarchy-theme-list-with-previews, omarchy-hyprland-monitor-scaling-set)
and refreshes sidebar glyphs to Nerd Font icons.
2026-05-14 02:25:05 -04:00
Ryan Hughes e54648dfd5 Match Omarchy menu border to Walker 2026-05-14 02:25:05 -04:00
Ryan Hughes 0e135bbb75 Integrate omarchy menu into shell 2026-05-14 02:25:05 -04:00
Ryan Hughes 7d2745b6a6 Refine quickshell bar and Noctalia compatibility 2026-05-14 02:21:48 -04:00
Ryan Hughes 5086654d51 Hide Wi-Fi manager button when no Wi-Fi station is present
On ethernet-only machines (no wl* device) the button was offering to
open a tool the user can't usefully act on.
2026-05-14 02:21:48 -04:00
Ryan Hughes ece9267995 Show network connection details and gracefully handle iwd-only setups
Network panel now polls 'ip route get 1.1.1.1' for the active interface
and renders a key/value grid with IP/CIDR, gateway, link speed +
duplex (ethernet) or signal/freq/bitrate (Wi-Fi). One bash script
collects everything in TSV.

Wi-Fi list now works on Omarchy's default iwd+impala stack: when
nmcli isn't installed, falls back to 'iwctl station <dev> get-networks'
and parses the connected/SSID/signal/security columns. The list is
hidden entirely when no Wi-Fi station device exists (ethernet-only
machines).

Right-click on the bar widget and the bottom 'Open Wi-Fi manager
(Impala)' button both run omarchy-launch-wifi directly (was wrapped
in extra OMARCHY_PATH= env that wasn't actually doing anything).
2026-05-14 02:21:48 -04:00
Ryan Hughes 0b271d3ab1 Show row close button on every known bluetooth device
Was gated on isConnected; users with paired-but-disconnected devices
(or devices Quickshell hasn't flagged as connected yet) saw no button
at all. Now shows on every known device. Action follows state:
connected -> disconnect, otherwise -> forget. Tooltip label flips to
match.
2026-05-14 02:21:48 -04:00
Ryan Hughes 6c84bc93da Drop row tooltip; add explicit disconnect button on connected devices
Bluetooth rows no longer hover-show a tooltip — clicking to connect
or pair is self-evident from the layout. Connected rows now ignore
their own click and surface a small × button on the right that
disconnects (with its own tooltip + urgent-color hover affordance).
Right-click still forgets the device.
2026-05-14 02:21:48 -04:00
Ryan Hughes 84e8a42813 Theme tooltips inside popups
PillButton's attached ToolTip is now a styled instance — uses the
widget's foreground color for text + border and a configurable
tooltipBackground (defaults to a near-black, overridable to
bar.background). No more stock OS chrome inside our popups.

Bluetooth panel passes bar.background/foreground through to its
header pills and replaces the row tooltip with the same themed style.
2026-05-14 02:21:48 -04:00
Ryan Hughes ab701f7c49 Show connection state per bluetooth device
Each device row is now a two-line entry: name on top, status on the
bottom (Connected · 80%, Connecting…, Pairing…, Disconnecting…, Failed,
Available · click to pair, or Paired). Status text and the bluetooth
icon both color-code by state — connected/active draws in foreground,
idle in dim, failed in urgent.

Pending click attempts arm a 10s timer; if the device hasn't reached
the expected state by then the row flips to 'Failed' with an urgent
color so you can see the connect attempt didn't take. State change
listeners clear the pending flag the moment the device actually
connects or pairs.

Hover tooltips and right-click-to-forget moved into the row so the
behavior is self-contained.
2026-05-14 02:21:48 -04:00
Ryan Hughes 53f9899d5a Show discovered devices and shrink bluetooth header buttons
- Scan and Manage are now small icon-only buttons in the header beside
  the On/Off pill, with hover tooltips via the new PillButton.tooltipText
  prop (backed by QtQuick.Controls ToolTip).
- New discoveredDevices list pulls everything in Bluetooth.devices that
  isn't paired/connected/bonded/trusted. While scanning, those appear
  under a 'Discovered' header. Click pairs and connects.
- Empty-state text only shows when there are no known and no
  discovered devices, otherwise it would push down real entries.
2026-05-14 02:21:48 -04:00
Ryan Hughes fbe45d517b Improve bluetooth panel layout
- Header now uses anchors so the on/off pill sits cleanly at the right
  instead of relying on a fixed-width spacer that drifted as content
  grew.
- Discovery toggle moved out of the header and into a labeled action
  row alongside a new 'Manage…' button that launches
  omarchy-launch-bluetooth (the Impala TUI).
- Empty-state message tells you what to do based on adapter state
  (off / scanning / no paired devices) instead of one generic line.
2026-05-14 02:21:48 -04:00
Ryan Hughes 3177a396ff Break weather popup info onto separate lines
wttr.in query switched from one-blob format to pipe-delimited fields
(location|condition|temp|feels|wind|humidity). The popup parses each
piece into its own property and renders a clean key/value grid:

  fort lauderdale, florida, us
  Partly cloudy

  Temperature  +82°F (feels +91°F)
  Wind         ↗15mph
  Humidity     82%

  [Refresh] [wttr.in]
2026-05-14 02:21:48 -04:00
Ryan Hughes bfa8e2234e Show weather popup on hover
PopupCard gains a triggerMode property — "click" stays as the default
(focus-grab dismissal), "hover" skips the grab so the cursor can move
freely between the trigger and the card. PopupCard also exposes a
containsMouse alias for widgets that want to coordinate hover state.

Weather widget now shows on hover and hides 220ms after the cursor
leaves both the trigger and the popup. The redundant tooltip is gone —
the popup is the detail view. Click is still wired for refresh
(middle) and notification dump (right).
2026-05-14 02:21:48 -04:00
Ryan Hughes 7425dd74ae Tighten weather popup layout and add outside-click dismiss
PopupCard now uses HyprlandFocusGrab so any popup in the shell closes
when the user clicks outside it (or any other window in the listed set).
Single primitive — every widget gets it for free.

Weather popup rewritten:
- Smaller cloud glyph beside the city name on one row, both vertically
  centered.
- Condition string flows on its own line and wraps inside the popup.
- Refresh and wttr.in pills sit side by side instead of stacked, no
  more dead vertical space.
- contentHeight tracks the actual content height so the popup is no
  taller than it needs to be.
2026-05-14 02:21:48 -04:00
Ryan Hughes 5476b9d13b Drop animated workspacesPro widget; legacy workspaces is the default
The legacy 'workspaces' built-in renders cleaner than the animated
workspacesPro file widget. Removed the widget file, its first-party
registration in Bar.qml, and updated the default layouts and docs.
'workspaces' loses the (legacy) tag in the catalog since there's no
alternative anymore.
2026-05-14 02:21:48 -04:00
Ryan Hughes 14ed2560f9 Hide first-party panels/overlays from bar-settings catalog
omarchy.bar-settings and omarchy.image-picker are shell infrastructure —
the customizer and image picker themselves. Listing them in 'Other
plugins' with move/remove arrows is misleading: removing the panel
plugin doesn't disable anything (it's summoned on demand via IPC), and
adding them never made sense.

Dropped them from shell-defaults.json's plugins[] and from the inline
builtinShellConfig fallback. normalizeDraft and availableToAdd now skip
manifests with __isFirstParty so existing user shell.json files
self-heal on next load.
2026-05-14 02:21:48 -04:00
Ryan Hughes 546f4893ff Fix duplicate onBarWidgetRegistryChanged in bar-settings panel
Two handlers were declared for the same property-change signal — QML
refuses to load the file with 'Property value set multiple times', so
summoning omarchy.bar-settings failed silently. Merged the debug log
into the existing catalogRevision++ handler.
2026-05-14 02:21:48 -04:00
Ryan Hughes bcbe12b075 Unify config into shell.json and add Noctalia plugin support 2026-05-14 02:21:48 -04:00
Ryan Hughes 5c0f5669c9 Switch the image picker to IpcHandler 2026-05-14 02:21:48 -04:00
Ryan Hughes 4e8bf99240 Polish docs and harden plugin loading 2026-05-14 02:21:48 -04:00
Ryan Hughes 714e8a453b Move bar-settings and background-switcher into the shell 2026-05-14 02:21:48 -04:00
Ryan Hughes c72a8756b3 Add omarchy-shell with a plugin registry 2026-05-14 02:21:48 -04:00
Ryan Hughes 619a0d5737 Update GUI state in resetToDefaults
suppressReload was eating the file-watch callback that would have
called loadConfig and refreshed the draft from disk, so the GUI stayed
on the pre-reset layout even though bar.json was already correct.
Apply the reset payload directly to draft (and bump draftRevision) so
section editors re-render in the same frame.
2026-05-14 02:21:48 -04:00
Ryan Hughes 34c645af72 Live-save bar settings as you edit
Every layout mutation (add/remove/reorder, settings dialog Apply,
position or centerAnchor change) now writes ~/.config/omarchy/bar.json
immediately. No more Save button — replaced with an inline 'Auto-saving
to ...' status line next to Reset to defaults.

To stop the FileView feedback loop (write → inotify → reload → set
draft → write again) the shell keeps a one-shot suppressReload flag
that swallows the userFile.onLoaded triggered by our own setText.
2026-05-14 02:21:47 -04:00
Ryan Hughes e9806b093b Reset to defaults: bundle builtin defaults as a guaranteed fallback
Reset previously could zero the bar out when defaultConfig was empty —
either because bar-defaults.json hadn't finished loading or because the
resolved path was wrong. The shell now carries an inline builtinBarConfig
that mirrors bar-defaults.json, and Reset uses it whenever the on-disk
defaults aren't yet populated.

Also: log the resolved omarchyPath / defaultsPath / userConfigPath at
startup, and route FileView load failures through console.warn so the
next time a user hits this they can see why.
2026-05-14 02:21:47 -04:00
Ryan Hughes 66155fff84 Persist full bar config instead of a diff against defaults
The diff serializer would write only the sections the user touched and
rely on the bar merging defaults back in at load time. After a Reset to
Defaults the user file became {}, so the next 'Add widget' diffed
against the section and wrote ONLY the user-added entry, dropping every
default widget the user could see in the GUI.

- Reset to Defaults now writes the merged defaults verbatim so the GUI
  and bar.json agree on the full widget list, and any subsequent edit
  operates on that explicit list.
- Save writes the full draft, not a diff. The user gets a predictable
  what-you-see-is-what-you-save customizer at the cost of Omarchy
  defaults changes not silently propagating after the first save.
2026-05-14 02:21:47 -04:00
Ryan Hughes 9862aeb661 Resolve first-party widgets relative to the shell.qml directory
Quickshell.shellDir points at the directory holding the loaded
shell.qml, so the bar can find its own widgets and bar-defaults.json
without depending on OMARCHY_PATH being set in the environment. The
old behaviour silently fell back to ~/.local/share/omarchy when the
env var was unset, which is fine for the installed shell but caused
'No such file or directory' for every first-party widget when running
quickshell -p straight from the repo.

- firstPartyWidgetSource now uses Qt.resolvedUrl("widgets/<name>.qml")
  so the resolution is anchored to shell.qml itself.
- deriveOmarchyPath() in both bar/shell.qml and bar-settings/shell.qml
  walks back from Quickshell.shellDir to find the omarchy root, with
  OMARCHY_PATH still allowed as an explicit override and the install
  default as a final fallback.
2026-05-14 02:21:47 -04:00
Ryan Hughes af5a71347b Address reviewer findings for bar settings GUI
CRITICAL:
- Re-inject working entry into the settings form every time the dialog
  opens. Previously the Loader's onLoaded only fires when the
  sourceComponent identity changes, so reopening the dialog on the same
  widget id (or on a second instance of the same id, e.g. two spacers)
  showed stale field values.
- 'Add widget' menu and the centerAnchor dropdown both read
  draft.layout[section] without depending on the section array
  identity. Mutating the array in place did not invalidate those
  bindings, so the menu would show a stale availability list and let
  non-spacer widgets be added twice. Layout mutations now replace the
  whole  object so any binding that reads it re-evaluates.

WARNINGS:
- Save now writes only the diff against the merged defaults so future
  bar-defaults changes are not silently shadowed by a stale snapshot.
- FileView onFileChanged handlers simplified to a single reload() —
  the explicit second loader call was duplicating loadConfig and
  bumping barConfigSerial twice per change (visible flicker).
- userFile onLoaded ignores reloads while the user has an unsaved
  working draft so external edits during editing do not clobber the
  in-progress state.
- SpinBox forms switched from onValueChanged to onValueModified so the
  initial-value bind no longer fires fieldChanged.
- Hyprland window-rule sizes aligned with the FloatingWindow
  implicitWidth/Height values (720 main, 380 dialog).
- omarchy-launch-bar-settings falls back to initialTitle when focusing,
  so a dialog that has not yet had its title applied still surfaces
  the existing instance.
2026-05-14 02:21:47 -04:00
Ryan Hughes a03f589258 Add visual bar customizer
Each layout entry is now an object — {id, ...inline settings} — instead
of a (name, separate modules map) pair. Multiple instances of the same
widget are trivially supported (spacer is the main beneficiary). Settings
travel with the entry, so reordering keeps them attached.

shell.qml:
- normalizeLayout converts string entries to {id} and drops invalid ones
- ModuleSlot binds to a full entry; moduleName and moduleSettings derive
  from it via entryId/entrySettings helpers
- centerAnchor lookup walks layoutEntries directly
- builtinBarConfig defaults use the object form
- README + bar-defaults.json updated to the new schema

bar-settings/ (new Quickshell config):
- Top toolbar with Reset / Save and Position / centerAnchor dropdowns
- Three section editors (left/center/right) with current widgets shown
  as cards: name + description + move up/down/settings/remove buttons
- '+ Add widget' menu per section, sorted alphabetically by display name
  (only spacer can repeat; everything else is one per bar)
- Per-widget settings dialog (FloatingWindow) loads an inline schema for
  widgets in the catalog. Initial schemas: spacer (size), calendar
  (formats), brightness (step)
- FileView { atomicWrites } writes to ~/.config/omarchy/bar.json on Save

omarchy-launch-bar-settings + hypr window rules pin the settings window
to a 760px floating square. controlCenter gains a 'Customize bar…' button
that launches it.
2026-05-14 02:21:47 -04:00
Ryan Hughes 9ddf05f230 Add controlCenter widget: one-click quick settings popup
Single bar button (md-tune-vertical glyph) that opens a popup with the
controls most users reach for repeatedly throughout the day:

- Volume slider + mute toggle (Pipewire defaultAudioSink)
- Brightness slider (brightnessctl, coalesced via writeTimer)
- 2x2 tile grid: Do Not Disturb (makoctl mode), Night Light (omarchy-
  toggle-nightlight, temperature probed), Keep Awake (hypridle pid
  detection), Theme (current symlink + opens omarchy menu themes)
- Power-profile row (Saver / Balanced / Performance), hidden when
  power-profiles-daemon is not installed

Polled every 3s via Process + StdioCollector pairs gated by !running
busy checks. Default layout adds it before powerMenu on the right.
2026-05-14 02:21:47 -04:00
Ryan Hughes db13654815 Address review on Noctalia-inspired widgets
- nightLight queries hyprctl hyprsunset temperature to detect state
  instead of pgrep, since omarchy-toggle-nightlight keeps the daemon
  running across toggles. Adds a 'toggling' busy flag so repeated
  clicks do not stack the toggle script.
- lockKeys reads each LED individually and emits 'missing' when the
  class is absent, fixing column-shift mislabels on keyboards that
  only expose a subset of the locks. Hides + stops polling when no
  LEDs are reported. Poll interval relaxed to 2s.
- activeWindow right-click now calls toplevel.close() against the
  captured window instead of dispatching killactive against whatever
  is focused at click time.
- Drop unused Common imports from activeWindow and lockKeys.
2026-05-14 02:21:47 -04:00
Ryan Hughes a901d4a71f Add 5 widgets inspired by Noctalia: activeWindow, nightLight, keyboardLayout, lockKeys, spacer
- activeWindow: shows the focused toplevel title from Quickshell.Wayland
  ToplevelManager. Left-click activates, middle-click closes, right-click
  killactive. Hidden in vertical bars to avoid title rotation noise.
- nightLight: toggles omarchy-toggle-nightlight; status detected via
  pgrep hyprsunset. Hides when hyprsunset isn't installed.
- keyboardLayout: shows current xkb layout short code, click cycles via
  hyprctl switchxkblayout. Refreshes on Hyprland activelayout events.
- lockKeys: caps/num/scroll lock indicators read from /sys/class/leds.
  Uses plain A/1/S letters; hides each indicator when off.
- spacer: configurable blank space (size) for layout tuning.

Defaults add activeWindow to the left section and nightLight to the
right section between audioPanel and brightness.
2026-05-14 02:21:47 -04:00
Ryan Hughes 55a9075415 Address reviewer follow-ups: powerProfile detect, slider live-value, glyphs
- powerProfile now hides on machines without power-profiles-daemon by
  checking against the published profile set rather than a never-undefined
  enum, and uses mdi-scale-balance for Balanced (not battery-charging).
- networkPanel renames the QML-keyword-shadowing 'signal' property to
  'signalStrength'; ethernet glyph corrected to mdi-ethernet (not the
  access-point icon).
- workspacesPro indicator color guards against null bar during construction.
- Common.Slider keeps an internal liveValue so external value bindings
  survive a drag; sliders fed from PipeWire/brightnessctl now follow
  external changes after the user interacts.
- audioPanel drops the redundant default-sink PwObjectTracker since the
  candidate-sinks tracker already covers it.
- networkPanel guards scan/connect commands against re-entry while the
  previous Process is running.
- notificationCenter only instantiates its NotificationServer when the
  module is opted in via settings.replaceMako (default off) so it does
  not collide with mako out of the box.
- PopupCard fully opaque while open (previously 0.98) — removes a
  visible ghost outline on first render.
- Drop dead code: calendar trailing MouseArea, weatherFlyout's unused
  parseForecast/forecast property, idleInhibitor's holdSeconds,
  media's scrollAnim.position; gate media's scroll animation off in
  vertical bar mode.
2026-05-14 02:21:47 -04:00
Ryan Hughes fe73986a01 Address momus review: fix icons, vertical layout, popup coordination
- Replace nerd font glyphs that were stripped during the initial widget
  writes with verified codepoints sourced from the JetBrains Mono Nerd
  Font cmap (cpu-64-bit, scale-balance, notification-clear-all, etc).
- systemStats now switches to a Column layout when the bar is vertical
  so the widget no longer overflows a 28px-wide side bar.
- Remove notificationCenter from defaults: it registers a notification
  server and would collide with the mako daemon Omarchy autostarts.
- PopupCard owns the popout coordinator lifecycle and delegates close to
  its widget owner when provided; calendar and media now pass owner.
- brightness debounces brightnessctl writes through a coalescing timer
  to avoid spawning a process per slider tick.
- audioPanel tracks unfiltered Pipewire sinks/streams via PwObjectTracker
  so audio metadata becomes available before filtering.
- ModuleSlot re-injects bar/moduleName/settings whenever the bar config
  serial changes so widget settings update live with bar.json.
- Guard null bar references in workspacesPro and systemStats components
  that are constructed before bar injection.
2026-05-14 02:21:47 -04:00
Ryan Hughes 221e0e1fcc Add modular interactive widgets to the Quickshell bar
Introduces a widgets/ directory of first-party QML modules and a common/
helpers library. Widgets auto-load by name from
default/quickshell/bar/widgets/<name>.qml — no edits to shell.qml needed
for additions beyond the firstPartyWidgets registry.

New widgets (all orientation-aware, top/bottom/left/right):
- media: MPRIS now-playing with scrolling label, cover art popup,
  play/pause/skip controls
- audioPanel: master volume slider, output device picker, per-app mixer
- networkPanel: Wi-Fi scan + connect, current connection details
- bluetoothPanel: paired device list, connect/disconnect, battery
- calendar: month grid popup keyed to today
- notificationCenter: live notification feed, DND toggle, clear all
- brightness: slider popup with scroll-to-adjust
- powerProfile: power-saver/balanced/performance picker (UPower)
- systemStats: inline CPU + RAM sparklines, expanded popup with btop
- weatherFlyout: weather popup with refresh and full report
- workspacesPro: animated focus indicator that slides between workspaces
- powerMenu: lock/suspend/log out/reboot/shutdown popup
- idleInhibitor: coffee-cup toggle wired to omarchy-toggle-idle
- microphone: mic state + mute toggle + scroll volume

Common helpers (default/quickshell/bar/common/):
- WidgetButton: hover/press scale animation, tooltip integration
- PopupCard: orientation-aware anchor + slide-in opacity transition
- Slider: drag/wheel-able linear slider with knob hover bump
- PillButton: rounded action button with icon + label

Bar root gains requestPopout/releasePopout so only one popup is open at
a time across widgets.

Updates bar-defaults.json to ship the new widgets out of the box while
keeping the legacy modules available.
2026-05-14 02:21:47 -04:00
Ryan Hughes 30dc9fddde Add Quickshell bar 2026-05-14 02:21:47 -04:00
Ryan Hughes 6b074c7a17 Merge pull request #5804 from bjarneo/tab-themes-and-backgrounds
Enable tab and shift+tab to navigate back and forth the wallpaper/theme selector
2026-05-14 02:02:56 -04:00
Bjarne Øverli d27c1e8346 Enable tab and shift+tab to navigate back and forth 2026-05-13 17:22:15 +02:00
Ryan Hughes 087909714e Add tmux keybindings viewer 2026-05-13 00:20:38 -04:00
Ryan Hughes be55e8048c Prevent modifier keys being stuck on Super + C / V / X 2026-05-12 22:41:27 -04:00
Ryan Hughes d932f47eaf Prevent bg-next from taking non-images 2026-05-12 21:57:57 -04:00
David Heinemeier Hansson 8d5e9e794e Move to pnpm dlx with a 5-day lag to skip the latest zero-day npm package exploits 2026-05-12 23:58:27 +02:00
Ryan Hughes c0e099fed5 Prevent wiping waybar configs to reposition 2026-05-11 20:07:18 -04:00
Ryan Hughes aada2a4c9c Preserve waybar position on refresh 2026-05-11 20:01:18 -04:00
Ryan Hughes 0601dea910 Merge pull request #5759 from basecamp/skwd-wall
Add QuickShell-based backgrounds selector
2026-05-11 18:38:39 -04:00
Ryan Hughes d54e8b5429 Use theme colors in image selector 2026-05-11 18:10:58 -04:00
Ryan Hughes f14d82b2f3 Start image selector on demand 2026-05-11 18:02:45 -04:00
Ryan Hughes cb87adfc99 Toggle image selector closed when already open 2026-05-11 17:53:40 -04:00
Ryan Hughes 3d8c061a39 Lazy load theme switcher thumbnails 2026-05-11 17:41:30 -04:00
Ryan Hughes 427783c41f Stub theme switcher benchmark 2026-05-11 17:20:46 -04:00
Ryan Hughes f24dd82181 Add filter typing to theme switcher 2026-05-11 17:10:45 -04:00
Ryan Hughes 903f27fdd1 Tighten image selector request handling 2026-05-11 16:50:26 -04:00
Ryan Hughes 0801209090 Keep selector labels readable 2026-05-11 16:07:37 -04:00
Ryan Hughes 314a7f75c0 Keep image selector preview centered 2026-05-11 16:03:31 -04:00
Ryan Hughes 3aaedd88b1 Remove selector width animation jitter 2026-05-11 15:57:59 -04:00
Ryan Hughes ca0ea7af57 Clamp image selector navigation 2026-05-11 15:52:38 -04:00
Ryan Hughes 693a23c58c Open image selector at selected item immediately 2026-05-11 15:47:01 -04:00
Ryan Hughes b89079ae5c Show theme names in theme switcher 2026-05-11 15:40:19 -04:00
David Heinemeier Hansson f6bd69bc0f Fix color sync 2026-05-11 15:29:31 -04:00
David Heinemeier Hansson ef129b9dab Consistency 2026-05-11 15:29:31 -04:00
David Heinemeier Hansson 0426f9b206 Cache warming 2026-05-11 15:29:31 -04:00
David Heinemeier Hansson b48f67058b Use consistent terms 2026-05-11 15:29:31 -04:00
David Heinemeier Hansson 000b7c79af Improve performance 2026-05-11 15:29:11 -04:00
David Heinemeier Hansson 0b2332e45d Permanent process for speed 2026-05-11 15:29:11 -04:00
David Heinemeier Hansson 5487c17d19 Single namespace is fine 2026-05-11 15:28:52 -04:00
David Heinemeier Hansson 008f0cf7dd Extract generic switcher 2026-05-11 15:28:52 -04:00
David Heinemeier Hansson 1e646be7ee We need quickshell available 2026-05-11 15:28:33 -04:00
David Heinemeier Hansson aba11e716f Tweaks 2026-05-11 15:28:33 -04:00
David Heinemeier Hansson 333ec367df Let's build our own wall 2026-05-11 15:28:33 -04:00
David Heinemeier Hansson 7a988d8909 Switch to using the switcher on all the normal keybindings 2026-05-11 15:27:56 -04:00
David Heinemeier Hansson 67751a45b5 Correct indention 2026-05-11 15:27:43 -04:00
David Heinemeier Hansson da822f6ffb Change from swaybg to skwd-wall
Nicer picker and support for movie bgs
2026-05-11 15:27:43 -04:00
Ryan Hughes 15658cc622 Merge pull request #5723 from basecamp/hyprland-lua-conversion
Hyprland lua conversion
2026-05-11 15:02:02 -04:00
David Heinemeier Hansson 43ec46f699 Add tds 4-way 2026-05-11 18:21:45 +02:00
Ryan Hughes 350b9a33b3 Fix keybinds display 2026-05-11 10:40:26 -04:00
Ryan Hughes e39206f24d Simplify montor scaling 2026-05-11 10:40:26 -04:00
Ryan Hughes 804571d8e2 Update all hyprland references to new lua ones 2026-05-11 10:40:26 -04:00
Ryan Hughes 82a1a91509 Add conf -> lua migration for Hyprland and update old migrations 2026-05-11 10:39:22 -04:00
Ryan Hughes 59a2619426 Update theme specifics to lua 2026-05-11 10:39:22 -04:00
Ryan Hughes f1bec15325 Update theme and SDDM to lua 2026-05-11 10:39:22 -04:00
Ryan Hughes c7b6a7f8c4 Swap defaults to lua 2026-05-11 10:39:22 -04:00
Ryan Hughes b93650991f Swap configs to lua 2026-05-11 10:39:22 -04:00
Ryan Hughes 7e6403133c Prevent lsp complaining 2026-05-11 10:39:22 -04:00
Mateusz Kowalczyk 1acaa7ba4d Fix brightness indicator glitches caused by hardware-side key auto-repeat (#5760)
* Avoid overlapping brightness change key events.

* Clarify the fix comment
2026-05-11 16:33:58 +02:00
David Heinemeier Hansson dc4e6a6a50 One more weather icon tweak for light drizzle 2026-05-11 16:32:51 +02:00
David Heinemeier Hansson 147cc721f0 Better rain mapping of icons 2026-05-11 16:21:54 +02:00
David Heinemeier Hansson 95531a39c0 How to evolve features that require input and interaction 2026-05-11 15:45:36 +02:00
Michal SlotaandDavid Heinemeier Hansson c6ecdd1000 Add dispatch shortcuts to help menu (#5756)
* feat: dispatch shortcuts from help menu

* Retain uwsm launch wrapping

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-11 15:40:04 +02:00
David Heinemeier Hansson 5738583319 RetroArch game launcher installer (#5753)
* Agents should use the notification helper

* Turn a single RetroArch game into its own application launcher

* Tweak installer

* Extract selection logic into cli

* Use consistent glyph

* One more
2026-05-11 13:01:45 +02:00
David Heinemeier Hansson fc787cd040 Agents should use the notification helper 2026-05-11 12:13:33 +02:00
David Heinemeier Hansson b2f89061c7 Add hunk for diffing as a lazy-install npx 2026-05-11 11:31:42 +02:00
Michael PlaczekandDavid Heinemeier Hansson 92f93cb0e5 Persist Bluetooth power state across reboots (#5683)
* Persist Bluetooth power state across reboots

BlueZ defaults to AutoEnable=true, which forces controllers on at boot
regardless of the last user-set state. Setting AutoEnable=false makes
bluetoothd respect the persisted Powered state, so toggling off in
bluetui (or any DBus client) survives a reboot.

Adds the change to the install path and a migration for existing setups.

* Set reboot-required in Bluetooth persistence migration

Per review feedback, the AutoEnable=false change only takes effect on
bluetoothd startup. Without setting reboot-required, omarchy-update-restart
won't prompt the user to reboot, so the fix would only kick in at their
next organic reboot — by which time the bug may appear unfixed.

Matches the convention in migrations/1776346552.sh and similar.

* No need to ask for a reboot when this is available on the next one

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-11 10:21:16 +02:00
Ty SmithandClaude Opus 4.7 c4dbf12920 Add pre-refresh-pacman.d hooks for custom repository support (#5681)
Fires after the channel template is copied to /etc/pacman.conf and before
`pacman -Syyuu` runs, letting users layer custom Pacman repositories or
IgnorePkg directives onto the freshly-written config so they're respected
by the upgrade. Useful for users running on overlay distros (CachyOS,
Chaotic-AUR, internal company repos) where a wipe-and-reinstall would
otherwise pull vanilla Arch versions.

Ships with `pre-refresh-pacman.d/add-custom-repo.sample` showing the
pattern.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:19:12 +02:00
René 324c9a4054 Fix fontconfig monospace binding overriding app-specific fonts (#5700)
Change monospace match from mode="assign" binding="strong" to
mode="append" binding="same" so the system font acts as a fallback
instead of overriding explicit app font configs like Ghostty's font-family.

Add migration to fix existing user configs.

Fixes #5675
2026-05-11 10:15:37 +02:00
David Heinemeier Hansson 378ba984ca Checkout the full git so branch switching is still possible
Closes #5701
2026-05-11 10:11:53 +02:00
David Heinemeier Hansson 705e26eb8a Migrate the existing imv configs 2026-05-11 10:00:28 +02:00
Jon Kinney cea144bc51 Trash imv deletes; add Ctrl+E to edit in Satty (#5721)
* Trash imv deletes; add Ctrl+E to edit in Satty

Ctrl+X / Ctrl+Shift+X now move the image to the freedesktop trash via
`gio trash` instead of unconditional `rm`, so accidental presses are
recoverable from the file manager's Trash.

Adds Ctrl+E to open the current image in Satty (already in
omarchy-base.packages) and quit imv, mirroring the typical
screenshot → annotate workflow.

* Add -- separator to gio trash invocations

Prevents filenames starting with - from being parsed as options.
2026-05-11 10:00:20 +02:00
46365b7d4d Scope branding image pickers to ~/Pictures, fix About transcode mode (#5733)
* Scope branding image pickers to ~/Pictures, fix About transcode mode

The "Set From Image" path in both Style → About and Style → Screensaver
was scanning all of $HOME for *.svg / *.png and feeding the result into
Walker dmenu. On a populated home directory this can be tens of thousands
of entries, leaving the picker effectively unusable.

Scope the picker to ${XDG_PICTURES_DIR:-$HOME/Pictures}, matching the
pattern already used in omarchy-capture-screenshot.

While here, drop the hardcoded --mode block from omarchy-branding-about
so the About transcode falls back to the default braille mode, matching
what omarchy-branding-screensaver does and what stock about.txt ships
with. The reduced --width 54 stays so the result fits the fastfetch
logo column.

* Explain the scope of the picker

---------

Co-authored-by: Gavin Nugent <245706154+28allday@users.noreply.github.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-11 09:53:07 +02:00
David Heinemeier Hansson 6afb51298c Hide the unused bits 2026-05-10 21:29:58 +02:00
David Heinemeier Hansson cd461059d0 Just a little wider 2026-05-10 20:33:34 +02:00
David Heinemeier Hansson b2d95ee24b libretro-vice is now an explosion of minor pkgs 2026-05-10 16:47:29 +02:00
David Heinemeier Hansson f09743b2e9 libretro-vice is now an explosion of minor pkgs 2026-05-10 16:47:21 +02:00
David Heinemeier Hansson 8788dd2eb2 Fix arg order 2026-05-10 16:44:09 +02:00
David Heinemeier Hansson 0af7cc4a76 Fix overlap 2026-05-10 16:39:01 +02:00
David Heinemeier Hansson f426ae0de8 Lookup code symbols for better keybindings list 2026-05-10 16:31:08 +02:00
Michał ZaborowskiandDavid Heinemeier Hansson 642d87d443 Add fine and coarse window resize steps (#5717)
* add additional window resize shortcuts

* add additional window resize shortcuts

* Use bigger difference between the steps

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-10 16:26:02 +02:00
David Heinemeier HanssonandCopilot Autofix powered by AI afcfaf6b1e Control Waybar position (#5730)
* Waybar positioning and shape

* Reveal weather again on left/right

* Fix indicators moving bar left/right

* Clean it up

* Cleanup

* One step forward, one step backwards

* Fix styles

* More style massage

* More tweaks

* Just position for now

* Fix keys

* No longer needed

* No need to reset

* Simplify

* Reset but protect waybar configs

* Not needed

* Fix glyph

* Not needed

* Cleanup

* Better wording

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-10 15:42:26 +02:00
David Heinemeier Hansson f26cc8f157 Send a notification when we switch 2026-05-10 15:22:41 +02:00
David Heinemeier Hansson 566c106ec1 Better wording 2026-05-10 15:11:47 +02:00
David Heinemeier Hansson 5346bc6db1 Only return state when we are toggling
Otherwise we know it!
2026-05-10 15:08:33 +02:00
David Heinemeier Hansson 0075645f0e Set hyprlock corners too 2026-05-10 15:05:34 +02:00
David Heinemeier Hansson f10c945e00 Add Receive as an option to sharing 2026-05-10 14:50:48 +02:00
David Heinemeier Hansson 8a94891e77 Better glyph 2026-05-10 14:34:44 +02:00
David Heinemeier Hansson 2e29714eee Add integrated Style > Corner controls across hyprland, mako, walker 2026-05-10 14:29:51 +02:00
David Heinemeier Hansson 40a20e2a5c Fix glyph for Zen 2026-05-10 14:28:18 +02:00
David Heinemeier Hansson 8a38615f19 Fix for new notification send 2026-05-10 11:50:30 +02:00
David Heinemeier Hansson 887c7e3751 Set mako corners round or sharp 2026-05-10 11:50:19 +02:00
David Heinemeier Hansson e099f47d53 Add toggling of hyprland window rounded corners 2026-05-10 11:31:02 +02:00
David Heinemeier Hansson 26c1f9c546 Refactor toggles and notifications 2026-05-10 11:26:59 +02:00
David Heinemeier Hansson d3a082ef11 Fix upcoming reminders listing
It was comparing NextElapseUSecMonotonic against /proc/uptime, but those
values aren’t in the same monotonic timeline for user timers, so valid
upcoming reminders were being skipped.
2026-05-10 10:52:20 +02:00
David Heinemeier Hansson 2ab639ac0a Latest version from HANCORE 2026-05-10 10:41:34 +02:00
David Heinemeier Hansson 3d42142b88 Fix group coloring 2026-05-09 22:48:23 +02:00
David Heinemeier Hansson aa6b7f2975 Make the border highlight clearer 2026-05-09 22:35:18 +02:00
David Heinemeier Hansson 47a53a18ba Add Solitude theme by HANCORE 2026-05-09 22:33:10 +02:00
David Heinemeier Hansson b2fb238453 Add Last Horizon theme by HANCORE 2026-05-09 22:05:54 +02:00
David Heinemeier Hansson 676a278653 Slim down now that we are auto-detecting GDK_SCALE 2026-05-09 20:13:04 +02:00
David Heinemeier Hansson 890e3fc8c5 Set the initial GDK_SCALE to match the monitor scale 2026-05-09 19:55:15 +02:00
David Heinemeier Hansson 3eca9573ea Make GDK_SCALE change alongside the general monitor scaling 2026-05-09 19:29:22 +02:00
David Heinemeier Hansson 7f6abcd743 Merge pull request #5628 from basecamp/rc
Omarchy 3.8.0
2026-05-09 17:20:48 +02:00
David Heinemeier Hansson 44a0123058 Merge branch 'dev' into rc 2026-05-09 16:21:52 +02:00
David Heinemeier Hansson 2e0e9af0a9 Use save for screenshot 2026-05-09 12:04:53 +02:00
David Heinemeier Hansson 9a2c53d571 Add save as an option for agents 2026-05-09 12:04:34 +02:00
David Heinemeier Hansson 10c42afcf2 Echo result so agents can use them and keep working with them 2026-05-09 11:40:31 +02:00
David Heinemeier Hansson cd9d6bb54b Screenshot on visual changes 2026-05-09 11:20:36 +02:00
David Heinemeier Hansson dde76059a8 Merge branch 'dev' into rc 2026-05-09 08:30:09 +02:00
David Heinemeier Hansson b9830069b1 Remove duplicate cliamp .desktop and icon 2026-05-09 08:28:53 +02:00
David Heinemeier Hansson cf9c274ee5 Cliamp now ships with the icon and desktop entry on its own 2026-05-09 08:25:05 +02:00
David Heinemeier Hansson 8f43960b8f Merge branch 'dev' into rc 2026-05-08 22:31:44 +02:00
David Heinemeier Hansson ad2ae30f1e Remove VAAPI GL video feature flags from Chromium-based browser configs to prevent crashing on some machines 2026-05-08 22:31:01 +02:00
David Heinemeier Hansson 1fb412b5df Fix guidance 2026-05-08 22:07:49 +02:00
David Heinemeier Hansson 27b598a6e5 Fix guidance 2026-05-08 22:07:38 +02:00
David Heinemeier Hansson 6f281d888f Merge branch 'dev' into rc 2026-05-08 22:06:23 +02:00
David Heinemeier Hansson ce60cb81c1 Don't do that 2026-05-08 22:06:18 +02:00
David Heinemeier Hansson a865ecc875 Merge branch 'dev' into rc 2026-05-08 22:04:46 +02:00
David Heinemeier Hansson fb174bfa70 Fix perms 2026-05-08 22:04:41 +02:00
David Heinemeier Hansson 25e6fe2ef9 Hide cursor like on decryption screen 2026-05-08 22:04:01 +02:00
David Heinemeier Hansson b2ffc1ffa9 Merge branch 'dev' into rc 2026-05-08 21:53:15 +02:00
David Heinemeier Hansson 7b860f4291 Be more forceful about killing waybar
Sometimes it can still get stuck
2026-05-08 21:52:42 +02:00
David Heinemeier Hansson 2a1c1980b4 Merge branch 'dev' into rc 2026-05-08 21:44:27 +02:00
David Heinemeier Hansson dad2870f58 Check for weather once a minute instead of once every ten
More accurate and deals with the first-boot issue on network too
2026-05-08 21:44:21 +02:00
David Heinemeier Hansson d0000ea8cf Merge branch 'dev' into rc 2026-05-08 21:34:52 +02:00
David Heinemeier Hansson 2594a80cba Use latest package name 2026-05-08 21:34:46 +02:00
David Heinemeier Hansson 4c482769b8 Ensure quotes 2026-05-08 21:28:09 +02:00
David Heinemeier Hansson cff80ff699 Merge branch 'dev' into rc 2026-05-08 21:16:50 +02:00
Jzwnlandjzwnl a4778a01c0 Delay weather waybar startup to handle network initialization on boot (#5677)
Co-authored-by: jzwnl <jzwnl@users.noreply.github.com>
2026-05-08 21:14:29 +02:00
David Heinemeier Hansson c8c395b231 Merge branch 'dev' into rc 2026-05-08 21:09:42 +02:00
Jacob MinkandDavid Heinemeier Hansson 1537547f91 Use packaged Dell XPS touchpad haptics (#5534)
* Configure Dell XPS haptics directly

* Use packaged Dell XPS touchpad haptics service

* fix: keep one Dell haptics migration

* We already have a dedicated Trigger > Hardware menu

* Name for action

* Moved

* Need to make the package available for offline

* Simplify

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-08 21:07:51 +02:00
David Heinemeier Hansson 30f9a8da54 Not worth the extraction 2026-05-08 20:52:29 +02:00
David Heinemeier Hansson df4ffe991f Balance in output 2026-05-08 20:48:36 +02:00
David Heinemeier Hansson 1b5dcdafd2 Ensure this is a full reset 2026-05-08 20:44:07 +02:00
David Heinemeier Hansson e11d20cfba Quiet the warning on reinstall of keyring 2026-05-08 20:41:33 +02:00
David Heinemeier Hansson 37d6e4f92f Order the same as install menu 2026-05-08 16:46:08 +02:00
David Heinemeier Hansson 45db959daf Split fido2 and fingerprint setups into setup and remove (#5670)
* Split fido2 and fingerprint setups into setup and remove

Don't make one thing do two things

* Extract helpers and clean up

* No need to hide them

* Don't need these
2026-05-08 16:41:20 +02:00
David Heinemeier Hansson 366b160960 Fix tests 2026-05-08 16:36:46 +02:00
David Heinemeier Hansson fbc177baa8 Simpler 2026-05-08 16:11:20 +02:00
David Heinemeier Hansson 7702e837e1 Add About screen branding and make everything generic and available through cli 2026-05-08 16:07:36 +02:00
David Heinemeier Hansson 8c5fb7abc3 Make sure arg enums can autocomplete too 2026-05-08 16:06:10 +02:00
David Heinemeier Hansson 7b90929129 Don't find hidden files or directories 2026-05-08 16:02:42 +02:00
David Heinemeier Hansson 065b9439cd Launch screensaver after restoring default too 2026-05-08 15:43:14 +02:00
David Heinemeier Hansson 48859c828b Start the screensaver directly after changes rather than needing a preview 2026-05-08 15:41:31 +02:00
David Heinemeier Hansson 8757b2e1a4 Don't look in hidden directories 2026-05-08 15:41:15 +02:00
David Heinemeier Hansson 0e6e292348 Add 4x scaling too
Great for gif feature demos
2026-05-08 14:52:13 +02:00
mtp13andDavid Heinemeier Hansson b10f8116c8 fix(weather): Use local units for temperature and wind (#5660)
* fix(weather): Use local units for temperature and wind

Remove the metric-to-imperial temperature conversion and display
temperature and wind values as returned for the current locale.

* Don't need this comment

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-08 14:24:26 +02:00
twagner12 c7c3ef87af Fix DefaultLimitNOFILE typo so fd-limit raise actually applies (#5661)
`DefaultLimitNOFILESoft` is not a valid systemd configuration key.
systemd silently ignores it and emits this warning on every boot,
for both the system manager (PID 1) and the user manager:

  Unknown key 'DefaultLimitNOFILESoft' in section [Manager], ignoring.

The consequence is that the script's stated goal of raising the soft
fd limit from 1024 to 65536 has not actually been happening: dev
tools (VS Code, Docker, dev servers, databases) continue to run with
systemd's default 1024 soft limit.

The valid key is `DefaultLimitNOFILE`, which accepts either a single
value (sets both soft and hard) or `SOFT:HARD`. Using `65536:524288`
raises the soft limit while preserving systemd's typical 524288
hard-limit default — using the bare `=65536` form would lower the
hard limit, which would be a regression.

After this change and `systemctl daemon-reexec` on both managers,
`systemctl show | grep DefaultLimitNOFILE` reports:

  DefaultLimitNOFILE=524288     (hard)
  DefaultLimitNOFILESoft=65536  (soft, derived display attribute)

and the boot-time "Unknown key" warnings disappear.

(The typo likely originated from someone reading `systemctl show`
output, where `DefaultLimitNOFILESoft=` appears as a read-only
display attribute for the soft component of the parsed limit.
It looks like a config key but isn't.)
2026-05-08 13:22:29 +02:00
David Heinemeier Hansson 99b158cbc1 Back to trigger menu on esc 2026-05-08 12:45:31 +02:00
David Heinemeier Hansson f59e9133e1 Use new helper 2026-05-08 12:44:20 +02:00
David Heinemeier Hansson acb2c37b1f Have transcode flow through walker menus rather than gum prompts 2026-05-08 12:40:54 +02:00
David Heinemeier Hansson cbf4044c70 Omarchy menu helpers
Get more stuff out of the terminal
2026-05-08 12:40:37 +02:00
David Heinemeier Hansson 129c39448c Reminders (#5669)
* Extract notification command for using the correct spacing

* Add easy to set reminders

* More human

* Clear and direct hotkeys

* Split

* Bake reminders into the skill
2026-05-08 11:58:12 +02:00
David Heinemeier Hansson 9f187b55da Make sure the Omarchy agent skill is available for all harnesses 2026-05-08 11:56:06 +02:00
David Heinemeier Hansson 5971bab9d1 Extract notification command for using the correct spacing 2026-05-08 09:01:09 +02:00
David Heinemeier Hansson a43c304d21 Add post-boot.d hooks and give daily weather as an example 2026-05-08 08:39:25 +02:00
David Heinemeier Hansson 5c3ca6084b Group duplicate notifications 2026-05-08 08:35:28 +02:00
David Heinemeier Hansson 0c6bd1929f Force metric return units 2026-05-07 19:44:19 +02:00
Patrick RodriguesandDavid Heinemeier Hansson fa1ed01cd7 Run SwayOSD as a session service (#5656)
* Run SwayOSD as a session service

* Restart SwayOSD after clean exits

* We don't need the input backend (as we manage the hotkeys in Hyprland)

* Ensure we cleanup the old service starter

* Not needed

* Not needed either

* Clean up migration

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-07 19:43:27 +02:00
Mateusz Kowalczyk 402da79f54 Install hardware video acceleration for non-X-series Panther Lake Chips (#5655)
* Adjust condition to also match non-X series Panther Lake GPUs.

* Add migration to install GPU acceleration on non-X Panther Lake systems.

* Update comment with Non-Arc Panther Lake GPUs mentioned.

* Simplify conditions in the migration
2026-05-07 19:12:48 +02:00
Ryan Hughes 4f502322bd Update pi package source 2026-05-07 12:21:08 -04:00
David Heinemeier Hansson f2f65fdfb2 Merge branch 'dev' into rc 2026-05-07 17:02:13 +02:00
David Heinemeier Hansson 2293df2913 This requires a full reboot to work 2026-05-07 17:02:08 +02:00
David Heinemeier Hansson 573f75afeb Fix that zed cli is zeditor 2026-05-07 16:55:54 +02:00
Diego Rios 9c3520caf9 Enable Bluetooth A2DP auto-connect in WirePlumber (#5336)
* Enable Bluetooth A2DP auto-connect in WirePlumber

* Harden Bluetooth A2DP migration
2026-05-07 16:48:26 +02:00
David Heinemeier Hansson 74f319bb6f Merge branch 'dev' into rc 2026-05-07 16:18:28 +02:00
David Heinemeier Hansson 4831c883e5 Explain the 152 2026-05-07 16:18:23 +02:00
David Heinemeier Hansson 98e9bc6d05 Merge branch 'dev' into rc 2026-05-07 16:16:06 +02:00
David Heinemeier Hansson e14c685c7e Quoting 2026-05-07 16:16:01 +02:00
David Heinemeier Hansson 121a1e088a Merge branch 'dev' into rc 2026-05-07 16:15:15 +02:00
David Heinemeier Hansson b794321cd0 Prevent notification text wrapping 2026-05-07 16:15:04 +02:00
David Heinemeier Hansson 95d8125e59 Set default terminal, browser, editor explicitly (#5651)
* Set default terminal, browser, editor explicitly

* Consistent glyphs and setup

* Tweaks

* Always there

* Proper order

* Don't make a browser the default just because it's getting installed
2026-05-07 16:11:48 +02:00
David Heinemeier Hansson 71e303d2aa Merge branch 'dev' into rc 2026-05-07 15:12:31 +02:00
David Heinemeier Hansson 6f03b72870 Start hyprland for sddm without producing errors 2026-05-07 15:12:25 +02:00
David Heinemeier Hansson 4fbd750916 Add note about correct perms for migrations 2026-05-07 15:10:07 +02:00
David Heinemeier Hansson 3a240c7838 Add note about correct perms for migrations 2026-05-07 15:09:53 +02:00
David Heinemeier Hansson c9d1cdce76 Merge branch 'dev' into rc 2026-05-07 15:04:15 +02:00
David Heinemeier Hansson 2cc4a263ba Use a minimal Hyprland config for the SDDM Wayland greeter 2026-05-07 15:04:10 +02:00
David Heinemeier Hansson d8a9186326 Merge branch 'dev' into rc 2026-05-07 15:02:25 +02:00
David Heinemeier Hansson cd2c41f618 Add dark/light mode theme syncing to pi agent 2026-05-07 15:02:18 +02:00
David Heinemeier Hansson 38326c77fc Merge branch 'dev' into rc 2026-05-07 14:54:53 +02:00
David Heinemeier Hansson 4eb3a9198d Ensure SDDM never tries to start an x11 session, just go straight to wayland
Closes #5643
2026-05-07 12:56:26 +02:00
Mateusz KowalczykandCopilot Autofix powered by AI bc62c23351 Asus Zenbook UX5406AA Display Backlight Fix (#5620)
* Extract ASUS Expertbook B9406 display backlight fix and apply it for Zenbook ux5406aa too

* Add migration for Panther Lake Asus display bakclight fix

* Check if limine-update exists before executing it in the migration

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-07 12:10:45 +02:00
David Heinemeier Hansson 81aaade827 Make sub matches show which commands are available under it 2026-05-07 12:09:30 +02:00
Iqbal AttilaandDavid Heinemeier Hansson b3a62245f5 Add bass speaker fix for Lenovo Yoga Pro 7 14IAH10 (#5597)
* Add bass speaker fix for Lenovo Yoga Pro 7 14IAH10

The ALC287 codec on Yoga Pro 7 14IAH10 requires the alc287-yoga9-bass-spk-pin
quirk to properly route audio to both amplifier speakers. Without this fix,
only one speaker works and bass output is missing.

This adds:
- Hardware detection script (omarchy-hw-lenovo-yoga-pro7-bass)
- Audio fix that creates /etc/modprobe.d/lenovo-yoga-pro7-bass.conf

Tested on Lenovo Yoga Pro 7 14IAH10 (Intel Core Ultra 9 285H).
Reference: https://wiki.archlinux.org/title/Lenovo_Yoga_9i_2022_(14AiPI7)

* Add Lenovo Yoga Pro 7 bass fix to install flow

Addresses review feedback: the hardware fix script was not invoked
during installation. Add it to install/config/all.sh so it runs
automatically on matching hardware.

* Fix implementation

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-07 11:51:27 +02:00
Vitalii Bondar ba1d699c11 Allow configuring OCR languages via OMARCHY_OCR_LANGS (#5623)
Hardcoded -l eng makes SUPER+CTRL+PRINT produce garbage on any
non-ASCII English text. Read the language list from
OMARCHY_OCR_LANGS instead, defaulting to eng for backward
compatibility.
2026-05-07 11:49:44 +02:00
David Heinemeier Hansson 66492e57a7 Merge branch 'dev' into rc 2026-05-07 11:23:17 +02:00
ec4a3d0249 Add Transcode entry to Nautilus context menu (#5635)
* Add Transcode entry to Nautilus context menu

Right-click on image or video files in Nautilus to send them to
omarchy-transcode in a presentation terminal, mirroring the existing
LocalSend integration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Use shlex.quote for shell-safe command construction in transcode.py

Quote both the resolved binary path and file paths via shlex.quote
instead of a custom escaper. Addresses Copilot review on #5635.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Style changes

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-07 11:19:31 +02:00
David Heinemeier Hansson 5cf83d2073 Match sddm to decrypt style (#5645)
* Make SDDM screen match the normal decrypt screen as close as possible

* Try with a different error style on wrong pw

* Do red lock on failure too
2026-05-07 11:16:40 +02:00
David Heinemeier Hansson 6b6d71a9d2 Add live weather (#5633)
* Add live weather

* Just use jq

* Add Weather app and make it click target

* Simplify

* Looks better

* Nicer flow

* Simplify

* Move to using the notification instead of tooltip

* No longer needed
2026-05-07 10:48:32 +02:00
David Heinemeier Hansson 42cd7472cf Give people an invitation to install dictation after the first update 2026-05-07 10:35:23 +02:00
David Heinemeier Hansson c8f65ccdb1 Add a structured way to install a hook 2026-05-07 10:34:28 +02:00
David Heinemeier Hansson c9aece1cb7 Make sure individual hooks can't halt the process 2026-05-07 10:33:12 +02:00
David Heinemeier Hansson fe0ea4de3e Move to .d directories for hooks instead of a single file
Easier to manage especially with separate installs
2026-05-07 09:59:18 +02:00
David Heinemeier Hansson f0969c6793 Allow hooks to live in a .d directory 2026-05-07 09:46:00 +02:00
David Heinemeier Hansson 6a0935d248 Improve notification 2026-05-07 09:34:04 +02:00
David Heinemeier Hansson b6ad01a0fc Merge branch 'dev' into rc 2026-05-07 09:06:25 +02:00
David Heinemeier Hansson 95ba1c4296 Reference new F9 push-to-talk button 2026-05-07 09:06:17 +02:00
David Heinemeier Hansson 3f5300c13d Update meta data ruel 2026-05-06 21:06:24 +02:00
David Heinemeier Hansson cd4fadd502 Merge branch 'dev' into rc 2026-05-06 21:05:41 +02:00
David Heinemeier Hansson ec428fa549 Just focus on the window rules 2026-05-06 21:05:34 +02:00
David Heinemeier Hansson f838cbb63f Use proper .desktop files 2026-05-06 21:05:34 +02:00
David Heinemeier Hansson d4ffa9e39d Just focus on the window rules 2026-05-06 21:05:24 +02:00
David Heinemeier Hansson dc8ad1dace Use proper .desktop files 2026-05-06 21:03:43 +02:00
Luke Hsiao 2a5db9ed78 Fix helix theme's word selection colors (#5630)
* Fix helix theme's word selection colors

Without this fix, if users enabled `editor.cursorline`, the word
selection would not be visually distinct from the cursorline selection.

Closes: https://github.com/basecamp/omarchy/issues/5580
Signed-off-by: Luke Hsiao <luke@hsiao.dev>

* Make Retro-82 color0 visually distinct from background

Signed-off-by: Luke Hsiao <luke@hsiao.dev>

* Make matte-black selection background distinct from color0

Signed-off-by: Luke Hsiao <luke@hsiao.dev>

---------

Signed-off-by: Luke Hsiao <luke@hsiao.dev>
2026-05-06 20:38:08 +02:00
David Heinemeier Hansson c80d060321 Merge branch 'dev' into rc 2026-05-06 20:37:36 +02:00
David Heinemeier Hansson 36f26d7584 Make sure we regenerate the npx stubs too 2026-05-06 20:34:27 +02:00
David Heinemeier Hansson d827b90a4d Merge branch 'master' into rc 2026-05-06 17:16:10 +02:00
David Heinemeier Hansson e22b70f380 Merge branch 'dev' into rc 2026-05-06 16:52:49 +02:00
David Heinemeier Hansson 590da72ff6 Next version is 3.8 2026-05-06 16:52:44 +02:00
David Heinemeier Hansson d59ad099a9 Merge branch 'dev' into rc 2026-05-06 16:52:16 +02:00
d1056a271b Install sof-firmware on Intel Panther Lake for DSP audio (#5606)
* Install sof-firmware on Intel Panther Lake for DSP audio

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Need to have the sof-firmware package available during original offline install

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-06 16:39:37 +02:00
David Heinemeier Hansson d6b5e645f8 Add Amiga, C64, Amstrad cores to default RetroArch setup 2026-05-06 16:34:11 +02:00
David Heinemeier Hansson 4072fc6b1f Install a new default browser (#5627) 2026-05-06 16:26:21 +02:00
David Heinemeier Hansson f284cda94c Not necessary to show either 2026-05-06 13:23:12 +02:00
David Heinemeier Hansson 207d4f1ec8 Don't need to show this 2026-05-06 13:21:55 +02:00
David Heinemeier Hansson cc378a1f7b Hide more irrelevant stuff for users 2026-05-06 13:20:34 +02:00
David Heinemeier Hansson 11a54d1745 More natural order 2026-05-06 13:15:22 +02:00
David Heinemeier Hansson 61ee5d2cf2 Shorter command naming 2026-05-06 13:13:34 +02:00
David Heinemeier Hansson 5b6684d690 Add examples 2026-05-06 13:09:33 +02:00
David Heinemeier Hansson feea828e07 Not relevant for most 2026-05-06 13:06:09 +02:00
David Heinemeier Hansson 60156309a4 No need to list this twice 2026-05-06 13:05:20 +02:00
David Heinemeier Hansson f398cd274b Consistent command naming 2026-05-06 13:02:21 +02:00
David Heinemeier Hansson 538ebcd935 Simplify 2026-05-06 12:58:55 +02:00
David Heinemeier Hansson fb117e56d8 Hide certain commands from the list that aren't relevant to the vast majority 2026-05-06 12:56:51 +02:00
David Heinemeier Hansson 4e4a688fdf Prevent flicker on waking from sleep 2026-05-06 12:52:18 +02:00
David Heinemeier Hansson 7330c0f9a5 Quiet stdout for hook
Don't need all that omazed chatter
2026-05-06 12:44:23 +02:00
David Heinemeier Hansson 080feaf2f6 Better explanation 2026-05-06 12:37:28 +02:00
David Heinemeier Hansson 6111ee1341 Reduce screen off after system lock to 3s 2026-05-06 12:35:40 +02:00
David Heinemeier Hansson 647cae0ca0 cli doc improvements 2026-05-06 12:18:57 +02:00
David Heinemeier Hansson 99357bf549 Better documentation 2026-05-06 12:14:00 +02:00
David Heinemeier Hansson 90ac2b8ccc Ensure manually locking the screen also turns off display after 10s 2026-05-06 12:12:40 +02:00
David Heinemeier Hansson 4d11f8cc06 Extract turning brightness devices on/off 2026-05-06 12:12:26 +02:00
David Heinemeier Hansson ca74b4d1c5 Simplify but letting omarchy-system-lock be responsible for turning off display and keyboard brightness 2026-05-06 11:40:34 +02:00
David Heinemeier HanssonandCopilot Autofix powered by AI dd46e965d8 Better system idle (#5619)
* Better system idle

* Just use refresh

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-06 11:16:25 +02:00
David Heinemeier Hansson bc4e26ecf4 Update with the latest sensibilities 2026-05-06 09:58:48 +02:00
David Heinemeier Hansson b9fdb70e42 No longer misc 2026-05-06 09:54:48 +02:00
David Heinemeier Hansson f2e38aa1eb Turn extended keys on for tmux
So pi will stop complaining about it and so shift + return will just
work
2026-05-06 09:32:43 +02:00
David Heinemeier Hansson abd0113b01 Only just a peak 2026-05-06 09:29:35 +02:00
Alex Oxte 0114e793a9 fix: resolve relative wallpaper paths before creating symlink (#5612) 2026-05-06 07:47:48 +02:00
David Heinemeier Hansson 7debca9f47 Add foot terminal as an option 2026-05-05 22:18:02 +02:00
Miha Rekar cd30fd094e Add support for Studio Display XDR (#5599) 2026-05-05 21:18:09 +02:00
Pierre Olivier MartelandDavid Heinemeier Hansson 37a7913f3d Bundle Zed editor with omazed for themes syncing (#4873)
* Install Zed Editor with omazed for themes syncing

* Move Zed theme switching into Omarchy

* Launch zed after install

* revert

* forgot omazed!

* omazed is now on OPR

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-05 21:17:27 +02:00
David Heinemeier Hansson 581719c64f Default to visible third column in scrolling mode but suggest how to make it one 2026-05-05 18:59:40 +02:00
Ilya Zarubin 4ac9c99a8e fix: typo themeing -> theming (#5593) 2026-05-05 18:53:54 +02:00
Joel Corbalan 4dd2542329 Theme Catppuccin: Fixed colorscheme name (#5572) 2026-05-05 18:51:00 +02:00
David Heinemeier Hansson a99030e6ed Revert "Fix overly dark catppuccin borders in neovim"
This reverts commit c97dbb8ea149baa045b926c2b6701e9621fb6115.
2026-05-05 18:50:42 +02:00
David Heinemeier Hansson afabb3af6c Fix overly dark catppuccin borders in neovim
Especially bad with lazygit
2026-05-05 14:35:42 +02:00
David Heinemeier Hansson 00f822728a Prevent theme refreshing from changing the background image 2026-05-05 14:22:29 +02:00
David Heinemeier Hansson fcbe63482b Style fix 2026-05-05 14:22:03 +02:00
Pete Jackson 3d87216efd Make middle-click paste consistent across apps by enabling GTK/Chromium apps to receive selection buffer on button3 (#5577)
* Enable middle-click primary paste for GTK/Chromium apps

gsettings-desktop-schemas 50.x ships gtk-enable-primary-paste=false as
the default. GTK4 apps (Ghostty 1.3.0+) and Chromium-family browsers
honor that setting and silently no-op middle-click paste, even though
the primary selection itself works (wl-paste --primary returns text,
Firefox/LibreOffice paste fine because they implement primary paste
themselves rather than going through GTK).

Flip the GSetting to true so middle-click paste behaves the way Linux
desktop users expect across Ghostty, Brave, and Chromium.

Closes #5371

* Address Copilot review on #5577

Move install-time GSetting from install/config/ to install/first-run/
so it runs after login when a session D-Bus is guaranteed (matching the
existing gnome-theme.sh convention). Make the migration's gsettings
call best-effort with `|| true` so a missing dconf service doesn't
trigger the omarchy-migrate skip prompt.
2026-05-05 14:11:54 +02:00
David Heinemeier Hansson 6f00af1026 Fix sorting with latest on top 2026-05-05 14:00:06 +02:00
David Heinemeier Hansson b8fc1bb6d9 Add omarchy menu transcoding 2026-05-05 13:55:21 +02:00
David Heinemeier Hansson 8950fd4143 Allow file completions when we are out of omarchy completions 2026-05-05 13:46:56 +02:00
David Heinemeier Hansson 7ce23d409f Style choose better 2026-05-05 13:28:09 +02:00
David Heinemeier Hansson 24e1adb01a Use correct meta data for the command 2026-05-05 12:53:51 +02:00
Ryan Hughes 95ea1ff712 Version bump 2026-05-04 19:48:40 -04:00
Ryan Hughes 2315271e8e Add migration to handle lingering panel_replay addition 2026-05-04 19:48:10 -04:00
Bruno da Silva 90e07f42d2 FIX: Bistream typo to Bitstream in three places 2026-05-04 19:48:10 -04:00
Ryan Hughes a3aedb0c4d Version bump 2026-05-04 19:46:42 -04:00
Ryan Hughes 0a5cd43aac Add migration to handle lingering panel_replay addition 2026-05-04 19:46:00 -04:00
Ryan Hughes 6253d46622 Add a simple way to set the screensaver to an ascii version of an image 2026-05-04 19:43:18 -04:00
David Heinemeier Hansson 5ac8014e89 Merge pull request #5568 from maxguzenski/no-gaps-and-rounding
Add decoration section with rounding = 0 to no-gaps mode
2026-05-04 17:48:32 +02:00
David Heinemeier Hansson 335d06d0d7 Set joypad dir too 2026-05-04 17:41:18 +02:00
David Heinemeier Hansson 1ffa1fe306 Ensure joypads are autoconfigured too 2026-05-04 17:35:43 +02:00
David Heinemeier Hansson e07fe35557 Fix perms 2026-05-04 17:29:52 +02:00
David Heinemeier Hansson 9aa3858794 Merge pull request #5467 from basecamp/rc
Omarchy 3.7.0
2026-05-04 15:44:57 +02:00
David Heinemeier Hansson 648d8339d2 Merge branch 'dev' into rc 2026-05-04 15:42:37 +02:00
David Heinemeier Hansson ac69be30a3 Fix permissions 2026-05-04 15:42:30 +02:00
David Heinemeier Hansson 7138044bd2 Merge branch 'dev' into rc 2026-05-04 15:33:23 +02:00
David Heinemeier Hansson 86a4e48942 Move push-to-talk to F9 to prevent clashes 2026-05-04 15:33:17 +02:00
David Heinemeier Hansson 86a38c4a98 Merge branch 'dev' into rc 2026-05-04 14:43:07 +02:00
David Heinemeier Hansson fe5a93c28c Brave Origin wrapper is broken so it can only have one feature! 2026-05-04 14:43:00 +02:00
Maximiliano Guzenski 22b25991a5 Add decoration section with rounding = 0 when no-gaps mode 2026-05-04 09:05:30 -03:00
David Heinemeier Hansson 516882a43c Merge branch 'dev' into rc 2026-05-04 13:57:18 +02:00
David Heinemeier Hansson 21cfaab07d This moved 2026-05-04 13:31:32 +02:00
David Heinemeier Hansson fcba6237fe No longer needed 2026-05-04 13:28:00 +02:00
David Heinemeier Hansson e3d3ee4044 Merge branch 'dev' into rc 2026-05-04 13:23:37 +02:00
David Heinemeier Hansson 68e7e2c58d Ensure you know what you're about to sudo for 2026-05-04 13:23:27 +02:00
David Heinemeier Hansson 48a771bdac Merge branch 'dev' into rc 2026-05-04 12:38:16 +02:00
David Heinemeier Hansson ce9ab3f81a Fix kernel check for when multiple kernels are installed 2026-05-04 12:38:12 +02:00
David Heinemeier Hansson 03750b31c1 Merge branch 'dev' into rc 2026-05-04 12:23:41 +02:00
David Heinemeier Hansson 40e68e46f8 Turn on two-finger right-clicks by default 2026-05-04 12:23:36 +02:00
David Heinemeier Hansson a6280eed26 Merge branch 'dev' into rc 2026-05-04 11:36:24 +02:00
David Heinemeier Hansson d10ca57100 Add moonlight game streaming client 2026-05-04 11:36:19 +02:00
David Heinemeier Hansson f4a0249ff3 Merge branch 'dev' into rc 2026-05-04 11:18:30 +02:00
David Heinemeier Hansson d6fdc89fb9 Rename to match 2026-05-04 11:18:25 +02:00
David Heinemeier Hansson fa9466304b You need games before you can play 2026-05-04 11:17:41 +02:00
David Heinemeier Hansson 011243f4fa Merge branch 'dev' into rc 2026-05-04 10:52:16 +02:00
David Heinemeier Hansson 5c639c37d3 Add retroarch db package too
So everything works out of the box
2026-05-04 10:52:09 +02:00
David Heinemeier Hansson e7f45386d4 Merge branch 'dev' into rc 2026-05-04 10:38:50 +02:00
David Heinemeier Hansson 41076288a5 Rewire menu 2026-05-04 10:35:42 +02:00
David Heinemeier Hansson 1a57257fbf Add heroic and just go with lutris 2026-05-04 10:31:06 +02:00
David Heinemeier Hansson 5653d9a094 Better description 2026-05-04 10:29:58 +02:00
David Heinemeier Hansson d00e2b8a98 Add xbox cloud gaming as an option 2026-05-04 09:57:13 +02:00
David Heinemeier Hansson 5d3d3fd78a Improve xbox controller installation flow 2026-05-04 09:45:33 +02:00
David Heinemeier Hansson 5b288f1e28 Group all gaming installers together 2026-05-04 09:13:33 +02:00
David Heinemeier Hansson d0e3e76df9 Collapse installer together 2026-05-04 09:08:15 +02:00
David Heinemeier Hansson 517ae9d0e0 Update install instructions 2026-05-04 09:04:48 +02:00
David Heinemeier Hansson 9fa2b29c24 Add Remove > Gaming options 2026-05-04 08:57:19 +02:00
David Heinemeier Hansson 3a09750e01 Make it more resilient 2026-05-04 08:54:12 +02:00
David Heinemeier Hansson 962ef67a5a Add missing dependency 2026-05-04 08:53:36 +02:00
David Heinemeier Hansson cb3fcf4f60 Merge branch 'dev' into rc 2026-05-04 07:55:06 +02:00
David Heinemeier Hansson 3ae8234343 Add missing migration for ghui 2026-05-04 07:54:57 +02:00
David Heinemeier Hansson d7e3c0dcde Merge branch 'dev' into rc 2026-05-03 22:53:51 +02:00
David Heinemeier Hansson 42034a950f Add Battle.net installer via Lutris 2026-05-03 22:53:34 +02:00
David Heinemeier Hansson 0edff316da Merge branch 'dev' into rc 2026-05-03 22:02:43 +02:00
David Heinemeier Hansson 25701813c9 More preconfigging 2026-05-03 21:48:06 +02:00
David Heinemeier Hansson fb6f1d816b Set vulkan video driver 2026-05-03 21:35:33 +02:00
David Heinemeier Hansson ec0bcb3014 Better retroarch install with config setup and no AUR 2026-05-03 21:35:04 +02:00
Ryan Hughes c7a9d2b345 Add push-to-talk keybind for voxtype 2026-05-03 15:33:51 -04:00
David Heinemeier Hansson dfc6eab8db Ensure the elephant menus are always linked on a refresh
Closes #4717
2026-05-03 21:01:27 +02:00
David Heinemeier Hansson 9c944e89f6 Better install of retroarch 2026-05-03 20:54:13 +02:00
David Heinemeier Hansson 301ea3ecc6 Fix hard to see color 2026-05-03 20:54:13 +02:00
Ryan Hughes 6a5e53784f Bypass portal by default for screenrecording due to portal bug 2026-05-03 14:42:34 -04:00
Severin RichnerandDavid Heinemeier Hansson d904ed3afe Add dynamic theming for gum (#4467)
* Add dynamic theming for gum.

* Add migration.

* Fix migration script.

* Add omarchy-theme-reset utility.

* Move env variables into seperate file and update migration script.

* Migration adds the new hyprland config file to the users hyprland.conf.

* Add gum.env.conf to default env.conf instead.

* Reuse new omarchy-theme-refresh and update migration.

* Ensure no errors are thrown if the file can't be loaded

* Just apply this on the next theme selection

So it's not disruptive

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-03 20:26:03 +02:00
David Heinemeier Hansson 774acbf5af Merge branch 'dev' into rc 2026-05-03 19:38:02 +02:00
David Heinemeier Hansson 6370a673ed Allow ssh git urls not just web for theme installs
Closes #3916
Co-authored-by: @Vargrul
2026-05-03 19:36:50 +02:00
David Heinemeier Hansson 10165f2799 Add video to gif transcoding 2026-05-03 19:16:16 +02:00
Mitch LombardiandDavid Heinemeier Hansson a700806f91 Update Plymouth theme to enable smoother progress bar (#3661)
* Update Plymouth theme to enable smoother progress bar

* Fix migration

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-03 17:03:30 +02:00
David Heinemeier Hansson e8b1b4649f Add expectations about slow start 2026-05-03 16:02:37 +02:00
David Heinemeier Hansson dbefc1b850 Simplify the steam install so we don't need user intervention for drivers 2026-05-03 15:58:32 +02:00
David Heinemeier Hansson 37a073d8fb Stop setting SDL_VIDEODRIVER as it may interfere with steam gaming
Closes #2564
2026-05-03 15:45:40 +02:00
K T Prajwal PrathikshandDavid Heinemeier Hansson 75eec08758 Add screenshot OCR functionality with Minimal Dependencies (#1918)
* Add script to capture screenshot, perform OCR, and copy text to clipboard

* Add binding for screenshot-ocr

* Update OCR binding description

* Add tesseract OCR and language data installation

* Update bindings to current dev

* Refactor screenshot capture method to use wayfreeze and slurp for region selection

* Refactor OCR text extraction to simplify error handling

* Bring up to date

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-03 15:25:33 +02:00
David Heinemeier Hansson fcf742a3a1 Raise ulimit headroom
Closes #1758
2026-05-03 15:08:01 +02:00
David Heinemeier Hansson 70258684a2 Add support for IPv6 in DNS configuration
Closes #1633
Co-authored-by: @jardahrazdera
2026-05-03 15:01:30 +02:00
David Heinemeier Hansson 9ee810ef87 Prevent mic open pop on the recording 2026-05-03 14:31:18 +02:00
David Heinemeier Hansson fddaa5e869 Merge branch 'dev' into rc 2026-05-03 13:42:26 +02:00
David Heinemeier Hansson a4f58e836d Reports of this hanging, it's unnecessary
What's there now works, and the alert can be removed by reapplying the
theme manually.
2026-05-03 13:42:21 +02:00
David Heinemeier Hansson f1a4e9f6e8 Merge branch 'dev' into rc 2026-05-03 13:38:42 +02:00
David Heinemeier Hansson 2fa451baa5 We already have Work 2026-05-03 13:38:37 +02:00
David Heinemeier Hansson 5cf34963d7 Merge branch 'dev' into rc 2026-05-03 12:47:41 +02:00
David Heinemeier Hansson 6cceabb90f Add notifications silencing to the toggle menu 2026-05-03 12:47:36 +02:00
David Heinemeier Hansson d40e314d4f Merge branch 'dev' into rc 2026-05-03 12:05:52 +02:00
David Heinemeier Hansson c7cc2bc38a Drop the idea of elephant-all by default
It adds 80mb to the ISO for stuff we don't use out of the box. Better
just to have elephant-all for those who want it later.
2026-05-03 12:05:46 +02:00
David Heinemeier Hansson dc249dba95 Merge branch 'dev' into rc 2026-05-03 11:57:19 +02:00
David Heinemeier Hansson 791b0ce9f3 Fix not removing walker 2026-05-03 11:57:13 +02:00
David Heinemeier Hansson 5066ae8d34 Add VaapiVideoDecode flags for Chromium and Brave by default
Closes #1011
2026-05-03 11:37:05 +02:00
David Heinemeier Hansson efe5575e35 Revert "Fix typo in font (#5556)" (#5559)
This reverts commit 7a342f9bfe.
2026-05-03 11:05:19 +02:00
Tsurg 7a342f9bfe Fix typo in font (#5556)
* Fix typo of Bitstream font

* Fix typo in Bitstream font installation command
2026-05-03 11:04:54 +02:00
Ryan Hughes 1b5a57658e Ensure new help colors are input at the top
The colors don't work when appended to the bottom of the file like they
were before and instead fallback to a mismatched green
2026-05-02 17:24:55 -04:00
Ryan Hughes c8533301ea Use consistent spelling of color 2026-05-02 17:23:14 -04:00
Ryan Hughes e7df3d46f4 Prevent passing package_bin_path=$1 as $@ to the actual run 2026-05-02 16:22:54 -04:00
Ryan Hughes 3ca4bb33f2 Revert #5519 for now
Encountered issues during testing that prove Nvidia is still a little
too shakey to take this as is. See #5554 for more details.
2026-05-02 15:28:18 -04:00
David Heinemeier Hansson 4a7ade9c52 Merge branch 'dev' into rc 2026-05-02 16:43:10 +02:00
David Heinemeier Hansson a3988f0f62 Ensure we get all the new color definitions 2026-05-02 16:43:05 +02:00
David Heinemeier Hansson 6f444ed116 Correct the order 2026-05-02 16:27:33 +02:00
David Heinemeier Hansson acd5123d66 Ensure packages stay put as they were, just install elephant-all 2026-05-02 16:25:34 +02:00
David Heinemeier Hansson 63b9b9e512 Avoid losing walker/elephant customizations 2026-05-02 16:09:35 +02:00
David Heinemeier Hansson 5839101084 Use format safe for both first install and migration 2026-05-02 16:04:30 +02:00
justin 4b605ac028 feat: have agents validate config after making changes to the user's hyprland config files (#5550) 2026-05-02 15:51:28 +02:00
Jonny Heggheim 1069d4d063 Remove exclude-effect flag from tte in omarchy-screensaver (#5551)
dev_worm got removed from tte 0.14.2 https://github.com/ChrisBuilds/terminaltexteffects/releases/tag/release-0.14.2
2026-05-02 15:50:41 +02:00
David Heinemeier Hansson 06fd369318 Merge branch 'dev' into rc 2026-05-02 14:47:48 +02:00
David Heinemeier Hansson f0a43a43d8 Add new Ristretto background as default 2026-05-02 14:47:42 +02:00
David Heinemeier Hansson 92b8b4461a No quoting needed for bash 5 2026-05-02 13:48:04 +02:00
David Heinemeier Hansson ea7979a057 No quoting needed for bash 5 2026-05-02 13:47:55 +02:00
David Heinemeier Hansson 60966d4553 Merge branch 'dev' into rc 2026-05-02 13:34:38 +02:00
David Heinemeier Hansson 7b602c984c Persist scaling choices in the common case of a single monitor declared 2026-05-02 13:31:50 +02:00
David Heinemeier Hansson aee06e4320 Setup hibernation more efficiently to prevent needless limine rebuilds 2026-05-02 13:10:47 +02:00
David Heinemeier Hansson 7fdd7877fc Don't rebuild limine more than necessary 2026-05-02 12:50:59 +02:00
David Heinemeier Hansson 211acd6f9f Merge branch 'dev' into rc 2026-05-02 12:13:33 +02:00
David Heinemeier Hansson 52136441df Use existing helper 2026-05-02 12:13:27 +02:00
David Heinemeier Hansson f9af87e671 Don't run --now, just on next restart 2026-05-02 12:12:24 +02:00
David Heinemeier Hansson 1351cc6442 Merge branch 'dev' into rc 2026-05-02 11:47:54 +02:00
Mikko NymanandDavid Heinemeier Hansson 62f6980852 Fix power profile auto-switching on USB-C only machines (#5444)
* Fix power profile auto-switching on USB-C only machines

* Small nits

* Stick with single unit name

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-02 11:47:02 +02:00
Luke Hsiao d80c98f025 Make color0 distinct from background/foreground and fix helix theme (#5538)
* Make color0 distinct from background/foreground

The majority of themes have color0 available as a subtle offset from
the background. For flexoki-light, because color0 matched foreground, it
wasn't subtle, and would cause invisible text when used as a background
color (e.g., in text editor rulers). For vantablack, ethereal,
hackerman, and white, color0 matched background and would cause
invisible text when a theme wanted text to be de-emphasized.

This patch makes it consistently a subtle offset from background.

* Make helix selection theme colors more subtle

Helix is more usable if the selection background is subtle. This is
primarily because of [[1]], which makes a bright selection like we had
before often make the foreground text unreadable in the picker preview.

[1]: https://github.com/helix-editor/helix/issues/12405
2026-05-02 11:36:06 +02:00
David Heinemeier Hansson ddb8081eaa Integrate Apple display brightness controls with the normal hotkeys
And add Shift + Brightness to go min/max
2026-05-02 11:20:22 +02:00
David Heinemeier Hansson 3d52764310 Prevent lid closing from giving a false positive notification
Closes #5532, #5544
2026-05-02 11:02:44 +02:00
David Heinemeier Hansson 241145f85b Simplify the audio input muting 2026-05-02 10:24:31 +02:00
David Heinemeier Hansson bdd8de0501 Fix perms 2026-05-02 09:40:36 +02:00
Ryan Hughes 59b16e7f0f Merge pull request #5541 from dotsilva/dev
fix: "Bistream" typo to "Bitstream"
2026-05-01 23:30:20 -04:00
Bruno da Silva ff23d01fce FIX: Bistream typo to Bitstream in three places 2026-05-01 21:24:41 -03:00
David Heinemeier Hansson a4ab555516 Merge branch 'dev' into rc 2026-05-01 18:33:06 +02:00
David Heinemeier Hansson 3fabdf73e1 Fix impossible to see colors 2026-05-01 18:33:01 +02:00
David Heinemeier Hansson a0185bfc65 Quiet down so we don't get useless output spam 2026-05-01 18:27:29 +02:00
David Heinemeier Hansson 1eaddf97ea Quiet down so we don't get useless output spam 2026-05-01 18:27:20 +02:00
David Heinemeier Hansson 8537f41a9d Merge branch 'dev' into rc 2026-05-01 18:22:57 +02:00
David Heinemeier Hansson fef3def7e2 Just do it 2026-05-01 18:17:21 +02:00
Luke Hsiao 3ce6e98022 Theme Helix with the current Omarchy theme (#5530)
Helix had no theme integration. Installing it from the menu just ran
omarchy-pkg-add and left the editor on whatever default theme it shipped
with, regardless of the active Omarchy palette.

This adds `default/themed/helix.toml.tpl` so the template
renderer produces a helix.toml on every theme switch, and
`omarchy-install-helix` that wires Helix up:
  - install the package
  - symlink `~/.config/helix/themes/omarchy.toml` at the rendered theme
  - seed a config.toml selecting that theme when the user has none

The install menu routes Helix through this script. omarchy-restart-helix
joins omarchy-theme-set and signals Helix with SIGUSR1 so theme changes
are live [[1]], the same way as other packages. Arch-based distros
package the binary as 'helix' rather than upstream 'hx', so the new
scripts target 'helix' and the installer ends with a hint about aliasing
'hx'.

[1]: https://docs.helix-editor.com/configuration.html
Ref: https://github.com/basecamp/omarchy/discussions/4986
2026-05-01 18:17:07 +02:00
David Heinemeier Hansson 7f1071298c Completion should not include --help 2026-05-01 18:04:42 +02:00
David Heinemeier Hansson eeb409588a This was actually correct using an alias 2026-05-01 18:04:42 +02:00
David Heinemeier Hansson 199183ea8b Hide individual omarchy-* binaries from initial-word command completion
The unified `omarchy` dispatcher is the user-facing entry point
2026-05-01 18:04:42 +02:00
David Heinemeier Hansson 91d43d3316 Update to use the new single command cli 2026-05-01 18:04:42 +02:00
Ryan HughesandDavid Heinemeier Hansson d2a4cc0c4d Add omarchy CLI (#5477)
* Add omarchy CLI

* Remove outdated or internal

* Add bash completions for command

* Add omarchy command documentation

* Add missing docs

* Correct to what's now right

* Fix tests

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-01 17:40:22 +02:00
N1R4andDavid Heinemeier Hansson cdb9694360 Add a toggle for touchscreen on supported devices (#5510)
* Add minimum brightness cap to prevent OLED displays turning off

* Revert "Add minimum brightness cap to prevent OLED displays turning off"

This reverts commit fe855ecf9aeacfa1b9ffcff3d43adb10908435e5.

* Add touchscreen toggle support

* Use finger glyph

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-01 16:47:11 +02:00
boobachadandDavid Heinemeier Hansson c078d7ad05 Implement non-uniform brightness step adjustments (#5525)
* Implement non-uniform brightness step adjustments

Add logic for non-uniform step size adjustments based on current brightness level.

* Ensure percentages are moved in a consistent manner so we don't get rounding errors

* Ensure we don't go below 1%

To prevent screen from turning off entirely on some models

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-01 16:43:32 +02:00
ManuelandDavid Heinemeier Hansson 69ddcfdf2e Updated interface color configuration to RRGGBB format (#5471)
Migrated branding and help text color definitions to the new hexadecimal RRGGBB format in accordance with the latest Limine specifications. This update also introduces the interface_help_colour_bright parameter to ensure consistent visual aesthetics during the auto-boot sequence.

For the time being, a green palette has been implemented to maintain the traditional visual identity of Omarchy.

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-05-01 16:37:34 +02:00
Nicolas Dorier 61848c1910 Fix hibernation on new NVidia drivers (#5519)
New drivers don't seem to expose the necessary suspend interface to use `NVreg_PreserveVideoMemoryAllocations`.

I explained the issues I experienced on:
https://github.com/basecamp/omarchy/discussions/5500

While https://github.com/omacom-io/omarchy-pkgs/pull/84 fixed part of
the issue, the default value of `NVreg_PreserveVideoMemoryAllocations`
is `2`, which doesn't work.
2026-05-01 16:32:09 +02:00
Tushar Chauhan 21f712c325 Fix: read files from symlink directory in background selector menu (#5524)
* Fix: read files from symlink directory in background selector menu

* safely parse the path to avoid option injection

* Simply use -L flag with find to resolve symlinks
2026-05-01 16:31:02 +02:00
Nicolas Dorier bb0adde652 Restore jetbrains no_follow_mouse (#5239)
This reverts partially commit e5783569489818724d221f4400cb952f300d29ad.
2026-05-01 16:12:40 +02:00
David Heinemeier Hansson cf16a6a1a6 Merge branch 'dev' into rc 2026-05-01 14:15:08 +02:00
David Heinemeier Hansson 26741bf4e3 Correct spelling 2026-05-01 14:15:03 +02:00
David Heinemeier Hansson 3d9a875104 Merge branch 'dev' into rc 2026-05-01 11:27:23 +02:00
David Heinemeier Hansson bf9be2ea1b Fix casing 2026-05-01 11:27:15 +02:00
David Heinemeier Hansson 0b35365f6b Merge branch 'dev' into rc 2026-05-01 11:24:23 +02:00
David Heinemeier Hansson 18275dcaca Preinstall cliamp 2026-05-01 09:45:25 +02:00
David Heinemeier Hansson 3c780c19ce Ensure Brave Origin is supported for live themeing just like regular Brave 2026-05-01 09:26:19 +02:00
David Heinemeier Hansson 317eb44fb2 Add ghui is a lazy-installed TUI 2026-05-01 08:47:26 +02:00
David Heinemeier Hansson c06bbe40bb Allow for lazy-install of bun runtime 2026-05-01 08:46:12 +02:00
David Heinemeier Hansson 0555c34416 Allow for bun to be the executor and bring back the spinner 2026-05-01 08:31:38 +02:00
David Heinemeier Hansson 58c350c171 Merge branch 'dev' into rc 2026-04-30 22:23:48 +02:00
David Heinemeier Hansson ec69817e74 Fix spelling 2026-04-30 22:23:43 +02:00
David Heinemeier Hansson efd34a2eea Merge branch 'dev' into rc 2026-04-30 21:45:16 +02:00
cfulton 6fac70d1a1 Replace deprecated sainnhe.everforest VSCode extension with reesew.everforest-theme (#5498) 2026-04-30 21:19:52 +02:00
guidovicino c6e55ca4ba Move Obsidian launch flags to user config (#5488)
* Move Obsidian flags to user config

* Preserve Obsidian flags during migration

* Keep existing Obsidian launch flags

* Copy default Obsidian flags in migration
2026-04-30 21:17:53 +02:00
David Heinemeier Hansson d5aa4b7b4b Only XPS now needs the custom ptl kernel 2026-04-30 21:09:22 +02:00
Andrii Furmanets 0b1607786d Restart Walker after removing web apps (#5513) 2026-04-30 20:14:29 +02:00
Danila Danilin 710356e900 fix: to check encrypted drives amount (#5515) 2026-04-30 20:14:01 +02:00
David Heinemeier Hansson 6ab4196985 Quiet this during install 2026-04-30 20:09:01 +02:00
David Heinemeier Hansson 24b32da1a1 Ensure policy directories are available before we try writing to them 2026-04-30 20:07:43 +02:00
David Heinemeier Hansson a43df3963a Correct the order to prevent error message during install 2026-04-30 20:04:54 +02:00
David Heinemeier Hansson 91608ea3d3 Update interface_branding_color for limine 12 2026-04-30 19:38:04 +02:00
David Heinemeier Hansson 6fc7f90e43 Revert "Try debugging F13P issue by not turning on fred there"
This reverts commit f15094c9ef.
2026-04-30 19:30:06 +02:00
David Heinemeier Hansson f15094c9ef Try debugging F13P issue by not turning on fred there 2026-04-30 18:42:37 +02:00
David Heinemeier Hansson 25b114fa7a Also not used 2026-04-30 18:40:58 +02:00
David Heinemeier Hansson 6b8c2a0a04 Not installed any more either 2026-04-30 18:34:26 +02:00
David Heinemeier Hansson 0c972657c2 Merge branch 'dev' into rc 2026-04-30 18:32:37 +02:00
David Heinemeier Hansson 340c8ceeef We already have this listed on base 2026-04-30 18:30:10 +02:00
David Heinemeier Hansson c86ebe0cc3 Nothing is actually using dart 2026-04-30 18:18:13 +02:00
David Heinemeier Hansson fa5d054173 Merge branch 'dev' into rc 2026-04-30 18:07:02 +02:00
Alexandre 5683b1bdbd Fix internal monitor disable for non-eDP-1 systems (#5511)
Detect the internal monitor name dynamically at runtime instead of
hardcoding eDP-1 in a static toggle conf file. Uses the same pattern
as omarchy-hyprland-monitor-internal-mirror.

Fixes #5443
2026-04-30 17:51:29 +02:00
David Heinemeier Hansson cbefe54238 Refresh the configs 2026-04-30 17:31:32 +02:00
David Heinemeier Hansson 36026d78ed Reapply "Simplify the migration"
This reverts commit 0a57fae591.
2026-04-30 17:30:44 +02:00
David Heinemeier Hansson 6ef510cc54 Reapply "Ensure we don't lose walker and elephant when we swap around"
This reverts commit e5536f3c4d.
2026-04-30 17:30:42 +02:00
David Heinemeier Hansson eca926b5a4 Reapply "Replace coterie of individual Elephant packages with the single elephant-all package"
This reverts commit 8ff1208bdd.
2026-04-30 17:30:40 +02:00
David Heinemeier Hansson 175a22a048 Reapply "Just use the direct packages now that there's a single bundle for elephant-all"
This reverts commit 14f155eecb.
2026-04-30 17:30:38 +02:00
David Heinemeier Hansson 14f155eecb Revert "Just use the direct packages now that there's a single bundle for elephant-all"
This reverts commit b8a4d0b2ef.
2026-04-30 17:28:44 +02:00
David Heinemeier Hansson 8ff1208bdd Revert "Replace coterie of individual Elephant packages with the single elephant-all package"
This reverts commit c5784432e0.
2026-04-30 17:28:31 +02:00
David Heinemeier Hansson e5536f3c4d Revert "Ensure we don't lose walker and elephant when we swap around"
This reverts commit 6ce6f94647.
2026-04-30 17:28:20 +02:00
David Heinemeier Hansson 0a57fae591 Revert "Simplify the migration"
This reverts commit 37b60e5505.
2026-04-30 17:28:15 +02:00
David Heinemeier Hansson 91af40fd7c Enable fred=on on intel ptl machines 2026-04-30 17:26:14 +02:00
David Heinemeier Hansson 37b60e5505 Simplify the migration 2026-04-30 17:23:02 +02:00
David Heinemeier Hansson 6ce6f94647 Ensure we don't lose walker and elephant when we swap around 2026-04-30 17:20:45 +02:00
David Heinemeier Hansson c5784432e0 Replace coterie of individual Elephant packages with the single elephant-all package
Closes https://github.com/omacom-io/omarchy-pkgs/issues/12
2026-04-30 12:51:40 +02:00
David Heinemeier Hansson b8a4d0b2ef Just use the direct packages now that there's a single bundle for elephant-all 2026-04-30 11:47:18 +02:00
David Heinemeier Hansson 0aafd647e7 Better ordering 2026-04-30 10:25:45 +02:00
Michał Borejszo d8a0deb400 Color man pages with bat (#5491) 2026-04-30 10:25:28 +02:00
Ben Smith 768eabaaba Refactor ThinkPad mic mute to use omarchy-swayosd-client (#5493) 2026-04-30 10:24:05 +02:00
Timo HuboisandDavid Heinemeier Hansson 17ac8453b2 Fix refresh-config when target directory is missing (#5497)
* Don't use the names here since they break the all-themes setup in omarchy-nvim

* Fix refresh-config when target directory is missing

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-04-30 10:22:48 +02:00
David Heinemeier Hansson a677d6b7a3 We don't need to set PATH here any more
Closes #5472
2026-04-30 10:04:17 +02:00
David Heinemeier Hansson cd7c38c8aa Merge branch 'dev' into rc 2026-04-30 09:40:17 +02:00
David Heinemeier Hansson 7f9e1628d9 Ensure we cleanup stale snapshots 2026-04-30 09:40:08 +02:00
David Heinemeier Hansson 9eb0f0e805 Merge branch 'dev' into rc 2026-04-29 20:19:48 +02:00
35789cc102 Add laptop display mirror toggle + hotkey (#5462)
* Add check for active external monitors before disabling internal display

Co-authored-by: Copilot <copilot@github.com>

* Revert "Add check for active external monitors before disabling internal display"

This reverts commit e20b3da87e3df0f084c0bca232a61ba58fab4cdc.

* feat(hyprland): add toggle for internal monitor mirroring

- Implement `omarchy-hyprland-monitor-internal-mirror` to dynamically mirror the internal eDP display to external monitor.
- Update `omarchy-hyprland-monitor-internal` to prevent disabling the laptop screen if mirroring is currently active.
- Add "Mirror Display" toggle to omarchy hardware Menu

* Ensure there is also a laptop monitor available to mirror before we do it

* Fix toggle use

* Recover from mirror mode gracefully

* Add hotkey to mirroring

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-04-29 20:19:32 +02:00
David Heinemeier Hansson b77f900420 Merge branch 'dev' into rc 2026-04-29 15:49:48 +02:00
Paul RepinandDavid Heinemeier Hansson 2f257e167c alacritty.toml fallback for legacy themes without colors.toml (#5404)
* add generation of colors.toml via alacritty.toml

* Shorter command name

* Fix perms

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-04-29 15:46:18 +02:00
N1R4 66da3644fc feat: add dosfstools for /boot recovery (#5448)
Adds dosfstools to default installs and migrates existing systems so fsck.fat and mkfs.fat are available for Omarchy’s FAT32 /boot partition. This also helps with recovery from dirty or corrupt FAT filesystems after unclean shutdowns or power loss.
2026-04-29 15:32:08 +02:00
David Heinemeier Hansson ac40c0dbad Audio has input and output, easier to understand this way 2026-04-29 15:17:53 +02:00
David Heinemeier Hansson ed62fa81ec Move omarchy-cmd-share to omarchy-menu-share 2026-04-29 15:15:39 +02:00
David Heinemeier Hansson 4cc4144074 Move omarchy-cmd-first-run out of the cmd namespace too 2026-04-29 15:14:18 +02:00
David Heinemeier Hansson 39a289412d Move omarchy-cmd-screensaver to just omarchy-screensaver
We should just have omarchy-cmd-* for cmd checks
2026-04-29 15:11:37 +02:00
David Heinemeier Hansson 74a3797cf0 Move omarchy-lock-screen to omarchy-system-lock to fit with the others 2026-04-29 15:08:20 +02:00
David Heinemeier Hansson a7593ade7a Move audio commands under the same namespace 2026-04-29 15:00:59 +02:00
David Heinemeier Hansson c647edbc04 Allow passwordless sudo to be toggled from the toggle menu 2026-04-29 14:58:34 +02:00
David Heinemeier Hansson 12d8e001ab Simpler command structure 2026-04-29 14:54:03 +02:00
David Heinemeier Hansson e76d7544d3 Move omarchy-cmd-screen* to omarchy-capture-screen*
Will match upcoming cli grouping too
2026-04-29 14:50:15 +02:00
David Zucker 858916783f Allow Docker DNS from 192.168 networks (#5465)
* Allow Docker DNS from 192.168 networks

* Only add Docker DNS rule when UFW is active
2026-04-29 14:10:02 +02:00
bontalor 4cc6ea3d9f Fix: Use accel_profile=flat instead of force_no_accel for proper pointer behavior (#5479)
* Replace force_no_accel with accel_profile

* Correct comment
2026-04-29 13:55:52 +02:00
David Heinemeier Hansson 1770494fa2 Merge branch 'dev' into rc 2026-04-29 11:23:41 +02:00
David Heinemeier Hansson 114633f0e8 Hide needless noisy on startup and shutdown 2026-04-29 10:41:00 +02:00
David Heinemeier Hansson 6e04ff776b Add additional Tokyo Night backgrounds 2026-04-29 10:21:09 +02:00
David Heinemeier Hansson 008bdf0ff7 Revert "Retain the splash"
This reverts commit 951d6dfc49.
2026-04-29 08:54:40 +02:00
David Heinemeier Hansson 951d6dfc49 Retain the splash 2026-04-29 08:51:14 +02:00
David Heinemeier Hansson ca9cefe718 Speedup login sequence by about 3s 2026-04-29 08:09:10 +02:00
David Heinemeier Hansson 4e341fea3e Add omarchy bg option to all themes 2026-04-28 19:17:46 +02:00
David Heinemeier Hansson 93141bd578 Set SDDM login screen to the same theme as boot unlock 2026-04-28 18:08:34 +02:00
David Heinemeier Hansson eb3da87992 We are going for another minor 2026-04-28 17:56:22 +02:00
David Heinemeier Hansson 2716a3207c Provide unlock themeing (#5476) 2026-04-28 17:55:59 +02:00
David Heinemeier Hansson 8e30b2442c Add subtle highlight for better readability
Closes #5402
Co-authored-by: @pipetogrep
2026-04-28 15:58:35 +02:00
David Heinemeier Hansson 069f0c35c7 Add oma.png alternative plymouth logo 2026-04-28 11:51:47 +02:00
David Heinemeier Hansson 72ac23610f Try with split out i aliases 2026-04-28 11:51:47 +02:00
David Heinemeier Hansson 53d073da7c Merge branch 'dev' into rc 2026-04-28 09:37:27 +02:00
Timo HuboisandDavid Heinemeier Hansson 11e549a8ba Isolate npx wrapper runtime from project PATH (#5341)
* Don't use the names here since they break the all-themes setup in omarchy-nvim

* Pin Omarchy npx wrappers to matching node+npx binaries

* Fix npx wrappers when mise node is missing

* Pin npx wrappers to matching mise node runtime

* Isolate npx wrapper runtime from project PATH

* Clarify npx wrapper bin resolution

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-04-27 16:06:55 +02:00
David Heinemeier Hansson 7b79360ea1 Bump version 2026-04-27 15:56:03 +02:00
David Heinemeier Hansson 2fab15c0a7 Merge branch 'dev' into rc 2026-04-27 15:00:25 +02:00
Justin Biard 985a8010e6 chore: update hyprland wiki links (#5466) 2026-04-27 14:59:16 +02:00
Mikko NymanandClaude Opus 4.7 a6bb239919 omarchy-setup-fingerprint: pre-remove libfprint to avoid noninteractive abort (#5463)
libfprint-git provides+conflicts libfprint, so pacman -S --noconfirm
defaults the conflict prompt to N and the install aborts. Pre-remove
libfprint with -Rdd; libfprint-git's provides=libfprint re-satisfies
fprintd's dep immediately after install.

replaces=(libfprint) on the OPR PKGBUILD was considered and rejected:
it does not affect direct -S installs (only -Syu repo scans), and on a
published package it would silently swap libfprint for libfprint-git
on every -Syu including machines with no FocalTech sensor.

Verified on B9406CAA non-interactively; fprintd-list and fprintd-verify
continue to work against existing enrollments.

Follow-up to #5454.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:58:27 +02:00
Mikko NymanandClaude Opus 4.7 542ae346a0 Add ASUS ExpertBook B9406 fingerprint reader (FocalTech FT9349) support (#5454)
The B9406CAA ships with a Focal Tech FT9349 ESS sensor on USB 2808:a97a.
Mainline libfprint at 1.94.x lacks the open-source focaltech_moc driver
entirely; the driver is in libfprint master under LGPL but no upstream
release has shipped with PID 0xa97a in id_table[]. OPR carries
libfprint-git with both the driver and the PID patch.

Inline detection in omarchy-setup-fingerprint via /sys/bus/usb so we
don't introduce a one-off hardware-match script just for this case.
The check is cheap and runs only when the user opts into fingerprint
setup (lazy install pattern). Once upstream libfprint catches up, the
extra branch becomes harmless because libfprint-git provides=libfprint.

Tested end-to-end on B9406CAA: fprintd-enroll completes, fprintd-verify
matches enrolled fingers and rejects others, survives suspend/resume.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 11:39:31 +02:00
David Heinemeier Hansson 09411c4581 Make the "your kernel has been updated" check more resilient 2026-04-27 11:00:06 +02:00
Mathias Eek 88da163768 Fix jumping cursor / disable-while-typing on ASUS ROG Flow Z13 detachable keyboard (#5452)
* Fix disable-while-typing on ASUS ROG Flow Z13 detachable keyboard

* Replace lsusb with DMI detection for Z13 keyboard

* Use omarchy-hw-match instead of custom z13_present function
2026-04-27 08:49:48 +02:00
David Heinemeier Hansson 44d8d9b00f Use helper for this test 2026-04-27 08:42:44 +02:00
David Heinemeier Hansson 99f4cfbb1e Configure user directories with just modern relevancy 2026-04-27 08:38:54 +02:00
Mikko NymanandClaude Opus 4.7 b164549b51 Add ASUS ExpertBook B9406 display and touchpad fixes for Panther Lake (#5435)
* Add ASUS ExpertBook B9406 display and touchpad fixes for Panther Lake

* Drop B9406 migration

Per DHH's review: the migration's audience is essentially nobody.
Anyone running Omarchy on B9406 today is doing so via nomodeset
manually at every boot, not via a working unmodified install. Fresh
installs after this PR ships will pick up the fixes through the
hardware-match install hook; no migration needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:31:43 +02:00
David Heinemeier Hansson 5f184e5099 Make it clearer 2026-04-26 08:45:52 +02:00
David Heinemeier Hansson 89162a4be1 Add way to customize the plymouth theme more easily 2026-04-26 08:45:02 +02:00
David Heinemeier Hansson 4b7fe16029 Offer direct boot as a toggle option 2026-04-25 21:33:46 +02:00
David Heinemeier Hansson e01741afab Hide useless lstopo.desktop 2026-04-25 21:17:18 +02:00
Ryan Hughes 2b4d89a9ea Keep hyprpicker alive until capture is complete to prevent mid-transition screenshots 2026-04-24 19:23:07 -04:00
David Heinemeier Hansson 0041950d46 Only reencode if we detect garbage frames 2026-04-24 10:47:04 +02:00
David Heinemeier Hansson 1d4c859c71 Proper fix for using device dark mode in Chrome 2026-04-24 10:39:40 +02:00
David Heinemeier Hansson 0be4648773 Better guard against scrambled frames in screenrecording on slower systems 2026-04-24 10:39:31 +02:00
David Heinemeier Hansson 4127c749fb Proper fix for using device dark mode in Chrome 2026-04-24 10:16:31 +02:00
David Heinemeier Hansson 1cf28a9fda Show detailed battery notification on right-click on Waybar battery icon 2026-04-24 10:00:41 +02:00
David Heinemeier Hansson 40f41a5299 Fix hw check for hybrid gpu 2026-04-24 09:56:08 +02:00
David Heinemeier Hansson bc9777c0b8 Merge pull request #5388 from basecamp/rc
Omarchy 3.6.0
2026-04-23 15:12:57 +02:00
David Heinemeier Hansson add6a3c506 Merge branch 'dev' into rc 2026-04-23 14:39:27 +02:00
David Heinemeier Hansson 5f7a577283 Fix starting tailscale in installer
Closes #5400
2026-04-23 14:39:22 +02:00
David Heinemeier Hansson 7dc086bb47 Merge branch 'dev' into rc 2026-04-23 13:03:33 +02:00
David Heinemeier Hansson 274a72c77a Fix the offer to restart when the linux kernel changes 2026-04-23 13:03:26 +02:00
David Heinemeier Hansson 8346eea614 Merge branch 'dev' into rc 2026-04-23 12:48:31 +02:00
David Heinemeier Hansson 09f93b5a92 Make vantablack fully black for OLED glory 2026-04-23 12:47:16 +02:00
David Heinemeier Hansson 1782e76d20 Also refresh the monitor config 2026-04-23 12:26:12 +02:00
David Heinemeier Hansson f40a3fd1b7 Fix brightness icons 2026-04-23 12:22:45 +02:00
David Heinemeier Hansson 1abc92aadd Merge branch 'dev' into rc 2026-04-23 11:57:26 +02:00
David Heinemeier Hansson a74afeb4e5 Switch to an earlier recovery for the internal monitor on reboot 2026-04-23 11:54:11 +02:00
David Heinemeier Hansson 3c8d7bb5ce Revert "There is no rc-mirror, only stable-mirror and mirror"
This reverts commit 2682cc2853.
2026-04-23 11:14:46 +02:00
David Heinemeier Hansson 96afc68461 Move to a hardware check for external monitors instead to deal with recover timing 2026-04-23 11:14:14 +02:00
David Heinemeier Hansson 6953ea9af8 The mirror needs to be edge here too 2026-04-23 10:59:04 +02:00
David Heinemeier Hansson 2682cc2853 There is no rc-mirror, only stable-mirror and mirror 2026-04-23 10:58:00 +02:00
David Heinemeier Hansson cac03a5b64 Add i alias for ide with opencode and claude 2026-04-23 10:54:34 +02:00
David Heinemeier Hansson 2982ac4725 Ensure headless monitors aren't counted 2026-04-23 10:49:24 +02:00
David Heinemeier Hansson 3618eebdaa Make recover always a silent operation 2026-04-23 10:18:38 +02:00
David Heinemeier Hansson 24ad7c7aba Drop home snapshots, limit root shots to just 5, no btrfs quotas
cc @ryanrhughes
2026-04-23 09:55:59 +02:00
David Heinemeier Hansson 86a9d3e04c Allow the timelimit to be extending by passing in an explicit set of minutes 2026-04-23 09:55:59 +02:00
copilot-swe-agent[bot]anddhh de2824edde Quote OMARCHY_PATH in migration source command
Agent-Logs-Url: https://github.com/basecamp/omarchy/sessions/36fb96d5-70fd-4365-9550-e7ff5f8735d1

Co-authored-by: dhh <2741+dhh@users.noreply.github.com>
2026-04-23 09:16:12 +02:00
copilot-swe-agent[bot]anddhh 141edc262f Quote OMARCHY_PATH in migration source command
Agent-Logs-Url: https://github.com/basecamp/omarchy/sessions/36fb96d5-70fd-4365-9550-e7ff5f8735d1

Co-authored-by: dhh <2741+dhh@users.noreply.github.com>
2026-04-22 20:31:31 +00:00
David Heinemeier Hansson 12494bcd8f Merge branch 'dev' into rc 2026-04-22 22:18:40 +02:00
David Heinemeier Hansson bc53a0a7c5 Does not look like we needed that change after all 2026-04-22 22:18:34 +02:00
James Maina f9f08580df Lowercase theme name in omarchy-theme-install to match omarchy-theme-set (#5392) 2026-04-22 22:06:54 +02:00
Pierre Olivier Martel 826d9f4e2f Use the code instead so this mapping works with non US English keyboards too (#5391) 2026-04-22 22:04:27 +02:00
David Heinemeier Hansson 577cd1e8ab Merge branch 'dev' into rc 2026-04-22 21:28:28 +02:00
David Heinemeier Hansson 27779668f3 Just use the official package directly 2026-04-22 21:27:56 +02:00
David Heinemeier Hansson 2f5be43407 Merge branch 'dev' into rc 2026-04-22 20:11:29 +02:00
David Heinemeier Hansson ced028f87a Don't fail if headers haven't been installed 2026-04-22 19:51:16 +02:00
Marco 590f2e25e6 Add ThinkPad mic mute LED sync following XPS hardware handler pattern (#4938)
Omarchy 3.5.1 shipped hardware-specific mic mute handling for Dell XPS
(bin/omarchy-cmd-mic-mute-xps) which made the original PR #4938 approach
obsolete — that PR added a global omarchy-cmd-mic-mute that handled the
toggle inline, but upstream now owns that dispatcher.

This follows the same pattern to add ThinkPad support by extending
omarchy-hw-match to also check product_family (ThinkPad systems report
"ThinkPad T14s Gen 2a" there, not in product_name) and adding
omarchy-cmd-mic-mute-thinkpad which uses brightnessctl to sync the
platform::micmute LED, simpler than the ALSA approach needed for XPS.

Note: as more vendors are added this per-vendor dispatcher will get
unwieldy. A future refactor could move hardware LED sync into
install/config/hardware/ scripts that drop a hook or config file,
keeping omarchy-cmd-mic-mute thin and vendor-agnostic.
2026-04-22 19:49:15 +02:00
David Heinemeier Hansson 01d1984093 Fix perms 2026-04-22 19:44:54 +02:00
David Heinemeier Hansson a1d5e0c40c Use new correct catppuccin-nvim colorscheme 2026-04-22 17:16:00 +02:00
David Heinemeier Hansson 35650a633d Fix broken hide button on screensharing overlay 2026-04-22 17:15:32 +02:00
David Heinemeier Hansson 77c9bca045 Merge branch 'dev' into rc 2026-04-22 16:33:50 +02:00
David Heinemeier Hansson a31751ad95 Bump version 2026-04-22 16:33:44 +02:00
David Heinemeier Hansson 2537edb1e6 Merge branch 'dev' into rc 2026-04-22 16:31:09 +02:00
David Heinemeier Hansson 0e335a1791 Add 1.25 as a scaling option too and rewrite the matching 2026-04-22 16:20:26 +02:00
David Heinemeier Hansson 0d5b680c2a Make it possible to travel backwards on the resolution scaling 2026-04-22 16:15:30 +02:00
fc258fa8b2 Elephant menu fix when using customized default themes (#4953)
* Adds a check to see if the default theme has a preview if the user customized default theme lacks one.

* Update default/elephant/omarchy_themes.lua

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Reduce duplication

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-04-22 15:52:49 +02:00
Pierre Olivier Martel 4223cd9922 Use OMARCHY_PATH in omarchy-migrate (#4937) 2026-04-22 15:40:30 +02:00
David Heinemeier Hansson 8953ee822a Only show hybrid gpu toggle if hardware is available 2026-04-22 15:05:26 +02:00
David Heinemeier Hansson 57b779e249 Extract hardware toggles into their own menu and hotkey 2026-04-22 15:00:09 +02:00
innerviewer b275ec50be (Bash) Add lazy initialization of try command. (#5179)
* (Bash) Add lazy initialization of `try` and `mise` commands.

* (Bash) Removed lazy-loading for `mise`
2026-04-22 14:02:53 +02:00
Sergio JrandClaude Opus 4.6 bde1537f6e Use wpctl instead of pactl to switch default audio sink (#5236)
pactl set-default-sink only updates PipeWire's default.audio.sink
metadata, but WirePlumber immediately overrides it back because
default.configured.audio.sink remains unchanged. wpctl set-default
updates both, making the switch actually persist.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 13:56:15 +02:00
Stefan Gründel 1c65d1db0f Add migration script to enable GPU in voxtype if Vulkan is available (#5203) 2026-04-22 13:53:52 +02:00
Nicolas Dorier d4f4592dd2 Make Typora's print dialog float (#5208) 2026-04-22 13:52:39 +02:00
pontino 4e7c51b3bc Update cx alias to use new --permission-mode flag (#5233)
The `--allow-dangerously-skip-permissions` flag has been replaced
by `--permission-mode bypassPermissions` in Claude Code.
2026-04-22 13:50:47 +02:00
Rob ZolkosandDavid Heinemeier Hansson 14a599ab9d Normalize screen recording audio levels (#5244)
* Normalize screen recording audio

Screen recordings capture audio at whatever the system level is set to,
which often results in very quiet audio. Add a post-processing step that
normalizes audio to -14 LUFS using ffmpeg loudnorm after the first-frame
trim. Video is copied without re-encoding. Silently skipped for
recordings with no audio track.

* Do both trimming and normalizing in a single step so we don't have to process the video twice

* Simplify conditional

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-04-22 13:50:05 +02:00
Waleed Hassan e757ae98ea Lumon color.toml updated for better visibility of selected text (#5269) 2026-04-22 13:40:10 +02:00
David Heinemeier Hansson 73641f721d Match order from elsewhere 2026-04-22 13:36:29 +02:00
David Heinemeier Hansson cd3e683fef Combine into one script with options 2026-04-22 13:35:20 +02:00
Pete Jackson 1ca9c7ea0d Skip browser policy refresh when browser is not running (#5321)
chromium/brave --refresh-platform-policy blocks forever when the
browser is not already running, hanging omarchy-update entirely.
Guard both calls with pgrep so the policy file is still written
(picked up on next launch) but the blocking refresh is skipped.

Closes #4772
2026-04-22 13:26:26 +02:00
Pete Jackson 0a4490a1f1 Restore keyboard layout after hyprland config refresh (#5319)
omarchy-refresh-hyprland overwrites input.conf with the default
template, which has kb_layout and kb_variant commented out. Non-US
users lose their keyboard layout with no obvious recovery path.

Re-run detect-keyboard-layout.sh after the refresh so the system
layout from /etc/vconsole.conf is re-applied, matching what the
initial install does.

Closes #2507
2026-04-22 13:24:59 +02:00
David Heinemeier Hansson edf7067797 Extract omarchy-swayosd-client
So we don't have to manually compose it every time
2026-04-22 13:04:30 +02:00
David Heinemeier Hansson ed9ecccb21 Better notification message 2026-04-22 13:04:01 +02:00
David Heinemeier Hansson 843ee8ff49 Extract omarchy-hw-touchpad and simplify conditionals 2026-04-22 12:59:57 +02:00
DrunkLeen d00b3ea24b feat: Add Touchpad toggle/on/off support (#5353)
* feat: add touchpad toggle keybinding and script

Binds XF86TouchpadToggle/On/Off to a new leenium-toggle-touchpad script
that enables/disables the touchpad via hyprctl and shows OSD feedback.

* fix: make XF86TouchpadOn/Off enforce state instead of toggling

Add on/off/toggle argument support to omarchy-toggle-touchpad.
XF86TouchpadOn now always enables and XF86TouchpadOff always disables,
so firmware that emits separate On/Off keysyms behaves correctly regardless
of current state. XF86TouchpadToggle retains the invert behavior.

* feat: add touchpad toggle to menu and persist state as hyprland config

- Show touchpad option in toggle menu only when a touchpad/trackpad device is detected
- Replace /tmp state file with a persistent hyprland device config at
~/.local/state/omarchy/toggles/hypr/touchpad-disabled.conf

* fix: avoid unsafe && || toggle logic for Touchpad state

Replace `[[ -f $STATE_CONF ]] && enable || disable` with an explicit if/else.
The previous logic was not a true ternary: if `enable` failed (e.g. OSD command error),
`disable` would run unintentionally, causing unreliable toggle behavior.
2026-04-22 12:59:43 +02:00
Pete JacksonandDavid Heinemeier Hansson 37189368fb Clean up orphaned voxtype status processes on exit (#5345)
* Clean up orphaned voxtype status processes on exit

Waybar's custom/voxtype module runs omarchy-voxtype-status, which spawns
a long-lived `voxtype status --follow` child. When Waybar reloads (config
change, display hotplug, Hyprland reload), it kills the wrapper but the
child survives as an orphan. Over time dozens accumulate.

`trap 'kill 0' EXIT` signals the entire process group when the wrapper
exits, ensuring the voxtype child is cleaned up.

* Explain purpose of trap

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-04-22 12:56:37 +02:00
Stefan GründelandDavid Heinemeier Hansson ac3c60228e Add keybindings to cycle through monitors (#5359)
* Add keybindings to cycle through monitors

* Unified language around focus

* Group focus keybindings

* More natural sounding

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-04-22 12:38:45 +02:00
David Heinemeier Hansson bff5e843e0 Revert "Add battery identifier to Waybar config" (#5386)
* Revert "Add battery identifier to Waybar config"

* Remove the migration too
2026-04-22 07:52:40 +02:00
David Heinemeier Hansson a3b7c41b9b Make iA writer theme fully back to work better on OLEDs 2026-04-21 22:28:02 +02:00
David Heinemeier Hansson ac08aaf834 Resume boosting didn't actually solve the real underlying problem
But a bios update to the XPS should.
2026-04-21 17:19:34 +02:00
David Heinemeier Hansson 5d2cdb5317 Remove xe cmdline args from limine conf too 2026-04-21 17:01:03 +02:00
David Heinemeier Hansson 58443c82c0 Remove default vrr on 2026-04-21 16:43:53 +02:00
David Heinemeier Hansson a70b05e8f3 New kernel patches mean we don't need to turn off the display features 2026-04-21 16:41:07 +02:00
David Heinemeier Hansson 9306f5ef29 Install video encoding processors for modern Intel CPU/GPU combos by default
Allows the use of Quick Sync Intel H264/265 exports in Kdenlive without
crashing!
2026-04-21 11:43:36 +02:00
Pete Jackson b317bd5358 Enable pause_media for voxtype by default (#5364)
Pauses MPRIS media players when recording starts so music
doesn't bleed into dictation. Adds the setting to the default
config for new installs and a migration for existing users.
2026-04-20 13:53:22 +02:00
Lewis Buckley a9baa8f45f Migrate existing waybar configs to pin battery to BAT0 (#5369)
* Migrate existing waybar configs to pin battery to BAT0

Follow-up to #4918: that PR updated the default template, but
existing users keep their copy at ~/.config/waybar/config.jsonc
and won't pick up the fix without running omarchy-refresh-waybar
(which wipes local customizations). Patch the existing file in
place and restart waybar.

* Make battery-pin migration tolerant of whitespace variants

Per Copilot review: grep/sed patterns now match "battery" : {,
"battery":{, and "bat" : "..." variants that hand-edited configs
may use. Also only restart waybar when a battery block actually
exists to edit.
2026-04-20 13:52:04 +02:00
David Heinemeier Hansson 7c9442f8d0 Merge pull request #4918 from danflood94/dev
Add battery identifier to Waybar config
2026-04-20 10:31:12 +02:00
David Heinemeier Hansson a71916794c Speed up the special workspace a little too 2026-04-19 18:26:16 +02:00
David Heinemeier Hansson c74c0ccf97 Speed up the window movements ever so slightly 2026-04-19 18:24:10 +02:00
David Heinemeier Hansson 0344348ff0 Better naming 2026-04-19 17:45:50 +02:00
David Heinemeier Hansson 7f9f0c25ed Reveal which flag wasn't working 2026-04-19 17:44:32 +02:00
David Heinemeier Hansson aa35454dda Fix ordering 2026-04-19 17:44:24 +02:00
David Heinemeier Hansson 2b323459df Closing/opening lid will toggle internal monitor on/off 2026-04-19 17:34:34 +02:00
David Heinemeier Hansson fa48d2398b Better description 2026-04-19 17:13:28 +02:00
David Heinemeier Hansson 96a08f91a5 Start the monitor watch so the feature is active right away 2026-04-19 17:12:53 +02:00
David Heinemeier Hansson d6aaf30e43 Ensure internal display is turned back on if the external one is disconnected 2026-04-19 17:11:48 +02:00
David Heinemeier Hansson bdead4a3d1 Don't need any notifications for this when the consequences are visible 2026-04-18 20:13:27 +02:00
David Heinemeier Hansson e852895e5c Use preexisting toggles setup 2026-04-17 16:03:06 +02:00
David Heinemeier Hansson adb8d01d57 Hint for disabling the ghost monitor on the 6K XDR 2026-04-17 15:47:54 +02:00
David Heinemeier Hansson db52ba346c Extract omarchy-toggle-enabled and use this with the waybar to persist 2026-04-17 15:45:44 +02:00
David Heinemeier Hansson 71bbcc8adc Use new shared omarchy-toggle 2026-04-17 15:37:36 +02:00
David Heinemeier Hansson 7db0ee9314 Use shared toggle feaeture 2026-04-17 15:35:36 +02:00
David Heinemeier Hansson 1b898b862c Make the window no-gaps flag permanent 2026-04-17 14:22:59 +02:00
David Heinemeier Hansson 79d3b8cec6 Move flags to persistent setup so hyprctl reload doesn't reset them 2026-04-17 14:16:47 +02:00
David Heinemeier Hansson 8912bcb81c Add mako restart to Update > Process 2026-04-17 09:37:58 +02:00
David Heinemeier Hansson 236a34b23c Merge pull request #5280 from basecamp/rc
Omarchy 3.5.1
2026-04-16 16:12:03 +02:00
David Heinemeier Hansson 2c4b4488c0 Merge branch 'dev' into rc 2026-04-16 16:03:49 +02:00
David Heinemeier Hansson 8f441acbef Use more resilient restore 2026-04-16 15:35:52 +02:00
David Heinemeier Hansson 8f7d64ba78 Merge branch 'dev' into rc 2026-04-16 15:34:24 +02:00
David Heinemeier Hansson a4c544b8f6 Bump version 2026-04-16 15:34:20 +02:00
David Heinemeier Hansson 9ece3318ec Merge branch 'dev' into rc 2026-04-16 15:24:11 +02:00
Spencer BullandDavid Heinemeier Hansson 90bf106a70 Disable PSR1 on XPS IPS and OLED. XPS IPS uses PSR2 (#5315)
* Disable PSR1 on XPS IPS and OLED. IPS uses PSR2

* Improve conditional flow

* Simplify the migration logic

* We don't actually need this

We are using += everywhere

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-04-16 12:10:52 +02:00
David Heinemeier Hansson d51a3f6b37 Merge branch 'dev' into rc 2026-04-15 18:16:27 +02:00
David Heinemeier Hansson a76757335a Ensure we set performance profile on boot when on AC 2026-04-15 18:14:07 +02:00
David Heinemeier Hansson cdb8628502 Merge branch 'dev' into rc 2026-04-15 16:21:03 +02:00
David Heinemeier Hansson ce29e45ccd Use /run/omarchy instead of just /tmp 2026-04-15 16:19:49 +02:00
David Heinemeier Hansson b919a5541a Merge branch 'dev' into rc 2026-04-15 16:17:19 +02:00
David Heinemeier Hansson 7bfd848c93 Only apply enable_psr=0 to xps oled machines 2026-04-15 16:17:14 +02:00
David Heinemeier Hansson 4ee44c36a6 Merge branch 'dev' into rc 2026-04-15 15:55:55 +02:00
David Heinemeier Hansson bf802df8bd Present copilot key->omarchy menu better 2026-04-15 15:55:47 +02:00
David Heinemeier Hansson 753885a659 Broaden the CPU boost after sleep fix to all Intel machines 2026-04-15 15:48:35 +02:00
David Heinemeier Hansson d71ddf022a Merge branch 'dev' into rc 2026-04-15 15:02:11 +02:00
David Heinemeier Hansson bebed70a7c Ensure cursor is on the new screen 2026-04-15 14:42:44 +02:00
David Heinemeier Hansson cd511f77b9 Merge branch 'dev' into rc 2026-04-15 14:36:44 +02:00
David Heinemeier Hansson 521c94bfca Add display toggle but protect against disabling the only active display 2026-04-15 14:35:32 +02:00
David Heinemeier Hansson e6b105d0b2 Merge branch 'dev' into rc 2026-04-15 13:52:05 +02:00
David Heinemeier Hansson f094c3acc0 Move powerprofile setting to userland script 2026-04-15 13:50:36 +02:00
David Heinemeier Hansson 4a2511ad5d Fix perms 2026-04-15 13:31:16 +02:00
David Heinemeier Hansson 2554fe8555 Merge branch 'dev' into rc 2026-04-15 12:34:23 +02:00
David Heinemeier Hansson 9f6df52d8d Restore previouse power profile after the boost 2026-04-15 12:34:17 +02:00
David Heinemeier Hansson feda8b7231 Merge branch 'dev' into rc 2026-04-15 12:11:18 +02:00
David Heinemeier Hansson 30ddfeda4a Just apply the GSK_RENDERER fix for walker 2026-04-15 12:10:26 +02:00
David Heinemeier Hansson c34adea2f0 No longer needed 2026-04-15 11:58:45 +02:00
David Heinemeier Hansson 46eb5568d9 Merge branch 'dev' into rc 2026-04-15 11:56:00 +02:00
David Heinemeier Hansson 3f28cd46d6 Use cairo renderer to workaround sluggish GTK4 renderer
Closes #5260
2026-04-15 11:55:18 +02:00
David Heinemeier Hansson 33c92f554b Merge branch 'dev' into rc 2026-04-15 11:49:29 +02:00
David Heinemeier Hansson 0017f2eab0 Make hibernation offset safer 2026-04-15 11:49:03 +02:00
David Heinemeier Hansson f7dc63ca72 Work around the return-from-sleep sluggishness on panther lake
This is due to slow ramp after s2idle
2026-04-15 02:26:37 -07:00
David Heinemeier Hansson 9389c1f57a Fix path 2026-04-15 10:39:21 +02:00
David Heinemeier Hansson 1f697ecc6d No longer need first-run to set these powerprofilectl rules 2026-04-15 10:36:45 +02:00
MarcoandDavid Heinemeier Hansson a49b8d255c Fix AC power udev rules failing silently at boot (#5275)
* Don't use the names here since they break the all-themes setup in omarchy-nvim

* Fix AC power udev rules to use systemd-run

* Migrate AC power udev rules to systemd-run

* Add --collect and stable unit names to systemd-run udev rules

* Add unit name, replace, and After ordering to power profile udev rules

* Remove invalid --replace flag from systemd-run power profile rules

* We don't need --replace becuase it's not needed

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-04-15 10:28:42 +02:00
Felipe Gonzalez 18ab342119 fix: suppress Chromium/Brave stderr noise during theme switch (#5285)
Redirect stderr to /dev/null alongside stdout when refreshing
browser platform policy, preventing shared_memory_switch errors
from cluttering the terminal.
2026-04-15 10:14:07 +02:00
Stefan Gründel a43915f91d update btop.conf for v1.4.6 (#5286) 2026-04-15 10:13:37 +02:00
Liam Mitchell 0204db0578 Fix intel video acceleration migration scripts (#5288) 2026-04-15 10:07:39 +02:00
João VictorandCopilot 092b16489d Add Logitech MX Keys key bindings (#5301)
* Add Logitech MX Keys key bindings

* Update config/hypr/bindings.conf

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update config/hypr/bindings.conf

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-15 10:03:15 +02:00
David Heinemeier Hansson a7893c8af4 Catch brave-origin too
Closes #5304
Co-authored-by: @Cleptomania
2026-04-15 10:00:49 +02:00
David Heinemeier Hansson 997011e985 Merge branch 'dev' into rc 2026-04-13 16:00:11 -07:00
David Heinemeier Hansson bbe0d34968 Enable correct xe 2026-04-13 16:00:06 -07:00
David Heinemeier Hansson 0e126b7916 Merge branch 'dev' into rc 2026-04-13 15:59:23 -07:00
David Heinemeier Hansson 3eee73c0de Ensure enable_psr=0 is set for all xps ptl systems 2026-04-13 15:59:16 -07:00
Ryan Hughes 1df2b16f2f Merge pull request #5293 from jondkinney/update-key-repeat-delay-to-lowest-possible
Update default repeat_delay to improve holding delete and vim motions
2026-04-13 12:54:50 -04:00
Jon Kinney 8a7c77291e Update default repeat_delay to improve holding delete and vim motions
In my testing, this is the lowest you can go without causing any issues
with double key presses.
2026-04-12 20:09:08 -05:00
David Heinemeier Hansson f81b3c842d Merge branch 'dev' into rc 2026-04-11 20:48:42 -04:00
David Heinemeier Hansson 1732551873 Switch from makima to a straight copilot to omarchy menu mapping 2026-04-11 20:41:42 -04:00
David Heinemeier Hansson 691b11901c Set device appearance as the default for chromium
So we will be switching correctly from light/dark mode depending on the
theme.
2026-04-11 20:09:51 -04:00
David Heinemeier Hansson 4fcc58de2a Expose omarchy-restart-trackpad
Until we have a permanent fix for the haptic trackpad in the XPS. Cc
@spencerbull
2026-04-11 18:08:31 -04:00
David Heinemeier Hansson ecbd912b09 Merge branch 'dev' into rc 2026-04-11 18:02:01 -04:00
David Heinemeier Hansson 429b99dc50 PSR1 is introducing lag/stutters
CC @spencerbull
2026-04-11 18:01:45 -04:00
Robert Stringer b2b2903e22 Skip starship init for dumb bash sessions (#5255) 2026-04-10 06:57:22 -04:00
David Heinemeier Hansson c9cc4be4f9 Need to grab this before the loop 2026-04-10 06:37:49 -04:00
Spencer BullandDavid Heinemeier Hansson 1746a86a7b Enable Dell XPS Mute Mic Key (#5252)
* If on XPS, enable XPS mute mic key with LED and fallback to original mic mute flow

* fix hardcode of alsa card 0 to resolved id

* Extract omarchy-hyprland-monitor-focused

* Extract dedicated xps mute script

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-04-10 06:35:41 -04:00
Pierre Olivier Martel 5ca74821d4 Fix Fcitx5 icon showing up in the waybar (again!) (#5263)
* Update Fcitx5 autostart handling

* Restart xcompose to apply the changes
2026-04-10 06:24:30 -04:00
David Heinemeier Hansson 111307fba3 Address the new haptic touchpad in the shipping xps 2026-04-10 06:10:38 -04:00
David Heinemeier Hansson 5fb06d2d77 Better glyph 2026-04-09 09:53:54 +02:00
David Heinemeier Hansson 8ad61cadcb Add way to toggle on/off internal display on laptops 2026-04-09 09:53:12 +02:00
David Heinemeier Hansson 6171d0ee8f Setup power profile defaults on first run since that's when the daemon is available 2026-04-08 14:35:52 +02:00
Timo Hubois 3bfed7da37 Merge pull request #5247 from timohubois/feat-mise-node-latest-npx-wrapper
Run Omarchy's npx wrappers through mise-managed Node
2026-04-07 12:21:39 +02:00
David Heinemeier Hansson cc3880b99e Enable lmpd if installed but not running 2026-04-07 09:15:08 +02:00
David Heinemeier Hansson 29a0663cc1 Ensure we enable the LPMD service after installing 2026-04-07 09:14:13 +02:00
David Heinemeier Hansson abace0cbd5 Prevent the need for double sudo 2026-04-06 09:36:35 +02:00
David Heinemeier Hansson 7bef44a4be Prevent spurious hyprland errors during git update 2026-04-06 09:05:02 +02:00
David Heinemeier Hansson eb3bd95fca We don't actually ever need this notification 2026-04-05 09:37:36 +02:00
David Heinemeier Hansson 76d09b29b7 Merge pull request #5192 from basecamp/rc
Omarchy 3.5.0
2026-04-03 19:16:29 +02:00
David Heinemeier Hansson f18dfdd101 Merge branch 'dev' into rc 2026-04-03 11:36:14 +02:00
David Heinemeier Hansson 84bee808e7 Conditional and a restart 2026-04-03 11:36:07 +02:00
David Heinemeier Hansson 316ecc85cb Drop the misleading network speed hover tips
Closes #4884
2026-04-03 11:31:01 +02:00
David Heinemeier Hansson 55b0323a94 Only install LPMD for supported Intel CPUs on battery 2026-04-03 11:17:25 +02:00
Márcio 05344eb660 Add LocalSend send action in Nautilus (#4946)
* Add LocalSend send action in Nautilus

* Reuse Nautilus install script in migration

* Detect LocalSend via PATH

* Verify Flatpak LocalSend is installed

* Check Flatpak LocalSend with flatpak info
2026-04-03 11:00:09 +02:00
David Heinemeier Hansson ba4b661e1e Remove unnecessary disown 2026-04-03 10:58:59 +02:00
David Heinemeier Hansson e4757abb80 Merge branch 'dev' into rc 2026-04-03 10:57:18 +02:00
David Heinemeier Hansson 6329062f24 Turn on VRR by default
Lowers idle load on a Dell XPS 14 w/ IPS from 5w to 1.1w. And it just
doesn't do anything on monitors that don't support VRR.
2026-04-03 10:57:13 +02:00
David Heinemeier Hansson fc1cb1a751 Merge branch 'dev' into rc 2026-04-02 18:06:32 +02:00
David Heinemeier Hansson a7fb8ba43a Don't use the names here since they break the all-themes setup in omarchy-nvim 2026-04-02 18:05:04 +02:00
David Heinemeier Hansson 50d3579d66 Don't use the names here since they break the all-themes setup in omarchy-nvim 2026-04-02 18:04:55 +02:00
David Heinemeier Hansson b72a831bf5 Merge branch 'dev' into rc 2026-04-02 17:39:10 +02:00
Marco fd7aa3dce1 Fix firmware updates failing with Limine bootloader (#5077)
Install fwupdx64.efi into /boot/EFI/arch/ before running fwupdmgr update
so UEFI capsule updates work on systems using Limine.
2026-04-02 17:38:54 +02:00
David Heinemeier Hansson e2b6b5ed5a Merge branch 'dev' into rc 2026-04-02 17:23:41 +02:00
David Heinemeier Hansson fcf8baea8e Rearrange 2026-04-02 16:41:20 +02:00
Rob Zolkos fde637ec21 Preserve package download progress during updates (#5188) 2026-04-02 16:37:51 +02:00
Kevin McConnell 585a909401 Add ONCE to Install->Service menu (#5186) 2026-04-02 16:24:02 +02:00
David Heinemeier Hansson e5bcec53d1 Merge branch 'dev' into rc 2026-04-02 16:07:23 +02:00
David Heinemeier Hansson b140a48ec7 Actually create the output dir!
Closes #5189
2026-04-02 16:07:17 +02:00
David Heinemeier Hansson 1b07638732 Merge branch 'dev' into rc 2026-04-02 16:06:29 +02:00
Bruno RibeiroandDavid Heinemeier Hansson 08b317901b Create pictures dir if non existent (#5181)
* Create pictures dir if non existent

* Simplify

Don't need to guard against mkdir failing

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-04-02 12:32:12 +02:00
Nicolas Dorier 9896c1e988 Remove jetbrains specific hyprland config (#5183)
IDE based on IntelliJ [2026.1](https://blog.jetbrains.com/platform/2026/02/wayland-by-default-in-2026-1-eap/) are now supporting wayland natively
properly.

I have personally updated Rider while removing the configuration file,
and didn't noticed any issue.
2026-04-02 11:14:35 +02:00
Federico SapuppoandDavid Heinemeier Hansson a5dc10960f Add migration for T2 fan curve config (#5176)
* Add migration for tuned fan curve on existing T2 installs

* Make sure it's clear to all what migration is being run (even if it doesn't apply to them)

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-04-02 11:06:57 +02:00
David Heinemeier Hansson 4a3355e767 Merge branch 'dev' into rc 2026-04-01 18:16:31 +02:00
Federico Sapuppo 031ba01c83 Add tuned fan curve for T2 MacBooks (#5132) 2026-04-01 18:08:30 +02:00
Federico Sapuppo 729c590236 Use percentage-based step for keyboard backlight brightness (#5135) 2026-04-01 18:04:38 +02:00
Federico Sapuppo e0fed4a9f7 Fix battery time showing hours instead of minutes (#5137)
When upower reported time in minutes, the value was incorrectly
passed through the hours conversion path, displaying e.g. "25h"
instead of "25m".
2026-04-01 18:00:58 +02:00
Federico Sapuppo 7c13bd5801 Fix Bluetooth on T2 Macs by loading hci_bcm4377 module (#5145) 2026-04-01 17:58:07 +02:00
David Heinemeier Hansson 54679f647a Reduce power saving feature turn-off to just PSR2 2026-04-01 17:25:10 +02:00
Stefan GründelandDavid Heinemeier Hansson 64ef804433 Enable GPU in voxtype if Vulkan is available (#5005)
* Enable GPU support in Voxtype installation if Vulkan is available

* Restart Voxtype and Waybar after enabling GPU support in setup

* fix: revert changing existing migration

* fix: add new migration

* Refactor Vulkan availability check in voxtype installation and migration scripts

* Extract vulkan availability check

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-03-27 21:18:19 +01:00
David Heinemeier Hansson 54bd72f10a Better naming 2026-03-27 21:05:06 +01:00
David Heinemeier Hansson f8a789b885 Extract bin for toggling window transparency 2026-03-27 21:03:59 +01:00
David Heinemeier Hansson f9b52a007e Remove any npx stubs too 2026-03-27 17:06:54 +01:00
David Heinemeier Hansson 54b0517c65 Fix dark/light mode switching 2026-03-27 16:59:41 +01:00
David Heinemeier Hansson 9dde3a9f0c Better datetime display matching battery 2026-03-27 14:32:38 +01:00
David Heinemeier Hansson ae5da6f805 Update all theme previews to consistent size and style 2026-03-27 13:14:14 +01:00
David Heinemeier Hansson 3db5a8e361 Use the same grey/nonexisting theme as vantablack 2026-03-27 12:55:34 +01:00
David Heinemeier Hansson 361cda9fa4 Use miasma theme 2026-03-26 17:23:36 +01:00
David Heinemeier Hansson a68f2ae045 Add Retro 82 theme (#5110)
* Add Retro 82 theme

* Start with the groove

* Rename
2026-03-26 16:55:00 +01:00
David Heinemeier Hansson b25d47288a Add another Lumon background 2026-03-26 16:51:13 +01:00
David Heinemeier Hansson ece103319c Add vscode theme for Lumon 2026-03-26 16:35:45 +01:00
David Heinemeier Hansson fdfca055cc Use optimized theme 2026-03-26 16:32:04 +01:00
David Heinemeier Hansson 92e3357f05 Add a trackpad restarter for new hw 2026-03-26 14:30:45 +01:00
David Heiemeier Hansson 15538c4f5a New solution to haptic touchpad 2026-03-26 11:08:44 +01:00
David Heiemeier Hansson e7f8826dea Ignore that there are dependencies on the headers, since we're adding new ones 2026-03-26 10:53:22 +01:00
David Heiemeier Hansson 80e019ff7c Fix check for the LG panel 2026-03-26 10:52:58 +01:00
David Heinemeier Hansson 9642599d15 Fix permissions 2026-03-26 09:46:59 +01:00
David Heinemeier Hansson 35b2330c2d Make the ptl kernel exclusive when it's needed 2026-03-26 09:27:44 +01:00
David Heinemeier Hansson 93fc7fb053 Group related configs together 2026-03-26 09:26:10 +01:00
David Heinemeier Hansson 2b63748c9b Correct references 2026-03-26 09:24:54 +01:00
David Heinemeier Hansson 435613ab32 Use new omarchy-hw-match 2026-03-26 09:24:50 +01:00
David Heinemeier Hansson af724a3843 Only apply this display fix to the dell XPS OLED panel 2026-03-26 09:24:43 +01:00
David Heinemeier Hansson 2a81068d8a Extract omarchy-hw-match 2026-03-26 09:23:59 +01:00
David Heinemeier Hansson 99bba33dfe Next release will be 3.5.0 2026-03-26 08:46:42 +01:00
David Heinemeier Hansson 6c5f417521 Move into intel bundle of fixes 2026-03-26 08:37:31 +01:00
Spencer BullandDavid Heinemeier Hansson f55c7e9ab5 Add hardware fix for Intel WiFi 7 EHT on Panther Lake (#5106)
Detect Intel BE200/BE211 WiFi cards and disable WiFi 7 (EHT) via
modprobe config. The iwlwifi driver has broken EHT RX adaptation
causing APs to drop to MCS 0, making WiFi unusable.
Falling back to WiFi 6 (HE) restores full performance.

Temporary fix: remove when Intel fixes the firmware/driver.

Signed-off-by: Spencer Bull <Spencer.Bull@Dell.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-03-26 08:36:41 +01:00
David Heinemeier Hansson 5d4e1032ca Fix runner reference 2026-03-26 08:33:51 +01:00
John AllenandJohn Allen 8c6fcbf2f0 Rename local path variable in ga() to avoid clobbering zsh PATH (#5115)
Co-authored-by: John Allen <git@porcnick.com>
2026-03-26 07:47:49 +01:00
Nicolas Dorier 22e84776a8 Fix missing limine-mkinitcpio in migration script (#5117)
Follow up to #5114, I forgot to regenerate the `/boot/limine.conf` file during the migration.
2026-03-26 07:47:15 +01:00
Ty Smith 176194a76f Add VSCode insiders theme support (#5119)
VSCode Insiders uses a different directory than VScode. This commit adds compatibility.
2026-03-26 07:46:02 +01:00
Milan Petrović 49c7b45572 Fix: typo in migration 1774366575 (#5116)
* Fix typo in migration 1774366575: lmp.sh -> lpmd.sh

* Fix additional lmp.sh -> lpmd.sh reference in all.sh
2026-03-25 20:29:41 +01:00
David Heinemeier Hansson 6c1d9cc92e Fix file path 2026-03-25 20:27:18 +01:00
David Heinemeier Hansson e921ea3cee Additional Gruvbox backgrounds by @OldJobobo 2026-03-25 16:26:25 +01:00
David Heinemeier Hansson 7eabd26d3e Add pi as a install-on-first-use agent
Closes #5091
2026-03-25 14:43:38 +01:00
Nicolas Dorier f26df5adf5 Fix: Limine drop-in config are overridden by default limine conf (#5114)
Limine drop-in config are overridden by `/etc/default/limine`
2026-03-25 14:41:19 +01:00
David Heinemeier Hansson a91fde6a3b Add 6K too 2026-03-25 14:25:01 +01:00
David Heinemeier Hansson 5726f22471 85 is probably the sweetspot 2026-03-25 14:22:31 +01:00
David Heinemeier Hansson e288092084 Adjust sizes and quality 2026-03-25 14:20:25 +01:00
David Heinemeier Hansson fdc370ec4c Ensure auto-boot still happens when we have two kernels 2026-03-25 13:52:52 +01:00
David Heinemeier Hansson 906ad82627 Bring back claude code as a regular package install
Not sure why, but Claude Code now complains if it's installed via npx!
2026-03-25 13:37:37 +01:00
David Heinemeier Hansson 280ed7c3c8 Switch to the new custom kernel that also has wifi fixes 2026-03-25 13:31:04 +01:00
David Heinemeier Hansson f54623ba56 Correct package name 2026-03-25 13:05:04 +01:00
David Heinemeier Hansson 43de5df392 Framework too 2026-03-24 20:43:56 +01:00
David Heinemeier Hansson a6ea7f50e4 Give asus it's own subdirectory too 2026-03-24 20:42:04 +01:00
David Heinemeier Hansson e8623fbbf6 Move apple fixes into their own subdirectory too 2026-03-24 20:40:43 +01:00
David Heinemeier Hansson c4c55b5c33 Group all intel specifics in subdirectory 2026-03-24 20:39:01 +01:00
David Heinemeier Hansson e2c8bb67cc Group intel hardware tweaks together 2026-03-24 20:35:16 +01:00
David Heinemeier Hansson 4d6221c890 Make custom ptl kernel the default on intel ptl systems
Then we don't need to turn off audio any longer
2026-03-24 20:31:58 +01:00
David Heinemeier Hansson fce6df52aa Ensure intel-lmp is installed on Intel machines for better core prioritization 2026-03-24 16:37:01 +01:00
Patrick Bochenek 58a16dfebc Set low urgency for immediate-feedback notifications (#5056)
Some users may choose to disable notification timeout for normal-level
notifications to ensure no notifications are missed when the user is
away from the screen. However, there are several Omarchy notifications
that only serve as an immediate feedback for a user action, such as
"Screensaver enabled" or "Editing config file foo.conf", for which
disabling the timeout is not desirable. This commit drops the urgency
level for such notification to low, allowing configuring a different
timeout value for them.

Notifications with an explicitly set timeout are excluded from this
commit.
2026-03-24 14:56:12 +01:00
David Heinemeier Hansson 886320acee Simplify the haptic trackpad fix 2026-03-19 23:09:41 -04:00
David Heinemeier Hansson e02186551a Use extracted colorscheme 2026-03-19 15:06:24 -04:00
David Heinemeier Hansson 4d46334d62 Fix the haptic trackpad restore on first boot 2026-03-19 13:48:44 -04:00
David Heinemeier Hansson 3fdd0e5d42 Ensure haptic feedback trackpad doesn't disappear after sleep/reboot 2026-03-19 08:28:54 -04:00
David Heinemeier Hansson e24a9f2249 Add paired-back Lumon Industries theme
Still needs tweaking for the neovim theme
2026-03-18 14:34:40 -04:00
David Heinemeier Hansson 8a543be9a8 Fix perms 2026-03-17 18:00:19 +01:00
David Heinemeier Hansson a0baa81fa9 No longer forcing this 2026-03-16 21:50:34 +01:00
David Heinemeier Hansson d200fdee3b Make it a human-sized frame, vertical for the webcam overlay 2026-03-16 11:34:57 +01:00
David Heinemeier Hansson 5561f534f1 No need to start this before the reboot 2026-03-15 21:18:48 +01:00
David Heinemeier Hansson 0a5d66ca5e Fix sudo for enabling thermald unit 2026-03-15 21:17:14 +01:00
David Heinemeier Hansson e39a45b1ed Fix the tuxedo backlighting 2026-03-15 21:02:57 +01:00
David Heinemeier Hansson e21b22decf Ensure .local/bin exists 2026-03-15 20:20:40 +01:00
David Heinemeier Hansson 5545cbf2ef Add migration for COPY mode indicator 2026-03-15 19:40:00 +01:00
Vitor Melo 24afb3e669 Add in tmux status COPY mode when activated (#5028) 2026-03-15 19:38:07 +01:00
David Heinemeier Hansson f7d739483f Note about restarting early 2026-03-15 19:24:11 +01:00
David Heinemeier Hansson 948bac9f6c Fix install of playwright 2026-03-15 19:19:03 +01:00
David Heinemeier Hansson e294394b8a Add npx wrapper installer for packages moving too fast for normal repos
They're lazy loaded too. So we don't need manual install from a menu.
2026-03-15 19:11:43 +01:00
David Heinemeier Hansson 506348ef4b Clean up file if no timer is present 2026-03-15 18:48:39 +01:00
David Heinemeier Hansson 3154290769 Install MIPI support for IPU7 Intel camera 2026-03-15 18:43:31 +01:00
David Heinemeier Hansson 0495ac2363 Try just to give it 15 mins at a time 2026-03-15 16:18:58 +01:00
David Heinemeier Hansson 2bd0e23a5f Fix the check proper 2026-03-15 16:10:59 +01:00
David Heinemeier Hansson d37d93caee Detect current sudoless correctly 2026-03-15 16:05:03 +01:00
David Heinemeier Hansson 4b75fbbfd5 Add danger mode for sudoless agent work on system administration 2026-03-15 16:01:49 +01:00
David Heinemeier Hansson ec25cde033 Bring all sudo commands together 2026-03-15 16:00:54 +01:00
David Heinemeier Hansson fa17b5b69f Use sudo keepalive to ensure that we don't get a sudo timeout on a long-running install 2026-03-15 15:19:29 +01:00
David Heinemeier Hansson dc820652c1 Use the proper system reboot command
Closes #5025
2026-03-15 14:45:15 +01:00
David Heinemeier Hansson 2d35b98bbb Thermald helps manage thermal load on Intel laptops with Sandy Bridge or newer (#5022)
Thanks to the team at Intel for pointing this out 👌
2026-03-15 12:13:26 +01:00
David Heinemeier Hansson f372b03411 Merge pull request #5009 from Alex23rodriguez/patch-1
Update omarchy-reinstall-git to make shallow clone
2026-03-14 17:24:07 +01:00
Alex Rod 609345f22f Update omarchy-reinstall-git to make shallow clone
The whole git history of the omarchy repository is not required to, for example, run omarchy-reinstall.
If needed, additional history can be fetched manually
2026-03-14 16:30:23 +01:00
David Heinemeier Hansson dc2380026e Match bash envs for ssh access 2026-03-12 16:48:59 +01:00
David Heinemeier Hansson 26f2106b29 Add keyboard backlighting drivers for tuxedo/slimbook machines 2026-03-11 21:55:14 +01:00
David Heinemeier Hansson 3ce7ef7570 Fix lack of dropins on post-install setup 2026-03-11 21:50:50 +01:00
David Heinemeier Hansson 4c5e6569c4 Need sudo to get offset 2026-03-11 21:17:19 +01:00
David Heinemeier Hansson daf738725e Fix Dell XPS audio until we have an upstream fix 2026-03-11 14:20:49 +01:00
David Heinemeier Hansson 12bdf01e5e Fullscreen and idle inhibit on moonlight 2026-03-11 13:59:32 +01:00
David Heinemeier Hansson 1fd26fddc0 Fix that mise settings wouldn't be applied before installing Ruby
So we'd be manually compiling.
2026-03-11 08:38:44 +01:00
David Heinemeier Hansson 2acc2ebfea Fix time to full/empty when there are only minutes left 2026-03-10 22:00:56 +01:00
David Heinemeier Hansson 86bac09343 Merge pull request #4954 from justindotdevv/feature/tmux-source-config-message
feat: show message when config is sourced
2026-03-10 13:19:16 +01:00
David Heinemeier Hansson 26a0d446a0 Update tmux.conf 2026-03-10 13:19:02 +01:00
David Heinemeier Hansson d508d84ea2 Merge pull request #4940 from alansikora/unmount-fuse-before-sleep
Unmount FUSE filesystems before suspend/hibernate
2026-03-10 13:12:26 +01:00
David Heinemeier Hansson 5f6b10368a Make makima remapping an opt-in feature under Setup > Key Remapping 2026-03-10 12:29:15 +01:00
David Heinemeier Hansson 93e243eba5 Add sff for using ff together with scp for easily sending files to remote servers 2026-03-10 11:58:41 +01:00
David Heinemeier Hansson c880e8fbcb Allow kitty to preview images when using ff 2026-03-10 10:50:10 +01:00
justindotdevv 016efa3b30 feat: show message when config is sourced 2026-03-10 05:27:48 +02:00
Alan Sikora dd86a893cb Move FUSE unmount hook to its own installer
The hook prevents gvfsd-fuse from blocking suspend, not just
hibernation. Install it during the config phase so every system
gets it, not only those that run hibernation setup.
2026-03-09 16:36:14 -03:00
David Heinemeier Hansson a3839551e3 Restrict FUSE unmount to gvfsd-fuse
Unmounting all FUSE filesystems breaks xdg-document-portal and AppImages
that rely on FUSE to stay mounted. It also doesn't restart them.
2026-03-09 15:28:30 +01:00
David Heinemeier Hansson e5b69c52f8 Merge pull request #4945 from Lo0rd3/battery-low-sound-hook
Add opt-in low battery sound hook
2026-03-09 08:16:58 +01:00
Alan Sikora cce8d878ec Decode octal-escaped mountpoints and disown backgrounded restart
/proc/mounts uses octal escaping for special characters (e.g. \040 for
spaces). Decode with printf before passing to fusermount so paths with
spaces are handled correctly.

Add disown after backgrounding the gvfs restart so it survives
systemd-sleep's cgroup cleanup when the hook script exits.
2026-03-08 23:42:01 -03:00
Alan SikoraandClaude Opus 4.6 e490ca7d78 Background gvfs restart to avoid blocking user.slice thaw
The post-sleep hook runs before systemd thaws user.slice, so a
synchronous systemctl --user restart hangs for ~90 seconds waiting
for the frozen user manager to respond — blocking the entire resume.

Background the restart with a short delay so it executes after
user.slice is thawed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:14:40 -03:00
lo0orde 49c3ea2703 Add opt-in low battery sound hook 2026-03-09 02:00:57 +00:00
Alan Sikora a89c4f849d Use install instead of cp -p to ensure root ownership of sleep hook
cp -p preserves the original user ownership from ~/.local/share/omarchy,
which would allow an unprivileged user to modify a script that runs as
root during suspend/hibernate.
2026-03-08 22:30:56 -03:00
Alan Sikora 24ca8e2bff Restart gvfs after wake to restore FUSE mount
gvfsd-fuse is spawned by gvfsd during init and not respawned if it
dies. After lazy-unmounting in pre-sleep, restart gvfs-daemon.service
in post-sleep so the FUSE mount at /run/user/*/gvfs is restored.
2026-03-08 20:58:56 -03:00
Alan Sikora 505c15e624 Fix field parsing in unmount-fuse hook
The read command had an extra _ placeholder that was swallowing the
fstype field, causing the fuse.* pattern to never match.
2026-03-08 20:46:11 -03:00
Alan Sikora f92712541d Unmount FUSE filesystems before suspend/hibernate to prevent silent failures 2026-03-08 20:39:33 -03:00
David Heinemeier Hansson 3b8ab81781 Merge branch 'dev' into rc 2026-03-08 18:29:04 +01:00
David Heinemeier Hansson b4cb5e287b Need intel-media-driver available as an option during install 2026-03-08 18:28:55 +01:00
David Heinemeier Hansson 524b4415fb Need intel-media-driver available as an option during install 2026-03-08 18:28:39 +01:00
David Heinemeier Hansson e02b88c9d4 Merge branch 'master' into dev 2026-03-08 17:54:08 +01:00
David Heinemeier Hansson da0bb37a19 Merge branch 'dev' into rc 2026-03-08 17:54:04 +01:00
David Heinemeier Hansson be43e1193f Add Shift+Return keyboard binding for multi-line as used by Claude Code to Alacritty 2026-03-08 17:52:20 +01:00
David Heinemeier Hansson d757dc3a89 Merge pull request #4924 from basecamp/rc
Omarchy 3.4.2
2026-03-08 17:04:51 +01:00
David Heinemeier Hansson bb26e9113c Merge branch 'dev' into rc 2026-03-08 17:04:28 +01:00
David Heinemeier Hansson ef93f6356c Move to git update to ensure it's not lost 2026-03-08 17:04:12 +01:00
David Heinemeier Hansson e0bd449c71 Presentation 2026-03-08 17:03:06 +01:00
David Heinemeier Hansson 07f4e53701 Better output 2026-03-08 17:02:31 +01:00
David Heinemeier Hansson 169ef6ec9e Time has to be updated before git repo is pulled
Or SSL certificate will complain
2026-03-08 17:01:33 +01:00
David Heinemeier Hansson e1107ed165 Merge branch 'dev' into rc 2026-03-08 16:35:53 +01:00
David Heinemeier Hansson 5663fae835 Explain how to have changes take affect 2026-03-08 16:35:49 +01:00
David Heinemeier Hansson 5e6ea17f5d Merge branch 'dev' into rc 2026-03-08 16:34:46 +01:00
David Heinemeier Hansson bc56a8bbad Add omarchy-restart-makima
For when you change keymappings
2026-03-08 16:30:50 +01:00
David Heinemeier Hansson fdd13b52ab Merge branch 'dev' into rc 2026-03-08 15:56:29 +01:00
David Heinemeier Hansson dfc270f79a Install Intel GPU hardware acceleration drivers if missing 2026-03-08 15:56:24 +01:00
David Heinemeier Hansson 595f5f668f Merge branch 'dev' into rc 2026-03-08 15:53:47 +01:00
David Heinemeier Hansson 9746370b62 Default to full screen capture for hyprland preview picker 2026-03-08 15:53:42 +01:00
David Heinemeier Hansson 549cd59e48 Style 2026-03-08 15:52:35 +01:00
David Heinemeier Hansson c4f57beba2 Show better with a preview notification that'll open recording on invocation 2026-03-08 15:52:31 +01:00
David Heinemeier Hansson 8e7a4ce61d Merge branch 'dev' into rc 2026-03-08 15:42:12 +01:00
David Heinemeier Hansson b982e1c986 Ensure we also catch the uhd graphics on the Framework 12 2026-03-08 15:41:00 +01:00
David Heinemeier Hansson e4acb3e8a9 Merge branch 'dev' into rc 2026-03-08 15:16:29 +01:00
David Heinemeier Hansson 462575c880 Fix worktree removal 2026-03-08 13:25:46 +01:00
David Heinemeier Hansson 631bdec159 Prevent extra CR 2026-03-08 13:25:46 +01:00
David Heinemeier Hansson 681540490e Fix regexp 2026-03-08 13:25:46 +01:00
David Heinemeier Hansson 3306c428dd Spelling 2026-03-08 13:25:46 +01:00
David Heinemeier Hansson e58ac44a0d Don't leak local var into global space 2026-03-08 13:25:46 +01:00
Alex Budkar 5f2d12f9e7 Update jetbrains.conf to capture find windows with singular space title (#4909)
Some find windows like "find everywhere" has initial title that consists of a singular space. Update title matching expression to capture that case as well.
2026-03-08 13:12:54 +01:00
David Heinemeier Hansson c900478aba Merge branch 'dev' into rc 2026-03-08 11:12:21 +01:00
David Heinemeier Hansson 3aff0017a1 Only update plocate db on power to prevent hangs after sleep 2026-03-08 11:09:57 +01:00
David Heinemeier HanssonandCopilot 71be883338 Update default/bash/fns/worktrees
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-07 22:32:37 +01:00
David Heinemeier Hansson fd03d57110 Fix the makima installation 2026-03-07 22:01:21 +01:00
David Heinemeier Hansson c9592e15fd Version bump coming 2026-03-07 20:00:44 +01:00
5a7008c01b Install intel-media-driver for Arc 130T/140T (intel 255H) (#4922)
* Install intel-media-driver for 130T/140T (intel 255H)

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-07 19:53:35 +01:00
Jorge Manrubia 3da14f892f Fix nvim transparency to preserve foreground colors (#4844)
* Fix nvim transparency to preserve foreground colors

The old approach used `vim.api.nvim_set_hl(0, name, { bg = "none" })`
which replaces the entire highlight definition, wiping out foreground
and other attributes. This breaks snacks.nvim's GitHub integration
(gh_pr/gh_issue pickers) which reads the foreground color from Normal
and NormalFloat at module load time, gets nil, and crashes in
Snacks.util.blend().

The fix uses nvim_get_hl to fetch existing attributes first, removes
only bg, then sets the highlight back — preserving all other attributes.

* Drop fallback that reintroduces the highlight-wiping bug

If nvim_get_hl fails for a group, skip it rather than falling back to
the old { bg = "none" } approach that clears all attributes.
2026-03-07 19:46:17 +01:00
Miguel P 2d859e25b5 add animation to the scratchpad (#4900) 2026-03-07 19:28:39 +01:00
justin 04054c9569 fix: small localsend window (#4923)
* feat(hypr): add window size rule for LocalSend

* chore(hypr): remove Share class from localsend window rule
2026-03-07 19:21:05 +01:00
David Heinemeier Hansson 8f804bcba4 Add automatic-rename settings to tmux configuration 2026-03-07 19:16:34 +01:00
a261819a9b Use tmux native method of renaming windows (#4910)
* Use tmux native method of renaming windows

This uses tmux's built in method of renaming windows, and it even works
with pane switching rather than relying on cd to change the window name

* Update config/tmux/tmux.conf

I agree with the copilot suggestion

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Jadon Brutcher <jadon.brutcher@deltamobile.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-07 19:13:18 +01:00
Timo Hubois df0109369c Prevent LM Studio install from downgrading to legacy package (#4915)
Use lmstudio-bin in the AI install menu so installs and reinstalls stay on the current LM Studio package stream instead of reverting to legacy lmstudio.
2026-03-07 19:11:26 +01:00
danflood94 2f36270369 Add battery identifier to Waybar config
fix(waybar): pin battery to BAT0 to prevent crash on HID disconnect
2026-03-06 21:10:47 +00:00
David Heinemeier Hansson 2a2e9086e1 We weren't running this for some reason! 2026-03-06 21:58:04 +01:00
David Heinemeier Hansson fc79da14bb Fix panther lake adjustments 2026-03-06 20:51:58 +01:00
David Heinemeier Hansson 6ad5d225f8 Remap Copilot key to Omarchy Menu key 2026-03-05 18:25:48 -04:00
David Heinemeier Hansson b50ec21442 Toggle existing omarchy menu if already open 2026-03-05 14:16:50 -04:00
David Heinemeier Hansson d3285f6b00 Can't do shim activation here without overwriting the ./bin path addition
But not necessary since uwsm/env handles the underlying problem
2026-03-05 13:42:55 -04:00
David Heinemeier Hansson 725054a478 Make it clear when you've zoomed in on a pane 2026-03-05 11:57:36 -04:00
David Heinemeier Hansson 2efc43762a Make tmux window named after cwd 2026-03-05 11:50:32 -04:00
David Heinemeier Hansson 7e69c87f75 Ensure mise activates the shims
To make sure mise shims are right for the tmux layout commands
2026-03-05 11:44:55 -04:00
David Heinemeier Hansson 7fbbc66cac Merge branch 'master' into dev 2026-03-04 09:49:04 -04:00
David Heinemeier Hansson 20b2be248e Ensure wayfreeze-git is also removed if it's still there
Closes #4859
2026-03-04 09:46:30 -04:00
Mridul AgarwalandMrid22 9b8eee7372 add aur notice for nordvpn (#4888)
Co-authored-by: Mrid22 <mridulaga@outlook.com>
2026-03-03 20:28:38 -05:00
Arcangelo Massari 14765466fb Fix White theme name mismatch for VS Code/Codium (#4858)
The White theme's vscode.json specified "White Omarchy" as the theme
name, but the Bjarne.white-theme extension registers the theme as
"White". This mismatch caused omarchy-theme-set-vscode to set
workbench.colorTheme to a non-existent theme name.
2026-03-03 20:28:09 -05:00
David GuttmanandClaude Opus 4.6 e648902683 Fix try interactive selector appearing on bashrc re-source (#4877)
`try init` defines a shell function named `try`. On subsequent
`source ~/.bashrc`, `try init` calls the function (interactive
selector) instead of the binary. Adding `command` bypasses the
function and always calls the binary.

Fixes #4876

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:24:43 -05:00
David Heinemeier Hansson e1ee8e8561 Fix smushing 2026-03-03 15:12:20 -05:00
David Heinemeier Hansson 546290e3de Fix Panther Lake GPU display issues by turning off power saving features until they're fixed 2026-03-03 15:12:10 -05:00
David Heinemeier Hansson 5e516be544 Even better 2026-03-02 14:15:46 +01:00
David Heinemeier Hansson 758e9d442f Fuller battery stats 2026-03-02 14:02:42 +01:00
David Heinemeier Hansson d9e3993d0c Match the t alias 2026-03-02 13:21:57 +01:00
David Heinemeier Hansson 1c8d7a7487 Add worktrees functions 2026-03-01 17:39:12 +01:00
David Heinemeier Hansson 149bf44763 Add Alt+Shift+Arrow keybindings for swapping tmux windows 2026-03-01 16:33:04 +01:00
David Heinemeier Hansson 80224cb954 Merge pull request #4825 from Cammisuli/cammisuli/respect-refresh-rate
Apply already configured resolution and refresh rate when toggling scale
2026-03-01 09:18:23 +01:00
Jonathan Cammisuli 39f1ca77d8 Apply already configured resolution and refresh rate when toggling scale
Closes #4824
2026-02-28 17:22:53 -05:00
David Heinemeier Hansson a6bf23a592 Suggest scrolling layout swiping 2026-02-28 21:14:34 +01:00
David Heinemeier Hansson 8805586247 Hide omarchy themes menu from elephant provider list
Closes #4820
2026-02-28 21:05:15 +01:00
David Heinemeier Hansson da990e0c9f Include Shift + Return as default for browser 2026-02-28 20:45:08 +01:00
David Heinemeier Hansson a8f3251e9b Include Shift + Return as default for browser 2026-02-28 20:44:54 +01:00
2166 changed files with 130455 additions and 11740 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
# Merges to protected branches need sign-off from an org owner.
* @dhh @ryanrhughes
+4
View File
@@ -0,0 +1,4 @@
# Python bytecode (orchestrator)
__pycache__/
*.pyc
+11
View File
@@ -0,0 +1,11 @@
{
"workspace": {
"library": [
"/usr/share/hypr/stubs"
],
"checkThirdParty": false
},
"diagnostics": {
"globals": ["hl"]
}
}
+89 -23
View File
@@ -1,17 +1,48 @@
# Task Guides
Deeper instructions for specific kinds of work live in `agents/skills/`. Read the
matching guide before starting:
- [`agents/skills/command-metadata.md`](agents/skills/command-metadata.md) - adding or changing commands in `bin/`
- [`agents/skills/install-scripts.md`](agents/skills/install-scripts.md) - working under `install/` or on system/user setup commands
- [`agents/skills/shell-dev.md`](agents/skills/shell-dev.md) - editing the Quickshell desktop under `shell/`
- [`agents/skills/icon-font.md`](agents/skills/icon-font.md) - adding branded glyphs to `default/fonts/omarchy/omarchy.ttf`
- [`agents/skills/acceptance-tests.md`](agents/skills/acceptance-tests.md) - writing or running graphical acceptance tests under `test/acceptance.d/`
- [`agents/skills/visual-verification.md`](agents/skills/visual-verification.md) - verifying any change with a visual effect in the running UI
- [`agents/skills/migrations.md`](agents/skills/migrations.md) - creating or changing migrations under `migrations/`
# Documentation Layout
Three documentation trees, split by genre and audience:
- `agents/skills/` - task procedure ("do this when doing X"), for anyone working on the codebase
- `docs/` - reference on how the system is shaped (file layout, update pipeline, theming, shell architecture), for anyone working on the codebase; skills link here for depth
- `manual/` - end-user documentation for using Omarchy, published; never codebase internals
# Style
- In markdown documents (`plans/`, `docs/`, `manual/`), write full lines — no hard wrapping at 80 columns; break only at structural boundaries like headings and list items
- Two spaces for indentation, no tabs
- Use bash 5 conditionals: use `[[ ]]` for string/file tests and `(( ))` for numeric tests
- In `[[ ]]`, don't quote variables, but do quote string literals when comparing values (e.g., `[[ $branch == "dev" ]]`)
- Prefer `(( ))` over numeric operators inside `[[ ]]` (e.g., `(( count < 50 ))`, not `[[ $count -lt 50 ]]`)
- Prefer a full `if`/`else` conditional for simple two-path control flow; don't rely on `exec` or `exit` in one branch to make following statements unreachable
- For strings/paths with spaces, quote them instead of escaping spaces with `\ ` (e.g., `"$APP_DIR/Disk Usage.desktop"`, not `$APP_DIR/Disk\ Usage.desktop`)
- Shebangs must use `#!/bin/bash` consistently (never `#!/usr/bin/env bash`)
- Scripts under `install/` and `migrations/` may be sourced and intentionally omit shebangs
# Command Naming
All commands start with `omarchy-`. Prefixes indicate purpose:
All commands start with `omarchy-`. Prefixes indicate purpose.
The authoritative list of user-facing command groups lives in `bin/omarchy` in `GROUP_DESCRIPTIONS`. Keep `GROUP_DESCRIPTIONS` updated when adding a new command prefix users are meant to browse to.
A group whose commands are all `# omarchy:hidden=true` gets no entry. That table drives the top-level group listing on its own, so an entry there advertises the group even when every command in it is hidden. `apply-` and `provision-` are deliberately absent for that reason; both still route, and `omarchy <group>` still prints a group header without one.
Common prefixes include:
- `cmd-` - check if commands exist, misc utility commands
- `capture-` - screenshots, screen recordings, and other capture tools
- `pkg-` - package management helpers
- `hw-` - hardware detection (return exit codes for use in conditionals)
- `refresh-` - copy default config to user's `~/.config/`
@@ -23,45 +54,80 @@ All commands start with `omarchy-`. Prefixes indicate purpose:
- `theme-` - theme management
- `update-` - update components
Do not maintain a second exhaustive prefix list here. Consult
`GROUP_DESCRIPTIONS` when selecting or checking a command group so this
guidance does not drift from the router.
# Runtime Environment
- `$OMARCHY_PATH` is set at the top level by the uwsm session environment and is always available to Omarchy runtime code.
- Commands in `bin/` and Quickshell QML should rely on `$OMARCHY_PATH` / `Quickshell.env("OMARCHY_PATH")`; do not derive fallback paths from `HOME`, `Quickshell.shellDir`, or re-export/default `OMARCHY_PATH` manually.
# Privileged Commands
- Follow the "Privilege Escalation" section of `default/agents/skills/omarchy/SKILL.md`. It draws the
`sudo`/`pkexec` line by whether the caller has a terminal to enter a password in, and the repo's
own scripts follow it.
# Git
- Commits should be atomic: include only one coherent change or fix, and do not mix unrelated work.
- Commit messages should be succinct and describe the change being made.
# Helper Commands
Use these instead of raw shell commands:
- `omarchy-cmd-missing` / `omarchy-cmd-present` - check for commands
- `omarchy-pkg-missing` / `omarchy-pkg-present` - check for packages
- `omarchy-pkg-missing` / `omarchy-pkg-present` - check for packages (don't use these if you can just use `omarchy-pkg-add`/`omarchy-pkg-drop`)
- `omarchy-pkg-add` - install packages (handles both pacman and AUR)
- `omarchy-pkg-drop` - remove packages; use this instead of raw `pacman -R*`
- `omarchy-notification-send` - send desktop notifications; do not call `notify-send` directly
- `omarchy-hw-asus-rog` - detect ASUS ROG hardware (and similar `hw-*` commands)
Commands installed by Omarchy's default package set are runtime invariants. Invoke them directly; do not add defensive `omarchy-cmd-present` / `omarchy-cmd-missing` checks around them. Use command-presence helpers only for genuinely optional dependencies or code that can run before the default package set is installed.
Exceptions are allowed for migration and package-helper scripts where the helper may not be available yet, where the helper itself is being implemented, or where direct package-manager behavior is required.
# Menu
- The menu definition lives in `default/omarchy/omarchy-menu.jsonc`;
[`docs/menu.md`](docs/menu.md) covers the schema, guards, and providers.
- Do not add `aliases` to new menu entries. Aliases are reserved for
established alternate names users already type, kept for compatibility.
# Config Structure
- `config/` - default configs copied to `~/.config/`
- `default/themed/*.tpl` - templates with `{{ variable }}` placeholders for theme colors
- `themes/*/colors.toml` - theme color definitions (accent, background, foreground, color0-15)
- `themes/*/colors.toml` - theme color definitions (accent, background, foreground, red/green/yellow/blue/magenta/cyan and bright_* variants)
# Tests
Run focused automated tests for the area you changed;
[`docs/testing.md`](docs/testing.md) covers how the suites are shaped. Current
test entry points:
- `./test/all` - aggregate runner for CLI and shell tests; it intentionally does not run graphical acceptance tests
- `./test/cli` - CLI routing, command metadata, theme helpers, and safe dispatch coverage
- `./test/shell` - all Omarchy shell tests under `test/shell.d/`
New Omarchy shell tests should live in `test/shell.d/*-test.sh` so `./test/shell` picks them up automatically. Source `test/shell.d/base-test.sh` for shared root-path discovery, assertions, and Node test helpers.
The graphical acceptance suite runs in a disposable VM, not in the active
development session; see [`agents/skills/acceptance-tests.md`](agents/skills/acceptance-tests.md).
Visual changes must be verified in the running UI in addition to automated
tests; follow [`agents/skills/visual-verification.md`](agents/skills/visual-verification.md).
# Refresh Pattern
To copy a default config to user config with automatic backup:
```bash
omarchy-refresh-config hypr/hyprlock.conf
omarchy-refresh-config hypr/hyprland.lua
```
This copies `~/.local/share/omarchy/config/hypr/hyprlock.conf` to `~/.config/hypr/hyprlock.conf`.
# Migrations
To create a new migration, run `omarchy-dev-add-migration --no-edit`. This creates a migration file named after the unix timestamp of the last commit.
Migration format:
- No shebang line
- Start with an `echo` describing what the migration does
- Use `$OMARCHY_PATH` to reference the omarchy directory
Example:
```bash
echo "Disable fingerprint in hyprlock if fingerprint auth is not configured"
if omarchy-cmd-missing fprintd-list || ! fprintd-list "$USER" 2>/dev/null | grep -q "finger"; then
sed -i 's/fingerprint:enabled = .*/fingerprint:enabled = false/' ~/.config/hypr/hyprlock.conf
fi
```
This copies `$OMARCHY_PATH/config/hypr/hyprland.lua` to `~/.config/hypr/hyprland.lua`. The argument
is interpolated into both paths and only checked with `[[ -e ]]`, so pass a plain relative path: a
name containing `..` resolves and copies, landing outside `~/.config` rather than being rejected.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+70
View File
@@ -4,6 +4,76 @@ Omarchy is a beautiful, modern & opinionated Linux distribution by DHH.
Read more at [omarchy.org](https://omarchy.org).
## The Omarchy Manual
The manual lives in [`manual/`](manual/), which is its authoritative source. It's
mirrored to [learn.omacom.io](https://learn.omacom.io/2/the-omarchy-manual), where
its screenshots are also hosted.
- [Welcome to Omarchy!](manual/01-welcome-to-omarchy.md)
**The Basics**
- [Getting Started](manual/02-getting-started.md)
- [Coming From Mac or Windows](manual/03-coming-from-mac-or-windows.md)
- [Navigation](manual/04-navigation.md)
- [The top bar](manual/05-the-top-bar.md)
- [Themes](manual/06-themes.md)
- [Hotkeys](manual/07-hotkeys.md)
- [Unified Clipboard & History](manual/08-unified-clipboard-history.md)
- [Reminders](manual/09-reminders.md)
- [Notices](manual/10-notices.md)
- [Text Extraction & Dictation](manual/11-text-extraction-dictation.md)
- [Screenshots & Recording](manual/12-screenshots-recording.md)
- [Toggles, idle & screensaver](manual/13-toggles-idle-screensaver.md)
- [Omarchy CLI](manual/14-omarchy-cli.md)
**The Applications**
- [Terminal](manual/15-terminal.md)
- [Neovim](manual/16-neovim.md)
- [AI](manual/17-ai.md)
- [Development Tools](manual/18-development-tools.md)
- [Shell Tools](manual/19-shell-tools.md)
- [Shell Functions](manual/20-shell-functions.md)
- [TUIs](manual/21-tuis.md)
- [GUIs](manual/22-guis.md)
- [Browsers](manual/23-browsers.md)
- [Commercial apps/services](manual/24-commercial-apps-services.md)
- [Web Apps](manual/25-web-apps.md)
- [Gaming](manual/26-gaming.md)
- [Filling out PDFs](manual/27-filling-out-pdfs.md)
- [Windows VM](manual/28-windows-vm.md)
- [Other Packages](manual/29-other-packages.md)
**Configuration**
- [Updates](manual/30-updates.md)
- [Dotfiles](manual/31-dotfiles.md)
- [Shell plugins](manual/32-shell-plugins.md)
- [Monitors](manual/33-monitors.md)
- [Keyboard, Mouse, Trackpad](manual/34-keyboard-mouse-trackpad.md)
- [Networking](manual/35-networking.md)
- [System sleep](manual/36-system-sleep.md)
- [Hardware authentication](manual/37-hardware-authentication.md)
- [Fonts](manual/38-fonts.md)
- [Backgrounds](manual/39-backgrounds.md)
- [Prompt](manual/40-prompt.md)
- [Branding](manual/41-branding.md)
- [Common tweaks](manual/42-common-tweaks.md)
- [Making your own theme](manual/43-making-your-own-theme.md)
**The Rest**
- [Mac support](manual/44-mac-support.md)
- [Troubleshooting](manual/45-troubleshooting.md)
- [FAQ](manual/46-faq.md)
- [System snapshots](manual/47-system-snapshots.md)
- [Security](manual/48-security.md)
- [Omarchy on...](manual/49-omarchy-on.md)
- [Dual Boot Install](manual/50-dual-boot-install.md)
- [Unattended Installs](manual/51-unattended-installs.md)
## License
Omarchy is released under the [MIT License](https://opensource.org/licenses/MIT).
+46
View File
@@ -0,0 +1,46 @@
# Acceptance Tests
Read this before writing or running the graphical acceptance suite under
`test/acceptance.d/`.
The graphical acceptance suite lives in `test/acceptance` with test files under
`test/acceptance.d/*-test.sh`. It exercises a real installed Omarchy desktop,
including session health, shell surfaces, panels, keyboard navigation,
representative applications, and system setup. Source
`test/acceptance.d/base-test.sh` for the shared helpers.
Run acceptance tests in a disposable VM through the sibling `omarchy-iso`
repository, not in the active development session. The suite opens and closes
applications and temporarily changes desktop configuration.
For acceptance-test-only changes, reuse an installed base and sync the suite:
```bash
cd ../omarchy-iso
./bin/omarchy-iso-test release/<iso>.iso --reuse-base --sync-omarchy ../omarchy --no-preview
```
Use `--sync-all ../omarchy` instead of `--sync-omarchy ../omarchy` when the
acceptance run must exercise local `bin/`, `config/`, or `shell/` source too.
Changes to package manifests, installation, finalization, or shipped defaults
require a fresh ISO built from the local checkouts and a run without
`--reuse-base`:
```bash
cd ../omarchy-iso
./bin/omarchy-iso-make --no-boot-offer --local-source ../omarchy ../omarchy-pkgs
./bin/omarchy-iso-test release/<generated-iso>.iso --no-preview
```
Keep unrelated acceptance workflows in separate test files. The runner records
a failed file and continues with the remaining files, which preserves as much
diagnostic coverage as possible. Restore modified user state with traps, close
anything the test opens, and capture every visually distinct state (including
entered input where relevant) as `success-<step>.png`; failure helpers capture
`failure-<step>.png`. The ISO harness collects the screenshots and logs under
its timestamped `test-runs/` directory and opens the screenshots after the run
unless `--no-preview` is passed.
The ISO harness exercises compositor-level shortcuts with QMP virtual keyboard
input. In-guest `wtype` is suitable for typing into focused controls, but it
does not reliably prove that a global Hyprland keybinding works.
+31
View File
@@ -0,0 +1,31 @@
# Command Metadata
Read this before adding or changing commands in `bin/`.
Commands in `bin/` can declare CLI metadata in comments near the top of the
file. `bin/omarchy` scans the first 80 lines, and tests expect command metadata
to remain valid.
Supported metadata keys:
- `# omarchy:group=...` - override the command group inferred from the filename
- `# omarchy:name=...` - override the command name inferred from the filename
- `# omarchy:summary=...` - short help text
- `# omarchy:args=...` - usage arguments
- `# omarchy:examples=...` - examples separated with ` | `
- `# omarchy:alias=...` / `# omarchy:aliases=...` - alternate routes
- `# omarchy:hidden=true` - hide from default command listings
- `# omarchy:requires-sudo=true` - mark commands that require sudo
Only use `omarchy:examples` where there are args that need explaining.
Prefer explicit metadata for user-facing commands. Keep routes consistent with
the filename unless there is a deliberate alias or compatibility route.
Example:
```bash
# omarchy:summary=Take a screenshot
# omarchy:args=[smart|region|windows|fullscreen] [slurp|copy]
# omarchy:examples=omarchy screenshot | omarchy capture screenshot region
```
+79
View File
@@ -0,0 +1,79 @@
# Omarchy Icon Font
Read this before adding a branded glyph to `default/fonts/omarchy/omarchy.ttf`.
The Omarchy icon font is a small private-use font carrying the marks Nerd
Fonts does not have: the Omarchy logo and the agent and app brand marks. The
menu draws one by naming the font on an entry:
```jsonc
"setup.default.agent.grok": {"icon":"","iconFont":"omarchy","label":"Grok", ...}
```
Without `iconFont`, an entry's `icon` is drawn in the menu font, so reach for
this font only when a Nerd Font glyph would misrepresent the thing. A generic
robot for four different AI apps is the case that justifies a real mark; a
folder or a microphone is not.
`default/fonts/omarchy/README.md` lists every glyph with the URL its artwork
came from. Keep that list accurate — it is the only record of provenance.
## Adding a glyph
`omarchy dev font` does the work:
```bash
omarchy dev font list
omarchy dev font add ollama https://simpleicons.org/icons/ollama.svg
```
`add` fetches the SVG, scales it into the same 64..960 box the existing marks
use so it lands at their optical size, appends it at the next free private-use
codepoint, and adds a line to the font README. It prints the codepoint and the
glyph itself.
The source must be a **monochrome SVG with a single `<path>`**, because the
menu recolors the glyph with the active theme's foreground and selection
colors. Brand icon sets such as <https://simpleicons.org> publish exactly that
shape. App favicons usually do not: they are multi-color, carry a container
tile, or split across several paths. Prefer the official mark when it is
published as flat monochrome art, and fall back to an icon set's redraw when it
is not.
Two-tone marks are a trap. Every path becomes solid foreground, so a logo whose
meaning depends on lighter and darker halves turns into an unreadable blob.
Pick a source whose silhouette alone reads.
## After adding
The command prints these, and all of them matter:
- Point the menu entry at the new codepoint with `"iconFont":"omarchy"`.
- Bump the charset range asserted in `test/shell.d/menu-test.sh`; that test
pins the font's coverage and fails until it matches.
- Check the README line the command added, and give it a proper display name
with `--label` if the glyph name is not the brand's name.
The font is package-owned: `omarchy-settings` installs it to
`/usr/share/fonts/omarchy/omarchy.ttf`, so a new glyph reaches the desktop
through a settings release, not through `omarchy update`. Between the merge and
that release, a pulled checkout renders the new entry with no icon.
## Verifying
Render the whole font and look at it, which catches inverted contours and
filled counters that a glyph list cannot show:
```bash
python3 -c "open('/tmp/row.txt','w').write(' '.join(chr(c) for c in range(0xE900, 0xE910)))"
magick -background white -fill black -font default/fonts/omarchy/omarchy.ttf \
-pointsize 110 label:@/tmp/row.txt /tmp/font-row.png
```
Then confirm it in the running menu per
[`visual-verification.md`](visual-verification.md). Fontconfig prefers the
packaged font over a copy in `~/.local/share/fonts` for the same family, so a
preview needs either the real file replaced or a `<rejectfont>` rule in
`~/.config/fontconfig/conf.d/` pointing fontconfig away from the packaged one.
Restart the shell afterwards — Qt reads the font database at startup, so
`omarchy menu refresh` alone will not pick up a changed font.
+19
View File
@@ -0,0 +1,19 @@
# Install Scripts
Read this before working under `install/` or on the system/user setup commands.
The ISO owns installation orchestration. This repo ships target-side setup
commands and reusable setup leaves:
- `bin/omarchy-apply-system` runs root-owned system setup during ISO finalization.
- `bin/omarchy-apply-hardware` runs idempotent hardware-specific setup and is called by `omarchy-apply-system`.
- `bin/omarchy-finalize-user` runs the per-user runtime finalization (skill symlinks, xdg-user-dirs, mime defaults, `install/user/all.sh`). Shipped user defaults are seeded by `/etc/skel` from `omarchy-settings`, not by this command. `bin/omarchy-reinstall-configs` is the explicit destructive resync of those defaults into an existing user's `$HOME`.
- leaf scripts under `install/` are sourced by `run_logged $OMARCHY_INSTALL/path/to/script.sh` and intentionally do not have shebangs.
- avoid `exit` in sourced setup scripts unless intentionally aborting setup.
- use `$OMARCHY_INSTALL` and `$OMARCHY_PATH` instead of hard-coded Omarchy paths.
- keep root-scoped hardware setup under `install/hardware/` and orchestrate it through `install/hardware/all.sh`.
- keep every per-user setup leaf under `install/user/` (including `install/user/hardware/` and `install/user/first-run/`) so it is clear what must run for each user.
- prefer helper commands for package and command checks where available.
Raw `command -v`, `pacman`, and `pacman-key` are acceptable in package-helper
contexts where direct package-manager behavior is the point of the script.
+170
View File
@@ -0,0 +1,170 @@
# Omarchy migrations
Read this before creating or changing migrations under `migrations/`.
Omarchy migrations are one-time repair scripts for existing installs. They are
used when a package update needs to change state that pacman cannot safely own by
itself.
## Migration model
Migrations live in:
```text
migrations/*.sh
```
They run as the current Omarchy user through `omarchy-migrate`, normally during
`omarchy update`. A migration may touch user/session state (`~/.config`,
`~/.local`, user systemd, browser/editor prefs, DBus/session state), and may also
perform machine-wide repairs when needed.
Completion state is per-user:
```text
~/.local/state/omarchy/migrations/<migration filename>
```
That means every user gets a chance to run every migration. Migrations run as the
user; privileged operations should invoke the appropriate helper or privilege
prompt themselves. Migrations must be idempotent: if one user already applied a
machine-wide repair, the same migration running for another user should detect
that and no-op.
## When migrations run
### During `omarchy update`
`omarchy update` is the normal update path. It runs package updates, then:
```bash
omarchy-migrate
omarchy-hook post-update
```
`omarchy-migrate` waits for any active pacman transaction to finish, then runs
all pending migrations for the current user in the visible update terminal.
### At login
Every graphical login starts `omarchy-migrate-notify.service` after
`graphical-session.target`. The notifier checks:
```bash
omarchy-migrate --pending
```
It stays silent while `omarchy update` holds its lock, since that update applies
the pending migrations itself.
If that user has pending migrations, it shows a notification that opens a
terminal for:
```bash
omarchy-migrate
```
The notifier never runs migrations silently in the background.
This is what covers users who did not run the update themselves: someone who
bypassed the pacman guard with `sudo env OMARCHY_ALLOW_DIRECT_PACMAN=1 pacman
-Syu`, and any second user on the machine, whose migration markers are per-user
and therefore still missing after another user updated.
Login is the only trigger on purpose. Watching the packaged migration directory
also fires during a normal `omarchy update`, which prompts for migrations that
`omarchy-migrate` is about to run in the visible update terminal.
### Manually
Users can safely run:
```bash
omarchy-migrate
```
at any time. Already-completed migrations are skipped.
## Inspecting pending migrations
Use:
```bash
omarchy-migrate --pending
```
Exit behavior:
- `0` — one or more migrations are pending
- non-zero — no migrations are pending
Output is one pending migration per line:
```text
1781158082.sh
```
## Creating a migration
Use the helper:
```bash
omarchy-dev-add-migration --no-edit
```
This creates:
```text
migrations/<unix timestamp>.sh
```
New migration format:
- File permissions must be `0644` (`-rw-r--r--`). Migration runners execute them
with `bash -euo pipefail`, not through executable bits.
- No shebang line.
- Start with an `echo` describing what the migration does.
- Use `$OMARCHY_PATH` to reference the Omarchy directory.
- Be idempotent. Check existing state before changing it.
- Migrations are strictly ordered and synchronous. A migration that cannot finish must exit non-zero, remain pending, and stop the queue; never mark later migrations complete against state an earlier migration has not established.
- Use helper commands such as `omarchy-cmd-present`, `omarchy-cmd-missing`,
`omarchy-pkg-add`, `omarchy-pkg-drop`, `omarchy-pkg-present`, and
`omarchy-pkg-missing` when appropriate.
- Never restart the Omarchy shell. `omarchy update` restarts it unconditionally
after migrations run, and the login-time shell already runs current code and
hot-reloads `shell.json` edits.
- Raw `pacman`, `command -v`, and direct config edits are acceptable when
needed for one-off repair work.
Example:
```bash
echo "Relink Neovim theme to Omarchy current state"
theme_link="$HOME/.config/nvim/lua/plugins/theme.lua"
current_relative_target="../../../../.local/state/omarchy/current/theme/neovim.lua"
[[ -L $theme_link ]] || exit 0
ln -sfn "$current_relative_target" "$theme_link"
```
## Testing migrations
Run a migration against a temporary home when possible:
```bash
HOME=$(mktemp -d) bash -euo pipefail migrations/<timestamp>.sh
```
To rerun a migration locally, remove its marker and run the migrator:
```bash
rm ~/.local/state/omarchy/migrations/<migration>.sh
omarchy-migrate
```
Omarchy 4.0 is upgraded through `bin/omarchy-upgrade-to-quattro`, not through the
normal migration runner. Do not add compatibility migrations for old installer
layouts; put pre-4 package-layout transition work in the upgrade command instead.
Clearing a privileged file that a retired installer left on disk is the exception, and belongs in a migration whether or not that installer was part of a package layout transition. The upgrade command only runs on a machine still making the 3 to 4 crossing, so anything put there never reaches an install that crossed already, and it never runs at all for an installer that was retired on its own — while the file the installer wrote is still sitting on those machines. The upgrade command finishes by running `omarchy-migrate` (`run_post_upgrade_migrations`), so one migration reaches every population; a copy in the upgrade command would only be a second copy of the same predicate to keep correct. Such a migration must name the defect it clears and match what the old installer actually produced before deleting it. Leave safe administrator-authored files alone; if one still contains the vulnerable privileged action, preserve it under an inactive name rather than discarding custom content or leaving the action executable. A user config that depends on the same retired compatibility path may be repaired in that migration when doing so eliminates an overlapping migration, but only by matching and replacing the exact legacy path while preserving the rest of the file.
+49
View File
@@ -0,0 +1,49 @@
# Omarchy Shell Development
Read this before editing the Quickshell desktop under `shell/`.
The Quickshell desktop runs as a single long-running process out of
`shell/`. Hyprland autostart launches it directly with `quickshell -n -p`;
do not start additional standalone Quickshell instances for individual
components.
Run `omarchy-restart-shell` after making changes to QML files.
## Plugin contract
- First-party plugins live directly under `shell/plugins/` or one category
level deeper, such as `shell/plugins/panels/weather/`. First-party bar-only
widgets may use adjacent `*.manifest.json` files. Third-party plugins live
at `~/.config/omarchy/plugins/<id>/` with a `manifest.json` at the root.
- Every plugin manifest declares `schemaVersion`, `id`, `name`, `version`,
`kinds`, and `entryPoints`. See
[`docs/omarchy-shell.md`](../../docs/omarchy-shell.md) and
`shell/services/PluginRegistry.qml` for the current contract; fields such as
`activation` are optional.
- Entry-point QML files are `Item`s (not `ShellRoot`), and accept the
shell-injected properties `omarchyPath`, `shell`, `manifest`, and
`pluginRegistry` / `barWidgetRegistry` as appropriate.
- Panel / overlay / menu plugins must expose `open(payloadJson)` and
`close()` lifecycle methods for `shell summon` and `shell hide`.
## IPC
- `bin/omarchy-shell` is the canonical IPC entry point. It forwards to
the running shell and does not start it. Prefer it over re-implementing
direct Quickshell socket calls in every CLI.
- The `shell` IPC target exposes lifecycle and configuration methods including
`ping`, `summon`, `hide`, `toggle`, `call`, `rescanPlugins`, `reloadConfig`,
`setPluginEnabled`, and `listPlugins`. `shell.qml` also registers
`image-selector`, which drives the `omarchy.image-picker` panel.
- Individual plugins register their own IPC targets, named for the plugin rather
than for where they appear: the background switcher registers `background`, and
bar widgets register one target each — `omarchy.indicators`,
`omarchy.system-update`, `omarchy.clock`. There is no `bar` target.
## Editing widget files with glyphs
Widget files in `shell/plugins/bar/widgets/` contain Nerd Font glyphs as raw
unicode characters. Agent file-editing tools can strip multi-byte codepoints
in some positions — do **not** rewrite widget files wholesale through those
tools. For glyph fixes, make a targeted edit with the surrounding context, or
use a Python script that inserts codepoints via `chr(0xXXXXX)`.
+44
View File
@@ -0,0 +1,44 @@
# Visual Verification
Read this before finishing any change with a visual effect: Omarchy shell
styling and layout, panels, menus, notifications, desktop appearance,
animations, transitions, screenshots, and screen recording flows.
Visual changes must be verified in the running UI in addition to automated
tests. Creating an artifact is not sufficient: inspect it for clipping,
overlap, incorrect spacing, stale state, focus problems, and visual
regressions before finishing.
Take a full-screen screenshot without opening the editor:
```bash
omarchy capture screenshot fullscreen save
```
The command prints the saved path and writes to the configured Pictures
directory. Use `omarchy screenshot` for the interactive smart-region flow.
Capture reference and candidate states as separate images when changing a
layer-shell surface or layout, then compare both.
Record a short full-screen video for animation, transition, timing, capture, or
screen-recording changes:
```bash
omarchy screenrecord --fullscreen
# Exercise the changed behavior.
omarchy screenrecord --stop-recording
```
The stop command prints the saved video path in the configured Videos
directory. Review the recording before finishing, and keep it short and focused
on the changed behavior.
For interactive UI work, use `wtype` to simulate keyboard input when available.
Example: start the UI in the background, wait briefly for focus, then run
`wtype -k Right -k Return` to exercise keyboard selection and confirm the
resulting command output or state change. Prefer this over manual-only
verification when a UI returns a selected value or changes a symlink/config.
If a launched UI would otherwise remain open, keep track of its PID and stop it
after the screenshot or recording; avoid broad process kills unless checking
with `ps` first.
+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Name=Basecamp
Exec=omarchy-launch-webapp https://launchpad.37signals.com
Terminal=false
Type=Application
Icon=basecamp
StartupNotify=true
+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Name=Discord
Exec=omarchy-launch-webapp https://discord.com/channels/@me
Terminal=false
Type=Application
Icon=omarchy-discord
StartupNotify=true
+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Name=Disk Usage
Exec=xdg-terminal-exec --app-id=TUI.float -e bash -c "dua i /"
Terminal=false
Type=Application
Icon=disk-usage
StartupNotify=true
+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Name=Docker
Exec=xdg-terminal-exec --app-id=TUI.tile -e omarchy-launch-docker-tui
Terminal=false
Type=Application
Icon=docker
StartupNotify=true
+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Name=Google Contacts
Exec=omarchy-launch-webapp https://contacts.google.com/
Terminal=false
Type=Application
Icon=google-contacts
StartupNotify=true
+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Name=Google Maps
Exec=omarchy-launch-webapp https://maps.google.com
Terminal=false
Type=Application
Icon=google-maps
StartupNotify=true
+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Name=Google Messages
Exec=omarchy-launch-webapp https://messages.google.com/web/conversations
Terminal=false
Type=Application
Icon=google-messages
StartupNotify=true
+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Name=Google Photos
Exec=omarchy-launch-webapp https://photos.google.com/
Terminal=false
Type=Application
Icon=google-photos
StartupNotify=true
+9
View File
@@ -0,0 +1,9 @@
[Desktop Entry]
Version=1.0
Name=HEY
Exec=omarchy-webapp-handler-hey %u
Terminal=false
Type=Application
Icon=hey
StartupNotify=true
MimeType=x-scheme-handler/mailto
+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Name=WhatsApp
Exec=omarchy-launch-webapp https://web.whatsapp.com/
Terminal=false
Type=Application
Icon=whatsapp
StartupNotify=true
+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Name=X
Exec=omarchy-launch-webapp https://x.com/
Terminal=false
Type=Application
Icon=x
StartupNotify=true
+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Name=YouTube
Exec=omarchy-launch-webapp https://youtube.com/
Terminal=false
Type=Application
Icon=youtube
StartupNotify=true
+9
View File
@@ -0,0 +1,9 @@
[Desktop Entry]
Version=1.0
Name=Zoom
Exec=omarchy-webapp-handler-zoom %u
Terminal=false
Type=Application
Icon=zoom
StartupNotify=true
MimeType=x-scheme-handler/zoommtg;x-scheme-handler/zoomus
+21
View File
@@ -0,0 +1,21 @@
[Desktop Entry]
Type=Application
TryExec=foot
Exec=foot
Icon=foot
Terminal=false
Categories=System;TerminalEmulator;
Name=Foot
GenericName=Terminal
Comment=A fast, lightweight and minimalistic Wayland terminal emulator
StartupNotify=true
StartupWMClass=foot
Actions=New;
X-TerminalArgExec=-e
X-TerminalArgAppId=--app-id=
X-TerminalArgTitle=--title=
X-TerminalArgDir=--working-directory=
[Desktop Action New]
Name=New Terminal
Exec=foot
-2
View File
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
-2
View File
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
-2
View File
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
-2
View File
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
-2
View File
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
-2
View File
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
-2
View File
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
-2
View File
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
-2
View File
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
-2
View File
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
-2
View File
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
-2
View File
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
-2
View File
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
-2
View File
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
-2
View File
@@ -1,2 +0,0 @@
[Desktop Entry]
Hidden=true
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

File renamed without changes.
-10
View File
@@ -1,10 +0,0 @@
[Desktop Entry]
Name=Typora
GenericName=Markdown Editor
Exec=typora --enable-wayland-ime %U
Icon=typora
Type=Application
StartupNotify=true
Categories=Office;WordProcessor;
MimeType=text/markdown;text/x-markdown;
Executable
+1092
View File
File diff suppressed because it is too large. Load diff
+130
View File
@@ -0,0 +1,130 @@
#!/bin/bash
# omarchy:summary=Launch the default coding agent in a terminal
# omarchy:args=[--inline] [--pick]
# omarchy:examples=omarchy agent | omarchy agent --inline
inline=false
pick=false
# Flags only. A bare prompt would shadow the subcommands under this group, so
# prompts go through omarchy-agent-prompt, which passes one here as --prompt.
while (($#)); do
case "$1" in
--inline)
inline=true
shift
;;
--pick)
pick=true
shift
;;
--prompt)
prompt=${2:?--prompt needs a value}
shift 2
;;
*)
echo "Unexpected argument: $1" >&2
echo "To pass a prompt: omarchy agent prompt \"$*\"" >&2
exit 1
;;
esac
done
# Agents refuse to remember trust for $HOME, so launches from the keybinding or
# menu start in the work directory instead of re-asking on every session.
[[ $PWD == "$HOME" && -d $HOME/Work ]] && cd "$HOME/Work"
agent=$(omarchy-default-agent)
# Omarchy ships without a default, so there is nothing to launch until one is
# picked. --pick offers the choice instead of the error, which is what the
# keybinding wants: a keypress that opens nothing explains nothing.
if [[ -z $agent ]]; then
[[ $pick == "true" ]] && exec omarchy-menu summon setup.default.agent
echo "Choose default agent with: omarchy default agent <name>" >&2
exit 1
fi
if omarchy-cmd-missing "$agent"; then
echo "$agent is not installed. Choose an installed agent with: omarchy default agent <name>" >&2
exit 1
fi
# Agents launched from the keybinding or menu run unattended, so each one starts
# with its own spelling of "don't stop to ask". Pi, Ori and Dim have none to
# skip.
case "$agent" in
opencode)
command=(opencode --auto)
[[ -n ${prompt:-} ]] && command+=(--prompt "$prompt")
;;
agy)
command=(agy --dangerously-skip-permissions)
[[ -n ${prompt:-} ]] && command+=(--prompt-interactive "$prompt")
;;
copilot)
command=(copilot --allow-all)
[[ -n ${prompt:-} ]] && command+=(--interactive "$prompt")
;;
crush)
# --yolo belongs to the interactive command only; `crush run` never prompts.
if [[ -n ${prompt:-} ]]; then
command=(crush run "$prompt")
else
command=(crush --yolo)
fi
;;
claude)
command=(claude --permission-mode auto)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
;;
dim)
# `dim exec` is the one-shot prompt form; interactive dim is the bare TUI.
if [[ -n ${prompt:-} ]]; then
command=(dim exec "$prompt")
else
command=(dim)
fi
;;
grok)
command=(grok --permission-mode bypassPermissions)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
;;
kimi)
# --prompt is kimi's one-shot form, like `crush run`; --auto spares its questions.
command=(kimi --auto)
[[ -n ${prompt:-} ]] && command+=(--prompt "$prompt")
;;
codex)
command=(codex --approve-for-me)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
;;
omp)
command=(omp --auto-approve)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
;;
ori)
# Ori is a harness launcher, and `ori code` is the agent it runs itself.
# A prompt alone means one headless turn there, printed after the turn ends,
# so --interactive is what seeds the session with it and keeps the window.
command=(ori code)
[[ -n ${prompt:-} ]] && command+=(--interactive --prompt "$prompt")
;;
pi)
command=(pi)
[[ -n ${prompt:-} ]] && command+=("$prompt")
;;
*)
echo "Unsupported default agent: $agent" >&2
exit 1
;;
esac
if [[ $inline == "true" ]]; then
exec "${command[@]}"
else
# A fixed app-id rather than the default org.omarchy.<binary>, so every agent
# window shares one class for window rules and themes to single out.
exec omarchy-launch-tui --app-id=org.omarchy.agent "${command[@]}"
fi
+52
View File
@@ -0,0 +1,52 @@
#!/bin/bash
# omarchy:summary=Diagnose a crashed process with the default coding agent
# omarchy:args=<pid> [comm] [exe] [signal]
# omarchy:examples=omarchy agent crash 1516893
# Clicked from a "Process crashed:" notification, or run by hand against any PID
# in `coredumpctl list`. The method lives in the diagnose-crash skill so it is
# edited in one place and works with whichever agent is default; this only
# gathers the facts and points at it.
set -euo pipefail
pid=${1:?usage: omarchy-agent-crash <pid> [comm] [exe] [signal]}
if [[ ! $pid =~ ^[0-9]+$ ]]; then
echo "Not a PID: $pid" >&2
echo "Usage: omarchy agent crash <pid> (see: coredumpctl list)" >&2
exit 1
fi
comm=${2:-unknown}
exe=${3:-unknown}
signal=${4:-unknown}
skill="$OMARCHY_PATH/default/agents/skills/diagnose-crash/SKILL.md"
# Looked up live so a hand-run PID still gets a timestamp. A rotated-away core
# only costs the timestamp, so failure is tolerated.
when=$(coredumpctl list "$pid" --no-pager --no-legend 2>/dev/null | tail -1 | cut -d' ' -f1-4) || true
when=${when:-unknown}
prompt=$(
cat <<PROMPT
A process crashed on this Omarchy machine and I want to know why.
What systemd-coredump recorded:
process: $comm
PID: $pid
binary: $exe
signal: $signal
time: $when
Use the diagnose-crash skill: it covers how to investigate, what to report, and
when a crash is worth reporting upstream to Omarchy. If your harness has no skill
mechanism, read the skill files directly and follow them instead:
$skill
PROMPT
)
exec omarchy-agent --prompt "$prompt"
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# omarchy:summary=Launch the default coding agent with a prompt
# omarchy:args=[--inline] <prompt...>
# omarchy:examples=omarchy agent prompt "Review this project"
# Prompts live here rather than on `omarchy agent`, where a bare prompt would
# shadow the subcommands under that group.
set -euo pipefail
inline=()
if [[ ${1:-} == "--inline" ]]; then
inline=(--inline)
shift
fi
if (($# == 0)); then
echo "Usage: omarchy agent prompt [--inline] <prompt...>" >&2
exit 1
fi
exec omarchy-agent "${inline[@]}" --prompt "$*"
+905
View File
@@ -0,0 +1,905 @@
#!/usr/bin/python3
# omarchy:summary=Print the Claude Code usage record as JSON
# omarchy:args=[--force] [--limits-only]
# omarchy:hidden=true
"""Collect Claude Code usage into one display-ready JSON record.
Everything the agents panel shows for Claude comes from this one
command: local transcript stats from ~/.claude/projects, the stats-cache and
history fallbacks for machines without transcripts, pi/omp and opencode
sessions that ran on an Anthropic provider, and the authoritative rate
limits from Anthropic's OAuth usage endpoint. The panel itself only ever
reads the JSON this prints; it never talks to disk formats or endpoints.
"""
from __future__ import annotations
import argparse
import datetime as dt
import fcntl
import hashlib
import json
import os
import re
import sqlite3
import sys
import tempfile
import time
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any
AGENT_ID = "claude"
AGENT_NAME = "Claude Code"
AUTH_HELP = "Run `claude auth login` to restore authoritative usage."
USAGE_ENDPOINT = "https://api.anthropic.com/api/oauth/usage"
PROBE_MIN_INTERVAL_SECONDS = 15
def config_dir() -> Path:
return expand_path(os.environ.get("CLAUDE_CONFIG_DIR") or "~/.claude")
def expand_path(value: str) -> Path:
return Path(os.path.expandvars(os.path.expanduser(value))).resolve()
def cache_root() -> Path:
root = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / "omarchy" / "agent-usage"
root.mkdir(parents=True, exist_ok=True)
return root
def date_string(value: dt.date) -> str:
return value.strftime("%Y-%m-%d")
def recent_date_strings() -> list[str]:
today = dt.datetime.now().date()
return [date_string(today - dt.timedelta(days=offset)) for offset in range(6, -1, -1)]
def local_date_string() -> str:
return date_string(dt.datetime.now().date())
def local_date_from_timestamp(value: Any) -> str:
if value is None:
return local_date_string()
if isinstance(value, (int, float)):
try:
seconds = float(value) / 1000.0 if float(value) > 10_000_000_000 else float(value)
return date_string(dt.datetime.fromtimestamp(seconds).date())
except Exception:
return local_date_string()
raw = str(value).strip()
if not raw:
return local_date_string()
# Claude JSONL timestamps are usually ISO-8601. Python accepts offsets but
# not a trailing Z until we normalize it to +00:00.
try:
parsed = dt.datetime.fromisoformat(raw.replace("Z", "+00:00"))
if parsed.tzinfo is not None:
parsed = parsed.astimezone()
return date_string(parsed.date())
except Exception:
return local_date_string()
def usage_token(usage: dict[str, Any], snake_key: str, camel_key: str) -> int:
value = usage.get(snake_key, usage.get(camel_key, 0))
try:
return round(float(value or 0))
except Exception:
return 0
def number(value: Any) -> int:
try:
n = float(value or 0)
return round(n) if n == n else 0
except Exception:
return 0
def empty_bucket() -> dict[str, int]:
return {
"inputTokens": 0,
"outputTokens": 0,
"cacheReadInputTokens": 0,
"cacheCreationInputTokens": 0,
}
# ---------------------------------------------------------------- local scan
def scan_projects(projects_path: Path) -> dict[str, Any]:
today = local_date_string()
recent_dates = recent_date_strings()
recent = {day: {"date": day, "messageCount": 0} for day in recent_dates}
seen: set[str] = set()
sessions: set[str] = set()
active_days: set[str] = set()
today_sessions: set[str] = set()
today_tokens: dict[str, int] = {}
usage_by_model: dict[str, dict[str, int]] = {}
prompts = 0
today_prompt_count = 0
today_token_total = 0
files = projects_path.rglob("*.jsonl") if projects_path.is_dir() else []
for path in files:
try:
with path.open("r", encoding="utf-8", errors="replace") as handle:
for line_number, line in enumerate(handle, 1):
# Cheap pre-filter before JSON parsing keeps files with unrelated
# lines inexpensive.
if '"usage":' not in line:
continue
try:
entry = json.loads(line)
except Exception:
continue
message = entry.get("message") if isinstance(entry.get("message"), dict) else {}
if entry.get("type") != "assistant" and message.get("role") != "assistant":
continue
usage = message.get("usage") or entry.get("usage")
if not isinstance(usage, dict):
continue
message_id = message.get("id") or entry.get("messageId") or ""
unique_key = str(message_id) if message_id else f"{path}:{entry.get('uuid') or entry.get('requestId') or line_number}"
if unique_key in seen:
continue
seen.add(unique_key)
input_tokens = usage_token(usage, "input_tokens", "inputTokens")
output_tokens = usage_token(usage, "output_tokens", "outputTokens")
cache_read = usage_token(usage, "cache_read_input_tokens", "cacheReadInputTokens")
cache_write = usage_token(usage, "cache_creation_input_tokens", "cacheCreationInputTokens")
total = input_tokens + output_tokens + cache_read + cache_write
if total <= 0:
continue
model = str(message.get("model") or entry.get("model") or "claude")
day = local_date_from_timestamp(entry.get("timestamp") or message.get("timestamp"))
session_key = str(entry.get("sessionId") or path)
sessions.add(session_key)
active_days.add(day)
prompts += 1
bucket = usage_by_model.setdefault(model, empty_bucket())
bucket["inputTokens"] += input_tokens
bucket["outputTokens"] += output_tokens
bucket["cacheReadInputTokens"] += cache_read
bucket["cacheCreationInputTokens"] += cache_write
if day in recent:
# recentDays.messageCount is actually a token total, despite the
# legacy name shared with synced snapshots.
recent[day]["messageCount"] += total
if day == today:
today_prompt_count += 1
today_sessions.add(session_key)
today_token_total += total
today_tokens[model] = today_tokens.get(model, 0) + total
except Exception as exc:
print(f"Ignoring unreadable Claude project file {path}: {exc}", file=sys.stderr)
return {
"todayPrompts": today_prompt_count,
"todaySessions": len(today_sessions),
"todayTotalTokens": today_token_total,
"todayTokensByModel": today_tokens,
"recentDays": [recent[day] for day in recent_dates],
"modelUsage": usage_by_model,
"totalPrompts": prompts,
"totalSessions": len(sessions),
# Days with any recorded usage, for the all-time "N days" summary. The
# dates travel too: merging snapshots from several machines needs their
# union, which a count alone cannot give.
"activeDays": len(active_days),
"activeDates": sorted(active_days),
}
def scan_cache_paths(projects_path: Path) -> tuple[Path, Path]:
digest = hashlib.sha1(str(projects_path).encode("utf-8")).hexdigest()[:16]
root = cache_root()
return root / f"claude-scan-{digest}.json", root / f"claude-scan-{digest}.lock"
def read_fresh_json(path: Path, max_age_seconds: float) -> dict[str, Any] | None:
if max_age_seconds <= 0 or not path.exists():
return None
try:
if time.time() - path.stat().st_mtime <= max_age_seconds:
return json.loads(path.read_text(encoding="utf-8"))
except Exception:
return None
return None
def write_json(path: Path, payload: dict[str, Any]) -> None:
# A temp name unique to this writer, not derived from the target: several
# collectors can run at once (the update command backgrounds one per agent,
# the panel refreshes on its own), and a shared temp path means the second
# replace finds the first one's file already moved away.
handle_fd, tmp_name = tempfile.mkstemp(dir=path.parent, prefix=path.name + ".", suffix=".tmp")
tmp = Path(tmp_name)
try:
with os.fdopen(handle_fd, "w", encoding="utf-8") as handle:
handle.write(json.dumps(payload, separators=(",", ":"), sort_keys=True) + "\n")
# mkstemp opens at 0600; these caches were world-readable before.
tmp.chmod(0o644)
tmp.replace(path)
except BaseException:
tmp.unlink(missing_ok=True)
raise
def cached_scan(projects_path: Path, max_age_seconds: float) -> dict[str, Any]:
cache_file, lock_file = scan_cache_paths(projects_path)
cached = read_fresh_json(cache_file, max_age_seconds)
if cached is not None:
return cached
with lock_file.open("w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
cached = read_fresh_json(cache_file, max_age_seconds)
if cached is not None:
return cached
summary = scan_projects(projects_path)
write_json(cache_file, summary)
return summary
# ------------------------------------------------------------- local fallback
#
# A machine without transcripts on disk can still know its history: Claude
# Code keeps aggregate counters in stats-cache.json and per-prompt history in
# history.jsonl. Only consulted when the project scan comes back empty.
def stats_cache_fallback(claude_dir: Path) -> dict[str, Any] | None:
try:
data = json.loads((claude_dir / "stats-cache.json").read_text(encoding="utf-8"))
except Exception:
return None
today = local_date_string()
daily_model_tokens = data.get("dailyModelTokens") or []
today_tokens = {}
for entry in daily_model_tokens:
if isinstance(entry, dict) and entry.get("date") == today:
today_tokens = entry.get("tokensByModel") or {}
break
daily_activity = [day for day in (data.get("dailyActivity") or []) if isinstance(day, dict)]
active_dates = sorted({str(day.get("date")) for day in daily_activity if number(day.get("messageCount")) > 0 and day.get("date")})
today_prompts, today_sessions = today_prompts_from_history(claude_dir)
return {
"todayPrompts": today_prompts,
"todaySessions": today_sessions,
"todayTotalTokens": sum(number(v) for v in today_tokens.values()),
"todayTokensByModel": today_tokens,
"recentDays": daily_activity[-7:],
"modelUsage": data.get("modelUsage") or {},
"totalPrompts": number(data.get("totalMessages")),
"totalSessions": number(data.get("totalSessions")),
"activeDays": len(active_dates),
"activeDates": active_dates,
}
def today_prompts_from_history(claude_dir: Path) -> tuple[int, int]:
prompts = 0
sessions: set[str] = set()
start_of_day = dt.datetime.combine(dt.datetime.now().date(), dt.time.min).timestamp() * 1000
try:
with (claude_dir / "history.jsonl").open("r", encoding="utf-8", errors="replace") as handle:
lines = handle.readlines()
except Exception:
return 0, 0
for line in reversed(lines):
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except Exception:
continue
if number(entry.get("timestamp")) < start_of_day:
break
prompts += 1
if entry.get("sessionId"):
sessions.add(str(entry.get("sessionId")))
return prompts, len(sessions)
# --------------------------------------------------------------- pi and omp
#
# These agents can consume a Claude subscription without writing native
# Claude Code transcripts. Their compatible JSONL session formats carry the
# provider, model, and token usage on every assistant message.
def scan_pi_usage(max_age_seconds: float) -> dict[str, Any] | None:
roots = [
Path.home() / ".pi" / "agent" / "sessions",
Path.home() / ".omp" / "agent" / "sessions",
]
cache_file = cache_root() / "claude-pi-sessions.json"
cached = read_fresh_json(cache_file, max_age_seconds)
if cached is not None:
return cached.get("stats")
today = local_date_string()
recent_dates = recent_date_strings()
recent = {day: {"date": day, "messageCount": 0} for day in recent_dates}
sessions: set[str] = set()
active_days: set[str] = set()
today_sessions: set[str] = set()
today_tokens: dict[str, int] = {}
usage_by_model: dict[str, dict[str, int]] = {}
seen: set[str] = set()
prompts = 0
today_prompt_count = 0
today_token_total = 0
for root in roots:
files = root.rglob("*.jsonl") if root.is_dir() else []
for path in files:
try:
with path.open("r", encoding="utf-8", errors="replace") as handle:
for line_number, line in enumerate(handle, 1):
if '"usage"' not in line or '"assistant"' not in line:
continue
try:
entry = json.loads(line)
message = entry.get("message") if isinstance(entry.get("message"), dict) else {}
if entry.get("type") != "message" or message.get("role") != "assistant":
continue
provider = str(message.get("provider") or "")
if provider != "anthropic":
continue
unique_key = f"{path}:{entry.get('id') or line_number}"
if unique_key in seen:
continue
seen.add(unique_key)
usage = message.get("usage") or {}
input_tokens = usage_token(usage, "input", "inputTokens")
output_tokens = usage_token(usage, "output", "outputTokens")
cache_read = usage_token(usage, "cacheRead", "cache_read_input_tokens")
cache_write = usage_token(usage, "cacheWrite", "cache_creation_input_tokens")
total = input_tokens + output_tokens + cache_read + cache_write
if total <= 0:
total = number(usage.get("totalTokens"))
input_tokens = total
if total <= 0:
continue
model = str(message.get("model") or "claude")
day = local_date_from_timestamp(entry.get("timestamp") or message.get("timestamp"))
except Exception:
continue
session_key = str(path)
sessions.add(session_key)
active_days.add(day)
prompts += 1
bucket = usage_by_model.setdefault(model, empty_bucket())
bucket["inputTokens"] += input_tokens
bucket["outputTokens"] += output_tokens
bucket["cacheReadInputTokens"] += cache_read
bucket["cacheCreationInputTokens"] += cache_write
if day in recent:
recent[day]["messageCount"] += total
if day == today:
today_prompt_count += 1
today_sessions.add(session_key)
today_token_total += total
today_tokens[model] = today_tokens.get(model, 0) + total
except OSError:
continue
stats = None
if prompts > 0:
stats = {
"todayPrompts": today_prompt_count,
"todaySessions": len(today_sessions),
"todayTotalTokens": today_token_total,
"todayTokensByModel": today_tokens,
"recentDays": [recent[day] for day in recent_dates],
"modelUsage": usage_by_model,
"totalPrompts": prompts,
"totalSessions": len(sessions),
"activeDays": len(active_days),
"activeDates": sorted(active_days),
}
write_json(cache_file, {"stats": stats})
return stats
# ---------------------------------------------------------------- opencode
#
# A Claude subscription burned entirely through opencode never writes a
# transcript under ~/.claude, but opencode records per-message provider,
# model, and token usage in its own database. Scan it for Anthropic-provider
# messages and merge the result into whatever the transcript scan found.
def scan_opencode_usage(max_age_seconds: float) -> dict[str, Any] | None:
db = Path(os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local" / "share")) / "opencode" / "opencode.db"
if not db.is_file():
return None
# Same freshness contract as the transcript scan: --limits-only promises to
# reuse recent local stats, and a big opencode history walked on every panel
# open would break that promise.
cache_file = cache_root() / f"claude-opencode-{hashlib.sha1(str(db).encode('utf-8')).hexdigest()[:16]}.json"
cached = read_fresh_json(cache_file, max_age_seconds)
if cached is not None:
return cached.get("stats")
today = local_date_string()
recent_dates = recent_date_strings()
recent = {day: {"date": day, "messageCount": 0} for day in recent_dates}
sessions: set[str] = set()
active_days: set[str] = set()
today_sessions: set[str] = set()
today_tokens: dict[str, int] = {}
usage_by_model: dict[str, dict[str, int]] = {}
prompts = 0
today_prompt_count = 0
today_token_total = 0
try:
# Read-only: opencode may be writing right now.
conn = sqlite3.connect(db.resolve().as_uri() + "?mode=ro", uri=True, timeout=2)
except sqlite3.Error:
return None
try:
conn.execute("PRAGMA query_only = ON")
for session_id, raw in conn.execute("SELECT session_id, data FROM message"):
# One malformed row must not abort the scan, so every shape assumption
# lives inside the try.
try:
entry = json.loads(raw)
# Exact match: opencode provider ids are free-form, and a custom
# "anthropic-proxy" gateway is not this subscription.
if not isinstance(entry, dict) or entry.get("role") != "assistant":
continue
if str(entry.get("providerID") or "") != "anthropic":
continue
tokens = entry.get("tokens") or {}
cache = tokens.get("cache") or {}
input_tokens = number(tokens.get("input"))
# opencode keeps thinking tokens out of output; both are generated.
output_tokens = number(tokens.get("output")) + number(tokens.get("reasoning"))
cache_read = number(cache.get("read"))
cache_write = number(cache.get("write"))
total = input_tokens + output_tokens + cache_read + cache_write
if total <= 0:
continue
created = number((entry.get("time") or {}).get("created"))
day = dt.datetime.fromtimestamp(created / 1000).strftime("%Y-%m-%d") if created > 0 else today
model = str(entry.get("modelID") or "claude").rstrip("/").split("/")[-1]
except Exception:
continue
session_key = "opencode:" + str(session_id)
sessions.add(session_key)
active_days.add(day)
prompts += 1
bucket = usage_by_model.setdefault(model, empty_bucket())
bucket["inputTokens"] += input_tokens
bucket["outputTokens"] += output_tokens
bucket["cacheReadInputTokens"] += cache_read
bucket["cacheCreationInputTokens"] += cache_write
if day in recent:
recent[day]["messageCount"] += total
if day == today:
today_prompt_count += 1
today_sessions.add(session_key)
today_token_total += total
today_tokens[model] = today_tokens.get(model, 0) + total
except sqlite3.Error:
return None
finally:
conn.close()
stats = None
if prompts > 0:
stats = {
"todayPrompts": today_prompt_count,
"todaySessions": len(today_sessions),
"todayTotalTokens": today_token_total,
"todayTokensByModel": today_tokens,
"recentDays": [recent[day] for day in recent_dates],
"modelUsage": usage_by_model,
"totalPrompts": prompts,
"totalSessions": len(sessions),
"activeDays": len(active_days),
"activeDates": sorted(active_days),
}
write_json(cache_file, {"stats": stats})
return stats
def merge_stats(base: dict[str, Any], extra: dict[str, Any]) -> dict[str, Any]:
merged = dict(base)
for key in ("todayPrompts", "todaySessions", "todayTotalTokens", "totalPrompts", "totalSessions"):
merged[key] = number(base.get(key)) + number(extra.get(key))
combined = dict(base.get("todayTokensByModel") or {})
for model, count in (extra.get("todayTokensByModel") or {}).items():
combined[model] = number(combined.get(model)) + number(count)
merged["todayTokensByModel"] = combined
usage = {model: dict(bucket) for model, bucket in (base.get("modelUsage") or {}).items()}
for model, bucket in (extra.get("modelUsage") or {}).items():
target = usage.setdefault(model, empty_bucket())
for field, count in (bucket or {}).items():
target[field] = number(target.get(field)) + number(count)
merged["modelUsage"] = usage
by_date: dict[str, int] = {}
for source in (base.get("recentDays") or [], extra.get("recentDays") or []):
for day in source:
date = str((day or {}).get("date") or "")
if date:
by_date[date] = by_date.get(date, 0) + number((day or {}).get("messageCount"))
merged["recentDays"] = [{"date": date, "messageCount": by_date[date]} for date in sorted(by_date)]
# Sources overlap in time, so union dates rather than summing counts. A
# fallback that only knows a count still bounds the answer from below.
dates = set(base.get("activeDates") or []) | set(extra.get("activeDates") or [])
merged["activeDates"] = sorted(dates)
merged["activeDays"] = max(len(dates), number(base.get("activeDays")), number(extra.get("activeDays")))
return merged
# ------------------------------------------------------------------- limits
# The access token, its expiry, and the display-safe plan label from the
# CLI's login. Nothing else leaves the credential store: the token goes
# nowhere but the Authorization header of the limits probe, and only the
# plan label may travel into the printed record.
def oauth_login(claude_dir: Path) -> tuple[str, int, str]:
try:
data = json.loads((claude_dir / ".credentials.json").read_text(encoding="utf-8"))
except Exception:
return "", 0, ""
login = data.get("claudeAiOauth")
if not isinstance(login, dict):
return "", 0, ""
plan = plan_label(str(login.get("rateLimitTier") or ""), str(login.get("subscriptionType") or ""))
return str(login.get("accessToken") or ""), number(login.get("expiresAt")), plan
def plan_label(tier: str, subscription: str) -> str:
if tier:
match = re.search(r"max_(\d+x)", tier, re.IGNORECASE)
if match:
return "Max " + match.group(1)
if subscription:
return subscription[0].upper() + subscription[1:]
return ""
def parse_utilization(value: Any) -> float:
try:
return float(str(value).strip().replace("%", ""))
except Exception:
return float("nan")
def normalize_utilization(value: Any, percent_scale: bool) -> float:
n = parse_utilization(value)
if not (n >= 0):
return -1.0
# Anthropic's OAuth usage endpoint currently reports percentages (for
# example 37.0 or 1.0). Older payloads sometimes used fractions (0.37).
# A payload containing any value >= 1 is percent-scaled, so 1.0 renders
# as 1%, not 100%.
if percent_scale or n > 1:
return min(1.0, n / 100.0)
return min(1.0, n)
def normalize_reset_at(value: Any) -> str:
if value is None:
return ""
raw = str(value).strip()
if raw == "":
return ""
if raw.isdigit():
ts = int(raw)
if ts < 1e12:
ts *= 1000
try:
return dt.datetime.fromtimestamp(ts / 1000, dt.timezone.utc).isoformat()
except Exception:
return raw
try:
parsed = dt.datetime.fromisoformat(raw.replace("Z", "+00:00"))
return parsed.isoformat()
except Exception:
return raw
def usage_bucket(payload: dict[str, Any], key: str) -> dict[str, Any] | None:
bucket = payload.get(key)
return bucket if isinstance(bucket, dict) else None
# An entry's `kind` names its window the way the flat buckets' keys do
# ("weekly_scoped", "five_hour_scoped"). The panel reads a window out of free
# text, which cannot survive a model name like "Opus 5 (1M context)" — the
# "1M" reads as a one-minute window — so the window is settled here instead
# and travels as an explicit title. It is capitalized the way the flat windows
# title themselves, so "Fable Weekly" sits beside "Weekly" rather than under it.
def scoped_window(kind: str) -> str:
text = kind.lower()
if "month" in text:
return "Monthly"
if "week" in text or "day" in text:
return "Weekly"
if "hour" in text or "session" in text:
return "Session"
return ""
# Alongside the flat buckets, the payload carries a `limits` array, and that
# array is the only place a model-scoped allowance shows up — a weekly window
# that only Fable draws from, say. The matching legacy keys
# (`seven_day_opus`, `seven_day_sonnet`, …) stayed behind at null, so a
# collector that reads buckets alone silently drops a limit the account is
# actually spending against. A model can hold more than one scoped window, and
# only the pair of model and window tells them apart, so both make the title
# and both make the key that keeps a repeat out.
def scoped_limits(payload: dict[str, Any], percent_scale: bool) -> list[dict[str, Any]]:
entries = payload.get("limits")
if not isinstance(entries, list):
return []
out: list[dict[str, Any]] = []
seen: set[tuple[str, str]] = set()
for entry in entries:
if not isinstance(entry, dict):
continue
scope = entry.get("scope")
model = scope.get("model") if isinstance(scope, dict) else None
if not isinstance(model, dict):
continue
# A display name is what the panel wants, but an entry carrying only an id
# still names a window worth showing.
name = str(model.get("display_name") or model.get("id") or "").strip()
kind = str(entry.get("kind") or "").strip()
if name == "" or (name, kind) in seen:
continue
percent = normalize_utilization(entry.get("percent"), percent_scale)
if percent < 0:
continue
seen.add((name, kind))
window = scoped_window(kind)
title = name + " " + window if window else name
out.append({
"label": title,
"title": title,
"percent": percent,
"resetsAt": normalize_reset_at(entry.get("resets_at")),
})
return out
def probe_limits(access_token: str) -> dict[str, Any]:
request = urllib.request.Request(
USAGE_ENDPOINT,
headers={
"Authorization": "Bearer " + access_token,
"anthropic-beta": "oauth-2025-04-20",
"Accept": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=10) as response:
payload = json.loads(response.read().decode("utf-8", errors="replace"))
except urllib.error.HTTPError as error:
retry_after = error.headers.get("retry-after", "") if error.headers else ""
if error.code == 429:
help_text = "Anthropic's usage endpoint is rate limiting checks right now" + (
f" (retry after {retry_after}s)" if retry_after else ""
) + ". Local Claude Code stats are still shown."
else:
help_text = f"Anthropic's usage endpoint returned status {error.code}. Local Claude Code stats are still shown."
return {"ok": False, "helpText": help_text}
except Exception:
# A transport failure reached no server at all — no route, no DNS. Any
# real answer, including an error status, is a server we should stop
# pestering; this is not.
return {
"ok": False,
"transport": True,
"helpText": "Couldn't reach Anthropic's usage endpoint. Retrying shortly. Local Claude Code stats are still shown.",
}
weekly = usage_bucket(payload, "seven_day_oauth_apps") or usage_bucket(payload, "seven_day")
session = usage_bucket(payload, "five_hour")
raw = [session.get("utilization") if session else None, weekly.get("utilization") if weekly else None]
# One payload speaks one convention, so the scoped entries settle the scale
# alongside the buckets rather than assuming their own.
entries = payload.get("limits")
if isinstance(entries, list):
raw += [entry.get("percent") for entry in entries if isinstance(entry, dict)]
percent_scale = any(parse_utilization(v) >= 1 for v in raw)
limits = []
if session is not None:
percent = normalize_utilization(session.get("utilization"), percent_scale)
if percent >= 0:
limits.append({"label": "Session (5-hour)", "percent": percent, "resetsAt": normalize_reset_at(session.get("resets_at"))})
if weekly is not None:
percent = normalize_utilization(weekly.get("utilization"), percent_scale)
if percent >= 0:
limits.append({"label": "Weekly (7-day)", "percent": percent, "resetsAt": normalize_reset_at(weekly.get("resets_at"))})
limits.extend(scoped_limits(payload, percent_scale))
if not limits:
return {"ok": False, "helpText": "Anthropic's usage endpoint returned no limits. Local Claude Code stats are still shown."}
return {"ok": True, "limits": limits}
# A cached percentage outlives the probe that measured it, but only until its
# window rolls over: once a window has reset, the figure describes a period
# that is over, and a stale 78% would misreport an allowance that is now
# untouched. A window with no reset time, or one that will not parse, is kept
# — an unreadable timestamp is no reason to throw away a real number.
def limit_window_open(entry: dict[str, Any], now: dt.datetime) -> bool:
raw = str(entry.get("resetsAt") or "")
if raw == "":
return True
try:
resets_at = dt.datetime.fromisoformat(raw.replace("Z", "+00:00"))
except Exception:
return True
if resets_at.tzinfo is None:
resets_at = resets_at.replace(tzinfo=dt.timezone.utc)
return resets_at > now
def usable_cached_limits(cached: dict[str, Any]) -> list[dict[str, Any]]:
entries = cached.get("limits")
if not isinstance(entries, list):
return []
now = dt.datetime.now(dt.timezone.utc)
return [entry for entry in entries if isinstance(entry, dict) and limit_window_open(entry, now)]
def collect_limits(access_token: str, expires_at_ms: int, force: bool) -> dict[str, Any]:
result = {"limits": [], "usageStatusText": "", "authHelpText": AUTH_HELP}
# A panel that is opened and shut repeatedly must not turn into a request
# per flick, so recent probe results are reused for a short window — and
# kept as the answer of record when a later probe fails.
probe_cache = cache_root() / "claude-limits.json"
cached = read_fresh_json(probe_cache, float("inf")) or {}
fallback = usable_cached_limits(cached)
# Probing needs a live token and only the Claude Code CLI can mint one: it
# refreshes the credential file when it runs, so a machine left alone long
# enough finds the saved token lapsed. Say so — an empty limits list with
# nothing else set hides the whole section and explains nothing — and keep
# showing the last numbers whose window has not since reset.
if access_token == "":
result["limits"] = fallback
result["usageStatusText"] = "Waiting for auth"
return result
if expires_at_ms > 0 and expires_at_ms <= time.time() * 1000:
result["limits"] = fallback
result["usageStatusText"] = "Sign-in expired"
result["authHelpText"] = (
"Claude Code's saved sign-in expired"
+ (" — showing the last known limits." if fallback else ".")
+ " Start Claude Code, or run `claude auth login`, to refresh it."
)
return result
# --force is a person asking for fresh numbers, so it skips the reuse window
# entirely; the interval is there to absorb repeated panel opens, not to
# overrule someone who pressed refresh.
fetched_at = number(cached.get("fetchedAtMs")) / 1000
if fallback and not force and time.time() - fetched_at < PROBE_MIN_INTERVAL_SECONDS:
result["limits"] = fallback
return result
probe = probe_limits(access_token)
if probe["ok"]:
result["limits"] = probe["limits"]
write_json(probe_cache, {"fetchedAtMs": round(time.time() * 1000), "limits": probe["limits"]})
return result
# The first probe after login often fires before DHCP has handed out a
# route. Ask the shell to try again sooner than its regular interval.
if probe.get("transport"):
result["retryAdvised"] = True
if fallback:
result["limits"] = fallback
else:
result["usageStatusText"] = "Claude limits unavailable"
result["authHelpText"] = probe["helpText"]
return result
# -------------------------------------------------------------------- record
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--force", action="store_true", help="rescan transcripts and re-probe limits, ignoring caches")
parser.add_argument("--limits-only", action="store_true", help="reuse any recent transcript scan; only the limits probe must be fresh")
parser.add_argument("--cache-seconds", type=float, default=20)
args = parser.parse_args()
claude_dir = config_dir()
scan_age = 0 if args.force else (900 if args.limits_only else args.cache_seconds)
stats = cached_scan(claude_dir / "projects", scan_age)
if number(stats.get("totalPrompts")) <= 0:
fallback = stats_cache_fallback(claude_dir)
if fallback is not None:
stats = fallback
else:
# No transcripts and no aggregate cache, but history.jsonl alone can
# still put numbers on today.
today_prompts, today_sessions = today_prompts_from_history(claude_dir)
if today_prompts or today_sessions:
stats = dict(stats, todayPrompts=today_prompts, todaySessions=today_sessions)
pi_usage = scan_pi_usage(scan_age)
if pi_usage is not None:
stats = merge_stats(stats, pi_usage)
opencode = scan_opencode_usage(scan_age)
if opencode is not None:
stats = merge_stats(stats, opencode)
access_token, expires_at_ms, plan = oauth_login(claude_dir)
limits = collect_limits(access_token, expires_at_ms, args.force)
record = {
"schemaVersion": 1,
"id": AGENT_ID,
"name": AGENT_NAME,
"updatedAt": dt.datetime.now(dt.timezone.utc).isoformat(),
"ready": number(stats.get("totalPrompts")) > 0 or len(limits["limits"]) > 0,
"hasLocalStats": True,
"tierLabel": plan,
"usageStatusText": limits["usageStatusText"],
"authHelpText": limits["authHelpText"],
"limits": limits["limits"],
}
if limits.get("retryAdvised"):
record["retryAdvised"] = True
record.update(stats)
print(json.dumps(record, separators=(",", ":"), sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+603
View File
@@ -0,0 +1,603 @@
#!/usr/bin/python3
# omarchy:summary=Print the Codex usage record as JSON
# omarchy:args=[--force] [--limits-only]
# omarchy:hidden=true
"""Collect Codex usage into one display-ready JSON record.
Local stats come from native Codex CLI session files, pi/omp sessions that
ran through openai-codex, and opencode sessions that ran on an OpenAI
provider; rate limits and the plan come from the Codex app-server RPC. The agents
panel only ever reads the JSON this prints.
"""
import argparse
import fcntl
import hashlib
import json
import os
import select
import shutil
import sqlite3
import subprocess
import sys
import tempfile
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
AGENT_ID = "codex"
AGENT_NAME = "Codex"
AUTH_HELP = "Run `codex login` to authenticate."
# A scan this recent is only reused to dedup concurrent collector runs (the
# update command backgrounds one per agent while the panel refreshes on its
# own); every periodic widget refresh lands a real rescan, however low
# refreshIntervalSec is set. --limits-only promises only fresh limits, so it
# may reuse a scan for up to 15 minutes.
SCAN_REUSE_SECONDS = 20
LIMITS_ONLY_REUSE_SECONDS = 900
def local_day(value):
if value is None:
return datetime.now().strftime("%Y-%m-%d")
if isinstance(value, (int, float)):
# pi message timestamps are milliseconds; Codex timestamps are usually seconds.
if value > 10_000_000_000:
value = value / 1000
return datetime.fromtimestamp(value).strftime("%Y-%m-%d")
text = str(value)
try:
if text.endswith("Z"):
dt = datetime.fromisoformat(text[:-1] + "+00:00")
else:
dt = datetime.fromisoformat(text)
if dt.tzinfo is not None:
dt = dt.astimezone()
return dt.strftime("%Y-%m-%d")
except Exception:
return datetime.now().strftime("%Y-%m-%d")
def number(value):
try:
return int(value or 0)
except Exception:
return 0
def model_name(raw):
value = str(raw or "codex")
return value if value else "codex"
def runtime_env():
home = str(Path.home())
path_parts = [
os.environ.get("PATH", ""),
f"{home}/.local/bin",
f"{home}/.npm-global/bin",
f"{home}/.local/share/mise/shims",
]
env = os.environ.copy()
env["PATH"] = os.pathsep.join(part for part in path_parts if part)
return env
ENV = runtime_env()
def find_command(name):
return shutil.which(name, path=ENV.get("PATH"))
now = datetime.now()
today = now.strftime("%Y-%m-%d")
recent_dates = [(now - timedelta(days=offset)).strftime("%Y-%m-%d") for offset in range(6, -1, -1)]
recent = {day: {"date": day, "messageCount": 0} for day in recent_dates}
today_tokens_by_model = {}
model_usage = {}
today_sessions = set()
active_days = set()
today_prompts = 0
today_total_tokens = 0
total_prompts = 0
total_sessions = set()
seen_pi_messages = set()
def add_usage(day, session_key, model, input_tokens, output_tokens, cache_read, cache_write):
global today_prompts, today_total_tokens, total_prompts
total = input_tokens + output_tokens + cache_read + cache_write
total_prompts += 1
total_sessions.add(session_key)
active_days.add(day)
bucket = model_usage.setdefault(model, {
"inputTokens": 0,
"outputTokens": 0,
"cacheReadInputTokens": 0,
"cacheCreationInputTokens": 0,
})
bucket["inputTokens"] += input_tokens
bucket["outputTokens"] += output_tokens
bucket["cacheReadInputTokens"] += cache_read
bucket["cacheCreationInputTokens"] += cache_write
if day in recent:
recent[day]["messageCount"] += total
if day == today:
today_prompts += 1
today_sessions.add(session_key)
today_total_tokens += total
today_tokens_by_model[model] = today_tokens_by_model.get(model, 0) + total
def scan_pi_sessions():
roots = [
Path.home() / ".pi" / "agent" / "sessions",
Path.home() / ".omp" / "agent" / "sessions",
]
rg = find_command("rg") or "rg"
for root in roots:
if not root.exists():
continue
try:
proc = subprocess.Popen(
[rg, "--json", "-e", r'"provider"\s*:\s*"openai-codex"', "-e", r'"api"\s*:\s*"openai-codex', str(root)],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
errors="replace",
env=ENV,
)
except FileNotFoundError:
return
assert proc.stdout is not None
for raw in proc.stdout:
try:
event = json.loads(raw)
if event.get("type") != "match":
continue
line = event.get("data", {}).get("lines", {}).get("text", "")
path = event.get("data", {}).get("path", {}).get("text", "pi-session")
entry = json.loads(line)
except Exception:
continue
if entry.get("type") != "message":
continue
message_key = path + ":" + str(entry.get("id") or "")
if message_key in seen_pi_messages:
continue
seen_pi_messages.add(message_key)
message = entry.get("message") or {}
if message.get("role") != "assistant":
continue
provider = str(message.get("provider") or "")
api = str(message.get("api") or "")
if provider != "openai-codex" and not api.startswith("openai-codex"):
continue
usage = message.get("usage") or {}
if not usage:
continue
total = number(usage.get("totalTokens"))
input_tokens = number(usage.get("input"))
output_tokens = number(usage.get("output"))
cache_read = number(usage.get("cacheRead"))
cache_write = number(usage.get("cacheWrite"))
if total and not (input_tokens or output_tokens or cache_read or cache_write):
input_tokens = total
if not (input_tokens or output_tokens or cache_read or cache_write):
continue
day = local_day(entry.get("timestamp") or message.get("timestamp"))
session_key = path
add_usage(day, session_key, model_name(message.get("model")), input_tokens, output_tokens, cache_read, cache_write)
try:
proc.wait(timeout=1)
except Exception:
proc.kill()
def scan_opencode_sessions():
# A subscription burned entirely through opencode leaves no native session
# files, but opencode records per-message provider, model, and token usage
# in its own database. Read-only: opencode may be writing right now.
# Returns whether the scan ran to completion: a scan cut short by a
# database error still contributes what it read, but must not be cached
# as if it were the whole story.
db = Path(os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local" / "share")) / "opencode" / "opencode.db"
if not db.is_file():
return True
try:
conn = sqlite3.connect(db.resolve().as_uri() + "?mode=ro", uri=True, timeout=2)
except sqlite3.Error:
return False
try:
conn.execute("PRAGMA query_only = ON")
# OpenCode DBs grow huge (every historical message, JSON included), and
# Python-side json.loads of every row wasted ~600 MB of RSS on machines
# whose subscription never ran on OpenAI. The json_extract conditions are
# the authority for the rows that reach them: role == "assistant" and
# providerID == "openai", the same exact-match values the Python filter
# below checks. The guards in front are pure acceleration, not a perfect
# proxy for the Python filter:
# - The LIKE gates skip rows whose JSON cannot contain the two
# key/value pairs, avoiding the JSON parse of tens-of-MB blobs. They
# can differ from json.loads on duplicate keys (Python keeps the
# last, SQLite json_extract keeps the first) and ASCII-escaped
# values (\"ass\\u0069stant\" decodes for Python but not for LIKE),
# so a row the authority would accept can be gated out. Both cases
# are vanishingly rare in real opencode data.
# - json_valid guards the parse itself: json_extract() RAISES on
# malformed JSON instead of returning NULL, and one such row would
# otherwise abort the whole scan. SQLite does not promise that AND
# terms evaluate left to right, so the guard is a CASE around each
# json_extract rather than a separate AND term. Rows that are not
# well-formed JSON are skipped here; the per-row try/except below
# stays as the final safety net for rows that pass the SQL filter
# but fail json.loads.
for session_id, raw in conn.execute(
"SELECT session_id, data FROM message"
" WHERE data LIKE '%\"role\"%:%\"assistant\"%'"
" AND data LIKE '%\"providerID\"%:%\"openai\"%'"
" AND CASE WHEN json_valid(data) THEN json_extract(data, '$.role') END = 'assistant'"
" AND CASE WHEN json_valid(data) THEN json_extract(data, '$.providerID') END = 'openai'"
):
# One malformed row must not abort the scan, so every shape assumption
# lives inside the try.
try:
entry = json.loads(raw)
# Exact match: opencode provider ids are free-form, and a custom
# "openai-local" gateway is not this subscription.
if not isinstance(entry, dict) or entry.get("role") != "assistant":
continue
if str(entry.get("providerID") or "") != "openai":
continue
tokens = entry.get("tokens") or {}
cache = tokens.get("cache") or {}
input_tokens = number(tokens.get("input"))
# opencode keeps thinking tokens out of output; both are generated.
output_tokens = number(tokens.get("output")) + number(tokens.get("reasoning"))
cache_read = number(cache.get("read"))
cache_write = number(cache.get("write"))
if not (input_tokens or output_tokens or cache_read or cache_write):
continue
day = local_day((entry.get("time") or {}).get("created"))
model = model_name(str(entry.get("modelID") or "").rstrip("/").split("/")[-1])
except Exception:
continue
add_usage(day, "opencode:" + str(session_id), model, input_tokens, output_tokens, cache_read, cache_write)
except sqlite3.Error:
# Transient lock, schema migration, corruption: the numbers stop here,
# incomplete.
return False
finally:
conn.close()
return True
def scan_native_codex_sessions():
codex_home = Path(os.environ.get("CODEX_HOME") or (Path.home() / ".codex"))
roots = [codex_home / "sessions", codex_home / "archived_sessions"]
files = []
cutoff = time.time() - 30 * 24 * 60 * 60
for root in roots:
if not root.exists():
continue
for path in root.rglob("*.jsonl"):
try:
if path.stat().st_mtime >= cutoff:
files.append(path)
except OSError:
pass
for path in files:
current_model = "codex"
try:
with path.open(errors="replace") as handle:
for raw in handle:
try:
entry = json.loads(raw)
except Exception:
continue
if entry.get("type") == "turn_context":
payload = entry.get("payload") or {}
current_model = model_name(payload.get("model") or payload.get("model_slug") or current_model)
continue
payload = entry.get("payload") or entry
if entry.get("type") == "response_item" and isinstance(payload, dict):
payload = payload.get("payload") or payload
if not isinstance(payload, dict):
continue
if payload.get("type") != "token_count":
continue
info = payload.get("info") or {}
# total_token_usage is cumulative for the session. Adding every
# snapshot makes usage grow quadratically, so count the last turn.
usage = info.get("last_token_usage") or {}
cache_read = number(usage.get("cached_input_tokens"))
cache_write = number(usage.get("cache_write_input_tokens"))
# Cached tokens are included in input_tokens, and reasoning tokens
# are included in output_tokens. Keep the cache split without
# counting either category twice.
input_tokens = max(0, number(usage.get("input_tokens")) - cache_read - cache_write)
output_tokens = number(usage.get("output_tokens"))
if not (input_tokens or output_tokens or cache_read or cache_write):
continue
day = local_day(entry.get("timestamp") or path.stat().st_mtime)
add_usage(day, str(path), current_model, input_tokens, output_tokens, cache_read, cache_write)
except Exception:
continue
def cache_root():
root = Path(os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache")) / "omarchy" / "agent-usage"
root.mkdir(parents=True, exist_ok=True)
return root
def scan_cache_paths():
codex_home = Path(os.environ.get("CODEX_HOME") or (Path.home() / ".codex"))
db = Path(os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local" / "share")) / "opencode" / "opencode.db"
# The digest covers every data path the scan reads: the codex session
# roots, the opencode DB, and (via Path.home()) the pi/omp session roots.
digest = hashlib.sha1((str(Path.home()) + "\n" + str(codex_home) + "\n" + str(db)).encode("utf-8")).hexdigest()[:16]
root = cache_root()
return root / f"codex-scan-{digest}.json", root / f"codex-scan-{digest}.lock"
def read_fresh_json(path, max_age_seconds):
if max_age_seconds <= 0 or not path.exists():
return None
try:
# A negative age means the mtime is in the future: the clock moved
# backwards since the write, so the cache's freshness cannot be trusted.
age = time.time() - path.stat().st_mtime
if 0 <= age <= max_age_seconds:
return json.loads(path.read_text(encoding="utf-8"))
except Exception:
return None
return None
def write_json(path, payload):
# A temp name unique to this writer, not derived from the target: several
# collectors can run at once (the update command backgrounds one per agent,
# the panel refreshes on its own), and a shared temp path means the second
# replace finds the first one's file already moved away.
handle_fd, tmp_name = tempfile.mkstemp(dir=path.parent, prefix=path.name + ".", suffix=".tmp")
tmp = Path(tmp_name)
try:
with os.fdopen(handle_fd, "w", encoding="utf-8") as handle:
handle.write(json.dumps(payload, separators=(",", ":")) + "\n")
# mkstemp opens at 0600; nothing in the cache is sensitive, so open it
# up to the usual 0644.
tmp.chmod(0o644)
tmp.replace(path)
except BaseException:
tmp.unlink(missing_ok=True)
raise
# The cache payload is a versioned envelope around the local-stats dict, so a
# corrupted or foreign-shaped file is a cache miss (rescan + rewrite) instead
# of a crash or a garbage record.
def read_cached_stats(cache_file, max_age_seconds):
cached = read_fresh_json(cache_file, max_age_seconds)
if not isinstance(cached, dict) or cached.get("schemaVersion") != 1:
return None
# today* fields only mean "today" on the day they were scanned. A cache
# from another local date (midnight passed, or the clock moved) is a miss,
# not merely old, whatever its mtime says.
if cached.get("scanDate") != today:
return None
stats = cached.get("stats")
if not isinstance(stats, dict):
return None
if not all(key in stats for key in ("todayPrompts", "todayTotalTokens", "recentDays", "activeDates", "modelUsage")):
return None
return stats
def write_cached_stats(cache_file, stats):
try:
write_json(cache_file, {"schemaVersion": 1, "scanDate": today, "stats": stats})
except Exception as exc:
print(f"omarchy-agent-usage-codex: could not write usage cache ({exc})", file=sys.stderr)
def local_stats():
"""Snapshot the aggregated local usage into the record's stats dict."""
return {
"todayPrompts": today_prompts,
"todaySessions": len(today_sessions),
"todayTotalTokens": today_total_tokens,
"todayTokensByModel": today_tokens_by_model,
"recentDays": [recent[day] for day in recent_dates],
"totalPrompts": total_prompts,
"totalSessions": len(total_sessions),
# Days with any recorded usage, for the all-time "N days" summary. The
# dates travel too: merging snapshots from several machines needs their
# union, which a count alone cannot give.
"activeDays": len(active_days),
"activeDates": sorted(active_days),
"modelUsage": model_usage,
}
def run_local_scans():
scan_pi_sessions()
scan_native_codex_sessions()
complete = scan_opencode_sessions()
return local_stats(), complete
def cached_local_stats(max_age):
"""Local stats, with the cache as a pure optimization.
The cache must never take the collector down: any cache-layer failure
(unwritable cache root, lock errors, disk full) degrades to a direct scan
and a warning on stderr. The JSON record is the contract; the cache is not.
"""
try:
return _cached_local_stats(max_age)
except Exception as exc:
print(f"omarchy-agent-usage-codex: cache unavailable ({exc}); scanning directly", file=sys.stderr)
stats, _ = run_local_scans()
return stats
def _cached_local_stats(max_age):
cache_file, lock_file = scan_cache_paths()
cached = read_cached_stats(cache_file, max_age)
if cached is not None:
return cached
with lock_file.open("w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
cached = read_cached_stats(cache_file, max_age)
if cached is not None:
return cached
stats, complete = run_local_scans()
# An interrupted scan still serves this run, but caching it would
# suppress the missing usage for every reader until the cache expires.
if complete:
write_cached_stats(cache_file, stats)
return stats
def rpc_request(proc, request_id, method, params=None, timeout=8):
payload = {"id": request_id, "method": method, "params": params or {}}
proc.stdin.write(json.dumps(payload) + "\n")
proc.stdin.flush()
deadline = time.time() + timeout
while time.time() < deadline:
ready, _, _ = select.select([proc.stdout], [], [], 0.25)
if not ready:
continue
line = proc.stdout.readline()
if not line:
break
try:
message = json.loads(line)
except Exception:
continue
if message.get("id") == request_id:
return message
raise TimeoutError(method)
def limit_window(window):
if not isinstance(window, dict):
return None
used = window.get("usedPercent")
if used is None:
return None
mins = number(window.get("windowDurationMins"))
if mins == 10080:
label = "Weekly (7-day)"
elif mins and mins % 60 == 0:
label = f"{mins // 60}h window"
elif mins:
label = f"{mins}m window"
else:
label = "Limit"
reset = window.get("resetsAt")
return {
"label": label,
"percent": float(used) / 100.0,
"resetsAt": datetime.fromtimestamp(number(reset), timezone.utc).isoformat() if reset else "",
}
def fetch_codex_rpc():
result = {"limits": [], "tierLabel": "", "usageStatusText": "", "authHelpText": AUTH_HELP}
codex = find_command("codex")
if not codex:
result["usageStatusText"] = "Codex unavailable"
result["authHelpText"] = "codex not found in PATH"
return result
try:
proc = subprocess.Popen(
[codex, "-s", "read-only", "-a", "on-request", "app-server"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
env=ENV,
)
except Exception as exc:
result["usageStatusText"] = "Codex unavailable"
result["authHelpText"] = str(exc)
return result
try:
rpc_request(proc, 1, "initialize", {"clientInfo": {"name": "omarchy-agent-usage", "version": "1"}}, timeout=8)
proc.stdin.write(json.dumps({"method": "initialized", "params": {}}) + "\n")
proc.stdin.flush()
account_msg = rpc_request(proc, 2, "account/read", timeout=4)
limits_msg = rpc_request(proc, 3, "account/rateLimits/read", timeout=4)
account = (account_msg.get("result") or {}).get("account") or {}
limits = (limits_msg.get("result") or {}).get("rateLimits") or {}
plan = limits.get("planType") or account.get("planType") or account.get("type") or ""
result["tierLabel"] = str(plan) if plan else ""
for window in (limits.get("primary"), limits.get("secondary")):
entry = limit_window(window)
if entry:
result["limits"].append(entry)
except Exception as exc:
result["usageStatusText"] = "Codex limits unavailable"
result["authHelpText"] = str(exc)
finally:
try:
proc.terminate()
proc.wait(timeout=1)
except Exception:
try:
proc.kill()
except Exception:
pass
return result
def main():
parser = argparse.ArgumentParser()
# --force rescans everything and rewrites the cache. --limits-only is kept
# for CLI compatibility with the panel's refreshLimits() call: only the
# limits probe must be fresh, so it may reuse a scan for far longer than a
# normal run, whose short window exists purely to dedup concurrent
# collector runs.
parser.add_argument("--force", action="store_true")
parser.add_argument("--limits-only", action="store_true")
args = parser.parse_args()
max_age = 0 if args.force else (LIMITS_ONLY_REUSE_SECONDS if args.limits_only else SCAN_REUSE_SECONDS)
stats = cached_local_stats(max_age)
rpc = fetch_codex_rpc()
record = {
"schemaVersion": 1,
"id": AGENT_ID,
"name": AGENT_NAME,
"updatedAt": datetime.now(timezone.utc).isoformat(),
"ready": True,
"hasLocalStats": True,
}
record.update(stats)
record.update(rpc)
print(json.dumps(record, separators=(",", ":")))
if __name__ == "__main__":
main()
+511
View File
@@ -0,0 +1,511 @@
#!/usr/bin/python3
# omarchy:summary=Print the Fireworks usage record as JSON
# omarchy:args=[--force] [--limits-only]
# omarchy:hidden=true
"""Collect Fireworks serverless usage into one display-ready JSON record.
Token stats come from the Fireworks billing API grouped by day and model for
the last 30 days. Fireworks does not expose its prepaid ledger, so the record
carries an estimated balance instead of rate limits: the credits configured in
~/.config/omarchy/agents/fireworks.json minus rated account costs since the
funding date. The agents panel only ever reads the JSON this prints.
"""
from __future__ import annotations
import argparse
import configparser
import json
import os
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import date, datetime, time, timedelta, timezone
from decimal import Decimal, InvalidOperation
from pathlib import Path
from typing import Any
AGENT_ID = "fireworks"
AGENT_NAME = "Fireworks"
AUTH_HELP = "Set FIREWORKS_API_KEY, run `firectl set-api-key`, or sign in to Fireworks in opencode."
API_BASE_URL = "https://api.fireworks.ai"
class FireworksError(Exception):
pass
def number(value: Any) -> int:
try:
return max(0, round(float(value or 0)))
except (TypeError, ValueError):
return 0
def money_value(value: Any) -> Decimal:
if not isinstance(value, dict):
return Decimal("0")
try:
units = Decimal(str(value.get("units", 0) or 0))
nanos = Decimal(str(value.get("nanos", 0) or 0)) / Decimal("1000000000")
return units + nanos
except (InvalidOperation, TypeError, ValueError):
return Decimal("0")
def model_id(row: dict[str, Any]) -> str:
group = row.get("group") if isinstance(row.get("group"), dict) else {}
raw = group.get("model_name") or row.get("modelName") or "unknown"
name = str(raw).rstrip("/").split("/")[-1] or "unknown"
return re.sub(r"(?<=\d)p(?=\d)", ".", name)
def row_date(row: dict[str, Any]) -> str:
# The query asks for day buckets in the local timezone, but the API reports
# each bucket's boundary in UTC: local Aug 7 starts at Aug 6 22:00Z east of
# Greenwich. Convert back to local time to recover the day the bucket names —
# taking the raw date prefix would file every day under its predecessor.
raw = str(row.get("startTime") or "")
if not raw:
return ""
try:
parsed = datetime.fromisoformat(raw.replace("Z", "+00:00"))
except ValueError:
return raw[:10] if len(raw) >= 10 else ""
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed.astimezone().date().isoformat()
def empty_bucket() -> dict[str, int]:
return {
"inputTokens": 0,
"outputTokens": 0,
"cacheReadInputTokens": 0,
"cacheCreationInputTokens": 0,
}
def empty_stats() -> dict[str, Any]:
return {
"todayPrompts": 0,
"todaySessions": 0,
"todayTotalTokens": 0,
"todayTokensByModel": {},
"recentDays": [],
"totalPrompts": 0,
"totalSessions": 0,
"activeDays": 0,
"activeDates": [],
"modelUsage": {},
}
def base_record(**overrides: Any) -> dict[str, Any]:
record: dict[str, Any] = {
"schemaVersion": 1,
"id": AGENT_ID,
"name": AGENT_NAME,
"updatedAt": datetime.now(timezone.utc).isoformat(),
"ready": False,
"hasLocalStats": False,
# Billing-API numbers are account-global, not machine-local: every synced
# device reports the same truth, so aggregation must not sum them.
"scope": "account",
# The billing API reports tokens, never prompt or session counts; the
# panel keeps those numbers out of today's tooltip when this is false.
"hasPromptStats": False,
"tierLabel": "Prepaid",
"usageStatusText": "",
"authHelpText": "",
"limits": [],
}
record.update(empty_stats())
record.update(overrides)
return record
def summarize_usage(payload: dict[str, Any], today: date | None = None) -> dict[str, Any]:
today = today or datetime.now().astimezone().date()
recent_dates = [(today - timedelta(days=offset)).isoformat() for offset in range(6, -1, -1)]
recent = {day: 0 for day in recent_dates}
today_by_model: dict[str, int] = {}
model_usage: dict[str, dict[str, int]] = {}
active_dates: set[str] = set()
rows = payload.get("serverlessCosts")
if not isinstance(rows, list):
rows = []
for raw_row in rows:
if not isinstance(raw_row, dict):
continue
day = row_date(raw_row)
model = model_id(raw_row)
prompt = number(raw_row.get("promptTokens"))
cached = min(prompt, number(raw_row.get("cachedPromptTokens")))
uncached = number(raw_row.get("uncachedPromptTokens"))
if "uncachedPromptTokens" not in raw_row:
uncached = max(0, prompt - cached)
output = number(raw_row.get("completionTokens"))
total = uncached + cached + output
if total <= 0:
continue
bucket = model_usage.setdefault(model, empty_bucket())
bucket["inputTokens"] += uncached
bucket["outputTokens"] += output
bucket["cacheReadInputTokens"] += cached
if day:
active_dates.add(day)
if day in recent:
recent[day] += total
if day == today.isoformat():
today_by_model[model] = today_by_model.get(model, 0) + total
return {
"todayTotalTokens": sum(today_by_model.values()),
"todayTokensByModel": today_by_model,
"recentDays": [{"date": day, "messageCount": recent[day]} for day in recent_dates],
"activeDays": len(active_dates),
"activeDates": sorted(active_dates),
"modelUsage": model_usage,
}
def read_auth_file(path: Path) -> tuple[str, str]:
if not path.is_file():
return "", ""
parser = configparser.ConfigParser(interpolation=None)
try:
parser.read(path)
except configparser.Error:
return "", ""
api_key = ""
account_id = ""
sections = [parser.defaults()]
sections.extend(parser[section] for section in parser.sections())
for values in sections:
api_key = api_key or str(values.get("api_key", values.get("api-key", ""))).strip()
account_id = account_id or str(values.get("account_id", values.get("account-id", ""))).strip()
return api_key, account_id
def opencode_auth_path() -> Path:
data_home = Path(os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local" / "share"))
return data_home / "opencode" / "auth.json"
def read_opencode_key(path: Path) -> str:
try:
parsed = json.loads(path.read_text())
except (OSError, json.JSONDecodeError):
return ""
entry = parsed.get("fireworks-ai") if isinstance(parsed, dict) else None
if not isinstance(entry, dict):
return ""
return str(entry.get("key") or "").strip()
def config_path() -> Path:
config_home = Path(os.environ.get("XDG_CONFIG_HOME") or (Path.home() / ".config"))
return config_home / "omarchy" / "agents" / "fireworks.json"
def read_config() -> dict[str, Any]:
try:
parsed = json.loads(config_path().read_text())
return parsed if isinstance(parsed, dict) else {}
except (OSError, json.JSONDecodeError):
return {}
def credentials(auth_path: Path, config: dict[str, Any]) -> tuple[str, str]:
file_key, file_account = read_auth_file(auth_path)
# opencode is the last resort: an explicit key or a firectl login should
# win over whatever another tool happens to be signed in with.
api_key = (
str(os.environ.get("FIREWORKS_API_KEY", "")).strip()
or file_key
or read_opencode_key(opencode_auth_path())
)
account_id = (
str(os.environ.get("FIREWORKS_ACCOUNT_ID", "")).strip()
or str(config.get("accountId") or "").strip()
or file_account
)
return api_key, account_id
def normalize_account_id(value: str) -> str:
return str(value or "").strip().removeprefix("accounts/").strip("/")
def timezone_name() -> str:
configured = str(os.environ.get("TZ", "")).strip()
if configured:
return configured
try:
target = (Path("/etc/localtime").resolve()).as_posix()
marker = "/zoneinfo/"
if marker in target:
return target.split(marker, 1)[1]
except OSError:
pass
return "UTC"
def local_midnight_utc(day: date) -> str:
# The API buckets by the requested timezone, so the window must run between
# local midnights — expressed in UTC, since a bare date with a Z suffix
# shifts the window by the UTC offset and clips today's tail west of
# Greenwich.
return datetime.combine(day, time.min).astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
def iso_timestamp(value: str) -> str:
raw = str(value or "").strip()
if not raw:
return ""
try:
if len(raw) == 10:
parsed = datetime.combine(date.fromisoformat(raw), time.min, tzinfo=timezone.utc)
else:
parsed = datetime.fromisoformat(raw.replace("Z", "+00:00"))
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
except ValueError:
raise FireworksError("Fireworks fundedAt must be an ISO date such as 2026-07-01")
class FireworksClient:
def __init__(self, api_key: str, base_url: str = API_BASE_URL):
self.api_key = api_key
self.base_url = base_url.rstrip("/")
def request(
self,
path: str,
query: dict[str, Any] | None = None,
body: dict[str, Any] | None = None,
) -> dict[str, Any]:
url = self.base_url + path
if query:
url += "?" + urllib.parse.urlencode(query, doseq=True)
data = None if body is None else json.dumps(body).encode("utf-8")
request = urllib.request.Request(
url,
data=data,
method="POST" if body is not None else "GET",
headers={
"Authorization": "Bearer " + self.api_key,
"Accept": "application/json",
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
decoded = json.load(response)
return decoded if isinstance(decoded, dict) else {}
except urllib.error.HTTPError as error:
if error.code == 401:
raise FireworksError("Fireworks rejected the API key")
if error.code == 403:
raise FireworksError("The Fireworks API key cannot read billing data")
if error.code == 404:
raise FireworksError("Fireworks account not found")
raise FireworksError(f"Fireworks API returned HTTP {error.code}")
except urllib.error.URLError as error:
raise FireworksError("Could not reach the Fireworks API") from error
except (json.JSONDecodeError, TimeoutError) as error:
raise FireworksError("Fireworks returned an invalid billing response") from error
def discover_account(self) -> tuple[str, dict[str, Any]]:
payload = self.request("/v1/accounts", query={"pageSize": 100})
accounts = [item for item in payload.get("accounts", []) if isinstance(item, dict)]
if len(accounts) == 1:
account = accounts[0]
return normalize_account_id(str(account.get("name") or "")), account
if not accounts:
raise FireworksError("No Fireworks account is available for this API key")
raise FireworksError("Set accountId in fireworks.json when the API key can access multiple accounts")
def account(self, account_id: str) -> dict[str, Any]:
quoted = urllib.parse.quote(normalize_account_id(account_id), safe="")
return self.request(f"/v1/accounts/{quoted}")
def usage(self, account_id: str, start_day: date, end_day: date) -> dict[str, Any]:
quoted = urllib.parse.quote(normalize_account_id(account_id), safe="")
query = {
"startTime": local_midnight_utc(start_day),
"endTime": local_midnight_utc(end_day),
"usageType": "SERVERLESS",
"timezone": timezone_name(),
"groupBy": ["model_name"],
}
# 30 days grouped by model can exceed one page; follow the continuation
# tokens or heavy accounts lose their tail. The bound is a runaway stop.
rows: list[Any] = []
for _ in range(20):
payload = self.request(f"/v1/accounts/{quoted}/billingUsage", query=query)
page = payload.get("serverlessCosts")
if isinstance(page, list):
rows.extend(page)
token = str(payload.get("nextPageToken") or "")
if not token:
break
query = dict(query, pageToken=token)
return {"serverlessCosts": rows}
def spent(self, account_id: str, start_at: str, end_at: str) -> Decimal:
quoted = urllib.parse.quote(normalize_account_id(account_id), safe="")
body = {
"startTime": start_at,
"endTime": end_at,
"scope": "ACCOUNT",
}
try:
payload = self.request(f"/v1/accounts/{quoted}/usageCosts:query", body=body)
if not isinstance(payload.get("subtotal"), dict):
raise FireworksError("Fireworks cost response did not include a subtotal")
return money_value(payload.get("subtotal"))
except FireworksError:
parsed_end = datetime.fromisoformat(end_at.replace("Z", "+00:00"))
summary_end = (parsed_end.date() + timedelta(days=1)).isoformat() + "T00:00:00Z"
payload = self.request(
f"/v1/accounts/{quoted}/billing/summary",
query={"startTime": start_at, "endTime": summary_end},
)
return sum(
(money_value(item.get("totalCost")) for item in payload.get("lineItems", []) if isinstance(item, dict)),
Decimal("0"),
)
def live_balance(client: FireworksClient, account_id: str) -> Decimal | None:
# accounts/{id}:getBalance exists but is permission-gated: keys without the
# billing role get PERMISSION_DENIED, and then the configured estimate below
# is the best we can do. The response shape is undocumented, so accept a
# Money object at the top level or under any plausible field name.
quoted = urllib.parse.quote(normalize_account_id(account_id), safe="")
try:
payload = client.request(f"/v1/accounts/{quoted}:getBalance")
except FireworksError:
return None
candidates = [payload] + [payload.get(field) for field in ("balance", "creditBalance", "prepaidBalance", "amount")]
for value in candidates:
if isinstance(value, dict) and ("units" in value or "nanos" in value):
return money_value(value)
return None
def estimated_balance(
client: FireworksClient,
account_id: str,
account: dict[str, Any],
config: dict[str, Any],
) -> dict[str, Any] | None:
try:
funded = Decimal(str(config.get("fundedAmount") or "0"))
except InvalidOperation:
raise FireworksError("Fireworks fundedAmount must be a number")
if not funded.is_finite():
raise FireworksError("Fireworks fundedAmount must be a finite number")
if funded <= 0:
return None
funded_at = iso_timestamp(str(config.get("fundedAt") or ""))
if not funded_at:
if not account:
account = client.account(account_id)
funded_at = iso_timestamp(str(account.get("createTime") or ""))
if not funded_at:
raise FireworksError("Set fundedAt because the Fireworks account creation date is unavailable")
end_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
spent = max(Decimal("0"), client.spent(account_id, funded_at, end_at))
return {
"remaining": float(max(Decimal("0"), funded - spent)),
"funded": float(funded),
"spent": float(spent),
"currency": "USD",
"estimated": True,
}
def scan(api_base_url: str, auth_path: Path) -> dict[str, Any]:
config = read_config()
api_key, account_id = credentials(auth_path, config)
if not api_key:
return base_record(usageStatusText="Fireworks unavailable", authHelpText=AUTH_HELP)
client = FireworksClient(api_key, api_base_url)
account: dict[str, Any] = {}
if account_id:
account_id = normalize_account_id(account_id)
else:
account_id, account = client.discover_account()
today = datetime.now().astimezone().date()
usage = client.usage(account_id, today - timedelta(days=29), today + timedelta(days=1))
record = base_record(ready=True, hasLocalStats=True)
record.update(summarize_usage(usage, today))
live = live_balance(client, account_id)
if live is not None:
try:
funded = Decimal(str(config.get("fundedAmount") or "0"))
if not funded.is_finite() or funded < 0:
funded = Decimal("0")
except InvalidOperation:
funded = Decimal("0")
record["balance"] = {
"remaining": float(live),
"funded": float(funded),
"spent": float(max(Decimal("0"), funded - live)),
"currency": "USD",
"estimated": False,
}
return record
try:
balance = estimated_balance(client, account_id, account, config)
if balance:
record["balance"] = balance
except FireworksError as error:
record["usageStatusText"] = "Balance unavailable"
record["authHelpText"] = str(error)
return record
def main() -> int:
parser = argparse.ArgumentParser(description="Print the Fireworks usage record as JSON")
# Stats and balance come from the same few API calls, so there is no cache
# to force past and no faster limits-only path. The flags exist so every
# collector accepts the same invocation.
parser.add_argument("--force", action="store_true")
parser.add_argument("--limits-only", action="store_true")
parser.add_argument("--auth-path", default=os.environ.get("FIREWORKS_AUTH_PATH", "~/.fireworks/auth.ini"))
parser.add_argument("--api-base-url", default=os.environ.get("FIREWORKS_API_BASE_URL", API_BASE_URL))
args = parser.parse_args()
try:
record = scan(args.api_base_url, Path(args.auth_path).expanduser())
except FireworksError as error:
record = base_record(usageStatusText="Fireworks unavailable", authHelpText=str(error))
except Exception as error:
record = base_record(usageStatusText="Fireworks unavailable", authHelpText="Fireworks usage scan failed")
print(f"omarchy-agent-usage-fireworks: {type(error).__name__}", file=sys.stderr)
print(json.dumps(record, separators=(",", ":")))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+68
View File
@@ -0,0 +1,68 @@
#!/bin/bash
# omarchy:summary=Regenerate the AI agent usage data files
# omarchy:args=[--force] [--limits-only] [--except <agent>] [agent...]
# omarchy:examples=omarchy agent usage-update | omarchy agent usage-update claude | omarchy agent usage-update --except codex
# Each omarchy-agent-usage-<agent> collector prints one display-ready JSON
# record; this writes them to ~/.local/state/omarchy/agents/usage/ where the
# agents panel watches them. Adding an agent is adding a collector — the
# panel picks up any record that appears here.
USAGE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/agents/usage"
mkdir -p "$USAGE_DIR"
flags=()
only=()
declare -A excluded
while (( $# > 0 )); do
case "$1" in
--force | --limits-only) flags+=("$1") ;;
--except)
excluded[$2]=1
shift
;;
*) only+=("$1") ;;
esac
shift
done
wanted() {
local agent="$1"
[[ -n ${excluded[$agent]} ]] && return 1
(( ${#only[@]} == 0 )) && return 0
local candidate
for candidate in "${only[@]}"; do
[[ $candidate == "$agent" ]] && return 0
done
return 1
}
collect() {
local collector="$1" agent="$2"
local record tmp
if ! record=$("$collector" "${flags[@]}") || [[ -z $record ]] || ! jq -e . >/dev/null 2>&1 <<<"$record"; then
echo "omarchy-agent-usage-update: $agent collector failed" >&2
return 1
fi
tmp=$(mktemp "$USAGE_DIR/.$agent.XXXXXX")
printf '%s\n' "$record" >"$tmp"
mv "$tmp" "$USAGE_DIR/$agent.json"
}
pids=()
for collector in "$OMARCHY_PATH"/bin/omarchy-agent-usage-*; do
[[ -x $collector ]] || continue
agent="${collector##*/omarchy-agent-usage-}"
[[ $agent == "update" ]] && continue
wanted "$agent" || continue
collect "$collector" "$agent" &
pids+=($!)
done
status=0
for pid in "${pids[@]}"; do
wait "$pid" || status=1
done
exit $status
+75
View File
@@ -0,0 +1,75 @@
#!/bin/bash
# omarchy:summary=Apply Omarchy hardware-specific packages and system configuration
# omarchy:group=apply
# omarchy:requires-sudo=true
# omarchy:examples=omarchy apply hardware --install-user dhh
# omarchy:hidden=true
set -euo pipefail
usage() {
cat <<USAGE
Usage: omarchy apply hardware --install-user USER
omarchy apply hardware --defer-provisioning
Runs idempotent hardware detection/setup for the installed machine. This is
called by omarchy-apply-system during ISO finalization and can be rerun later
for diagnostics or after hardware/package updates.
--defer-provisioning runs without an install user (deferred-provisioning installs create the user at
first boot); group grants are recorded in /var/lib/omarchy/provisioning/groups.
USAGE
}
install_user="${OMARCHY_INSTALL_USER:-}"
defer_provisioning=0
while (($#)); do
case "$1" in
--install-user)
install_user="${2:-}"
shift 2
;;
--defer-provisioning)
defer_provisioning=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
esac
done
if (( EUID != 0 )); then
echo "Error: omarchy-apply-hardware must run as root" >&2
exit 1
fi
if (( defer_provisioning )); then
install_user=""
else
if [[ -z $install_user || $install_user == "root" ]]; then
echo "Error: --install-user must name the target non-root user" >&2
exit 1
fi
if ! getent passwd "$install_user" >/dev/null; then
echo "Error: user '$install_user' does not exist" >&2
exit 1
fi
fi
export OMARCHY_INSTALL_USER="$install_user"
export OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
export OMARCHY_INSTALL="${OMARCHY_INSTALL:-$OMARCHY_PATH/install}"
export OMARCHY_INSTALL_LOG_FILE="${OMARCHY_INSTALL_LOG_FILE:-/var/log/omarchy-install.log}"
export PATH="$OMARCHY_PATH/bin:$PATH"
source "$OMARCHY_INSTALL/helpers/logging.sh"
source "$OMARCHY_INSTALL/hardware/all.sh"
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
# omarchy:summary=Configure Quickshell lock screen authentication
# omarchy:requires-sudo=true
# omarchy:hidden=true
set -e
target_user=${OMARCHY_INSTALL_USER:-${SUDO_USER:-}}
if [[ -z $target_user && -n ${PKEXEC_UID:-} ]]; then
target_user=$(getent passwd "$PKEXEC_UID" | cut -d: -f1)
fi
target_user=${target_user:-$USER}
as_root() {
if (( EUID == 0 )); then
"$@"
else
sudo "$@"
fi
}
echo "Configuring lock screen password authentication..."
as_root tee /etc/pam.d/omarchy-lock-password >/dev/null <<'EOF'
#%PAM-1.0
auth required pam_faillock.so preauth silent deny=10 unlock_time=120
-auth [success=2 default=ignore] pam_systemd_home.so
auth [success=1 default=bad] pam_unix.so try_first_pass nullok
auth [default=die] pam_faillock.so authfail deny=10 unlock_time=120
auth optional pam_permit.so
auth required pam_env.so
auth required pam_faillock.so authsucc
account include system-local-login
EOF
if omarchy-cmd-present fprintd-list && fprintd-list "$target_user" 2>/dev/null | grep -qi finger; then
echo "Configuring lock screen fingerprint authentication..."
as_root tee /etc/pam.d/omarchy-lock-fingerprint >/dev/null <<'EOF'
#%PAM-1.0
auth required pam_fprintd.so
account include system-local-login
EOF
else
as_root rm -f /etc/pam.d/omarchy-lock-fingerprint
fi
# omarchy-shell can't reach a running shell during chroot install. The echo
# is just confirmation, so swallow the failure rather than letting it become
# the script's exit code.
if omarchy-shell lock status >/dev/null 2>&1; then
echo "Lock screen authentication configured."
fi
+102
View File
@@ -0,0 +1,102 @@
#!/bin/bash
# omarchy:summary=Apply Omarchy system setup in the installed target
# omarchy:group=apply
# omarchy:requires-sudo=true
# omarchy:examples=omarchy apply system --install-user dhh --first-install
# omarchy:hidden=true
set -euo pipefail
usage() {
cat <<USAGE
Usage: omarchy apply system --install-user USER [--first-install|--upgrade]
omarchy apply system --defer-provisioning --first-install
Runs root-owned Omarchy system setup. The ISO calls this in the target chroot
after packages are installed and the target user exists. It also calls
omarchy-apply-hardware, so hardware setup cannot be accidentally skipped.
--defer-provisioning runs without an install user: deferred-provisioning installs create the user at
first boot, so scripts that grant group memberships record them in
/var/lib/omarchy/provisioning/groups instead of calling usermod.
USAGE
}
install_user="${OMARCHY_INSTALL_USER:-}"
first_install=0
upgrade=0
defer_provisioning=0
while (($#)); do
case "$1" in
--install-user)
install_user="${2:-}"
shift 2
;;
--defer-provisioning)
defer_provisioning=1
shift
;;
--first-install)
first_install=1
shift
;;
--upgrade)
upgrade=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
esac
done
if (( EUID != 0 )); then
echo "Error: omarchy-apply-system must run as root" >&2
exit 1
fi
if (( defer_provisioning )); then
install_user=""
else
if [[ -z $install_user || $install_user == "root" ]]; then
echo "Error: --install-user must name the target non-root user" >&2
exit 1
fi
if ! getent passwd "$install_user" >/dev/null; then
echo "Error: user '$install_user' does not exist" >&2
exit 1
fi
fi
export OMARCHY_INSTALL_USER="$install_user"
export OMARCHY_FIRST_INSTALL="$first_install"
export OMARCHY_UPGRADE="$upgrade"
export OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
export OMARCHY_INSTALL="${OMARCHY_INSTALL:-$OMARCHY_PATH/install}"
export OMARCHY_INSTALL_LOG_FILE="${OMARCHY_INSTALL_LOG_FILE:-/var/log/omarchy-install.log}"
export PATH="$OMARCHY_PATH/bin:$PATH"
source "$OMARCHY_INSTALL/helpers/logging.sh"
start_install_log
source "$OMARCHY_INSTALL/config/all.sh"
if (( defer_provisioning )); then
omarchy-apply-hardware --defer-provisioning
else
omarchy-apply-hardware --install-user "$install_user"
fi
source "$OMARCHY_INSTALL/login/all.sh"
source "$OMARCHY_INSTALL/post-install/all.sh"
stop_install_log
+1217
View File
File diff suppressed because it is too large. Load diff
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# omarchy:summary=Toggle microphone mute. Drives the hardware mic-mute LED on laptops that expose one.
wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle >/dev/null
if wpctl get-volume @DEFAULT_AUDIO_SOURCE@ | grep -q MUTED; then
omarchy-brightness-keyboard-mute on
omarchy-osd -i microphone-muted -m "Microphone muted"
else
omarchy-brightness-keyboard-mute off
omarchy-osd -i microphone -m "Microphone on"
fi
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# omarchy:summary=Set the default audio input and move active streams
# omarchy:args=<node-id> <source-name>
# omarchy:examples=omarchy audio input set default 43 alsa_input.pci-0000_00_1f.3.analog-stereo
node_id=${1:-}
source_name=${2:-}
if [[ -z $node_id || -z $source_name ]]; then
echo "Usage: omarchy-audio-input-set-default <node-id> <source-name>" >&2
exit 1
fi
wpctl set-default "$node_id" 2>/dev/null || true
pactl set-default-source "$source_name" 2>/dev/null || true
pactl list short source-outputs 2>/dev/null | awk '{ print $1 }' | while read -r output; do
[[ -n $output ]] && pactl move-source-output "$output" "$source_name" 2>/dev/null || true
done
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# omarchy:summary=Set the default audio output and move active streams
# omarchy:args=<node-id> <sink-name>
# omarchy:examples=omarchy audio output set default 42 alsa_output.pci-0000_00_1f.3.analog-stereo
node_id=${1:-}
sink_name=${2:-}
if [[ -z $node_id || -z $sink_name ]]; then
echo "Usage: omarchy-audio-output-set-default <node-id> <sink-name>" >&2
exit 1
fi
timeout 2 wpctl set-default "$node_id" 2>/dev/null || true
timeout 2 pactl set-default-sink "$sink_name" 2>/dev/null || true
# Move only real application streams. A DSP filter-chain's own output is also a
# sink input but carries no application.name, and moving it would rewire the
# processing itself -- onto headphones, or into its own virtual sink, which is a
# cycle. EasyEffects' output stream must stay put for the same reason.
timeout 2 pactl list sink-inputs 2>/dev/null | awk '
/^Sink Input #/ {id = substr($3, 2)}
/application\.name = / {
app = $0
sub(/.*application\.name = "/, "", app)
sub(/"$/, "", app)
if (app != "EasyEffects") print id
}' | while read -r input; do
[[ -n $input ]] && timeout 2 pactl move-sink-input "$input" "$sink_name" 2>/dev/null || true
done
+55
View File
@@ -0,0 +1,55 @@
#!/bin/bash
# omarchy:summary=Print the sink whose volume and mute a given output really uses
# omarchy:args=[sink-name]
# omarchy:group=audio
# omarchy:examples=omarchy audio output sink | omarchy audio output sink omarchy_speaker_tuning
set -uo pipefail
# A DSP sink -- a speaker tuning filter-chain, or EasyEffects -- can be the
# selected output without being where loudness lives. Changing its volume alters
# the level going *into* the processing: the display moves while the speakers do
# not, and on a chain with a compressor or limiter the tone changes too. Resolve
# through it to the physical sink it feeds.
#
# With no argument this resolves the current default output, so when headphones or
# HDMI are selected it returns those, not the speakers a tuning happens to front.
# Callers that need to describe some *other* output -- an output switcher naming
# the next one in the rotation -- pass that sink explicitly.
sink="${1:-$(pactl get-default-sink 2>/dev/null)}"
if [[ -z $sink || $sink == alsa_output.* ]]; then
printf '%s\n' "$sink"
exit 0
fi
# A DSP sink feeds its physical output through a stream of its own; follow that
# stream down to the sink underneath.
downstream="$(pactl list sink-inputs 2>/dev/null |
awk -v virt="$sink" '
/^Sink Input #/ {target = ""}
/^[[:space:]]*Sink:/ {target = $2}
/node\.name = / {
name = $0
sub(/.*node\.name = "/, "", name)
sub(/"$/, "", name)
if (index(name, virt) == 1 && target != "") {print target; exit}
}
/application\.name = "EasyEffects"/ {
if (virt == "easyeffects_sink" && target != "") {print target; exit}
}')"
if [[ -n $downstream ]]; then
name="$(pactl list sinks short 2>/dev/null |
awk -v id="$downstream" '$1 == id {print $2; exit}')"
if [[ -n $name ]]; then
printf '%s\n' "$name"
exit 0
fi
fi
# Nothing resolvable downstream -- the DSP sink may simply be idle and unlinked.
# Fall back to the sink itself so callers still have something to act on.
printf '%s\n' "$sink"
+59
View File
@@ -0,0 +1,59 @@
#!/bin/bash
# omarchy:summary=Switch between audio outputs while preserving the mute status
# Skip the physical sink an active speaker tuning fronts: rotating onto it would
# silently bypass the tuning rather than pick a different output.
fronted=$(omarchy-audio-tuning fronted-sink 2>/dev/null || true)
sinks=$(timeout 2 pactl -f json list sinks |
jq --arg fronted "$fronted" '[.[]
| select((.ports | length == 0) or ([.ports[]? | .availability != "not available"] | any))
| select($fronted == "" or .name != $fronted)]')
sinks_count=$(jq 'length' <<<"$sinks")
if (( sinks_count == 0 )); then
omarchy-osd -m "No audio devices found"
exit 1
fi
current_sink_name=$(timeout 2 pactl get-default-sink)
current_sink_index=$(jq -r --arg name "$current_sink_name" 'map(.name) | index($name)' <<<"$sinks")
if [[ $current_sink_index != "null" ]]; then
next_sink_index=$(((current_sink_index + 1) % sinks_count))
else
next_sink_index=0
fi
next_sink=$(jq -c ".[$next_sink_index]" <<<"$sinks")
next_sink_name=$(jq -r '.name' <<<"$next_sink")
next_sink_description=$(jq -r '.description // .properties."device.description" // .name' <<<"$next_sink")
# A tuning sink sits at a fixed 100% and unmuted while real loudness lives on the
# physical sink beneath it, so read the level from whichever sink actually carries
# it or the OSD contradicts the volume keys.
next_sink_effective=$(omarchy-audio-output-sink "$next_sink_name")
next_sink_volume=$(timeout 2 pactl get-sink-volume "$next_sink_effective" 2>/dev/null |
awk 'NR == 1 {for (i = 1; i <= NF; i++) if ($i ~ /%$/) {sub("%", "", $i); print $i; exit}}')
[[ -n $next_sink_volume ]] || next_sink_volume=$(jq -r '.volume | to_entries[0].value.value_percent | sub("%"; "") | tonumber' <<<"$next_sink")
if [[ $(timeout 2 pactl get-sink-mute "$next_sink_effective" 2>/dev/null) == *yes ]]; then
next_sink_is_muted=true
else
next_sink_is_muted=false
fi
if [[ $next_sink_is_muted == "true" ]] || (( next_sink_volume == 0 )); then
icon_state="muted"
elif (( next_sink_volume <= 33 )); then
icon_state="low"
elif (( next_sink_volume <= 66 )); then
icon_state="medium"
else
icon_state="high"
fi
if [[ $next_sink_name != $current_sink_name ]]; then
omarchy-audio-output-set-default "$(jq -r '.index' <<<"$next_sink")" "$next_sink_name"
fi
omarchy-osd -i "volume-${icon_state}" -m "$next_sink_description"
+86
View File
@@ -0,0 +1,86 @@
#!/bin/bash
# omarchy:summary=Adjust output volume and show the Omarchy OSD
# omarchy:args=<raise|lower|mute-toggle|+N|-N>
# omarchy:examples=omarchy audio output volume raise | omarchy audio output volume lower | omarchy audio output volume mute-toggle | omarchy audio output volume +1
action="${1:-}"
if [[ -z $action ]]; then
echo "Usage: omarchy-audio-output-volume <raise|lower|mute-toggle|+N|-N>"
exit 1
fi
# Resolve through any DSP sink to the physical one, so the keys always move real
# loudness and the processing always sees full-scale input. Shared with the audio
# panel and the output switcher.
sink="$(omarchy-audio-output-sink)"
if [[ -z $sink ]]; then
echo "Could not resolve an audio sink to control." >&2
exit 1
fi
# pactl reports the same percentage scale wpctl does (both are the raw volume
# over PA_VOLUME_NORM), so the OSD reads identically either way.
volume_percent() {
pactl get-sink-volume "$sink" 2>/dev/null |
awk 'NR == 1 {
for (i = 1; i <= NF; i++)
if ($i ~ /%$/) {sub("%", "", $i); print $i; exit}
}'
}
volume_muted() {
[[ $(pactl get-sink-mute "$sink" 2>/dev/null) == *yes ]]
}
case "$action" in
raise) action="+5" ;;
lower) action="-5" ;;
esac
if [[ $action == "mute-toggle" ]]; then
runtime_dir="${XDG_RUNTIME_DIR:-/tmp}"
debounce_file="$runtime_dir/omarchy-audio-output-volume-mute-toggle.last"
now=$(date +%s%3N)
last=0
[[ -r $debounce_file ]] && read -r last <"$debounce_file" || true
if ((now - last < 250)); then
exit 0
fi
printf '%s\n' "$now" >"$debounce_file"
pactl set-sink-mute "$sink" toggle
elif [[ $action =~ ^([+-])([0-9]+)$ ]]; then
direction="${BASH_REMATCH[1]}"
step="${BASH_REMATCH[2]}"
current="$(volume_percent)"
if [[ -z $current ]]; then
echo "Could not read volume for $sink." >&2
exit 1
fi
if [[ $direction == "+" ]]; then
next=$((current + step))
((next <= 100)) || next=100
else
next=$((current - step))
((next >= 0)) || next=0
fi
pactl set-sink-mute "$sink" 0
pactl set-sink-volume "$sink" "${next}%"
else
echo "Unknown volume action: $action"
exit 1
fi
percent=$(volume_percent)
if volume_muted || ((${percent:-0} == 0)); then
icon="volume-muted"
else
icon="volume-high"
fi
omarchy-osd -i "$icon" -p "${percent:-0}"
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
# omarchy:summary=Print PulseAudio sink availability for the shell
# omarchy:group=audio
# A speaker tuning is a virtual sink in front of the real speakers. Both exist
# in the graph, but selecting the physical one would only bypass the tuning, so
# report it unavailable and keep it out of the output list.
fronted="$(omarchy-audio-tuning fronted-sink 2>/dev/null || true)"
pactl list sinks 2>/dev/null | awk -v fronted="$fronted" '
function emit_sink() {
if (name == "") return
if (fronted != "" && name == fronted) {
print name "\t0"
return
}
print name "\t" ((port_count == 0 || available) ? 1 : 0)
}
/^Sink #/ {
emit_sink()
name = ""
in_ports = 0
port_count = 0
available = 0
next
}
/^[[:space:]]*Name:/ {
name = $2
next
}
/^[[:space:]]*Ports:$/ {
in_ports = 1
next
}
in_ports && /^\tActive Port:/ {
in_ports = 0
next
}
in_ports && /^\t\t/ {
port_count++
if ($0 !~ /not available/) available = 1
next
}
END {
emit_sink()
}
'
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# omarchy:summary=Cycle to the next media source and transfer playback when the current source is playing
# omarchy:args=[next|previous]
# omarchy:examples=omarchy audio source switch | omarchy-audio-source-switch previous
direction="${1:-next}"
case "$direction" in
next)
omarchy-shell media sourceSwitch
;;
previous)
omarchy-shell media sourceSwitchPrevious
;;
*)
echo "Usage: omarchy-audio-source-switch [next|previous]" >&2
exit 1
;;
esac
+357
View File
@@ -0,0 +1,357 @@
#!/bin/bash
# omarchy:summary=Manage the speaker tuning for this laptop
# omarchy:args=<on|off|status|match|fronted-sink> [--force]
# omarchy:group=audio
# omarchy:examples=omarchy audio tuning status | omarchy audio tuning on | omarchy audio tuning off
set -uo pipefail
tunings_dir="$OMARCHY_PATH/default/audio/tunings"
config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
# The tuning is hosted by its own PipeWire client, under its own config name, so
# switching it needs no audio restart -- a restart drops every PulseAudio client's
# connection, and applications that do not reconnect (Spotify) then have to be
# restarted by hand. The name is deliberately not PipeWire's stock
# filter-chain.conf, which merges every fragment in filter-chain.conf.d/ and would
# make this service host unrelated user filters too.
host_config_name=omarchy-speaker-tuning.conf
host_config="$config_home/pipewire/$host_config_name"
host_source="$OMARCHY_PATH/default/audio/filter-chain-host.conf"
fragment="$config_home/pipewire/$host_config_name.d/90-tuning.conf"
unit_name=omarchy-speaker-tuning.service
unit="$config_home/systemd/user/$unit_name"
unit_source="$OMARCHY_PATH/default/systemd/user/$unit_name"
# Earlier revisions loaded the tuning into the daemon, as a WirePlumber smart
# filter, or into the shared filter-chain.conf.d namespace. Remove all three so
# they cannot be loaded alongside the current one.
stale_daemon="$config_home/pipewire/pipewire.conf.d/90-omarchy-speaker-tuning.conf"
stale_wireplumber="$config_home/wireplumber/wireplumber.conf.d/90-omarchy-speaker-tuning.conf"
stale_shared="$config_home/pipewire/filter-chain.conf.d/90-omarchy-speaker-tuning.conf"
sink_name=omarchy_speaker_tuning
action="${1:-status}"
force=0
[[ ${2:-} == "--force" ]] && force=1
sink_matching() {
pactl list sinks short 2>/dev/null | awk -v p="$1" '$2 ~ p {print $2; exit}'
}
# Dell keys its Cirrus speaker firmware on the DMI product SKU, which makes it the
# most precise identifier available for these machines -- narrower than a product
# name, and it distinguishes models whose names differ only by marketing. Compared
# case-insensitively against an exact SKU, never a substring, so a tuning cannot
# accidentally widen to a whole product line.
sku_matches() {
local sku want
sku="$(cat /sys/class/dmi/id/product_sku 2>/dev/null)"
[[ -n $sku ]] || return 1
for want in "$@"; do
[[ ${sku,,} == "${want,,}" ]] && return 0
done
return 1
}
dmi_matches() {
local want
for want in "$@"; do
omarchy-hw-match "$want" 2>/dev/null && return 0
done
return 1
}
# Print the tuning directory matching this laptop, if any. Matching is data, not
# code: a tuning declares the DMI string it belongs to and the sink it expects, so
# most tunings can be added as a directory with no new script. A tuning whose
# hardware needs a sharper test can set match_command to any predicate instead.
tuning_match() {
local dir
for dir in "$tunings_dir"/*/; do
[[ -r $dir/tuning.conf ]] || continue
unset match_dmi match_sku match_command sink_pattern
# shellcheck disable=SC1090
source "$dir/tuning.conf"
# Deliberately does not look at the live audio graph. The install hooks run in
# the ISO chroot with no audio server, and a match that depended on a present
# sink would come back empty there -- so the machine would get neither the LV2
# dependency nor the tuning, and nothing would retry.
# A tuning may list several models it has been validated on. match_dmi and
# match_sku are arrays, so a plain string still works as a single entry.
if [[ -n ${match_command:-} ]]; then
"$match_command" 2>/dev/null || continue
elif [[ -n ${match_sku:-} ]]; then
sku_matches "${match_sku[@]}" || continue
elif [[ -n ${match_dmi:-} ]]; then
dmi_matches "${match_dmi[@]}" || continue
else
continue
fi
# Required whichever way the tuning matched: the graph's target sink is
# substituted from it, so a tuning without one cannot be installed and must
# not be reported as a match.
[[ -n ${sink_pattern:-} ]] || continue
printf '%s\n' "${dir%/}"
return 0
done
return 1
}
# The physical sink the matched tuning is built for, taken from the tuning's own
# sink_pattern rather than a hard-coded regex, so hardware with a different sink
# name needs no change here.
tuned_hardware_sink() {
local dir found
dir="$(tuning_match)" || return 1
unset sink_pattern
# shellcheck disable=SC1090
source "$dir/tuning.conf"
[[ -n ${sink_pattern:-} ]] || return 1
found="$(sink_matching "$sink_pattern")"
[[ -n $found ]] || return 1
printf '%s\n' "$found"
}
tuning_present() {
pactl list sinks short 2>/dev/null | awk '{print $2}' | grep -x "$sink_name" >/dev/null
}
# Only real application streams may be moved. A filter-chain's own output is also
# a sink input but carries no application.name, and moving it would rewire the
# tuning itself.
app_streams() {
pactl list sink-inputs 2>/dev/null | awk '
/^Sink Input #/ {id = substr($3, 2)}
/application\.name = / {
app = $0
sub(/.*application\.name = "/, "", app)
sub(/"$/, "", app)
if (app != "EasyEffects") print id
}'
}
move_apps_to() {
local target="$1" id
for id in $(app_streams); do
pactl move-sink-input "$id" "$target" 2>/dev/null || true
done
}
# WirePlumber can link the output elsewhere if the target is missing when the host
# starts. node.dont-fallback guards against it, but verify rather than assume.
tuning_downstream_sink() {
omarchy-audio-output-sink "$sink_name" 2>/dev/null
}
easyeffects_running() {
pactl list sinks short 2>/dev/null | awk '{print $2}' | grep -x easyeffects_sink >/dev/null ||
pgrep -u "$(id -u)" -x easyeffects >/dev/null 2>&1 ||
systemctl --user is-active --quiet easyeffects.service 2>/dev/null
}
# Unloading a daemon-loaded drop-in is the one case that still needs an audio
# restart, because the daemon only reads its own config at startup.
drop_stale_daemon_config() {
[[ -e $stale_daemon || -e $stale_wireplumber ]] || return 0
rm -f "$stale_daemon" "$stale_wireplumber"
omarchy-restart-audio >/dev/null 2>&1
local _
for _ in {1..40}; do
pactl info >/dev/null 2>&1 && break
sleep 0.25
done
}
case "$action" in
match)
tuning_match
;;
fronted-sink)
# The tuning is a virtual sink in front of the real speakers, so both exist in
# the graph. Selecting the physical one would only bypass the tuning, so
# callers keep it out of the output list while the tuning is up. This answers
# "is a tuning in place", not "where should volume go" -- for the latter see
# omarchy-audio-output-sink, which follows the current default output.
tuning_present || exit 1
tuned_hardware_sink
;;
status)
if [[ -r $fragment ]]; then
echo "Installed: yes ($fragment)"
else
echo "Installed: no"
fi
# Both is-active and is-enabled print their answer *and* exit non-zero when
# negative, so a "|| echo" fallback prints it twice.
host_state="$(systemctl --user is-active "$unit_name" 2>/dev/null)"
host_enabled="$(systemctl --user is-enabled "$unit_name" 2>/dev/null)"
echo "Host service: ${host_state:-inactive} (${host_enabled:-disabled})"
if tuning_present; then
echo "Tuning sink: present"
else
echo "Tuning sink: absent"
fi
echo "Default sink: $(pactl get-default-sink 2>/dev/null)"
if dir="$(tuning_match)"; then
unset description
# shellcheck disable=SC1090
source "$dir/tuning.conf"
echo "Matches: ${description:-?} ($(basename "$dir"))"
else
echo "Matches: nothing ships for this laptop"
fi
;;
off)
if [[ ! -r $fragment && ! -r $unit && ! -r $stale_daemon && ! -r $stale_wireplumber &&
! -r $stale_shared ]]; then
echo "No speaker tuning installed."
exit 0
fi
speakers="$(tuned_hardware_sink)" || speakers=""
systemctl --user disable --now "$unit_name" >/dev/null 2>&1
rm -f "$fragment" "$host_config" "$unit" "$stale_shared"
rmdir "$config_home/pipewire/$host_config_name.d" 2>/dev/null
systemctl --user daemon-reload >/dev/null 2>&1
drop_stale_daemon_config
for _ in {1..20}; do
tuning_present || break
sleep 0.25
done
if [[ -n $speakers ]]; then
pactl set-default-sink "$speakers" >/dev/null 2>&1
# Streams left on the vanished tuning sink reconnect wherever PipeWire puts
# them, which is not necessarily the speakers.
move_apps_to "$speakers"
fi
echo "Speaker tuning removed."
;;
on)
[[ -d $tunings_dir ]] || {
echo "No tunings shipped at $tunings_dir" >&2
exit 1
}
selected="$(tuning_match)" || {
echo "No speaker tuning matches this laptop."
exit 0
}
unset description sink_pattern
# shellcheck disable=SC1090
source "$selected/tuning.conf"
# At first-run the session is up but the sink can still be settling.
for _ in {1..20}; do
speaker_sink="$(sink_matching "$sink_pattern")"
[[ -n $speaker_sink ]] && break
sleep 0.5
done
[[ -n ${speaker_sink:-} ]] || {
echo "A tuning applies to this laptop but no sink matching $sink_pattern" >&2
echo "is present, so there is no audio server yet. Re-run after login:" >&2
echo " omarchy audio tuning on" >&2
exit 1
}
if easyeffects_running; then
cat >&2 <<'EOF'
EasyEffects is running. It moves any stream that follows the default sink to its
own sink, so a tuning installed now would be bypassed.
Stop it first: systemctl --user disable --now easyeffects.service
EOF
exit 1
fi
# Every tuning ends in a limiter, which is an LV2 plugin. Without it the graph
# fails to instantiate and the tuning sink never appears.
ls /usr/lib/lv2/lsp-plugins.lv2/limiter_stereo.ttl >/dev/null 2>&1 || {
echo "lsp-plugins-lv2 is required for the tuning limiter." >&2
exit 1
}
rendered="$(mktemp)"
trap 'rm -f "$rendered"' EXIT
sed "s|@SPEAKER_SINK@|$speaker_sink|g" "$selected/filter-chain.conf" >"$rendered"
# Everything that makes the tuning current has to match, not just the graph:
# an active-but-disabled service disappears at next login, and a stale unit
# file would shadow later fixes to the shipped one indefinitely.
if ((!force)) && [[ -r $fragment ]] && cmp -s "$rendered" "$fragment" &&
[[ -r $host_config ]] && cmp -s "$host_source" "$host_config" &&
[[ -r $unit ]] && cmp -s "$unit_source" "$unit" &&
systemctl --user is-active --quiet "$unit_name" 2>/dev/null &&
systemctl --user is-enabled --quiet "$unit_name" 2>/dev/null &&
[[ "$(tuning_downstream_sink)" == "$speaker_sink" ]]; then
echo "Speaker tuning already current: $description"
exit 0
fi
drop_stale_daemon_config
rm -f "$stale_shared"
install -Dm644 "$host_source" "$host_config"
install -Dm644 "$rendered" "$fragment"
install -Dm644 "$unit_source" "$unit"
systemctl --user daemon-reload >/dev/null 2>&1
systemctl --user enable "$unit_name" >/dev/null 2>&1
systemctl --user restart "$unit_name" >/dev/null 2>&1
echo "Installed speaker tuning: $description"
for _ in {1..40}; do
tuning_present && break
sleep 0.25
done
if ! tuning_present; then
systemctl --user disable --now "$unit_name" >/dev/null 2>&1
rm -f "$fragment" "$host_config" "$unit"
systemctl --user daemon-reload >/dev/null 2>&1
echo "Tuning sink never appeared, so it was removed. Audio is untouched." >&2
echo "Check: systemctl --user status $unit_name" >&2
exit 1
fi
# Confirm the output really landed on the sink this tuning was measured for.
for _ in {1..20}; do
[[ "$(tuning_downstream_sink)" == "$speaker_sink" ]] && break
sleep 0.25
done
downstream="$(tuning_downstream_sink)"
if [[ $downstream != "$speaker_sink" ]]; then
systemctl --user disable --now "$unit_name" >/dev/null 2>&1
rm -f "$fragment" "$host_config" "$unit"
systemctl --user daemon-reload >/dev/null 2>&1
echo "The tuning output linked to ${downstream:-nothing} instead of" >&2
echo "$speaker_sink, so it was removed rather than left tuning the wrong" >&2
echo "device. Audio is untouched." >&2
exit 1
fi
pactl set-default-sink "$sink_name" >/dev/null 2>&1
# A default sink only captures newly created streams, so anything already
# playing would keep bypassing the tuning until its app was restarted.
move_apps_to "$sink_name"
echo "Speakers now play through the tuning."
;;
*)
echo "Usage: omarchy-audio-tuning <on|off|status|match|fronted-sink> [--force]" >&2
exit 2
;;
esac
+403
View File
@@ -0,0 +1,403 @@
#!/bin/bash
# omarchy:summary=Configure the bar and its widget layout
# omarchy:group=bar
# omarchy:args=use <id> | reset | defaults | position <top|bottom|left|right> | transparent <true|false|toggle> | put <id> [placement] | move <id> [placement] | set <id> <key> <value> [--json] [placement]
# omarchy:examples=omarchy bar use local.neon-bar | omarchy bar put omarchy.keyboard-layout --after omarchy.clock | omarchy bar move omarchy.clock --section center --index 0 | omarchy bar set omarchy.clock format HH:mm
set -euo pipefail
source omarchy-shell-config
usage() {
cat <<USAGE
Usage: omarchy bar <command> [args...]
use <id> Use a bar option as the active bar
reset Return to the built-in Omarchy bar
defaults Restore the default bar and service widgets
position <top|bottom|left|right> Bar position
transparent <true|false|toggle> Bar transparency
put <id> [placement] Put a widget on the bar, leaving one
that is already there where it is
move <id> [placement] Move a widget within or between sections
set <id> <key> <value> [--json] [placement]
Set a per-widget option
Placement:
--section <left|center|right> Target section
--index <n> Target index
--before <id> Insert before a widget
--after <id> Insert after a widget
--from-section <section> Source section
--from-index <n> Source index
Enable and disable widgets with 'omarchy plugin enable' and
'omarchy plugin disable'.
'put' places a widget the way 'plugin enable' does, but leaves one that is
already on the bar where it is, and falls back to the widget's usual spot when
--before / --after names a widget the bar does not carry.
Examples:
omarchy bar use local.neon-bar
omarchy bar put omarchy.keyboard-layout --after omarchy.clock
omarchy bar move omarchy.media left
omarchy bar move omarchy.clock --section center --index 0
omarchy bar set omarchy.clock format HH:mm
USAGE
}
# ------------------------------------------------------------------ validation
bar_option_exists() {
omarchy-plugin-catalog | jq -e --arg id "$1" '
any(.[]; (.kinds | index("bar")) and .barPath != null and .id == $id)
' >/dev/null
}
validate_section() {
[[ $1 =~ ^(left|center|right)$ ]] || fail "section must be left, center, or right"
}
validate_index() {
[[ $1 =~ ^[0-9]+$ ]] || fail "index must be a non-negative integer"
}
# --------------------------------------------------------------------- placement
PLACEMENT_SECTION=""
PLACEMENT_INDEX=""
PLACEMENT_BEFORE=""
PLACEMENT_AFTER=""
PLACEMENT_FROM_SECTION=""
PLACEMENT_FROM_INDEX=""
parse_placement() {
while (( $# > 0 )); do
case "$1" in
--section)
PLACEMENT_SECTION="${2:-}"
validate_section "$PLACEMENT_SECTION"
shift 2
;;
--index)
PLACEMENT_INDEX="${2:-}"
validate_index "$PLACEMENT_INDEX"
shift 2
;;
--before)
PLACEMENT_BEFORE="${2:-}"
[[ -n $PLACEMENT_BEFORE ]] || fail "--before requires a widget id"
shift 2
;;
--after)
PLACEMENT_AFTER="${2:-}"
[[ -n $PLACEMENT_AFTER ]] || fail "--after requires a widget id"
shift 2
;;
--from-section)
PLACEMENT_FROM_SECTION="${2:-}"
validate_section "$PLACEMENT_FROM_SECTION"
shift 2
;;
--from-index)
PLACEMENT_FROM_INDEX="${2:-}"
validate_index "$PLACEMENT_FROM_INDEX"
shift 2
;;
-h | --help)
usage
exit 0
;;
*)
fail "unknown option: $1"
;;
esac
done
[[ -z $PLACEMENT_BEFORE || -z $PLACEMENT_AFTER ]] || fail "use only one of --before or --after"
}
placement_json() {
jq -cn \
--arg section "$PLACEMENT_SECTION" \
--arg index "$PLACEMENT_INDEX" \
--arg before "$PLACEMENT_BEFORE" \
--arg after "$PLACEMENT_AFTER" \
--arg fromSection "$PLACEMENT_FROM_SECTION" \
--arg fromIndex "$PLACEMENT_FROM_INDEX" '
{}
+ (if $section == "" then {} else {section: $section} end)
+ (if $index == "" then {} else {index: ($index | tonumber)} end)
+ (if $before == "" then {} else {before: $before} end)
+ (if $after == "" then {} else {after: $after} end)
+ (if $fromSection == "" then {} else {fromSection: $fromSection} end)
+ (if $fromIndex == "" then {} else {fromIndex: ($fromIndex | tonumber)} end)
'
}
# -------------------------------------------------------------------- commands
cmd_use() {
local plugin="${1:-}"
[[ -n $plugin ]] || fail "bar option id is required"
(( $# == 1 )) || fail "use takes a single bar option id"
if [[ $plugin == "default" || $plugin == "built-in" ]]; then
plugin="omarchy.bar"
fi
bar_option_exists "$plugin" || fail "$plugin is not a known bar option; run 'omarchy plugin list'"
if [[ $plugin == "omarchy.bar" ]]; then
commit "$NORMALIZE | del(.bar.id)"
else
commit "$NORMALIZE | .bar.id = \$plugin" --arg plugin "$plugin"
fi
echo "Using $plugin as the active bar"
}
cmd_defaults() {
(( $# == 0 )) || fail "defaults does not take arguments"
local optional_widgets="[]"
local service widget
for service in dropbox tailscale; do
if "omarchy-installed-service-$service"; then
widget=$(jq -cn \
--arg id "omarchy.$service" \
--arg section "$(bar_widget_default_section "omarchy.$service")" \
'{id: $id, section: $section}')
optional_widgets=$(jq -c --argjson widget "$widget" '. + [$widget]' <<<"$optional_widgets")
fi
done
# This remains one file mutation so it also works during the headless
# Quattro upgrade and cannot race the shell's in-memory config.
commit "$NORMALIZE
| .bar = \$defaults[0].bar
| def entry_id: if type == \"object\" then (.id // \"\" | tostring) else tostring end;
def anchor_for(\$section): { left: \"omarchy.workspaces\", center: \"omarchy.weather\", right: \"omarchy.tray\" }[\$section];
reduce \$widgets[] as \$widget (.;
.bar.layout.left = (.bar.layout.left | map(select(entry_id != \$widget.id)))
| .bar.layout.center = (.bar.layout.center | map(select(entry_id != \$widget.id)))
| .bar.layout.right = (.bar.layout.right | map(select(entry_id != \$widget.id)))
| (.bar.layout[\$widget.section] | map(entry_id) | index(anchor_for(\$widget.section))) as \$anchor
| (\$anchor | if . == null then (.bar.layout[\$widget.section] | length) else . + 1 end) as \$index
| .bar.layout[\$widget.section] = (
.bar.layout[\$widget.section][0:\$index]
+ [{id: \$widget.id}]
+ .bar.layout[\$widget.section][\$index:]
)
)
" \
--slurpfile defaults "$DEFAULTS_FILE" \
--argjson widgets "$optional_widgets"
echo "Restored the default Omarchy bar"
}
cmd_position() {
local position="${1:-}"
[[ -n $position ]] || fail "position is required"
(( $# == 1 )) || fail "position takes a single value"
[[ $position =~ ^(top|bottom|left|right)$ ]] || fail "position must be top, bottom, left, or right"
commit "$NORMALIZE | .bar.position = \$position" --arg position "$position"
echo "Bar position set to $position"
}
cmd_transparent() {
local transparent="${1:-}"
[[ -n $transparent ]] || fail "transparent is required"
(( $# == 1 )) || fail "transparent takes a single value"
[[ $transparent =~ ^(true|false|toggle)$ ]] || fail "transparent must be true, false, or toggle"
if [[ $transparent == "toggle" ]]; then
commit "$NORMALIZE | .bar.transparent = (.bar.transparent != true)"
echo "Bar transparency toggled"
else
commit "$NORMALIZE | .bar.transparent = \$transparent" --argjson transparent "$transparent"
echo "Bar transparency set to $transparent"
fi
}
bar_widget_default_section() {
local catalog
catalog=$(omarchy-plugin-catalog 2>/dev/null) || {
echo "center"
return 0
}
jq -r --arg id "$1" '
map(select(.id == $id))[0].barWidget.defaultSection // "center"
| if IN("left", "center", "right") then . else "center" end
' <<<"$catalog"
}
# Asks the shell to place a widget, waiting out one still coming up. Answers 0
# with the reply in PUT_RESULT, 1 when there was no shell to ask. Only an
# absent shell is carried on from: omarchy-migrate marks a 0 return as done.
PUT_RESULT=""
SHELL_ANSWERED=0
ask_to_put() {
local id="$1" placement="$2" attempt absent=0
for (( attempt = 0; attempt < ${OMARCHY_SHELL_READY_ATTEMPTS:-50}; attempt++ )); do
if PUT_RESULT=$(omarchy-shell shell putBarWidget "$id" "$placement" 2>&1); then
SHELL_ANSWERED=1
[[ $PUT_RESULT == "not ready" ]] || return 0
elif [[ $PUT_RESULT == *"not ready"* ]]; then
SHELL_ANSWERED=1
elif [[ $PUT_RESULT == *"is not running"* ]]; then
# Answered once and now gone: it stopped mid-request.
if (( SHELL_ANSWERED )); then
fail "omarchy-shell did not become ready; $id was not put on the bar"
fi
# A shell being spawned has no socket yet, and nothing says a launch is
# under way, so give one a few seconds to turn up.
if (( ++absent >= ${OMARCHY_SHELL_ABSENT_ATTEMPTS:-30} )); then
echo "omarchy-shell is not running; $id was not put on the bar" >&2
return 1
fi
else
fail "could not put $id on the bar: $PUT_RESULT"
fi
sleep 0.1
done
fail "omarchy-shell did not become ready; $id was not put on the bar"
}
# Placement lives in the shell, which owns the config it has in memory. Putting
# a widget therefore asks the shell rather than editing the file behind it.
cmd_put() {
local id="${1:-}"
[[ -n $id ]] || fail "put requires a widget id"
shift
local positional_section=""
if (( $# > 0 )) && [[ $1 != --* ]]; then
positional_section="$1"
validate_section "$positional_section"
shift
fi
parse_placement "$@"
[[ -z $positional_section || -z $PLACEMENT_SECTION ]] ||
fail "specify a section positionally or with --section, not both"
[[ -z $positional_section ]] || PLACEMENT_SECTION="$positional_section"
[[ -z $PLACEMENT_FROM_SECTION && -z $PLACEMENT_FROM_INDEX ]] ||
fail "put does not accept --from-section or --from-index"
local placement
placement=$(placement_json)
ask_to_put "$id" "$placement" || return 0
# An update runs migrations before it restarts the shell, so this one can
# predate the fallback. Ask it again without the neighbour it cannot find.
if [[ $PUT_RESULT == "could not find target widget"* ]]; then
PLACEMENT_BEFORE=""
PLACEMENT_AFTER=""
ask_to_put "$id" "$(placement_json)" || return 0
fi
[[ $PUT_RESULT != "unknown" ]] || fail "$id is not a known widget; run 'omarchy plugin list'"
[[ $PUT_RESULT == "ok" ]] || fail "$PUT_RESULT"
# Says nothing about whether it had to be placed: a widget already on the bar
# is left where it is, and both outcomes are the same answer to the caller.
echo "$id is on the bar"
}
cmd_move() {
local id="${1:-}"
[[ -n $id ]] || fail "move requires a widget id"
shift
local positional_section=""
if (( $# > 0 )) && [[ $1 != --* ]]; then
positional_section="$1"
validate_section "$positional_section"
shift
fi
parse_placement "$@"
[[ -z $positional_section || -z $PLACEMENT_SECTION ]] ||
fail "specify a section positionally or with --section, not both"
[[ -z $positional_section || -z $PLACEMENT_INDEX ]] ||
fail "specify a section positionally or use --index, not both"
[[ -z $positional_section || -z $PLACEMENT_BEFORE ]] ||
fail "specify a section positionally or use --before, not both"
[[ -z $positional_section || -z $PLACEMENT_AFTER ]] ||
fail "specify a section positionally or use --after, not both"
[[ -z $positional_section ]] || PLACEMENT_SECTION="$positional_section"
local result
result=$(omarchy-shell shell moveBarWidget "$id" "$(placement_json)")
[[ $result == "ok" ]] || fail "$result"
echo "Moved $id"
}
cmd_set() {
local id="${1:-}"
local key="${2:-}"
local value="${3:-}"
[[ -n $id ]] || fail "set requires a widget id"
[[ -n $key ]] || fail "set requires a setting key"
(( $# >= 3 )) || fail "set requires a value"
shift 3
local value_is_json="false"
if (( $# > 0 )) && [[ $1 == "--json" ]]; then
value_is_json="true"
shift
fi
parse_placement "$@"
[[ -z $PLACEMENT_BEFORE && -z $PLACEMENT_AFTER ]] ||
fail "set does not accept --before or --after"
local value_json
if [[ $value_is_json == "true" ]]; then
value_json=$(jq -cn --argjson value "$value" '$value') ||
fail "invalid JSON value: $value"
else
value_json=$(jq -cn --arg value "$value" '$value')
fi
local result
result=$(omarchy-shell shell setBarWidget "$id" "$key" "$value_json" "$(placement_json)")
[[ $result == "ok" ]] || fail "$result"
echo "Set $key on $id"
}
# --------------------------------------------------------------------- dispatch
command="${1:-}"
(( $# > 0 )) && shift || true
case "$command" in
use)
cmd_use "$@"
;;
reset)
(( $# == 0 )) || fail "reset does not take arguments"
cmd_use omarchy.bar
;;
defaults)
cmd_defaults "$@"
;;
position)
cmd_position "$@"
;;
transparent)
cmd_transparent "$@"
;;
put)
cmd_put "$@"
;;
move)
cmd_move "$@"
;;
set)
cmd_set "$@"
;;
-h | --help | help | "")
usage
;;
*)
fail "unknown command: $command"
;;
esac
+125
View File
@@ -0,0 +1,125 @@
#!/bin/bash
# omarchy:summary=Choose a legible transparent bar text color
# omarchy:hidden=true
set -e
position=${1:-top}
bar_size=${2:-}
text_color=${3:-}
background_color=${4:-}
background_path=""
screen_size=""
shift 4 2>/dev/null || true
while (($# > 0)); do
case "$1" in
--background)
background_path=${2:-}
shift 2
;;
--screen)
screen_size=${2:-}
shift 2
;;
*)
shift
;;
esac
done
valid_hex() {
[[ $1 =~ ^#[0-9A-Fa-f]{6}$ ]]
}
fallback() {
printf '%s\n' "$text_color"
exit 0
}
contrast() {
local color="$1"
local sample="$2"
awk -v fg="$color" -v bg="$sample" '
function channel(hex, start) {
return strtonum("0x" substr(hex, start, 2)) / 255
}
function linear(c) {
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ^ 2.4
}
function luminance(hex, r, g, b) {
r = linear(channel(hex, 2))
g = linear(channel(hex, 4))
b = linear(channel(hex, 6))
return 0.2126 * r + 0.7152 * g + 0.0722 * b
}
BEGIN {
l1 = luminance(fg)
l2 = luminance(bg)
if (l1 < l2) {
t = l1
l1 = l2
l2 = t
}
printf "%.6f\n", (l1 + 0.05) / (l2 + 0.05)
}
'
}
valid_hex "$text_color" || fallback
valid_hex "$background_color" || fallback
[[ $position =~ ^(top|bottom|left|right)$ ]] || fallback
[[ $bar_size =~ ^[0-9]+$ ]] || fallback
omarchy-cmd-present magick || fallback
if [[ -z $background_path ]]; then
background_path=$(readlink -f "$HOME/.local/state/omarchy/current/background" 2>/dev/null || true)
fi
[[ -f $background_path ]] || fallback
if [[ -z $screen_size ]]; then
if omarchy-cmd-present hyprctl && omarchy-cmd-present jq; then
screen_size=$(hyprctl monitors -j 2>/dev/null | jq -r '.[0] | "\(.width)x\(.height)"' 2>/dev/null || true)
fi
fi
[[ $screen_size =~ ^([0-9]+)x([0-9]+)$ ]] || fallback
screen_width=${BASH_REMATCH[1]}
screen_height=${BASH_REMATCH[2]}
((screen_width > 0 && screen_height > 0 && bar_size > 0)) || fallback
case "$position" in
top)
crop="${screen_width}x${bar_size}+0+0"
;;
bottom)
crop_y=$((screen_height - bar_size))
((crop_y >= 0)) || fallback
crop="${screen_width}x${bar_size}+0+${crop_y}"
;;
left)
crop="${bar_size}x${screen_height}+0+0"
;;
right)
crop_x=$((screen_width - bar_size))
((crop_x >= 0)) || fallback
crop="${bar_size}x${screen_height}+${crop_x}+0"
;;
esac
pixel=$(magick "$background_path" -auto-orient \
-resize "${screen_width}x${screen_height}^" \
-gravity center -extent "${screen_width}x${screen_height}" \
-gravity NorthWest -crop "$crop" +repage \
-resize '1x1!' -format '%[fx:int(255*r)],%[fx:int(255*g)],%[fx:int(255*b)]' info:- 2>/dev/null || true)
[[ $pixel =~ ^([0-9]+),([0-9]+),([0-9]+)$ ]] || fallback
sample=$(printf '#%02x%02x%02x' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}")
text_contrast=$(contrast "$text_color" "$sample")
background_contrast=$(contrast "$background_color" "$sample")
awk -v text="$text_contrast" -v background="$background_contrast" 'BEGIN { exit !(background > text) }' \
&& printf '%s\n' "$background_color" \
|| printf '%s\n' "$text_color"
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# omarchy:summary=Send the low battery warning notification and run battery-low hooks.
# omarchy:args=<percentage>
# omarchy:hidden=true
set -euo pipefail
if (($# != 1)); then
echo "Usage: omarchy-battery-low <percentage>" >&2
exit 1
fi
level=$1
omarchy-notification-send -g 󱐋 -u critical "Time to recharge!" "Battery is down to ${level}%" -i battery-caution -t 30000
omarchy-hook battery-low "$level"
-23
View File
@@ -1,23 +0,0 @@
#!/bin/bash
# Designed to be run by systemd timer every 30 seconds and alerts if battery is low
BATTERY_THRESHOLD=10
NOTIFICATION_FLAG="/run/user/$UID/omarchy_battery_notified"
BATTERY_LEVEL=$(omarchy-battery-remaining)
BATTERY_STATE=$(upower -i $(upower -e | grep 'BAT') | grep -E "state" | awk '{print $2}')
send_notification() {
notify-send -u critical "󱐋 Time to recharge!" "Battery is down to ${1}%" -i battery-caution -t 30000
}
if [[ -n $BATTERY_LEVEL && $BATTERY_LEVEL =~ ^[0-9]+$ ]]; then
if [[ $BATTERY_STATE == "discharging" ]] && (( BATTERY_LEVEL <= BATTERY_THRESHOLD )); then
if [[ ! -f $NOTIFICATION_FLAG ]]; then
send_notification $BATTERY_LEVEL
touch $NOTIFICATION_FLAG
fi
else
rm -f $NOTIFICATION_FLAG
fi
fi
+1 -2
View File
@@ -1,7 +1,6 @@
#!/bin/bash
# Returns true if a battery is present on the system.
# Used by the battery monitor and other battery-related checks.
# omarchy:summary=Returns true if a battery is present on the system.
for bat in /sys/class/power_supply/BAT*; do
[[ -r $bat/present ]] &&
-9
View File
@@ -1,9 +0,0 @@
#!/bin/bash
# Returns the battery percentage remaining as an integer.
# Used by the battery monitor and the Ctrl + Shift + Super + B hotkey.
upower -i $(upower -e | grep BAT) | awk '/percentage/ {
print int($2)
exit
}'
+137
View File
@@ -0,0 +1,137 @@
#!/bin/bash
# omarchy:summary=Returns a formatted battery status string with percentage and power draw/charge.
# omarchy:args=[--shell]
shell_output=false
power_supply_path="${OMARCHY_POWER_SUPPLY_PATH:-/sys/class/power_supply}"
case "${1:-}" in
"")
;;
--shell)
shell_output=true
;;
*)
echo "Usage: omarchy-battery-status [--shell]" >&2
exit 2
;;
esac
battery=$(upower -e 2>/dev/null | grep BAT | head -n 1)
[[ -z $battery ]] && exit 0
battery_info=$(upower -i "$battery")
percentage=$(awk '/percentage/ { print int($2); exit }' <<<"$battery_info")
capacity=$(awk '/energy-full:/ { printf "%d", $2; exit }' <<<"$battery_info")
time_remaining=$(awk '/time to (empty|full)/ {
value = $4
unit = $5
if (unit ~ /^minute/) {
printf "%dm", int(value)
} else {
hours = int(value)
minutes = int((value - hours) * 60)
if (minutes > 0) {
printf "%dh %dm", hours, minutes
} else {
printf "%dh", hours
}
}
exit
}' <<<"$battery_info")
power_rate_raw=$(awk '/energy-rate/ { print $2; exit }' <<<"$battery_info")
native_path=$(awk '/native-path/ { print $2; exit }' <<<"$battery_info")
battery_path="$power_supply_path/$native_path"
# UPower's energy-rate can lag the kernel telemetry by tens of seconds. Use
# the instantaneous sysfs reading when available so the open panel stays live.
if [[ -r $battery_path/power_now ]]; then
power_rate_raw=$(awk -v microwatts="$(<"$battery_path/power_now")" 'BEGIN { print microwatts / 1000000 }')
elif [[ -r $battery_path/current_now && -r $battery_path/voltage_now ]]; then
power_rate_raw=$(awk \
-v microamps="$(<"$battery_path/current_now")" \
-v microvolts="$(<"$battery_path/voltage_now")" \
'BEGIN { print microamps * microvolts / 1000000000000 }')
fi
power_rate=$(awk -v rate="${power_rate_raw:-0}" 'BEGIN {
rounded = sprintf("%.1f", rate)
sub(/\.0$/, "", rounded)
print rounded
}')
state=$(awk '/state/ { print $2; exit }' <<<"$battery_info")
threshold_start=$(awk '/charge-start-threshold:/ { gsub(/%/, "", $2); print int($2); exit }' <<<"$battery_info")
threshold_end=$(awk '/charge-end-threshold:/ { gsub(/%/, "", $2); print int($2); exit }' <<<"$battery_info")
[[ -z $threshold_end ]] && threshold_end=$(cat "$power_supply_path"/BAT*/charge_control_end_threshold 2>/dev/null | head -1)
[[ -z $threshold_start ]] && threshold_start=$(cat "$power_supply_path"/BAT*/charge_control_start_threshold 2>/dev/null | head -1)
ac_online=false
for supply in "$power_supply_path"/*; do
[[ -r $supply/type ]] || continue
[[ $(<"$supply/type") == "Mains" ]] || continue
[[ -r $supply/online ]] || continue
if [[ $(<"$supply/online") == "1" ]]; then
ac_online=true
break
fi
done
charge_idle=false
if awk -v rate="${power_rate_raw:-0}" 'BEGIN { exit !(rate <= 0.2) }'; then
charge_idle=true
fi
charge_holding=false
if [[ $ac_online == "true" && -n $threshold_end ]]; then
if [[ $state == "pending-charge" ]]; then
charge_holding=true
elif [[ $state == "fully-charged" ]] && (( percentage < 99 )); then
charge_holding=true
elif [[ $state == "charging" && $charge_idle == "true" ]] && (( threshold_end < 99 && percentage >= threshold_end )); then
charge_holding=true
fi
fi
if [[ $shell_output == "true" ]]; then
printf 'percentage\t%s\n' "${percentage}%"
if [[ $charge_holding == "true" ]]; then
printf 'state\tholding\n'
else
printf 'state\t%s\n' "$state"
fi
printf 'rate\t%s\n' "${power_rate}W"
printf 'size\t%s\n' "${capacity}Wh"
printf 'time\t%s\n' "$time_remaining"
cycles=$(cat "$power_supply_path"/BAT*/cycle_count 2>/dev/null | head -1)
[[ -n $cycles ]] && printf 'cycles\t%s\n' "$cycles"
if [[ -n $threshold_end ]]; then
if [[ -n $threshold_start && $threshold_start != $threshold_end ]]; then
printf 'threshold\t%s-%s%%\n' "$threshold_start" "$threshold_end"
else
printf 'threshold\t%s%%\n' "$threshold_end"
fi
fi
exit 0
fi
if [[ $charge_holding == "true" ]]; then
if [[ -n $threshold_start && $threshold_start != $threshold_end ]]; then
threshold_label="${threshold_start}-${threshold_end}%"
else
threshold_label="${threshold_end}%"
fi
echo "Battery ${percentage}% · Holding at ${threshold_label} · ${power_rate}W / ${capacity}Wh"
elif [[ $state == "charging" ]]; then
echo "Battery ${percentage}% · ${time_remaining} to full ·  ${power_rate}W / ${capacity}Wh"
else
echo "Battery ${percentage}% · ${time_remaining} left ·  ${power_rate}W / ${capacity}Wh"
fi
+52
View File
@@ -0,0 +1,52 @@
#!/bin/bash
# omarchy:summary=Control a Bluetooth device
# omarchy:group=bluetooth
# omarchy:args=[pair|connect|disconnect|forget] <address>
# omarchy:examples=omarchy bluetooth device connect 00:11:22:33:44:55
set -e
usage() {
echo "Usage: omarchy-bluetooth-device [pair|connect|disconnect|forget] <address>" >&2
exit 1
}
action=${1:-}
address=${2:-}
[[ $action == "pair" || $action == "connect" || $action == "disconnect" || $action == "forget" ]] || usage
[[ $address =~ ^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$ ]] || usage
power_on() {
[[ $(timeout 2s bluetoothctl show 2>/dev/null) == *"Powered: yes"* ]] && return
# Not bluetoothctl directly: Bluetooth is turned off by an rfkill soft block,
# and BlueZ refuses to power an adapter up while one is set.
omarchy-bluetooth-power on || true
}
trust_device() {
bluetoothctl trust "$address" >/dev/null 2>&1 || true
}
case "$action" in
pair)
power_on
timeout 20s bluetoothctl pair "$address" >/dev/null 2>&1 || true
trust_device
timeout 20s bluetoothctl connect "$address" >/dev/null 2>&1 || true
;;
connect)
power_on
trust_device
timeout 20s bluetoothctl connect "$address" >/dev/null 2>&1 || true
;;
disconnect)
timeout 10s bluetoothctl disconnect "$address" >/dev/null 2>&1 || true
;;
forget)
power_on
timeout 10s bluetoothctl disconnect "$address" >/dev/null 2>&1 || true
timeout 10s bluetoothctl remove "$address" >/dev/null 2>&1 || true
;;
esac
+89
View File
@@ -0,0 +1,89 @@
#!/bin/bash
# omarchy:summary=Turn Bluetooth on or off, remembered across reboots
# omarchy:group=bluetooth
# omarchy:args=<on|off|toggle|is-on>
# BlueZ never persists an adapter's Powered property, so turning Bluetooth off
# through bluetoothctl lasts only until the next boot. The rfkill soft block does
# persist: systemd-rfkill saves every switch under /var/lib/systemd/rfkill and
# restores it early on the next boot, which is its entire job. Blocking is also
# what the kernel hands every radio at once, so a machine with two controllers
# gets both, where bluetoothctl only ever addresses the default one.
#
# So the block is the state, and BlueZ follows it: unblocking leaves AutoEnable
# at its stock default and bluetoothd powers the adapter up by itself. Every
# Omarchy path that turns Bluetooth on or off goes through here, because a plain
# `bluetoothctl power on` fails outright while the block is set.
POWER_WAIT_SECONDS=${OMARCHY_BLUETOOTH_POWER_WAIT_SECONDS:-2}
controllers() {
timeout 2s bluetoothctl list 2>/dev/null | awk '{print $2}'
}
# Any controller counts. The block is all-or-nothing across the radios, so the
# state has to be read the same way; a bare `bluetoothctl show` would report the
# default controller and miss a powered dongle sitting behind it.
powered() {
local controller
for controller in $(controllers); do
[[ $(timeout 2s bluetoothctl show "$controller" 2>/dev/null) == *"Powered: yes"* ]] && return 0
done
return 1
}
# One deadline around the whole wait rather than a fixed number of probes: every
# probe can sit on its own timeout when D-Bus is wedged, and counting probes then
# stretches a two-second wait into half a minute.
wait_powered() {
local deadline=$((SECONDS + POWER_WAIT_SECONDS))
while :; do
powered && return 0
((SECONDS < deadline)) || return 1
sleep 0.2
done
}
power_on() {
rfkill unblock bluetooth
# Usually all it takes: with AutoEnable at its default, bluetoothd powers the
# adapter up on its own once the block is gone. It will not do that for an
# adapter powered down without a block, so ask directly before giving up.
wait_powered && return 0
timeout 5s bluetoothctl power on >/dev/null 2>&1
wait_powered && return 0
echo "omarchy-bluetooth-power: adapter did not come up" >&2
return 1
}
case "${1:-}" in
on)
power_on
;;
off)
# No bluetoothctl power off to go with this: the block already drops the
# adapter to Powered: no, and it is the half that survives the reboot.
rfkill block bluetooth
;;
toggle)
if powered; then
rfkill block bluetooth
else
power_on
fi
;;
is-on)
powered
;;
*)
echo "Usage: omarchy-bluetooth-power <on|off|toggle|is-on>" >&2
exit 1
;;
esac
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
# Set the branch for Omarchy's git repository.
if (($# == 0)); then
echo "Usage: omarchy-branch-set [master|rc|dev]"
exit 1
else
branch="$1"
fi
if [[ $branch != "master" && $branch != "rc" && $branch != "dev" ]]; then
echo "Error: Invalid branch '$branch'. Must be one of: master, rc, dev"
exit 1
fi
git -C $OMARCHY_PATH switch $branch
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# omarchy:summary=Edit, set, or reset About branding
# omarchy:group=branding
# omarchy:name=about
# omarchy:args=<image|text|reset>
# omarchy:examples=omarchy branding about image | omarchy branding about text | omarchy branding about reset
set -euo pipefail
case "${1:-}" in
image)
image=$(omarchy-file-select --title "Pick PNG or SVG for About" --extensions "png svg")
if omarchy-transcode-ascii "$image" ~/.config/omarchy/branding/about.txt --width 54 --height 26; then
omarchy-launch-about >/dev/null 2>&1
fi
;;
text)
omarchy-launch-editor ~/.config/omarchy/branding/about.txt >/dev/null 2>&1 && omarchy-launch-about >/dev/null 2>&1
;;
reset)
cp "$OMARCHY_PATH/icon.txt" ~/.config/omarchy/branding/about.txt && omarchy-launch-about >/dev/null 2>&1
;;
*)
echo "Usage: omarchy-branding-about <image|text|reset>" >&2
exit 1
;;
esac
+104
View File
@@ -0,0 +1,104 @@
#!/bin/bash
# omarchy:summary=Shared helpers for animating the About branding (source this, don't run it).
# omarchy:group=branding
# omarchy:name=about-animation
# omarchy:hidden=true
# The About branding's animation: a sheen, and the frames that make it. *When*
# those frames are written stays with the caller, because that is inseparable
# from how the caller's window closes and resizes; this only says what to write,
# and where — it is handed a logo and knows nothing about About beyond that.
# A band of light leans across the logo, rests, and leans across again. Two
# columns per row puts it at 45 degrees on screen, where a cell is twice as tall
# as it is wide.
ESC=$'\e'
# Bright white, so the band does not depend on how the terminal reads bold. Foot's
# bold-text-in-bright brightens a bold regular colour into its bright counterpart,
# which turns a bold green logo bright green — exactly the colour a bright green
# band would have used, leaving a glint nobody can see. No regular colour
# brightens into bright white, so this one shows either way.
SHEEN_BAND="${ESC}[1m${ESC}[97m"
SHEEN_SLANT=2
SHEEN_HALF=2
SHEEN_FRAME_SECONDS=0.025
SHEEN_REST_TICKS=8
# Re-measure the grid every half second, the cadence a still logo already cost,
# rather than spawning a process per frame.
SHEEN_POLL_FRAMES=20
# A frame is one string holding every row of the logo, positioned and coloured,
# so a tick writes the whole logo at once and never shows it half drawn.
compose_frame() {
local centre=$1
local row line length at from to frame=""
for (( row = 0; row < SHEEN_ROWS; row++ )); do
line=${SHEEN_LINES[row]}
length=${#line}
# Where the band crosses this row. Running off the right needs no clamp,
# because a slice that starts past the end of a line is already empty, but a
# negative offset would count from the end of it instead of off the left.
at=$(( centre - row * SHEEN_SLANT ))
from=$(( at - SHEEN_HALF ))
from=$(( from < 0 ? 0 : from ))
to=$(( at + SHEEN_HALF + 1 ))
to=$(( to < 0 ? 0 : to ))
frame+="${ESC}[$((SHEEN_TOP + row));${SHEEN_LEFT}H"
frame+="${SHEEN_BASE}${line:0:from}${SHEEN_BAND}${line:from:to - from}${SHEEN_BASE}${line:to}"
done
SHEEN_COMPOSED=$frame
}
# Builds every frame up front: a tick that recomputed a logo's worth of colour
# spans in bash would not hold the frame rate, and the sweep is the same every
# time. Leaves them in SHEEN_FRAMES, and answers whether this logo can be
# animated at all — one it cannot put back exactly as it found it is one to leave
# alone, because nothing on screen would say the difference.
#
# sheen_build <logo-file> <top-row> <left-column> <base-colour> <columns-available>
sheen_build() {
local file=$1 columns=$5
SHEEN_TOP=$2
SHEEN_LEFT=$3
SHEEN_BASE=$4
SHEEN_LINES=()
# A failing redirection reports itself before 2>/dev/null would apply, so order
# it first: the caller's window must not get a shell error painted across it.
mapfile -t SHEEN_LINES 2>/dev/null <"$file" || return 1
SHEEN_ROWS=${#SHEEN_LINES[@]}
(( SHEEN_ROWS > 0 )) || return 1
local row line width=0
for (( row = 0; row < SHEEN_ROWS; row++ )); do
line=${SHEEN_LINES[row]}
# A renderer substitutes $1 to $9 for colours, so a logo written with those is
# not the text that reached the screen.
[[ $line == *'$'* ]] && return 1
# These frames slice the row by character and a terminal draws it by column,
# so one character has to be one cell — and everything that breaks that breaks
# it here. A wide glyph, a combining mark or a joined emoji is not one cell; a
# tab or an escape is one the renderer expanded itself; and a shell whose
# locale is counting bytes is not counting characters at all.
(( ${#line} == $(printf '%s' "$line" | LC_ALL=C.UTF-8 wc -L) )) || return 1
if (( ${#line} > width )); then
width=${#line}
fi
done
(( width > 0 && width <= columns )) || return 1
SHEEN_FRAMES=()
local centre last=$(( width + SHEEN_ROWS * SHEEN_SLANT + SHEEN_HALF ))
for (( centre = -SHEEN_HALF; centre <= last; centre++ )); do
compose_frame "$centre"
SHEEN_FRAMES+=("$SHEEN_COMPOSED")
done
}
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# omarchy:summary=Edit, set, or reset screensaver branding
# omarchy:group=branding
# omarchy:name=screensaver
# omarchy:args=<image|text|reset>
# omarchy:examples=omarchy branding screensaver image | omarchy branding screensaver text | omarchy branding screensaver reset
set -euo pipefail
case "${1:-}" in
image)
image=$(omarchy-file-select --title "Pick PNG or SVG for screensaver" --extensions "png svg")
if omarchy-transcode-ascii "$image" ~/.config/omarchy/branding/screensaver.txt; then
omarchy-launch-screensaver force >/dev/null 2>&1
fi
;;
text)
omarchy-launch-editor ~/.config/omarchy/branding/screensaver.txt >/dev/null 2>&1 && omarchy-launch-screensaver force >/dev/null 2>&1
;;
reset)
cp "$OMARCHY_PATH/logo.txt" ~/.config/omarchy/branding/screensaver.txt && omarchy-launch-screensaver force >/dev/null 2>&1
;;
*)
echo "Usage: omarchy-branding-screensaver <image|text|reset>" >&2
exit 1
;;
esac
+114 -13
View File
@@ -1,21 +1,122 @@
#!/bin/bash
# Adjust brightness on the most likely display device.
# Usage: omarchy-brightness-display <step>
# omarchy:summary=Show or adjust brightness on the focused display.
# omarchy:args=[--no-osd] [--monitor name] [+N%|N%-|N%|off|on]
# omarchy:examples=omarchy brightness display | omarchy brightness display +5% | omarchy brightness display --monitor DP-1 50% | omarchy brightness display off | omarchy brightness display on
step="${1:-+5%}"
no_osd=0
monitor=""
# Start with the first possible output, then refine to the most likely given an order heuristic.
device="$(ls -1 /sys/class/backlight 2>/dev/null | head -n1)"
for candidate in amdgpu_bl* intel_backlight acpi_video*; do
if [[ -e /sys/class/backlight/$candidate ]]; then
device="$candidate"
while (( $# > 0 )); do
case "$1" in
--no-osd)
no_osd=1
shift
;;
--monitor)
(( $# >= 2 )) || exit 1
monitor="$2"
shift 2
;;
*)
break
fi
;;
esac
done
# Set the actual brightness of the display device.
brightnessctl -d "$device" set "$step" >/dev/null
# Get the brightness of the passed display
backlight_brightness() {
brightnessctl -d "$1" -m 2>/dev/null | awk -F, '{ gsub("%", "", $4); print $4; found=1 } END{ exit !found }'
}
# Use SwayOSD to display the new brightness setting.
omarchy-swayosd-brightness "$(brightnessctl -d "$device" -m | cut -d',' -f4 | tr -d '%')"
[[ -n $monitor ]] || monitor="$(omarchy-hyprland-monitor-focused 2>/dev/null || true)"
monitor_is_internal() {
[[ $monitor =~ ^(eDP|LVDS|DSI)- ]]
}
use_apple_display() {
omarchy-hyprland-monitor-focused-apple "$monitor"
}
use_ddc_display() {
[[ -n $monitor ]] && ! monitor_is_internal
}
if (( $# == 0 )); then
if use_apple_display; then
omarchy-brightness-display-apple
exit
elif use_ddc_display; then
omarchy-brightness-display-ddc "$monitor"
exit
fi
device="$(omarchy-hw-display)" || exit 1
backlight_brightness "$device"
exit
fi
step="$1"
if [[ $step == "off" ]]; then
hyprctl dispatch 'hl.dsp.dpms({ action = "disable" })' >/dev/null 2>&1
exit 0
elif [[ $step == "on" ]]; then
# Skip the dispatch when every active display is already lit: a redundant
# DPMS enable right after system resume forces another modeset, which blanks
# the panel for a beat (visible flash at the unlock screen).
hyprctl monitors -j 2>/dev/null | jq -e '[.[] | select(.disabled == false)] | length > 0 and all(.dpmsStatus)' >/dev/null 2>&1 && exit 0
hyprctl dispatch 'hl.dsp.dpms({ action = "enable" })' >/dev/null 2>&1
exit 0
fi
# Drop overlapping brightness key events so concurrent invocations do not race.
# Hardware key repeat present on some devices can otherwise glitch OSD rendering.
exec {lock_fd}>"${XDG_RUNTIME_DIR:-/tmp}/omarchy-brightness-display.lock"
flock -n "$lock_fd" || exit 0
if use_apple_display; then
if (( no_osd )); then
omarchy-brightness-display-apple --no-osd "$step"
else
omarchy-brightness-display-apple "$step"
fi
elif use_ddc_display; then
brightness="$(omarchy-brightness-display-ddc "$monitor" "$step")" || exit 1
(( no_osd )) || omarchy-osd -i brightness -p "$brightness"
else
# Current device highlighted
device="$(omarchy-hw-display)" || exit 1
# Current brightness percentage
current=$(backlight_brightness "$device") || exit 1
# Apply non-uniform step size: 1% steps if at or below 5%, otherwise set an
# absolute target percentage to avoid raw backlight rounding causing uneven OSD steps.
if [[ $step == "+5%" ]]; then
if (( current < 5 )); then
(( target = current + 1 ))
else
(( target = current + 5 ))
fi
(( target > 100 )) && target=100
step="$target%"
elif [[ $step == "5%-" ]]; then
if (( current <= 5 )); then
(( target = current - 1 ))
else
(( target = current - 5 ))
fi
(( target < 1 )) && target=1
step="$target%"
fi
# Set brightness of the display device.
brightnessctl -d "$device" set "$step" >/dev/null
# Show the new brightness in OSD
(( no_osd )) || omarchy-osd -i brightness -p "$(backlight_brightness "$device")"
fi
+102 -7
View File
@@ -1,12 +1,107 @@
#!/bin/bash
# Adjust the brightness on Apple Studio Displays and Apple XDR Displays using asdcontrol.
# omarchy:summary=Show or adjust Apple Studio Display and Apple XDR Display brightness using asdcontrol.
# omarchy:args=[--no-osd] [+N%|N%-|N%]
# omarchy:examples=omarchy brightness display apple | omarchy brightness display apple +5% | omarchy brightness display apple --no-osd 50%
# Only cache under the user-private runtime dir. With no XDG_RUNTIME_DIR we skip
# caching (detect every run) rather than fall back to a predictable, world-writable
# /tmp path another user could pre-create.
device_cache=""
if [[ -n ${XDG_RUNTIME_DIR:-} ]]; then
device_cache="$XDG_RUNTIME_DIR/omarchy-brightness-display-apple.device"
fi
no_osd=0
if [[ ${1:-} == "--no-osd" ]]; then
no_osd=1
shift
fi
detect_apple_display_device() {
local devices=()
local path=""
for path in /dev/usb/hiddev* /dev/hiddev*; do
[[ -e $path ]] && devices+=("$path")
done
(( ${#devices[@]} > 0 )) || return 1
sudo asdcontrol --detect "${devices[@]}" 2>/dev/null | awk -F: '/^\/dev\/(usb\/)?hiddev/{ print $1; exit }'
}
find_apple_display_device() {
local cached=""
local device=""
if [[ -n $device_cache && -r $device_cache ]]; then
read -r cached <"$device_cache" || true
# Trust a cached value only if it still names a hiddev character device. A
# stale or unexpected cache (a regular file, a non-hiddev node) is ignored and
# we re-detect instead of handing an arbitrary path to asdcontrol. The globs
# are left unquoted on purpose: [[ ]] pattern-matches an unquoted right side,
# and quoting them would turn the match into a literal string comparison.
if [[ ( $cached == /dev/hiddev* || $cached == /dev/usb/hiddev* ) && -c $cached ]]; then
printf '%s\n' "$cached"
return 0
fi
fi
device="$(detect_apple_display_device)" || return 1
[[ -n $device ]] || return 1
if [[ -n $device_cache ]]; then
printf '%s\n' "$device" >"$device_cache"
fi
printf '%s\n' "$device"
}
current_brightness() {
local device="$1"
sudo asdcontrol "$device" 2>/dev/null | awk -F= '
/BRIGHTNESS=/ {
print int($2 * 100 / 60000)
found = 1
}
END { exit !found }
'
}
retry_with_fresh_device() {
rm -f "$device_cache"
device="$(find_apple_display_device || true)"
if [[ -z $device ]]; then
echo "No Apple Display HID device found" >&2
exit 1
fi
}
device="$(find_apple_display_device || true)"
if [[ -z $device ]]; then
echo "No Apple Display HID device found" >&2
exit 1
fi
if (( $# == 0 )); then
echo "Adjust Apple Display Brightness by passing +5000 or -5000 (or any range from 0-60000)"
else
device="$(sudo asdcontrol --detect /dev/usb/hiddev* | grep ^/dev/usb/hiddev | cut -d: -f1)"
sudo asdcontrol "$device" -- "$1" >/dev/null
value="$(sudo asdcontrol "$device" | awk -F= '/BRIGHTNESS=/{print $2+0}')"
omarchy-swayosd-brightness "$(( value * 100 / 60000 ))"
if current_brightness "$device"; then
exit
else
retry_with_fresh_device
current_brightness "$device"
exit
fi
fi
step="$1"
if [[ $step =~ ^([0-9]+)%-$ ]]; then
step="-${BASH_REMATCH[1]}%"
fi
if ! sudo asdcontrol "$device" -- "$step" >/dev/null; then
retry_with_fresh_device
sudo asdcontrol "$device" -- "$step" >/dev/null
fi
(( no_osd )) || omarchy-osd -i brightness -p "$(current_brightness "$device")"
+168
View File
@@ -0,0 +1,168 @@
#!/bin/bash
# omarchy:summary=Show or adjust DDC/CI display brightness for a Hyprland monitor.
# omarchy:args=<monitor> [+N%|N%-|N%]
# omarchy:examples=omarchy-brightness-display-ddc DP-1 | omarchy-brightness-display-ddc DP-1 50%
monitor="${1:-}"
step="${2:-}"
[[ -n $monitor ]] || exit 1
cache_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-brightness-display-ddc"
cache_name="${monitor//[^[:alnum:]_.-]/_}"
cache_file="$cache_dir/$cache_name.bus"
unavailable_cache_seconds=60
range_cache_seconds=10
cache_unavailable() {
mkdir -p "$cache_dir" 2>/dev/null || true
printf 'unavailable %s\n' "$(date +%s)" >"$cache_file" 2>/dev/null || true
}
detect_bus() {
ddcutil --skip-ddc-checks detect --brief 2>/dev/null | awk -v monitor="$monitor" '
/I2C bus:/ {
bus = $NF
sub(/^.*\/i2c-/, "", bus)
}
/DRM connector:/ {
connector = $NF
sub(/^card[0-9]+-/, "", connector)
if (connector == monitor && bus != "") {
print bus
exit
}
bus = ""
}
'
}
find_bus() {
local bus=""
local cached_value=""
local now=0
if [[ -r $cache_file ]]; then
read -r bus cached_value <"$cache_file" || true
fi
if [[ $bus == "unavailable" ]]; then
now=$(date +%s)
if [[ $cached_value =~ ^[0-9]+$ ]] && (( now - cached_value < unavailable_cache_seconds )); then
return 1
fi
bus=""
rm -f "$cache_file"
fi
if [[ -z $bus ]]; then
bus="$(detect_bus)" || return 1
if [[ ! $bus =~ ^[0-9]+$ ]]; then
cache_unavailable
return 1
fi
mkdir -p "$cache_dir" 2>/dev/null || true
printf '%s\n' "$bus" >"$cache_file" 2>/dev/null || true
fi
printf '%s\n' "$bus"
}
read_vcp() {
local bus="$1"
ddcutil --bus "$bus" --skip-ddc-checks getvcp 10 --brief 2>/dev/null | awk '
$1 == "VCP" && toupper($2) == "10" && $3 == "C" && $4 ~ /^[0-9]+$/ && $5 ~ /^[0-9]+$/ && $5 > 0 {
print $4, $5
found = 1
exit
}
END { exit !found }
'
}
read_brightness() {
local bus=""
local now=0
local values=""
bus="$(find_bus)" || return 1
values="$(read_vcp "$bus")" || {
rm -f "$cache_file"
return 1
}
now=$(date +%s)
mkdir -p "$cache_dir" 2>/dev/null || true
printf '%s %s %s\n' "$bus" "${values##* }" "$now" >"$cache_file" 2>/dev/null || true
printf '%s %s\n' "$bus" "$values"
}
bus=""
current=""
maximum=""
percent=""
range_cached_at=""
if [[ -z $step ]]; then
read -r bus current maximum < <(read_brightness) || exit 1
[[ -n ${bus:-} && -n ${current:-} && -n ${maximum:-} ]] || exit 1
(( percent = (current * 100 + maximum / 2) / maximum ))
printf '%s\n' "$percent"
exit 0
fi
if [[ $step =~ ^([0-9]+)%$ ]]; then
target="${BASH_REMATCH[1]}"
range_cache_fresh=0
if [[ -r $cache_file ]]; then
read -r bus maximum range_cached_at <"$cache_file" || true
fi
if [[ $bus =~ ^[0-9]+$ && $maximum =~ ^[0-9]+$ && $range_cached_at =~ ^[0-9]+$ ]] && (( maximum > 0 )); then
now=$(date +%s)
if (( range_cached_at <= now && now - range_cached_at < range_cache_seconds )); then
range_cache_fresh=1
fi
fi
if (( ! range_cache_fresh )); then
read -r bus current maximum < <(read_brightness) || exit 1
fi
elif [[ $step =~ ^\+([0-9]+)%$ ]]; then
read -r bus current maximum < <(read_brightness) || exit 1
[[ -n ${bus:-} && -n ${current:-} && -n ${maximum:-} ]] || exit 1
(( percent = (current * 100 + maximum / 2) / maximum ))
amount="${BASH_REMATCH[1]}"
if (( amount == 5 && percent < 5 )); then
(( target = percent + 1 ))
else
(( target = percent + amount ))
fi
elif [[ $step =~ ^([0-9]+)%-$ ]]; then
read -r bus current maximum < <(read_brightness) || exit 1
[[ -n ${bus:-} && -n ${current:-} && -n ${maximum:-} ]] || exit 1
(( percent = (current * 100 + maximum / 2) / maximum ))
amount="${BASH_REMATCH[1]}"
if (( amount == 5 && percent <= 5 )); then
(( target = percent - 1 ))
else
(( target = percent - amount ))
fi
else
exit 1
fi
(( target < 1 )) && target=1
(( target > 100 )) && target=100
(( raw_target = (target * maximum + 50) / 100 ))
if ! ddcutil --bus "$bus" --skip-ddc-checks --noverify setvcp 10 "$raw_target" >/dev/null 2>&1; then
rm -f "$cache_file"
exit 1
fi
printf '%s\n' "$target"
Loaded 100 of 2166 files, more files were not shown because too many files have changed in this diff. Show more