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>
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>
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>
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.
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>
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>
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>
* 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>
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>
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.
* 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>
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>
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>
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
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
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>
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>
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>
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>
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
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>
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>