Commit Graph
1014 Commits
Author SHA1 Message Date
6ddc39520d Clean up the terminal and reconnect when SSH connections drop (#6661)
* Clean up the terminal and reconnect when SSH connections drop

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

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

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

* Detect dead SSH connections within a minute

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

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

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

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

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

* Tolerate the explicit RemoteCommand none when probing ssh -G

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 19:15:21 +02:00
dd61d4a75b Ship herdr alongside tmux (#6406)
* Ship herdr with a config that mirrors our tmux setup

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

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

* Add herdr versions of the tmux dev layout functions

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

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

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

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

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

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

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

* Lay hsl's grid out in visual order

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

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

* Report herdr config reload failures instead of swallowing them

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

* Hide herdr's pane scrollbars to match tmux

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

* Escape queued herdr layout commands

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

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

* Rename the omarchy-herdr package to herdr

---------

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

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

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

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

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

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

---------

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 04:01:02 -07:00
81ecac4a60 Fix WhatsApp Slim migration targeting brave-origin-beta instead of brave-origin (#6482)
* Fix WhatsApp Slim migration targeting brave-origin-beta instead of brave-origin

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

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

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

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

---------

Co-authored-by: akashgagda <akashgagda@users.noreply.github.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 10:26:58 +02:00
b85ae70ebd Stop pipefail from turning grep -q SIGPIPE exits into false negatives (#6614)
* Stop pipefail from turning grep -q SIGPIPE exits into false negatives

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Drop the scan infix from the collector command names

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

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

* Keep the credential store out of the printed usage record

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 15:46:10 +02:00
c3bd4a86ae Fix unbound XKBLAYOUT under set -u in the keyboard-layout migration (#6539)
* Fix unbound XKBLAYOUT under set -u in the keyboard-layout migration

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

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

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

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

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

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

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

* Read the keyboard layout from vconsole.conf alone

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

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

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

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

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

* Drop the test scaffolding from the keyboard-layout migration

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

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

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

---------

Co-authored-by: Toni Nowak <t.nowak@ai-flow.no>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 15:43:45 +02:00
6ee243cc37 Split the end-user omarchy skill into topic guides (#6602)
* Split the end-user omarchy skill into topic guides

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

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

* Add capture and contributing guides to the omarchy skill

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

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

* Scope Hyprland reload guidance to the Lua config files

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 14:36:42 +02:00
David Heinemeier HanssonandGitHub 9cb3640c9f Fix T2 Mac suspend and fan defaults (#6562)
* Fix T2 Mac suspend and fan defaults

* Avoid repeated T2 boot image rebuilds

* Harden T2 migration test matching
2026-08-07 11:15:28 +02:00
David Heinemeier Hansson ce93c31af6 Merge branch 'quattro' into add-default-agent
# Conflicts:
#	migrations/1785633225.sh
2026-08-04 05:33:49 -07:00
David Heinemeier HanssonandClaude Fable 5 c992cdff10 Restart the shell unconditionally after every update
Updates routinely replace the shell's QML, and a stale process can
lazy-load new files into old code. Restarting at the end of every
omarchy update removes the need for migrations to restart the shell
or defer one with the restart-shell-required marker: the login-time
migration path already runs a fresh shell that hot-reloads shell.json.

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

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

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

* Harden external brightness caching

---------

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

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

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

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

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

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

* Address review on the WhatsApp theme bridge

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

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

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

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

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

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

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

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

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

* Harden Chromium theme bridge

* Address Chromium theme bridge review

---------

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

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

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

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

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

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

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

---------

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

* Refine Wi-Fi QR sharing

* Use QR glyph for Wi-Fi sharing

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

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

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

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

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

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

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

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

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

* Share WEP networks correctly and restore the QR quiet zone

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

Move it to a systemd user service:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:19:33 -07:00
David Heinemeier Hansson f22e8cd353 Merge remote-tracking branch 'origin/quattro' into fix-snapshot-locate-and-timeline-leak
# Conflicts:
#	docs/file-layout.md
2026-07-26 19:14:09 -07:00
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 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
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 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 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 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 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 Hansson ec7cd9f039 It's only these two for now 2026-07-24 19:49:13 -07:00